> _**SDK skill (FULL — basic + all 19 namespaces)** · ~336,695 tokens · hoody-sdk v1.0.0-beta.12_

# SDK mode — drive Hoody from TypeScript/JavaScript

Typed client, 1000+ methods, 19 namespaces. Node, Bun, browser. Built-in retries, error redaction, async iterators, automatic 401 re-auth, cross-origin auth strip. Tradeoff vs HTTP/CLI: needs JS runtime + `npm install`.

## What is Hoody

Hoody is a remote-first computing platform: every workflow — coding, browsing, scheduling, agent runtimes, file storage, GUI desktops, HTTP services, scripts, databases, displays — runs in account-owned cloud containers reachable by URL, with zero local setup. **Thesis: everything remote, no friction.** A container is a **full Linux box (systemd + root, just like a VM — not a Docker-style minimal sandbox)** you can spin up, fill, and use from anywhere; ports are auto-published on `https://...containers.hoody.com`; GUIs render to browser tabs; one-shot scripts mount as HTTP endpoints; databases, terminals, file watchers, full XFCE/MATE desktops, and SSH are first-class kits. Standard distro tooling works as expected — `apt install nginx && systemctl enable --now nginx`, `journalctl`, `crontab`, etc. The CLI / SDK / HTTP surfaces are three skins on the same control + container plane — drive whichever fits your runtime.

**Prefer a GUI?** The **Hoody Agent** browser GUI runs at `https://{P}-{C}-agent-1.{N}.containers.hoody.com` — file browser, code editor, agent sessions, PR review, MCP, memory, image-gen, web search, all in a browser tab. It's the human-facing surface over the same `agent` kit these skills drive programmatically (same `-agent-1` host — the HTTP API lives under its `/api/v1/agent/` path). **Every kit URL is also iframable** (`code`, `files`, `terminal`, `display`, `desktop`, `notes`, `agent`, …) — you can compose a full HTML "operating system" out of kit iframes with no native code. Use whichever surface fits the moment.

**Need a custom API, script-as-service, or multi-step workflow?** Default to `exec`. Drop a `.ts` / `.js` (or shell-out via Bun) into the scripts dir and it auto-mounts as an HTTP endpoint — no framework, no deploy, schema-validated, logged, metric-instrumented, alias-able to a public hostname. Each script is a micro-service, kept warm by the kit; **multi-step workflows** (call agent A → check with agent B → trigger action C) are just one script orchestrating the steps. (Not a sandbox for untrusted code — see `exec` namespace.)

**Need a GET-only URL for something that's actually a POST/PUT?** Use `box.curl.execute(...)` (or compose the GET-bridge URL via the kit URL helper). The `curl` kit can turn any REST call into a single GET-able link for browser-only callers, restricted webhooks, agents with web-fetch-only access. See `curl` namespace.

**Stuck, or unsure how to do something?** Ask Hoody's public docs assistant — an unauthenticated MCP endpoint at `https://chatbot.hoody.com/mcp` (one tool, `search_hoody_docs`; or the `POST /api/chat` SSE fallback) answers any "how do I…" with cited doc URLs. Use it for discovery when you're not sure which namespace fits.

## Install + import

```
npm install hoody-sdk
```

```typescript
import { HoodyClient } from 'hoody-sdk';
```

Browser UMD (exposes `window.HoodySDK`): `https://cdn.jsdelivr.net/npm/hoody-sdk/dist/hoody-sdk.browser.min.js`.

## Init

```typescript
// Token
const hoody = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN! });

// Credentials (eager login)
const hoody = await HoodyClient.authenticate('https://api.hoody.com', { username, password });

// Lazy credentials (login on first call)
const hoody = new HoodyClient({ baseURL, credentials: { username, password } });
```

Recommended `baseURL`: `https://api.hoody.com` (pass it explicitly — `HoodyClient`'s default is empty, so most callers should set it via the constructor option or the `HoodyClient.authenticate(baseURL, …)` shorthand). Realm-scoped: `https://{realmId}.api.hoody.com`. Token scoping → § Auth model. Kit URLs → § Proxy URLs.

## Namespace structure

- **`hoody.api.*`** — account-level (auth, projects, containers, realms, vault, wallet, etc.). There is no `hoody.api.billing` namespace; billing endpoints live under `wallet`/`rentals`/`serverRental`.
- **`box.<ns>.*`** — kit services inside a container (terminal, files, browser, display, exec, sqlite, …). `const box = await hoody.withContainer(container)` — auto-reauthenticates on first 401.

Pattern: `client.<ns>.<svc>.<method>`.

## Iterator + pagination

Most paginated endpoints ship three flavours (cursor-style endpoints like `notes.comments.list` ship `list()` only with `nextCursor`/`hasMore`):

- `list(...)` — one page; pagination shape varies per endpoint. Most container/project list endpoints expose `data.pagination.{total,page,limit,totalPages}`; some kit endpoints expose top-level `meta.page`.
- `listAll(...)` — collect all pages.
- `listIterator(...)` — `AsyncIterableIterator<T>`, fetches on demand, supports early `break`. Default for non-trivial sets.

Namespaced helpers follow the same `*` / `*All` / `*Iterator` triple (e.g. `containers.listByProject*`).

## Errors

SDK throws **`ApiError`** on every 4xx/5xx. Type guards: `isApiError`, `isRetryableApiError`. Also `ValidationError` (client-side input) and `VaultCryptoError`. Retryable codes: `408/425/429/500/502/503/504`. Secrets (`Authorization`, `Cookie`, `?token=…`, body `{password,token,apikey}`, URL userinfo) auto-redacted to `[REDACTED]` before `catch`. See § Reference appendix.

## Streaming

- **SSE / live event endpoints** — generated SSE methods (`box.watch.streams.streamSse`, `box.exec.logs.stream`, `box.proxyLogs.logs.streamLogs`, …) return `Promise<ApiResponse<unknown>>` from a single `http.get`, NOT async iterators. To consume them line-by-line, use the URL with `EventSource`/`fetch`+ReadableStream, or poll the corresponding cursor endpoint (`listEvents` + `since_id` for `watch`, etc.).
- **WebSocket** — `box.notifications.connectStream`, `box.terminal.sessions.connectWebSocket`, and `box.curl.events.streamWs` return a wrapper: `await wrapper.connect()` to open, then per-wrapper typed callbacks — terminal exposes `onOutput` (Uint8Array); notifications exposes `onNotification` / `onHeartbeat`; curl exposes `onJobstarted` / `onJobprogress` / `onJobcompleted`; lifecycle close on every wrapper is `onDisconnect`. NOT Node-style `.on('message')`. (`box.watch.streams.streamWs` returns a plain `Promise<ApiResponse<unknown>>` — not a WebSocket wrapper; drop to raw `WebSocket` for it.) See per-namespace SDK skill.
- **Reverse tunnels** — `tunnelExpose` / `tunnelPull` / `tunnelServe` re-exported from the main `hoody-sdk` package (see `lib/tunnel-client.ts`).

Runnable snippets in § Core operations cheat-sheet below.

## Index

Recipes: § Core operations cheat-sheet. Per-namespace deep dives: `SKILL-SDK/<ns>.md`.

---

# Proxy URLs — capability-based per-container routing

## What it is

`https://{projectId}-{containerId}-{kit_slug}-{n}.{node}.containers.hoody.com`

- Proxy terminates TLS on `*.containers.hoody.com`, parses segments, routes.
- Hoody API: global `https://api.hoody.com`.
- Aliases (`my-api.{N}.containers.hoody.com`) are shortcuts; canonical URL is authoritative.

## Components

| Segment | Meaning |
|---|---|
| `projectId` | 24-char hex. |
| `containerId` | 24-char hex. Bearer credential. |
| `kit_slug` | Kit id (see Kit slug table); some namespaces differ from their slug (e.g. `notifications` → `n-1`, `proxyLogs` → `logs-1`). |
| `n` | 1-based instance index; single-instance kits use `1`. |
| `node` | Bare server hostname (use the `server_name` field from container responses). |
| Suffix | `.containers.hoody.com` (or the configured containers domain derived from the API base URL — Hoody accepts `.com` / `.run` / `.icu` / `.lat` siblings). |

## Every kit URL is iframable — compose a UI out of kits

Each kit URL serves a regular HTML/JS page (or HTTP/WS API) — drop it into an `<iframe>` and you have a working surface in your own app:

```html
<iframe src="https://{P}-{C}-code-1.{N}.containers.hoody.com"      ></iframe>  <!-- full VS Code -->
<iframe src="https://{P}-{C}-files-1.{N}.containers.hoody.com"     ></iframe>  <!-- file browser -->
<iframe src="https://{P}-{C}-terminal-1.{N}.containers.hoody.com"  ></iframe>  <!-- HTML5 terminal -->
<iframe src="https://{P}-{C}-display-1.{N}.containers.hoody.com"   ></iframe>  <!-- X11 desktop -->
<iframe src="https://{P}-{C}-desktop-1.{N}.containers.hoody.com"   ></iframe>  <!-- XFCE/MATE in a tab -->
<iframe src="https://{P}-{C}-notes-1.{N}.containers.hoody.com"     ></iframe>  <!-- notebooks -->
<iframe src="https://{P}-{C}-agent-1.{N}.containers.hoody.com"></iframe>  <!-- Hoody Agent GUI -->
```

You can compose a **full HTML "operating system" out of Hoody kit iframes** — no native code, no installer, just URLs. Hoody itself does this: `os.hoody.com` is essentially a kit-iframe shell.

### Collaborative embeds — work from inside the chat

Most modern collaboration tools accept iframes (or unfurl URLs into rich previews that render iframes), so a Hoody kit URL drops straight into:

| Platform | Embed surface | Outcome |
|---|---|---|
| **Slack** | Canvas embed, custom unfurls, Slack apps | Drop a `terminal-1` URL into a channel canvas → live shell that everyone in the channel can see + drive. Pair with a Cline `?extension=…` `code-1` URL for "agent-in-the-channel" support. |
| **Notion** | `/embed` block (paste URL → Embed) | Wiki page that contains the live editor / agent / file browser as part of the doc. |
| **ClickUp** | Embed view in any list / dashboard | Project board with the relevant repo's `files-1` and `code-1` panes alongside tickets. |
| **Matrix / Element** | `m.html` event, custom widgets | Same as Slack — live terminal / agent in a room. |
| **Microsoft Teams / Zoom** | Apps that accept URL iframes | Shared workbench during calls. |
| **Discord** | Activity URLs, link previews | Drop the URL; viewers click into the live surface. |
| **Confluence / Jira** | "Smart Link" / iframe macro | Runbook page with the live tool baked in. |
| **Plain HTML** | `<iframe src="…">` in any page | Internal portal, status page, customer demo. |

The point: **don't make people leave their chat.** When someone hits a bug, drop the `terminal-N` URL with a Cline / Continue extension already focused into the thread — others can read, type, kibitz, take over, all without context-switching to a new tab. The container's filesystem is shared across every embed (same kit URL = same shell), so collaborators land on the *same* state.

> ⚠ **Sharing a terminal / shell embed = giving root.** A `terminal`, `code`, `desktop`, `display`, or `agent` URL in a Slack channel, Notion page, or any other chat is effectively a root-shell credential. Anyone who can render the iframe can:
> - read every file the container can read (env vars, tokens, vault entries, source code, customer data),
> - run any command (curl exfiltration, `rm -rf`, package installs, network scans, fork-bombs),
> - leverage the container's other kit URLs and SDK accessors,
> - leave persistent footprints (cron jobs, daemons, snapshots, alias creations).
>
> An `<alias>.{N}.containers.hoody.com` does NOT add a security layer — it only hides `containerId`. **Share these URLs only with people you'd trust with `ssh root@…` access.** For broader audiences:
> - Layer **`proxyPermissionsContainer.set{Password,Token,Jwt,Ip}Group`** so a recipient still has to authenticate.
> - Use a **dedicated demo container with no secrets** — wallet credentials, vault data, source code only what they need to see.
> - Set an **`expires_at`** on the alias for auto-expiry.
> - Watch **`proxyLogs`** for unexpected callers; revoke via `proxyAliases.setState(aliasId, { enabled: false })` instantly if leaked.
> - For untrusted reviewers (customers, support tickets, public demos): prefer a **read-only `display`** embed of a screenshot stream over a live terminal, or build a constrained `exec` script that exposes only the operation they need.

### Tips for embedders

- The proxy sets sane cross-origin headers; iframe loading works out of the box for kits that need it (`files`, `code`, `terminal`, `display`, `desktop`, `notes`, `agent`, `browser` viewer surfaces).
- Capability-token gates apply per iframe — gate the kit URL with Password / Token / JWT / IP via `proxyPermissionsContainer.*` and the embedded surface inherits the gate (so a public Slack canvas embed can still require auth).
- Use `proxyAliases.create({ program: '<kit>' })` to ship a brandable hostname (`https://repo-acme.{N}.containers.hoody.com`) into the iframe instead of leaking the `{containerId}`.
- For `display` / `desktop`: clipboard, file-transfer, audio, and notification features are toggleable via query params (`?clipboard=true&sound=true` …) — see the `display` namespace.
- For `code`: append `?extension=<publisher>.<name>` to embed a single extension (e.g. Cline) without the IDE chrome — perfect for chat-channel "agent" widgets.
- API kits (`sqlite`, `cron`, `watch`, `curl`, `pipe`, `http-<port>`, …) don't render a UI but you can still iframe them for status-page widgets, long-poll dashboards, etc.
- `allow="clipboard-read; clipboard-write"` on the `<iframe>` is recommended for `code`, `terminal`, `display` so paste / copy work inside the embed.

## No local bypass — every call goes through Hoody Proxy

There is **no raw localhost-port bypass** to a kit. Even from inside the same container, every call to a kit service goes through the Hoody Proxy on HTTPS — the kit binds to an internal interface that requires the proxy's authenticated, capability-checked, hook-instrumented path. An agent script trying to bypass via `http://127.0.0.1:<kit_port>` will not reach the kit. For in-container self-calls, use the supported proxy-local shorthand `https://localhost.containers.hoody.com/<service-segment>` (resolves to the local kit via the proxy with the same auth path) or the full `{projectId}-{containerId}-…` kit URL.

Why uniform proxy routing:

- **Security uniformity** — the same `proxyPermissionsContainer.*` gates, `proxyHooks.*` MITM rules, and `proxyLogs.*` capture apply to every request, whether it came from across the internet or from a script in the next process. No "trusted internal" loophole that leaks to attackers via SSRF.
- **One mental model** — same URL works from your laptop, from another container, from inside the container itself. You write the same code; the proxy is transparent.
- **Cost is negligible** — when source and kit are co-located on the same physical machine, the proxy hop adds microseconds (Unix socket / loopback under the hood), not a network round-trip.

Practical consequence: from inside a container, when calling its OWN kits, use the same kit URL form as anywhere else (`https://{P}-{C}-<kit>-1.{N}.containers.hoody.com/...`). The `hoody` CLI and the Hoody SDK both already do this. Don't try to discover and target the kit's internal port — it is firewalled and won't accept the connection.

### Container ↔ container — anyone reaches anyone (with permissions)

Because routing is uniform, **a process in container X can call any kit on container Y just by hitting Y's kit URL** — same URL form, same gate stack, same logs. Examples:

- An autonomous agent in container X reads / writes files in container Y via the `files` kit at `https://{P-of-Y}-{C-of-Y}-files-1.{N-of-Y}.containers.hoody.com/api/v1/files/...`.
- A scheduler in X copies a file from Y's `files` kit, runs `exec` in Z, writes the result back to Y's `sqlite` kit — three containers, three kit URLs, one transparent network.
- A monitoring container scrapes `/metrics` from every container in a project's fleet by listing them via `containers.listByProject` and hitting each one's kit URL.

Cross-container access still goes through the gate stack — Y's `proxyPermissionsContainer.*` rules apply to whoever's calling, no matter where they're calling from. So:

- **By default** (no gates set), Y's URL is a capability — anyone with the URL has access. Within your account that's usually fine; for production / shared / multi-tenant fleets you SHOULD gate.
- **With gates set**, X must satisfy them — typically `setTokenGroup` with an auth-token whose `realm_ids` include Y's realm, or a JWT issued for Y's surface. Mint the token in X via `authTokens.create` (with realm-scope), inject it as `Authorization: Bearer …` on the call to Y.

This is why "no local bypass" matters: if same-container calls were a backdoor, an attacker who pwned X could quietly read Y's data with no gate checked. Routing everything through the proxy means **every** container-to-container call sees the **same** auth + audit machinery as every external call.

## Capability-token semantics — open by default, permission for production

**The URL itself is the credential.** A well-formed kit URL routes without any `Authorization` header — anyone who knows the full URL has the same access as the owner. `containerId` is the secret. Container-internal IDs (session, tab, notebook, terminal_id, displayId) follow the same "knowing the ID = having access" model.

**This is intentional for development**: spawn a container, share the URL, collaborator reaches it instantly. **It is NOT acceptable for production exposure** — leaked URL = leaked container. Treat any production deployment as "must have a gate".

### When to gate

- Anything reachable from a public network or shared with a third party.
- Anything that handles secrets, customer data, payments, or user PII.
- Anything where a leaked URL would be hard to rotate (long-lived background jobs, public dashboards, customer-facing demos).

### How to gate

Configure under `proxyPermissionsContainer` (per-container) or `proxyPermissionsProject` (whole project — applies to every container in the project) on the control plane. Multiple gates compose with AND — all must pass.

| Gate | Accessor | Caller behavior |
|---|---|---|
| Password | `setPasswordGroup` | Browser / `curl -u user:pass` — HTTP Basic. |
| Token | `setTokenGroup` | `Authorization: Bearer <token>`. |
| JWT | `setJwtGroup` | Verifies issuer / audience signed JWT. |
| IP | `setIpGroup` | Source IP must match a CIDR. |

Toggle the whole gate stack on/off via `updateState` without dropping the configured groups (handy for break-glass debugging).

Defense in depth: gate the kit URL AND scope any auth-token bearer (realms, IP allowlist) AND keep a short TTL on JWTs. A leaked auth-token is recoverable; a leaked-and-public kit URL is not.

## Kit slug table — every namespace's public URL

Throughout: `{P}` = `projectId` (24-hex), `{C}` = `containerId` (24-hex), `{N}` = `server_name` (e.g. `node-example-1`). All URLs route through `*.containers.hoody.com`.

| Namespace | Kit slug | Public URL (single-instance form) |
|---|---|---|
| `agent` | `agent-{index}` | `https://{P}-{C}-agent-1.{N}.containers.hoody.com` — the in-container AI agent HTTP gateway (`hoody-agent-d`): sessions/prompt, models, skills, memory, todos, workflows, hooks, github, tools, logs |
| `api` | — (control plane) | `https://api.hoody.com` (global, not per-container) |
| `run` | `run-1` | `https://{P}-{C}-run-1.{N}.containers.hoody.com` |
| `browser` | `browser-1` | `https://{P}-{C}-browser-1.{N}.containers.hoody.com` |
| `code` | `code-1` (multi-instance) | `https://{P}-{C}-code-1.{N}.containers.hoody.com` (also `-code-2`, `-code-3`, …) |
| `cron` | `cron-1` | `https://{P}-{C}-cron-1.{N}.containers.hoody.com` |
| `curl` | `curl-1` | `https://{P}-{C}-curl-1.{N}.containers.hoody.com` |
| `daemon` | `daemon-1` | `https://{P}-{C}-daemon-1.{N}.containers.hoody.com` |
| `display` | `display-<N>` (multi) | `https://{P}-{C}-display-1.{N}.containers.hoody.com` (`display-1`, `-2`, …) |
| (no SDK namespace — registered program) | `desktop-<N>` | `https://{P}-{C}-desktop-1.{N}.containers.hoody.com?desktop_env=xfce` — opens a full XFCE/MATE desktop in the browser (see § Desktop alias) |
| `exec` | `exec-1`; script by PATH | `https://{P}-{C}-exec-1.{N}.containers.hoody.com/{script}` (a script under `scripts/{sub}/` is ALSO reachable at the `{sub}.…-exec-1.…` subdomain) |
| `files` | `files-1` | `https://{P}-{C}-files-1.{N}.containers.hoody.com` |
| `notes` | `notes-1` | `https://{P}-{C}-notes-1.{N}.containers.hoody.com` |
| `notifications` | `n-1` (paired w/ `display-N`) | `https://{P}-{C}-n-1.{N}.containers.hoody.com` |
| `pipe` | `pipe-1` | `https://{P}-{C}-pipe-1.{N}.containers.hoody.com` |
| `proxyLogs` | `logs-1` | `https://{P}-{C}-logs-1.{N}.containers.hoody.com` |
| `sqlite` | `sqlite-1` | `https://{P}-{C}-sqlite-1.{N}.containers.hoody.com` |
| `terminal` | `terminal-<id>` (per session) | `https://{P}-{C}-terminal-1.{N}.containers.hoody.com` (`terminal-3` for session 3, etc.) |
| `tunnel` | `tunnel-1` | `https://{P}-{C}-tunnel-1.{N}.containers.hoody.com` |
| `watch` | `watch-1` | `https://{P}-{C}-watch-1.{N}.containers.hoody.com` |
| (any port) | `http-<port>` / `https-<port>` | `https://{P}-{C}-http-8080.{N}.containers.hoody.com` (see § User-hosted services) |
| (none — direct shell) | `ssh` | `ssh root@{P}-{C}-ssh.{N}.containers.hoody.com` (see § SSH access) — port `22`, public-key only |

### Concrete example

For project `65f1...c8a`, container `65f2...41e`, server `node-example-1`:

| Surface | URL |
|---|---|
| Files API | `https://65f1...c8a-65f2...41e-files-1.node-example-1.containers.hoody.com/api/v1/files/workspace/main.py` |
| Exec script `render.ts` (flat) | `https://65f1...c8a-65f2...41e-exec-1.node-example-1.containers.hoody.com/render` (path; a `scripts/render/` dir would also serve at `render.…-exec-1.…`) |
| SQLite kit | `https://65f1...c8a-65f2...41e-sqlite-1.node-example-1.containers.hoody.com/api/v1/sqlite/db/...` |
| Display 1 (X11 / Xpra) | `https://65f1...c8a-65f2...41e-display-1.node-example-1.containers.hoody.com/` |
| **Full XFCE desktop** | `https://65f1...c8a-65f2...41e-desktop-1.node-example-1.containers.hoody.com/` |
| Same, but MATE | `https://65f1...c8a-65f2...41e-desktop-1.node-example-1.containers.hoody.com/?desktop_env=mate` |
| Terminal session 3 | `https://65f1...c8a-65f2...41e-terminal-3.node-example-1.containers.hoody.com/api/v1/terminal/...` |
| Proxy logs | `https://65f1...c8a-65f2...41e-logs-1.node-example-1.containers.hoody.com/` |
| Watch (file-events) | `https://65f1...c8a-65f2...41e-watch-1.node-example-1.containers.hoody.com/watchers/...` |
| Coding agent HTTP API | `https://65f1...c8a-65f2...41e-agent-1.node-example-1.containers.hoody.com/api/v1/agent/...` |
| Hoody Agent GUI (for humans) | `https://65f1...c8a-65f2...41e-agent-1.node-example-1.containers.hoody.com/` |
| User HTTP server on `:8080` | `https://65f1...c8a-65f2...41e-http-8080.node-example-1.containers.hoody.com/` |

### Conventions

- `code` and `display` are multi-instance — append a numeric instance: `-code-1`, `-code-2`, `-display-1`, `-display-7`.
- `terminal` packs the terminal **instance** index into the slug (`terminal-3` = instance 3). That is separate from `?terminal_id=`, which selects the **session** on the kit's own routes (`/execute`, `/paste`, `/press`, `/raw`) and is still required there.
- `display`/`terminal` index pairs by default: a session with `terminal_id=N` runs `DISPLAY=:N`, reachable at the matching `display-N` kit URL. Override by setting `DISPLAY` explicitly in the session env.
- `exec` serves each script at a **path** on the exec host: a file `hello.js` is reachable at `https://{P}-{C}-exec-1.{N}.containers.hoody.com/hello` (extension stripped, lower-cased). A script placed under a subdirectory `scripts/{sub}/` is ALSO reachable at the `{sub}.` **subdomain** (`{sub}.{P}-{C}-exec-1.{N}…`) — the subdomain maps to that directory, NOT to a flat top-level filename.
- `notifications` ↔ `display-{n}`: the notification kit pairs with display N at slug `n-N`.
- `proxyAliases.create` rejects `program: 'web'`; use `program: 'exec'` for `hoody_kit` runners. Full valid program set is enumerated in the §Proxy aliases table below — note `proxy` (not `proxyLogs`) and `run` (not `app`).

## Desktop alias — `desktop-<N>` (full XFCE / MATE desktop in a browser tab)

Open `https://{P}-{C}-desktop-1.{N}.containers.hoody.com` and you land on a complete Linux desktop session — no SDK call, no extra kit, no installation step. There is **no generated REST operation for this surface** (it has no namespace), but SDK helpers (`getKitUrl('desktop', container, N)`) and CLI helpers can compose or open the URL — the URL itself is the whole interface.

### How it works

`desktop-<N>` is a thin alias on top of `terminal` + `display`:

1. The proxy rewrites the request to the `terminal` kit with forced query args `desktop=true`, `redirect=display`, `terminal_id=<offset>+N`, `display=<offset>+N`.
2. The terminal kit spawns the chosen desktop environment under that virtual display.
3. As soon as Xpra is up, the browser is `302`'d to the matching `display-<offset>+N` kit URL.

The terminal index is offset by `1600` so desktop sessions can't collide with regular `terminal-1`/`terminal-2`/… slots. `desktop-1` uses `terminal_id=1601` and lands on `display-1601`; `desktop-7` uses `terminal_id=1607` and `display-1607`. You usually don't see those numbers — the redirect is invisible.

### Choose the desktop environment

Append `?desktop_env=`:

| Value | DE |
|---|---|
| `xfce` (default) | XFCE 4 |
| `mate` | MATE |

```
https://{P}-{C}-desktop-1.{N}.containers.hoody.com?desktop_env=mate
```

Other DEs (GNOME, KDE) are not auto-spawned by the alias — install + run them yourself via `daemon` and reach via the matching `display-<N>` URL directly.

### Forced vs caller-overridable

The proxy locks `desktop=true`, `redirect=display`, `terminal_id`, and `display` to the offset values — passing them in the query string is ignored (defense-in-depth so a caller can't escape the offset isolation). **Only `desktop_env` is honored from the URL.**

### When to use the desktop alias vs `display-<N>` directly

- **Desktop alias** — quick "give me a Linux desktop in a browser tab" surface; no setup needed.
- **`display-<N>` directly** — when a session is already running there (e.g. you launched apps from `terminal-N` so the GUI is on `display-N`), or when you want fine-grained control over the X session.

### SDK helpers

The SDK ships builder methods so you don't compose URLs by hand: `client.getKitUrl(slug, container, idx?)` for one kit, `client.getKitUrls(container)` for the full `{terminal, browser, code, …, desktop, exec, files, …}` record. For desktop with a DE override, use the dedicated helper `client.getDesktopUrl(container, { desktopEnv: 'mate', serviceIndex: 1 })` (default DE is xfce; `getDesktopEnvironments()` lists known values). Or compose by hand with `client.getKitUrl('desktop', container, 1)` and append `?desktop_env=mate`. CLI / HTTP consumers compose the URLs from `containers.get(...)` (`projectId`, `id`, `server_name`) using the patterns above.

## SSH access — direct shell, no proxy

**HTTP via kit URLs is the default and encouraged path** — every request flows through the proxy's logging, request-hooks, and capability-token gate stack, and the URL is reachable from anywhere with no client install. Use SSH only when those guarantees aren't needed and you specifically want a raw shell: heavily-firewalled boxes that should not expose any web surface, native tooling that wants stdin/stdout (`rsync`, `scp`, `git push` over SSH, port-forward `-L`/`-R`), or when running CI inside another network's egress allow-list. Day-to-day: prefer `terminal` (gives you a proxy-logged HTTP-driven PTY, plus `display` for GUIs).

### Hostname

`ssh root@{projectId}-{containerId}-ssh.{node}.containers.hoody.com` (port `22`).

Note the `-ssh.` (no instance number, no kit-suffix). This URL routes to TCP `22` on the container.

### Public-key authentication only

Set `ssh_public_key` (full OpenSSH line, e.g. `ssh-ed25519 AAAA…`) on `containers.create` / `containers.update` / `containers.copy` — that becomes the container's `authorized_keys` (root). Password auth is disabled.

**The public key MUST be unique across containers — one container per key.** Reusing a key returns `409` ("This SSH public key is already in use. SSH public keys must be unique per container."). Generate a fresh keypair per container; you can rotate via `containers.update` with a new `ssh_public_key`.

### What you get — root

SSH login is `root@…` automatically. No sudo prompts, no separate user account; the same shell the kit's `terminal` namespace would give you. Anything inside the container is yours.

### IP filtering

By default the SSH endpoint is reachable from any IP (URL is the credential, plus the keypair check). To restrict source IPs:

- **Control plane** — `firewall.addIngressRule` on TCP `22` with `source` CIDR list. Surface is per-container and reflects in `firewall.list`.
- **In-container** — `iptables` / `nftables` rules baked into the image, runtime-installed by your provisioning, or wired to a `daemon` program. Useful when you want a baseline allow-list independent of Hoody's firewall surface.

Either layer applies to SSH only; it does NOT gate kit URLs (those go through the proxy on a different IP path). For kit URLs, use `proxyPermissionsContainer.setIpGroup` instead.

### When SSH vs kit URLs

| Need | Use |
|---|---|
| Anywhere-reachable HTTP, logged, gated | Kit URL (`terminal`, `files`, `exec`, `display`, `http-<port>` …) |
| `git clone <ssh url>` / `rsync` / `scp` | SSH |
| Port-forward an internal-only service back to your laptop | SSH (`-L 8080:localhost:8080`) |
| Heavily-firewalled box that must not expose any web surface | SSH only; close all kit URLs via gates |
| Drive a TUI from REST | `terminal` namespace (proxy-logged) |
| Drive an X11 GUI from REST | `display` kit + `terminal` for spawn (proxy-logged) |
| Audit-trail of every command run | Kit URL (`terminal`) — proxy logs each request |

## User-hosted services — `http-<port>` / `https-<port>`

**Anything you bind on a container port is automatically reachable from the public URL.** No alias, no firewall edit, no proxy registration. Use one of two slugs:

| Slug form | Inner protocol | Edge URL |
|---|---|---|
| `http-<port>` | proxy speaks **HTTP** to `localhost:<port>` inside the container | `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` |
| `https-<port>` | proxy speaks **HTTPS** to `localhost:<port>` (target must terminate TLS) | `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` |

Edge is always `https://` regardless — TLS terminates at the proxy. The `http-` / `https-` slug only describes what the proxy talks on the inside.

Examples:

```
# Plain HTTP server on :8080 inside the container
https://65f1...c8a-65f2...41e-http-8080.node-example-1.containers.hoody.com

# Service that already terminates TLS on :8443
https://65f1...c8a-65f2...41e-https-8443.node-example-1.containers.hoody.com

# WebSockets just work (use `wss://`)
wss://65f1...c8a-65f2...41e-http-3000.node-example-1.containers.hoody.com/ws
```

Defaults when port omitted: `http` ⇒ port 80, `https` ⇒ port 443. Port range `1..65535`. Capability-token rules still apply — gate the URL via `proxyPermissionsContainer.*` if you don't want it open.

## Friendly aliases — `<alias>.{N}.containers.hoody.com`

A **proxy alias** is a custom hostname that points at one specific program inside a container, without revealing the `projectId` / `containerId`. Same capability-token semantics — alias URL on its own is the credential — but the URL is shareable, brandable, and hides the container plumbing.

### Why use them

- **Hide `containerId`**: shipping `https://my-api.{N}.containers.hoody.com` is fine; shipping `https://65f1...c8a-65f2...41e-http-8080.{node}.containers.hoody.com` leaks the container identifier (which IS the credential of last resort).
- **Brandable**: short, memorable, copy-pasteable.
- **Stable**: alias survives container rebuilds — repoint at a new container, public URL stays the same.
- **Same gate stack**: layer Password / Token / JWT / IP via `proxyPermissionsContainer` exactly as on the canonical URL.
- **No DNS, no TLS work**: the proxy issues the cert and resolves the hostname for you.

### Anatomy

`proxyAliases.create({ container_id, program, alias?, index?, target_path?, allow_path_override?, expires_at?, enabled? })`

| Field | Notes |
|---|---|
| `container_id` | 24-char hex id of the target container — required. |
| `alias` | 3-61 chars, lowercase alphanumeric **plus hyphens** (`a-z0-9-`, no leading/trailing hyphen). Becomes `<alias>.{N}.containers.hoody.com`. Globally unique per server. |
| `program` | Which kit/protocol to route to. Server validates against `container-programs.json`. Valid names: `http`, `https`, `agent`, `browser`, `cdp`, `cli`, `code`, `cron`, `curl`, `daemon`, `desktop`, `display`, `exec`, `files`, `notes`, `notifications`, `pipe`, `proxy`, `run`, `sqlite`, `ssh`, `terminal`, `tunnel`, `watch`, `workspaces` — plus every declared alias of those. Note `proxy` (NOT `proxyLogs`) and `run` (NOT `app`). **`'web'` is rejected — for `hoody_kit` runners use `program: 'exec'`**. |
| `index` | Optional; defaults to `1`. Set explicitly for multi-instance programs: port for `http`/`https`, `terminal_id` for `terminal`, display number for `display`. |
| `target_path` | Optional path appended to inner request (`/api/v1` or `/index.php?debug=1`). |
| `allow_path_override` | Defaults to `true`. If `true`, callers can append path segments after the alias hostname; if `false`, only `target_path` is reachable. |
| `expires_at` | Auto-disable timestamp — ISO 8601 string, Unix seconds, or Unix milliseconds (auto-detected when value > 1e12). Must be in the future. |
| `enabled` | Toggle without deleting (keeps alias slot reserved). |

### Practical examples

Each row below shows the create-call fields and the resulting public URL. Issue the call via your mode's surface (see Reference table for the exact command/endpoint).

| Goal | `program` | `index` | `target_path` | `allow_path_override` | Public URL |
|---|---|---|---|---|---|
| HTTP API on container port 8080 | `http` | `8080` | — | `true` | `https://myapi.{N}.containers.hoody.com/v1/users` |
| Pin a single hoody-exec script (`/render` only) | `exec` | — | `/render` | `false` | `https://tileapi.{N}.containers.hoody.com` |
| Pipe rendezvous as drop-zone hostname | `pipe` | — | `/jobs/pending` | `false` | `https://upload.{N}.containers.hoody.com` |
| Reverse tunnel public URL with auto-expiry | `tunnel` | — | — | — | `https://demo.{N}.containers.hoody.com` (expires `2026-06-01`) |
| GUI display 1 wrapped in a brandable host | `display` | `1` | — | `true` | `https://gui.{N}.containers.hoody.com` |
| Read-only HTTPS upstream (target self-terminates TLS) | `https` | `8443` | — | `true` | `https://secureapi.{N}.containers.hoody.com` |

### Gating an alias

Aliases inherit the container's gate stack — set `proxyPermissionsContainer.setPasswordGroup` (etc.) on the underlying container and the alias URL becomes password-gated too. There is no per-alias-only gate; gating is at the container/project level.

### Operational notes

- `setState({ enabled: false })` disables the alias instantly without releasing the slot — useful to revoke a leaked URL while you investigate.
- Wildcards / multi-program aliases not supported — one alias = one `(program, index)` target.
- Conflicts return `409`: same alias name on the same physical server, uniqueness enforced across all users and slices (not per-user).
- Custom apex domain (e.g. `api.example.com`) requires DNS CNAME + cert provisioning — not part of this surface.

## Common pitfalls

- For kit URL composition use `server_name` (parent physical, always routable). `subserver_name` is the slice display label and is not a routable DNS surface — show it in UI as `subserver_name ?? server_name` but never substitute it into a kit URL.
- **After `containers.create`, kit URLs may return `502`/`503` for a brief window even once `status === 'running'`** — provisioning continues asynchronously after the API flips status (kits attach, networking warms, dev_kit installers finish). Polling `status === 'running'` is necessary but not sufficient. Practical rule: retry the first kit call on transient 5xx with bounded backoff rather than a fixed sleep.
- `http-<port>` reaches the service immediately after the listener is up — no alias needed unless you want a friendly hostname or want to hide the `containerId`.
- **Default = open.** Treat any URL you publish (canonical or alias) as a public secret. Production exposure without a gate = leaked URL = full container access.

---

# Auth model — token taxonomy, capability URLs, and gates

## Three credential types

1. **JWT** — `authentication.login`. TTL `1d`/`7d` (defaults; configurable via `JWT_EXPIRES_IN` / `JWT_REFRESH_EXPIRES_IN`). Sole credential for admin+impersonation.
2. **Auth token** — `authTokens.create`. Prefix `hdy_`. Scopable (realms, `resources.*`), IP-restrictable, rotatable. Long-lived headless credential.
3. **Kit URL** — `https://{projectId}-{containerId}-{kit_slug}-{serviceIndex}.{server}.containers.hoody.com` is the bearer for that kit. See § Proxy URLs.

## Header rule — `Authorization: Bearer <token>`

`Bearer <token>` (one space, case-sensitive) else `401`. JWT vs auth-token by `startsWith(tokenPrefix)`; each rejects the other.

## What each credential can do

- **Admin — JWT only.** `requireAdmin` rejects auth tokens regardless of `is_admin`.
- **Impersonation — JWT + `is_admin` only.** On an auth-token `x-impersonate-user` is **silently ignored** (the whole impersonation block is gated on `!isAuthToken`); the `403` fires only for a non-admin JWT.
- **Auth-token guards.** IP allowlist, expiry, enable/disable.
- **Basic.** `user:pass`, `user:authToken`, `authToken:pass`. If both sides parse as tokens (token:token), the username side is tried first then the password side falls through; no impersonation.
- **Container surfaces — no admin tier**; claim or kit URL = full access.

## Vault — double gate

`/vault/*` auth-token needs BOTH `vault_access===true` AND `hasPermission(token,'resources.vault')` else `403`. JWT owner passes; no cross-user override.

## Login

- `username` OR `email` + `password`.
- Response: `data.token` (not `accessToken`), `refreshToken`, `expires_in`.
- 2FA: returns `requires_2fa`, `temp_token` (5-min); exchange at `POST /api/v1/users/auth/2fa/verify`.
- Email signup → username `<localpart>-<4hex>`.

## Kit URLs as credentials

Bearer by default. Layer gates (AND) via `proxyPermissionsContainer`/`proxyPermissionsProject` `.set{Password,Token,Jwt,Ip}Group`, toggled by `updateState`. See § Proxy URLs.

### Container claim — optional portable credential

Every built-in kit — **including `agent`** — accepts the bare per-container kit URL (the URL is the bearer). There is **no** `X-Hoody-Container-Claim` / `X-Hoody-Token` handshake and no `401 CLAIM_REQUIRED` on the built-in kits; the `agent` kit behaves exactly like the others here.

Separately, `POST /api/v1/containers/:id/authorize` (SDK: `client.api.containers.authorize(id)`; CLI: `hoody containers authorize <cid>`) mints a signed, portable **container claim** — `data: { container_claim: { kid, payload_b64, signature_hex }, expires_in, container_id, project_id }`. It is an *optional* credential for a program **you** run inside a container to verify a caller **offline** against the API's Ed25519 public key (`GET /api/v1/meta/public-key`); a `503 SIGNING_NOT_CONFIGURED` means no signing key is provisioned on that deployment. No built-in kit requires it.

## Realms — workspace isolation

A **realm** is a 24-hex-id namespace inside your account that walls off projects, containers, tokens, and vault entries. Two realms can hold same-named projects without colliding; an auth token scoped to realm A literally cannot see realm B — `404` on every realm-B id. Treat each substantial project as its own realm: an agent (or human) operating with a realm-scoped token can never accidentally drop a container, wipe a vault key, or run a script in someone else's workspace.

### Why realms

- **Blast-radius cap** — leaked or buggy token erases at most one realm's worth of state.
- **Mistake-proof multi-project work** — the token can only see the realm whose subdomain it's invoked from; a `containers.delete <wrong-id>` becomes a `404` instead of a destructive op.
- **Clean separation** — billing, vault, snapshots, and proxy-aliases are all realm-scoped.
- **Cheap to spin up** — realms are free; create one per project rather than reusing.

### Realm-scoped URL

`https://{realmId}.api.hoody.com` — same control-plane API, but every operation that takes an id resolves only against that realm. Off-realm ids return `404` (or `403` for cross-realm-only operations like `containers.copy`/`sync`). Operations targeting a specific realm without the subdomain accept `?realm_id=<id>` instead.

### How to attach a container to a realm

Pass `realm_ids: ['<24-hex>']` when creating the container — and an array of multiple realm IDs is fine if a container needs to be visible in several. Container realm membership is **independent of project realm membership** (a project in realm A can hold a container in realms A+B). Read it back from `containers.get(...).realm_ids`.

### Auth tokens × realms

Mint a realm-scoped token via `authTokens.create({ realm_ids: ['<id>'] })`. The token then routes only against `<realmId>.api.hoody.com` (calling bare `api.hoody.com` returns `403` "requires realm-scoped URL"). Add / drop realms post-mint with `authTokens.addRealm` / `removeRealm`. Globally-scoped tokens (no `realm_ids`) can still target a specific realm by using the realm subdomain or `?realm_id=`.

### Best practice — one realm + one token per project

Realms are **implicit**: there is no `realms.create` endpoint. A realm comes into existence the first time you reference it on a resource. Pick or generate a 24-hex string (e.g. via `crypto.randomBytes(12).toString('hex')` / `openssl rand -hex 12`) and use it everywhere for the project.

1. **Pick a realm id** — any 24-char lowercase hex; or list existing ones with `realms.list`.
2. `authTokens.create` with `realm_ids: [realm_id]` and a sensible `permission_template` (e.g. `external_customer`) — **the token is shown once; copy it before navigating away.**
3. `projects.create` with `realm_ids: [realm_id]` — pin the project to the realm.
4. `containers.create` with `realm_ids: [realm_id]` (plus `hoody_kit: true` for kit URLs) — pin the container too.
5. From now on, drive the project against `https://<realm_id>.api.hoody.com` with that single token. Set the token in `HOODY_TOKEN` and the realm in `HOODY_REALM` (or use the `--realm` flag) so every call goes to the right scope.

Result: that token can only see projects, containers, tokens, and vault entries in the realm. An agent given just this token cannot accidentally touch your other realms — even if it's the same Hoody account.

## Storing auth tokens

The `hdy_…` token from `authTokens.create` is shown ONCE; the server stores only a hash. Three storage options:

- **Write it down outside Hoody** (recommended) — password manager, secrets manager, env file outside the container. The token is a long-lived bearer; treat it like an SSH key.
- **Vault, plaintext** — `vault.set('<key>', { value: 'hdy_…' })`. Encrypted at rest server-side; readable by anyone holding a JWT or vault-scoped auth-token for the account. Convenient for self-hosted automation.
- **Vault, client-side encrypted** — pre-encrypt with your own key (libsodium/`crypto.subtle`/age) before calling `vault.set`. Hoody never sees the plaintext; you store only the wrapping-key elsewhere.

Vault gate: any vault read needs BOTH `vault_access===true` on the token AND `hasPermission(token, 'resources.vault')`. JWT-as-owner bypasses both. See `vault.set/get/list/delete/clear`.

## Token revocation

- `client.api.authentication.logout` — for a JWT this is a **logout-everywhere**: it bumps `session_generation`, revoking every access and refresh token minted before the call. Auth tokens are unaffected.
- `client.api.authentication.refreshToken` — server requires the refresh token in BOTH the request body AND a matching `Authorization: Bearer` header, else `401 Invalid refresh token`. **No client injects it for you** — the generated SDK sends the body only, and the CLI sets `skipAuth` (stripping `Authorization` entirely), so SDK, CLI and raw `fetch`/`curl` callers must all set the header themselves. For headless flows prefer minting a long-lived `authTokens.create`.
- `client.api.authTokens.delete` / disable / IP-restrict — effective next request.

## 2FA

`tfa.setup` returns `{ qr_code, manual_entry_key, backup_codes }`; `tfa.verifySetup` enables. Backup codes rotatable, one-time, hashed. `tfa.setTokenGate` on → sensitive auth-token mutations need TOTP+JWT.

---

# Pre-installed tools — what every container ships with

Every Hoody container starts as a **Debian/Ubuntu base** with a curated battery of dev tools already on `$PATH`. **Containers run real systemd as PID 1 with full root, just like a VM** — not a Docker-style minimal sandbox. That means `systemctl`, `journalctl`, `apt install <package> && systemctl enable --now <unit>`, `crontab -e`, drop-in unit overrides, socket activation, kernel modules, and every other distro affordance Just Work. Two tiers of pre-installed software:

1. **Default tier** — installed on every container, no flag needed.
2. **`dev_kit: true`** — the comprehensive coding setup (Node 24, Bun, Rust, Go, Nix, Docker, …). Pass on `containers.create` (or `--dev-kit` flag); when omitted, `dev_kit` defaults to the resolved `hoody_kit` value.

Anything missing? Just `apt install`, `pip install`, `npm i -g`, `cargo install`, `go install`, `nix-env -i`, etc. — root is yours, the box is yours.

## `kvm: true` — run full VMs inside the container

Containers on **rented / dedicated (bare-metal) servers** can enable `/dev/kvm` passthrough and run hardware-accelerated virtual machines (QEMU/KVM, libvirt, Firecracker, …) inside the container. Pass `kvm: true` on `containers.create` (`--kvm` flag), or toggle it later on a **stopped** container (`containers.setContainerKvm`). Defaults to off. **Never available on free-tier servers** — the API refuses with `403`. `dev_kvm` is accepted as an input alias of `kvm` (`kvm` wins; if both are sent they must agree). Every container response carries the current `kvm` boolean.

## `hoody` CLI is pre-installed inside every container

The `hoody` binary is on every container's `$PATH` for every user (root, `user`, dynamic uids, even from a cron job's environment). You can call it from a shell session, an `exec` script, a `daemon` program, a cron entry, an SSH session — anywhere. No install step, no extra package.

Inside the container, `$HOODY_CONTAINER_ID` is pre-populated by the kit (the CLI also accepts `$HOODY_CONTAINER` as a compatibility alias) so commands targeting "this container" can skip the `--container` flag. `$HOODY_TOKEN` is NOT auto-injected — set it via vault/secrets if container code needs to call the API. Login state is per-user under `~/.hoody/config.json`.

This is the same binary as `hoody` outside the container — every example in the CLI skill works inside a container's shell exactly as it would on a developer laptop.

## Default user — `user` (uid 1000) with passwordless sudo

Containers ship with a **non-root account named `user`** (uid 1000, gid 1000, member of `sudo`). Home is `/home/user`. **`/etc/sudoers.d/user` grants `user ALL=(ALL) NOPASSWD: ALL`** — passwordless `sudo` lets agents (and humans) escalate to root for any operation without prompting. Live-verified.

**Use `user` for everyday work, sudo when you actually need root.** Reasons:

- Files created under `user` are owned by uid 1000 — friendlier when you copy/sync them out of the container or back-stop with rsync.
- Many apps (npm, pip in venvs, Bun, Cargo, Go, Nix single-user, Docker rootless, browsers) write into `$HOME` and behave better when `$HOME` is a real user home, not `/root`.
- `journalctl --user`, `systemctl --user`, dbus user buses all hang off a regular user.

The kit's `terminal` / `daemon` / `cron` namespaces let you pass `user: 'user'` (default in many surfaces is `root` — be explicit). Examples: `daemon.programs.add({ name: 'my-app', command: '…', user: 'user' })`, `terminal.sessions.create({ user: 'user', shell: 'bash', cwd: '/home/user' })`. The generated `client.exec.execution.execute(path, ...)` does NOT take a `user` param — the script runs under whatever uid the kit was started as.

**Production hardening — disable passwordless sudo.** For containers exposed to untrusted callers (open kit URLs without proxy gates, public alias hostnames, agents you don't fully trust), revoke the NOPASSWD line:

```bash
sudo rm /etc/sudoers.d/user                        # remove the drop-in
sudo passwd user                                   # set a real password
```

Or replace the contents with a tighter policy (e.g. `user ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp` for one specific command). Edit via `sudo visudo -f /etc/sudoers.d/user` to validate syntax before commit. Reminder: a leaked kit URL is already a root-shell credential (see auth-model — capability-token semantics); production exposure should ALSO have `proxyPermissionsContainer.*` gates and ideally a non-root default user.

## Default tier — always present

### Network & download
`curl`, `wget`, `net-tools` (`ifconfig`/`netstat`), `dnsutils` (`dig`/`nslookup`), `traceroute`, `socat`, `ncat`, `gpg`, `screen`, `rsync`, `sshpass`.

### Shell & terminal
`bash` (default), `zsh`, `fish`, `tmux`, `nano`, `xterm`, `psmisc`, `coreutils`, `lsof`, `bc`, `tree`, `fuse3`.

### Search / parse / archive
`ripgrep` (`rg`), `jq`, `yq`, `unzip`, `rar`.

### X11 helpers
`xsel`, `xclip`, `wmctrl`, `xdotool` — drive any GUI from the shell, pair with the `display` kit.

### Version control
`git`.

### Database
`sqlite3`.

### Scheduler
`cron`.

### Build toolchain (always)
`build-essential`, `gcc`, `g++`, `make`, `cmake`, `autoconf`, `automake`, `pkg-config`, `bison`, `flex`, `libtool`, `gettext`.

### Python (always — for scripts)
`python3` (system), `python3-dev`, `python3-pip`, `python3-setuptools`, plus dev bindings for cairo / GTK / dbus / cryptography / Pillow / paramiko / netifaces.

### System dev libs (headers, for compiling against)
`libssl-dev`, `libffi-dev`, `libcairo2-dev`, `libgtk-3-dev`, `libglib2.0-dev`, `libpango1.0-dev`, `libncurses-dev`, X11 dev libs (`libx11-dev`, `libxrandr-dev`, `libxext-dev`, `libxtst-dev`, …), OpenGL (`libgl1-mesa-dev`), media codecs (`libjpeg`, `libpng`, `libwebp`, `libavcodec`, `libx264`, `libvpx`, `libaom`), GStreamer.

## `dev_kit: true` tier — comprehensive coding setup

Set the flag on `containers.create` to additionally provision:

### JavaScript / TypeScript
- **Node.js 24** (system-wide via NodeSource — not nvm).
- **Bun** (system-wide).
- **Yarn + pnpm** (via Corepack).
- **npm globals**: `typescript`, `ts-node`, `tsx`, `@types/node`, `eslint`, `prettier`, `@biomejs/biome`, `npm-check-updates`, `nodemon`, `concurrently`.

### Python
- **pipx** (isolated CLI installs).

### Compiled languages
- **Rust** + **cargo** (system rustup).
- **Go** (latest).

### Package managers
- **Nix** (multi-user) — declarative installs without touching apt.
- **pkgx** — runs CLIs without installing them globally.

### Containers
- **Docker Engine** + **buildx** + **compose plugin** + **containerd**.

### CLI utilities
`shellcheck`, `direnv`, `httpie`, `fd-find` (aliased as `fd`), `bat`, `fzf`, `gh` (GitHub CLI).

### AI agent CLIs (installed by default)
- **Claude Code** (`claude`, npm `@anthropic-ai/claude-code`)
- **Codex** (`codex`, npm `@openai/codex`)
- **opencode** (`opencode`, npm `opencode-ai`)
- **Gemini CLI** (`gemini`, npm `@google/gemini-cli`)

Installed without credentials. Push your local credentials/config in afterwards
with the SDK `syncAgentConfig()` helper (see SDK core-ops § "Sync agent config")
— no manual login inside the container needed.

## How to add more

```bash
# Anything Debian-packaged
apt-get install -y <pkg>

# Python (use a venv or pipx for isolated CLIs)
pipx install <tool>

# Node global
npm i -g <pkg>

# Rust (only if dev_kit installed)
cargo install <crate>

# Go (only if dev_kit installed)
go install <module>@latest

# Nix (only if dev_kit installed) — best for one-off binaries that bring 30 deps
nix-env -iA nixpkgs.<pkg>
```

State is per-container: `containers.copy` clones the disk including everything you installed; `snapshots.create` saves a point-in-time you can later `snapshots.restore` to roll back to. There is no global "shared layer" leak — each container's filesystem is its own.

---

# SDK — Core operations cheat-sheet

TS SDK recipes against `https://api.hoody.com`. `hoody` = account `HoodyClient`; `box = await hoody.withContainer(container)` = container-scoped. See § Auth model and § Proxy URLs above.

### Setup

```typescript
import { HoodyClient, tunnelExpose } from 'hoody-sdk';
const hoody = new HoodyClient({ baseURL: 'https://api.hoody.com' });
const box = await hoody.withContainer(container);
```

### 1. Sign up

```typescript
// Password 12-128 chars with at least 3 of 4 classes (upper/lower/digit/symbol).
await hoody.api.authentication.signup({
  email: 'you@example.com',
  password: process.env.HOODY_PASSWORD,
});
```

### 2. Verify email

```typescript
await hoody.api.authentication.verifyEmail({
  token: codeFromEmail, // 64-char token from the verification email link
});
```

### 3. Log in

`username` is alphanumeric with underscores and hyphens (`^[a-zA-Z0-9_-]+$`); use the separate `email` field for email-based login. Login password min length is 8 (signup is 12).

```typescript
const hoody = new HoodyClient({ baseURL: 'https://api.hoody.com' });
const login = await hoody.api.authentication.login({
  email: 'you@example.com',  // or `username: 'alex_3'`
  password: process.env.HOODY_PASSWORD,
});
const token = (login.data as any).token;
hoody.setToken(token);
```

### 4. Log in with 2FA

```typescript
const r = await hoody.api.authentication.login({
  email: 'you@example.com',
  password: process.env.HOODY_PASSWORD,
});
if ((r.data as any)?.requires_2fa) {
  const r2 = await hoody.api.tfa.verify({
    temp_token: (r.data as any).temp_token,
    code: codeFromAuthenticator,
  });
  hoody.setToken((r2.data as any).token); // verify returns the real token; client doesn't auto-persist it
}
```

### 5. Mint a realm-scoped auth token

```typescript
const created = await hoody.api.authTokens.create({
  alias: 'Customer: Acme Corp',
  permission_template: 'external_customer',
  realm_ids: ['507f1f77bcf86cd799439011'],
});
const customerToken = created.data!.token; // shown ONCE
```

### 6. List projects

```typescript
const page = await hoody.api.projects.list();
const all  = await hoody.api.projects.listAll();
for await (const p of hoody.api.projects.listIterator()) { /* ... */ }
```

### 7. Create a project

```typescript
const project = await hoody.api.projects.create({
  alias: 'acme-workspace',
  realm_ids: ['507f1f77bcf86cd799439011'],
});
const projectId = project.data!.id;
```

### 8. List containers

```typescript
await hoody.api.containers.listByProject(projectId);
```

### 9. Create a container

```typescript
const c = await hoody.api.containers.create(projectId, {
  server_id: process.env.HOODY_SERVER_ID!,
  name: 'box-1',
  hoody_kit: true,
  realm_ids: ['507f1f77bcf86cd799439011'],
});
const container = c.data!;
```

### 10. Start / stop / restart

```typescript
await hoody.api.containers.manage(container.id, 'start');
// valid ops: 'start' | 'stop' | 'force-stop' | 'restart' | 'pause' | 'resume'
```

### 11. Get container + build Kit URLs

```typescript
const got = await hoody.api.containers.get(container.id, { runtime: 'true' });
const c = got.data!;

// Single kit URL — pass slug + optional instance index.
const terminalUrl = hoody.getKitUrl('terminal', c);            // → terminal-1
const display3    = hoody.getKitUrl('display', c, 3);          // → display-3
const port8080    = hoody.getKitUrl('http', c, 8080);          // → http-8080

// All standard kit URL patterns at once — Record<slug, url>. This is a convenience
// method; it returns the canonical URL for every standard kit and does NOT
// check whether the program is actually live on this container.
const all = hoody.getKitUrls(c);
// { terminal, browser, code, curl, cron, daemon, display, desktop, exec,
//   files, notifications, sqlite, agent, watch, logs, notes, run, pipe, tunnel,
//   proxy }   ← 20 keys

// Desktop helper — picks the DE via query string. Default xfce; pass `mate`
// for MATE. Returns the same URL the user opens in a browser tab.
const xfce = hoody.getDesktopUrl(c);
const mate = hoody.getDesktopUrl(c, { desktopEnv: 'mate', serviceIndex: 1 });
```

### 12. Snapshot + restore

```typescript
await hoody.api.containers.createSnapshot(container.id, {
  alias: 'pre-deploy',
  expiry: 30, // days
});
await hoody.api.containers.restoreSnapshot(container.id, 'pre-deploy');
```

### 13. One-shot command

```typescript
const r = await box.terminal.execution.execute(
  { command: 'uname -a && uptime' },
  { ephemeral: true },
);
```

### 14. Persistent terminal session

```typescript
const sess = await box.terminal.sessions.create({
  shell: '/bin/bash',
  user: 'user',
  cwd: '/workspace',
});
const terminalId = (sess.data as any).terminal_id;

await box.terminal.execution.execute(
  { command: 'cd repo && git status' },
  { terminal_id: terminalId },
);
```

### 15. Run a hoody-exec script as HTTP

Use CommonJS (`module.exports = handler`) and `validate: false`.

```typescript
const r = await box.exec.execution.execute('/api/build');
```

### 16. Read / write a file

```typescript
const text = await box.files.get('/etc/hostname', { responseType: 'text' });
await box.files.put('/workspace/hello.txt', Buffer.from('hello'));
```

### 17. Browser screenshot

```typescript
const png = await box.browser.interaction.takeScreenshot({
  browser_id: '0',
  url: 'https://example.com',
  fullPage: true,
  format: 'png',
  responseType: 'arrayBuffer',
});
```

### 18. Click + type on a virtual display

```typescript
// data first (x, y, button as 1=left/2=middle/3=right), displayId in options.
await box.display.input.clickAt({ x: 640, y: 360, button: 1 }, { displayId: 1 });
await box.display.input.typeAt({ x: 640, y: 360, text: 'hello world' }, { displayId: 1 });
```

### 19. SQLite KV set / get

```typescript
// kvStore.set takes a STRING value (JSON-encode objects yourself).
// db / table / path / ttl / if_match all live in options, not the body.
await box.sqlite.kvStore.set(
  'user:42',
  JSON.stringify({ name: 'Ada', tier: 'pro' }),
  { db: '/data/app.db', create_db_if_missing: true },
);
const v = await box.sqlite.kvStore.get('user:42', { db: '/data/app.db' });
```

### 20. Watch a directory (SSE)

```typescript
// CreateWatcherRequest takes `paths: string[]` (NOT `path`) and `kinds`
// (NOT `events`). Recursive defaults to server config.
const w = await box.watch.watchers.create({
  paths: ['/workspace'],
  recursive: true,
  kinds: ['created', 'modified', 'removed'],
});
const watcherId = (w.data as any).id;

// streamSse returns Promise<ApiResponse<unknown>>; for browser-like SSE
// consumption use the raw URL with EventSource, or use `listEvents` + cursor.
let lastId: number | undefined;
for (;;) {
  const page = await box.watch.streams.listEvents(watcherId, { since_id: lastId, limit: 200 });
  const items = (page.data as any)?.items ?? [];
  for (const ev of items) lastId = ev.id;
  if (items.length === 0) break;
}
```

### 21. Reverse tunnel (localhost → public URL)

```typescript
const tunnelUrl = hoody.getKitUrl('tunnel', container).replace(/^https:/, 'wss:')
  + '/api/v1/tunnel/connect';
const handle = await tunnelExpose({
  url: tunnelUrl,
  token,                             // your api login token from §3
  containerPort: 80,                 // 0 = auto
  to: { host: '127.0.0.1', port: 3000 },   // LocalTarget = { host, port } — no `kind` field
});
console.log(handle.publicUrl);
await handle.close();
```

### 22. Vault set / get

```typescript
// Top-level `value`, not `{data:{value}}`.
await hoody.api.vault.set('openai_api_key', { value: 'sk-...' });
const v = await hoody.api.vault.get('openai_api_key');
```

### 23. Wallet balance

```typescript
const b = await hoody.api.wallet.getAggregateBalances();
```

### 24. Sync agent config / credentials into a container

Dev-kit containers ship the agent CLIs (`claude`, `codex`, `opencode`, `gemini`)
but no credentials. Push your local config in so they work immediately. Needs a
container-scoped client (`withContainer`). Writes via the files kit (raw bytes),
hardens perms (files `0600`, dirs `0700`, chown to the container user).

```typescript
const box = await hoody.withContainer(container);

// Whole config dir minus history/cache (default scope):
await box.syncAgentConfig('codex');

// Just the auth/credential files:
await box.syncAgentConfig('claude', { only: 'credentials' });

// Several at once; preview without writing:
await box.syncAgentConfigs(['codex', 'claude', 'gemini'], { dryRun: true });

// Override the local source and/or include history:
await box.syncAgentConfig('opencode', { source: '/custom/opencode', includeHistory: true });

// Inspect the registry (which files each tool maps to):
box.listAgentConfigTools();
```

Categories: `credentials`, `config`, `skills` (default), plus `history`/`cache`
(off by default). Narrow with `only` / `categories`, or `include`/`exclude` globs.
Known tools: `codex`, `claude`, `opencode`, `gemini`. Symlinks are skipped and
`..` paths rejected. Returns `{ planned, written, skipped, errors, bytesWritten }`.

---

# SDK — Reference appendix

## `HoodyClientConfig` key options

- `baseURL` (recommended `https://api.hoody.com`; pass it explicitly — `HoodyClient`'s default is empty); `realmId` -> `{realmId}.api.hoody.com`.
- Auth: `token` and/or `credentials{username,password}` (both fields are independent on `HoodyClientConfig`); `autoRefresh`, `autoRetryAuth`; hooks `onTokenExpired`, `refreshToken`, `kitAuth`+`onKitAuthExpired`, `onError`.
- Retry: `timeout`, `retries`, `retryDelayMs`, `retryOnStatuses` (default 408/425/429/500/502/503/504); honours `Retry-After`, cap 30s.
- Misc: `headers`, `cache{enabled,ttl}`, `transport.keepAlive`, `forceIPv4`, `forceIPv4Cache{enabled,ttlMs}`, `middlewares`, `clientId`/`clientName`, `urlTemplates`.
- Per-call: `responseType` (`json|text|arrayBuffer|blob|auto`), `timeoutMs`, `signal`, `rawResponse` (skip envelope; cast `as unknown as RawShape`).

## `ApiError`

`status, code?, url?, method?, request{method,url,body?,query?,headers?}, response{statusCode?,message?,code?,details?}`.

- `0` transport (`cause` set) · `400` validation · `401` SDK calls `onTokenExpired`/`onKitAuthExpired` once then throws · `403` realm/permission · `404` missing in token's realm · `408/425/429/500/502/503/504` retryable.
- `code`: `validation_error`, `unauthorized`, `invalid_token`, `not_found`, `conflict`, `already_exists`, `rate_limited`(+`retryAfterMs`). Match `status` first.

## Pagination

`list()` page · `listAll()` memory-bound · `for await (... of listIterator())` streamed. A few endpoints expose scoped iterators (e.g. `containers.listByProjectIterator`); not every list endpoint has a `listBy*Iterator` helper.

## Streaming

SSE methods (`watch.streams.streamSse`, `exec.logs.stream`, `proxyLogs.logs.streamLogs`, …) return `Promise<ApiResponse<unknown>>`, NOT async iterators — for live consumption use the URL with `EventSource`/`fetch`+ReadableStream, or poll a cursor (`listEvents` + `since_id` for `watch`). WebSocket-wrapper methods (`notifications.connectStream`, `terminal.sessions.connectWebSocket`, `curl.events.streamWs`) expose `await wrapper.connect()` plus per-wrapper typed callbacks: terminal has `onOutput` (Uint8Array); notifications has `onNotification`/`onHeartbeat`; curl has `onJobstarted`/`onJobprogress`/`onJobcompleted`; lifecycle close on every wrapper is `onDisconnect`. (`watch.streams.streamWs` returns plain `Promise<ApiResponse<unknown>>` — not a WebSocket wrapper; drop to raw `WebSocket` for it.) Tunnel `tunnelExpose({ url | container, token, containerPort, to: { host, port }, takeover? })` from `hoody-sdk` (re-exported); `ExposeOptions` accepts either a fully-qualified `url` or a container WebSocket hostname; when `url` is omitted the helper builds `ws://${container}/api/v1/tunnel/connect` () — that default works only for plain-HTTP local dev. For production HTTPS kit URLs you MUST pass `url: 'wss://{P}-{C}-tunnel-1.{N}.containers.hoody.com/api/v1/tunnel/connect'` explicitly.

## Type imports

`import type { HoodyClientConfig, ApiError } from 'hoody-sdk'`. The root entry only re-exports the runtime + a curated set of public types. For response types like `ApiContainersListResponse`, import from a published namespace subpath (e.g. `hoody-sdk/api`) — those ARE in the package's `exports` map. Deep-importing `hoody-sdk/generated/types` is blocked by the exports map; alternatively derive types via `Awaited<ReturnType<typeof client.api.containers.list>>`.

## Quirks

- **Login: raw API + `client.api.authentication.login(...)` accept either `username` or `email`** + `password` on `POST /api/v1/users/auth/login`. The `HoodyClientConfig.credentials` shorthand and `HoodyClient.authenticate(...)` only expose a `username` field — call the generated `login()` directly when you need email-based login.
- **`authentication.refreshToken` needs the header set by YOU** — the server requires the refresh token in BOTH the request body AND a matching `Authorization: Bearer` header, else `401 Invalid refresh token` (`user.controller.ts:1492-1495`). The generated method sends the **body only** and does NOT inject the header (its own docstring tells the caller to send it), so the transport would otherwise carry the expired access token. The auto-refresh helper at `client.ts:686-713` calls `refreshToken` first and falls back to `api.authentication.login(this.credentials)` on 401. SDK, CLI and raw `fetch`/`curl` callers must all add the header manually; for headless flows prefer minting a long-lived `authTokens.create`.

---

## Subskill index

- [`agent`](https://hoody.com/SKILLS/SKILL-SDK/agent.md) — In-container AI coding agent over HTTP
- [`api`](https://hoody.com/SKILLS/SKILL-SDK/api.md) — Platform control plane: identity, projects, containers, billing, vault
- [`browser`](https://hoody.com/SKILLS/SKILL-SDK/browser.md) — Per-container Chromium/Firefox via Playwright/Patchright
- [`code`](https://hoody.com/SKILLS/SKILL-SDK/code.md) — VS Code in the browser, per container
- [`cron`](https://hoody.com/SKILLS/SKILL-SDK/cron.md) — managed crontab entries per system user
- [`curl`](https://hoody.com/SKILLS/SKILL-SDK/curl.md) — full HTTP client gateway + REST-as-GET-URL bridge
- [`daemon`](https://hoody.com/SKILLS/SKILL-SDK/daemon.md) — supervisord program lifecycle (start any program; logs always retained)
- [`display`](https://hoody.com/SKILLS/SKILL-SDK/display.md) — programmatic GUI desktops with screenshots, input, and windows
- [`exec`](https://hoody.com/SKILLS/SKILL-SDK/exec.md) — micro-services: any script or API as an instant HTTP endpoint
- [`files`](https://hoody.com/SKILLS/SKILL-SDK/files.md) — container filesystem over HTTP, with automatic Git-like change history
- [`notes`](https://hoody.com/SKILLS/SKILL-SDK/notes.md) — Collaborative notebooks, hierarchical nodes, documents, databases
- [`notifications`](https://hoody.com/SKILLS/SKILL-SDK/notifications.md) — Trigger and consume desktop notifications inside a container
- [`pipe`](https://hoody.com/SKILLS/SKILL-SDK/pipe.md) — Zero-storage streaming HTTP transfers
- [`proxyLogs`](https://hoody.com/SKILLS/SKILL-SDK/proxyLogs.md) — Per-container request/response/event log query, stats, and SSE tail
- [`run`](https://hoody.com/SKILLS/SKILL-SDK/run.md) — resolve apps to shell commands
- [`sqlite`](https://hoody.com/SKILLS/SKILL-SDK/sqlite.md) — SQLite HTTP API
- [`terminal`](https://hoody.com/SKILLS/SKILL-SDK/terminal.md) — Persistent multiplayer PTY sessions over HTTP and WebSocket
- [`tunnel`](https://hoody.com/SKILLS/SKILL-SDK/tunnel.md) — reverse tunnels for HTTP/WS/TCP via container relay
- [`watch`](https://hoody.com/SKILLS/SKILL-SDK/watch.md) — Linux inotify file-change streams with replay history


---

<!-- ===== namespace: agent ===== -->

# `agent` — In-container AI coding agent over HTTP

## Purpose

The `hoody-agent-d` HTTP gateway exposes the in-container AI agent as a typed namespace: create a chat session, send a prompt, stream the turn (tool calls, gates, output), then confirm/answer/cancel as the agent works. 22 services — `sessions`, `agents`, `models`, `skills`, `memory`, `github`, `workflows`, `tools`, `hooks`, `mcp`, `settings`, `system`, `loops`, `logs`, `tasks`, `statistics`, `jobs`, `discovery`, `headless`, `todos`, plus `hoody` (token bootstrap) and a namespace-root `exportLogs`.

## When to use

- **Drive the agent programmatically** — create a session, prompt it, and consume the turn: `client.agent.sessions.createSession` → stream the turn for live tool/gate/output events (per surface — see the streaming note under Quirks), or `promptSync` for one blocking call → resolve gates with `confirmGate` / `answerQuestion` → `cancelSession` to interrupt.
- **Inspect or configure the agent** — list `models` (`listModels` / `getModel`) and `listProviders` (configure providers via `setProviderDefault` / `setProviderAPIKey` / `startProviderOAuth`); switch a session's active model with `sessions.setSessionModel`, browse/install `skills`, read/edit `memory`, manage `workflows`, `hooks`, `agents` (named agent profiles), and `tools` — both the sessionless catalogue/registry (`tools.listTools` / `listReadOnlyTools` / `getTool`, and `runTool` (blocks, returns the result) / `runToolAsync` (returns `{ job_id }`, poll `jobs`) / `streamTool` (one-shot result over SSE frames — not a per-token stream) to invoke a tool with no session — read-only by default, a mutating tool needs `allow_mutations: true` or a confirmed re-issue) and the per-session surface (`listSessionTools` for a session's *effective* tool set, `listSessionMCPTools`, and `runSessionTool`). Unlike the sessionless `runTool`, a per-session run executes against the session's *frozen* realm/container/cwd/tool-mode and claims the session's single serial turn slot — so it returns 409 `turn_in_flight` while a turn is running, 409 `gate_parked` while a gate is open, and 404 `tool_not_found` if the tool is not in that session's effective list; its mutating-tool posture comes from the live session's gate chain (the `allow_mutations` escape hatch is sessionless-only).
- **One-shot non-interactive runs** — `client.agent.headless.createHeadlessRun` for a fire-and-forget agent invocation that returns a job; poll `jobs.getJob` / `jobs.getJobResult`.
- **GitHub from inside the agent** — first establish an account with `github.githubLogin` (omit the body for a GitHub device flow → poll `github.githubLoginPoll`; or pass a `token` PAT to persist it directly), then `githubAuthStatus` to confirm; once an account is active, `githubClone` / `githubCommit` (and `githubStatus` / `githubBranches` / `githubRepos` / `githubPullRequest` / `githubSync`) for repo operations the agent performs in-container.

## When NOT to use

- Want the interactive TUI, not the API? Run the bare `hoody agent` launcher (`cli/agent-command.ts`) — it opens the in-container Agent TUI over the terminal-kit WebSocket; this namespace is the HTTP control surface beside it.
- Raw shell / command exec → `terminal` (PTY) or `exec` (one-shot). File I/O → `files`. The agent runs these as tools internally; call them directly when you don't need the LLM.
- Account-level resources (containers, billing, realms) → `api`.

## Prerequisites

- Container with the `agent` kit running (`hoody-agent-d` gateway); capability URL.
- At least one model provider configured (`client.agent.models.listProviders` / `listModels` to confirm one is usable) before prompting.
- A session id from `createSession` for every prompt/gate/cancel call.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Prompt a session and stream the turn

`client.agent.sessions.createSession` (returns a session id) → stream the turn with `{ text }` to receive live events (tool calls, gates, output deltas) — see the streaming note under Quirks for the supported per-surface method (SDK helper / HTTP SSE route / CLI command) → resolve any gate as it arrives → turn ends.

### 2. One-shot synchronous prompt

`client.agent.sessions.createSession` → `client.agent.sessions.promptSync` with `{ text }` — blocks until the turn finishes and returns the result in one call. Best for short, non-interactive prompts where you don't need streamed events.

### 3. Resolve gates mid-turn

While a prompt streams, the agent may pause for human input: a confirmation gate → `client.agent.sessions.confirmGate`; an open question → `client.agent.sessions.answerQuestion` (to get a helper model to DRAFT an answer for a parked question call `client.agent.sessions.answerAssist` — it does NOT answer the gate: it dispatches an async job (HTTP 202) whose suggestion arrives via `client.agent.jobs.getJobResult` and an `event.question_suggestion` on the session stream — only one assist may be in flight per session — and the real answer still goes through `answerQuestion`; for unattended runs arm `client.agent.sessions.setSessionAutoReply` (a self-driving auto-user loop that withholds write-class actions unless you opt in with `allow_writes: true`, either on the arm call or later via `setSessionAutoReplyWrites`)). Echoing a wrong/stale `gate_id`/`generation`, or answering when nothing is parked, returns 409 (`no_pending_gate` / `stale_gate` / `gate_already_answered` / `gate_type_mismatch`). Interrupt a running turn with `client.agent.sessions.cancelSession`. Tear the session down: `client.agent.sessions.closeSession` removes it from the live map; `deleteSession` drops the live connection but keeps the persisted record (re-attach via `createSession({ attach })`), while a *hard* delete (set the `hard` flag — SDK `deleteSession(id, { hard: true })`, HTTP `?hard=true`, CLI `--hard`) also erases the persisted record. To roll a session back without tearing it down, `client.agent.sessions.trimSession` with `{ turn_idx }` truncates conversation history to (and including) that turn index.

### 4. Pick a model / provider

`client.agent.models.listProviders` to see configured providers, `client.agent.models.listModels` to list available models, then `client.agent.sessions.setSessionModel` to bind a model to a session before prompting — SYNCHRONOUS: the response reports the actual outcome ({status:'ok', model, persisted} on success; structured 409/422 errors while busy or for an unconstructable spec) and a successful switch PERSISTS into the chat agent's frontmatter (a global repin for future sessions of that agent). PRECEDENCE: the agent's frontmatter `model` is the DEFAULT for a session that does not request one; an explicit `createSession({ model })` (or this live `setSessionModel`) OVERRIDES that pin for the session — create is session-scoped and does not rewrite the agent, this live switch repins globally. The shipped default agent ships pinned, so its pin is the out-of-the-box default until an explicit model is chosen (`createSession({ model })` with attach or backend:"acp" is rejected 400 — a resumed/delegated session cannot take an explicit model). Each session has further per-session knobs (all session-scoped PATCHes that apply live): `setSessionEffort` (`{ effort }` — `low|medium|high|xhigh`, or `""` for the model default), `setSessionVerbosity` (`{ level }` — `normal|concise|terse|minimal`), `setSessionHoodyEnv` (`{ enabled }` — toggle whether the `HOODY_*` shell-env contract is injected for the bash tool), and `setSessionAgent` (`{ agent }` — bind a named profile from `agents`).

### 5. Skills, memory, todos, workflows, agents

- **Skills** — `client.agent.skills.listSkills` (each carries an enabled + trust state), `installSkillHub` / `searchSkillHub` / `previewSkillHub` to find and install from the hub. A newly installed/imported skill must be trusted before its code runs — `client.agent.skills.trustSkill` is the gate (identify the skill by `root_dir`+`rel_dir`, set the `trusted` flag); `toggleSkill` only enables/disables by `name` (set the `disabled` flag). Like the J-class todo ops, both `trustSkill` (which grants arbitrary code-execution trust) and `installSkillHub` (which writes arbitrary skill code to disk) have NO human-confirmation gate over this namespace — that denial lives only on the model-facing skill-edit tool path, and the HTTP edge has no app-level admin gate, so an autonomous API caller can silently trust and install skill code; gate these in your own caller.
- **Memory** — `client.agent.memory.searchMemory` for hybrid recall (BM25 + vector + graph) and `listMemoryItems` to enumerate by `project`; `saveMemoryItem` / `editMemoryItem` / `deleteMemoryItem` to write; `getMemoryGraph` for the relation graph (or `getMemoryItem` to read one record by `id`). `setMemoryEnabled` (`{ enabled }`) is the memory capture/privacy switch — it persists `features.memory` and flips the live store — and `flushMemory` forces the store's durability barrier; both are not admin-gated. Memory is project-scoped (pass `project`); the reads (`searchMemory` / `listMemoryItems` / `getMemoryGraph` / `listMemoryProjects`) and `consolidateMemory` are active-realm-only (a realm header is rejected), while the writes (`saveMemoryItem` / `editMemoryItem` / `deleteMemoryItem`) carry no such restriction. `searchMemory` / `getMemoryGraph` also return `503 store_unavailable` while the store is still warming — retry rather than treating it as an empty result.
- **Todos** — `client.agent.todos.listTodos` / `createTodo` to file; then `triageTodos` (LLM inbox pass), `claimTodo` / `releaseTodo`, `runTodo` (dispatch a background orchestrator — returns `{job_id, session_id}`), `cancelTodoRun` to abort an in-flight run, and `approveTodoProposal` / `denyTodoProposal` to resolve a proposed run — approve is NOT inert: it spawns a background worker session equivalent to `runTodo` (a J-class autonomous run that spends model budget), while `denyTodoProposal` spawns nothing — plus `snoozeTodo` / `archiveTodo`. To advance a todo through its lifecycle (inbox→active→done) or edit its fields, `updateTodo` applies a CAS-guarded patch / `state` transition — read the todo's OWN `revision` with `getTodo` and pass it back (`getTodosRevision` is a store-wide change cursor, NOT the CAS token; a stale value → `409 todo_conflict`); a stale revision is rejected (409). `archiveTodo` is not terminal — `purgeTodos` permanently and irreversibly destroys all archived todos (no confirmation gate; treat as destructive). Mind the comment split: `postTodoComment` (`/messages`, plural) only appends a comment, whereas `messageTodo` (`/message`, singular) ALSO kicks an orchestrator turn — a budget-spending LLM run that returns `{job_id}` — so use the plural form for a plain note. Note `runTodo` / `triageTodos` / `approveTodoProposal` are J-class autonomous runs with NO confirmation gate on the RPC (reaching the RPC is itself treated as the human approval; that denial lives only on the model-facing `run_todo` *tool*), so calling them from automation silently dispatches a real LLM run — gate them in your own caller.
- **Workflows** — `listWorkflows` / `getWorkflow` / `putWorkflow` / `deleteWorkflow` / `hideWorkflow` manage saved definitions; `runSessionWorkflow` dispatches one onto a live session and returns a JOB, not a run (optionally seed the run with a `{ prompt }` body — input text fed to the workflow) — poll `client.agent.jobs.getJob` until its `run_id` populates (null during the brief dispatch window), then track via `listWorkflowRuns` / `getWorkflowRun` and stop with `cancelWorkflowRun`; feed a running workflow with `client.agent.sessions.postWorkflowMessage` (`{ text }`). Run events flow on the owning session's stream, not a per-run bus.
- **Agent profiles** — `client.agent.agents.listAgents` to enumerate named profiles; `createAgent` / `copyAgent` / `renameAgent` / `deleteAgent`; `getAgentSource` → edit → `putAgentSource` (pass `base_gen` from the read for conflict detection); `setAgentModel` / `setAgentTools` / `toggleAgentTool` / `setAgentTurns` / `resetAgentToShipped`. To make a session use a profile, `client.agent.sessions.setSessionAgent` (`{ agent }`) — that selects, it does NOT edit the profile. Two daemon guard rails: `deleteAgent` refuses a shipped-default profile or the configured default chat agent (`is_error:true` — use `resetAgentToShipped` or `putAgentSource` instead), and `resetAgentToShipped` refuses a profile that has no shipped default (`is_error:true`).

### 6. Fire-and-observe, recurring prompts, and re-attach

- **Fire-and-observe** — `client.agent.sessions.postSessionMessage` with `{ text }` dispatches a turn and returns `{ job_id }` immediately (HTTP 202) without streaming or blocking; watch completion via `agent_done` on `client.agent.sessions.streamSession`. Refuses with 409 `turn_in_flight` if a turn is running, or 409 `gate_parked` if a gate is open.
- **Observe / re-attach** — `client.agent.sessions.streamSession` attaches (WebSocket primary, SSE fallback) to a live session's full `event.*` stream; pass `since` (gateway int64 seq, or the `Last-Event-ID` header) to resume from the 1024-event replay ring after a disconnect (a gap past eviction yields `event: lagged {code:replay_gap}`). `client.agent.sessions.replaySession` returns the buffered event tail of a *live* session (with `min_seq`/`max_seq`) for a one-shot catch-up (only a *live* session has this ring; for a non-live session there is nothing to replay — attach via `streamSession` to receive the daemon's `event.replay` instead).
- **Recurring prompts (loops)** — `client.agent.loops.createLoop` with `{ prompt, interval }` (plus optional `max_runs` / `stop_when` / `max_cost_usd` / `max_wall_ms` caps) schedules a prompt to re-fire on a live session; `listLoops` / `updateLoop` (pause via `{ paused: true }`) / `deleteLoop` to manage, `runLoopNow` to fire one immediately. Loops are entirely session-scoped.

### 7. Headless one-shot run

`client.agent.headless.createHeadlessRun` for a non-interactive invocation that returns a job handle; poll `client.agent.jobs.getJob` and fetch the output with `client.agent.jobs.getJobResult`.

### 8. Configure MCP servers for a session

Reads first: `client.agent.mcp.listMCPServers` (`{ session_id }`) returns the EFFECTIVE merged `mcp_servers` config, the per-layer settings files behind it, and each server's LIVE runtime state (connected, negotiated protocol revision, tool count, pid, revocation reason, recent stderr). Every write is TWO steps: `client.agent.mcp.beginMCPWrite` (`{ session_id, op, scope }`, op ∈ `upsert`|`delete`|`set_enabled`|`import`, scope ∈ `user` (default)|`project`|`local`) mints a single-use nonce bound to {session, op, resolved settings path} and returns the target path plus the current `mcp_servers` hash — then present that `nonce` plus the hash as `expect_hash` on the matching `client.agent.mcp.upsertMCPServer` / `deleteMCPServer` / `setMCPServerEnabled` / `importMCPServers`. BOTH are required on every write: skipping step one fails closed, a nonce minted for a different op or scope fails closed, and `expect_hash` has no omit-it default — a first write into a settings file that does not exist yet states that expectation with the empty-array hash rather than leaving the field out. To adopt someone else's config, preview it with `client.agent.mcp.parseMCPImport` (`{ session_id, document }` — writes nothing, needs no nonce, strips credential values) and then `importMCPServers` with a fresh `op: "import"` nonce; imported servers land DISABLED, so enable each one with `setMCPServerEnabled`. After editing a settings file by hand, or to recover a server that died, `client.agent.mcp.reconnectMCP` (`{ session_id }`) re-reads the layers and reconciles every live session's pool — a healthy unchanged server is not restarted. `client.agent.mcp.probeMCPServer` trials a candidate config without saving it, but it is human-only (see Common errors).

## Quirks & gotchas

- The bare `hoody agent` verb is a **hand-written TUI launcher** (`cli/agent-command.ts`), distinct from this generated HTTP namespace; they coexist — the launcher opens the in-container Agent TUI, the namespace is the typed control surface.
- Source of truth is the `hoody-agent-d` gateway's own OpenAPI document, served at `GET /api/v1/agent/openapi.{json,yaml}`; every route lives under the single `/api/v1/agent` prefix. The kit URL is itself the credential; no HTTP bearer header is required.
- The proxy service slug is `agent` and the kit URL host carries the index segment (`-agent-{index}`); resolve it via `getKitUrl('agent', container)` rather than hand-building.
- `promptSync` blocks until the turn finishes (or returns `{pending_gate}` the moment a turn parks on a confirm/question) — long un-parked agent turns can still exceed default HTTP client timeouts; prefer streamed prompting for anything non-trivial so you can observe progress and resolve gates as they arrive. (SDK note: the generated `promptStream` accessor returns a WebSocket client that does NOT match the daemon's SSE wire format — use the supported `streamAgentPrompt` helper exported from the SDK, which POSTs `prompt:stream` and exposes `events` / `text` / `done` plus a cancel() method that aborts the turn.)For non-interactive turns where you cannot resolve gates by hand, enable the `auto_approve` gate policy on `promptStream` / `promptSync` to auto-approve confirm gates for the life of the turn (off by default) — HTTP: `?policy=auto_approve` (or the `X-Hoody-Gate-Policy: auto_approve` header); SDK: `policy: 'auto_approve'` in the prompt options; the generated CLI: `--policy auto_approve`. Note this only answers **confirm** gates, never questions. - Every prompt/gate/cancel call is **session-scoped** — you must hold a session id from `createSession` first; there is no implicit default session. Hook writes are session-scoped too (the guarded writes — `upsertHook` / `deleteHook` / `toggleHook` / `disableAllHooks` — plus `beginHookWrite`, and the side-effecting `testHook` / `ackHookTrust`, all require a live `session_id` — `ackHookTrust` clears the per-session hook-trust prompt (the execution-trust probe `listHooks` reports), the gate that must be acknowledged before any hook command is allowed to fire, mirroring `trustSkill` for skills; `reloadHooks` accepts one only to also return the reloaded summary) AND nonce-guarded: call `client.agent.hooks.beginHookWrite` (`{ session_id, op, scope }`, op ∈ upsert|delete|toggle|set_disabled) to mint a single-use nonce, then pass that `nonce` on the matching `upsertHook` / `deleteHook` / `toggleHook` / `disableAllHooks` — the nonce binds to that session+op+scope tuple and the write fails closed without it. Note hooks are an arbitrary-command surface: `upsertHook` persists a command that fires on lifecycle events and `testHook` executes one immediately. The human-only confirmation gate lives only on the model-facing tool path, not on these RPCs, and the gateway HTTP edge has no app-level admin gate — the same access that authorizes any agent-kit call authorizes these too, with nothing extra, so gating this surface is the caller's/proxy's responsibility.
- **Credential VALUES are never returned by the MCP surface.** `listMCPServers` reports `env_keys` / `header_keys` — key NAMES only — because a redacted value invites a client to write the placeholder back as the real secret; a write whose body carries the redaction placeholder for a credential is REFUSED rather than stored. To change a secret you must supply its real value; to leave one alone, omit the field — `upsertMCPServer` merges FIELD BY FIELD over the existing entry of the same name, so omitted fields keep their stored value (including fields this build does not model), and `setMCPServerEnabled` flips only the `enabled` flag so credentials and options survive a disable. Writes apply to live sessions before the response returns: a deleted, disabled, or re-pointed server is REVOKED in every live session first (a stdio child is reaped when its last holder releases), so a caller mid-turn cannot still reach it. Import is WHOLE-BATCH — one bad entry aborts everything — it understands the hoody (`mcp_servers` list), Claude/Cursor (`mcpServers` map) and VS Code (`servers` map) dialects, and REFUSES a document carrying more than one of them rather than guessing.

## Common errors

- A gate or question left unresolved stalls the turn — a streamed prompt that emitted an `event.confirm_request` (confirm gate) or `event.user_question` (question gate) will not complete until you answer it: `confirmGate` for a confirm, `answerQuestion` for a question. For unattended runs, arm `setSessionAutoReply` (a self-driving auto-user loop), or pass `policy: "auto_approve"` on the prompt — but `auto_approve` only auto-approves **confirm** gates, never questions; a parked question still stalls until `answerQuestion` (or the auto-reply loop) answers it.
- `tasks.listTasks` and `tasks.requestTaskTranscript` do NOT return data inline — they ask a *live* session to emit its background-subagent snapshot/transcript onto the session's WebSocket/SSE stream (`event.tasks_snapshot` for the snapshot, `event.task_transcript` for the transcript) and return only a JSON ack; you must already be attached via `streamSession` to receive the payload. `cancelTask` / `cancelAllTasks` stop background tasks mid-turn (server-layer; tasks survive `cancelSession` but are not restartable).
- `memory.consolidateMemory` (POST /memory/consolidate) is **human-only and ALWAYS fails over this namespace** — the gateway server-stamps a machine marker and the daemon's non-bypassable gate returns `403 human_only` for every HTTP/SDK/CLI call; it can only be triggered from an interactive human session. Do not call it programmatically.
- `mcp.probeMCPServer` (POST /mcp/probe) is **human-only and ALWAYS fails over this namespace** — probing STARTS A PROCESS (stdio) or makes an outbound request to a caller-chosen URL (http/sse), so a machine caller may not self-approve it and receives `403 human_only` on every HTTP/SDK/CLI call. The surface still exposes it for completeness, it simply always refuses. The deny list is still enforced on the candidate config before anything is started. Use `parseMCPImport` for a write-free preview instead; there is no programmatic substitute for the live trial.
- An MCP write needs BOTH a `nonce` and an `expect_hash` — neither is optional, and a stale hash is a CONFLICT rather than a silent overwrite. `upsertMCPServer` / `deleteMCPServer` / `setMCPServerEnabled` / `importMCPServers` each require a fresh single-use `nonce` from `beginMCPWrite` minted for that exact op and scope (one minted for a different op or scope fails closed) AND the `mcp_servers` hash you last read, from either `beginMCPWrite` or `listMCPServers`. A mismatch means someone else edited the layer since you read it — re-read, re-mint, retry; each nonce is good for exactly one write, so a retry always needs a new one. There is no "omit it for the first write" shortcut: writing into a settings file that does not exist yet means passing the empty-array hash.
- `deleteWorkflow` only removes **user** workflows — built-in/**system** workflows are refused (`is_error:true`) and re-seed on every boot; `hideWorkflow` is the only way to remove a system workflow from view.
- Empty values on agent-profile edits mean *inherit / unrestrict*, not *clear to nothing*: `setAgentModel` with `model: ""` removes the model line (falls back to the default model), and `setAgentTools` with `tools: []` removes the allow-list line, which means **all tools are allowed** (NOT zero). Pass a non-empty `tools` array to genuinely restrict.
- Prompting with no usable model/provider configured fails the turn — check `client.agent.models.listProviders` returns a ready provider before you prompt (blocking or streamed).
- Calling prompt/gate/cancel against a session whose live connection was torn down (`closeSession`, or `deleteSession` without `hard`) returns not-found — re-attach with `createSession({ attach: id })` if the record survives, or start fresh with `createSession`. After a *hard* `deleteSession` (the `hard` flag set) the record is gone and only a fresh `createSession` works.

## Related namespaces

`terminal`, `exec`, `files`, `notes`, `api`.

## Reference

**Accessor:** `client.agent`  |  **Import:** `import * as agent from 'hoody-sdk/agent'`

### `client.agent` (1) — General agent operations

#### `exportLogs` — Export logs as a downloadable file.

```typescript
client.agent.exportLogs(source?: string, min_level?: string, comp?: string, session_id?: string, text?: string, since?: string, until?: string, event?: string, tool?: string, model?: string, status?: string, method?: string, min_status?: integer, max_status?: integer, errors_only?: boolean, event_type?: string, resource_type?: string, container?: string, kind?: string, host?: string, since_seq?: integer, limit?: integer, format?: string, filename?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `source` | `string` | query | No | Log source to export (see logsSources; one local source, one platform source, or omitted for all local sources). |
| `min_level` | `string` | query | No | Minimum log level (debug\|info\|warn\|error). |
| `comp` | `string` | query | No | Component filter (daemon source). |
| `session_id` | `string` | query | No | Session id filter. |
| `text` | `string` | query | No | Case-insensitive substring filter over message+attrs. |
| `since` | `string` | query | No | Lower time bound (RFC3339 or relative like 1h/7d). |
| `until` | `string` | query | No | Upper time bound (RFC3339 or relative). |
| `event` | `string` | query | No | Session lifecycle event filter (session source). |
| `tool` | `string` | query | No | Tool name filter (tool source). |
| `model` | `string` | query | No | Model filter (llm source). |
| `status` | `string` | query | No | Tool outcome filter: ok\|error\|cancelled (tool source). |
| `method` | `string` | query | No | HTTP method filter (activity source). |
| `min_status` | `integer` | query | No | Minimum HTTP status (activity source). |
| `max_status` | `integer` | query | No | Maximum HTTP status (activity source). |
| `errors_only` | `boolean` | query | No | Only error rows (activity source; true/false). |
| `event_type` | `string` | query | No | Event type filter (events source). |
| `resource_type` | `string` | query | No | Resource type filter (events source). |
| `container` | `string` | query | No | Container filter (proxy source; empty = all running realm containers). Maps to the daemon's container_id filter. |
| `kind` | `string` | query | No | Proxy row kind: request\|response\|event (proxy source). |
| `host` | `string` | query | No | Proxy URL host filter (exact or dot-aligned suffix). |
| `since_seq` | `integer` | query | No | Exclusive lower seq bound for incremental exports (local sources). |
| `limit` | `integer` | query | No | TOTAL row cap across the export (default: everything the snapshot matches; platform default 2000). |
| `format` | `string` | query | No | Export format: jsonl (default) or txt. |
| `filename` | `string` | query | No | Download filename override (reduced to a safe basename). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/logs/export`

---

### `client.agent.agents` (14) — Chat-agent definitions and per-agent settings

#### `copyAgent` — Copy a chat agent.

```typescript
client.agent.agents.copyAgent(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ new_name*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/agents/{name}/copy`
**CLI:** `hoody agent agents copy`

---

#### `createAgent` — Create a chat-agent definition.

```typescript
client.agent.agents.createAgent(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ name*: string, frontmatter: object, system_prompt: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/agents`
**CLI:** `hoody agent agents create`

---

#### `deleteAgent` — Delete a custom chat agent.

```typescript
client.agent.agents.deleteAgent(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/agents/{name}`
**CLI:** `hoody agent agents delete`

---

#### `getAgentSource` — Read a chat agent's source.

```typescript
client.agent.agents.getAgentSource(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/agents/{name}/source`
**CLI:** `hoody agent agents get-source`

---

#### `listAgents` — List chat-agent definitions.

```typescript
client.agent.agents.listAgents(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/agents`
**CLI:** `hoody agent agents list`

---

#### `listAgentsAll` — List chat-agent definitions. (collect all pages)

```typescript
client.agent.agents.listAgentsAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/agents`
**CLI:** `hoody agent agents list`

---

#### `listAgentsIterator` — List chat-agent definitions. (async iterator)

```typescript
client.agent.agents.listAgentsIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/agents`
**CLI:** `hoody agent agents list`

---

#### `putAgentSource` — Write a chat agent's source.

```typescript
client.agent.agents.putAgentSource(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ content*: string, base_gen: int }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/agents/{name}/source`
**CLI:** `hoody agent agents put-source`

---

#### `renameAgent` — Rename a chat agent.

```typescript
client.agent.agents.renameAgent(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ new_name*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/agents/{name}/rename`
**CLI:** `hoody agent agents rename`

---

#### `resetAgentToShipped` — Reset an agent to its shipped default.

```typescript
client.agent.agents.resetAgentToShipped(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/agents/{name}/reset-to-shipped`
**CLI:** `hoody agent agents reset-to-shipped`

---

#### `setAgentModel` — Set an agent's model.

```typescript
client.agent.agents.setAgentModel(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ model: string }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/agents/{name}/model`
**CLI:** `hoody agent agents set-model`

---

#### `setAgentTools` — Set an agent's tool allow-list.

```typescript
client.agent.agents.setAgentTools(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ tools: any[] }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/agents/{name}/tools`
**CLI:** `hoody agent agents set-tools`

---

#### `setAgentTurns` — Set an agent's max-turns.

```typescript
client.agent.agents.setAgentTurns(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ turns: int }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/agents/{name}/turns`
**CLI:** `hoody agent agents set-turns`

---

#### `toggleAgentTool` — Toggle a single tool for an agent.

```typescript
client.agent.agents.toggleAgentTool(name: string, tool: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `tool` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/agents/{name}/tools/{tool}/toggle`
**CLI:** `hoody agent agents toggle-tool`

---

### `client.agent.discovery` (6) — API discovery and related-operation hints

#### `listContainers` — List containers in a realm (for binding).

```typescript
client.agent.discovery.listContainers(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/containers`
**CLI:** `hoody agent discovery list-containers`

---

#### `listContainersAll` — List containers in a realm (for binding). (collect all pages)

```typescript
client.agent.discovery.listContainersAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/containers`
**CLI:** `hoody agent discovery list-containers`

---

#### `listContainersIterator` — List containers in a realm (for binding). (async iterator)

```typescript
client.agent.discovery.listContainersIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/containers`
**CLI:** `hoody agent discovery list-containers`

---

#### `listRealms` — List realms (for binding).

```typescript
client.agent.discovery.listRealms(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/realms`
**CLI:** `hoody agent discovery list-realms`

---

#### `listRealmsAll` — List realms (for binding). (collect all pages)

```typescript
client.agent.discovery.listRealmsAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/realms`
**CLI:** `hoody agent discovery list-realms`

---

#### `listRealmsIterator` — List realms (for binding). (async iterator)

```typescript
client.agent.discovery.listRealmsIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/realms`
**CLI:** `hoody agent discovery list-realms`

---

### `client.agent.github` (12) — GitHub auth, repos, clone, status, commit, sync, PRs

#### `githubAuthStatus` — GitHub auth status.

```typescript
client.agent.github.githubAuthStatus(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/github/auth/status`
**CLI:** `hoody agent github auth-status`

---

#### `githubBranches` — List GitHub branches.

```typescript
client.agent.github.githubBranches(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/github/branches`
**CLI:** `hoody agent github branches`

---

#### `githubClone` — Clone a GitHub repository.

```typescript
client.agent.github.githubClone(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ repo: string, dir: string, full_name: string, clone_url: string, shallow: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/github/clone`
**CLI:** `hoody agent github clone`

---

#### `githubCommit` — Stage all and commit.

```typescript
client.agent.github.githubCommit(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ message*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/github/commit`
**CLI:** `hoody agent github commit`

---

#### `githubLogin` — Start a GitHub device-flow login (or add a PAT).

```typescript
client.agent.github.githubLogin(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ token: string, host: string, activate: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/github/auth/login`
**CLI:** `hoody agent github login`

---

#### `githubLoginPoll` — Poll a GitHub device-flow login to completion.

```typescript
client.agent.github.githubLoginPoll(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ host: string, device_code*: string, interval: int, expires_in: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/github/auth/login/poll`
**CLI:** `hoody agent github login-poll`

---

#### `githubLogout` — Remove a linked GitHub account.

```typescript
client.agent.github.githubLogout(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ key*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/github/auth/logout`

---

#### `githubPullRequest` — Open a pull request.

```typescript
client.agent.github.githubPullRequest(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ title*: string, body: string, base: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/github/pr`
**CLI:** `hoody agent github pull-request`

---

#### `githubRepos` — List GitHub repos.

```typescript
client.agent.github.githubRepos(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/github/repos`
**CLI:** `hoody agent github repos`

---

#### `githubSetActiveAccount` — Switch the active GitHub account.

```typescript
client.agent.github.githubSetActiveAccount(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ key*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/github/auth/active`

---

#### `githubStatus` — GitHub working-tree status.

```typescript
client.agent.github.githubStatus(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/github/status`
**CLI:** `hoody agent github status`

---

#### `githubSync` — Sync (fetch → pull → push).

```typescript
client.agent.github.githubSync(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ direction: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/github/sync`
**CLI:** `hoody agent github sync`

---

### `client.agent.headless` (1) — One-shot headless agent runs

#### `createHeadlessRun` — Create a headless one-shot run.

```typescript
client.agent.headless.createHeadlessRun(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ prompt*: string, workflow: string, model: string, format: string, stream: bool, timeout_ms: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/headless/runs`
**CLI:** `hoody agent headless create-run`

---

### `client.agent.hoody` (1) — Hoody operations

#### `bootstrapHoodyToken` — Bootstrap the Hoody platform credential (install-if-absent).

```typescript
client.agent.hoody.bootstrapHoodyToken(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ token*: string, capability: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/hoody/auth/bootstrap`

---

### `client.agent.hooks` (9) — Lifecycle hook configuration

#### `ackHookTrust` — Acknowledge hook trust.

```typescript
client.agent.hooks.ackHookTrust(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/hooks/trust/ack`
**CLI:** `hoody agent hooks ack-trust`

---

#### `beginHookWrite` — Begin a hook write (nonce).

```typescript
client.agent.hooks.beginHookWrite(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, op*: string, scope*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/hooks/begin-write`
**CLI:** `hoody agent hooks begin-write`

---

#### `deleteHook` — Delete a hook.

```typescript
client.agent.hooks.deleteHook(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, nonce*: string, scope: string }`

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/hooks`
**CLI:** `hoody agent hooks delete`

---

#### `disableAllHooks` — Disable all hooks.

```typescript
client.agent.hooks.disableAllHooks(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, nonce*: string, scope: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/hooks/disable-all`
**CLI:** `hoody agent hooks disable-all`

---

#### `listHooks` — List hooks.

```typescript
client.agent.hooks.listHooks(session_id?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `session_id` | `string` | query | No | Live session id (hooks are session-scoped; required by the daemon RPC). Query alias of the body session_id (the body value wins). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ session_id: string }`

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/hooks`
**CLI:** `hoody agent hooks list`

---

#### `reloadHooks` — Reload hooks from disk.

```typescript
client.agent.hooks.reloadHooks(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/hooks/reload`
**CLI:** `hoody agent hooks reload`

---

#### `testHook` — Test-fire a hook.

```typescript
client.agent.hooks.testHook(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/hooks/test`
**CLI:** `hoody agent hooks test`

---

#### `toggleHook` — Toggle a hook.

```typescript
client.agent.hooks.toggleHook(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, nonce*: string, scope: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/hooks/toggle`
**CLI:** `hoody agent hooks toggle`

---

#### `upsertHook` — Upsert a hook.

```typescript
client.agent.hooks.upsertHook(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, nonce*: string, scope: string, event: string, matcher: string, command: string, timeout: int, name: string, description: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/hooks`
**CLI:** `hoody agent hooks upsert`

---

### `client.agent.jobs` (3) — Async job lifecycle (observe / result / cancel)

#### `deleteJob` — Cancel a pending/running job, or delete a finished record.

```typescript
client.agent.jobs.deleteJob(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/jobs/{id}`
**CLI:** `hoody agent jobs delete`

---

#### `getJob` — Get an async job's status.

```typescript
client.agent.jobs.getJob(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/jobs/{id}`
**CLI:** `hoody agent jobs get`

---

#### `getJobResult` — Get an async job's result.

```typescript
client.agent.jobs.getJobResult(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/jobs/{id}/result`
**CLI:** `hoody agent jobs get-result`

---

### `client.agent.logs` (5) — Structured log query and read

#### `logsSources` — Log sources.

```typescript
client.agent.logs.logsSources(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/logs/sources`
**CLI:** `hoody agent logs logs-sources`

---

#### `logsStats` — Log statistics.

```typescript
client.agent.logs.logsStats(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/logs/stats`
**CLI:** `hoody agent logs logs-stats`

---

#### `queryLogs` — Query logs.

```typescript
client.agent.logs.queryLogs(source?: string, level?: string, host?: string, since?: string, until?: string, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `source` | `string` | query | No | Filter to a log source/facet (see logsSources). |
| `level` | `string` | query | No | Filter to a minimum log level. |
| `host` | `string` | query | No | Filter to a host. |
| `since` | `string` | query | No | Lower time/cursor bound (since_seq cursor passes through verbatim). |
| `until` | `string` | query | No | Upper time bound. |
| `limit` | `integer` | query | No | Caps the result set (daemon default 200). A non-numeric value is rejected 400. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/logs`
**CLI:** `hoody agent logs query-logs`

---

#### `readLogEntry` — Read a log entry.

```typescript
client.agent.logs.readLogEntry(ref: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `ref` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/logs/entries/{ref}`
**CLI:** `hoody agent logs read-log-entry`

---

#### `streamLogs` — Stream the log tail (SSE).

```typescript
client.agent.logs.streamLogs(source?: string, level?: string, host?: string, since_seq?: integer, limit?: integer, Last-Event-ID?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `source` | `string` | query | No | Filter the tail to a log source/facet. |
| `level` | `string` | query | No | Filter to a minimum log level. |
| `host` | `string` | query | No | Filter to a host. |
| `since_seq` | `integer` | query | No | Initial resume cursor (the Last-Event-ID header overrides it). A non-numeric value is rejected 400. |
| `limit` | `integer` | query | No | Caps each poll batch. A non-numeric value is rejected 400. |
| `Last-Event-ID` | `string` | header | No | SSE resume cursor — the gateway int64 seq to resume from; OVERRIDES the ?since_seq query param. Sent automatically by an SSE client on reconnect. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/logs/stream`
**CLI:** `hoody agent logs stream-logs`

---

### `client.agent.loops` (7) — Recurring self-paced agent loops

#### `createLoop` — Create a loop.

```typescript
client.agent.loops.createLoop(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ prompt*: string, interval: string, max_runs: int, stop_when: string, max_cost_usd: number, max_wall_ms: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/loops`
**CLI:** `hoody agent loops create`

---

#### `deleteLoop` — Delete a loop.

```typescript
client.agent.loops.deleteLoop(id: string, loopId: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `loopId` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/sessions/{id}/loops/{loopId}`
**CLI:** `hoody agent loops delete`

---

#### `listLoops` — List a session's loops.

```typescript
client.agent.loops.listLoops(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/loops`
**CLI:** `hoody agent loops list`

---

#### `listLoopsAll` — List a session's loops. (collect all pages)

```typescript
client.agent.loops.listLoopsAll(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/loops`
**CLI:** `hoody agent loops list`

---

#### `listLoopsIterator` — List a session's loops. (async iterator)

```typescript
client.agent.loops.listLoopsIterator(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/loops`
**CLI:** `hoody agent loops list`

---

#### `runLoopNow` — Run a loop immediately.

```typescript
client.agent.loops.runLoopNow(id: string, loopId: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `loopId` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/loops/{loopId}/run-now`
**CLI:** `hoody agent loops run-now`

---

#### `updateLoop` — Update a loop.

```typescript
client.agent.loops.updateLoop(id: string, loopId: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `loopId` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ paused: bool, expires_in: string, max_cost_usd: number, max_wall_ms: int }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/sessions/{id}/loops/{loopId}`
**CLI:** `hoody agent loops update`

---

### `client.agent.mcp` (9) — Mcp operations

#### `beginMCPWrite` — Begin an MCP config write.

```typescript
client.agent.mcp.beginMCPWrite(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, op*: "upsert" | "delete" | "set_enabled" | "import", scope: "user" | "project" | "local" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/mcp/write-intents`
**CLI:** `hoody agent mcp begin-write`

---

#### `deleteMCPServer` — Delete an MCP server.

```typescript
client.agent.mcp.deleteMCPServer(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, nonce*: string, scope: "user" | "project" | "local", name*: string, expect_hash*: string }`

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/mcp/servers`
**CLI:** `hoody agent mcp delete`

---

#### `importMCPServers` — Import MCP servers from another tool's config.

```typescript
client.agent.mcp.importMCPServers(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, nonce*: string, scope: "user" | "project" | "local", document: string, servers: any[], replace: bool, expect_hash*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/mcp/import`
**CLI:** `hoody agent mcp import`

---

#### `listMCPServers` — List configured MCP servers.

```typescript
client.agent.mcp.listMCPServers(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/mcp/servers`
**CLI:** `hoody agent mcp list`

---

#### `parseMCPImport` — Preview an MCP config import.

```typescript
client.agent.mcp.parseMCPImport(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, document*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/mcp/parse`
**CLI:** `hoody agent mcp parse`

---

#### `probeMCPServer` — Probe an MCP server without saving it.

```typescript
client.agent.mcp.probeMCPServer(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, server*: object }`

**Returns:** `void`  |  **HTTP:** `POST /api/v1/agent/mcp/probe`
**CLI:** `hoody agent mcp probe`

---

#### `reconnectMCP` — Reload MCP config and reconnect.

```typescript
client.agent.mcp.reconnectMCP(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/mcp/reconnect`
**CLI:** `hoody agent mcp reconnect`

---

#### `setMCPServerEnabled` — Enable or disable an MCP server.

```typescript
client.agent.mcp.setMCPServerEnabled(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, nonce*: string, scope: "user" | "project" | "local", name*: string, enabled*: bool, expect_hash*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/mcp/servers/enable`
**CLI:** `hoody agent mcp set-enabled`

---

#### `upsertMCPServer` — Create or update an MCP server.

```typescript
client.agent.mcp.upsertMCPServer(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string, nonce*: string, scope: "user" | "project" | "local", expect_hash*: string, server*: object }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/mcp/servers`
**CLI:** `hoody agent mcp upsert`

---

### `client.agent.memory` (15) — Long-term memory records (admin browse / CRUD)

#### `consolidateMemory` — Trigger a memory consolidation pass (human-only).

```typescript
client.agent.memory.consolidateMemory(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ project*: string, min_observations: int }`

**Returns:** `void`  |  **HTTP:** `POST /api/v1/agent/memory/consolidate`
**CLI:** `hoody agent memory consolidate`

---

#### `deleteMemoryItem` — Delete a memory item.

```typescript
client.agent.memory.deleteMemoryItem(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ id*: string, project: string, kind: string }`

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/memory/items`
**CLI:** `hoody agent memory delete-item`

---

#### `editMemoryItem` — Edit a memory item.

```typescript
client.agent.memory.editMemoryItem(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ project: string, kind: string, content: string }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/memory/items/{id}`
**CLI:** `hoody agent memory edit-item`

---

#### `flushMemory` — Flush the memory store.

```typescript
client.agent.memory.flushMemory(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/memory/flush`
**CLI:** `hoody agent memory flush`

---

#### `getMemoryGraph` — Read a project's memory relation graph.

```typescript
client.agent.memory.getMemoryGraph(project?: string, node_type?: string, limit?: integer, offset?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `project` | `string` | query | No | Project key whose graph to read. |
| `node_type` | `string` | query | No | Optional node-type filter. |
| `limit` | `integer` | query | No | Maximum nodes/edges to return. |
| `offset` | `integer` | query | No | Pagination offset into the graph. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/memory/graph`
**CLI:** `hoody agent memory get-graph`

---

#### `getMemoryItem` — Read a memory item.

```typescript
client.agent.memory.getMemoryItem(id: string, project?: string, kind?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `project` | `string` | query | No | Project key the memory belongs to. |
| `kind` | `string` | query | No | Memory kind/store the record lives in. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/memory/items/{id}`
**CLI:** `hoody agent memory get-item`

---

#### `listMemoryItems` — List memory items.

```typescript
client.agent.memory.listMemoryItems(project?: string, kind?: string, type?: string, query?: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `project` | `string` | query | No | Project key to scope the listing to. |
| `kind` | `string` | query | No | Memory kind/store to filter by. |
| `type` | `string` | query | No | Memory type to filter by (e.g. workflow, fact). kind=memory ONLY — rejected 400 with a lesson/slot/observation kind. |
| `query` | `string` | query | No | Free-text filter over the records. kind=memory ONLY — rejected 400 with a lesson/slot/observation kind. |
| `page` | `integer` | query | No | 1-based page number. |
| `limit` | `integer` | query | No | Items per page (1..200). 0 or omitted pages at the 200 ceiling; a value over 200 is clamped to 200. The effective page size is echoed in meta.limit. NOT "no pagination" — the daemon never returns an unbounded set. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/memory/items`
**CLI:** `hoody agent memory list-items`

---

#### `listMemoryItemsAll` — List memory items. (collect all pages)

```typescript
client.agent.memory.listMemoryItemsAll(project?: string, kind?: string, type?: string, query?: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `project` | `string` | query | No | Project key to scope the listing to. |
| `kind` | `string` | query | No | Memory kind/store to filter by. |
| `type` | `string` | query | No | Memory type to filter by (e.g. workflow, fact). kind=memory ONLY — rejected 400 with a lesson/slot/observation kind. |
| `query` | `string` | query | No | Free-text filter over the records. kind=memory ONLY — rejected 400 with a lesson/slot/observation kind. |
| `page` | `integer` | query | No | 1-based page number. |
| `limit` | `integer` | query | No | Items per page (1..200). 0 or omitted pages at the 200 ceiling; a value over 200 is clamped to 200. The effective page size is echoed in meta.limit. NOT "no pagination" — the daemon never returns an unbounded set. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/memory/items`
**CLI:** `hoody agent memory list-items`

---

#### `listMemoryItemsIterator` — List memory items. (async iterator)

```typescript
client.agent.memory.listMemoryItemsIterator(project?: string, kind?: string, type?: string, query?: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `project` | `string` | query | No | Project key to scope the listing to. |
| `kind` | `string` | query | No | Memory kind/store to filter by. |
| `type` | `string` | query | No | Memory type to filter by (e.g. workflow, fact). kind=memory ONLY — rejected 400 with a lesson/slot/observation kind. |
| `query` | `string` | query | No | Free-text filter over the records. kind=memory ONLY — rejected 400 with a lesson/slot/observation kind. |
| `page` | `integer` | query | No | 1-based page number. |
| `limit` | `integer` | query | No | Items per page (1..200). 0 or omitted pages at the 200 ceiling; a value over 200 is clamped to 200. The effective page size is echoed in meta.limit. NOT "no pagination" — the daemon never returns an unbounded set. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/memory/items`
**CLI:** `hoody agent memory list-items`

---

#### `listMemoryProjects` — List memory projects.

```typescript
client.agent.memory.listMemoryProjects(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/memory/projects`
**CLI:** `hoody agent memory list-projects`

---

#### `listMemoryProjectsAll` — List memory projects. (collect all pages)

```typescript
client.agent.memory.listMemoryProjectsAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/memory/projects`
**CLI:** `hoody agent memory list-projects`

---

#### `listMemoryProjectsIterator` — List memory projects. (async iterator)

```typescript
client.agent.memory.listMemoryProjectsIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/memory/projects`
**CLI:** `hoody agent memory list-projects`

---

#### `saveMemoryItem` — Save a memory item.

```typescript
client.agent.memory.saveMemoryItem(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ project*: string, content*: string, type: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/memory/items`
**CLI:** `hoody agent memory save-item`

---

#### `searchMemory` — Search memory (hybrid recall).

```typescript
client.agent.memory.searchMemory(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ project: string, query: string, limit: int, kinds: any[], skip_graph: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/memory/search`
**CLI:** `hoody agent memory search`

---

#### `setMemoryEnabled` — Toggle memory capture.

```typescript
client.agent.memory.setMemoryEnabled(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ enabled: bool }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/memory/enabled`
**CLI:** `hoody agent memory set-enabled`

---

### `client.agent.models` (22) — Catalogued models and fusion composites

#### `addProviderAccount` — Add an OAuth account to a provider's pool.

```typescript
client.agent.models.addProviderAccount(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/providers/{id}/auth/accounts`
**CLI:** `hoody agent models add-provider-account`

---

#### `deleteProviderAPIKey` — Delete a provider API key.

```typescript
client.agent.models.deleteProviderAPIKey(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/providers/{id}/auth/api-key`
**CLI:** `hoody agent models delete-provider-api-key`

---

#### `getModel` — Get a model by spec.

```typescript
client.agent.models.getModel(spec: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `spec` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/models/{spec}`
**CLI:** `hoody agent models get`

---

#### `getProvider` — Get a provider.

```typescript
client.agent.models.getProvider(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/providers/{id}`
**CLI:** `hoody agent models get-provider`

---

#### `getProviderAuth` — Get a provider's auth status.

```typescript
client.agent.models.getProviderAuth(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/providers/{id}/auth`
**CLI:** `hoody agent models get-provider-auth`

---

#### `listModels` — List models.

```typescript
client.agent.models.listModels(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/models`
**CLI:** `hoody agent models list`

---

#### `listModelsAll` — List models. (collect all pages)

```typescript
client.agent.models.listModelsAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/models`
**CLI:** `hoody agent models list`

---

#### `listModelsIterator` — List models. (async iterator)

```typescript
client.agent.models.listModelsIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/models`
**CLI:** `hoody agent models list`

---

#### `listProviderAccounts` — List a provider's OAuth account pool.

```typescript
client.agent.models.listProviderAccounts(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/providers/{id}/auth/accounts`
**CLI:** `hoody agent models list-provider-accounts`

---

#### `listProviderAccountsAll` — List a provider's OAuth account pool. (collect all pages)

```typescript
client.agent.models.listProviderAccountsAll(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/providers/{id}/auth/accounts`
**CLI:** `hoody agent models list-provider-accounts`

---

#### `listProviderAccountsIterator` — List a provider's OAuth account pool. (async iterator)

```typescript
client.agent.models.listProviderAccountsIterator(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/providers/{id}/auth/accounts`
**CLI:** `hoody agent models list-provider-accounts`

---

#### `listProviders` — List LLM providers.

```typescript
client.agent.models.listProviders(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/providers`
**CLI:** `hoody agent models list-providers`

---

#### `listProvidersAll` — List LLM providers. (collect all pages)

```typescript
client.agent.models.listProvidersAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/providers`
**CLI:** `hoody agent models list-providers`

---

#### `listProvidersIterator` — List LLM providers. (async iterator)

```typescript
client.agent.models.listProvidersIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/providers`
**CLI:** `hoody agent models list-providers`

---

#### `logoutProviderOAuth` — Remove a provider's OAuth login.

```typescript
client.agent.models.logoutProviderOAuth(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/providers/{id}/auth/oauth`
**CLI:** `hoody agent models logout-provider-o-auth`

---

#### `pollProviderOAuth` — Poll a provider OAuth login.

```typescript
client.agent.models.pollProviderOAuth(id: string, job: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `job` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/providers/{id}/auth/oauth/{job}`
**CLI:** `hoody agent models poll-provider-o-auth`

---

#### `removeProviderAccount` — Remove a pooled OAuth account.

```typescript
client.agent.models.removeProviderAccount(id: string, key: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `key` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/providers/{id}/auth/accounts/{key}`
**CLI:** `hoody agent models remove-provider-account`

---

#### `setProviderAPIKey` — Store a provider API key.

```typescript
client.agent.models.setProviderAPIKey(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ api_key*: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/providers/{id}/auth/api-key`
**CLI:** `hoody agent models set-provider-api-key`

---

#### `setProviderAccountActive` — Make a pooled OAuth account active.

```typescript
client.agent.models.setProviderAccountActive(id: string, key: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `key` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/providers/{id}/auth/accounts/{key}/active`
**CLI:** `hoody agent models set-provider-account-active`

---

#### `setProviderDefault` — Set a provider's default credential method.

```typescript
client.agent.models.setProviderDefault(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ default*: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/providers/{id}/auth/default`
**CLI:** `hoody agent models set-provider-default`

---

#### `startProviderOAuth` — Start a provider OAuth login.

```typescript
client.agent.models.startProviderOAuth(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ add_account: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/providers/{id}/auth/oauth`
**CLI:** `hoody agent models start-provider-o-auth`

---

#### `submitProviderOAuthCode` — Submit a provider OAuth authorization code.

```typescript
client.agent.models.submitProviderOAuthCode(id: string, job: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `job` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/providers/{id}/auth/oauth/{job}/code`
**CLI:** `hoody agent models submit-provider-o-auth-code`

---

### `client.agent.sessions` (27) — Create, drive, and tear down agent sessions

#### `answerAssist` — Propose answers for a parked question (helper model).

```typescript
client.agent.sessions.answerAssist(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ mode: string, model: string, gen: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/answer:assist`
**CLI:** `hoody agent sessions answer-assist`

---

#### `answerQuestion` — Answer a parked question gate.

```typescript
client.agent.sessions.answerQuestion(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ gate_id: string, generation: int, answer: string, text: string, answers: object }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/answer`
**CLI:** `hoody agent sessions answer-question`

---

#### `cancelSession` — Cancel the active turn (Esc).

```typescript
client.agent.sessions.cancelSession(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/cancel`
**CLI:** `hoody agent sessions cancel`

---

#### `closeSession` — Close the session (teardown).

```typescript
client.agent.sessions.closeSession(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/close`
**CLI:** `hoody agent sessions close`

---

#### `confirmGate` — Answer a parked confirm gate.

```typescript
client.agent.sessions.confirmGate(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ gate_id: string, generation: int, approved: bool, persist_dirs: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/confirm`
**CLI:** `hoody agent sessions confirm-gate`

---

#### `createSession` — Create, fork, or attach a session.

```typescript
client.agent.sessions.createSession(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ realm: string, container: string, cwd: string, config_dir: string, model: string, agent: string, tool_mode: string, dir_scope: string, attach: string, fork: string, fork_turn_idx: int, backend: string, delegated_agent: string, headless: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions`
**CLI:** `hoody agent sessions create`

---

#### `deleteSession` — Close (and optionally hard-delete) a session.

```typescript
client.agent.sessions.deleteSession(id: string, hard?: boolean, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `hard` | `boolean` | query | No | When true, also remove the persisted session record (not just the live connection). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/sessions/{id}`
**CLI:** `hoody agent sessions delete`

---

#### `getSession` — Get a session summary.

```typescript
client.agent.sessions.getSession(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}`
**CLI:** `hoody agent sessions get`

---

#### `getSessionTranscript` — Read a session's transcript without attaching.

```typescript
client.agent.sessions.getSessionTranscript(id: string, after_turn?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `after_turn` | `integer` | query | No | Exclusive completed-turn skip cursor: return content strictly after completed turn N (0 = full transcript; values past the end clamp; negative/non-integer = 400). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/transcript`

---

#### `listSessionCwds` — List distinct session working directories.

```typescript
client.agent.sessions.listSessionCwds(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/cwds`
**CLI:** `hoody agent sessions list-cwds`

---

#### `listSessions` — List sessions.

```typescript
client.agent.sessions.listSessions(include_system?: boolean, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `include_system` | `boolean` | query | No | When true, also include daemon-owned system/resident sessions in the listing. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions`
**CLI:** `hoody agent sessions list`

---

#### `listSessionsAll` — List sessions. (collect all pages)

```typescript
client.agent.sessions.listSessionsAll(include_system?: boolean, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `include_system` | `boolean` | query | No | When true, also include daemon-owned system/resident sessions in the listing. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/sessions`
**CLI:** `hoody agent sessions list`

---

#### `listSessionsIterator` — List sessions. (async iterator)

```typescript
client.agent.sessions.listSessionsIterator(include_system?: boolean, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `include_system` | `boolean` | query | No | When true, also include daemon-owned system/resident sessions in the listing. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/sessions`
**CLI:** `hoody agent sessions list`

---

#### `postSessionMessage` — Dispatch a turn (fire-and-observe).

```typescript
client.agent.sessions.postSessionMessage(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ text: string, tool_mode: string, dir_scope: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/messages`
**CLI:** `hoody agent sessions post-message`

---

#### `postWorkflowMessage` — Send a message to a running workflow.

```typescript
client.agent.sessions.postWorkflowMessage(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ text: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/workflow/messages`
**CLI:** `hoody agent sessions post-workflow-message`

---

#### `promptStream` — Dispatch a turn and stream the response.

```typescript
client.agent.sessions.promptStream(id: string, policy?: string, X-Hoody-Gate-Policy?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `policy` | `string` | query | No | auto_approve auto-answers confirm gates for the life of the stream (alias of the X-Hoody-Gate-Policy header); off by default. |
| `X-Hoody-Gate-Policy` | `string` | header | No | Confirm-gate posture: "auto_approve" adopts the headless auto-answer posture (off by default; the in:query alias is ?policy=). Any other value is rejected 400. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ text: string, tool_mode: string, dir_scope: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/prompt:stream`
**CLI:** `hoody agent sessions prompt-stream`

---

#### `promptSync` — Dispatch a turn and block to completion.

```typescript
client.agent.sessions.promptSync(id: string, policy?: string, X-Hoody-Gate-Policy?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `policy` | `string` | query | No | auto_approve adopts the headless auto-answer posture (alias of the X-Hoody-Gate-Policy header); off by default. |
| `X-Hoody-Gate-Policy` | `string` | header | No | Confirm-gate posture: "auto_approve" adopts the headless auto-answer posture (off by default; the in:query alias is ?policy=). Any other value is rejected 400. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ text: string, tool_mode: string, dir_scope: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/prompt:sync`
**CLI:** `hoody agent sessions prompt-sync`

---

#### `replaySession` — Replay a live session's buffered events.

```typescript
client.agent.sessions.replaySession(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/replay`
**CLI:** `hoody agent sessions replay`

---

#### `setSessionAgent` — Switch the chat agent.

```typescript
client.agent.sessions.setSessionAgent(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ agent: string }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/sessions/{id}/agent`
**CLI:** `hoody agent sessions set-chat-agent`

---

#### `setSessionAutoReply` — Arm/disarm the auto-reply loop.

```typescript
client.agent.sessions.setSessionAutoReply(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ armed: bool, rounds: int, model: string, allow_writes: bool }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/sessions/{id}/auto-reply`
**CLI:** `hoody agent sessions set-auto-reply`

---

#### `setSessionAutoReplyWrites` — Flip the auto-reply write opt-in.

```typescript
client.agent.sessions.setSessionAutoReplyWrites(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ allow_writes: bool }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/sessions/{id}/auto-reply/writes`
**CLI:** `hoody agent sessions set-auto-reply-writes`

---

#### `setSessionEffort` — Set reasoning effort.

```typescript
client.agent.sessions.setSessionEffort(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ effort: string }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/sessions/{id}/effort`
**CLI:** `hoody agent sessions set-effort`

---

#### `setSessionHoodyEnv` — Toggle Hoody shell-env injection.

```typescript
client.agent.sessions.setSessionHoodyEnv(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ enabled: bool }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/sessions/{id}/hoody-env`
**CLI:** `hoody agent sessions set-hoody-env`

---

#### `setSessionModel` — Switch the session model.

```typescript
client.agent.sessions.setSessionModel(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ model*: string }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/sessions/{id}/model`
**CLI:** `hoody agent sessions set-model`

---

#### `setSessionVerbosity` — Set response verbosity.

```typescript
client.agent.sessions.setSessionVerbosity(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ level: string }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/sessions/{id}/verbosity`
**CLI:** `hoody agent sessions set-verbosity`

---

#### `streamSession` — Attach to a session's event stream (WebSocket / SSE).

```typescript
client.agent.sessions.streamSession(id: string, since?: integer, Last-Event-ID?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `since` | `integer` | query | No | Resume from this gateway int64 seq (also accepted as the Last-Event-ID header). |
| `Last-Event-ID` | `string` | header | No | SSE resume cursor — the gateway int64 seq to resume from (the in:header alias of ?since); sent automatically by an SSE client on reconnect. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/stream`
**CLI:** `hoody agent sessions stream`

---

#### `trimSession` — Trim session history to a turn index.

```typescript
client.agent.sessions.trimSession(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ turn_idx: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/trim`
**CLI:** `hoody agent sessions trim`

---

### `client.agent.settings` (11) — Process-wide settings (home settings.json)

#### `deleteFusion` — Delete a fusion composite.

```typescript
client.agent.settings.deleteFusion(slug: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `slug` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/settings/fusion/{slug}`
**CLI:** `hoody agent settings delete-fusion`

---

#### `getACPStatus` — Get BYOA ACP backend status.

```typescript
client.agent.settings.getACPStatus(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/acp/agents`
**CLI:** `hoody agent settings get-acp-status`

---

#### `getSettings` — Get settings.

```typescript
client.agent.settings.getSettings(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/settings`
**CLI:** `hoody agent settings get`

---

#### `listFusion` — List fusion composites.

```typescript
client.agent.settings.listFusion(include_invalid?: boolean, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `include_invalid` | `boolean` | query | No | When true, also return composites that failed validation as a top-level `invalid` array beside `items` (each with a reason + raw-file index) so a broken composite is editable/deletable. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/settings/fusion`
**CLI:** `hoody agent settings list-fusion`

---

#### `listFusionAll` — List fusion composites. (collect all pages)

```typescript
client.agent.settings.listFusionAll(include_invalid?: boolean, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `include_invalid` | `boolean` | query | No | When true, also return composites that failed validation as a top-level `invalid` array beside `items` (each with a reason + raw-file index) so a broken composite is editable/deletable. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/settings/fusion`
**CLI:** `hoody agent settings list-fusion`

---

#### `listFusionIterator` — List fusion composites. (async iterator)

```typescript
client.agent.settings.listFusionIterator(include_invalid?: boolean, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `include_invalid` | `boolean` | query | No | When true, also return composites that failed validation as a top-level `invalid` array beside `items` (each with a reason + raw-file index) so a broken composite is editable/deletable. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/settings/fusion`
**CLI:** `hoody agent settings list-fusion`

---

#### `patchSettings` — Patch settings.

```typescript
client.agent.settings.patchSettings(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ patch*: object }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/settings`
**CLI:** `hoody agent settings patch`

---

#### `setACPAgentModel` — Set a BYOA backend's default model and effort.

```typescript
client.agent.settings.setACPAgentModel(agent: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `agent` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ model: string, effort: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/acp/agents/{agent}/model`

---

#### `setACPEnabled` — Enable or disable a BYOA ACP backend.

```typescript
client.agent.settings.setACPEnabled(agent: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `agent` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ enabled: bool }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/acp/agents/{agent}/enabled`

---

#### `setACPSecret` — Store an ACP per-agent secret value.

```typescript
client.agent.settings.setACPSecret(agent: string, key: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `agent` | `string` | path | Yes | Path identifier. |
| `key` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ value: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/acp/agents/{agent}/secrets/{key}`
**CLI:** `hoody agent settings set-acp-secret`

---

#### `upsertFusion` — Create or update a fusion composite.

```typescript
client.agent.settings.upsertFusion(slug: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `slug` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ spec*: object }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/settings/fusion/{slug}`
**CLI:** `hoody agent settings upsert-fusion`

---

### `client.agent.skills` (17) — Reusable agent skill definitions

#### `applySkillImport` — Apply a skill import.

```typescript
client.agent.skills.applySkillImport(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/skills/import/apply`
**CLI:** `hoody agent skills apply-import`

---

#### `clearSkillHubCache` — Clear the skill hub cache.

```typescript
client.agent.skills.clearSkillHubCache(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/skills/hub/cache`
**CLI:** `hoody agent skills clear-hub-cache`

---

#### `createSkill` — Create a skill.

```typescript
client.agent.skills.createSkill(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ name*: string, description: string, content: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/skills`
**CLI:** `hoody agent skills create`

---

#### `deleteSkill` — Delete a skill.

```typescript
client.agent.skills.deleteSkill(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ root_dir*: string, rel_dir*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/skills/delete`
**CLI:** `hoody agent skills delete`

---

#### `getSkillHubCache` — Skill hub cache stats.

```typescript
client.agent.skills.getSkillHubCache(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/skills/hub/cache`
**CLI:** `hoody agent skills get-hub-cache`

---

#### `getSkillSource` — Read a skill's source.

```typescript
client.agent.skills.getSkillSource(root_dir?: string, rel_dir?: string, root?: string, rel?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `root_dir` | `string` | query | No | Skill root directory (identity; alias: root). |
| `rel_dir` | `string` | query | No | Skill relative directory (identity; alias: rel). |
| `root` | `string` | query | No | Friendly alias of root_dir (translated to root_dir server-side). |
| `rel` | `string` | query | No | Friendly alias of rel_dir (translated to rel_dir server-side). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/skills/source`
**CLI:** `hoody agent skills get-source`

---

#### `installSkillHub` — Install a hub skill.

```typescript
client.agent.skills.installSkillHub(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ id*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/skills/hub/install`
**CLI:** `hoody agent skills install-hub`

---

#### `listSkills` — List skills.

```typescript
client.agent.skills.listSkills(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/skills`
**CLI:** `hoody agent skills list`

---

#### `listSkillsAll` — List skills. (collect all pages)

```typescript
client.agent.skills.listSkillsAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/skills`
**CLI:** `hoody agent skills list`

---

#### `listSkillsIterator` — List skills. (async iterator)

```typescript
client.agent.skills.listSkillsIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/skills`
**CLI:** `hoody agent skills list`

---

#### `previewSkillHub` — Preview a hub skill.

```typescript
client.agent.skills.previewSkillHub(id?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | query | No | Hub skill identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/skills/hub/preview`
**CLI:** `hoody agent skills preview-hub`

---

#### `putSkillSource` — Write a skill's source.

```typescript
client.agent.skills.putSkillSource(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ root_dir*: string, rel_dir*: string, content*: string, base_gen: int }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/skills/source`
**CLI:** `hoody agent skills put-source`

---

#### `renameSkill` — Rename a skill.

```typescript
client.agent.skills.renameSkill(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ root_dir*: string, rel_dir*: string, new_name*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/skills/rename`
**CLI:** `hoody agent skills rename`

---

#### `scanSkillImport` — Scan for importable skills.

```typescript
client.agent.skills.scanSkillImport(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/skills/import/scan`
**CLI:** `hoody agent skills scan-import`

---

#### `searchSkillHub` — Search the skill hub.

```typescript
client.agent.skills.searchSkillHub(q?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `q` | `string` | query | No | Search query. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/skills/hub/search`
**CLI:** `hoody agent skills search-hub`

---

#### `toggleSkill` — Enable/disable a skill.

```typescript
client.agent.skills.toggleSkill(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ name*: string, disabled: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/skills/toggle`
**CLI:** `hoody agent skills toggle`

---

#### `trustSkill` — Set a skill's trust state.

```typescript
client.agent.skills.trustSkill(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ root_dir*: string, rel_dir*: string, trusted*: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/skills/trust`
**CLI:** `hoody agent skills trust`

---

### `client.agent.statistics` (3) — Per-session and aggregate usage counters

#### `getStatistics` — Cross-session statistics.

```typescript
client.agent.statistics.getStatistics(scope?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `scope` | `string` | query | No | cwd (default) rolls up the current working directory; all rolls up every session. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/statistics`
**CLI:** `hoody agent statistics get`

---

#### `usageByAccount` — Usage rollup by account.

```typescript
client.agent.statistics.usageByAccount(since?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `since` | `integer` | query | No | Unix-seconds lower bound; omit for all-time. A negative/non-numeric value is rejected 400. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/usage/by-account`
**CLI:** `hoody agent statistics usage-by-account`

---

#### `usageByModel` — Usage rollup by model.

```typescript
client.agent.statistics.usageByModel(since?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `since` | `integer` | query | No | Unix-seconds lower bound; omit for all-time. A negative/non-numeric value is rejected 400. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/usage/by-model`
**CLI:** `hoody agent statistics usage-by-model`

---

### `client.agent.system` (5) — Health, metrics, and operational endpoints

#### `docs` — API documentation UI.

```typescript
client.agent.system.docs()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/docs`
**CLI:** `hoody agent system docs`

---

#### `healthCheck` — Standardized health check.

```typescript
client.agent.system.healthCheck()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/health`
**CLI:** `hoody agent system health-check`

---

#### `metrics` — Prometheus metrics.

```typescript
client.agent.system.metrics()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/metrics`
**CLI:** `hoody agent system metrics`

---

#### `openapiJSON` — OpenAPI spec (JSON).

```typescript
client.agent.system.openapiJSON()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/openapi.json`
**CLI:** `hoody agent system openapi-json`

---

#### `openapiYAML` — OpenAPI spec (YAML).

```typescript
client.agent.system.openapiYAML()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/openapi.yaml`
**CLI:** `hoody agent system openapi-yaml`

---

### `client.agent.tasks` (4) — Background task management

#### `cancelAllTasks` — Cancel all background tasks.

```typescript
client.agent.tasks.cancelAllTasks(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/tasks/cancel`
**CLI:** `hoody agent tasks cancel-all`

---

#### `cancelTask` — Cancel a background task.

```typescript
client.agent.tasks.cancelTask(id: string, tid: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `tid` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/tasks/{tid}/cancel`
**CLI:** `hoody agent tasks cancel`

---

#### `listTasks` — Request the session's task snapshot.

```typescript
client.agent.tasks.listTasks(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/tasks`
**CLI:** `hoody agent tasks list`

---

#### `requestTaskTranscript` — Request a task's transcript (upsert-poll).

```typescript
client.agent.tasks.requestTaskTranscript(id: string, tid: string, after_seq?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `tid` | `string` | path | Yes | Path identifier. |
| `after_seq` | `integer` | query | No | int64 upsert-poll cursor; entries at/below it are re-sent (default 0). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/tasks/{tid}/transcript`
**CLI:** `hoody agent tasks request-transcript`

---

### `client.agent.todos` (19) — Container-aware task list management

#### `approveTodoProposal` — Approve a todo proposal.

```typescript
client.agent.todos.approveTodoProposal(id: string, pid: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `pid` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/proposals/{pid}/approve`
**CLI:** `hoody agent todos approve-proposal`

---

#### `archiveTodo` — Archive a todo.

```typescript
client.agent.todos.archiveTodo(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ revision*: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/archive`
**CLI:** `hoody agent todos archive`

---

#### `cancelTodoRun` — Cancel a todo's run.

```typescript
client.agent.todos.cancelTodoRun(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/cancel-run`
**CLI:** `hoody agent todos cancel-run`

---

#### `claimTodo` — Claim a todo.

```typescript
client.agent.todos.claimTodo(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ revision: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/claim`
**CLI:** `hoody agent todos claim`

---

#### `createTodo` — File a todo.

```typescript
client.agent.todos.createTodo(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ title*: string, body: string, priority: int, tags: any[], cwd: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos`
**CLI:** `hoody agent todos create`

---

#### `denyTodoProposal` — Deny a todo proposal.

```typescript
client.agent.todos.denyTodoProposal(id: string, pid: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `pid` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/proposals/{pid}/deny`
**CLI:** `hoody agent todos deny-proposal`

---

#### `getTodo` — Read a todo.

```typescript
client.agent.todos.getTodo(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/todos/{id}`
**CLI:** `hoody agent todos get`

---

#### `getTodosRevision` — Get the todos store revision.

```typescript
client.agent.todos.getTodosRevision(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/todos/revision`
**CLI:** `hoody agent todos get-revision`

---

#### `listTodos` — List todos.

```typescript
client.agent.todos.listTodos(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ states: any[], tags: any[], query: string, open_only: bool, all: bool }`

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/todos`
**CLI:** `hoody agent todos list`

---

#### `listTodosAll` — List todos. (collect all pages)

```typescript
client.agent.todos.listTodosAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ states: any[], tags: any[], query: string, open_only: bool, all: bool }`

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/todos`
**CLI:** `hoody agent todos list`

---

#### `listTodosIterator` — List todos. (async iterator)

```typescript
client.agent.todos.listTodosIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ states: any[], tags: any[], query: string, open_only: bool, all: bool }`

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/todos`
**CLI:** `hoody agent todos list`

---

#### `messageTodo` — Comment + run an orchestrator turn.

```typescript
client.agent.todos.messageTodo(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ text*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/message`
**CLI:** `hoody agent todos message`

---

#### `postTodoComment` — Comment on a todo.

```typescript
client.agent.todos.postTodoComment(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ text*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/messages`
**CLI:** `hoody agent todos post-comment`

---

#### `purgeTodos` — Purge archived todos.

```typescript
client.agent.todos.purgeTodos(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/purge`
**CLI:** `hoody agent todos purge`

---

#### `releaseTodo` — Release a todo.

```typescript
client.agent.todos.releaseTodo(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/release`
**CLI:** `hoody agent todos release`

---

#### `runTodo` — Run a todo's orchestrator.

```typescript
client.agent.todos.runTodo(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/run`
**CLI:** `hoody agent todos run`

---

#### `snoozeTodo` — Snooze a todo.

```typescript
client.agent.todos.snoozeTodo(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ wake_at*: string, revision*: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/{id}/snooze`
**CLI:** `hoody agent todos snooze`

---

#### `triageTodos` — Run an LLM triage pass.

```typescript
client.agent.todos.triageTodos(X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/todos/triage`
**CLI:** `hoody agent todos triage`

---

#### `updateTodo` — Update a todo (CAS).

```typescript
client.agent.todos.updateTodo(id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ revision*: int, title: string, body: string, state: string, priority: int, rank: int, tags: any[], cwd: string }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/agent/todos/{id}`
**CLI:** `hoody agent todos update`

---

### `client.agent.tools` (17) — Tool catalogue and gated tool execution

#### `getTool` — Get one tool schema.

```typescript
client.agent.tools.getTool(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/tools/{name}`
**CLI:** `hoody agent tools get`

---

#### `listReadOnlyTools` — List the read-only tool subset.

```typescript
client.agent.tools.listReadOnlyTools(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/tools/read-only`
**CLI:** `hoody agent tools list-read-only`

---

#### `listReadOnlyToolsAll` — List the read-only tool subset. (collect all pages)

```typescript
client.agent.tools.listReadOnlyToolsAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/tools/read-only`
**CLI:** `hoody agent tools list-read-only`

---

#### `listReadOnlyToolsIterator` — List the read-only tool subset. (async iterator)

```typescript
client.agent.tools.listReadOnlyToolsIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/tools/read-only`
**CLI:** `hoody agent tools list-read-only`

---

#### `listSessionMCPTools` — List a session's MCP tools.

```typescript
client.agent.tools.listSessionMCPTools(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/tools/mcp`
**CLI:** `hoody agent tools list-session-mcp`

---

#### `listSessionMCPToolsAll` — List a session's MCP tools. (collect all pages)

```typescript
client.agent.tools.listSessionMCPToolsAll(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/tools/mcp`
**CLI:** `hoody agent tools list-session-mcp`

---

#### `listSessionMCPToolsIterator` — List a session's MCP tools. (async iterator)

```typescript
client.agent.tools.listSessionMCPToolsIterator(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/tools/mcp`
**CLI:** `hoody agent tools list-session-mcp`

---

#### `listSessionTools` — List a session's effective tool set.

```typescript
client.agent.tools.listSessionTools(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/tools`
**CLI:** `hoody agent tools list-session`

---

#### `listSessionToolsAll` — List a session's effective tool set. (collect all pages)

```typescript
client.agent.tools.listSessionToolsAll(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/tools`
**CLI:** `hoody agent tools list-session`

---

#### `listSessionToolsIterator` — List a session's effective tool set. (async iterator)

```typescript
client.agent.tools.listSessionToolsIterator(id: string, page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/sessions/{id}/tools`
**CLI:** `hoody agent tools list-session`

---

#### `listTools` — List the tool catalogue.

```typescript
client.agent.tools.listTools(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/tools`
**CLI:** `hoody agent tools list`

---

#### `listToolsAll` — List the tool catalogue. (collect all pages)

```typescript
client.agent.tools.listToolsAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/tools`
**CLI:** `hoody agent tools list`

---

#### `listToolsIterator` — List the tool catalogue. (async iterator)

```typescript
client.agent.tools.listToolsIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/tools`
**CLI:** `hoody agent tools list`

---

#### `runSessionTool` — Run a tool inside a live session (gated).

```typescript
client.agent.tools.runSessionTool(id: string, name: string, confirm?: boolean, confirm_token?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `name` | `string` | path | Yes | Path identifier. |
| `confirm` | `boolean` | query | No | Query alias of the body `confirm` field — re-issue a previously-parked confirmation (pair with confirm_token). |
| `confirm_token` | `string` | query | No | Query alias of the body `confirm_token` field — the single-use token returned in the 409 tool_needs_confirmation details. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ params: object, confirm: bool, confirm_token: string, allow_mutations: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/tools/{name}/run`
**CLI:** `hoody agent tools run-session`

---

#### `runTool` — Run a tool (sessionless, gated).

```typescript
client.agent.tools.runTool(name: string, confirm?: boolean, confirm_token?: string, X-Hoody-Tool-Mode?: string, X-Hoody-Dir-Scope?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `confirm` | `boolean` | query | No | Query alias of the body `confirm` field — re-issue a previously-parked confirmation (pair with confirm_token). |
| `confirm_token` | `string` | query | No | Query alias of the body `confirm_token` field — the single-use token returned in the 409 tool_needs_confirmation details. |
| `X-Hoody-Tool-Mode` | `string` | header | No | Sessionless tool-mode for the ephemeral session (e.g. read_only / full); default per the daemon. Ignored on the in-session run (it inherits the session's frozen tool-mode). |
| `X-Hoody-Dir-Scope` | `string` | header | No | Sessionless directory-access scope for the ephemeral session (e.g. home / full); default home. Ignored on the in-session run (it inherits the session's frozen dir-scope). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ params: object, confirm: bool, confirm_token: string, allow_mutations: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/tools/{name}/run`
**CLI:** `hoody agent tools run`

---

#### `runToolAsync` — Run a tool asynchronously (sessionless, gated).

```typescript
client.agent.tools.runToolAsync(name: string, confirm?: boolean, confirm_token?: string, X-Hoody-Tool-Mode?: string, X-Hoody-Dir-Scope?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `confirm` | `boolean` | query | No | Query alias of the body `confirm` field — re-issue a previously-parked confirmation (pair with confirm_token). |
| `confirm_token` | `string` | query | No | Query alias of the body `confirm_token` field — the single-use token returned in the 409 tool_needs_confirmation details. |
| `X-Hoody-Tool-Mode` | `string` | header | No | Sessionless tool-mode for the ephemeral session (e.g. read_only / full); default per the daemon. Ignored on the in-session run (it inherits the session's frozen tool-mode). |
| `X-Hoody-Dir-Scope` | `string` | header | No | Sessionless directory-access scope for the ephemeral session (e.g. home / full); default home. Ignored on the in-session run (it inherits the session's frozen dir-scope). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ params: object, confirm: bool, confirm_token: string, allow_mutations: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/tools/{name}/runAsync`
**CLI:** `hoody agent tools run-async`

---

#### `streamTool` — Run a tool with a streamed result (sessionless, gated).

```typescript
client.agent.tools.streamTool(name: string, confirm?: boolean, confirm_token?: string, X-Hoody-Tool-Mode?: string, X-Hoody-Dir-Scope?: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `confirm` | `boolean` | query | No | Query alias of the body `confirm` field — re-issue a previously-parked confirmation (pair with confirm_token). |
| `confirm_token` | `string` | query | No | Query alias of the body `confirm_token` field — the single-use token returned in the 409 tool_needs_confirmation details. |
| `X-Hoody-Tool-Mode` | `string` | header | No | Sessionless tool-mode for the ephemeral session (e.g. read_only / full); default per the daemon. Ignored on the in-session run (it inherits the session's frozen tool-mode). |
| `X-Hoody-Dir-Scope` | `string` | header | No | Sessionless directory-access scope for the ephemeral session (e.g. home / full); default home. Ignored on the in-session run (it inherits the session's frozen dir-scope). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ params: object, confirm: bool, confirm_token: string, allow_mutations: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/tools/{name}/stream`
**CLI:** `hoody agent tools stream`

---

### `client.agent.workflows` (14) — Multi-step workflow definitions and runs

#### `cancelWorkflowRun` — Cancel a workflow run.

```typescript
client.agent.workflows.cancelWorkflowRun(run_id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `run_id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/workflows/runs/{run_id}/cancel`
**CLI:** `hoody agent workflows cancel-run`

---

#### `deleteWorkflow` — Delete a workflow definition.

```typescript
client.agent.workflows.deleteWorkflow(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/agent/workflows/{name}`
**CLI:** `hoody agent workflows delete`

---

#### `getWorkflow` — Read one workflow definition.

```typescript
client.agent.workflows.getWorkflow(name: string, include_revision?: boolean, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `include_revision` | `boolean` | query | No | If "true", the tool output's first line is `revision: <opaque>` — pass that value as putWorkflow's expected_revision to guard against concurrent edits; the JSON below it is unchanged. Strictly parsed: exactly one value, "true" or "false"; anything else (empty, "TRUE", "1", repeated) is a 400 bad_request. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/workflows/{name}`
**CLI:** `hoody agent workflows get`

---

#### `getWorkflowRun` — Get one workflow run by id.

```typescript
client.agent.workflows.getWorkflowRun(run_id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `run_id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/workflows/runs/{run_id}`
**CLI:** `hoody agent workflows get-run`

---

#### `hideWorkflow` — Hide or un-hide a workflow.

```typescript
client.agent.workflows.hideWorkflow(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ hidden: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/workflows/{name}/hide`
**CLI:** `hoody agent workflows hide`

---

#### `listWorkflowRuns` — Snapshot in-flight and recent workflow runs.

```typescript
client.agent.workflows.listWorkflowRuns(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/workflows/runs`
**CLI:** `hoody agent workflows list-runs`

---

#### `listWorkflowRunsAll` — Snapshot in-flight and recent workflow runs. (collect all pages)

```typescript
client.agent.workflows.listWorkflowRunsAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/workflows/runs`
**CLI:** `hoody agent workflows list-runs`

---

#### `listWorkflowRunsIterator` — Snapshot in-flight and recent workflow runs. (async iterator)

```typescript
client.agent.workflows.listWorkflowRunsIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/workflows/runs`
**CLI:** `hoody agent workflows list-runs`

---

#### `listWorkflows` — List workflow definitions.

```typescript
client.agent.workflows.listWorkflows(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/agent/workflows`
**CLI:** `hoody agent workflows list`

---

#### `listWorkflowsAll` — List workflow definitions. (collect all pages)

```typescript
client.agent.workflows.listWorkflowsAll(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/agent/workflows`
**CLI:** `hoody agent workflows list`

---

#### `listWorkflowsIterator` — List workflow definitions. (async iterator)

```typescript
client.agent.workflows.listWorkflowsIterator(page?: integer, limit?: integer, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number for pagination. |
| `limit` | `integer` | query | No | Maximum items per page (0 = no pagination). |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/agent/workflows`
**CLI:** `hoody agent workflows list`

---

#### `putWorkflow` — Create or replace a workflow definition.

```typescript
client.agent.workflows.putWorkflow(name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ definition*: object, expected_revision: string, expected_absent: bool }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/agent/workflows/{name}`
**CLI:** `hoody agent workflows put`

---

#### `resumeWorkflowRun` — Resume a failed or cancelled workflow run.

```typescript
client.agent.workflows.resumeWorkflowRun(run_id: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `run_id` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | Yes |  |

**Body:** `{ session_id*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/workflows/runs/{run_id}/resume`

---

#### `runSessionWorkflow` — Run a workflow onto an existing session.

```typescript
client.agent.workflows.runSessionWorkflow(id: string, name: string, X-Hoody-Cwd?: string, X-Hoody-Config-Dir?: string, X-Hoody-Container?: string, X-Hoody-Realm?: string, realm?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Path identifier. |
| `name` | `string` | path | Yes | Path identifier. |
| `X-Hoody-Cwd` | `string` | header | No | Per-request working-directory scope: the.hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
| `X-Hoody-Config-Dir` | `string` | header | No | Per-request --config-dir override selecting which on-disk.hoody install a stateless read/write resolves (HoodyPaths). |
| `X-Hoody-Container` | `string` | header | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
| `X-Hoody-Realm` | `string` | header | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `realm` | `string` | query | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
| `data` | `object` | body | No |  |

**Body:** `{ prompt: string, inputs: object }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/agent/sessions/{id}/workflows/{name}/runs`
**CLI:** `hoody agent workflows run-session`


---

<!-- ===== namespace: api ===== -->

# `api` — Platform control plane: identity, projects, containers, billing, vault

## Purpose

Control plane outside container kits. Owns identity (signup, login, OAuth, 2FA, auth tokens), project/container hierarchy, proxy permissions, network/firewall/storage, billing, rentals, encrypted user vault, pools. Also exposes account-wide notifications/events/activity inbox. All other namespaces depend on IDs/tokens minted here.

## When to use

- Authenticate users; mint auth tokens for headless sessions.
- Create/list/mutate/destroy projects, containers, snapshots, proxy-aliases.
- Grant/revoke project/container access; set proxy auth (password/token/JWT/IP).
- Wallet, billing, rental ops.
- User-scoped encrypted vault.
- Account-wide notification/event/activity queries.

## When NOT to use

- File I/O, shell/program, SQLite, GUI/browser, background processes, agent runtime — use `files`, `terminal`/`exec`, `sqlite`, `display`/`browser`, `daemon`, `agent` respectively.

## Prerequisites

- Control plane at `https://api.hoody.com`.
- Bearer token in `Authorization`. Mint via `authentication.login` (1d JWT / 7d refresh) or `authTokens.create` (long-lived, scopable).
- 2FA mutations have varying auth: `tfa.setup` needs password only; `tfa.verifySetup` needs OTP code only; `tfa.verify` needs `temp_token` + code; `tfa.disable` needs password + OTP **or** backup code; `tfa.regenerateBackupCodes` needs password + a **6-digit TOTP only** (`^\\d{6}$` — backup codes are rejected with 400).
- Project/container writes: project owner or matching permission row.
- Billing writes: registered payment method.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Auth bootstrap (signup → verify → login [+2FA])

1. `client.api.authentication.signup`
2. `client.api.authentication.verifyEmail`
3. `client.api.authentication.login`
4. `client.api.tfa.verify` (if 2FA enabled — uses `temp_token`)
5. `client.api.authentication.getCurrentUser`

### 2. Mint a long-lived auth token

1. `client.api.authTokens.create`
2. `client.api.authTokens.list`
3. `client.api.authTokens.addRealm`
4. `client.api.authTokens.removeRealm`
5. `client.api.authTokens.copy`
6. `client.api.authTokens.delete`

### 3. Set up 2FA

1. `client.api.tfa.setup`
2. `client.api.tfa.verifySetup`
3. `client.api.tfa.getStatus`
4. `client.api.tfa.regenerateBackupCodes`
5. `client.api.tfa.setTokenGate`

### 4. Create first project + container

Read kit URLs by getting the container with `include_proxy_domains` set to `true` (`containers.get(id, { include_proxy_domains: "true" })`) — the `proxy_domains` array is only populated when `include_proxy_domains` is passed.
1. `client.api.realms.list`
2. `client.api.images.list`
3. `client.api.projects.create`
4. `client.api.containers.create`
5. `client.api.containers.manage`
6. `client.api.containers.get`

### 5. Grant another user access

Project-scope analogues live under `proxyPermissionsProject.*`.
1. `client.api.projects.listPermissions`
2. `client.api.projects.addPermission`
3. `client.api.projects.updatePermission`
4. `client.api.projects.removePermission`
5. `client.api.proxyPermissionsContainer.setPasswordGroup`
6. `client.api.proxyPermissionsContainer.setTokenGroup`
7. `client.api.proxyPermissionsContainer.setJwtGroup`
8. `client.api.proxyPermissionsContainer.updateState`

### 6. Container exposure & shares

1. `client.api.containers.updateNetworkConfig`
2. `client.api.containers.startNetwork`
3. `client.api.firewall.addEgressRule`
4. `client.api.firewall.addIngressRule`
5. `client.api.proxyAliases.create`
6. `client.api.proxyAliases.setState`
7. `client.api.storageShares.create`
8. `client.api.storageShares.listIncoming` (container-scoped)
9. `client.api.storageShares.toggleIncomingMount`
10. `client.api.storageShares.delete`

### 7. Container lifecycle ops (snapshot/restore/copy + env + kvm)

1. `client.api.containers.createSnapshot`
2. `client.api.containers.listSnapshots`
3. `client.api.containers.restoreSnapshot`
4. `client.api.containers.copy`
5. `client.api.containers.deleteSnapshot`
6. `client.api.env.list`
7. `client.api.env.set`
8. `client.api.env.bulkSet`
9. `client.api.env.delete`
10. `client.api.containers.setContainerKvm` — enable/disable `/dev/kvm` passthrough (run full VMs inside the container) on a **stopped** container. Also settable at creation via the `kvm` field/flag on `client.api.containers.create`.

### 8. Billing: wallet → rent

1. `client.api.wallet.addPaymentMethod`
2. `client.api.wallet.setDefaultPaymentMethod`
3. `client.api.wallet.createStripeCheckout` (crypto: `client.api.wallet.createCryptoInvoice`)
4. `client.api.wallet.getStripePaymentIntent` (crypto: `client.api.wallet.getCryptoPaymentIntent`)
5. `client.api.wallet.getAggregateBalances`
6. `client.api.wallet.transferToAi`
7. `client.api.wallet.listTransactions`
8. `client.api.serverRental.browse`
9. `client.api.serverRental.rent`
10. `client.api.rentals.list`
11. `client.api.rentals.extend`
12. `client.api.serverCommands.execute`

Vault, pools (+ pool members + pool invitations), notifications/events/activity inbox are pure CRUD — see the auto-generated Reference for method signatures, services and the corresponding endpoints / commands.

## Quirks & gotchas

- Login accepts `username` OR `email` + `password` (`anyOf`); only the email lookup is lowercased, usernames are matched case-sensitive.
- JWT lifecycle: `authentication.logout` is a logout-ALL for JWTs — it bumps `session_generation`, invalidating every access + refresh JWT issued before that moment (all sessions, not just the current one); long-lived auth tokens are unaffected (revoke those with `authTokens.delete`). `authentication.refreshToken` requires the refresh token in **both** the request body AND a matching `Authorization: Bearer` header — the generated SDK / CLI auto-refresh only send the body, so the typed call typically 401s; for headless flows mint a long-lived `authTokens.create` instead, or call refresh manually with both the body and the header set to the same refresh token.
- `authentication.getAvailableRegions` returns `r.data.regions` (single-wrapped, like every other endpoint — older docs incorrectly called it doubly-wrapped).
- Duplicate signup returns `200` (anti-enumeration). For an unverified user, the controller silently overwrites the password and re-sends the verification email; for a verified user it's a no-op. Do NOT probe with this.
- The `agent` kit needs **no** `X-Hoody-Container-Claim` / `X-Hoody-Token` headers — like every other kit it accepts the bare per-container kit URL directly, with no auth headers. The `containers.authorize(id)` call mints an *optional* portable container claim for offline verification by your own container programs; no built-in kit requires it. See § Auth model.
- Vault via auth tokens requires `vault_access === true` AND `resources.vault` on the token; else 403. JWT sessions are not gated.
- Rate limits: login 1000/30min failures-only; signup 5/hour fail-closed.
- Auth tokens rejected on admin endpoints (JWT only); `x-impersonate-user` JWT only.
- `containers.manage` polymorphic: `POST /api/v1/containers/{id}/{operation}` from `ContainerOperation` enum (not body field).
- No admin concept on user-owned resources; permission row IS the authorization.
- Kit URL `<projectId>-<containerId>-<kit>-1.<server>.containers.hoody.com` IS the credential; watch containerId leakage.
- `proxyDiscovery.listContainerProxyServices` returns `services: []` often; pass `program: 'exec'` to `proxyAliases.create`.
- `wallet.listInvoices` returns `200 {invoices:[],pagination:{...}}` for never-billed accounts (current). `utilities.getIpInfo` returns IP, user-agent, headers, referer, timestamp, auth flag, protocol, and `ip_info` — not just IP.
- `storageShares.listIncoming(id)` is container-scoped; pass `containerId`.
- `verifyEmail` body has `token` + optional `response_mode` + `code_challenge` only — there is no `email` field. With `response_mode: 'intent'` + PKCE, returns `auth_intent_token`; if 2FA is on, returns `requires_2fa: true` + `temp_token` for `tfa.verify`.
- KVM (`PUT /containers/{id}/kvm`, field `kvm` on create/responses) is rented/dedicated (bare-metal) servers ONLY — free tier is hard-refused with 403, and the container must be STOPPED to toggle (409 otherwise). Canonical field is `kvm`; `dev_kvm` is an input-only alias (`kvm` wins; both present and disagreeing → 400). Default off.

## Common errors

- 400 — schema validation; login: "Username or email, and password are required".
- 401 — Bearer missing/malformed. JWTs require the literal `Bearer ` prefix; `hdy_…` auth tokens are also accepted bare (`Authorization: hdy_…`).
- 403 — missing permission row or `resources.*` flag.
- 404 — missing resource OR 403 masked.
- 409 — uniqueness (duplicate username, proxy-alias).
- 412 / 428 — prior step needed (payment method, email verification, 2FA).
- 422 — semantic validation (password complexity, region, rental_days).
- 429 — login 1000/30min (failures only), signup 5/hour, refresh 30/30min.
- 400 — `events` socket.io may return "Session ID unknown" when the client session expires.
- Always-200 — `forgotPassword`, `resendVerification`, duplicate-`signup`; do NOT probe with these.

## Related namespaces

- `agent` — uses tokens/realms minted here.
- `files` / `terminal` / `exec` / `sqlite` / `daemon` — operate on containers created here.
- `tunnel` — relies on this namespace for proxy aliases and firewall rules.
- `notifications` (kit) — in-container desktop notifications; the account-inbox notifications/events/activity surfaces live here in the control plane.

## Reference

**Accessor:** `client.api`  |  **Import:** `import * as api from 'hoody-sdk/api'`

### `client.api.activity` (4) — Activity Logs

#### `getStats` — Get activity stats

```typescript
client.api.activity.getStats()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/users/auth/activity/stats`
**CLI:** `hoody activity stats`

---

#### `list` — Get activity logs

```typescript
client.api.activity.list(page?: integer, limit?: integer, start_date?: string, end_date?: string, errors_only?: string, min_status?: integer, max_status?: integer, method?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number |
| `limit` | `integer` | query | No | Results per page |
| `start_date` | `string` | query | No | Filter logs after this date |
| `end_date` | `string` | query | No | Filter logs before this date |
| `errors_only` | `string` | query | No | Show only errors (status >= 400) |
| `min_status` | `integer` | query | No | Minimum status code |
| `max_status` | `integer` | query | No | Maximum status code |
| `method` | `string` | query | No | Filter by HTTP method |
| `realm_id` | `string` | query | No | Filter by realm ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/users/auth/activity`
**CLI:** `hoody activity logs`

---

#### `listAll` — Get activity logs (collect all pages)

```typescript
client.api.activity.listAll(page?: integer, limit?: integer, start_date?: string, end_date?: string, errors_only?: string, min_status?: integer, max_status?: integer, method?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number |
| `limit` | `integer` | query | No | Results per page |
| `start_date` | `string` | query | No | Filter logs after this date |
| `end_date` | `string` | query | No | Filter logs before this date |
| `errors_only` | `string` | query | No | Show only errors (status >= 400) |
| `min_status` | `integer` | query | No | Minimum status code |
| `max_status` | `integer` | query | No | Maximum status code |
| `method` | `string` | query | No | Filter by HTTP method |
| `realm_id` | `string` | query | No | Filter by realm ID |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/users/auth/activity`
**CLI:** `hoody activity logs`

---

#### `listIterator` — Get activity logs (async iterator)

```typescript
client.api.activity.listIterator(page?: integer, limit?: integer, start_date?: string, end_date?: string, errors_only?: string, min_status?: integer, max_status?: integer, method?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number |
| `limit` | `integer` | query | No | Results per page |
| `start_date` | `string` | query | No | Filter logs after this date |
| `end_date` | `string` | query | No | Filter logs before this date |
| `errors_only` | `string` | query | No | Show only errors (status >= 400) |
| `min_status` | `integer` | query | No | Minimum status code |
| `max_status` | `integer` | query | No | Maximum status code |
| `method` | `string` | query | No | Filter by HTTP method |
| `realm_id` | `string` | query | No | Filter by realm ID |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/users/auth/activity`
**CLI:** `hoody activity logs`

---

### `client.api.ai` (1) — AI

#### `listModels` — List available AI models (Hoody catalog)

```typescript
client.api.ai.listModels()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/ai/models`
**CLI:** `hoody ai list`

---

### `client.api.authTokens` (14) — Auth Tokens

#### `addRealm` — Add realm to auth token

```typescript
client.api.authTokens.addRealm(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Auth token ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ realm_id*: string, otp_code: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/tokens/{id}/add-realm`
**CLI:** `hoody auth realms add`

---

#### `copy` — Copy auth token

```typescript
client.api.authTokens.copy(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the token |
| `data` | `object` | body | Yes |  |

**Body:** `{ alias: string, expires_at: string | "today" | "tomorrow" | number | null, otp_code: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/tokens/{id}/copy`
**CLI:** `hoody auth copy`

---

#### `create` — Create a new auth token

```typescript
client.api.authTokens.create(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ alias: string, public_key: string | null, public_storage: object | null, ip_whitelist: string[] | string, permission_template: string, permissions: { containers: object, projects: object, financial: object, resources: object, admin: object }, realm_ids: string[], allow_no_realm: bool, vault_access: bool, event_access: bool, deny_reauthorization: bool, expires_at: string | "today" | "tomorrow" | number, otp_code: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/tokens`
**CLI:** `hoody auth create`

---

#### `delete` — Delete auth token

```typescript
client.api.authTokens.delete(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the token |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/auth/tokens/{id}`
**CLI:** `hoody auth delete`

---

#### `get` — Get auth token by ID

```typescript
client.api.authTokens.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the token |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/auth/tokens/{id}`
**CLI:** `hoody auth get`

---

#### `getCurrent` — Get current auth token details

```typescript
client.api.authTokens.getCurrent()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/auth/tokens/me`
**CLI:** `hoody auth get-current`

---

#### `getPublicProfile` — Get auth token public profile by public key

```typescript
client.api.authTokens.getPublicProfile(public_key: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `public_key` | `string` | path | Yes | ED25519 public key to resolve |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/auth/tokens/public-profiles/{public_key}`
**CLI:** `hoody auth profile by-public-key`

---

#### `list` — List auth tokens

```typescript
client.api.authTokens.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/auth/tokens`
**CLI:** `hoody auth list`

---

#### `listAll` — List auth tokens (collect all pages)

```typescript
client.api.authTokens.listAll()
```

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/auth/tokens`
**CLI:** `hoody auth list`

---

#### `listAuthTokenPermissionTemplates` — List permission templates

```typescript
client.api.authTokens.listAuthTokenPermissionTemplates()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/auth/tokens/templates`

---

#### `listIterator` — List auth tokens (async iterator)

```typescript
client.api.authTokens.listIterator()
```

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/auth/tokens`
**CLI:** `hoody auth list`

---

#### `removeRealm` — Remove realm from auth token

```typescript
client.api.authTokens.removeRealm(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Auth token ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ realm_id*: string, otp_code: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/tokens/{id}/remove-realm`
**CLI:** `hoody auth realms remove`

---

#### `update` — Update auth token

```typescript
client.api.authTokens.update(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the token to update |
| `data` | `object` | body | Yes |  |

**Body:** `{ alias: string, public_key: string | null, public_storage: object | null, ip_whitelist: string[] | string, permissions: { containers: object, projects: object, financial: object, resources: object, admin: object }, realm_ids: string[], allow_no_realm: bool, vault_access: bool, event_access: bool, expires_at: string | "today" | "tomorrow" | number | null, is_enabled: bool, otp_code: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/auth/tokens/{id}`
**CLI:** `hoody auth update`

---

#### `updatePublicProfile` — Update current auth token public profile

```typescript
client.api.authTokens.updatePublicProfile(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ public_key: string | null, public_storage: object | null } (at least one of: public_key | public_storage required)`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/auth/tokens/me/public-profile`
**CLI:** `hoody auth profile update`

---

### `client.api.authentication` (28) — Authentication

#### `api_issueIdentityClaim` — Issue a fresh audience-bound identity claim

```typescript
client.api.authentication.api_issueIdentityClaim(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ audience*: string, expires_in: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/auth/identity-claim`

---

#### `forgotPassword` — Request password reset

```typescript
client.api.authentication.forgotPassword(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ email*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/forgot-password`
**CLI:** `hoody auth password forgot`

---

#### `getAvailableRegions` — Get available server regions

```typescript
client.api.authentication.getAvailableRegions()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/auth/available-regions`
**CLI:** `hoody auth regions`

---

#### `getCurrentUser` — Get current user profile

```typescript
client.api.authentication.getCurrentUser()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/users/auth/me`
**CLI:** `hoody auth profile current`

---

#### `getCurrentUserAlias` — Get current user profile (alias of /users/auth/me)

```typescript
client.api.authentication.getCurrentUserAlias()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/users/me`

---

#### `getOAuthConfig` — Get the public sign-in configuration

```typescript
client.api.authentication.getOAuthConfig()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/auth/config`

---

#### `githubOAuthCallback` — GitHub OAuth callback

```typescript
client.api.authentication.githubOAuthCallback(code?: string, state: string, error?: string, error_description?: string, error_uri?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `code` | `string` | query | No |  |
| `state` | `string` | query | Yes |  |
| `error` | `string` | query | No | Provider-side failure code (e.g. access_denied). Present instead of `code` when the user declines. |
| `error_description` | `string` | query | No |  |
| `error_uri` | `string` | query | No |  |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/auth/github/callback`
**CLI:** `hoody auth oauth github callback`

---

#### `githubOAuthRedirect` — Redirect to GitHub OAuth

```typescript
client.api.authentication.githubOAuthRedirect(client?: string, intent?: string, redirect_uri: string, code_challenge: string, invite_code?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `client` | `string` | query | No | Optional client-declared source channel for analytics: web \| ssh \| webssh \| cli \| sdk \| agent. Folded into the signed OAuth state so it survives the provider round trip. Unrecognised values are recorded as "unknown". |
| `intent` | `string` | query | No | OAuth intent: login (default). "star_check" is accepted but ignored (retired). |
| `redirect_uri` | `string` | query | Yes | Frontend URL to redirect to after OAuth completes (must be on an allowed domain) |
| `code_challenge` | `string` | query | Yes | PKCE code_challenge (base64url SHA-256 of code_verifier). Required — all OAuth flows must use PKCE post-migration. |
| `invite_code` | `string` | query | No | Optional invite code ("coupon") captured from the signup link. Normalized and hashed at redirect time — only the hash travels in the OAuth state, never the raw code. Memorized hash-only on a NEW account and applied automatically; not validated here. |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/auth/github`
**CLI:** `hoody auth oauth github redirect`

---

#### `googleOAuthCallback` — Google OAuth callback

```typescript
client.api.authentication.googleOAuthCallback(code?: string, state: string, error?: string, error_description?: string, error_uri?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `code` | `string` | query | No |  |
| `state` | `string` | query | Yes |  |
| `error` | `string` | query | No | Provider-side failure code (e.g. access_denied). Present instead of `code` when the user declines. |
| `error_description` | `string` | query | No |  |
| `error_uri` | `string` | query | No |  |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/auth/google/callback`
**CLI:** `hoody auth oauth google callback`

---

#### `googleOAuthRedirect` — Redirect to Google OAuth

```typescript
client.api.authentication.googleOAuthRedirect(client?: string, redirect_uri: string, code_challenge: string, invite_code?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `client` | `string` | query | No | Optional client-declared source channel for analytics: web \| ssh \| webssh \| cli \| sdk \| agent. Folded into the signed OAuth state so it survives the provider round trip. Unrecognised values are recorded as "unknown". |
| `redirect_uri` | `string` | query | Yes | Frontend URL to redirect to after OAuth completes (must be on an allowed domain) |
| `code_challenge` | `string` | query | Yes | PKCE code_challenge (base64url SHA-256 of code_verifier). Required — all OAuth flows must use PKCE post-migration. |
| `invite_code` | `string` | query | No | Optional invite code ("coupon") captured from the signup link. Normalized and hashed at redirect time — only the hash travels in the OAuth state, never the raw code. Memorized hash-only on a NEW account and applied automatically; not validated here. |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/auth/google`
**CLI:** `hoody auth oauth google redirect`

---

#### `login` — Login with username and password

```typescript
client.api.authentication.login(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ username: string, email: string, password*: string, response_mode: "intent" | "tokens", client: string, code_challenge: string } (at least one of: username | email required)`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/auth/login`
**CLI:** `hoody auth login`

---

#### `logout` — Logout

```typescript
client.api.authentication.logout()
```

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/auth/logout`
**CLI:** `hoody auth logout`

---

#### `oauthAuthorize` — Begin a PKCE OAuth authorization

```typescript
client.api.authentication.oauthAuthorize(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ code_challenge*: string, redirect_uri*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/authorize`

---

#### `oauthCancelIntent` — Cancel a pending OAuth AuthIntent or 2FA temp_token

```typescript
client.api.authentication.oauthCancelIntent()
```

**Returns:** `void`  |  **HTTP:** `POST /api/v1/auth/intent/cancel`

---

#### `oauthDeviceAuthorize` — Start the device-leg OAuth (cookie + ticket gated)

```typescript
client.api.authentication.oauthDeviceAuthorize(ticket: string, provider: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `ticket` | `string` | query | Yes |  |
| `provider` | `string` | query | Yes |  |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/auth/device/authorize`

---

#### `oauthDeviceCode` — Start a device authorization flow (RFC-8628-inspired)

```typescript
client.api.authentication.oauthDeviceCode(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ client_name: string, client: string, code_challenge: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/device/code`

---

#### `oauthDeviceDeny` — Refuse the device ('Don't authorize')

```typescript
client.api.authentication.oauthDeviceDeny(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ ticket*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/device/deny`

---

#### `oauthDeviceLogin` — Password sign-in for the device authorize step (cookie + ticket gated)

```typescript
client.api.authentication.oauthDeviceLogin(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ ticket*: string, username: string, email: string, password*: string } (at least one of: username | email required)`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/device/login`

---

#### `oauthDeviceToken` — Poll for device-flow tokens (RFC-8628-inspired)

```typescript
client.api.authentication.oauthDeviceToken(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ device_code*: string, code_verifier: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/device/token`

---

#### `oauthDeviceVerifyCode` — Confirm a device user_code (verification page)

```typescript
client.api.authentication.oauthDeviceVerifyCode(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ user_code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/device/verify_code`

---

#### `oauthExchange` — Exchange a PKCE authorization code for tokens

```typescript
client.api.authentication.oauthExchange(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ code*: string, code_verifier*: string, redirect_uri*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/exchange`

---

#### `oauthLaunchInitiate` — Initiate OAuth popup-handoff launch

```typescript
client.api.authentication.oauthLaunchInitiate(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ provider*: "github" | "google", client: string, code_challenge*: string, state_id*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/launch/initiate`

---

#### `oauthLaunchStart` — Start OAuth popup-handoff via single-use ticket

```typescript
client.api.authentication.oauthLaunchStart(ticket: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `ticket` | `string` | query | Yes | One-shot ticket from /launch/initiate response |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/auth/launch/start`

---

#### `refreshToken` — Refresh access token

```typescript
client.api.authentication.refreshToken(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ refreshToken*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/auth/refresh`
**CLI:** `hoody auth refresh`

---

#### `resendVerification` — Resend verification email

```typescript
client.api.authentication.resendVerification(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ email*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/resend-verification`
**CLI:** `hoody auth email resend`

---

#### `resetPassword` — Reset password

```typescript
client.api.authentication.resetPassword(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ token*: string, password*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/reset-password`
**CLI:** `hoody auth password reset`

---

#### `signup` — Sign up with email and password

```typescript
client.api.authentication.signup(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ email*: string, password*: string, region: string, invite_code: string, client: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/signup`
**CLI:** `hoody auth signup`

---

#### `verifyEmail` — Verify email address

```typescript
client.api.authentication.verifyEmail(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ client: string, token*: string, response_mode: "intent" | "tokens", code_challenge: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/auth/verify-email`
**CLI:** `hoody auth email verify`

---

### `client.api.containers` (29) — Containers

#### `authorize` — Authorize Container Access

```typescript
client.api.containers.authorize(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID (24-char hex) |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/authorize`
**CLI:** `hoody containers authorize`

---

#### `copy` — Copy a container

```typescript
client.api.containers.copy(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the source container to copy |
| `data` | `object` | body | Yes |  |

**Body:** `{ target_project_id*: string, target_server_id: string, name: string, ssh_public_key: string|null, source_snapshot: string, copy_firewall_rules: bool=false, copy_network_rules: bool=false, kvm: bool, dev_kvm: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/copy`
**CLI:** `hoody containers copy`

---

#### `create` — Create a new container

```typescript
client.api.containers.create(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ server_id*: string, name: string, color: string, container_image: string|null, ai: bool=true, environment_vars: { [key: string]: string }, ssh_public_key: string|null, comment: string|null, hoody_kit: bool=true, dev_kit: bool, kvm: bool, dev_kvm: bool, autostart: bool=true, ramdisk: bool=true, cache: bool=true, cache_image: bool=false, prespawn: bool=false, bypass_prespawn: bool=false, realm_ids: string[] }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/projects/{id}/containers`
**CLI:** `hoody containers create`

---

#### `createSnapshot` — Create container snapshot

```typescript
client.api.containers.createSnapshot(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to create snapshot for |
| `data` | `object` | body | Yes |  |

**Body:** `{ alias: string, expiry: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/snapshots`
**CLI:** `hoody snapshots create`

---

#### `delete` — Delete a container

```typescript
client.api.containers.delete(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to delete |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}`
**CLI:** `hoody containers delete`

---

#### `deleteSnapshot` — Delete container snapshot

```typescript
client.api.containers.deleteSnapshot(id: string, name: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container |
| `name` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/snapshots/{name}`
**CLI:** `hoody snapshots delete`

---

#### `get` — Get a container by ID

```typescript
client.api.containers.get(runtime?: string, include_proxy_domains?: string, include_proxy_permissions?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `runtime` | `string` | query | No | Include live runtime information. Accepts "true", "false", or a URL-encoded JSON string like `{"displays":true}`. An empty JSON object `{}` fetches all info. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | `string` | query | No | Include proxy domains (aliases) for this container. When true, adds a proxy_domains array to the container object. |
| `include_proxy_permissions` | `string` | query | No | Include the full proxy-permissions documents (container-level proxy_permissions and parent-project-level project_proxy_permissions) for each container. Returns proxy authentication group configuration including credentials — request only when explicitly needed. Auth tokens additionally require the resources.proxy_aliases permission. |
| `id` | `string` | path | Yes | Unique identifier of the container to retrieve |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}`
**CLI:** `hoody containers get`

---

#### `getNetworkConfig` — Get container network configuration

```typescript
client.api.containers.getNetworkConfig(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to retrieve network configuration for |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/network`
**CLI:** `hoody network get`

---

#### `getStats` — Get container resource statistics

```typescript
client.api.containers.getStats(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/stats`
**CLI:** `hoody containers stats`

---

#### `getStatusLogs` — Get status logs for a container

```typescript
client.api.containers.getStatusLogs(page?: number, limit?: number, sort_by?: string, sort_order?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/status-logs`
**CLI:** `hoody containers status-logs`

---

#### `list` — Get all containers

```typescript
client.api.containers.list(page?: number, limit?: number, sort_by?: string, sort_order?: string, realm_id?: string, runtime?: string, include_proxy_domains?: string, include_proxy_permissions?: string, include_prespawn?: string, include_expired?: string, include_deleting?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No | Page number for pagination - starts from 1 |
| `limit` | `number` | query | No | Number of containers to return per page - maximum 100 items |
| `sort_by` | `string` | query | No | Field to sort containers by |
| `sort_order` | `string` | query | No | Sort direction - ascending or descending |
| `realm_id` | `string` | query | No | Filter by realm ID. Only returns containers that belong to this realm. Alternative to using realm subdomain in URL. |
| `runtime` | `string` | query | No | Include live runtime information. Accepts "true", "false", or a URL-encoded JSON string like `{"displays":true}`. An empty JSON object `{}` fetches all info. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | `string` | query | No | Include proxy domains (aliases) for each container. When true, adds a proxy_domains array to each container object. |
| `include_proxy_permissions` | `string` | query | No | Include the full proxy-permissions documents (container-level proxy_permissions and parent-project-level project_proxy_permissions) for each container. Returns proxy authentication group configuration including credentials — request only when explicitly needed. Auth tokens additionally require the resources.proxy_aliases permission. |
| `include_prespawn` | `string` | query | No | Include prespawn containers in the listing. By default, prespawn containers are excluded from results. |
| `include_expired` | `string` | query | No | Include containers that have expired due to server termination. By default, expired containers are excluded from results. |
| `include_deleting` | `string` | query | No | Include containers currently being deleted. By default, deleting containers are excluded from results. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/`
**CLI:** `hoody containers list`

---

#### `listAll` — Get all containers (collect all pages)

```typescript
client.api.containers.listAll(page?: number, limit?: number, sort_by?: string, sort_order?: string, realm_id?: string, runtime?: string, include_proxy_domains?: string, include_proxy_permissions?: string, include_prespawn?: string, include_expired?: string, include_deleting?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No | Page number for pagination - starts from 1 |
| `limit` | `number` | query | No | Number of containers to return per page - maximum 100 items |
| `sort_by` | `string` | query | No | Field to sort containers by |
| `sort_order` | `string` | query | No | Sort direction - ascending or descending |
| `realm_id` | `string` | query | No | Filter by realm ID. Only returns containers that belong to this realm. Alternative to using realm subdomain in URL. |
| `runtime` | `string` | query | No | Include live runtime information. Accepts "true", "false", or a URL-encoded JSON string like `{"displays":true}`. An empty JSON object `{}` fetches all info. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | `string` | query | No | Include proxy domains (aliases) for each container. When true, adds a proxy_domains array to each container object. |
| `include_proxy_permissions` | `string` | query | No | Include the full proxy-permissions documents (container-level proxy_permissions and parent-project-level project_proxy_permissions) for each container. Returns proxy authentication group configuration including credentials — request only when explicitly needed. Auth tokens additionally require the resources.proxy_aliases permission. |
| `include_prespawn` | `string` | query | No | Include prespawn containers in the listing. By default, prespawn containers are excluded from results. |
| `include_expired` | `string` | query | No | Include containers that have expired due to server termination. By default, expired containers are excluded from results. |
| `include_deleting` | `string` | query | No | Include containers currently being deleted. By default, deleting containers are excluded from results. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/containers/`
**CLI:** `hoody containers list`

---

#### `listByProject` — Get all containers for a project

```typescript
client.api.containers.listByProject(page?: number, limit?: number, sort_by?: string, sort_order?: string, runtime?: string, include_proxy_domains?: string, include_proxy_permissions?: string, include_prespawn?: string, include_deleting?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |
| `runtime` | `string` | query | No | Include live runtime information. Accepts "true", "false", or a URL-encoded JSON string like `{"displays":true}`. An empty JSON object `{}` fetches all info. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | `string` | query | No | Include proxy domains (aliases) for each container. When true, adds a proxy_domains array to each container object. |
| `include_proxy_permissions` | `string` | query | No | Include the full proxy-permissions documents (container-level proxy_permissions and parent-project-level project_proxy_permissions) for each container. Returns proxy authentication group configuration including credentials — request only when explicitly needed. Auth tokens additionally require the resources.proxy_aliases permission. |
| `include_prespawn` | `string` | query | No | Include prespawn containers in the listing. By default, prespawn containers are excluded. |
| `include_deleting` | `string` | query | No | Include containers currently being deleted. By default, deleting containers are excluded from results. |
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/projects/{id}/containers`
**CLI:** `hoody containers list`

---

#### `listByProjectAll` — Get all containers for a project (collect all pages)

```typescript
client.api.containers.listByProjectAll(page?: number, limit?: number, sort_by?: string, sort_order?: string, runtime?: string, include_proxy_domains?: string, include_proxy_permissions?: string, include_prespawn?: string, include_deleting?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |
| `runtime` | `string` | query | No | Include live runtime information. Accepts "true", "false", or a URL-encoded JSON string like `{"displays":true}`. An empty JSON object `{}` fetches all info. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | `string` | query | No | Include proxy domains (aliases) for each container. When true, adds a proxy_domains array to each container object. |
| `include_proxy_permissions` | `string` | query | No | Include the full proxy-permissions documents (container-level proxy_permissions and parent-project-level project_proxy_permissions) for each container. Returns proxy authentication group configuration including credentials — request only when explicitly needed. Auth tokens additionally require the resources.proxy_aliases permission. |
| `include_prespawn` | `string` | query | No | Include prespawn containers in the listing. By default, prespawn containers are excluded. |
| `include_deleting` | `string` | query | No | Include containers currently being deleted. By default, deleting containers are excluded from results. |
| `id` | `string` | path | Yes |  |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/projects/{id}/containers`
**CLI:** `hoody containers list`

---

#### `listByProjectIterator` — Get all containers for a project (async iterator)

```typescript
client.api.containers.listByProjectIterator(page?: number, limit?: number, sort_by?: string, sort_order?: string, runtime?: string, include_proxy_domains?: string, include_proxy_permissions?: string, include_prespawn?: string, include_deleting?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |
| `runtime` | `string` | query | No | Include live runtime information. Accepts "true", "false", or a URL-encoded JSON string like `{"displays":true}`. An empty JSON object `{}` fetches all info. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | `string` | query | No | Include proxy domains (aliases) for each container. When true, adds a proxy_domains array to each container object. |
| `include_proxy_permissions` | `string` | query | No | Include the full proxy-permissions documents (container-level proxy_permissions and parent-project-level project_proxy_permissions) for each container. Returns proxy authentication group configuration including credentials — request only when explicitly needed. Auth tokens additionally require the resources.proxy_aliases permission. |
| `include_prespawn` | `string` | query | No | Include prespawn containers in the listing. By default, prespawn containers are excluded. |
| `include_deleting` | `string` | query | No | Include containers currently being deleted. By default, deleting containers are excluded from results. |
| `id` | `string` | path | Yes |  |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/projects/{id}/containers`
**CLI:** `hoody containers list`

---

#### `listIterator` — Get all containers (async iterator)

```typescript
client.api.containers.listIterator(page?: number, limit?: number, sort_by?: string, sort_order?: string, realm_id?: string, runtime?: string, include_proxy_domains?: string, include_proxy_permissions?: string, include_prespawn?: string, include_expired?: string, include_deleting?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No | Page number for pagination - starts from 1 |
| `limit` | `number` | query | No | Number of containers to return per page - maximum 100 items |
| `sort_by` | `string` | query | No | Field to sort containers by |
| `sort_order` | `string` | query | No | Sort direction - ascending or descending |
| `realm_id` | `string` | query | No | Filter by realm ID. Only returns containers that belong to this realm. Alternative to using realm subdomain in URL. |
| `runtime` | `string` | query | No | Include live runtime information. Accepts "true", "false", or a URL-encoded JSON string like `{"displays":true}`. An empty JSON object `{}` fetches all info. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | `string` | query | No | Include proxy domains (aliases) for each container. When true, adds a proxy_domains array to each container object. |
| `include_proxy_permissions` | `string` | query | No | Include the full proxy-permissions documents (container-level proxy_permissions and parent-project-level project_proxy_permissions) for each container. Returns proxy authentication group configuration including credentials — request only when explicitly needed. Auth tokens additionally require the resources.proxy_aliases permission. |
| `include_prespawn` | `string` | query | No | Include prespawn containers in the listing. By default, prespawn containers are excluded from results. |
| `include_expired` | `string` | query | No | Include containers that have expired due to server termination. By default, expired containers are excluded from results. |
| `include_deleting` | `string` | query | No | Include containers currently being deleted. By default, deleting containers are excluded from results. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/containers/`
**CLI:** `hoody containers list`

---

#### `listSnapshots` — Get container snapshots

```typescript
client.api.containers.listSnapshots(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to retrieve snapshots for |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/snapshots`
**CLI:** `hoody snapshots list`

---

#### `listSnapshotsAll` — Get container snapshots (collect all pages)

```typescript
client.api.containers.listSnapshotsAll(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to retrieve snapshots for |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/containers/{id}/snapshots`
**CLI:** `hoody snapshots list`

---

#### `listSnapshotsIterator` — Get container snapshots (async iterator)

```typescript
client.api.containers.listSnapshotsIterator(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to retrieve snapshots for |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/containers/{id}/snapshots`
**CLI:** `hoody snapshots list`

---

#### `manage` — Manage container

```typescript
client.api.containers.manage(operation: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `operation` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/{operation}`
**CLI:** `hoody containers manage`

---

#### `removeNetworkConfig` — Remove container network configuration

```typescript
client.api.containers.removeNetworkConfig(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to remove network configuration from |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/network`
**CLI:** `hoody network delete`

---

#### `restoreSnapshot` — Restore container from snapshot

```typescript
client.api.containers.restoreSnapshot(id: string, name: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to restore |
| `name` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/snapshots/{name}`
**CLI:** `hoody snapshots restore`

---

#### `setContainerKvm` — Enable or disable /dev/kvm (run VMs in the container)

```typescript
client.api.containers.setContainerKvm(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container |
| `data` | `object` | body | Yes |  |

**Body:** `{ kvm: bool, dev_kvm: bool }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/kvm`
**CLI:** `hoody containers kvm`

---

#### `startNetwork` — Start container network proxy/blocking

```typescript
client.api.containers.startNetwork(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to start network for |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/network/start`
**CLI:** `hoody network start`

---

#### `stopNetwork` — Stop container network proxy/blocking

```typescript
client.api.containers.stopNetwork(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to stop network for |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/network/stop`
**CLI:** `hoody network stop`

---

#### `sync` — Sync a copied container with its source

```typescript
client.api.containers.sync(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to sync (must have been created via copy) |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/sync`
**CLI:** `hoody containers sync`

---

#### `update` — Update a container

```typescript
client.api.containers.update(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to update |
| `data` | `object` | body | Yes |  |

**Body:** `{ name: string, color: string, ai: bool, autostart: bool, ramdisk_scope: "container" | "project", ramdisk: bool, environment_vars: { [key: string]: string }, ssh_public_key: string|null, comment: string|null, realm_ids: string[] }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}`
**CLI:** `hoody containers update`

---

#### `updateNetworkConfig` — Update container network configuration

```typescript
client.api.containers.updateNetworkConfig(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container to configure network for |
| `data` | `object` | body | Yes |  |

**Body:** `{ type*: "socks5" | "http" | "https" | "block", proxy: string, country: string, city: string, region: string, comment: string, dns_servers: string[] }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/network`
**CLI:** `hoody network update`

---

#### `updateSnapshotAlias` — Update snapshot alias

```typescript
client.api.containers.updateSnapshotAlias(id: string, name: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container |
| `name` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ alias*: string|null }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/snapshots/{name}/alias`
**CLI:** `hoody snapshots update-alias`

---

### `client.api.env` (4) — Container Environment

#### `bulkSet` — Bulk set container environment variables

```typescript
client.api.env.bulkSet(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ [key: string]: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/env`
**CLI:** `hoody containers env bulk-set`

---

#### `delete` — Delete a single environment variable

```typescript
client.api.env.delete(id: string, key: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `key` | `string` | path | Yes | Environment variable key |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/env/{key}`
**CLI:** `hoody containers env delete`

---

#### `list` — List container environment variables

```typescript
client.api.env.list(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/env`
**CLI:** `hoody containers env list`

---

#### `set` — Set a single environment variable

```typescript
client.api.env.set(id: string, key: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `key` | `string` | path | Yes | Environment variable key |
| `data` | `object` | body | Yes |  |

**Body:** `{ value*: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/env/{key}`
**CLI:** `hoody containers env set`

---

### `client.api.events` (8) — Events

#### `bulkDelete` — Bulk delete events

```typescript
client.api.events.bulkDelete(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ event_type: "container.creating" | "container.running" | "container.stopped" | "container.failed" | "container.deleting" | "container.deleted" | "container.autostart_enabled" | "container.autostart_disabled" | …(69 values), resource_type: "container" | "storage_share" | "notification" | "project" | "server" | "firewall" | "proxy_alias" | "proxy_permissions" | …(12 values), resource_id: string, before_date: string, realm_id: string }`

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/events`
**CLI:** `hoody events bulk-delete`

---

#### `cleanup` — Cleanup old events

```typescript
client.api.events.cleanup(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ retention_days*: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/events/cleanup`
**CLI:** `hoody events cleanup`

---

#### `delete` — Delete a single event

```typescript
client.api.events.delete(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Event ID to delete |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/events/{id}`
**CLI:** `hoody events delete`

---

#### `get` — Get event details by ID

```typescript
client.api.events.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Event ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/events/{id}`
**CLI:** `hoody events get`

---

#### `getStats` — Get event statistics

```typescript
client.api.events.getStats(start_date?: string, end_date?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `start_date` | `string` | query | No | Start of time range |
| `end_date` | `string` | query | No | End of time range |
| `realm_id` | `string` | query | No | Filter by realm |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/events/stats`
**CLI:** `hoody events stats`

---

#### `list` — List event history

```typescript
client.api.events.list(limit?: integer, offset?: integer, sort_by?: string, sort_order?: string, event_type?: string, resource_type?: string, resource_id?: string, project_id?: string, container_id?: string, start_date?: string, end_date?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No | Number of events to return (max 500) |
| `offset` | `integer` | query | No | Number of events to skip |
| `sort_by` | `string` | query | No | Field to sort by |
| `sort_order` | `string` | query | No | Sort direction |
| `event_type` | `string` | query | No | Filter by specific event type |
| `resource_type` | `string` | query | No | Filter by resource type |
| `resource_id` | `string` | query | No | Filter by specific resource ID |
| `project_id` | `string` | query | No | Filter by project ID |
| `container_id` | `string` | query | No | Filter by container ID |
| `start_date` | `string` | query | No | Filter events after this timestamp |
| `end_date` | `string` | query | No | Filter events before this timestamp |
| `realm_id` | `string` | query | No | Filter by realm ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/events`
**CLI:** `hoody events list`

---

#### `listAll` — List event history (collect all pages)

```typescript
client.api.events.listAll(limit?: integer, offset?: integer, sort_by?: string, sort_order?: string, event_type?: string, resource_type?: string, resource_id?: string, project_id?: string, container_id?: string, start_date?: string, end_date?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No | Number of events to return (max 500) |
| `offset` | `integer` | query | No | Number of events to skip |
| `sort_by` | `string` | query | No | Field to sort by |
| `sort_order` | `string` | query | No | Sort direction |
| `event_type` | `string` | query | No | Filter by specific event type |
| `resource_type` | `string` | query | No | Filter by resource type |
| `resource_id` | `string` | query | No | Filter by specific resource ID |
| `project_id` | `string` | query | No | Filter by project ID |
| `container_id` | `string` | query | No | Filter by container ID |
| `start_date` | `string` | query | No | Filter events after this timestamp |
| `end_date` | `string` | query | No | Filter events before this timestamp |
| `realm_id` | `string` | query | No | Filter by realm ID |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/events`
**CLI:** `hoody events list`

---

#### `listIterator` — List event history (async iterator)

```typescript
client.api.events.listIterator(limit?: integer, offset?: integer, sort_by?: string, sort_order?: string, event_type?: string, resource_type?: string, resource_id?: string, project_id?: string, container_id?: string, start_date?: string, end_date?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No | Number of events to return (max 500) |
| `offset` | `integer` | query | No | Number of events to skip |
| `sort_by` | `string` | query | No | Field to sort by |
| `sort_order` | `string` | query | No | Sort direction |
| `event_type` | `string` | query | No | Filter by specific event type |
| `resource_type` | `string` | query | No | Filter by resource type |
| `resource_id` | `string` | query | No | Filter by specific resource ID |
| `project_id` | `string` | query | No | Filter by project ID |
| `container_id` | `string` | query | No | Filter by container ID |
| `start_date` | `string` | query | No | Filter events after this timestamp |
| `end_date` | `string` | query | No | Filter events before this timestamp |
| `realm_id` | `string` | query | No | Filter by realm ID |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/events`
**CLI:** `hoody events list`

---

### `client.api.firewall` (10) — Container Firewall

#### `addEgressRule` — Add Egress Rule

```typescript
client.api.firewall.addEgressRule(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ action*: "allow" | "reject" | "drop", protocol*: "tcp" | "udp" | "icmp4", description*: string, destination_port: string, destination: string, source_port: string, state: "enabled" | "disabled", icmp_type: string, icmp_code: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/firewall/egress`
**CLI:** `hoody firewall egress create`

---

#### `addIngressRule` — Add Ingress Rule

```typescript
client.api.firewall.addIngressRule(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ action*: "allow" | "reject" | "drop", protocol*: "tcp" | "udp" | "icmp4", description*: string, destination_port: string, source: string, source_port: string, state: "enabled" | "disabled", icmp_type: string, icmp_code: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/firewall/ingress`
**CLI:** `hoody firewall ingress create`

---

#### `list` — List container firewall rules

```typescript
client.api.firewall.list(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/firewall/rules`
**CLI:** `hoody firewall list`

---

#### `listAll` — List container firewall rules (collect all pages)

```typescript
client.api.firewall.listAll(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/containers/{id}/firewall/rules`
**CLI:** `hoody firewall list`

---

#### `listIterator` — List container firewall rules (async iterator)

```typescript
client.api.firewall.listIterator(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/containers/{id}/firewall/rules`
**CLI:** `hoody firewall list`

---

#### `removeEgressRule` — Remove Egress Rule(s)

```typescript
client.api.firewall.removeEgressRule(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ all: bool, action: "allow" | "reject" | "drop", protocol: "tcp" | "udp" | "icmp4", destination_port: string, destination: string, source_port: string, description: string, state: "enabled" | "disabled"="enabled", icmp_type: string, icmp_code: string }`

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/firewall/egress`
**CLI:** `hoody firewall egress delete`

---

#### `removeIngressRule` — Remove Ingress Rule(s)

```typescript
client.api.firewall.removeIngressRule(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ all: bool, action: "allow" | "reject" | "drop", protocol: "tcp" | "udp" | "icmp4", destination_port: string, source: string, source_port: string, description: string, state: "enabled" | "disabled"="enabled", icmp_type: string, icmp_code: string }`

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/firewall/ingress`
**CLI:** `hoody firewall ingress delete`

---

#### `reset` — Reset container firewall

```typescript
client.api.firewall.reset(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/firewall/reset`
**CLI:** `hoody firewall reset`

---

#### `toggleEgressRule` — Toggle Egress Rule State

```typescript
client.api.firewall.toggleEgressRule(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ state*: "enabled" | "disabled", action: "allow" | "reject" | "drop", protocol: "tcp" | "udp" | "icmp4", destination_port: string, source_port: string, destination: string, description: string, icmp_type: string, icmp_code: string }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/containers/{id}/firewall/egress`
**CLI:** `hoody firewall egress toggle`

---

#### `toggleIngressRule` — Toggle Ingress Rule State

```typescript
client.api.firewall.toggleIngressRule(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ state*: "enabled" | "disabled", action: "allow" | "reject" | "drop", protocol: "tcp" | "udp" | "icmp4", destination_port: string, source_port: string, source: string, description: string, icmp_type: string, icmp_code: string }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/containers/{id}/firewall/ingress`
**CLI:** `hoody firewall ingress toggle`

---

### `client.api.images` (11) — Container Images

#### `getDetails` — Get public image details

```typescript
client.api.images.getDetails(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the public container image to retrieve details for |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/images/public/{id}`
**CLI:** `hoody images get`

---

#### `getIcon` — Get image icon

```typescript
client.api.images.getIcon(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container image to retrieve icon for |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/images/{id}/icon`
**CLI:** `hoody images icon`

---

#### `importFree` — Import free image

```typescript
client.api.images.importFree(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the public container image to import |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/images/import/{id}`
**CLI:** `hoody images import-free`

---

#### `list` — List user images

```typescript
client.api.images.list(page?: integer, limit?: integer, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number for pagination - starts from 1 |
| `limit` | `integer` | query | No | Number of images to return per page - maximum 100 items |
| `sort_by` | `string` | query | No | Field to sort user images by - currently only supports creation date |
| `sort_order` | `string` | query | No | Sort direction - ascending or descending |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/images/user`
**CLI:** `hoody images mine`

---

#### `listAll` — List user images (collect all pages)

```typescript
client.api.images.listAll(page?: integer, limit?: integer, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number for pagination - starts from 1 |
| `limit` | `integer` | query | No | Number of images to return per page - maximum 100 items |
| `sort_by` | `string` | query | No | Field to sort user images by - currently only supports creation date |
| `sort_order` | `string` | query | No | Sort direction - ascending or descending |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/images/user`
**CLI:** `hoody images mine`

---

#### `listIterator` — List user images (async iterator)

```typescript
client.api.images.listIterator(page?: integer, limit?: integer, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number for pagination - starts from 1 |
| `limit` | `integer` | query | No | Number of images to return per page - maximum 100 items |
| `sort_by` | `string` | query | No | Field to sort user images by - currently only supports creation date |
| `sort_order` | `string` | query | No | Sort direction - ascending or descending |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/images/user`
**CLI:** `hoody images mine`

---

#### `listPublic` — List public images

```typescript
client.api.images.listPublic(os?: string, architecture?: string, min_price?: number, max_price?: number, min_rating?: number, max_rating?: number, search?: string, page?: integer, limit?: integer, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `os` | `string` | query | No | Filter images by operating system - e.g., debian (the platform currently carries debian/13 only) |
| `architecture` | `string` | query | No | Filter images by CPU architecture - e.g., amd64, arm64, armhf |
| `min_price` | `number` | query | No | Minimum price filter for paid images - 0 includes free images |
| `max_price` | `number` | query | No | Maximum price filter for paid images - useful for budget constraints |
| `min_rating` | `number` | query | No | Minimum average rating filter - filters images with rating >= this value (0-5 stars) |
| `max_rating` | `number` | query | No | Maximum average rating filter - filters images with rating <= this value (0-5 stars) |
| `search` | `string` | query | No | Search term to filter images by name, description, or tags |
| `page` | `integer` | query | No | Page number for pagination - starts from 1 |
| `limit` | `integer` | query | No | Number of images to return per page - maximum 100 items |
| `sort_by` | `string` | query | No | Field to sort images by - name, date added, price, or average rating |
| `sort_order` | `string` | query | No | Sort direction - ascending or descending |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/images/public`
**CLI:** `hoody images list`

---

#### `listPublicAll` — List public images (collect all pages)

```typescript
client.api.images.listPublicAll(os?: string, architecture?: string, min_price?: number, max_price?: number, min_rating?: number, max_rating?: number, search?: string, page?: integer, limit?: integer, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `os` | `string` | query | No | Filter images by operating system - e.g., debian (the platform currently carries debian/13 only) |
| `architecture` | `string` | query | No | Filter images by CPU architecture - e.g., amd64, arm64, armhf |
| `min_price` | `number` | query | No | Minimum price filter for paid images - 0 includes free images |
| `max_price` | `number` | query | No | Maximum price filter for paid images - useful for budget constraints |
| `min_rating` | `number` | query | No | Minimum average rating filter - filters images with rating >= this value (0-5 stars) |
| `max_rating` | `number` | query | No | Maximum average rating filter - filters images with rating <= this value (0-5 stars) |
| `search` | `string` | query | No | Search term to filter images by name, description, or tags |
| `page` | `integer` | query | No | Page number for pagination - starts from 1 |
| `limit` | `integer` | query | No | Number of images to return per page - maximum 100 items |
| `sort_by` | `string` | query | No | Field to sort images by - name, date added, price, or average rating |
| `sort_order` | `string` | query | No | Sort direction - ascending or descending |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/images/public`
**CLI:** `hoody images list`

---

#### `listPublicIterator` — List public images (async iterator)

```typescript
client.api.images.listPublicIterator(os?: string, architecture?: string, min_price?: number, max_price?: number, min_rating?: number, max_rating?: number, search?: string, page?: integer, limit?: integer, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `os` | `string` | query | No | Filter images by operating system - e.g., debian (the platform currently carries debian/13 only) |
| `architecture` | `string` | query | No | Filter images by CPU architecture - e.g., amd64, arm64, armhf |
| `min_price` | `number` | query | No | Minimum price filter for paid images - 0 includes free images |
| `max_price` | `number` | query | No | Maximum price filter for paid images - useful for budget constraints |
| `min_rating` | `number` | query | No | Minimum average rating filter - filters images with rating >= this value (0-5 stars) |
| `max_rating` | `number` | query | No | Maximum average rating filter - filters images with rating <= this value (0-5 stars) |
| `search` | `string` | query | No | Search term to filter images by name, description, or tags |
| `page` | `integer` | query | No | Page number for pagination - starts from 1 |
| `limit` | `integer` | query | No | Number of images to return per page - maximum 100 items |
| `sort_by` | `string` | query | No | Field to sort images by - name, date added, price, or average rating |
| `sort_order` | `string` | query | No | Sort direction - ascending or descending |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/images/public`
**CLI:** `hoody images list`

---

#### `purchase` — Purchase image

```typescript
client.api.images.purchase(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the paid container image to purchase |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/images/purchase/{id}`
**CLI:** `hoody images purchase`

---

#### `rate` — Rate image

```typescript
client.api.images.rate(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the container image to rate |
| `data` | `object` | body | Yes |  |

**Body:** `{ rating*: number }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/images/rate/{id}`
**CLI:** `hoody images rate`

---

### `client.api.meta` (2) — Meta

#### `getPublicKey` — Get Hoody API Signing Public Key

```typescript
client.api.meta.getPublicKey()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/meta/public-key`
**CLI:** `hoody meta get`

---

#### `getSocialStats` — Get Hoody Social Counters

```typescript
client.api.meta.getSocialStats()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/meta/social-stats`

---

### `client.api.notifications` (8) — Notifications

#### `list` — Get all notifications for the authenticated user

```typescript
client.api.notifications.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notifications/`
**CLI:** `hoody inbox list`

---

#### `listAll` — Get all notifications for the authenticated user (collect all pages)

```typescript
client.api.notifications.listAll()
```

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/notifications/`
**CLI:** `hoody inbox list`

---

#### `listIterator` — Get all notifications for the authenticated user (async iterator)

```typescript
client.api.notifications.listIterator()
```

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/notifications/`
**CLI:** `hoody inbox list`

---

#### `listPublic` — Get all public notifications

```typescript
client.api.notifications.listPublic()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notifications/public`
**CLI:** `hoody inbox list-public`

---

#### `listPublicAll` — Get all public notifications (collect all pages)

```typescript
client.api.notifications.listPublicAll()
```

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/notifications/public`
**CLI:** `hoody inbox list-public`

---

#### `listPublicIterator` — Get all public notifications (async iterator)

```typescript
client.api.notifications.listPublicIterator()
```

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/notifications/public`
**CLI:** `hoody inbox list-public`

---

#### `markAllRead` — Mark all notifications as read

```typescript
client.api.notifications.markAllRead()
```

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/notifications/read-all`
**CLI:** `hoody inbox mark-all`

---

#### `markRead` — Mark a notification as read

```typescript
client.api.notifications.markRead(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the notification to mark as read |

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/notifications/{id}/read`
**CLI:** `hoody inbox mark`

---

### `client.api.poolInvitations` (3) — Pool Invitations

#### `accept` — Accept invitation

```typescript
client.api.poolInvitations.accept(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/pools/{id}/accept`
**CLI:** `hoody pools invitations accept`

---

#### `list` — List pending invitations

```typescript
client.api.poolInvitations.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/pools/invitations/pending`
**CLI:** `hoody pools invitations list`

---

#### `reject` — Reject invitation

```typescript
client.api.poolInvitations.reject(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/pools/{id}/reject`
**CLI:** `hoody pools invitations reject`

---

### `client.api.poolMembers` (3) — Pool Members

#### `invite` — Invite member

```typescript
client.api.poolMembers.invite(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ username*: string, role*: "admin" | "user" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/pools/{id}/members`
**CLI:** `hoody pools members invite`

---

#### `remove` — Remove member

```typescript
client.api.poolMembers.remove(id: string, userId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `userId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/pools/{id}/members/{userId}`
**CLI:** `hoody pools members delete`

---

#### `updateRole` — Update member role

```typescript
client.api.poolMembers.updateRole(id: string, userId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `userId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ role*: "admin" | "user" }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/pools/{id}/members/{userId}`
**CLI:** `hoody pools members update-role`

---

### `client.api.pools` (7) — Pools

#### `create` — Create pool

```typescript
client.api.pools.create(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ name*: string, description: string, settings: object }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/pools`
**CLI:** `hoody pools create`

---

#### `delete` — Delete pool

```typescript
client.api.pools.delete(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/pools/{id}`
**CLI:** `hoody pools delete`

---

#### `get` — Get pool details

```typescript
client.api.pools.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/pools/{id}`
**CLI:** `hoody pools get`

---

#### `list` — List user pools

```typescript
client.api.pools.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/pools`
**CLI:** `hoody pools list`

---

#### `listAll` — List user pools (collect all pages)

```typescript
client.api.pools.listAll()
```

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/pools`
**CLI:** `hoody pools list`

---

#### `listIterator` — List user pools (async iterator)

```typescript
client.api.pools.listIterator()
```

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/pools`
**CLI:** `hoody pools list`

---

#### `update` — Update pool

```typescript
client.api.pools.update(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string, settings: object }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/pools/{id}`
**CLI:** `hoody pools update`

---

### `client.api.projects` (14) — Projects

#### `addPermission` — Grant project access

```typescript
client.api.projects.addPermission(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ user_id*: string, permission_level*: "read" | "edit" | "delete" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/projects/{id}/permissions`
**CLI:** `hoody projects permissions create`

---

#### `create` — Create a new project

```typescript
client.api.projects.create(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ alias*: string, color: string, max_containers: number|null, realm_ids: string[] }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/projects/`
**CLI:** `hoody projects create`

---

#### `delete` — Delete project

```typescript
client.api.projects.delete(include_deleted_items?: boolean, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `include_deleted_items` | `boolean` | query | No | Include a lightweight list of deleted container IDs/names in the response for confirmation UX. |
| `id` | `string` | path | Yes | Project ID to delete |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/projects/{id}`
**CLI:** `hoody projects delete`

---

#### `get` — Get project by ID

```typescript
client.api.projects.get(include_permissions?: boolean, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `include_permissions` | `boolean` | query | No | Include project permissions with user details in response |
| `id` | `string` | path | Yes | Project ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/projects/{id}`
**CLI:** `hoody projects get`

---

#### `getStats` — Get statistics for all containers in a project

```typescript
client.api.projects.getStats(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique identifier of the project |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/projects/{id}/stats`
**CLI:** `hoody projects stats`

---

#### `list` — List all projects

```typescript
client.api.projects.list(page?: number, limit?: number, sort_by?: string, sort_order?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No | Page number (1-based) |
| `limit` | `number` | query | No | Items per page (max 100) |
| `sort_by` | `string` | query | No | Field to sort by |
| `sort_order` | `string` | query | No | Sort direction |
| `realm_id` | `string` | query | No | Filter by realm ID. Only returns projects that belong to this realm. Alternative to using realm subdomain in URL. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/projects/`
**CLI:** `hoody projects list`

---

#### `listAll` — List all projects (collect all pages)

```typescript
client.api.projects.listAll(page?: number, limit?: number, sort_by?: string, sort_order?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No | Page number (1-based) |
| `limit` | `number` | query | No | Items per page (max 100) |
| `sort_by` | `string` | query | No | Field to sort by |
| `sort_order` | `string` | query | No | Sort direction |
| `realm_id` | `string` | query | No | Filter by realm ID. Only returns projects that belong to this realm. Alternative to using realm subdomain in URL. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/projects/`
**CLI:** `hoody projects list`

---

#### `listIterator` — List all projects (async iterator)

```typescript
client.api.projects.listIterator(page?: number, limit?: number, sort_by?: string, sort_order?: string, realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No | Page number (1-based) |
| `limit` | `number` | query | No | Items per page (max 100) |
| `sort_by` | `string` | query | No | Field to sort by |
| `sort_order` | `string` | query | No | Sort direction |
| `realm_id` | `string` | query | No | Filter by realm ID. Only returns projects that belong to this realm. Alternative to using realm subdomain in URL. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/projects/`
**CLI:** `hoody projects list`

---

#### `listPermissions` — List project permissions

```typescript
client.api.projects.listPermissions(page?: number, limit?: number, sort_by?: string, sort_order?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |
| `id` | `string` | path | Yes | Project ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/projects/{id}/permissions`
**CLI:** `hoody projects permissions list`

---

#### `listPermissionsAll` — List project permissions (collect all pages)

```typescript
client.api.projects.listPermissionsAll(page?: number, limit?: number, sort_by?: string, sort_order?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |
| `id` | `string` | path | Yes | Project ID |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/projects/{id}/permissions`
**CLI:** `hoody projects permissions list`

---

#### `listPermissionsIterator` — List project permissions (async iterator)

```typescript
client.api.projects.listPermissionsIterator(page?: number, limit?: number, sort_by?: string, sort_order?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |
| `id` | `string` | path | Yes | Project ID |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/projects/{id}/permissions`
**CLI:** `hoody projects permissions list`

---

#### `removePermission` — Revoke project access

```typescript
client.api.projects.removePermission(id: string, permissionId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |
| `permissionId` | `string` | path | Yes | Permission ID to remove |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/projects/{id}/permissions/{permissionId}`
**CLI:** `hoody projects permissions delete`

---

#### `update` — Update project

```typescript
client.api.projects.update(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID to update |
| `data` | `object` | body | Yes |  |

**Body:** `{ alias*: string, color: string, max_containers: number|null, realm_ids: string[] }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/projects/{id}`
**CLI:** `hoody projects update`

---

#### `updatePermission` — Update project permission

```typescript
client.api.projects.updatePermission(id: string, permissionId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |
| `permissionId` | `string` | path | Yes | Permission ID to update |
| `data` | `object` | body | Yes |  |

**Body:** `{ permission_level*: "read" | "edit" | "delete" }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/projects/{id}/permissions/{permissionId}`
**CLI:** `hoody projects permissions update`

---

### `client.api.proxyAliases` (8) — Proxy Aliases

#### `create` — Create a new proxy alias

```typescript
client.api.proxyAliases.create(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ container_id*: string, alias: string | null | false, program*: string, port: int, index: int, target_path: string|null, allow_path_override: bool=true, expires_at: string|null, enabled: bool=true }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/proxy/aliases`
**CLI:** `hoody proxy create`

---

#### `delete` — Delete proxy alias

```typescript
client.api.proxyAliases.delete(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Proxy alias ID to delete |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/proxy/aliases/{id}`
**CLI:** `hoody proxy delete`

---

#### `get` — Get proxy alias by ID

```typescript
client.api.proxyAliases.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Proxy alias ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/proxy/aliases/{id}`
**CLI:** `hoody proxy get`

---

#### `list` — List proxy aliases

```typescript
client.api.proxyAliases.list(project_id?: string, container_id?: string, realm_id?: string, enabled?: string, expired?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `project_id` | `string` | query | No | Filter by project ID |
| `container_id` | `string` | query | No | Filter by container ID |
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |
| `enabled` | `string` | query | No | Filter by enabled status |
| `expired` | `string` | query | No | Filter by expiration: "true" = only expired, "false" = only non-expired |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/proxy/aliases`
**CLI:** `hoody proxy list`

---

#### `listAll` — List proxy aliases (collect all pages)

```typescript
client.api.proxyAliases.listAll(project_id?: string, container_id?: string, realm_id?: string, enabled?: string, expired?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `project_id` | `string` | query | No | Filter by project ID |
| `container_id` | `string` | query | No | Filter by container ID |
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |
| `enabled` | `string` | query | No | Filter by enabled status |
| `expired` | `string` | query | No | Filter by expiration: "true" = only expired, "false" = only non-expired |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/proxy/aliases`
**CLI:** `hoody proxy list`

---

#### `listIterator` — List proxy aliases (async iterator)

```typescript
client.api.proxyAliases.listIterator(project_id?: string, container_id?: string, realm_id?: string, enabled?: string, expired?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `project_id` | `string` | query | No | Filter by project ID |
| `container_id` | `string` | query | No | Filter by container ID |
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |
| `enabled` | `string` | query | No | Filter by enabled status |
| `expired` | `string` | query | No | Filter by expiration: "true" = only expired, "false" = only non-expired |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/proxy/aliases`
**CLI:** `hoody proxy list`

---

#### `setState` — Enable or disable proxy alias

```typescript
client.api.proxyAliases.setState(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Proxy alias ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ enabled*: bool }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/proxy/aliases/{id}/state`
**CLI:** `hoody proxy set-state`

---

#### `update` — Update proxy alias

```typescript
client.api.proxyAliases.update(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Proxy alias ID to update |
| `data` | `object` | body | Yes |  |

**Body:** `{ alias: string, program: string, port: int, index: int, target_path: string|null, allow_path_override: bool, expires_at: string | number | null, enabled: bool }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/proxy/aliases/{id}`
**CLI:** `hoody proxy update`

---

### `client.api.proxyDiscovery` (5) — Proxy Discovery

#### `getContainerProxyService` — Get merged proxy view for a service

```typescript
client.api.proxyDiscovery.getContainerProxyService(id: string, service: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `service` | `string` | path | Yes | Service name |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/proxy/services/{service}`
**CLI:** `hoody containers proxy discovery services get`

---

#### `getContainerProxySettings` — Get container proxy root settings

```typescript
client.api.proxyDiscovery.getContainerProxySettings(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/proxy/settings`
**CLI:** `hoody containers proxy settings get`

---

#### `listContainerProxyGroups` — List container proxy groups

```typescript
client.api.proxyDiscovery.listContainerProxyGroups(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/proxy/groups`
**CLI:** `hoody containers proxy discovery groups list`

---

#### `listContainerProxyServices` — List services referenced in proxy config

```typescript
client.api.proxyDiscovery.listContainerProxyServices(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/proxy/services`
**CLI:** `hoody containers proxy discovery services list`

---

#### `updateContainerProxySettings` — Update container proxy root settings

```typescript
client.api.proxyDiscovery.updateContainerProxySettings(id: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `if-match` | `string` | header | No | file:v<N> ETag precondition |
| `data` | `object` | body | Yes |  |

**Body:** `{ enable_proxy: bool, default: "allow" | "deny" }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/proxy/settings`
**CLI:** `hoody containers proxy settings update`

---

### `client.api.proxyHooks` (8) — Proxy Hooks

#### `addContainerProxyHook` — Append or insert a new hook

```typescript
client.api.proxyHooks.addContainerProxyHook(id: string, service: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `service` | `string` | path | Yes | Service name |
| `if-match` | `string` | header | No | file:v<N> ETag precondition |
| `data` | `object` | body | Yes |  |

**Body:** `{ match*: { method: string | string[], path: string, headers: object }, script*: { subdomain: string, execId: string, path*: string }, timeout: int, applies_to: { groups: string[] }, position: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/proxy/hooks/{service}`
**CLI:** `hoody containers proxy hooks create`

---

#### `clearContainerProxyServiceHooks` — Clear all hooks for a service

```typescript
client.api.proxyHooks.clearContainerProxyServiceHooks(id: string, service: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `service` | `string` | path | Yes | Service name |
| `if-match` | `string` | header | No | file:v<N> ETag precondition |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/proxy/hooks/{service}`
**CLI:** `hoody containers proxy hooks clear-service`

---

#### `getContainerProxyHook` — Get a single hook by id

```typescript
client.api.proxyHooks.getContainerProxyHook(id: string, service: string, hookId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `service` | `string` | path | Yes | Service name |
| `hookId` | `string` | path | Yes | 26-char Crockford base32 ULID (lowercase) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/proxy/hooks/{service}/{hookId}`
**CLI:** `hoody containers proxy hooks get`

---

#### `listContainerProxyHooks` — List all proxy hooks for a container

```typescript
client.api.proxyHooks.listContainerProxyHooks(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/proxy/hooks`
**CLI:** `hoody containers proxy hooks list`

---

#### `listContainerProxyServiceHooks` — List hooks for a specific service

```typescript
client.api.proxyHooks.listContainerProxyServiceHooks(id: string, service: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `service` | `string` | path | Yes | Service name |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/proxy/hooks/{service}`
**CLI:** `hoody containers proxy hooks list-service`

---

#### `moveContainerProxyHook` — Move a hook to a new position

```typescript
client.api.proxyHooks.moveContainerProxyHook(id: string, service: string, hookId: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `service` | `string` | path | Yes | Service name |
| `hookId` | `string` | path | Yes | 26-char Crockford base32 ULID (lowercase) |
| `if-match` | `string` | header | No | file:v<N> ETag precondition |
| `data` | `object` | body | Yes |  |

**Body:** `{ position*: int }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/containers/{id}/proxy/hooks/{service}/{hookId}/position`
**CLI:** `hoody containers proxy hooks move`

---

#### `removeContainerProxyHook` — Remove a hook

```typescript
client.api.proxyHooks.removeContainerProxyHook(id: string, service: string, hookId: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `service` | `string` | path | Yes | Service name |
| `hookId` | `string` | path | Yes | 26-char Crockford base32 ULID (lowercase) |
| `if-match` | `string` | header | No | file:v<N> ETag precondition |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/proxy/hooks/{service}/{hookId}`
**CLI:** `hoody containers proxy hooks delete`

---

#### `updateContainerProxyHook` — Replace a hook in place

```typescript
client.api.proxyHooks.updateContainerProxyHook(id: string, service: string, hookId: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `service` | `string` | path | Yes | Service name |
| `hookId` | `string` | path | Yes | 26-char Crockford base32 ULID (lowercase) |
| `if-match` | `string` | header | No | file:v<N> ETag precondition |
| `data` | `object` | body | Yes |  |

**Body:** `{ match*: { method: string | string[], path: string, headers: object }, script*: { subdomain: string, execId: string, path*: string }, timeout: int, applies_to: { groups: string[] }, position: int }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/proxy/hooks/{service}/{hookId}`
**CLI:** `hoody containers proxy hooks update`

---

### `client.api.proxyPermissionsContainer` (13) — Proxy Permissions Container

#### `delete` — Delete container proxy permissions

```typescript
client.api.proxyPermissionsContainer.delete(id: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/proxy/permissions`
**CLI:** `hoody containers proxy permissions delete`

---

#### `get` — Get container proxy permissions

```typescript
client.api.proxyPermissionsContainer.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/proxy/permissions`
**CLI:** `hoody containers proxy permissions get`

---

#### `removeAuthGroup` — Remove container authentication group

```typescript
client.api.proxyPermissionsContainer.removeAuthGroup(id: string, groupName: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `groupName` | `string` | path | Yes | Group name to remove |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/proxy/permissions/groups/{groupName}`
**CLI:** `hoody containers proxy groups delete`

---

#### `removeGroup` — Remove all program permissions for a container group

```typescript
client.api.proxyPermissionsContainer.removeGroup(id: string, groupName: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `groupName` | `string` | path | Yes | Group name |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/proxy/permissions/permissions/{groupName}`
**CLI:** `hoody containers proxy groups permissions clear`

---

#### `removeProgram` — Remove a single program permission for a container group

```typescript
client.api.proxyPermissionsContainer.removeProgram(id: string, groupName: string, program: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `groupName` | `string` | path | Yes | Group name |
| `program` | `string` | path | Yes | Program name (e.g., http, ssh, files) |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/containers/{id}/proxy/permissions/permissions/{groupName}/{program}`
**CLI:** `hoody containers proxy groups permissions delete`

---

#### `replace` — Replace container proxy permissions JSON

```typescript
client.api.proxyPermissionsContainer.replace(id: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ project*: string, container*: string, groups*: { [key: string]: { type: "jwt" | "password" | "ip" | "token" | "hoody-identity", secret: string, algorithm: "HS256" | "RS256" | "ES256" | "sha256", sources: string[], claims: object, username: string, password: string, salt: string, range: string, header: string, cookie: string, param: string, value: string, audience: string, allow_types: ("user" | "admin")[], users: string[], max_age_seconds: int, expose_type: bool } }, permissions*: { [key: string]: { [key: string]: bool | number | number[] | string | "*" } }, default: "allow" | "deny", enable_proxy: bool, hooks: { [key: string]: { match*: object, script*: object, timeout: int }[] } }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/proxy/permissions`
**CLI:** `hoody containers proxy permissions replace`

---

#### `setGroup` — Set container group program permission

```typescript
client.api.proxyPermissionsContainer.setGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ program*: string, access*: bool | number | number[] | string | "*" }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/proxy/permissions/permissions/{groupName}`
**CLI:** `hoody containers proxy groups permissions set`

---

#### `setIpGroup` — Set IP authentication group (container)

```typescript
client.api.proxyPermissionsContainer.setIpGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ range*: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/proxy/permissions/groups/{groupName}/ip`
**CLI:** `hoody containers proxy groups ip set`

---

#### `setJwtGroup` — Set JWT authentication group (container)

```typescript
client.api.proxyPermissionsContainer.setJwtGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ secret*: string, algorithm*: "HS256" | "RS256" | "ES256", sources*: string[], claims: { [key: string]: string | number | bool } }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/proxy/permissions/groups/{groupName}/jwt`
**CLI:** `hoody containers proxy groups jwt set`

---

#### `setPasswordGroup` — Set password authentication group (container)

```typescript
client.api.proxyPermissionsContainer.setPasswordGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ username*: string, password*: string, algorithm: "sha256", salt*: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/proxy/permissions/groups/{groupName}/password`
**CLI:** `hoody containers proxy groups password set`

---

#### `setTokenGroup` — Set token authentication group (container)

```typescript
client.api.proxyPermissionsContainer.setTokenGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ header*: string, value*: string } | { cookie*: string, value*: string } | { param*: string, value*: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/containers/{id}/proxy/permissions/groups/{groupName}/token`
**CLI:** `hoody containers proxy groups token set`

---

#### `updateDefault` — Update container default proxy permission policy

```typescript
client.api.proxyPermissionsContainer.updateDefault(id: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ default*: "allow" | "deny" }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/containers/{id}/proxy/permissions/default`
**CLI:** `hoody containers proxy default`

---

#### `updateState` — Update container proxy enable state

```typescript
client.api.proxyPermissionsContainer.updateState(id: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ enable_proxy*: bool }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/containers/{id}/proxy/permissions/state`
**CLI:** `hoody containers proxy state`

---

### `client.api.proxyPermissionsProject` (13) — Proxy Permissions Project

#### `delete` — Delete project proxy permissions

```typescript
client.api.proxyPermissionsProject.delete(id: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/projects/{id}/proxy/permissions`
**CLI:** `hoody projects proxy permissions delete`

---

#### `get` — Get project proxy permissions

```typescript
client.api.proxyPermissionsProject.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/projects/{id}/proxy/permissions`
**CLI:** `hoody projects proxy permissions get`

---

#### `removeAuthGroup` — Remove project authentication group

```typescript
client.api.proxyPermissionsProject.removeAuthGroup(id: string, groupName: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |
| `groupName` | `string` | path | Yes | Group name to remove |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/projects/{id}/proxy/permissions/groups/{groupName}`
**CLI:** `hoody projects proxy groups delete`

---

#### `removeGroup` — Remove all program permissions for a project group

```typescript
client.api.proxyPermissionsProject.removeGroup(id: string, groupName: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |
| `groupName` | `string` | path | Yes | Group name |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/projects/{id}/proxy/permissions/permissions/{groupName}`
**CLI:** `hoody projects proxy groups permissions clear`

---

#### `removeProgram` — Remove a single program permission for a project group

```typescript
client.api.proxyPermissionsProject.removeProgram(id: string, groupName: string, program: string, if-match?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |
| `groupName` | `string` | path | Yes | Group name |
| `program` | `string` | path | Yes | Program name (e.g., http, ssh, files) |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/projects/{id}/proxy/permissions/permissions/{groupName}/{program}`
**CLI:** `hoody projects proxy groups permissions delete`

---

#### `replace` — Replace project proxy permissions JSON

```typescript
client.api.proxyPermissionsProject.replace(id: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ project*: string, groups*: { [key: string]: { type: "jwt" | "password" | "ip" | "token" | "hoody-identity", secret: string, algorithm: "HS256" | "RS256" | "ES256" | "sha256", sources: string[], claims: object, username: string, password: string, salt: string, range: string, header: string, cookie: string, param: string, value: string, audience: string, allow_types: ("user" | "admin")[], users: string[], max_age_seconds: int, expose_type: bool } }, permissions*: { [key: string]: { [key: string]: bool | number | number[] | string | "*" } }, default: "allow" | "deny", enable_proxy: bool }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/projects/{id}/proxy/permissions`
**CLI:** `hoody projects proxy permissions replace`

---

#### `setGroup` — Set project group program permission

```typescript
client.api.proxyPermissionsProject.setGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ program*: string, access*: bool | number | number[] | string | "*" }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/projects/{id}/proxy/permissions/permissions/{groupName}`
**CLI:** `hoody projects proxy groups permissions set`

---

#### `setIpGroup` — Set IP authentication group (project)

```typescript
client.api.proxyPermissionsProject.setIpGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ range*: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/projects/{id}/proxy/permissions/groups/{groupName}/ip`
**CLI:** `hoody projects proxy groups ip set`

---

#### `setJwtGroup` — Set JWT authentication group (project)

```typescript
client.api.proxyPermissionsProject.setJwtGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ secret*: string, algorithm*: "HS256" | "RS256" | "ES256", sources*: string[], claims: { [key: string]: string | number | bool } }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/projects/{id}/proxy/permissions/groups/{groupName}/jwt`
**CLI:** `hoody projects proxy groups jwt set`

---

#### `setPasswordGroup` — Set password authentication group (project)

```typescript
client.api.proxyPermissionsProject.setPasswordGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ username*: string, password*: string, algorithm: "sha256", salt*: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/projects/{id}/proxy/permissions/groups/{groupName}/password`
**CLI:** `hoody projects proxy groups password set`

---

#### `setTokenGroup` — Set token authentication group (project)

```typescript
client.api.proxyPermissionsProject.setTokenGroup(id: string, groupName: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `groupName` | `string` | path | Yes |  |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ header*: string, value*: string } | { cookie*: string, value*: string } | { param*: string, value*: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/projects/{id}/proxy/permissions/groups/{groupName}/token`
**CLI:** `hoody projects proxy groups token set`

---

#### `updateDefault` — Update project default proxy permission policy

```typescript
client.api.proxyPermissionsProject.updateDefault(id: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ default*: "allow" | "deny" }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/projects/{id}/proxy/permissions/default`
**CLI:** `hoody projects proxy default`

---

#### `updateState` — Update project proxy enable state

```typescript
client.api.proxyPermissionsProject.updateState(id: string, if-match?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Project ID |
| `if-match` | `string` | header | No | file:v<N> ETag precondition — read current file_version from GET first |
| `data` | `object` | body | Yes |  |

**Body:** `{ enable_proxy*: bool }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/projects/{id}/proxy/permissions/state`
**CLI:** `hoody projects proxy state`

---

### `client.api.realms` (1) — Realms

#### `list` — List your realm IDs

```typescript
client.api.realms.list(include_usage?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `include_usage` | `boolean` | query | No | Include resource counts per realm_id (projects, containers, servers, auth_tokens). Adds "usage" object to response data. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/realms/`
**CLI:** `hoody realms list`

---

### `client.api.rentals` (5) — Rentals

#### `extend` — Extend rental

```typescript
client.api.rentals.extend(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ expected_rental_end*: string, additional_days*: int, max_charge_cents: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/rentals/{id}/extend`
**CLI:** `hoody servers extend`

---

#### `get` — Get rental details

```typescript
client.api.rentals.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/rentals/{id}`
**CLI:** `hoody servers get-rental`

---

#### `list` — List user rentals

```typescript
client.api.rentals.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/rentals`
**CLI:** `hoody servers list-rentals`

---

#### `listAll` — List user rentals (collect all pages)

```typescript
client.api.rentals.listAll()
```

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/rentals`
**CLI:** `hoody servers list-rentals`

---

#### `listIterator` — List user rentals (async iterator)

```typescript
client.api.rentals.listIterator()
```

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/rentals`
**CLI:** `hoody servers list-rentals`

---

### `client.api.serverCommands` (4) — Server Commands

#### `execute` — Execute server command

```typescript
client.api.serverCommands.execute(serverId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `serverId` | `string` | path | Yes | Server ID to execute command on |
| `data` | `object` | body | Yes |  |

**Body:** `{ command_id: string, command_slug: string, parameters: object, wait: bool=true, timeout: number, confirmation_token: string } (exactly one of: command_id | command_slug required)`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/servers/{serverId}/execute-command`
**CLI:** `hoody servers exec`

---

#### `list` — Get available commands

```typescript
client.api.serverCommands.list(category?: string, risk_level?: string, serverId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `category` | `string` | query | No | Filter by command category |
| `risk_level` | `string` | query | No | Filter by maximum risk level |
| `serverId` | `string` | path | Yes | Server ID to get available commands for |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/servers/{serverId}/available-commands`
**CLI:** `hoody servers commands`

---

#### `listAll` — Get available commands (collect all pages)

```typescript
client.api.serverCommands.listAll(category?: string, risk_level?: string, serverId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `category` | `string` | query | No | Filter by command category |
| `risk_level` | `string` | query | No | Filter by maximum risk level |
| `serverId` | `string` | path | Yes | Server ID to get available commands for |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/servers/{serverId}/available-commands`
**CLI:** `hoody servers commands`

---

#### `listIterator` — Get available commands (async iterator)

```typescript
client.api.serverCommands.listIterator(category?: string, risk_level?: string, serverId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `category` | `string` | query | No | Filter by command category |
| `risk_level` | `string` | query | No | Filter by maximum risk level |
| `serverId` | `string` | path | Yes | Server ID to get available commands for |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/servers/{serverId}/available-commands`
**CLI:** `hoody servers commands`

---

### `client.api.serverRental` (14) — Server Rental

#### `browse` — Browse rental marketplace

```typescript
client.api.serverRental.browse(country?: string, region?: string, max_price_per_day?: number, available_durations?: array, min_cpu_cores?: number, min_cpu_score?: number, cpu_score_type?: string, min_ram_gb?: number, ram_types?: array, min_total_storage_gb?: number, disk_types?: array, min_bandwidth_mbps?: number, min_traffic_tb?: number, unlimited_traffic_only?: boolean, category?: string, featured_only?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `country` | `string` | query | No | Filter by country code (e.g., US, DE) |
| `region` | `string` | query | No | Filter by region (e.g., us-east, eu-central) |
| `max_price_per_day` | `number` | query | No | Maximum price per day in USD |
| `available_durations` | `array` | query | No | Filter servers that support these rental durations (days) |
| `min_cpu_cores` | `number` | query | No | Minimum CPU cores |
| `min_cpu_score` | `number` | query | No | Minimum CPU benchmark score |
| `cpu_score_type` | `string` | query | No | CPU benchmark type for score filtering |
| `min_ram_gb` | `number` | query | No | Minimum RAM in GB |
| `ram_types` | `array` | query | No | Filter by RAM types |
| `min_total_storage_gb` | `number` | query | No | Minimum total storage in GB |
| `disk_types` | `array` | query | No | Filter servers with these disk types |
| `min_bandwidth_mbps` | `number` | query | No | Minimum network bandwidth in Mbps |
| `min_traffic_tb` | `number` | query | No | Minimum monthly traffic allowance in TB |
| `unlimited_traffic_only` | `boolean` | query | No | Show only servers with unlimited traffic |
| `category` | `string` | query | No | Filter by server category |
| `featured_only` | `boolean` | query | No | Show only featured servers |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/servers/available`
**CLI:** `hoody servers marketplace`

---

#### `browseAll` — Browse rental marketplace (collect all pages)

```typescript
client.api.serverRental.browseAll(country?: string, region?: string, max_price_per_day?: number, available_durations?: array, min_cpu_cores?: number, min_cpu_score?: number, cpu_score_type?: string, min_ram_gb?: number, ram_types?: array, min_total_storage_gb?: number, disk_types?: array, min_bandwidth_mbps?: number, min_traffic_tb?: number, unlimited_traffic_only?: boolean, category?: string, featured_only?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `country` | `string` | query | No | Filter by country code (e.g., US, DE) |
| `region` | `string` | query | No | Filter by region (e.g., us-east, eu-central) |
| `max_price_per_day` | `number` | query | No | Maximum price per day in USD |
| `available_durations` | `array` | query | No | Filter servers that support these rental durations (days) |
| `min_cpu_cores` | `number` | query | No | Minimum CPU cores |
| `min_cpu_score` | `number` | query | No | Minimum CPU benchmark score |
| `cpu_score_type` | `string` | query | No | CPU benchmark type for score filtering |
| `min_ram_gb` | `number` | query | No | Minimum RAM in GB |
| `ram_types` | `array` | query | No | Filter by RAM types |
| `min_total_storage_gb` | `number` | query | No | Minimum total storage in GB |
| `disk_types` | `array` | query | No | Filter servers with these disk types |
| `min_bandwidth_mbps` | `number` | query | No | Minimum network bandwidth in Mbps |
| `min_traffic_tb` | `number` | query | No | Minimum monthly traffic allowance in TB |
| `unlimited_traffic_only` | `boolean` | query | No | Show only servers with unlimited traffic |
| `category` | `string` | query | No | Filter by server category |
| `featured_only` | `boolean` | query | No | Show only featured servers |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/servers/available`
**CLI:** `hoody servers marketplace`

---

#### `browseIterator` — Browse rental marketplace (async iterator)

```typescript
client.api.serverRental.browseIterator(country?: string, region?: string, max_price_per_day?: number, available_durations?: array, min_cpu_cores?: number, min_cpu_score?: number, cpu_score_type?: string, min_ram_gb?: number, ram_types?: array, min_total_storage_gb?: number, disk_types?: array, min_bandwidth_mbps?: number, min_traffic_tb?: number, unlimited_traffic_only?: boolean, category?: string, featured_only?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `country` | `string` | query | No | Filter by country code (e.g., US, DE) |
| `region` | `string` | query | No | Filter by region (e.g., us-east, eu-central) |
| `max_price_per_day` | `number` | query | No | Maximum price per day in USD |
| `available_durations` | `array` | query | No | Filter servers that support these rental durations (days) |
| `min_cpu_cores` | `number` | query | No | Minimum CPU cores |
| `min_cpu_score` | `number` | query | No | Minimum CPU benchmark score |
| `cpu_score_type` | `string` | query | No | CPU benchmark type for score filtering |
| `min_ram_gb` | `number` | query | No | Minimum RAM in GB |
| `ram_types` | `array` | query | No | Filter by RAM types |
| `min_total_storage_gb` | `number` | query | No | Minimum total storage in GB |
| `disk_types` | `array` | query | No | Filter servers with these disk types |
| `min_bandwidth_mbps` | `number` | query | No | Minimum network bandwidth in Mbps |
| `min_traffic_tb` | `number` | query | No | Minimum monthly traffic allowance in TB |
| `unlimited_traffic_only` | `boolean` | query | No | Show only servers with unlimited traffic |
| `category` | `string` | query | No | Filter by server category |
| `featured_only` | `boolean` | query | No | Show only featured servers |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/servers/available`
**CLI:** `hoody servers marketplace`

---

#### `get` — Get server details (alias for /rentals/:id)

```typescript
client.api.serverRental.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/servers/{id}`
**CLI:** `hoody servers get`

---

#### `getMyReservation` — One of your reservations

```typescript
client.api.serverRental.getMyReservation(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/reservations/{id}`

---

#### `getRentalRuntime` — Get live runtime info for a rented server or subserver

```typescript
client.api.serverRental.getRentalRuntime(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/rentals/{id}/runtime`

---

#### `getServerRuntime` — Get live runtime info (alias for /rentals/:id/runtime)

```typescript
client.api.serverRental.getServerRuntime(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/servers/{id}/runtime`

---

#### `list` — List user servers (alias for /rentals)

```typescript
client.api.serverRental.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/servers`
**CLI:** `hoody servers list`

---

#### `listAll` — List user servers (alias for /rentals) (collect all pages)

```typescript
client.api.serverRental.listAll()
```

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/servers`
**CLI:** `hoody servers list`

---

#### `listIterator` — List user servers (alias for /rentals) (async iterator)

```typescript
client.api.serverRental.listIterator()
```

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/servers`
**CLI:** `hoody servers list`

---

#### `listMyReservations` — Your reservations

```typescript
client.api.serverRental.listMyReservations(limit?: integer, offset?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/reservations`

---

#### `listServerOffers` — Browse machines available to order

```typescript
client.api.serverRental.listServerOffers()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/offers`

---

#### `rent` — Rent server

```typescript
client.api.serverRental.rent(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ pool_id: string, rental_days*: int, max_charge_cents: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/servers/{id}/rent`
**CLI:** `hoody servers rent`

---

#### `reserveServerOffer` — Reserve an offer (charges immediately)

```typescript
client.api.serverRental.reserveServerOffer(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ days*: number, max_charge_cents: number, idempotency_key*: string, pool_id: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/offers/{id}/reserve`

---

### `client.api.storageShares` (15) — Storage Shares

#### `create` — Create storage share

```typescript
client.api.storageShares.create(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Source container ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ source_path*: string, target_container_id: string, target_project_id: string, mode*: "readonly" | "readwrite", alias: string, label: string, description: string, enabled: bool, expires_at: number }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/containers/{id}/storage/shares`
**CLI:** `hoody storage create`

---

#### `delete` — Delete storage share

```typescript
client.api.storageShares.delete(shareId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `shareId` | `string` | path | Yes | Share ID (globally unique, no container ID needed) |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/storage/shares/{shareId}`
**CLI:** `hoody storage delete`

---

#### `get` — Get storage share

```typescript
client.api.storageShares.get(id: string, shareId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Source container ID |
| `shareId` | `string` | path | Yes | Share ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/storage/shares/{shareId}`
**CLI:** `hoody storage get`

---

#### `list` — List storage shares

```typescript
client.api.storageShares.list(target_type?: string, label?: string, status?: string, enabled?: string, include_expired?: string, realm_id?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `target_type` | `string` | query | No | Filter by target type |
| `label` | `string` | query | No | Filter by label |
| `status` | `string` | query | No | Filter by status |
| `enabled` | `string` | query | No | Filter by enabled status |
| `include_expired` | `string` | query | No | Include expired shares (default: false) |
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |
| `id` | `string` | path | Yes | Source container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/storage/shares`
**CLI:** `hoody storage list`

---

#### `listAll` — List storage shares (collect all pages)

```typescript
client.api.storageShares.listAll(target_type?: string, label?: string, status?: string, enabled?: string, include_expired?: string, realm_id?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `target_type` | `string` | query | No | Filter by target type |
| `label` | `string` | query | No | Filter by label |
| `status` | `string` | query | No | Filter by status |
| `enabled` | `string` | query | No | Filter by enabled status |
| `include_expired` | `string` | query | No | Include expired shares (default: false) |
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |
| `id` | `string` | path | Yes | Source container ID |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/containers/{id}/storage/shares`
**CLI:** `hoody storage list`

---

#### `listGlobal` — List all your storage shares

```typescript
client.api.storageShares.listGlobal(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/storage/shares`
**CLI:** `hoody storage list-all`

---

#### `listGlobalAll` — List all your storage shares (collect all pages)

```typescript
client.api.storageShares.listGlobalAll(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/storage/shares`
**CLI:** `hoody storage list-all`

---

#### `listGlobalIterator` — List all your storage shares (async iterator)

```typescript
client.api.storageShares.listGlobalIterator(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/storage/shares`
**CLI:** `hoody storage list-all`

---

#### `listIncoming` — Get incoming shares

```typescript
client.api.storageShares.listIncoming(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Container ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/containers/{id}/storage/incoming`
**CLI:** `hoody storage incoming list`

---

#### `listIncomingGlobal` — Get all incoming shares

```typescript
client.api.storageShares.listIncomingGlobal(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/storage/incoming`
**CLI:** `hoody storage incoming list-all`

---

#### `listIncomingGlobalAll` — Get all incoming shares (collect all pages)

```typescript
client.api.storageShares.listIncomingGlobalAll(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/storage/incoming`
**CLI:** `hoody storage incoming list-all`

---

#### `listIncomingGlobalIterator` — Get all incoming shares (async iterator)

```typescript
client.api.storageShares.listIncomingGlobalIterator(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/storage/incoming`
**CLI:** `hoody storage incoming list-all`

---

#### `listIterator` — List storage shares (async iterator)

```typescript
client.api.storageShares.listIterator(target_type?: string, label?: string, status?: string, enabled?: string, include_expired?: string, realm_id?: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `target_type` | `string` | query | No | Filter by target type |
| `label` | `string` | query | No | Filter by label |
| `status` | `string` | query | No | Filter by status |
| `enabled` | `string` | query | No | Filter by enabled status |
| `include_expired` | `string` | query | No | Include expired shares (default: false) |
| `realm_id` | `string` | query | No | Filter by realm ID. Alternative to using realm subdomain in URL. |
| `id` | `string` | path | Yes | Source container ID |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/containers/{id}/storage/shares`
**CLI:** `hoody storage list`

---

#### `toggleIncomingMount` — Toggle incoming share mount

```typescript
client.api.storageShares.toggleIncomingMount(id: string, shareId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Target container ID (receiver container) |
| `shareId` | `string` | path | Yes | Share ID to toggle |
| `data` | `object` | body | Yes |  |

**Body:** `{ mount*: bool }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/containers/{id}/storage/incoming/{shareId}/mount`
**CLI:** `hoody storage incoming toggle-mount`

---

#### `update` — Update storage share

```typescript
client.api.storageShares.update(id: string, shareId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Source container ID |
| `shareId` | `string` | path | Yes | Share ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ mode: "readonly" | "readwrite", alias: string|null, label: string|null, description: string|null, enabled: bool, expires_at: number|null }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/containers/{id}/storage/shares/{shareId}`
**CLI:** `hoody storage update`

---

### `client.api.tfa` (7) — Two-Factor Authentication

#### `disable` — Disable 2FA

```typescript
client.api.tfa.disable(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ password*: string, code*: string }`

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/users/auth/2fa`
**CLI:** `hoody auth 2fa disable`

---

#### `getStatus` — Get 2FA Status

```typescript
client.api.tfa.getStatus()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/users/auth/2fa/status`
**CLI:** `hoody auth 2fa status`

---

#### `regenerateBackupCodes` — Regenerate Backup Codes

```typescript
client.api.tfa.regenerateBackupCodes(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ password*: string, code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/auth/2fa/backup-codes/regenerate`
**CLI:** `hoody auth 2fa regenerate`

---

#### `setTokenGate` — Set 2FA token gate preference

```typescript
client.api.tfa.setTokenGate(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ enabled*: bool, password: string, otp_code: string }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/users/auth/2fa/token-gate`
**CLI:** `hoody auth 2fa gate`

---

#### `setup` — Initialize 2FA Setup

```typescript
client.api.tfa.setup(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ password*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/auth/2fa/setup`
**CLI:** `hoody auth 2fa setup`

---

#### `verify` — Verify 2FA Code During Login

```typescript
client.api.tfa.verify(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ temp_token: string, code*: string, response_mode: "intent" | "tokens", client: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/auth/2fa/verify`
**CLI:** `hoody auth 2fa verify`

---

#### `verifySetup` — Complete 2FA Setup

```typescript
client.api.tfa.verifySetup(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/auth/2fa/verify-setup`
**CLI:** `hoody auth 2fa verify-setup`

---

### `client.api.users` (9) — Users

#### `get` — Get user by ID

```typescript
client.api.users.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | User ID to retrieve |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/users/{id}`
**CLI:** `hoody users get`

---

#### `getFreeTierStatus` — Get free-tier claim status

```typescript
client.api.users.getFreeTierStatus()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/users/me/free-tier-status`

---

#### `getSecurityHistory` — Get your account security history

```typescript
client.api.users.getSecurityHistory(page?: integer, limit?: integer, include_failed?: boolean, include_security?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number |
| `limit` | `integer` | query | No | Results per page |
| `include_failed` | `boolean` | query | No | Also return REJECTED sign-in attempts against this account. Opt-in: mixing them in by default would make failed attempts look like your own sessions. |
| `include_security` | `boolean` | query | No | Also return other account-security events already recorded for you: logout, 2FA enabled/disabled, OTP verification outcomes, backup-code regeneration. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/users/me/security-history`

---

#### `getSecurityHistoryAll` — Get your account security history (collect all pages)

```typescript
client.api.users.getSecurityHistoryAll(page?: integer, limit?: integer, include_failed?: boolean, include_security?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number |
| `limit` | `integer` | query | No | Results per page |
| `include_failed` | `boolean` | query | No | Also return REJECTED sign-in attempts against this account. Opt-in: mixing them in by default would make failed attempts look like your own sessions. |
| `include_security` | `boolean` | query | No | Also return other account-security events already recorded for you: logout, 2FA enabled/disabled, OTP verification outcomes, backup-code regeneration. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/users/me/security-history`

---

#### `getSecurityHistoryIterator` — Get your account security history (async iterator)

```typescript
client.api.users.getSecurityHistoryIterator(page?: integer, limit?: integer, include_failed?: boolean, include_security?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number |
| `limit` | `integer` | query | No | Results per page |
| `include_failed` | `boolean` | query | No | Also return REJECTED sign-in attempts against this account. Opt-in: mixing them in by default would make failed attempts look like your own sessions. |
| `include_security` | `boolean` | query | No | Also return other account-security events already recorded for you: logout, 2FA enabled/disabled, OTP verification outcomes, backup-code regeneration. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/users/me/security-history`

---

#### `markOnboardingMilestone` — Mark an onboarding milestone as completed

```typescript
client.api.users.markOnboardingMilestone(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ milestone*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/me/onboarding`

---

#### `redeemInviteCode` — Redeem a beta invite code

```typescript
client.api.users.redeemInviteCode(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/me/redeem-invite`
**CLI:** `hoody users redeem-invite`

---

#### `retrySetup` — Retry free-tier account setup

```typescript
client.api.users.retrySetup(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ region: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/users/me/retry-setup`
**CLI:** `hoody users retry-setup`

---

#### `update` — Update user profile

```typescript
client.api.users.update(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | User ID to update |
| `data` | `object` | body | Yes |  |

**Body:** `{ alias: string, public_key: string, metadata: object, password: string, current_password: string, is_admin: bool, is_banned: bool }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/users/{id}`
**CLI:** `hoody users update`

---

### `client.api.utilities` (1) — Utilities

#### `getIpInfo` — Get IP Information

```typescript
client.api.utilities.getIpInfo()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/ip`
**CLI:** `hoody ip get`

---

### `client.api.vault` (8) — User Vault

#### `clear` — Clear entire vault

```typescript
client.api.vault.clear(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (realm_id = ""). Case-insensitive — uppercase is normalized to lowercase. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/vault`
**CLI:** `hoody vault clear`

---

#### `delete` — Delete vault key

```typescript
client.api.vault.delete(realm_id?: string, key: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (realm_id = ""). Case-insensitive — uppercase is normalized to lowercase. |
| `key` | `string` | path | Yes | Vault key name (alphanumeric, dots, underscores, hyphens) |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/vault/keys/{key}`
**CLI:** `hoody vault delete`

---

#### `get` — Get vault key

```typescript
client.api.vault.get(realm_id?: string, key: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (realm_id = ""). Case-insensitive — uppercase is normalized to lowercase. |
| `key` | `string` | path | Yes | Vault key name (alphanumeric, dots, underscores, hyphens) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/vault/keys/{key}`
**CLI:** `hoody vault get`

---

#### `getStats` — Get vault statistics

```typescript
client.api.vault.getStats(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (realm_id = ""). Case-insensitive — uppercase is normalized to lowercase. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/vault/stats`
**CLI:** `hoody vault stats`

---

#### `list` — List vault keys

```typescript
client.api.vault.list(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (realm_id = ""). Case-insensitive — uppercase is normalized to lowercase. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/vault/keys`
**CLI:** `hoody vault list`

---

#### `listAll` — List vault keys (collect all pages)

```typescript
client.api.vault.listAll(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (realm_id = ""). Case-insensitive — uppercase is normalized to lowercase. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/vault/keys`
**CLI:** `hoody vault list`

---

#### `listIterator` — List vault keys (async iterator)

```typescript
client.api.vault.listIterator(realm_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (realm_id = ""). Case-insensitive — uppercase is normalized to lowercase. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/vault/keys`
**CLI:** `hoody vault list`

---

#### `set` — Set vault key

```typescript
client.api.vault.set(realm_id?: string, key: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `realm_id` | `string` | query | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (realm_id = ""). Case-insensitive — uppercase is normalized to lowercase. |
| `key` | `string` | path | Yes | Vault key name (alphanumeric, dots, underscores, hyphens) |
| `data` | `object` | body | Yes |  |

**Body:** `{ value*: string, metadata: object|null }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/vault/keys/{key}`
**CLI:** `hoody vault set`

---

### `client.api.waitlist` (2) — Waitlist

#### `waitlistEnrich` — Enrich an existing waitlist signup

```typescript
client.api.waitlist.waitlistEnrich(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ email*: string, interest: "dev" | "ai" | "saas" | "security" | "creative" | "productivity" | "it" | "other" | …(9 values), context: "individual" | "startup" | "medium" | "enterprise" | "academic" | "", industry: "softdev" | "saas" | "paas" | "cloud" | "ai-ml" | "datasci" | "cyber" | "devops" | …(29 values), role: "lead" | "manager" | "director" | "exec" | "founder" | "engineer" | "devops" | "ml" | …(14 values) }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/waitlist`

---

#### `waitlistJoin` — Join the Hoody waitlist

```typescript
client.api.waitlist.waitlistJoin(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ email*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/waitlist`

---

### `client.api.wallet` (34) — Wallet

#### `addPaymentMethod` — Add a new payment method

```typescript
client.api.wallet.addPaymentMethod(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ name*: string, details: object, is_default: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/wallet/payment-methods/`
**CLI:** `hoody wallet payment-methods create`

---

#### `claimGithubBonus` — Claim the GitHub connection bonus

```typescript
client.api.wallet.claimGithubBonus()
```

**Returns:** `any`  |  **HTTP:** `POST /api/v1/wallet/github-bonus/claim`

---

#### `createCryptoInvoice` — Start a crypto payment (hosted invoice)

```typescript
client.api.wallet.createCryptoInvoice(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ amount*: string, idempotency_key: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/wallet/payments/crypto/invoice`

---

#### `createStripeCheckout` — Start a card payment (Stripe Checkout)

```typescript
client.api.wallet.createStripeCheckout(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ amount*: string, idempotency_key: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/wallet/payments/stripe/checkout`

---

#### `deletePaymentMethod` — Delete a payment method

```typescript
client.api.wallet.deletePaymentMethod(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/wallet/payment-methods/{id}`
**CLI:** `hoody wallet payment-methods delete`

---

#### `downloadInvoicePdf` — Download invoice PDF

```typescript
client.api.wallet.downloadInvoicePdf(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/invoices/{id}/pdf`
**CLI:** `hoody wallet invoices download`

---

#### `generateInvoice` — Generate invoice for transaction

```typescript
client.api.wallet.generateInvoice(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/wallet/invoices/generate/{id}`
**CLI:** `hoody wallet invoices generate`

---

#### `getAggregateBalances` — Get aggregate balances (general + AI)

```typescript
client.api.wallet.getAggregateBalances()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/balances`
**CLI:** `hoody wallet balance get`

---

#### `getAiBalance` — Get AI balance (limit, usage, remaining)

```typescript
client.api.wallet.getAiBalance()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/balances/ai`
**CLI:** `hoody wallet balance ai`

---

#### `getCryptoPaymentIntent` — Get a crypto payment intent

```typescript
client.api.wallet.getCryptoPaymentIntent(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/payments/crypto/intents/{id}`

---

#### `getGeneralBalance` — Get general balance only

```typescript
client.api.wallet.getGeneralBalance()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/balances/general`
**CLI:** `hoody wallet balance general`

---

#### `getGithubBonus` — Get GitHub connection bonus status

```typescript
client.api.wallet.getGithubBonus()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/github-bonus`

---

#### `getInvoice` — Get invoice by ID

```typescript
client.api.wallet.getInvoice(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/invoices/{id}`
**CLI:** `hoody wallet invoices get`

---

#### `getPaymentAvailability` — Get top-up payment availability (providers, bounds, AI transfer fee)

```typescript
client.api.wallet.getPaymentAvailability()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/payment-availability`

---

#### `getPaymentMethod` — Get payment method by ID

```typescript
client.api.wallet.getPaymentMethod(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/payment-methods/{id}`
**CLI:** `hoody wallet payment-methods get`

---

#### `getStripePaymentIntent` — Get a card payment intent

```typescript
client.api.wallet.getStripePaymentIntent(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/payments/stripe/intents/{id}`

---

#### `getTransaction` — Get transaction by ID

```typescript
client.api.wallet.getTransaction(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/transactions/{id}`
**CLI:** `hoody wallet transactions get`

---

#### `listAiFeeHistory` — Get AI credit fee history

```typescript
client.api.wallet.listAiFeeHistory(page?: number, limit?: number, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/ai-fee-history`
**CLI:** `hoody wallet transactions fees`

---

#### `listAiFeeHistoryAll` — Get AI credit fee history (collect all pages)

```typescript
client.api.wallet.listAiFeeHistoryAll(page?: number, limit?: number, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/wallet/ai-fee-history`
**CLI:** `hoody wallet transactions fees`

---

#### `listAiFeeHistoryIterator` — Get AI credit fee history (async iterator)

```typescript
client.api.wallet.listAiFeeHistoryIterator(page?: number, limit?: number, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `number` | query | No |  |
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/wallet/ai-fee-history`
**CLI:** `hoody wallet transactions fees`

---

#### `listCryptoPaymentIntents` — List crypto payment intents

```typescript
client.api.wallet.listCryptoPaymentIntents(limit?: integer, offset?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/payments/crypto/intents`

---

#### `listInvoices` — Get all invoices

```typescript
client.api.wallet.listInvoices(page?: integer, limit?: integer, sort_by?: string, sort_order?: string, filter?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number for pagination - starts from 1 |
| `limit` | `integer` | query | No | Number of invoices to return per page - maximum 100 |
| `sort_by` | `string` | query | No | Field to sort by. One of: id, invoice_number, status, amount, currency, issue_date, due_date, paid_date, created_at, updated_at, user_id, transaction_id. Unrecognised values fall back to created_at. |
| `sort_order` | `string` | query | No |  |
| `filter` | `string` | query | No | JSON object string filtering by the sortable fields, e.g. {"status":"paid"} or {"amount":{"gte":10}}. Operators: eq, ne, gt, gte, lt, lte, like, in. Unknown fields or operators are rejected with 400. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/invoices/`
**CLI:** `hoody wallet invoices list`

---

#### `listInvoicesAll` — Get all invoices (collect all pages)

```typescript
client.api.wallet.listInvoicesAll(page?: integer, limit?: integer, sort_by?: string, sort_order?: string, filter?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number for pagination - starts from 1 |
| `limit` | `integer` | query | No | Number of invoices to return per page - maximum 100 |
| `sort_by` | `string` | query | No | Field to sort by. One of: id, invoice_number, status, amount, currency, issue_date, due_date, paid_date, created_at, updated_at, user_id, transaction_id. Unrecognised values fall back to created_at. |
| `sort_order` | `string` | query | No |  |
| `filter` | `string` | query | No | JSON object string filtering by the sortable fields, e.g. {"status":"paid"} or {"amount":{"gte":10}}. Operators: eq, ne, gt, gte, lt, lte, like, in. Unknown fields or operators are rejected with 400. |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/wallet/invoices/`
**CLI:** `hoody wallet invoices list`

---

#### `listInvoicesIterator` — Get all invoices (async iterator)

```typescript
client.api.wallet.listInvoicesIterator(page?: integer, limit?: integer, sort_by?: string, sort_order?: string, filter?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number for pagination - starts from 1 |
| `limit` | `integer` | query | No | Number of invoices to return per page - maximum 100 |
| `sort_by` | `string` | query | No | Field to sort by. One of: id, invoice_number, status, amount, currency, issue_date, due_date, paid_date, created_at, updated_at, user_id, transaction_id. Unrecognised values fall back to created_at. |
| `sort_order` | `string` | query | No |  |
| `filter` | `string` | query | No | JSON object string filtering by the sortable fields, e.g. {"status":"paid"} or {"amount":{"gte":10}}. Operators: eq, ne, gt, gte, lt, lte, like, in. Unknown fields or operators are rejected with 400. |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/wallet/invoices/`
**CLI:** `hoody wallet invoices list`

---

#### `listPaymentMethods` — Get all payment methods

```typescript
client.api.wallet.listPaymentMethods()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/payment-methods/`
**CLI:** `hoody wallet payment-methods list`

---

#### `listPaymentMethodsAll` — Get all payment methods (collect all pages)

```typescript
client.api.wallet.listPaymentMethodsAll()
```

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/wallet/payment-methods/`
**CLI:** `hoody wallet payment-methods list`

---

#### `listPaymentMethodsIterator` — Get all payment methods (async iterator)

```typescript
client.api.wallet.listPaymentMethodsIterator()
```

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/wallet/payment-methods/`
**CLI:** `hoody wallet payment-methods list`

---

#### `listStripePaymentIntents` — List card payment intents

```typescript
client.api.wallet.listStripePaymentIntents(limit?: integer, offset?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/payments/stripe/intents`

---

#### `listTransactions` — List transactions

```typescript
client.api.wallet.listTransactions(limit?: number, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/wallet/transactions`
**CLI:** `hoody wallet transactions list`

---

#### `listTransactionsAll` — List transactions (collect all pages)

```typescript
client.api.wallet.listTransactionsAll(limit?: number, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/wallet/transactions`
**CLI:** `hoody wallet transactions list`

---

#### `listTransactionsIterator` — List transactions (async iterator)

```typescript
client.api.wallet.listTransactionsIterator(limit?: number, sort_by?: string, sort_order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `number` | query | No |  |
| `sort_by` | `string` | query | No |  |
| `sort_order` | `string` | query | No |  |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/wallet/transactions`
**CLI:** `hoody wallet transactions list`

---

#### `setDefaultPaymentMethod` — Set a payment method as default

```typescript
client.api.wallet.setDefaultPaymentMethod(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/wallet/payment-methods/{id}/default`
**CLI:** `hoody wallet payment-methods set-default`

---

#### `transferToAi` — Transfer from general balance to AI credits

```typescript
client.api.wallet.transferToAi(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ amount*: string, idempotency_key: string, expected_fee_bps: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/wallet/transfers`
**CLI:** `hoody wallet transfer`

---

#### `updatePaymentMethod` — Update a payment method

```typescript
client.api.wallet.updatePaymentMethod(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ details: object, status: "active" | "inactive", is_default: bool }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/wallet/payment-methods/{id}`
**CLI:** `hoody wallet payment-methods update`


---

<!-- ===== namespace: browser ===== -->

# `browser` — Per-container Chromium/Firefox via Playwright/Patchright

## Purpose

- Long-lived browser instances per container, keyed by `browser_id`.
- Playwright default; Patchright on `stealth=true`.
- Surfaces: nav, screenshot, JS eval, PDF, HTML/text, cookies, console+network logs, history, CDP.

## When to use

- JS-heavy pages, screenshots, PDFs.
- Authenticated sessions across navs.
- Anti-fingerprint via `stealth=true`.
- Console/network capture.
- Dedicated recorded "project browser" the user can watch live and you can inspect anytime (workflow 6).
- External CDP via `getDevtoolsUrl` (Chromium ships `webSocketDebuggerUrl` ON by default — pass `useRemoteDebuggingPort=false` to disable; Firefox never exposes it).

## When NOT to use

- Plain HTTP, persistence, server scripts, stream UI, shell → `curl`/`sqlite`/`exec`/`display`/`terminal`.

## Prerequisites

- Headful (default `showBrowser=true`) needs X display: `?display=` or `DISPLAY`. Extensions also require `showBrowser=true`.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Start + navigate
1. `instances.start` — `browser_id` + overrides (proxy, stealth, fingerprintId, viewport, locale, timezoneId, userAgent, geolocation).
2. `interaction.browse`/`browsePost` — `tab_id`, `only_if_not_exists=true`.
3. `introspection.getMetadata`.

### 2. Extract content
After browse: `getHtml`/`getText`/`takeScreenshot`/`exportPdf` — params in Reference.

### 3. Authenticated scraping
1. `instances.start` matching `userAgent`/`viewport`/`locale`.
2. `cookies.set` — POST array (Playwright `addCookies` shape).
3. `interaction.browse` to protected URL.
4. `page.getHtml`/`getText`.
5. `cookies.clear`.

### 4. JS eval + logs
1. `instances.start` → `interaction.browse`.
2. `interaction.evalGet` (`?script=`) or `evalPost` (raw JS body).
3. `debugging.getConsoleLogs` (`since`,`type`,`clear=true`).
4. `debugging.getNetworkLogs`.

### 5. History
`history.list` (filters: `since`/`domain`/`browser_id`) and `history.clear` (`before` + `browser_id` AND).

### 6. Dedicated project browser — suggest it to the user
When the work is a website project or business research, **offer** the user a dedicated recorded browser. This is a suggestion to the user only — set it up when they ask, don't spin it up unprompted.
1. Pick a slot number X (e.g. 2) and start headful on the matching display: `instances.start` with a stable `browser_id` (the project slug), `browser_port` (e.g. 30000+X), `display` 500+X, `showBrowser=true` — plus any per-project identity: own egress proxy (`proxyServer`/`proxyUsername`/`proxyPassword`/`proxyBypass`), `stealth`, `userAgent`, `viewport`, `locale`, `geolocation`, extensions.
2. Give the user the direct live-view URL — it's the standard kit URL with the `browser-` slug: `https://{P}-{C}-browser-X.{N}.containers.hoody.com/`. The root page of `browser-X` embeds display 500+X live, so an instance started on display 500+X gets its own stable viewing URL — changing the X in the URL is how you address each browser's live window. `introspection.getDevtoolsUrl` adds a live DevTools inspector as a second link.
3. Everything browsed there — by the user clicking around in the live view or by the agent via the API — lands in persistent per-`browser_id` history (`history.list`): live debugging and business research accumulate into one durable project trail. The instance itself is reaped after ~1h idle (see Quirks) — history survives; re-run `instances.start` with the same options to revive the window.
4. Then offer log capture as a follow-up: console/network buffers hold only the last 500 entries and die with the instance, so a recurring `cron` job (or agent loop) draining `debugging.getConsoleLogs`/`debugging.getNetworkLogs` with `clear=true` into `sqlite`/`files`/agent memory preserves full context for later sessions.

## Quirks & gotchas

- `browser_id` is an opaque, 0-based string. The kit's request handler reads `browser_port || port` from query/body (NOT `browser_id`); the generated SDK sends `browser_id` as a query param; the URL serviceIndex slot (e.g. `-browser-1.`) is a separate proxy routing variable set via `_templateVars`, defaulting to 1. Drive via the SDK and prefer `browser_id`; raw `curl` against the kit must send `browser_port=<num>`.
- Endpoints auto-create unless `start=false` or `--disable-auto-start`.
- `stealth` defaults true; bare `?stealth`=true. Mid-flight change throws `Instance backend mismatch` — `stop` first.
- `stealth=true` ignored on Firefox (Patchright is Chromium-only).
- Extensions need `showBrowser=true`; force persistent profile.
- `chromiumVersion`: full / major / channel (`stable|beta|dev|canary`); first new version blocks on download.
- Console/network logs: 500-entry ring buffers — drain or filter `since`.
- **Idle instances are killed after `HOODY_INSTANCE_MAX_AGE` ms (default 3 600 000 = 1 h).** Cleanup runs every `HOODY_CLEANUP_INTERVAL` ms (default 300000 = 5 min) and SIGTERMs anything past max-age. Heartbeat any active instance with a request to refresh `lastAccessed`. Long-running scrape loops or warm cookies WILL silently lose state across hours of idle.
- Instances do NOT survive kit-process restarts: graceful shutdown (SIGTERM/SIGINT) terminates every child.
- History records ALL navs (incl. headful clicks) at `/hoody/storage/hoody-browser/history`, retention 30 d (`HOODY_HISTORY_RETENTION_DAYS`). Disable: `--history-disable` / `HOODY_HISTORY_DISABLE=true`.
- **`history.clear` with no filters wipes all history** — pair `before` + `browser_id` (or both).
- `browser_id` history filter sanitised as path component.
- `eval` POST accepts BOTH `Content-Type: application/json` with `{"script":"..."}` (what the SDK and CLI send) and `Content-Type: text/plain` with the raw JS body — the kit tries JSON parse first, falls back to storing the body as `__raw`.
- Chromium CDP defaults to `useRemoteDebuggingPort=true`; `getDevtoolsUrl` only 404s when the instance itself is missing (NOT when CDP is "off"). To disable CDP exposure, pass `useRemoteDebuggingPort=false` at start. CDP listener binds to `0.0.0.0` by default — treat it as a security-relevant default and either keep CDP off in shared environments or front it with the kit URL's capability gate.
- `viewport` and `geolocation` query/body values are **JSON strings**, not free-form `"WxH"` / `"lat,lng"` — the kit `JSON.parse`s and rejects anything else. Examples: `viewport='{"width":1280,"height":800}'`, `geolocation='{"latitude":48.8,"longitude":2.3,"accuracy":50}'`.
- Screenshot `format` enum is `png | jpeg | base64` (NO `json`). Base64 mode returns `{ data: "<b64>" }` only — there is NO `mimeType` or `dataUrl` in the response (the kit's JSON body has `data` only).

## Common errors

- `VALIDATION_ERROR` 400 — bad `browser_id`, malformed `viewport`/`geolocation`, history `limit` not 1–500, `offset`<0.
- `NOT_FOUND` 404 `Instance not found` — `stop`, `getDevtoolsUrl`, `start=false` no instance.
- `HISTORY_DISABLED` 404 `History is disabled` — history endpoints with `--history-disable`.
- `INSTANCE_BACKEND_MISMATCH` 409 (message starts `Instance backend mismatch`) — `stealth` differs from the running instance's backend; `stop` then `start`.
- `VALIDATION_ERROR` 400 `display is required when showBrowser=true (no DISPLAY detected)` — `showBrowser=true` with no `display` field on `instances.start` and no `$DISPLAY` env.
- `TIMEOUT` 408 — exceeds `HOODY_REQUEST_TIMEOUT` (600000ms).
- `METHOD_NOT_ALLOWED` 405 — only GET/POST/DELETE/PUT/PATCH.

## Related namespaces

- `display` — headful renders into `display-{n}`; root URL iframes it.
- `terminal` — helper CLIs.
- `curl` — JS-free HTTP.
- `files` — pull artefacts out.
- `sqlite` — persist scraped data.
- `exec` — server post-processing.

## Examples

Every step in every example was live-tested against a real `browser-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first. ⚠ The kit keys instances on **`browser_port`**, not `browser_id` — on instance endpoints `browser_id` is ignored (it only namespaces history), so calls that differ only in `browser_id` all resolve to the same default instance. The examples run sequentially against that single default instance; for genuinely concurrent flows add a unique `browser_port=$((30000+i))` to every call, as Example 2 does (see the Quirks gotcha).

### 1. Spin up a headless instance and navigate to a URL

**Goal:** boot a Chromium instance, point it at a page, confirm it's alive. Headless avoids the X-display dependency (`showBrowser=false`).

**Step 1 — start.** Returns the start-response payload (`engine`, `headless`, `chromiumBuildId`, `browser_host`, `browser_port`). `instances.start` is idempotent — calling again with the same `browser_id` returns the existing instance.

```typescript
const meta = await client.browser.instances.start({
  browser_id: '0',
  showBrowser: false,
  stealth: false,
});
console.log(meta.data!.engine, meta.data!.headless, meta.data!.chromiumBuildId);
```
**Step 2 — navigate.** `interaction.browse` opens or reuses a tab and waits for load.

```typescript
await client.browser.interaction.browse({
  browser_id: '0',
  url: 'https://httpbin.org/html',
});
```
### 2. Full-page PNG screenshot

**Goal:** capture the entire scrolled height (not just the viewport). `format=base64` returns `{ data: "<b64>" }`; the default `format=png` returns raw `image/png` bytes.

```typescript
import { writeFileSync } from 'fs';
const r = await client.browser.interaction.takeScreenshot({
  browser_id: '0',
  fullPage: true,
  format: 'base64',
  url: 'https://httpbin.org/html',
});
const b64 = (r.data as any).data as string; // kit returns { data: "<b64>" }; the client wraps it
writeFileSync('/tmp/page.png', Buffer.from(b64, 'base64'));
```
### 3. Get the page HTML and the rendered text

**Goal:** read the post-JS DOM (HTML) and the visible text the user would see (`page.getText` calls Playwright `innerText` on `body`).

```typescript
const html = await client.browser.page.getHtml({ browser_id: '0' });
const text = await client.browser.page.getText({ browser_id: '0' });
console.log(html.data!.slice(0, 200), '\n---\n', text.data!.slice(0, 200));
```
### 4. Execute JavaScript in the page and capture the return value

**Goal:** run a script in the page context. `evalGet` puts the script in `?script=` (size-bound by URL). `evalPost` accepts `{ script }` JSON via the SDK / CLI / `Content-Type: application/json`; raw `Content-Type: text/plain` HTTP also works (body = script source). Either shape returns the result.

```typescript
const t = await client.browser.interaction.evalGet({
  browser_id: '0', script: 'document.title',
});
const r = await client.browser.interaction.evalPost(
  { script: 'JSON.stringify({title: document.title, links: document.querySelectorAll("a").length})' },
  { browser_id: '0' },
);
console.log(t.data, r.data);
```
### 5. Set cookies and read them back

**Goal:** prime the cookie jar, then verify. POST body is the Playwright `addCookies` shape: an array of `{ name, value, url }` (or `{ name, value, domain, path }`). Each cookie REQUIRES either `url` OR `domain+path`.

```typescript
await client.browser.cookies.set({
  cookies: [
    { name: 'session', value: 'abc123', url: 'https://httpbin.org' },
    { name: 'theme',   value: 'dark',   url: 'https://httpbin.org' },
  ],
}, { browser_id: '0' });
const jar = await client.browser.cookies.get({
  browser_id: '0', url: 'https://httpbin.org',
});
console.log(jar.data!.cookies);
```
### 6. Authenticated scrape — set session cookie, browse, extract, clear

**Goal:** scrape a logged-in page without re-doing OAuth. Pre-load the session cookie a real login would have set, hit the protected URL, read text, then DELETE the jar.

```typescript
const BID = 'examples-auth-9c';
await client.browser.instances.start({ browser_id: BID, showBrowser: false, stealth: false });
await client.browser.cookies.set(
  { cookies: [{ name: 'sessionid', value: 'REAL_TOKEN', url: 'https://app.example.com' }] },
  { browser_id: BID },
);
await client.browser.interaction.browse({ browser_id: BID, url: 'https://app.example.com/dashboard' });
const t = await client.browser.page.getText({ browser_id: BID });
await client.browser.cookies.clear({ browser_id: BID });
```
### 7. `browsePost` with explicit viewport, locale, and User-Agent

**Goal:** mimic a French mobile Safari to test geo/UA-gated content. Browser-context settings (`viewport`, `locale`, `timezoneId`, `userAgent`) are set at `instances.start`; `interaction.browsePost` carries the navigation body.

`viewport` is a JSON string such as `'{"width":390,"height":844}'`. `geolocation` is a JSON string such as `'{"latitude":48.8566,"longitude":2.3522,"accuracy":20}'`.

```typescript
const BID = 'examples-mobile-fr';
await client.browser.instances.start({
  browser_id: BID, showBrowser: false, stealth: true,
  viewport: '{"width":390,"height":844}', locale: 'fr-FR', timezoneId: 'Europe/Paris',
  userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1',
  geolocation: '{"latitude":48.8566,"longitude":2.3522,"accuracy":20}',
});
await client.browser.interaction.browsePost(
  { url: 'https://httpbin.org/headers', active: true, onlyIfNotExists: false },
  { browser_id: BID },
);
const t = await client.browser.page.getText({ browser_id: BID });
```
### 8. Inspect browsing history (filter by domain, then drop one host)

**Goal:** the kit records every navigation under `/hoody/storage/hoody-browser/history` (30-day retention, 500-entry pagination cap). Query, then selectively delete.

```typescript
const since = new Date(Date.now() - 3600_000).toISOString();
const recent = await client.browser.history.list({ since, limit: 50 });
console.log(recent.data!.total, recent.data!.has_more);
const before = new Date(Date.now() - 7 * 86400_000).toISOString();
await client.browser.history.clear({ before, browser_id: '0' });
```
### 9. Capture instance metadata (engine, viewport, debug URL)

**Goal:** introspect what the kit actually launched — engine (`playwright`/`patchright`), Chromium build, executable path, current display, debug socket. Use `start=false` to query *without* auto-starting (404 if no instance exists).

```typescript
const meta = await client.browser.introspection.getMetadata({
  browser_id: '0', start: false,
});
const tabs = await client.browser.introspection.listTabs({
  browser_id: '0', start: false,
});
console.log(meta.data!.engine, meta.data!.chromiumBuildId, tabs.data);
```
For an external CDP attachment, `introspection.getDevtoolsUrl` returns the live `webSocketDebuggerUrl` (CDP is on by default; default `useRemoteDebuggingPort=true`). If you started with `useRemoteDebuggingPort=false`, the call still returns 200 but `webSocketDebuggerUrl` is null. A 404 `Instance not found` means no instance exists at all (e.g. queried with `start=false`).

### 10. Stop and shutdown — the ONLY safe ending

**Goal:** browser instances stay alive across requests within a kit-process lifetime (graceful kit restart SIGTERMs every child — see Quirks & gotchas); if you forget to shut down you'll exhaust the per-container port pool and the next `instances.start` will fail with `Failed to start server. Is port 30001 in use?` (live-verified — orphaned chromium on the starting port permanently jams the kit until the container is restarted).

`instances.stop` and `introspection.shutdown` both terminate the child and delete any extension profile dir (the child's SIGTERM handler runs the same cleanup as `/shutdown`); persistent profile dirs only exist when extensions were loaded. One `instances.stop` per instance is a complete teardown — calling both is redundant.

```typescript
for (const BID of ['0', 'examples-auth-9c', 'examples-mobile-fr']) {
  try { await client.browser.instances.stop({ browser_id: BID }); } catch {}
}
const m = await client.browser.health.getMetrics();
console.log(m.data!.instances);
```
A `404 Instance not found` from `stop` means it was already gone — safe to ignore. `shutdown` auto-creates a missing instance unless you pass `start=false`; always include `start=false` in teardown calls.

## Reference

**Accessor:** `client.browser`  |  **Import:** `import * as browser from 'hoody-sdk/browser'`

### `client.browser.cookies` (3) — Browser State

#### `clear` — Clear all cookies

```typescript
client.browser.cookies.clear(browser_id: string, start?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |

**Returns:** `any`  |  **HTTP:** `DELETE /cookies`
**CLI:** `hoody browser cookies clear`

---

#### `get` — Get cookies

```typescript
client.browser.cookies.get(browser_id: string, start?: boolean, url?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `url` | `string` | query | No | Filter cookies by URL |

**Returns:** `any`  |  **HTTP:** `GET /cookies`
**CLI:** `hoody browser cookies get`

---

#### `set` — Set cookies

```typescript
client.browser.cookies.set(browser_id: string, start?: boolean, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `data` | `object` | body | Yes |  |

**Body:** `{ cookies*: { name*: string, value*: string, url*: string, domain: string, path: string, httpOnly: bool, secure: bool }[] }`

**Returns:** `any`  |  **HTTP:** `POST /cookies`
**CLI:** `hoody browser cookies set`

---

### `client.browser.debugging` (2) — Debugging

#### `getConsoleLogs` — Get console logs

```typescript
client.browser.debugging.getConsoleLogs(browser_id: string, tabId?: integer, start?: boolean, type?: string, since?: string, clear?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | `integer` | query | No | The ID of the tab to interact with |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `type` | `string` | query | No | Filter by message type (log, error, warning, info, etc.) |
| `since` | `string` | query | No | Only return logs after this ISO timestamp |
| `clear` | `boolean` | query | No | Clear the buffer after reading |

**Returns:** `any`  |  **HTTP:** `GET /console`
**CLI:** `hoody browser console`

---

#### `getNetworkLogs` — Get network logs

```typescript
client.browser.debugging.getNetworkLogs(browser_id: string, tabId?: integer, start?: boolean, since?: string, clear?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | `integer` | query | No | The ID of the tab to interact with |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `since` | `string` | query | No | Only return logs after this ISO timestamp |
| `clear` | `boolean` | query | No | Clear the buffer after reading |

**Returns:** `any`  |  **HTTP:** `GET /network`
**CLI:** `hoody browser network`

---

### `client.browser.health` (4) — Server monitoring and health check operations

#### `check` — Health check

```typescript
client.browser.health.check()
```

**Returns:** `browser_HealthCheck`  |  **HTTP:** `GET /api/v1/browser/health`
**CLI:** `hoody browser health`

---

#### `getMetrics` — Server metrics

```typescript
client.browser.health.getMetrics()
```

**Returns:** `browser_Metrics`  |  **HTTP:** `GET /metrics`
**CLI:** `hoody browser metrics`

---

#### `getOpenApiJson` — Get OpenAPI specification (JSON)

```typescript
client.browser.health.getOpenApiJson()
```

**Returns:** `any`  |  **HTTP:** `GET /openapi.json`

---

#### `getOpenApiYaml` — Get OpenAPI specification (YAML)

```typescript
client.browser.health.getOpenApiYaml()
```

**Returns:** `any`  |  **HTTP:** `GET /openapi.yaml`

---

### `client.browser.history` (2) — Operations for querying and managing persistent browsing history

#### `clear` — Delete browsing history

```typescript
client.browser.history.clear(before?: string, browser_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `before` | `string` | query | No | Delete entries before this ISO 8601 timestamp |
| `browser_id` | `string` | query | No | Delete entries for specific browser ID only |

**Returns:** `any`  |  **HTTP:** `DELETE /history`
**CLI:** `hoody browser history delete`

---

#### `list` — Query browsing history

```typescript
client.browser.history.list(since?: string, domain?: string, browser_id?: string, limit?: integer, offset?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `since` | `string` | query | No | Return entries after this ISO 8601 timestamp |
| `domain` | `string` | query | No | Filter by domain (exact match) |
| `browser_id` | `string` | query | No | Filter by browser ID |
| `limit` | `integer` | query | No | Maximum entries to return (1-500) |
| `offset` | `integer` | query | No | Number of entries to skip for pagination |

**Returns:** `any`  |  **HTTP:** `GET /history`
**CLI:** `hoody browser history query`

---

### `client.browser.instances` (3) — Operations for creating and managing browser instances

#### `restart` — Restart browser instance

```typescript
client.browser.instances.restart(browser_id: string, chromiumVersion?: string, fingerprintId?: string, useRemoteDebuggingPort?: boolean, remoteDebuggingPort?: integer, remoteDebuggingAddress?: string, extensions?: string, extensionsDir?: string, extensionsStoreIds?: string, proxyServer?: string, proxyUsername?: string, proxyPassword?: string, proxyBypass?: string, enableQuic?: boolean, enableDnsOverHttps?: boolean, dnsOverHttpsUrl?: string, display?: string, showBrowser?: boolean, sessionName?: string, timezoneId?: string, locale?: string, userAgent?: string, viewport?: string, noViewport?: boolean, geolocation?: string, launchArguments?: array, browser?: string, firefoxVersion?: string, firefoxExecutablePath?: string, showDevtools?: boolean, userProfile?: object, stealth?: boolean, iframe?: boolean, iframe_url?: string, maximize_new_windows?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `chromiumVersion` | `string` | query | No | Chromium/Chrome version selection for the instance. This option applies only when `browser=chromium`.  Supported formats: - Full version: `136.0.7103.113` - Major version: `136` (mapped to a known stable patch for the current OS) - Channel tag: `stable`, `beta`, `dev`, `canary`  The server will **block** until the required browser is downloaded into `BROWSERS_DIR`. |
| `fingerprintId` | `string` | query | No | Base fingerprint profile id. The server will load `storage/config/fingerprints/<fingerprintId>.json` and use its `context` and `launch` defaults, then apply any request overrides. |
| `useRemoteDebuggingPort` | `boolean` | query | No | If `true`, the child process will launch Chromium with `--remote-debugging-port` and will populate `webSocketDebuggerUrl` in metadata responses. |
| `remoteDebuggingPort` | `integer` | query | No | Optional fixed DevTools port (only used when `useRemoteDebuggingPort=true`). If omitted, a free port is chosen. |
| `remoteDebuggingAddress` | `string` | query | No | Interface address for DevTools. Defaults to `127.0.0.1`. Use `0.0.0.0` only in trusted environments. |
| `extensions` | `string` | query | No | Comma-separated list (or JSON array string) of absolute extension directory paths to load. Extensions require `showBrowser=true` (headful mode) and will launch a persistent profile. |
| `extensionsDir` | `string` | query | No | Directory containing extension subfolders to load (each subfolder is treated as an extension). Extensions require `showBrowser=true` (headful mode) and will launch a persistent profile. |
| `extensionsStoreIds` | `string` | query | No | Chrome Web Store extension IDs to download and load (Chromium only). Requires `showBrowser=true` and works only with `browser=chromium`. |
| `proxyServer` | `string` | query | No | Proxy server URL (http, https, socks5, socks5h) |
| `proxyUsername` | `string` | query | No | Proxy username (if required) |
| `proxyPassword` | `string` | query | No | Proxy password (if required) |
| `proxyBypass` | `string` | query | No | Comma-separated list of hosts that should bypass the proxy |
| `enableQuic` | `boolean` | query | No | Enable QUIC/HTTP3 transport. Defaults to `false` (QUIC blocked). Use `enableQuic=true` to re-enable QUIC. |
| `enableDnsOverHttps` | `boolean` | query | No | Enable DNS-over-HTTPS for browser DNS resolution. Defaults to `true`. |
| `dnsOverHttpsUrl` | `string` | query | No | DoH resolver URL (HTTPS only). Defaults to Cloudflare: `https://cloudflare-dns.com/dns-query`. |
| `display` | `string` | query | No | X display number or identifier for headful mode. Required when `showBrowser=true` and no `DISPLAY` environment variable is set on the server. |
| `showBrowser` | `boolean` | query | No | Whether to run the browser headful (visible). Defaults to `true`. |
| `sessionName` | `string` | query | No | Custom session name for identifying this browser instance |
| `timezoneId` | `string` | query | No | IANA timezone identifier for browser geolocation |
| `locale` | `string` | query | No | BCP 47 language tag for browser locale |
| `userAgent` | `string` | query | No | User agent string to apply to the browser context. |
| `viewport` | `string` | query | No | Viewport configuration as JSON string. Example: {"width":1920,"height":1080,"deviceScaleFactor":1} Pass `null` to disable fixed-viewport emulation entirely — the page then follows the real browser window size (responsive; most useful in headful mode). |
| `noViewport` | `boolean` | query | No | Set to `true` to disable fixed-viewport emulation (alias for `viewport=null`). The page then resizes with the browser window instead of being pinned to an emulated resolution. Cannot be combined with a fixed `viewport` object. |
| `geolocation` | `string` | query | No | Geolocation configuration as JSON string. Example: {"latitude":40.7128,"longitude":-74.0060,"accuracy":100} |
| `launchArguments` | `array` | query | No | Additional browser launch arguments (repeatable or JSON array) |
| `browser` | `string` | query | No | Browser engine to use (`chromium` or `firefox`) |
| `firefoxVersion` | `string` | query | No | Firefox version label (informational only). Playwright-managed Firefox builds are used by default. If omitted, a Playwright Firefox build is downloaded on demand. |
| `firefoxExecutablePath` | `string` | query | No | Absolute path to a custom Firefox executable (overrides download) |
| `showDevtools` | `boolean` | query | No | Whether to open DevTools on launch (Chromium only) |
| `userProfile` | `object` | query | No | Optional user profile object (JSON string) for fingerprinting defaults |
| `stealth` | `boolean` | query | No | Launch Chromium in stealth mode using Patchright (anti-detection patches). Only applies to `browser=chromium`. Ignored for Firefox. Defaults to `true`. Bare `?stealth` is treated as `true`. |
| `iframe` | `boolean` | query | No | Enable or disable the full-page display iframe on the root URL. |
| `iframe_url` | `string` | query | No | Explicit URL for the display iframe. |
| `maximize_new_windows` | `boolean` | query | No | Control the `maximize_new_windows` flag stamped onto the generated display URL (always explicit `true`/`false`); when true the hoody-display client opens new top-level app windows maximized. Enabled by default; set to `false` to opt out. |

**Returns:** `any`  |  **HTTP:** `GET /restart`
**CLI:** `hoody browser restart`

---

#### `start` — Create or retrieve browser instance

```typescript
client.browser.instances.start(browser_id: string, chromiumVersion?: string, fingerprintId?: string, useRemoteDebuggingPort?: boolean, remoteDebuggingPort?: integer, remoteDebuggingAddress?: string, extensions?: string, extensionsDir?: string, extensionsStoreIds?: string, proxyServer?: string, proxyUsername?: string, proxyPassword?: string, proxyBypass?: string, enableQuic?: boolean, enableDnsOverHttps?: boolean, dnsOverHttpsUrl?: string, display?: string, showBrowser?: boolean, sessionName?: string, timezoneId?: string, locale?: string, userAgent?: string, viewport?: string, noViewport?: boolean, geolocation?: string, stealth?: boolean, iframe?: boolean, iframe_url?: string, maximize_new_windows?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `chromiumVersion` | `string` | query | No | Chromium/Chrome version selection for the instance. This option applies only when `browser=chromium`.  Supported formats: - Full version: `136.0.7103.113` - Major version: `136` (mapped to a known stable patch for the current OS) - Channel tag: `stable`, `beta`, `dev`, `canary`  The server will **block** until the required browser is downloaded into `BROWSERS_DIR`. |
| `fingerprintId` | `string` | query | No | Base fingerprint profile id. The server will load `storage/config/fingerprints/<fingerprintId>.json` and use its `context` and `launch` defaults, then apply any request overrides. |
| `useRemoteDebuggingPort` | `boolean` | query | No | If `true`, the child process will launch Chromium with `--remote-debugging-port` and will populate `webSocketDebuggerUrl` in metadata responses. |
| `remoteDebuggingPort` | `integer` | query | No | Optional fixed DevTools port (only used when `useRemoteDebuggingPort=true`). If omitted, a free port is chosen. |
| `remoteDebuggingAddress` | `string` | query | No | Interface address for DevTools. Defaults to `127.0.0.1`. Use `0.0.0.0` only in trusted environments. |
| `extensions` | `string` | query | No | Comma-separated list (or JSON array string) of absolute extension directory paths to load. Extensions require `showBrowser=true` (headful mode) and will launch a persistent profile. |
| `extensionsDir` | `string` | query | No | Directory containing extension subfolders to load (each subfolder is treated as an extension). Extensions require `showBrowser=true` (headful mode) and will launch a persistent profile. |
| `extensionsStoreIds` | `string` | query | No | Comma-separated list (or JSON array string) of Chrome Web Store extension IDs to download and load. Requires `showBrowser=true` and works only with `browser=chromium`. |
| `proxyServer` | `string` | query | No | Proxy server for browser traffic. Supports `http://`, `https://`, `socks5://`, or `socks5h://`. Example: `socks5://127.0.0.1:9050` |
| `proxyUsername` | `string` | query | No | Proxy username (if required) |
| `proxyPassword` | `string` | query | No | Proxy password (if required) |
| `proxyBypass` | `string` | query | No | Comma-separated list of hosts that should bypass the proxy |
| `enableQuic` | `boolean` | query | No | Enable QUIC/HTTP3 transport. Defaults to `false` (QUIC blocked). Use `enableQuic=true` to re-enable QUIC. |
| `enableDnsOverHttps` | `boolean` | query | No | Enable DNS-over-HTTPS for browser DNS resolution. Defaults to `true`. |
| `dnsOverHttpsUrl` | `string` | query | No | DoH resolver URL (HTTPS only). Defaults to Cloudflare: `https://cloudflare-dns.com/dns-query`. |
| `display` | `string` | query | No | X display number or identifier for headful mode. Required when `showBrowser=true` and no `DISPLAY` environment variable is set on the server. |
| `showBrowser` | `boolean` | query | No | Whether to run the browser headful (visible). Defaults to `true`. |
| `sessionName` | `string` | query | No | Custom session name for identifying this browser instance |
| `timezoneId` | `string` | query | No | IANA timezone identifier for browser geolocation |
| `locale` | `string` | query | No | BCP 47 language tag for browser locale |
| `userAgent` | `string` | query | No | User agent string to apply to the browser context. |
| `viewport` | `string` | query | No | Viewport configuration as JSON string. Example: {"width":1920,"height":1080,"deviceScaleFactor":1} Pass `null` to disable fixed-viewport emulation entirely — the page then follows the real browser window size (responsive; most useful in headful mode). |
| `noViewport` | `boolean` | query | No | Set to `true` to disable fixed-viewport emulation (alias for `viewport=null`). The page then resizes with the browser window instead of being pinned to an emulated resolution. Cannot be combined with a fixed `viewport` object. |
| `geolocation` | `string` | query | No | Geolocation configuration as JSON string. Example: {"latitude":40.7128,"longitude":-74.0060,"accuracy":100} |
| `stealth` | `boolean` | query | No | Launch Chromium in stealth mode using Patchright (anti-detection patches). Only applies to `browser=chromium`. Ignored for Firefox. Defaults to `true`. Bare `?stealth` is treated as `true`. |
| `iframe` | `boolean` | query | No | Enable or disable the full-page display iframe on the root URL. When enabled (default), navigating to `/` serves an HTML page with an iframe pointing to the Hoody display URL. |
| `iframe_url` | `string` | query | No | Explicit URL for the display iframe. If not provided, the URL is auto-detected from the Host header subdomain pattern. |
| `maximize_new_windows` | `boolean` | query | No | Control the `maximize_new_windows` flag stamped onto generated display URLs (iframe pages, status pages, `iframe_url` metadata). The flag is always explicit (`true` or `false`); when true the hoody-display client opens new top-level app windows maximized. Enabled by default; set to `false` to keep the display client's centered default-size placement (the explicit `false` also overrides a display-side `default-settings.txt` enable). Explicit `iframe_url` values are never modified. |

**Returns:** `browser_BrowserMetadata`  |  **HTTP:** `GET /start`
**CLI:** `hoody browser start`

---

#### `stop` — Stop browser instance

```typescript
client.browser.instances.stop(browser_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |

**Returns:** `any`  |  **HTTP:** `GET /stop`
**CLI:** `hoody browser stop`

---

### `client.browser.interaction` (5) — Operations for interacting with browser tabs and content

#### `browse` — Navigate to URL

```typescript
client.browser.interaction.browse(browser_id: string, start?: boolean, url?: string, tabId?: integer, active?: boolean, onlyIfNotExists?: boolean, ignoreGetParameters?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `url` | `string` | query | No | The URL to navigate to |
| `tabId` | `integer` | query | No | The ID of the tab to interact with |
| `active` | `boolean` | query | No | Make the tab active (focused) after navigation |
| `onlyIfNotExists` | `boolean` | query | No | Only create a new tab if no tab with the same URL exists |
| `ignoreGetParameters` | `boolean` | query | No | Ignore query parameters when checking for existing URL |

**Returns:** `browser_BrowseResult`  |  **HTTP:** `GET /browse`
**CLI:** `hoody browser navigate`

---

#### `browsePost` — Navigate to URL (POST)

```typescript
client.browser.interaction.browsePost(browser_id: string, start?: boolean, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `data` | `object` | body | Yes |  |

**Body:** `{ url*: string, tabId: int, active: bool=true, onlyIfNotExists: bool=false, ignoreGetParameters: bool=false }`

**Returns:** `browser_BrowseResult`  |  **HTTP:** `POST /browse`
**CLI:** `hoody browser navigate-post`

---

#### `evalGet` — Execute JavaScript

```typescript
client.browser.interaction.evalGet(browser_id: string, start?: boolean, script: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `script` | `string` | query | Yes | JavaScript code to execute (can be base64 encoded) |

**Returns:** `any`  |  **HTTP:** `GET /eval`
**CLI:** `hoody browser eval`

---

#### `evalPost` — Execute JavaScript (POST)

```typescript
client.browser.interaction.evalPost(browser_id: string, start?: boolean, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `data` | `object` | body | Yes |  |

**Body:** `{ script: string }`

**Returns:** `any`  |  **HTTP:** `POST /eval`
**CLI:** `hoody browser eval-post`

---

#### `takeScreenshot` — Capture browser screenshot

```typescript
client.browser.interaction.takeScreenshot(browser_id: string, start?: boolean, url?: string, tabId?: integer, onlyIfNotExists?: boolean, ignoreGetParameters?: boolean, format?: string, quality?: integer, fullPage?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `url` | `string` | query | No | The URL to navigate to |
| `tabId` | `integer` | query | No | The ID of the tab to interact with |
| `onlyIfNotExists` | `boolean` | query | No | Only create a new tab if no tab with the same URL exists |
| `ignoreGetParameters` | `boolean` | query | No | Ignore query strings when checking for existing URL |
| `format` | `string` | query | No | Output format |
| `quality` | `integer` | query | No | Image quality for JPEG format (0-100) |
| `fullPage` | `boolean` | query | No | Capture the entire scrollable page |

**Returns:** `any`  |  **HTTP:** `GET /screenshot`
**CLI:** `hoody browser screenshot`

---

### `client.browser.introspection` (7) — Operations for inspecting and controlling browser instances

#### `closeTab` — Close a browser tab

```typescript
client.browser.introspection.closeTab(browser_id: string, start?: boolean, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `data` | `object` | body | No |  |

**Body:** `{ tabId: int }`

**Returns:** `any`  |  **HTTP:** `POST /tab/close`
**CLI:** `hoody browser tabs close`

---

#### `getDevtoolsUrl` — Get DevTools URLs

```typescript
client.browser.introspection.getDevtoolsUrl(browser_id: string, start?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |

**Returns:** `any`  |  **HTTP:** `GET /devtools-url`
**CLI:** `hoody browser devtools`

---

#### `getMetadata` — Get instance metadata

```typescript
client.browser.introspection.getMetadata(browser_id: string, start?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |

**Returns:** `browser_BrowserMetadata`  |  **HTTP:** `GET /metadata`
**CLI:** `hoody browser info`

---

#### `getViewport` — Get the current viewport policy

```typescript
client.browser.introspection.getViewport(browser_host?: string, browser_port?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_host` | `string` | query | No | Instance host. Optional — must be paired with browser_port; when both are omitted the single running instance is selected (400 AMBIGUOUS_INSTANCE with more than one). |
| `browser_port` | `integer` | query | No | Instance port. Optional — must be paired with browser_host. |

**Returns:** `browser_ViewportStatus`  |  **HTTP:** `GET /viewport`

---

#### `listTabs` — List browser tabs

```typescript
client.browser.introspection.listTabs(browser_id: string, start?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |

**Returns:** `any`  |  **HTTP:** `GET /tabs`
**CLI:** `hoody browser tabs list`

---

#### `setViewport` — Change the viewport at runtime

```typescript
client.browser.introspection.setViewport(browser_host?: string, browser_port?: integer, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_host` | `string` | query | No | Instance host. Optional — must be paired with browser_port; when both are omitted the single running instance is selected (400 AMBIGUOUS_INSTANCE with more than one). |
| `browser_port` | `integer` | query | No | Instance port. Optional — must be paired with browser_host. |
| `data` | `object` | body | Yes |  |

**Body:** `{ viewport*: { width*: int, height*: int }|null }`

**Returns:** `browser_ViewportStatus`  |  **HTTP:** `POST /viewport`

---

#### `shutdown` — Shutdown browser instance

```typescript
client.browser.introspection.shutdown(browser_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |

**Returns:** `any`  |  **HTTP:** `GET /shutdown`
**CLI:** `hoody browser shutdown`

---

### `client.browser.page` (3) — Page Content

#### `exportPdf` — Export page as PDF

```typescript
client.browser.page.exportPdf(browser_id: string, tabId?: integer, start?: boolean, url?: string, format?: string, landscape?: boolean, printBackground?: boolean, margin?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | `integer` | query | No | The ID of the tab to interact with |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |
| `url` | `string` | query | No | Optional URL to navigate to before generating the PDF |
| `format` | `string` | query | No | Paper format (e.g. A4, Letter) |
| `landscape` | `boolean` | query | No | Use landscape orientation |
| `printBackground` | `boolean` | query | No | Include background graphics |
| `margin` | `string` | query | No | Uniform margin (e.g. '1cm', '0.5in') |

**Returns:** `any`  |  **HTTP:** `GET /pdf`
**CLI:** `hoody browser pdf`

---

#### `getHtml` — Get page HTML

```typescript
client.browser.page.getHtml(browser_id: string, tabId?: integer, start?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | `integer` | query | No | The ID of the tab to interact with |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |

**Returns:** `any`  |  **HTTP:** `GET /html`
**CLI:** `hoody browser html`

---

#### `getText` — Get page text

```typescript
client.browser.page.getText(browser_id: string, tabId?: integer, start?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `browser_id` | `string` | query | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | `integer` | query | No | The ID of the tab to interact with |
| `start` | `boolean` | query | No | Controls instance creation behavior. - Default mode: instances are created automatically. Set to `false` to prevent creation. - When auto-start is disabled globally: set to `true` to create an instance. |

**Returns:** `any`  |  **HTTP:** `GET /text`
**CLI:** `hoody browser text`


---

<!-- ===== namespace: code ===== -->

# `code` — VS Code in the browser, per container

## Purpose

**This is the VS Code IDE running in a browser tab — not a programmatic API.** Open `https://{P}-{C}-code-1.{N}.containers.hoody.com/?folder=<abs-path>&id=1` and you get the full editor: file tree, diff view, debugger, terminals, extensions marketplace, all backed by the container's filesystem. The public `code-1` URL is served by an **orchestrator** that requires both `folder` and `id` query params on `/` (it boots child editor instance `id` and iframes it from the `{P}-{C}-http-<60000+id>.{N}` subdomain); with neither param it serves the OpenAPI spec, with only one it returns 400. The `code` namespace methods documented here exist mainly to *configure* the editor (open a folder, install an extension, mint a path-proxy URL) — the day-to-day use is "open the URL".

Like every Hoody kit URL, `code` is **iframable**: drop the `code-1` URL into an `<iframe>` and you've embedded VS Code in your own page. Same for every other kit (`files`, `terminal`, `display`, `desktop`, `browser`, `notes`, `agent`, …) — you can compose a full HTML "operating system" out of Hoody kit iframes with no native code, just URLs and standard CSP / cookie wiring.

**Headline mode: extension-only embed.** Add `&extension=<publisher>.<name>` (alongside the required `folder` and `id` params) and the editor boots into a **single extension's UI** — no file tree, no command palette, no marketplace, no IDE chrome. Just that extension's panel filling the viewport. Pair the extension with the iframable kit URL and you have:

- A coding agent (e.g. **Cline** — `saoudrizwan.claude-dev`) accessible from any browser, including mobile phones with no IDE installed.
- The same agent embedded into your own dashboard, a docs site, a Notion page, a Slack canvas (URL preview), a CRM, or any other HTML surface.
- A focused tool surface (Continue, Copilot Chat, Roo Cline, GitLens, Marquee, Excalidraw, Markdown previewer, Jupyter, …) without exposing the full editor.
- A "Cline-as-a-service" deployment: brand it via `proxyAliases.create({ program: 'code', target_path: '/?extension=saoudrizwan.claude-dev&folder=/workspace&id=1' })` and ship a URL like `https://agent.{server_name}.containers.hoody.com` to your team, hiding the `containerId`.

The container's filesystem still backs the extension (it can edit files, run terminals, hit the network, etc.) — same persistent state as the full IDE. Multiple child instances let you run multiple single-extension surfaces side-by-side under one container — same `code-1` URL, different `id` values (e.g. Cline at `?extension=…&folder=…&id=1`, Continue at `…&id=2`).

## When to use

- **Humans editing code**: hand the user the URL — that's the whole product.
- **Single-extension embed for an agent or tool** — `?extension=<publisher>.<name>` boots straight into that extension's UI; iframable, mobile-friendly, distributable as a brand-able alias.
- Pre-open a workspace/folder for them via `?folder=` / `?workspace=` (so the URL is bookmarkable).
- Pre-install extensions (custom internal VSIX) via the kit's launch flags or in-editor — the `extensions.*` HTTP endpoints are spec-only, not implemented (see Quirks).
- Tunnel an in-container port through the editor's path-proxy on the **child instance** subdomain: `/proxy/{port}` (rewrites `req.base`) or `/absproxy/{port}` (verbatim — use this for APIs/WS).

## When NOT to use

Not for: non-interactive shell → `terminal`/`exec`, file I/O without a UI → `files`, long-lived processes → `daemon`, headless web → `browser`. Not a coding-agent control plane → `agent` (the typed in-container AI-agent HTTP namespace, slug `agent`; the Hoody Agent browser GUI on the same `-agent-1` host is the human surface).

## Prerequisites

- Password mode: session cookie via the HTML form login at the **child instance** root (`POST /login`, form-encoded — see Example 7). The generated `auth.*` accessors build `/api/v1/code/login|logout`, which no surface routes.
- VSIX install: the `extensions.*` HTTP endpoints are spec-only (not implemented) — pre-install via the child launch flags or in-editor (see Example 3).

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

## Common workflows

### 1. Open a workspace folder

1. Open `https://{P}-{C}-code-1.{N}.containers.hoody.com/?folder=<abs-path>&id=1` — both `folder` and `id` are required at the public kit URL.
2. `vscode.getVSCode` (`folder`/`workspace`/`locale`) is the child-instance surface behind it; `ew=true` clears the persisted folder.

### 2. Embed a single extension (single-tool browser surface)

Pin one extension as the entire UI of a `code-N` instance — no editor chrome around it.

1. Make sure the extension is installed in the container's VS Code: pre-install via the child launch flags (`--install-extension`, `--preload-builtin-extensions-dir`) or in-editor — the `extensions.install`/`extensions.list` HTTP endpoints are spec-only (see Quirks).
2. Open / iframe `https://{P}-{C}-code-1.{N}.containers.hoody.com/?extension=<publisher>.<name>&folder=<abs-path>&id=1` (`folder` and `id` are required). Examples:
   - `?extension=saoudrizwan.claude-dev` — Cline (autonomous coding agent), works on a phone.
   - `?extension=continue.continue` — Continue chat.
   - `?extension=ms-toolsai.jupyter` — Jupyter notebooks UI only.
   - `?extension=eamodio.gitlens` — GitLens panel.
3. Point `folder` at the repo you want the extension to see (it is required either way).
4. Embed: `<iframe src="…/?extension=…&folder=…" allow="clipboard-read; clipboard-write"></iframe>` — works in any HTML page, mobile browser, Slack URL preview surface, etc.
5. Brand the URL: `proxyAliases.create({ program: 'code', target_path: '/?extension=saoudrizwan.claude-dev&folder=/workspace/myrepo&id=1' })` → `https://agent.{server_name}.containers.hoody.com`. Hides the `containerId`; gate via `proxyPermissionsContainer.*` for production.

To run two distinct single-extension surfaces under the same container, use different `id` values at the same `code-1` URL (`id=1` for Cline, `id=2` for Continue, etc. — each `id` is its own child editor instance).

### 3. Install and verify a VSIX

`extensions.install` / `extensions.list` are spec-only — not implemented by the current kit (see Quirks). Pre-install via the child launch flags or in-editor, then verify on disk (Example 4).

### 4. Tunnel a container port

The **child instance** mounts `/proxy/:port` and `/absproxy/:port` at its root (`{P}-{C}-http-<60000+id>.{N}` subdomain); the public `code-1` root does NOT route them. The generated accessors `client.code.proxy.resolve` / `resolveAbsolute` build URLs against `/api/v1/code/proxy/...` which no surface routes — drive path-proxy with raw URLs (`https://{P}-{C}-http-60001.{N}.containers.hoody.com/proxy/{port}/...`) or the hand-extended embed-URL builder under the `code.vscode` service (see Quirks for `embedUrl`).

### 5. Authenticate password mode

Form-POST the password to the **child instance** root `/login` (see Example 7) — the generated `auth.*` accessors build `/api/v1/code/login|logout`, which no surface routes.

## Quirks & gotchas

- Login limit 2/min, 12/hr per process.
- `/api/v1/code/health` resets idle heartbeat; only `/healthz` excluded.
- 3 mount prefixes; use `/api/v1/code`.
- `getVSCode` persists `folder`/`workspace` as the last-opened workspace in the instance's user-data dir; `ew=true` wipes.
- `mintKey` idempotent.
- `extensions.install` / `extensions.list` are declared in the kit OpenAPI (and surfaced by the generated SDK/CLI) but the current kit does NOT implement them — no extensions router is mounted anywhere; requests fall through (child: vscode catch-all SPA/302; `code-1`: orchestrator plain-text 404). Pre-install via the child launch flags (`--install-extension`, `--install-builtin-extension`, `--preload-builtin-extensions-dir`) or in-editor.
- `/proxy/:port` and `/absproxy/:port` are mounted at the **child instance root** (`{P}-{C}-http-<60000+id>.{N}` subdomain), NOT under `/api/v1/code` and NOT at the public `code-1` root — the orchestrator has no proxy routes. Use `https://{P}-{C}-http-60001.{N}.containers.hoody.com/proxy/{port}/...` (or `/absproxy/{port}/...`).
- `/proxy/:port/...` rewrites `req.base` so the upstream sees `/<rest>`; `/absproxy/:port/...` keeps the full `/absproxy/:port/...` prefix verbatim — use `absproxy` for APIs/WS where the upstream cares about its own base path.
- `health.checkUpdate` (the generated accessor — the `update.*` service does not exist) queries GitHub releases. NOTE: the generated path is `/api/v1/code/update/check`, but `/update` is mounted on the **child instance** root only — the generated call and the public `code-1` root both miss it (orchestrator has no `/update` route). Call `https://{P}-{C}-http-<60000+id>.{N}…/update/check` directly. The route returns `{ checked, latest, current, isLatest }`, but the generated TS type `CodeHealthCheckUpdateResponse` (from the OpenAPI spec) mis-declares `{ current, latest, updateAvailable }` and drops `checked` — read `.isLatest`/`.checked` from the raw JSON, not `.updateAvailable`. `?force=true` bypasses the 24 h cache (kit-level only — not exposed via the generated SDK or CLI surfaces; reachable only by raw HTTP to `/update/check?force=true`).
- `health.check` at the public `code-1` URL hits the ORCHESTRATOR, which returns the standardized 9-field `{ status: "ok", service, built, started, memory, fds, pid, ip, userAgent }` envelope — matching the generated TS type. The flat `{ status: "alive" | "expired", lastHeartbeat }` shape only appears on the child instance's health route (`{P}-{C}-http-<60000+id>.{N}` subdomain).
- `kit_slug` always `code`.
- `embedUrl` is a hand-extended SDK builder hung off the `code.vscode` service; an equivalent embed builder also exists on the CLI surface. Composes the iframe URL without firing a request.

## Common errors

- `extensions.install` / `extensions.list`: not implemented — at `code-1` the orchestrator returns a plain-text 404; on the child the request falls through to the vscode catch-all (SPA / 302 / 401). The `MISSING_URL`/`INVALID_URL_FORMAT`/`DOWNLOAD_FAILED`/`INSTALLATION_FAILED` codes exist only in the OpenAPI spec, never at runtime.
- Path-proxy 400 `Invalid port`. The cited line only checks `isNaN(port)`; the 1024–65535 range is enforced by the OpenAPI client validator before the request lands.
- `/proxy/{port}/` unauth root returns 302 `/login`; deeper 401.
- `auth.login` rate-limit returns HTML, not JSON.
- Unauth `getVSCode` returns 302 `/login`, not 401.
- On the **child instance** (`-http-<port>` subdomain) unmatched `/api/v1/code/*` paths fall through to the editor's catch-all route, which serves the SPA shell / 401 / 302 depending on auth — NOT a JSON 404. At the public `code-1` URL they return the orchestrator's plain-text 404 instead. Test against an explicit known-good path (e.g. `/api/v1/code/health`) for kit liveness.

## Related namespaces

→ `terminal`, `files`, `exec`, `browser`, `daemon`.

## Examples

`code` is a **URL-first** namespace — for most workflows the kit URL itself (with the right query string and / or iframe wrapper) IS the deliverable. The methods here exist to *configure* the editor (install an extension, mint a key, log in), not to drive it. Each example below is a copy-pasteable recipe in the mode you're reading. URL-composition steps are pure string templates and need no kit call. HTTP / SDK steps were verified against the kit source (orchestrator + child-instance routes). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first.

### 1. Open the editor with a folder pre-loaded — bookmarkable URL

**Goal:** ship a teammate a single URL that opens VS Code already pointing at the right repo. `?folder=<absolute-path>` is persisted as the last-opened workspace in the instance's user-data dir, so even subsequent un-querystringed visits land back on it (until you pass `?ew=true` to clear).

```typescript
const url = `https://${P}-${C}-code-1.${N}.containers.hoody.com/?folder=${encodeURIComponent('/workspace/myrepo')}&id=1`;
console.log(url);
```
To clear the persisted folder later (so the next visit opens the welcome page), append `?ew=true` (`vscode.getVSCode` with `ew=true` wipes the persisted workspace).

### 2. Extension-only embed — boot straight into one extension's UI

**Goal:** open `code-1` as a single extension's panel — no file tree, no command palette, no marketplace. The whole viewport is that extension. Pair with a folder so the extension opens with the right repo selected.

URL pattern (no kit call):

```
https://${P}-${C}-code-1.${N}.containers.hoody.com/?extension=<publisher>.<name>&folder=<absolute-path>&id=1
```

```typescript
const ext = 'saoudrizwan.claude-dev';
const folder = '/workspace/myrepo';
const url = `https://${P}-${C}-code-1.${N}.containers.hoody.com/?extension=${ext}&folder=${encodeURIComponent(folder)}&id=1`;
```
The `extension` query value MUST match `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$` — `publisher.name` only, no version.

### 3. Install a custom VSIX programmatically

**Goal:** push an internal extension (`.vsix`) into the container's VS Code. ⚠ **`extensions.install` is spec-only** — the current kit mounts no extensions router, so the documented `POST /api/v1/code/extensions/install` never reaches a handler (`code-1`: orchestrator plain-text 404; child: vscode catch-all SPA/302). What actually works:

- **At child boot (launch flags):** `--install-extension <id-or-vsix-path>`, `--install-builtin-extension <vsix-path>`, or drop VSIXes in the preload dir consumed by `--preload-builtin-extensions-dir` (the platform passes `/hoody/storage/hoody-code/extensions` by default).
- **In-editor:** Extensions view → `…` menu → "Install from VSIX…" (the VSIX must already be on the container filesystem — push it via the `files` namespace).

### 4. List installed extensions + verify a specific one is present

**Goal:** sanity-check after a deploy that the extensions you expected are actually loaded. ⚠ **`extensions.list` is spec-only** (same as `extensions.install` — no extensions router is mounted). Verify on disk instead: each child instance keeps its extensions at `/hoody/storage/hoody-code/data/<id>/extensions`, with directory names like `<publisher>.<name>-<version>`. Run the check inside the container via the `terminal` / `exec` namespaces:

```bash
ls /hoody/storage/hoody-code/data/1/extensions
# → ms-python.python-2024.0.0  saoudrizwan.claude-dev-3.7.0  …
ls /hoody/storage/hoody-code/data/1/extensions | grep -q '^saoudrizwan\.claude-dev-' \
  || echo "MISSING: saoudrizwan.claude-dev"
```

(Generated `extensions.list` accessors on any surface target the unimplemented endpoint and return no data.)

### 5. Path-proxy a container port through the editor — `/proxy/{port}` for browser previews

**Goal:** surface a dev server (Vite, Next.js, `python -m http.server`) running inside the container at `localhost:3000` to your laptop's browser. `/proxy/{port}` rewrites `req.base` so relative URLs in the upstream HTML still resolve under the proxy prefix — use this for **browser-rendered apps with relative asset paths**.

```
https://${P}-${C}-http-60001.${N}.containers.hoody.com/proxy/3000/
```

(`http-<60000+id>` is the **child instance** subdomain — `60001` for `id=1`. The public `code-1` root does NOT route `/proxy`.)

```typescript
const previewUrl = `https://${P}-${C}-http-60001.${N}.containers.hoody.com/proxy/3000/`;
// Drop into an <iframe src={previewUrl}> on your dashboard.
```
Port range is `1024–65535`; the kit only enforces `isNaN(port)` at `pathProxy.ts:18` (returns `400 Invalid port`); the 1024–65535 range is enforced by the SDK client validator (`proxy.service.generated.ts:157`) but NOT by the server — raw HTTP callers (curl/fetch) must self-enforce the range.

### 6. Absproxy a container port — `/absproxy/{port}` for raw API / WebSocket passthrough

**Goal:** call a JSON API or open a WebSocket running inside the container with the **path preserved verbatim**. Use this — not `/proxy/` — for any case where the upstream cares about the literal request path (REST APIs, gRPC-Web, WebSockets at fixed routes). The `/proxy` variant rewrites `req.base` and will break path-sensitive servers.

```
https://${P}-${C}-http-60001.${N}.containers.hoody.com/absproxy/8080/v1/items?q=cake
                                                                              ^^^^^^^^^^^^^^^^^ ← upstream sees exactly this path
```

```typescript
const apiBase = `https://${P}-${C}-http-60001.${N}.containers.hoody.com/absproxy/8080`;
// fetch / WebSocket against ${apiBase}/<your-real-path> — path is preserved verbatim.
```
`/proxy` and `/absproxy` share the same auth gate: unauth root returns `302 /login`, deeper paths `401`.

### 7. Authenticate password mode — `auth.getLoginPage` + `auth.login`

**Goal:** when the kit is launched with `--password <plaintext>` / `--hashed-password <argon2>`, every route is gated. The login flow is HTML-form-based (not a JSON API): fetch `/login` to bootstrap, then `POST /login` form-encoded to get a session cookie.

```typescript
// The generated auth.* accessors build /api/v1/code/login, which no surface routes —
// form-POST the password to the CHILD instance root /login via fetch instead:
const res = await fetch(`https://${P}-${C}-http-60001.${N}.containers.hoody.com/login`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({ password: process.env.CODE_PASSWORD! }),
  redirect: 'manual',
  credentials: 'include',
});
// res.headers.get('set-cookie') → session cookie; re-send on every follow-up.
```
Rate limit: **2/min, 12/hr per process**. Hitting the limit returns HTML, not JSON — tail the body for the literal string `Login rate limited!` (i18n LOGIN_RATE_LIMIT, English locale value).

### 8. Health check + extension verify — post-deploy smoke test

**Goal:** after a container rebuild, confirm the `code` kit is up AND the extensions you ship pre-installed actually loaded. One-shot smoke.

```typescript
const health = (await client.code.health.check()).data!;
if (health.status !== 'ok') throw new Error('kit unhealthy');  // orchestrator envelope at code-1
// Extensions: extensions.list() targets the unimplemented endpoint (see Example 4);
// verify on disk (e.g. via the terminal namespace):
//   ls /hoody/storage/hoody-code/data/1/extensions
```
⚠ On the **child instance**, `/api/v1/code/health` resets the idle-shutdown heartbeat — only `/healthz` (kit-internal, not surfaced through the public path) is excluded; hitting it on a cron keeps the child hot. The orchestrator's `code-1` health endpoint does not touch child heartbeats.

### 9. Logout + verify the session cookie is revoked

**Goal:** end the password-mode session cleanly when a teammate steps away. `auth.logout` clears the cookie server-side; subsequent requests with the old cookie redirect to `/login`.

```typescript
await fetch(`https://${P}-${C}-http-60001.${N}.containers.hoody.com/logout`, {
  credentials: 'include', redirect: 'manual',
});
// Any subsequent child /api/v1/code call now 302s to /login.
```
Note: on the child instance, `/api/v1/code` unauth → `302 /login` (NOT `401`); plan your client to follow / catch the redirect rather than expect a JSON `401` body.

### 10. Embed the extension URL in an iframe — drop VS Code into your own page

**Goal:** ship a brand-able dashboard / docs site / Notion-style canvas with VS Code (or a single extension) embedded as a panel. The kit URL is iframable; just hand the right `src` and `allow` attributes.

```html
<!-- Full editor, with a folder pre-loaded -->
<iframe
  src="https://${P}-${C}-code-1.${N}.containers.hoody.com/?folder=/workspace/myrepo&id=1"
  style="width:100%;height:100vh;border:0"
  allow="clipboard-read; clipboard-write; cross-origin-isolated"
></iframe>

<!-- Single extension only (no IDE chrome) — Cline as a service -->
<iframe
  src="https://${P}-${C}-code-1.${N}.containers.hoody.com/?extension=saoudrizwan.claude-dev&folder=/workspace/myrepo&id=1"
  style="width:100%;height:100vh;border:0"
  allow="clipboard-read; clipboard-write"
></iframe>
```

To hide the `containerId` behind a brandable host, wrap the URL with a `proxyAliases.create({ program: 'code', target_path: '/?extension=saoudrizwan.claude-dev&folder=/workspace/myrepo&id=1', allow_path_override: true })` — the iframe `src` becomes `https://agent.{server_name}.containers.hoody.com`, the `containerId` never leaves the server. Gate it via `proxyPermissionsContainer.*` for production (IP allow-list, basic-auth, etc. — see the `api` namespace).

The same iframe pattern works for **every** Hoody kit (`files`, `terminal`, `display`, `desktop`, `browser`, `notes`, `agent`, …) — compose a full HTML "operating system" out of kit iframes with no native code.

## Reference

**Accessor:** `client.code`  |  **Import:** `import * as code from 'hoody-sdk/code'`

### `client.code.auth` (3) — Authentication endpoints

#### `getLoginPage` — Get login page

```typescript
client.code.auth.getLoginPage(to?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `to` | `string` | query | No | URL to redirect to after successful login |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/code/login`
**CLI:** `hoody code auth login`

---

#### `login` — Submit login credentials

```typescript
client.code.auth.login(to?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `to` | `string` | query | No | URL to redirect to after successful login |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/code/login`
**CLI:** `hoody code auth submit`

---

#### `logout` — Logout

```typescript
client.code.auth.logout()
```

**Returns:** `void`  |  **HTTP:** `GET /api/v1/code/logout`
**CLI:** `hoody code auth logout`

---

### `client.code.extensions` (4) — Extension management (CLI)

#### `install` — Install VS Code extension from URL

```typescript
client.code.extensions.install(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ url*: string, asBuiltin: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/code/extensions/install`
**CLI:** `hoody code extensions install`

---

#### `list` — List installed extensions

```typescript
client.code.extensions.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/code/extensions/list`
**CLI:** `hoody code extensions list`

---

#### `listAll` — List installed extensions (collect all pages)

```typescript
client.code.extensions.listAll()
```

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/code/extensions/list`
**CLI:** `hoody code extensions list`

---

#### `listIterator` — List installed extensions (async iterator)

```typescript
client.code.extensions.listIterator()
```

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/code/extensions/list`
**CLI:** `hoody code extensions list`

---

### `client.code.health` (2) — Health and monitoring

#### `check` — Service health check

```typescript
client.code.health.check()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/code/health`
**CLI:** `hoody code health`

---

#### `checkUpdate` — Check for updates

```typescript
client.code.health.checkUpdate()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/code/update/check`
**CLI:** `hoody code check-update`

---

### `client.code.proxy` (2) — Port forwarding and proxying

#### `resolve` — Proxy to local port (path-based)

```typescript
client.code.proxy.resolve(port: integer, path?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `port` | `integer` | path | Yes | Local port to proxy to |
| `path` | `string` | path | No | Path to append to the proxied request |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/code/proxy/{port}/{path}`
**CLI:** `hoody code proxy-path`

---

#### `resolveAbsolute` — Proxy to local port (absolute path)

```typescript
client.code.proxy.resolveAbsolute(port: integer, path?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `port` | `integer` | path | Yes | Local port to proxy to |
| `path` | `string` | path | No | Path (preserved in forwarded request) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/code/absproxy/{port}/{path}`
**CLI:** `hoody code proxy`

---

### `client.code.static` (5) — Static assets and resources

#### `get` — Get static asset

```typescript
client.code.static.get(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Path to static file |

**Returns:** `any`  |  **HTTP:** `GET /_static/{path}`
**CLI:** `hoody code assets static`

---

#### `getInjectedScript` — Get Hoody Code injected script

```typescript
client.code.static.getInjectedScript(script: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `script` | `string` | path | Yes | Script filename |

**Returns:** `any`  |  **HTTP:** `GET /hoody-code/injected/{script}`
**CLI:** `hoody code assets injected-script`

---

#### `getOpenAPI` — Get OpenAPI specification

```typescript
client.code.static.getOpenAPI()
```

**Returns:** `any`  |  **HTTP:** `GET /openapi.yaml`

---

#### `getRobots` — Get robots.txt

```typescript
client.code.static.getRobots()
```

**Returns:** `any`  |  **HTTP:** `GET /robots.txt`
**CLI:** `hoody code assets robots`

---

#### `getSecurityPolicy` — Get security policy

```typescript
client.code.static.getSecurityPolicy()
```

**Returns:** `any`  |  **HTTP:** `GET /security.txt`
**CLI:** `hoody code assets security-policy`

---

### `client.code.vscode` (3) — VS Code web interface

#### `getManifest` — Get PWA manifest

```typescript
client.code.vscode.getManifest()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/code/manifest.json`
**CLI:** `hoody code assets manifest`

---

#### `getVSCode` — Get VS Code web interface

```typescript
client.code.vscode.getVSCode(folder?: string, workspace?: string, extension?: string, ew?: boolean, locale?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `folder` | `string` | query | No | Absolute path to folder to open in VS Code.  - Takes precedence over `workspace` parameter - Can be a local filesystem path - Stored in settings for next session |
| `workspace` | `string` | query | No | Absolute path to VS Code workspace file (.code-workspace).  - Used when `folder` is not provided - Workspace files can contain multiple folders and settings - Stored in settings for next session |
| `extension` | `string` | query | No | Extension identifier to open in extension-only mode.  **Format**: `PUBLISHER.NAME`  **Behavior when set**: - File explorer is hidden - Extension's views and UI are prominently displayed - Perfect for creating extension-powered web apps  **Use cases**: - Custom web-based tools built on VS Code extensions - Specialized editors (Jupyter notebooks, database tools, etc.) - Kiosk mode for specific workflows  **Examples**: - `ms-python.python` - Python development - `ms-toolsai.jupyter` - Jupyter notebooks - `ms-azuretools.vscode-docker` - Docker management - `redhat.vscode-yaml` - YAML editing |
| `ew` | `boolean` | query | No | "Empty Window" flag - indicates workspace was closed.  When present, clears the last opened folder/workspace from settings. |
| `locale` | `string` | query | No | Display language for VS Code UI.  Format: IETF language tag (e.g., en, fr, de, ja, zh-CN)  See: https://en.wikipedia.org/wiki/IETF_language_tag |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/code`
**CLI:** `hoody code vs`

---

#### `mintKey` — Generate server web key

```typescript
client.code.vscode.mintKey()
```

**Returns:** `any`  |  **HTTP:** `POST /api/v1/code/mint-key`
**CLI:** `hoody code auth mint-key`


---

<!-- ===== namespace: cron ===== -->

# `cron` — managed crontab entries per system user

## Purpose

Edit `crontab(1)` of a system user. UUID-keyed managed entries (name, comment, expiry, enabled) coexist with hand-written lines. Sweep drops expired.

## When to use

- Recurring jobs via cron daemon.
- Future commands with `expires_at` cleanup.
- Repair crontab without losing hand-written lines.

## When NOT to use

Not for: ad-hoc → `terminal`/`exec`, long-runners → `daemon`, FS triggers → `watch`.

## Prerequisites

- `crontab(1)` + cron daemon present; `user` in `/etc/passwd`.
- `user`-scoped.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Schedule

`entries.create` schedule + command (+ name/comment/expires_at/enabled) → `ManagedEntry` with `id`.

### 2. List

`entries.list` (`page`/`limit`, max 200); the SDK additionally offers auto-pagination helpers (`listAll`, `listIterator`) over the same endpoint.

### 3. Edit / disable / extend

`entries.update` PATCH. `clear_expiration: true` overrides `expires_at`. `enabled: false` keeps rule prefixed `# hoody-cron-disabled:`.

### 4. Bulk replace

`crontab.get` (sweep) then `crontab.put` body — revalidates `# hoody-cron:` blocks; response has `removed_expired`.

### 5. Audit all users

`crontab.listGlobal` → paginated `{ items: [{ user, crontab }], total, page, limit }`; the SDK additionally offers auto-pagination helpers (`listGlobalAll`, `listGlobalIterator`) over the same endpoint.

## Quirks & gotchas

- `user`: matches `^[A-Za-z0-9_.-]{1,32}$` for the character class, but the validator additionally rejects a **leading** `-` (trailing `-` is allowed).
- Vixie 5-field plus standard `@`-macros; Quartz rejected.
- `command`/`name`/`comment` reject newline/null/VT/FF/NEL/LS/PS; caps 4096/120/500.
- `expires_at` RFC 3339, strictly future.
- Body cap 256 KiB (configurable via `max_crontab_bytes`, default 256 KiB) AND 10,000 lines; duplicate entry id rejected, and a duplicate `id=` within one metadata line is rejected.
- **`crontab.put` is a full overwrite — destroys every managed entry on the target user, not just the raw lines.** Server persists only the parsed payload via `state.backend.set(&user, &cleaned)`; entries not in the request body are gone. If you mix `entries.create` and `crontab.put` on the same user, every PUT wipes prior managed state. Strategy: pick one (managed-only via the `entries` endpoints, OR raw-only via `crontab.put`); if you must mix, treat the PUT body as the canonical source of truth and re-create managed entries after each PUT.
- Forged `# hoody-cron:` lines in PUT body are revalidated and rejected.
- `entries.list`/`entries.get` clean expired entries before serializing under a per-user mutex — a GET can mutate the spool.
- `entries.list` items have `type: "managed"` or `"raw"`; only `managed` items carry `id`.
- Sweep every 60s default; per-user lock.

## Common errors

- `400 INVALID_EXPIRES_AT` / `EXPIRES_IN_PAST`.
- `400 INVALID_SCHEDULE / Invalid schedule` — Vixie 5-field plus `@`-macros only; Quartz / 6-field rejected.
- `413 PAYLOAD_TOO_LARGE` — body exceeds 256 KiB cap.
- `500 BACKEND_ERROR` — `crontab(1)` fail / 30s timeout.
- `403 Forbidden` — private IP, no dev-server.

## Related namespaces

- `terminal`, `daemon`, `exec`, `watch`, `files`.

## Examples

Every step in every example was live-tested against a real `cron-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first.

### 1. Set up a nightly DB backup with trial-run dry-fire

**Goal:** schedule `pg_dump` daily at 02:00 in the container's LOCAL timezone (the kit does no TZ conversion; only `expires_at` is UTC-anchored), set the entry to expire end of 2026; first verify it actually fires by running it every minute for one cycle.

**Step 1 — create the entry.** Capture the returned `id`; `schedule_human` should read `"At 02:00 every day"`.

```typescript
const r = await client.cron.entries.create('root', {
  schedule: '0 2 * * *',
  command: 'pg_dump -U postgres mydb | gzip > /backups/db-$(date +\\%F).sql.gz',  // `%` MUST be escaped — crontab truncates at a bare %
  name: 'nightly-db-backup',
  expires_at: '2026-12-31T23:59:59Z',
});
const id = r.data!.id;
```
**Step 2 — trial-run** by tightening to every minute. Wait ~70 s then `tail /var/log/syslog` (via the `terminal` kit) to confirm cron actually fired the job.

```typescript
await client.cron.entries.update('root', id, {
  schedule: '* * * * *',
  comment: 'TEST MODE — revert before merge',
});
```
**Step 3 — promote back to nightly** with a clean comment.

```typescript
await client.cron.entries.update('root', id, {
  schedule: '0 2 * * *',
  comment: 'production schedule',
});
```
### 2. Maintenance window — pause every managed job, do work, resume

**Goal:** disable every managed entry so nothing fires during a 30-min DB migration; re-enable once clean.

**Step 1 — capture every enabled managed id.**

```typescript
const list = await client.cron.entries.list('root');
const ids = list.data!.entries
  .filter(e => e.type === 'managed' && e.enabled)
  .map(e => e.id);
```
**Step 2 — bulk disable.**

```typescript
await Promise.all(ids.map(id => client.cron.entries.update('root', id, { enabled: false })));
```
**Step 3 — run your migration. Step 4 — bulk re-enable** (same loop, body `{ enabled: true }`). Entries pick up at their next regular tick; no missed-window catch-up.

### 3. Migrate a hand-written crontab into managed entries

**Goal:** convert legacy raw lines (no `id`, no metadata) into managed entries with names + lifecycle fields. ⚠ Read the warning at step 3 before running anything destructive.

**Step 1 — read the raw crontab.** `entries.list` shows the same lines as `{ type: 'raw', line: '...' }` items.

```typescript
const raw = (await client.cron.crontab.get('root')).data!.crontab;
const rawLines = raw.split('\n').filter(l => l && !l.startsWith('#'));
```
**Step 2 — re-create as managed.** Parse each raw line into `(schedule, command)` and POST it.

```typescript
for (const line of rawLines) {
  const [m, h, dom, mon, dow, ...cmdParts] = line.split(/\s+/);
  await client.cron.entries.create('root', {
    schedule: `${m} ${h} ${dom} ${mon} ${dow}`,
    command: cmdParts.join(' '),
    name: `migrated-${cmdParts.join(' ').replace(/[^a-z0-9]+/gi,'-').slice(0,40)}`,
  });
}
```
**Step 3 — ⚠ DESTRUCTIVE — wipe the raw lines.** `crontab.put` REPLACES the entire crontab including managed entries (live-verified — see Quirks). Do this ONLY after step 2 has succeeded, and re-create the managed entries AFTER the wipe if you want them back.

```typescript
await client.cron.crontab.put('root', { crontab: '' });
// then re-create managed entries — `crontab.put` wipes them too
```
### 4. Hourly poll → tighten to every 5 minutes after a failure

**Goal:** a health-poller is failing intermittently; you want denser data without redeploying anything. Find by name, change schedule, restore later.

**Step 1 — find the entry id by name.**

```typescript
const list = await client.cron.entries.list('root');
const id = list.data!.entries.find(e => e.name === 'health-poll')!.id;
```
**Step 2 — tighten to `*/5 * * * *`.** `schedule_human` becomes `"Every 5 minutes"` immediately on the response.

```typescript
await client.cron.entries.update('root', id, { schedule: '*/5 * * * *' });
```
**Step 3 — restore** to hourly with body `{ schedule: '0 * * * *' }` once the investigation is over (same call, different schedule string).

### 5. Time-bounded experiment — auto-expire after 30 days

**Goal:** run a daily metrics sample for one month, then have it self-remove. Then learn how to extend or unbound the deadline.

**Step 1 — create with `expires_at`.** ISO 8601 RFC 3339; must be in the future.

```typescript
const r = await client.cron.entries.create('root', {
  schedule: '@daily',
  command: '/opt/metrics/sample.sh',
  name: 'metrics-experiment',
  expires_at: '2026-07-08T00:00:00Z',
});
const id = r.data!.id;
```
After the timestamp passes, the kit's 60 s sweep **deletes** expired managed entries. `entries.list`/`entries.get` also clean expired entries before serializing, so once the sweep runs you can no longer read the expired entry — the entry simply disappears from listings. The `removed_expired` count on `crontab.put` tells you how many expired entries got dropped during a bulk replace.

**Step 2a — extend the deadline mid-experiment** (push out by 30 days):

```typescript
await client.cron.entries.update('root', id, { expires_at: '2026-08-07T00:00:00Z' });
```
**Step 2b — make it permanent** instead. Pass `clear_expiration: true`. If you also send `expires_at` in the same call, `clear_expiration` silently wins (server returns `200` with `expires_at: null` — no error).

```typescript
await client.cron.entries.update('root', id, { clear_expiration: true });
```
### 6. Quick-disable a misbehaving entry by name

**Goal:** A teammate paged you about a runaway cron at 3am. You don't have the id, only the name they mentioned (`noisy-job`).

**Step 1 — find its id by name.**

```typescript
const r = await client.cron.entries.list('root');
const entry = r.data!.entries.find(e => e.type === 'managed' && e.name === 'noisy-job');
const entryId = entry!.id;
```
**Step 2 — disable it (entry stays in the listing for forensics; cron won't fire it).**

```typescript
await client.cron.entries.update('root', entryId, {
  enabled: false,
  comment: `disabled ${new Date().toISOString()} — investigating`,
});
```
**Step 3 — re-enable later** by calling the same update with `{ enabled: true }`.

### 7. Audit which users on the container have any cron entries

**Goal:** compliance question — "who has scheduled jobs?". Container has 60+ system users; you want one shot.

`crontab.listGlobal` returns one record per account in `/etc/passwd` (`{ user, crontab }`). Filter client-side for non-empty `crontab`.

```typescript
const r = await client.cron.crontab.listGlobal();
const userswithCron = r.data!.items.filter(i => i.crontab.trim() !== '');
```
For each non-empty user, drill in via `entries.list(user)` for the managed view, or read the `crontab` text directly from step 1.

### 8. Atomic full-crontab replace from versioned config

**Goal:** your IaC layer keeps the canonical crontab as a string in Git; on deploy, push the whole thing. ⚠ Destructive — wipes managed AND raw entries.

**Step 1 — snapshot current state** for forensics:

```typescript
const snapshot = (await client.cron.crontab.get('root')).data;
```
**Step 2 — push the canonical config.** Body MUST be `application/json` (raw `text/plain` returns `415`). Response carries `removed_expired` (count of managed entries that were dropped because their `expires_at` had passed).

```typescript
import { readFileSync } from 'fs';
const newCrontab = readFileSync('/etc/iac/canonical-crontab.txt', 'utf8');
await client.cron.crontab.put('root', { crontab: newCrontab });
```
### 9. Update only the comment / metadata, leave the schedule untouched

**Goal:** add a runbook URL or owner tag without risking changing what the entry does. PATCH is partial — fields you don't pass stay put.

```typescript
await client.cron.entries.update('root', id, {
  comment: 'owner: @team · runbook: https://wiki.example.com/cron-x',
});
```
`updated_at` advances; `schedule` / `command` / `enabled` are unchanged.

### 10. Rotate-and-replace pattern — read, edit text, write back

**Goal:** a teammate wants ONE hand-written line gone without disturbing the rest. You don't have an id (it's raw).

**Step 1 — fetch** the multi-line string. **Step 2 — edit client-side** (split, drop, rejoin). **Step 3 — write back.** ⚠ This also wipes any managed entries — re-create them with `entries.create` afterwards if you had any.

```typescript
const cur = (await client.cron.crontab.get('root')).data!.crontab;
const next = cur.split('\n').filter(l => l !== '*/30 * * * * /old.sh').join('\n');
await client.cron.crontab.put('root', { crontab: next });
```

## Reference

**Accessor:** `client.cron`  |  **Import:** `import * as cron from 'hoody-sdk/cron'`

### `client.cron.crontab` (5) — Raw crontab management

#### `get` — Get Crontab

```typescript
client.cron.crontab.get(user: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `user` | `string` | path | Yes | System username |

**Returns:** `cron_RawCrontabResponse`  |  **HTTP:** `GET /users/{user}/crontab`
**CLI:** `hoody cron crontabs get`

---

#### `listGlobal` — List All Crontabs

```typescript
client.cron.crontab.listGlobal(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number (1-based) |
| `limit` | `integer` | query | No | Items per page (max 200) |

**Returns:** `cron_RawCrontabListResponse`  |  **HTTP:** `GET /crontab`
**CLI:** `hoody cron crontabs list`

---

#### `listGlobalAll` — List All Crontabs (collect all pages)

```typescript
client.cron.crontab.listGlobalAll(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number (1-based) |
| `limit` | `integer` | query | No | Items per page (max 200) |

**Returns:** `cron_RawCrontabListResponse[]`  |  **HTTP:** `GET /crontab`
**CLI:** `hoody cron crontabs list`

---

#### `listGlobalIterator` — List All Crontabs (async iterator)

```typescript
client.cron.crontab.listGlobalIterator(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | Page number (1-based) |
| `limit` | `integer` | query | No | Items per page (max 200) |

**Returns:** `AsyncIterableIterator<cron_RawCrontabListResponse>`  |  **HTTP:** `GET /crontab`
**CLI:** `hoody cron crontabs list`

---

#### `put` — Put Crontab

```typescript
client.cron.crontab.put(user: string, data: cron_RawCrontabRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `user` | `string` | path | Yes | System username |
| `data` | `cron_RawCrontabRequest` | body | Yes |  |

**Returns:** `cron_RawCrontabUpdateResponse`  |  **HTTP:** `PUT /users/{user}/crontab`
**CLI:** `hoody cron crontabs replace`

---

### `client.cron.entries` (7) — Managed entry CRUD

#### `create` — Create Entry

```typescript
client.cron.entries.create(user: string, data: cron_CreateEntryRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `user` | `string` | path | Yes | System username |
| `data` | `cron_CreateEntryRequest` | body | Yes |  |

**Returns:** `cron_ManagedEntryResponse`  |  **HTTP:** `POST /users/{user}/entries`
**CLI:** `hoody cron entries create`

---

#### `delete` — Delete Entry

```typescript
client.cron.entries.delete(user: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `user` | `string` | path | Yes | System username |
| `id` | `string` | path | Yes | Managed entry id |

**Returns:** `cron_DeleteEntryResponse`  |  **HTTP:** `DELETE /users/{user}/entries/{id}`
**CLI:** `hoody cron entries delete`

---

#### `get` — Get Entry

```typescript
client.cron.entries.get(user: string, id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `user` | `string` | path | Yes | System username |
| `id` | `string` | path | Yes | Managed entry id |

**Returns:** `cron_ManagedEntryResponse`  |  **HTTP:** `GET /users/{user}/entries/{id}`
**CLI:** `hoody cron entries get`

---

#### `list` — List Entries

```typescript
client.cron.entries.list(user: string, page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `user` | `string` | path | Yes | System username |
| `page` | `integer` | query | No | Page number (1-based) |
| `limit` | `integer` | query | No | Items per page (max 200) |

**Returns:** `cron_EntryListResponse`  |  **HTTP:** `GET /users/{user}/entries`
**CLI:** `hoody cron entries list`

---

#### `listAll` — List Entries (collect all pages)

```typescript
client.cron.entries.listAll(user: string, page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `user` | `string` | path | Yes | System username |
| `page` | `integer` | query | No | Page number (1-based) |
| `limit` | `integer` | query | No | Items per page (max 200) |

**Returns:** `cron_EntryListResponse[]`  |  **HTTP:** `GET /users/{user}/entries`
**CLI:** `hoody cron entries list`

---

#### `listIterator` — List Entries (async iterator)

```typescript
client.cron.entries.listIterator(user: string, page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `user` | `string` | path | Yes | System username |
| `page` | `integer` | query | No | Page number (1-based) |
| `limit` | `integer` | query | No | Items per page (max 200) |

**Returns:** `AsyncIterableIterator<cron_EntryListResponse>`  |  **HTTP:** `GET /users/{user}/entries`
**CLI:** `hoody cron entries list`

---

#### `update` — Update Entry

```typescript
client.cron.entries.update(user: string, id: string, data: cron_UpdateEntryRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `user` | `string` | path | Yes | System username |
| `id` | `string` | path | Yes | Managed entry id |
| `data` | `cron_UpdateEntryRequest` | body | Yes |  |

**Returns:** `cron_ManagedEntryResponse`  |  **HTTP:** `PATCH /users/{user}/entries/{id}`
**CLI:** `hoody cron entries update`

---

### `client.cron.health` (1) — Health

#### `check` — Health Check

```typescript
client.cron.health.check()
```

**Returns:** `cron_HealthResponse`  |  **HTTP:** `GET /health`
**CLI:** `hoody cron health`

---

### `client.cron.system` (2) — System endpoints

#### `getOpenApiJson` — Get Open Api Json

```typescript
client.cron.system.getOpenApiJson()
```

**Returns:** `any`  |  **HTTP:** `GET /openapi.json`

---

#### `getOpenApiYaml` — Get Open Api Yaml

```typescript
client.cron.system.getOpenApiYaml()
```

**Returns:** `any`  |  **HTTP:** `GET /openapi.yaml`


### Body schemas

- `cron_RawCrontabRequest` — `{ crontab*: string }`
- `cron_CreateEntryRequest` — `{ command*: string, comment: string|null, enabled: bool|null, expires_at: string|null, name: string|null, schedule*: string }`
- `cron_UpdateEntryRequest` — `{ clear_expiration: bool|null, command: string|null, comment: string|null, enabled: bool|null, expires_at: string|null, name: string|null, schedule: string|null }`


---

<!-- ===== namespace: curl ===== -->

# `curl` — full HTTP client gateway + REST-as-GET-URL bridge

## Purpose

Full HTTP client gateway. Sync/async jobs, cookie jars, bodies to storage, cron schedules. **Killer use case: turn any REST request — POST / PUT / PATCH / DELETE with bodies and headers — into a single GET-able URL** that works in a browser tab, a webhook field that only takes a URL, an LLM tool with web-search-only access, an `<img src>` / `<a href>`, or any environment that can't issue a non-GET request. The kit takes care of the actual HTTP call; the caller just hits a query-string URL.

## When to use

- **REST-as-GET bridge** — any environment that can only do GET (browsers, restricted webhooks, agents with only "fetch URL" capability, RSS-style schedulers, copy-pasteable links). See workflow #1 for the URL recipe.
- A full HTTP client surface (TLS, client certs, HTTP/2/3, proxies, retries) when you can't / don't want to use `fetch()`.
- Long downloads as background jobs.
- Multi-step auth with cookie jars (server-side session reused across hits).
- Recurring HTTP (pings, scrapes, webhooks) on a cron.

## When NOT to use

Not for: browser → `browser`, shell → `exec`/`terminal`, KV/SQL → `sqlite`, files → `files`, non-HTTP timers → `cron`.

## Prerequisites

- Instance `1`; no workspace ID.
- `schedules.*` 404s if disabled.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Convert any REST call into a single GET-able URL (`executeCurlRequestGet`)

`GET /api/v1/curl/request?url=<TARGET>&method=<VERB>` on the curl kit URL. The kit executes the upstream request and returns a JSON envelope `{ success, job_id, status_code, headers, body, is_binary, timing, metadata }`. Useful when the caller can only emit a GET (browser, webhook, sandboxed agent, RSS-ish puller, link in an email).

Note: the GET bridge accepts `url` + `method` + the 13 timing/follow/session/response/save flags (`response`, `mode`, `session_id`, `follow_redirects`, `timeout`, `user_agent`, `referer`, `bearer_token`, `save`, `save_path`, `insecure`, `compressed`, `job_name`) **AND a full request body + headers right in the query string**: `data` (raw body, curl `--data`), `json` (parsed JSON; sets `Content-Type: application/json`), `data_base64` (binary-safe; standard OR URL-safe base64, padding optional; takes precedence over `data`/`json`), and repeatable `header=Name: Value`. **Supplying a body auto-upgrades the default method GET→POST** — so a body-bearing POST/PUT/PATCH (with headers) is expressible as a single GET URL. Only `form`/multipart and binary `--data-binary @file` uploads remain POST-only.

Live examples (verified — replace the kit URL with your container's):

- Plain GET upstream: `https://{P}-{C}-curl-1.{N}.containers.hoody.com/api/v1/curl/request?url=https://httpbin.org/get`
- HEAD upstream: `https://{P}-{C}-curl-1.{N}.containers.hoody.com/api/v1/curl/request?url=https://httpbin.org/get&method=HEAD`

Combine with `proxyAliases.create({ program: 'curl' })` to give the bridge a brandable hostname like `https://api-bridge.{server_name}.containers.hoody.com/api/v1/curl/request?...` and hide the `containerId`.

The CLI command and the SDK accessor `client.curl.executeCurlRequestGet` **execute** the request and return the envelope; they do NOT just compose a URL string. To compose a URL without firing it, build it client-side or use `proxyAliases.create({ program: 'curl', target_path: '/api/v1/curl/request' })` to get a stable prefix.

For the imperative full-cURL surface (binary uploads, `--data-binary @file`, multipart, follow-redirects, custom TLS, etc.) use the POST form below — though note the kit's request validator rejects `cacert`/`cert`/`key`/`proxy`/`proxy_user`/`proxy_password` (the rejected fields are limited to those six; all other body/auth/connection fields are accepted).

### 2. Sync request

`execute` with `mode:"sync"` (default), `response:"json"` (envelope) or `"transparent"` (raw).

### 3. Async job

1. `execute` with `mode:"async"` → `job_id`.
2. Poll `jobs.get` or subscribe `events.streamWs` filtered by `job_id`.
3. `jobs.getResult`; `jobs.cancel` aborts.

### 4. Cookie-jar session

1. `execute` with `session_id:"<id>"` auto-creates jar.
2. Reuse same `session_id` on follow-ups.
3. `sessions.getCookies` / `sessions.delete`.

### 5. Save download

1. `execute` with `save:true` and optional relative `save_path` under `downloads/by-job/{job_id}/`.
2. `storage.list`/`getFile`/`deleteFile` with relative path (e.g. `by-job/<uuid>/x.pdf`).

### 6. Scheduled request

1. `schedules.create` with `{cron,request}` → `schedule_id`.
2. `schedules.list`/`schedules.get`/`schedules.toggle` (`{"enabled":bool}`)/`schedules.delete`.
3. Each firing creates a job; inspect via `jobs.list`.

## Quirks & gotchas

- Default `response`: POST→`transparent`, GET→`json`.
- Default `mode:"sync"`; pass `"async"` for `job_id`.
- `save_path` rejected if empty, absolute, rooted, or has `..`.
- Saved files at `downloads/by-job/{job_id}/...`; pass relative path.
- **Each saved download is mirrored under three indexes** for navigation: `by-job/{job_id}/<save_path>`, `by-domain/<host>/<job_id>`, `by-date/<YYYY-MM-DD>/<job_id>`. `storage.list` returns one item per index path; the bytes are the same file (live-verified — `storage.list?limit=5` after one save returns 3 items pointing to the same content).
- `*.list` returns ALL when `limit` omitted; always pass `limit`.
- `schedules.*` 404s if disabled.
- `schedules.toggle` needs explicit boolean `enabled`; else 400.
- **`schedules.create.cron` is 6-field (with seconds), NOT the standard 5-field crontab.** `*/15 * * * *` is rejected as `Invalid cron expression`; use `0 */15 * * * *` (at second 0 every 15 min). The standard @-nicknames (`@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`) ARE accepted (expanded internally to 6-field), but Go-style `@every 15m` is NOT — for anything else use explicit 6-field expressions. Different syntax from the `cron` namespace, which uses Vixie 5-field.
- `session_id` is caller-provided.
- `events.streamWs` is WebSocket `/api/v1/curl/ws`; filter by `job_id`.

## Common errors

- `408 timeout` — raise timeout or use async (upstream libcurl timeouts surface as `504` instead).
- `410 cancelled`.
- `503 queue full` (also SSE capacity exhausted) — back off.

## Related namespaces

`browser` (JS/DOM), `exec` (shell), `cron` (timers), `files` (general IO), `sqlite` (parsed data).

## Examples

Every step in every example was live-tested against a real `curl-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first.

### 1. Webhook receiver bridge — outbound system can only fire GETs

**Goal:** your CRM can fire URLs but not POST. Translate a click → real upstream POST with JSON body + bearer.

**Step 1 — compose the bridge URL — body and headers go right in the query string.** The GET bridge takes `url`, `method`, the timing/session/response flags (`response`, `mode`, `session_id`, `timeout`, `bearer_token`, …), **plus the request body + headers**: `data` (raw body), `json` (JSON body; sets `Content-Type: application/json`), `data_base64` (binary-safe base64, standard or URL-safe, precedence over `data`/`json`), and repeatable `header=Name: Value`. **Supplying a body auto-upgrades the method GET→POST.** So a real `POST … {json} + headers` becomes one GET-able link a CRM/webhook can fire:

```
# Full POST as ONE GET URL (json body + header; method auto-upgrades to POST):
https://${P}-${C}-curl-1.${N}.containers.hoody.com/api/v1/curl/request?url=<urlencoded-target>&json=%7B%22event%22%3A%22X%22%7D&header=Authorization:%20Bearer%20XYZ
# For payloads with &, quotes, newlines, or binary, prefer data_base64 (URL-safe base64) to dodge escaping:
https://${P}-${C}-curl-1.${N}.containers.hoody.com/api/v1/curl/request?url=<target>&data_base64=eyJldmVudCI6IlgifQ&header=Content-Type:%20application/json
```

(`form`/multipart and binary `--data-binary @file` uploads remain POST-only — use the POST form below for those.)

```typescript
const r = await client.curl.execute({
  url: 'https://my-api/events',
  method: 'POST',
  data: JSON.stringify({ event: 'X' }),
  headers: { 'Content-Type': 'application/json' },
  response: 'json',   // POST defaults to TRANSPARENT — without this, r.data is the raw
});                   // upstream body and status_code is undefined
console.log(r.data!.status_code);
```
**Step 2 — hide the `containerId` behind a proxy alias.** Now `https://webhook-bridge.{server_name}.containers.hoody.com/api/v1/curl/request?...` becomes the public URL.

```typescript
await client.api.proxyAliases.create({
  container_id: C,
  alias: 'webhook-bridge',
  program: 'curl',
  target_path: '/api/v1/curl/request',
  allow_path_override: true,
});
```
### 2. Multi-step OAuth login — cookie jar reuse across hits

**Goal:** authenticate against an API that uses a CSRF token + session cookie, then issue an authorized call. Pick a unique `session_id` per flow — once deleted, the same id returns `404 Session not found: <id> (tombstoned)` until the tombstone is garbage-collected (~24 h), after which the id is reusable again.

**Step 1 — fetch CSRF.** The Set-Cookie / response cookies are stored in the kit's jar.

```typescript
const sid = `oauth-${Date.now()}`;
const csrf = await client.curl.execute({
  url: 'https://api.example.com/csrf', method: 'GET', session_id: sid, response: 'json',
});
const token = JSON.parse(csrf.data!.body).csrf_token;
```
**Step 2 — submit login.** The session cookie returned by the upstream is auto-stored in the same jar.

```typescript
await client.curl.execute({
  url: 'https://api.example.com/login', method: 'POST',
  data: `username=alex&password=secret&csrf=${token}`,
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  session_id: sid,
});
```
**Step 3 — authorized call.** Stored cookie is auto-attached.

```typescript
const me = await client.curl.execute({
  url: 'https://api.example.com/me', method: 'GET', session_id: sid, response: 'json',
});
console.log(me.data!.body);
const cookies = await client.curl.sessions.getCookies(sid);
await client.curl.sessions.delete(sid);
```
### 3. Fan-out — submit 3 async jobs, await all, collect results

**Goal:** fetch from 3 upstreams in parallel, combine the results.

**Step 1 — submit each, capture `job_id`s.**

```typescript
const urls = ['https://httpbin.org/delay/1', 'https://httpbin.org/delay/2', 'https://httpbin.org/get'];
const submits = await Promise.all(urls.map(url =>
  client.curl.execute({ url, method: 'GET', mode: 'async' })
));
const jobIds = submits.map(s => s.data!.job_id);
```
**Step 2 — poll until all complete.** Live-verified — 3 httpbin jobs reached `completed/200` within ~4 s.

```typescript
async function waitAll(ids: string[]) {
  while (true) {
    const states = await Promise.all(ids.map(id => client.curl.jobs.get(id)));
    if (states.every(s => s.data.status === 'completed')) return;
    await new Promise(r => setTimeout(r, 1000));
  }
}
await waitAll(jobIds);
```
**Step 3 — collect bodies.** `jobs.getResult` returns just the upstream body.

```typescript
const bodies = await Promise.all(jobIds.map(id => client.curl.jobs.getResult(id)));
```
### 4. Cancel a runaway long-poll mid-flight

**Goal:** kill a hung async request, free the queue slot. Status flips from `running` to `cancelled`, and the cancelled job's `error` field is set to `Cancelled`.

```typescript
const sub = await client.curl.execute({
  url: 'https://httpbin.org/delay/30', method: 'GET', mode: 'async', timeout: 60,
});
const jid = sub.data!.job_id;
await new Promise(r => setTimeout(r, 1000));
await client.curl.jobs.cancel(jid);
await new Promise(r => setTimeout(r, 1000));
const status = await client.curl.jobs.get(jid);
// status.data.status === 'cancelled', status.data.error === 'Cancelled'
```
### 5. Schedule + drift detection — fire every 15 min, audit history

**Goal:** ping a health endpoint every 15 min, fast-find failures. ⚠ Scheduler uses **6-field** cron syntax (with seconds) — `*/15 * * * *` (5-field) is rejected as `Invalid cron expression`.

**Step 1 — create.**

```typescript
const r = await client.curl.schedules.create({
  cron: '0 */15 * * * *',
  request: { url: 'https://prod.example.com/health', method: 'GET', job_name: 'prod-health' },
});
const scheduleId = r.data!.schedule_id;
```
**Step 2 — audit failures.**

```typescript
const r = await client.curl.jobs.list({ limit: 200 });
const phFailures = r.data.items.filter(j => j.status === 'failed' && j.name === 'prod-health');
```
**Step 3 — pause during deploy** (toggle `enabled: false` and back, or `delete` to drop entirely):

```typescript
await client.curl.schedules.toggle(scheduleId, { enabled: false });
// Resume:    await client.curl.schedules.toggle(scheduleId, { enabled: true });
// Drop:      await client.curl.schedules.delete(scheduleId);
```
### 6. Background download → kit storage → fetch later

**Goal:** pull a 1 GB ISO without blocking the caller; access bytes from elsewhere later.

**Step 1 — submit async + save.**

```typescript
const sub = await client.curl.execute({
  url: 'https://example.com/big.iso', method: 'GET', mode: 'async',
  save: true, save_path: 'iso/ubuntu.iso', timeout: 600,
});
const jid = sub.data!.job_id;
```
**Step 2 — wait + inspect storage.** Three index entries point at the SAME bytes (`by-job/`, `by-domain/`, `by-date/`).

```typescript
while ((await client.curl.jobs.get(jid)).data.status !== 'completed') {
  await new Promise(r => setTimeout(r, 2000));
}
const idx = await client.curl.storage.list({ limit: 10 });
```
**Step 3 — fetch & delete.** Single delete on ANY of the three mirror paths removes all three (live-verified — others return `404` afterwards).

```typescript
const bytes = await client.curl.storage.getFile(`by-job/${jid}/iso/ubuntu.iso`);
await client.curl.storage.deleteFile(`by-job/${jid}/iso/ubuntu.iso`);
```
### 7. Bearer-authenticated upstream — header auto-injection

**Goal:** call the GitHub API with a token without composing the Authorization header. Live-verified against `httpbin.org/bearer` (`{"authenticated":true,"token":"…"}`).

```typescript
const r = await client.curl.execute({
  url: 'https://api.github.com/user', method: 'GET',
  bearer_token: 'ghp_xxxxxxxxxxxx', response: 'json',
});
const remaining = r.data!.headers['x-ratelimit-remaining'];
```
**HTTP Basic alternative** — swap the auth fields. Body `{ url, method, auth_user, auth_password, auth_method: 'basic' }`. Live-verified against `httpbin.org/basic-auth/alex/secret` → `{"authenticated":true,"user":"alex"}`.

### 8. REST→GET bridge for chat-channel embedding

**Goal:** drop a one-liner URL into Slack so a teammate can re-trigger a build by clicking. URL pattern + alias + IP gate.

**Step 1 — compose** (no kit call — URL pattern). The GET bridge carries the full request in the query string — `url`, `method`, body via `data`/`json`/`data_base64`, and repeatable `header=Name: Value` (a body auto-upgrades the method to POST). So a build-trigger that needs a JSON body + auth header is still one clickable link.

```
# GET-bridge URL — a real POST (json body + bearer header) as a single clickable link:
https://${P}-${C}-curl-1.${N}.containers.hoody.com/api/v1/curl/request?url=<url-encoded-build-trigger>&json=%7B%22ref%22%3A%22main%22%7D&header=Authorization:%20Bearer%20XYZ
```

**Step 2 — wrap with an alias** so the public URL hides `containerId`:

```typescript
await client.api.proxyAliases.create({
  container_id: C,
  alias: 'rebuild-main',
  program: 'curl',
  target_path: '/api/v1/curl/request?url=https%3A%2F%2Fci.example.com%2Fbuild&method=POST',
  allow_path_override: false,
});
```
**Step 3 — gate it** — only your office IPs can fire it (uses `proxyPermissionsContainer.setIpGroup`; see the `api` namespace).

### 9. Recover a result from yesterday's scheduled job

**Goal:** a scheduled scrape ran 18 hours ago; you want the body now. Job records are NOT auto-expired by the kit — they persist until the container's storage is cleared.

**Step 1 — find the right job** (the schedule was created with `request.job_name: 'prod-health'`):

```typescript
const list = await client.curl.jobs.list({ limit: 200 });
const jid = list.data.items.find(j => j.status === 'completed' && j.name === 'prod-health')!.id;
```
**Step 2 — fetch.** `jobs.getResult` returns just the upstream body; `jobs.get` returns the full record (timing, headers, original request).

```typescript
const body = await client.curl.jobs.getResult(jid);
const full = await client.curl.jobs.get(jid);
```
### 10. Storage triage — purge files older than N days

**Goal:** keep storage tidy by deleting old downloads. Use the `by-date/` index because the date is in the path.

```typescript
const cutoff = new Date(Date.now() - 30 * 86400_000).toISOString().slice(0, 10);
const r = await client.curl.storage.list({ limit: 200 });
const old = r.data.items.filter(i => i.path.startsWith('by-date/') && i.path.split('/')[1] < cutoff);
await Promise.all(old.map(i => client.curl.storage.deleteFile(i.path)));
```

## Reference

**Accessor:** `client.curl`  |  **Import:** `import * as curl from 'hoody-sdk/curl'`

### `client.curl` (2) — cURL execution endpoints

#### `execute` — Execute HTTP request with full cURL capabilities

```typescript
client.curl.execute(data: curl_CurlRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `curl_CurlRequest` | body | Yes |  |

**Returns:** `curl_JsonResponse`  |  **HTTP:** `POST /api/v1/curl/request`
**CLI:** `hoody curl exec`

---

#### `executeCurlRequestGet` — Execute simple HTTP request via query parameters

```typescript
client.curl.executeCurlRequestGet(url: string, method?: string, response?: string, mode?: string, session_id?: string, follow_redirects?: boolean, timeout?: integer, user_agent?: string, referer?: string, bearer_token?: string, save?: boolean, save_path?: string, insecure?: boolean, compressed?: boolean, job_name?: string, data?: string, json?: string, header?: array, data_base64?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `url` | `string` | query | Yes | Target URL (required) |
| `method` | `string` | query | No | HTTP method (default: GET) |
| `response` | `string` | query | No | Response mode: transparent or json (default: json) |
| `mode` | `string` | query | No | Execution mode: sync or async (default: sync) |
| `session_id` | `string` | query | No | Session ID for cookie persistence |
| `follow_redirects` | `boolean` | query | No | Follow redirects (default: true) |
| `timeout` | `integer` | query | No | Timeout in seconds |
| `user_agent` | `string` | query | No | User-Agent header |
| `referer` | `string` | query | No | Referer header |
| `bearer_token` | `string` | query | No | Bearer token |
| `save` | `boolean` | query | No | Save to storage |
| `save_path` | `string` | query | No | Custom save path, relative to downloads/by-job/{job_id} (no absolute paths or `..`) |
| `insecure` | `boolean` | query | No | Allow insecure SSL |
| `compressed` | `boolean` | query | No | Request compressed |
| `job_name` | `string` | query | No | Job name for async |
| `data` | `string` | query | No | Raw request body (curl --data); alias `body`; presence upgrades default method to POST |
| `json` | `string` | query | No | JSON request body, sent with Content-Type: application/json (curl --json); upgrades default method to POST |
| `header` | `array` | query | No | Custom header as `Name: Value`. Repeatable — supply once per header |
| `data_base64` | `string` | query | No | Base64 request body (binary-safe; standard or URL-safe); alias `body_base64`. Takes precedence over data/json; upgrades default method to POST |

**Returns:** `curl_JsonResponse`  |  **HTTP:** `GET /api/v1/curl/request`
**CLI:** `hoody curl get-url`

---

### `client.curl.events` (3) — WebSocket event endpoints

#### `sseJobEvents` — Subscribe to job events over Server-Sent Events

```typescript
client.curl.events.sseJobEvents(job_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `job_id` | `string` | query | No | Optional job ID filter |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/curl/sse`

---

#### `streamWs` — Subscribe to job events over WebSocket

```typescript
client.curl.events.streamWs(job_id?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `job_id` | `string` | query | No | Optional job ID filter |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/curl/ws`
**CLI:** `hoody curl jobs events`

---

#### `wsRequestChannel` — Execute cURL requests over a WebSocket channel

```typescript
client.curl.events.wsRequestChannel(max_concurrent?: integer, max_concurrent_streams?: integer, max_pool?: integer, max_queue?: integer, max_frame_bytes?: integer, max_request_bytes?: integer, chunk_bytes?: integer, stream_timeout_secs?: integer, idle_timeout_secs?: integer, max_outbound_messages?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `max_concurrent` | `integer` | query | No | Alias for max concurrent streams on this channel connection |
| `max_concurrent_streams` | `integer` | query | No | Maximum concurrently executing streams on this channel connection |
| `max_pool` | `integer` | query | No | Alias for max_concurrent; does not configure outbound libcurl connection pooling |
| `max_queue` | `integer` | query | No | Maximum queued streams waiting for a per-connection execution slot |
| `max_frame_bytes` | `integer` | query | No | Maximum inbound WebSocket text frame size in bytes |
| `max_request_bytes` | `integer` | query | No | Maximum assembled request JSON size in bytes |
| `chunk_bytes` | `integer` | query | No | Maximum upstream response bytes encoded into one channel body frame |
| `stream_timeout_secs` | `integer` | query | No | Per-stream execution timeout in seconds |
| `idle_timeout_secs` | `integer` | query | No | Idle channel timeout in seconds |
| `max_outbound_messages` | `integer` | query | No | Maximum queued outbound channel messages |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/curl/channel`

---

### `client.curl.health` (1) — Health

#### `check` — Service health check

```typescript
client.curl.health.check()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/curl/health`
**CLI:** `hoody curl health`

---

### `client.curl.jobs` (6) — Job management endpoints

#### `cancel` — Cancel a pending or running job

```typescript
client.curl.jobs.cancel(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique job identifier (UUID format) |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/curl/jobs/{id}`
**CLI:** `hoody curl jobs cancel`

---

#### `get` — Get detailed job information

```typescript
client.curl.jobs.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique job identifier (UUID format) |

**Returns:** `curl_Job`  |  **HTTP:** `GET /api/v1/curl/jobs/{id}`
**CLI:** `hoody curl jobs get`

---

#### `getResult` — Get job response body

```typescript
client.curl.jobs.getResult(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique job identifier (UUID format) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/curl/jobs/{id}/result`
**CLI:** `hoody curl jobs result`

---

#### `list` — List all async jobs

```typescript
client.curl.jobs.list(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `curl_PaginatedJobSummaries`  |  **HTTP:** `GET /api/v1/curl/jobs`
**CLI:** `hoody curl jobs list`

---

#### `listAll` — List all async jobs (collect all pages)

```typescript
client.curl.jobs.listAll(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `curl_PaginatedJobSummaries[]`  |  **HTTP:** `GET /api/v1/curl/jobs`
**CLI:** `hoody curl jobs list`

---

#### `listIterator` — List all async jobs (async iterator)

```typescript
client.curl.jobs.listIterator(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `AsyncIterableIterator<curl_PaginatedJobSummaries>`  |  **HTTP:** `GET /api/v1/curl/jobs`
**CLI:** `hoody curl jobs list`

---

### `client.curl.ops` (1) — Operational endpoints (health and metrics)

#### `metrics` — Prometheus metrics

```typescript
client.curl.ops.metrics()
```

**Returns:** `any`  |  **HTTP:** `GET /metrics`
**CLI:** `hoody curl metrics`

---

### `client.curl.schedules` (7) — Schedule management endpoints

#### `create` — Create a recurring scheduled job

```typescript
client.curl.schedules.create(data: curl_CreateScheduleRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `curl_CreateScheduleRequest` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/curl/schedule`
**CLI:** `hoody curl schedules create`

---

#### `delete` — Delete a schedule

```typescript
client.curl.schedules.delete(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique schedule identifier |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/curl/schedule/{id}`
**CLI:** `hoody curl schedules delete`

---

#### `get` — Get schedule details

```typescript
client.curl.schedules.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique schedule identifier (UUID format) |

**Returns:** `curl_ScheduledJob`  |  **HTTP:** `GET /api/v1/curl/schedule/{id}`
**CLI:** `hoody curl schedules get`

---

#### `list` — List all scheduled jobs

```typescript
client.curl.schedules.list(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `curl_PaginatedSchedules`  |  **HTTP:** `GET /api/v1/curl/schedule`
**CLI:** `hoody curl schedules list`

---

#### `listAll` — List all scheduled jobs (collect all pages)

```typescript
client.curl.schedules.listAll(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `curl_PaginatedSchedules[]`  |  **HTTP:** `GET /api/v1/curl/schedule`
**CLI:** `hoody curl schedules list`

---

#### `listIterator` — List all scheduled jobs (async iterator)

```typescript
client.curl.schedules.listIterator(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `AsyncIterableIterator<curl_PaginatedSchedules>`  |  **HTTP:** `GET /api/v1/curl/schedule`
**CLI:** `hoody curl schedules list`

---

#### `toggle` — Enable or disable a schedule

```typescript
client.curl.schedules.toggle(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Unique schedule identifier |
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/curl/schedule/{id}/toggle`
**CLI:** `hoody curl schedules toggle`

---

### `client.curl.sessions` (6) — Session management endpoints

#### `delete` — Delete a session

```typescript
client.curl.sessions.delete(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Session identifier to delete |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/curl/sessions/{id}`
**CLI:** `hoody curl sessions delete`

---

#### `get` — Get session details

```typescript
client.curl.sessions.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Session identifier (caller-provided string) |

**Returns:** `curl_Session`  |  **HTTP:** `GET /api/v1/curl/sessions/{id}`
**CLI:** `hoody curl sessions get`

---

#### `getCookies` — Get session cookies only

```typescript
client.curl.sessions.getCookies(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Session identifier |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/curl/sessions/{id}/cookies`
**CLI:** `hoody curl sessions cookies`

---

#### `list` — List all cookie sessions

```typescript
client.curl.sessions.list(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `curl_PaginatedSessions`  |  **HTTP:** `GET /api/v1/curl/sessions`
**CLI:** `hoody curl sessions list`

---

#### `listAll` — List all cookie sessions (collect all pages)

```typescript
client.curl.sessions.listAll(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `curl_PaginatedSessions[]`  |  **HTTP:** `GET /api/v1/curl/sessions`
**CLI:** `hoody curl sessions list`

---

#### `listIterator` — List all cookie sessions (async iterator)

```typescript
client.curl.sessions.listIterator(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `AsyncIterableIterator<curl_PaginatedSessions>`  |  **HTTP:** `GET /api/v1/curl/sessions`
**CLI:** `hoody curl sessions list`

---

### `client.curl.storage` (5) — Storage management endpoints

#### `deleteFile` — Delete a saved file

```typescript
client.curl.storage.deleteFile(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Relative path to file in storage |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/curl/storage/{path}`
**CLI:** `hoody curl storage delete`

---

#### `getFile` — Download a saved file

```typescript
client.curl.storage.getFile(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Relative path to file in storage (supports nested paths) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/curl/storage/{path}`
**CLI:** `hoody curl storage get`

---

#### `list` — List all saved downloads

```typescript
client.curl.storage.list(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `curl_PaginatedStorageEntries`  |  **HTTP:** `GET /api/v1/curl/storage`
**CLI:** `hoody curl storage list`

---

#### `listAll` — List all saved downloads (collect all pages)

```typescript
client.curl.storage.listAll(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `curl_PaginatedStorageEntries[]`  |  **HTTP:** `GET /api/v1/curl/storage`
**CLI:** `hoody curl storage list`

---

#### `listIterator` — List all saved downloads (async iterator)

```typescript
client.curl.storage.listIterator(page?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer` | query | No | 1-based page number (optional) |
| `limit` | `integer` | query | No | Items per page (optional; current handler returns all items when omitted) |

**Returns:** `AsyncIterableIterator<curl_PaginatedStorageEntries>`  |  **HTTP:** `GET /api/v1/curl/storage`
**CLI:** `hoody curl storage list`


### Body schemas

- `curl_CurlRequest` — `{ auth_method: string|null, auth_password: string|null, auth_user: string|null, bearer_token: string|null, cacert: string|null, cert: string|null, cert_type: string|null, compressed: bool|null, connect_timeout: int|null, cookie: string|null, data: string|null, follow_redirects: bool|null, form: { [key: string]: string }|null, headers: { [key: string]: string }|null, insecure: bool|null, job_name: string|null, json: any, keepalive: bool|null, keepalive_time: int|null, key: string|null, max_filesize: int|null, max_redirects: int|null, method: string|null, mode: null | curl_ExecutionMode, proxy: string|null, proxy_password: string|null, proxy_user: string|null, range: string|null, referer: string|null, response: null | curl_ResponseMode, retry_count: int|null, retry_delay: int|null, save: bool|null, save_path: string|null, schedule: string|null, session_id: string|null, speed_limit: int|null, speed_time: int|null, tcp_nodelay: bool|null, timeout: int|null, url*: string, user_agent: string|null }`
- `curl_CreateScheduleRequest` — `{ cron*: string, request*: curl_CurlRequest }`
- `curl_ExecutionMode` — `"sync" | "async"`
- `curl_ResponseMode` — `"transparent" | "json"`


---

<!-- ===== namespace: daemon ===== -->

# `daemon` — supervisord program lifecycle (start any program; logs always retained)

## Purpose

**Default for "start a program" / "spawn a process"** when you don't need an interactive shell or a TUI. REST over `supervisord` — every process is supervised, auto-restart-eligible, log-captured (stdout + stderr written under `/hoody/storage/hoody-daemon/logs/<name>/stdout.log` and `/hoody/storage/hoody-daemon/logs/<name>/stderr.log` — one directory per program, with rotated timestamped log files behind those symlinks), and inspectable after the fact. Log FILES outlive the process on disk, but the ephemeral tracking entry is reaped on stop/exit — after that `quickStart.getEphemeralLogs` 404s (see Quirks); capture logs before stopping, or read the on-disk files directly (e.g. via the `files` namespace). `status.getLogs` covers configured (non-ephemeral) programs.

Two flavours:

- **Quick-start (ephemeral, no config write)** — `quickStart.launch { command, user, ttl?, wait?, timeout? }`. Returns `temporary_id = quick_<ts>_<seq>`. Best for one-offs and short-lived jobs (build steps, batch transforms, "run this once and tell me the output"). Logs survive the process; pull with `quickStart.getEphemeralLogs`. Optional `ttl` auto-stops after N seconds.
- **Registered program (durable, persists across kit restarts)** — `programs.add { name, command, user, enabled: true, boot: true?, autorestart: 'unexpected', … }` → `control.start { wait: true }`. Use this when the process should come back after a container restart, when you want auto-restart on crash, or when you need port-range fan-out / lazy-load on first proxy hit.

## When to use

- "Run this command and keep the logs" → `quickStart.launch`.
- "Run this server / agent / script as a long-running supervised process, restart on failure" → `programs.add` + `control.start`.
- Background workers, port-range fan-out, lazy-loaded HTTP services, supervisord-event webhooks.

## When NOT to use

- **Traditional system services that ship native systemd units** (apache2, nginx, postgresql, mysql, redis, mosquitto, sshd, postfix, …) — leave them on `systemd`. Hoody containers are full Linux boxes with systemd + root (they behave like VMs, not Docker), so the standard `apt install nginx && systemctl enable --now nginx` flow Just Works and benefits from the upstream unit's hardening (drop-in directories, sd_notify, journal integration, etc.). Mixing systemd-managed and `daemon`-managed processes in the same container is fine — pick whichever fits the program.
- Need an interactive TTY (Claude Code, Codex, htop, vim, anything that paints the screen) → use `terminal` with a **pinned non-ephemeral `terminal_id`**, NOT `daemon`. Daemon programs have no TTY.
- Need to pipe input mid-run / send keystrokes → `terminal` (`pressTerminalKeys`, `pasteTerminalText`).
- One-shot synchronous request/response → `exec` (HTTP handler, returns body).
- Schedule (cron syntax) → `cron`. Access logs → `proxyLogs`. File-system events → `watch`.

### When to prefer `daemon` over `systemd`

- Custom scripts and binaries you wrote that don't have a packaged unit.
- Quick experiments where you want REST-driven start/stop/log without writing a unit file.
- Port-range fan-out (`port_range` + `port_param` + `lazy_load`) — supervisord-side feature, not a systemd one.
- Programs you want to provision / mutate / remove via the Hoody API (CI scripts, multi-tenant container fleets) — `programs.add` is one HTTP call.

### When to prefer `systemd` over `daemon`

- Any service whose Debian/Ubuntu package already drops a working unit in `/lib/systemd/system/` (most server software).
- You want `journalctl -u <service>`, `systemctl status`, drop-in overrides, socket-activated services, timers (cron-equivalent), or any other systemd feature.
- The program is part of the container's "default-on" baseline (boots with the container, never managed externally).

## Prerequisites

- `/dev/hoody`, `/hoody`, `LOG_BASE_DIR=/hoody/storage/hoody-daemon/logs/` exist.
- `user` = real system account.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Register and boot

- `programs.add` (`name`/`command`/`user`; `enabled`, `boot`; opt `directory`/`environment`/`autorestart`/`priority`/`*_logfile`) -> `control.start` `{ wait: true, timeout: 30 }` -> `status.get` (`include_stats`); `status.getLogs` on failure.

### 2. Ephemeral with TTL

- `quickStart.launch` (`command`/`user`, opt `ttl?`/`wait?`/`timeout?`) returns `temporary_id` = `quick_<ts>_<seq>`. Poll/tail with `quickStart.getStatus`/`getEphemeralLogs`; `quickStart.stop` to terminate.

### 3. Lazy port-range fleet

- `programs.add` + `port_range: { start, end }`, `port_param`, `lazy_load: true`, `enabled: true`. `programs.list?port=8042&include_status=true`. `control.start` `{ port: 8042 }`; `control.stop` `{ port }` or `{ all: true }`.

### 4. Reach a service you just started

Any container port is publicly addressable as soon as the listener is up — no proxy alias, firewall edit, or extra registration needed:

- HTTP service on `:8080` → `https://{projectId}-{containerId}-http-8080.{node}.containers.hoody.com`
- HTTPS service on `:8443` → `https://{projectId}-{containerId}-https-8443.{node}.containers.hoody.com`

Edge is always `https://`; the slug only describes the inner protocol. Gate access via `proxyPermissionsContainer.*` if it shouldn't be public. See § Proxy URLs.

### 5. Update / wipe

`programs.edit` field-merge; `control.disable`/`enable`; `programs.remove`; `programs.reset` -> `programs.default.json`.

## Quirks & gotchas

- Boolean query params (`hoody_kit`, `lazy_load`, `enabled`, `boot`, `include_status`, `include_stats`) STRICT: only `true`/`false`; `1`/`yes`/`0`/`""` -> 400.
- Webhook URLs HTTPS-only unless `NODE_ENV=development` (which also skips every host check below). Userinfo is always rejected; in production the literal label `localhost` and private/CGNAT/link-local/v6-ULA addresses are too, including when written as NAT64, v4-mapped-v6, or non-standard v4 (`2130706433`, `0x7f000001`, `127.1`) — but only when the URL authority is already an IP literal or `localhost`. A DNS name is accepted whatever it resolves to, so treat this as input validation, not an egress control. Redirects are never followed.
- Webhook edits deep-merge (omitted fields NOT cleared); events need `notifications.enabled` + `event_listener_enabled` + supervisord `eventlistener`.
- Duplicate names + overlapping port ranges rejected on create AND update (adjacent OK); `port_param` requires `port_range`.
- `command` no newlines/CR/NUL; `user` `(?i)[a-z_][a-z0-9_-]*\$?` (case-insensitive) via `id`; `*_logfile` confined to `LOG_BASE_DIR`.
- `control.start` on `port_range` REQUIRES `{ port }`; `control.stop` `{ port }` or `{ all: true }`.
- `quick_<ms>_<seq>` IDs (e.g. `quick_1700000000000_1`) are produced by the kit (which accepts `^quick_[a-zA-Z0-9_]+$` via `sanitize_temp_id`) but the OpenAPI path-param schema is the stricter `^quick_\d+$`, and the generated **TypeScript SDK enforces it client-side** — the CLI and raw HTTP pass the full id straight to the kit and work fine. In SDK mode, drive `quickStart.getStatus` / `quickStart.stop` via raw HTTP to avoid the local validator, OR call `quickStart.list` and select by `name`. TTL polled ~10 s.
- Default ephemeral log paths are `/hoody/storage/hoody-daemon/logs/<name>/stdout.log` and `/hoody/storage/hoody-daemon/logs/<name>/stderr.log` (one directory per program), NOT `<name>.out.log`/`<name>.err.log`.
- After `quickStart.stop`, the in-memory tracking entry is removed (the on-disk log files persist but are unreachable through `getEphemeralLogs`, which returns `404`). Capture logs (read `quickStart.getEphemeralLogs` or fetch the on-disk file directly) BEFORE calling `stop`.
- `control.start` accepts `if_not_running: true` for an idempotent boot — early-returns with `already_running: true` if the program is already running (port-range responses include an `instance` block with per-instance status/pid; standard programs return `instance: null` — no pid). Use it for "ensure started" workflows.
- **`environment` REPLACES the whole map** on `programs.edit` (not per-key merge). If the existing env is `{A:1,B:2}` and you PATCH `{environment:{A:9}}`, the result is `{A:9}`. To preserve secrets, GET the program first and re-send the merged map.
- Webhook delivery requires THREE flags ON: per-program `webhooks.enabled: true`, kit-level `notifications.enabled: true`, AND kit-level `event_listener_enabled: true`. Toggling only the per-program flag will not fire callbacks.
- Proxy `X-Bypass-Local-Restrictions` strips `command`/`environment`/`directory`/`user`/`webhooks`/`stdout_logfile`/`stderr_logfile`.
- CLI `--port-range-{start,end}` -> nested `port_range`.

## Common errors

- 400 webhook: `must use HTTPS`, `contains userinfo`, `reserved IP range`.
- 400 `name already in use` / `Port range overlaps` / `port_param requires port_range`.
- success=false `Port parameter required` / `Program with ID {id} is disabled` -> `control.enable`.
- Direct private-IP connections get a plain `403 Forbidden` (the kit logs `connection blocked reason=private_ip` server-side; that string is NOT in the response) -> use the capability URL.
- 1 MB JSON body limit.

## Related namespaces

`exec` sync. `cron` scheduled. `proxyLogs` access log. `terminal` TTY. `display` virtual display.

## Examples

Every step in every example was live-tested against a real `daemon-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first. The kit returns numeric `program.id` (not a UUID) — capture it from the `programs.add` response.

### 1. Register a long-running supervised program with auto-boot

**Goal:** add a tick-emitting worker that supervisord keeps alive across kit restarts, then start it without blocking the caller.

**Step 1 — add (`enabled: true`, `boot: true`).** Capture `program.id`. Use `user: "user"` (uid 1000); the daemon's `command` allows shell metachars but you must own the quoting — single-quote the outer payload to avoid double-escaping.

```typescript
const r = await client.daemon.programs.add({
  name: 'examples-daemon-tick',
  command: `sh -c 'while :; do echo tick $(date -u +%s); sleep 5; done'`,
  user: 'user', enabled: true, boot: true, autorestart: 'unexpected',
});
const id = r.data!.program.id;
```
**Step 2 — `control.start` with `wait: false`.** ⚠ `wait: true` blocks the HTTP request until the process is running and easily exceeds 30 s on a cold kit (live-verified — `wait:true,timeout:30` returned client-side timeout). Pass `wait: false` and poll `status.get` instead.

```typescript
await client.daemon.control.start(id, { wait: false });
while ((await client.daemon.status.get(id)).data!.status.status !== 'running') {
  await new Promise(r => setTimeout(r, 1000));
}
```
### 2. Quick-start ephemeral with TTL — launch, check status, tail logs

**Goal:** fire a one-shot command (no config write), let it run up to 10 minutes, retrieve its output. ⚠ **Major quirk (TypeScript SDK only)** — `quickStart.launch` returns `temporary_id = "quick_<ms>_<seq>"` (e.g. `quick_1700000000000_1`), but the generated SDK's client-side validator rejects that exact id on `quickStart.getStatus` with `id must match pattern: ^quick_\d+$` (live-verified); the CLI and raw HTTP accept the full id and work fine. Truncating to `quick_<ms>` returns 404. **SDK workaround:** use `quickStart.list` to find the entry by `name`, or call `quickStart.getEphemeralLogs` (which DOES accept the full id) to verify the run.

**Step 1 — launch.**

```typescript
const r = await client.daemon.quickStart.launch({
  name: 'examples-daemon-qs',
  command: `sh -c "for i in $(seq 1 30); do echo qs-$i; sleep 1; done"`,
  user: 'user', ttl: 600,
});
const qid = r.data!.temporary_id;
```
**Step 2 — find via `quickStart.list` (in SDK mode this avoids the TypeScript SDK's client-side `^quick_\d+$` pattern validator on `getStatus`; CLI and raw HTTP don't need the detour).**

```typescript
const list = await client.daemon.quickStart.list();
const me = list.data!.ephemeral_programs.find(e => e.name === 'examples-daemon-qs');
```
**Step 3 — tail logs (the full id with `_<seq>` is accepted here).**

```typescript
const lg = await client.daemon.quickStart.getEphemeralLogs(qid, { type: 'stdout', lines: 10 } as any);
```
**Step 4 — stop.** ⚠ The kit's `quickStart.stop` accepts the full `quick_<ms>_<seq>` id (as do the CLI and raw HTTP), but the **generated TypeScript SDK enforces a stricter `^quick_\d+$` pattern client-side** and rejects real ids returned by `quickStart.launch`. In SDK mode drive stop via raw HTTP **POST** to `/api/v1/daemon/quick-start/{id}/stop`, or look up by `name` via `quickStart.list` and use the SDK only for ids matching the stricter pattern.

```typescript
// SDK validator rejects full quick_<ms>_<seq> ids; drop to raw fetch:
await fetch(`${kitUrl}/api/v1/daemon/quick-start/${qid}/stop`, { method: 'POST', headers: kitHeaders });
```
### 3. Lazy port-range fan-out — one program, N port-bound instances

**Goal:** declare an HTTP service that listens on any port in `18800–18802`, materialised on demand the first time someone hits the proxy URL.

**Step 1 — add with `port_range` + `port_param` + `lazy_load`.** ⚠ `port_param` cannot be empty (live-verified — kit returns `400 Invalid port_param format: ""`). Use a real CLI flag, e.g. `--port`. The kit appends ` <flag> <port>` to your command at start-time.

```typescript
const r = await client.daemon.programs.add({
  name: 'examples-daemon-fanout',
  command: 'python3 -m http.server',
  user: 'user', enabled: true,
  port_range: { start: 18800, end: 18802 },
  port_param: '--port', lazy_load: true,
});
const id = r.data!.program.id;
```
**Step 2 — start one specific port and read fleet-wide status.** `programs.list?port=18800&include_status=true` returns the program with a `status: { type: "port-range", running_instances, total_instances, instances: [{ port, status, … }] }` block (live-verified).

```typescript
await client.daemon.control.start(id, { port: 18800 } as any);
const r = await client.daemon.programs.list({ port: 18800, include_status: 'true' } as any);
```
The instance is reachable at `https://${P}-${C}-http-18800.${N}.containers.hoody.com` — no extra alias needed (see § "Reach a service you just started").

### 4. Tail program logs (`status.getLogs` with type / lines)

**Goal:** investigate why a worker keeps restarting. `getLogs` returns `{ logs, type, lines, log_file }` where `log_file` is the on-disk path under `/hoody/storage/hoody-daemon/logs/<name>/{stdout,stderr}.log` (live-verified).

```typescript
const lg = await client.daemon.status.getLogs(id, { type: 'stderr', lines: 200 } as any);
console.log(lg.data!.logs);
```
For a port-range program, pass `?port=18800` to read the per-instance log file.

### 5. Webhook on supervisord process events (e.g. crash → HTTPS callback)

**Goal:** when the program enters the `FATAL` state, POST to your HTTPS endpoint. ⚠ Webhook URLs must be **HTTPS** unless `NODE_ENV=development` (and reject userinfo, `localhost`, and private/CGNAT/link-local ranges). ⚠ **Event names are kit-specific, not the supervisord canonical `PROCESS_STATE_*` ones**: live-verified the kit accepts only `STARTING, RUNNING, BACKOFF, STOPPING, STOPPED, EXITED, FATAL, UNKNOWN, "all", "*"`. Sending `PROCESS_STATE_FATAL` returns `400 Invalid event type`.

```typescript
// ProgramInput requires name + command + user even on partial updates;
// re-pass them from a prior programs.get snapshot.
const cur = (await client.daemon.programs.get(id)).data as any;
await client.daemon.programs.edit(id, {
  name: cur.name, command: cur.command, user: cur.user,
  webhooks: {
    enabled: true,
    urls: ['https://hooks.example.com/daemon-events'],
    events: ['FATAL', 'BACKOFF'],
    headers: { 'X-Source': 'hoody-daemon' },
    timeout: 10, retry: 2,
  },
});
```
⚠ Webhook edits **deep-merge** — fields you omit from the `webhooks` block are NOT cleared. To turn off, send `{ webhooks: { enabled: false } }`. To rotate URLs, send the new full `urls` array (it replaces — array-set, not array-add). The **generated SDK type** (`ProgramInput`) marks `name`, `command`, and `user` as required on every `programs.edit` body (re-send them from a `programs.get` snapshot); the raw kit HTTP API does NOT require them on updates — it field-merges and accepts a partial `ProgramData`, so for surgical patches drop down to `fetch()`.

### 6. Wipe + reset to defaults — non-destructive snapshot first

**Goal:** restore the supervisord program set to whatever ships in `programs.default.json`. ⚠ **Destructive** — every program you added gets wiped. Snapshot before you call. (This step is intentionally NOT live-tested — it would break the shared test container; pattern shown for reference.)

```typescript
const snapshot = (await client.daemon.programs.list()).data;   // SDK list has no `limit` option
await client.daemon.programs.reset();
// then re-create what you want from snapshot.programs[]
```
### 7. Patch only the env vars on a running program

**Goal:** flip `LOG_LEVEL=debug` without restating `command`/`user`/etc. `programs.edit` is a partial merge — fields you don't pass are preserved (live-verified — the response shows merged `environment` plus all original fields intact).

```typescript
// programs.edit takes ProgramInput — name/command/user are required (PUT-style replacement).
// Snapshot, mutate, re-send the full object:
const cur = (await client.daemon.programs.get(id)).data!;
await client.daemon.programs.edit(id, {
  name: cur.name,
  command: cur.command,
  user: cur.user,
  environment: { LOG_LEVEL: 'debug', BUILD: 'examples' },
});
await client.daemon.control.stop(id, {} as any);
await client.daemon.control.start(id, { wait: false });
```
⚠ `environment` REPLACES the whole map (not per-key merge). If you have `{A:1,B:2}` and PATCH `{A:9}`, you end up with just `{A:9}` — re-send everything you want to keep.

### 8. Inspect a running program with stats

**Goal:** read CPU/RSS/uptime to feed a dashboard. `status.get?include_stats=true` returns the basic `{ id, status }` plus stats fields when the process is actually `running` (when in `backoff`/`stopped`, only `status` and a string `uptime` like `"too quickly (process log may have details)"` come back — live-verified).

```typescript
const r = await client.daemon.status.get(id, { include_stats: 'true' } as any);
```
⚠ `include_stats` is a **string** boolean (`"true"`/`"false"`) — `1`/`yes`/empty string return `400` (strict-bool query parse, see Quirks).

### 9. Stop one port instance OR every instance in a port-range fleet

**Goal:** kill just port 18800 vs. drain the whole fleet for a deploy.

**Single port** (other ports keep running):

```typescript
await client.daemon.control.stop(id, { port: 18800 } as any);
```
**Whole fleet** (`all: true`):

```typescript
await client.daemon.control.stop(id, { all: true } as any);
```
⚠ For a port-range program, `control.start` REQUIRES `{ port }` (single-port only); there is no "start them all" — boot each port individually or rely on `lazy_load: true` to materialise on first proxy hit.

### 10. Disable now, re-enable after the migration

**Goal:** keep the program defined but stop supervisord from auto-restarting it. `disable` flips `enabled: false` (process stays in the listing for forensics); `enable` brings it back without touching `command`/`environment`.

```typescript
await client.daemon.control.disable(id);
// … run migration …
await client.daemon.control.enable(id);
await client.daemon.control.start(id, { wait: false });
```
⚠ Trying `control.start` while `enabled: false` returns `success: false` with `Program with ID {id} is disabled` (e.g. `Program with ID 7 is disabled`) — call `control.enable` first.

## Reference

**Accessor:** `client.daemon`  |  **Import:** `import * as daemon from 'hoody-sdk/daemon'`

### `client.daemon.control` (4) — Program control endpoints - enable, disable, start, and stop programs

#### `disable` — Disable a program

```typescript
client.daemon.control.disable(id: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `integer` | path | Yes | Unique numeric identifier of the program |

**Returns:** `daemon_ProgramResponse`  |  **HTTP:** `POST /api/v1/daemon/programs/{id}/disable`
**CLI:** `hoody daemon programs disable`

---

#### `enable` — Enable a program

```typescript
client.daemon.control.enable(id: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `integer` | path | Yes | Unique numeric identifier of the program |

**Returns:** `daemon_ProgramResponse`  |  **HTTP:** `POST /api/v1/daemon/programs/{id}/enable`
**CLI:** `hoody daemon programs enable`

---

#### `start` — Start a program or port instance

```typescript
client.daemon.control.start(id: integer, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `integer` | path | Yes | Unique numeric identifier of the program |
| `data` | `object` | body | No |  |

**Body:** `{ port: int, wait: bool=false, timeout: int=30, if_not_running: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/daemon/programs/{id}/start`
**CLI:** `hoody daemon programs start`

---

#### `stop` — Stop a program or port instance

```typescript
client.daemon.control.stop(id: integer, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `integer` | path | Yes | Unique numeric identifier of the program |
| `data` | `object` | body | No |  |

**Body:** `{ port: int, all: bool }`

**Returns:** `daemon_Success`  |  **HTTP:** `POST /api/v1/daemon/programs/{id}/stop`
**CLI:** `hoody daemon programs stop`

---

### `client.daemon.health` (1) — Service health check endpoint - returns standardized 9-field health response for monitoring and readiness probes

#### `check` — Service health check

```typescript
client.daemon.health.check()
```

**Returns:** `daemon_HealthResponse`  |  **HTTP:** `GET /api/v1/daemon/health`
**CLI:** `hoody daemon health`

---

### `client.daemon.programs` (8) — Program management endpoints - create, read, update, and delete daemon programs

#### `add` — Add a new CUSTOM program

```typescript
client.daemon.programs.add(data: daemon_ProgramInput)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `daemon_ProgramInput` | body | Yes |  |

**Returns:** `daemon_AddProgramResponse`  |  **HTTP:** `POST /api/v1/daemon/programs/add`
**CLI:** `hoody daemon programs create`

---

#### `edit` — Edit a program

```typescript
client.daemon.programs.edit(id: integer, data: daemon_ProgramInput)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `integer` | path | Yes | Unique numeric identifier of the program |
| `data` | `daemon_ProgramInput` | body | Yes |  |

**Returns:** `daemon_ProgramResponse`  |  **HTTP:** `POST /api/v1/daemon/programs/edit/{id}`
**CLI:** `hoody daemon programs edit`

---

#### `get` — Get a specific program

```typescript
client.daemon.programs.get(id: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `integer` | path | Yes | Unique numeric identifier of the program |

**Returns:** `daemon_ProgramResponse`  |  **HTTP:** `GET /api/v1/daemon/programs/{id}`
**CLI:** `hoody daemon programs get`

---

#### `list` — List all programs

```typescript
client.daemon.programs.list(hoody_kit?: string, lazy_load?: string, enabled?: string, boot?: string, port?: integer, port_from?: integer, port_to?: integer, include_status?: string, include_stats?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `hoody_kit` | `string` | query | No | Filter by hoody_kit status. Use "true" for Hoody Kit programs only, "false" for user (non-kit) programs only. |
| `lazy_load` | `string` | query | No | Filter by lazy_load status. Use "true" for lazy-loaded programs only (started on-demand), "false" for programs that auto-start. |
| `enabled` | `string` | query | No | Filter by enabled status. Use "true" for enabled programs only, "false" for disabled programs only. |
| `boot` | `string` | query | No | Filter by boot status. Use "true" for programs that auto-start on system boot, "false" for manual-start programs. |
| `port` | `integer` | query | No | Filter programs by single port number. Returns only programs whose port_range includes this specific port. Example: ?port=8042 returns programs with ranges containing 8042. |
| `port_from` | `integer` | query | No | Filter by port range start (must be used with port_to). Returns programs whose port ranges overlap with the specified range. Uses overlap logic: program.start <= port_to AND program.end >= port_from. |
| `port_to` | `integer` | query | No | Filter by port range end (must be used with port_from). Returns programs whose port ranges overlap with the specified range. Multiple programs may be returned if their ranges overlap. |
| `include_status` | `string` | query | No | Include runtime status for each program. When true, adds a "status" field to each program showing current running state, instances, and process details. |
| `include_stats` | `string` | query | No | Include resource stats (CPU, memory, process tree) for each running program. Implies include_status=true. Adds a "stats" field with pid, started_at, cpu_percent, memory_rss_bytes, process_count, and per-process breakdown. Only present for running programs. |

**Returns:** `daemon_ProgramListResponse`  |  **HTTP:** `GET /api/v1/daemon/programs`
**CLI:** `hoody daemon programs list`

---

#### `listAll` — List all programs (collect all pages)

```typescript
client.daemon.programs.listAll(hoody_kit?: string, lazy_load?: string, enabled?: string, boot?: string, port?: integer, port_from?: integer, port_to?: integer, include_status?: string, include_stats?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `hoody_kit` | `string` | query | No | Filter by hoody_kit status. Use "true" for Hoody Kit programs only, "false" for user (non-kit) programs only. |
| `lazy_load` | `string` | query | No | Filter by lazy_load status. Use "true" for lazy-loaded programs only (started on-demand), "false" for programs that auto-start. |
| `enabled` | `string` | query | No | Filter by enabled status. Use "true" for enabled programs only, "false" for disabled programs only. |
| `boot` | `string` | query | No | Filter by boot status. Use "true" for programs that auto-start on system boot, "false" for manual-start programs. |
| `port` | `integer` | query | No | Filter programs by single port number. Returns only programs whose port_range includes this specific port. Example: ?port=8042 returns programs with ranges containing 8042. |
| `port_from` | `integer` | query | No | Filter by port range start (must be used with port_to). Returns programs whose port ranges overlap with the specified range. Uses overlap logic: program.start <= port_to AND program.end >= port_from. |
| `port_to` | `integer` | query | No | Filter by port range end (must be used with port_from). Returns programs whose port ranges overlap with the specified range. Multiple programs may be returned if their ranges overlap. |
| `include_status` | `string` | query | No | Include runtime status for each program. When true, adds a "status" field to each program showing current running state, instances, and process details. |
| `include_stats` | `string` | query | No | Include resource stats (CPU, memory, process tree) for each running program. Implies include_status=true. Adds a "stats" field with pid, started_at, cpu_percent, memory_rss_bytes, process_count, and per-process breakdown. Only present for running programs. |

**Returns:** `daemon_ProgramListResponse[]`  |  **HTTP:** `GET /api/v1/daemon/programs`
**CLI:** `hoody daemon programs list`

---

#### `listIterator` — List all programs (async iterator)

```typescript
client.daemon.programs.listIterator(hoody_kit?: string, lazy_load?: string, enabled?: string, boot?: string, port?: integer, port_from?: integer, port_to?: integer, include_status?: string, include_stats?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `hoody_kit` | `string` | query | No | Filter by hoody_kit status. Use "true" for Hoody Kit programs only, "false" for user (non-kit) programs only. |
| `lazy_load` | `string` | query | No | Filter by lazy_load status. Use "true" for lazy-loaded programs only (started on-demand), "false" for programs that auto-start. |
| `enabled` | `string` | query | No | Filter by enabled status. Use "true" for enabled programs only, "false" for disabled programs only. |
| `boot` | `string` | query | No | Filter by boot status. Use "true" for programs that auto-start on system boot, "false" for manual-start programs. |
| `port` | `integer` | query | No | Filter programs by single port number. Returns only programs whose port_range includes this specific port. Example: ?port=8042 returns programs with ranges containing 8042. |
| `port_from` | `integer` | query | No | Filter by port range start (must be used with port_to). Returns programs whose port ranges overlap with the specified range. Uses overlap logic: program.start <= port_to AND program.end >= port_from. |
| `port_to` | `integer` | query | No | Filter by port range end (must be used with port_from). Returns programs whose port ranges overlap with the specified range. Multiple programs may be returned if their ranges overlap. |
| `include_status` | `string` | query | No | Include runtime status for each program. When true, adds a "status" field to each program showing current running state, instances, and process details. |
| `include_stats` | `string` | query | No | Include resource stats (CPU, memory, process tree) for each running program. Implies include_status=true. Adds a "stats" field with pid, started_at, cpu_percent, memory_rss_bytes, process_count, and per-process breakdown. Only present for running programs. |

**Returns:** `AsyncIterableIterator<daemon_ProgramListResponse>`  |  **HTTP:** `GET /api/v1/daemon/programs`
**CLI:** `hoody daemon programs list`

---

#### `remove` — Remove a program

```typescript
client.daemon.programs.remove(id: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `integer` | path | Yes | Unique numeric identifier of the program |

**Returns:** `daemon_RemoveProgramResponse`  |  **HTTP:** `POST /api/v1/daemon/programs/remove/{id}`
**CLI:** `hoody daemon programs delete`

---

#### `reset` — Reset programs to default

```typescript
client.daemon.programs.reset()
```

**Returns:** `any`  |  **HTTP:** `POST /api/v1/daemon/programs/reset`
**CLI:** `hoody daemon programs reset`

---

### `client.daemon.quickStart` (7) — Ephemeral program launcher - Create temporary programs that auto-cleanup when stopped or on reboot

#### `getEphemeralLogs` — Get ephemeral program logs

```typescript
client.daemon.quickStart.getEphemeralLogs(id: string, type?: string, lines?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Ephemeral program temporary ID |
| `type` | `string` | query | No | Log stream: stdout or stderr |
| `lines` | `integer` | query | No | Number of lines to return from end of file |

**Returns:** `daemon_LogResponse`  |  **HTTP:** `GET /api/v1/daemon/quick-start/{id}/logs`
**CLI:** `hoody daemon ephemeral logs`

---

#### `getStatus` — Get ephemeral program status

```typescript
client.daemon.quickStart.getStatus(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Temporary ID of the ephemeral program (format: quick_<timestamp>) |

**Returns:** `daemon_QuickStartResponse`  |  **HTTP:** `GET /api/v1/daemon/quick-start/{id}/status`
**CLI:** `hoody daemon ephemeral status`

---

#### `launch` — Launch ephemeral CUSTOM program

```typescript
client.daemon.quickStart.launch(data: daemon_EphemeralProgramInput)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `daemon_EphemeralProgramInput` | body | Yes |  |

**Returns:** `daemon_QuickStartResponse`  |  **HTTP:** `POST /api/v1/daemon/quick-start`
**CLI:** `hoody daemon ephemeral start`

---

#### `list` — List all ephemeral programs

```typescript
client.daemon.quickStart.list()
```

**Returns:** `daemon_QuickStartListResponse`  |  **HTTP:** `GET /api/v1/daemon/quick-start`
**CLI:** `hoody daemon ephemeral list`

---

#### `listAll` — List all ephemeral programs (collect all pages)

```typescript
client.daemon.quickStart.listAll()
```

**Returns:** `daemon_QuickStartListResponse[]`  |  **HTTP:** `GET /api/v1/daemon/quick-start`
**CLI:** `hoody daemon ephemeral list`

---

#### `listIterator` — List all ephemeral programs (async iterator)

```typescript
client.daemon.quickStart.listIterator()
```

**Returns:** `AsyncIterableIterator<daemon_QuickStartListResponse>`  |  **HTTP:** `GET /api/v1/daemon/quick-start`
**CLI:** `hoody daemon ephemeral list`

---

#### `stop` — Stop ephemeral program

```typescript
client.daemon.quickStart.stop(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Temporary ID of the ephemeral program to stop |

**Returns:** `daemon_QuickStartStopResponse`  |  **HTTP:** `POST /api/v1/daemon/quick-start/{id}/stop`
**CLI:** `hoody daemon ephemeral stop`

---

### `client.daemon.status` (3) — Status monitoring endpoints - monitor runtime status of programs and instances

#### `get` — Get specific program status

```typescript
client.daemon.status.get(id: integer, port?: integer, include_stats?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `integer` | path | Yes | Unique numeric identifier of the program |
| `port` | `integer` | query | No | Filter to specific port instance (for port-range programs only) |
| `include_stats` | `string` | query | No | Include resource stats (CPU, memory, process tree) for running programs. Adds a "stats" field with pid, started_at, cpu_percent, memory_rss_bytes, process_count, and per-process breakdown. |

**Returns:** `daemon_StatusResponse`  |  **HTTP:** `GET /api/v1/daemon/status/{id}`
**CLI:** `hoody daemon programs status`

---

#### `getAll` — Get all program statuses

```typescript
client.daemon.status.getAll()
```

**Returns:** `daemon_AllStatusResponse`  |  **HTTP:** `GET /api/v1/daemon/status`
**CLI:** `hoody daemon programs statuses`

---

#### `getLogs` — Get program logs

```typescript
client.daemon.status.getLogs(id: integer, type?: string, lines?: integer, port?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `integer` | path | Yes | Program ID |
| `type` | `string` | query | No | Log stream: stdout or stderr |
| `lines` | `integer` | query | No | Number of lines to return from end of file |
| `port` | `integer` | query | No | Port number (required for port-range programs) |

**Returns:** `daemon_LogResponse`  |  **HTTP:** `GET /api/v1/daemon/programs/{id}/logs`
**CLI:** `hoody daemon programs logs`


### Body schemas

- `daemon_ProgramInput` — `{ id: int, name*: string, description: string, command*: string, user*: string, enabled: bool=true, boot: bool=false, delay_seconds: int=0, autorestart: "true" | "false" | "unexpected"="unexpected", directory: string, priority: int=999, stdout_logfile: string, stderr_logfile: string, logs_enabled: bool=true, log_max_bytes: int=5242880, log_backups: int=2, environment: { [key: string]: string }, hoody_kit: bool=false, port_range: { start*: int, end*: int }, port_param: string="--port", lazy_load: bool=false, display: string|null, terminal_id: int, terminal_shell: "bash" | "zsh" | "fish" | "sh" | "tmux"|null, terminal_interactive: bool|null, webhooks: { enabled: bool, urls: string[], events: string | string[], headers: object, timeout: int, retry: int }|null }`
- `daemon_EphemeralProgramInput` — `{ command*: string, user*: string, name: string, autorestart: "true" | "false" | "unexpected"="unexpected", directory: string, environment: { [key: string]: string }, priority: int=999, delay_seconds: int=0, stdout_logfile: string, stderr_logfile: string, logs_enabled: bool=true, log_max_bytes: int=5242880, log_backups: int=2, ttl: int, wait: bool=false, timeout: int=30, display: string|null, terminal_id: int, terminal_shell: "bash" | "zsh" | "fish" | "sh" | "tmux"|null, terminal_interactive: bool|null }`


---

<!-- ===== namespace: display ===== -->

# `display` — programmatic GUI desktops with screenshots, input, and windows

## Purpose

Per-container HTML5 desktop (X11 via proxy): screenshots, input, windows, clipboard.

## When to use

- Click/type/drag/scroll at coords; screenshots/thumbnails for vision; X11 window ops; clipboard r/w.
- **Multiple GUI apps → one display (one `terminal_id`) per app (almost always the right call).** A single X display *can* host many windows, but giving each app its own display (`display: ":N"` paired to a distinct `terminal_id`) gives each its own `display-<N>` kit URL — a dedicated full-surface stream you can screenshot, embed / iframe, and route input to **independently, per window** — with no window-search / focus juggling on a shared display. Pin matching ids (`terminal_id=1`↔`:1`, `terminal_id=2`↔`:2`, …) so the routing stays one-to-one. Reuse a single display only when you deliberately want the apps composited together (e.g. a full desktop — see the `desktop-<N>` alias).

## When NOT to use

Not for: shell → `terminal`/`exec`, files → `files`, headless web → `browser`, toasts → `notifications`.

## Prerequisites

- Active Xpra/X11 (`DISPLAY=:N`). To render X apps from a terminal into display `:N`, create the terminal session with an explicit string `display: "N"` (or CLI `--display N`); `terminal_id=N` alone does NOT set `DISPLAY`.
- Display ID resolution: `*-display-N.*` host (e.g. `https://{projectId}-{containerId}-display-1.{node}.containers.hoody.com` for display `1`) or query override `?displayId=N`.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. See-then-act loop

1. `screenshots.capture` (`base64=true` for vision).
2. `input.clickAt` / `typeAt`.
3. `screenshots.captureMetadata` — cheap timestamp check.
4. Re-capture only when timestamp advanced.

### 2. Find and focus a window

1. `listWindows` (`onlyVisible=true`).
2. `input.windowSearch` — name/class/classname.
3. `input.windowFocus` / `windowRaise`.
4. `input.windowGeometry` — coords.
5. `input.windowActive` — confirm.

### 3. Drag / select

1. `input.mouseMove` — optional pre-position.
2. `input.drag` `(sx,sy)`→`(ex,ey)`, optional `steps`.
3. Or `input.select` — click + shift-click.
4. `input.reset` — release stuck buttons.

### 4. Clipboard hand-off

1. `setClipboard` — `text`, optional `selection`.
2. `input.keyboardKey` — `["ctrl+v"]` (`["shift+Insert"]` for primary).
3. `getClipboard` — read back after GUI copy.

### 5. Batch input replay

1. `input.batch` — POST ordered actions.
2. `input.wait` — interleave waits.
3. `screenshots.capture` — confirm.

## Quirks & gotchas

- `?displayId=N` overrides `*-display-N.*` host.
- `displayId` `1..999999`, digits only (regex `^\d+$` at displayContext.ts:22, range check at :24); invalid silently falls through to host-derived id by :31.
- All endpoints except `health` and the HTML client root (`GET /api/v1/display/`) need a displayId or return `400 NO_DISPLAY_CONTEXT`.
- Screenshot GETs return binary PNG; `base64=true` for JSON.
- `getByTimestamp` needs numeric `timestamp`, not `timestamp_human`.
- Clipboard `selection`: `clipboard` (default), `primary`, `secondary`. PRIMARY ≠ Ctrl+V.
- Window IDs accept decimal or hex (`0x...`); returns decimal.
- `accessClient` returns HTML, browser-only.
- SDK-only quirk: the screenshot-list accessor hangs off the namespace root (`client.display.listScreenshots`), not the `screenshots` service — there is no `screenshots.list`.
- `getInformation` returns display info, a window list (each with per-window `position`/`size`), and the screenshot list — but NOT the Xvfb canvas dimensions (those live on `input.geometry`, see `inputRoutes.ts:499`).
- `input.reset` clears stuck modifiers/buttons.

## Common errors

- `400 NO_DISPLAY_CONTEXT` — supply `?displayId=N` or `*-display-N.*`.
- `DISPLAY_NOT_AVAILABLE` — X server for displayId unreachable; thrown by `inputService.parseError` (re-emitted by the input route handler) and also by the clipboard/window route handlers.
- `404` on `getByTimestamp` — no match. Refresh by calling `screenshots.captureMetadata` (`/api/v1/display/screenshot/info`) — that endpoint **takes a fresh screenshot** and returns its metadata, not just a timestamp lookup; then retry `getByTimestamp` with the new ts. (`/screenshot/last/info`/`getLatestMetadata` only returns metadata for the *latest* screenshot — not a usable replacement for a missed timestamp.)

## Related namespaces

`terminal`, `notifications`, `browser`, `files`, `exec`.

## Examples

Every step in every example was live-tested against a real `display-1` kit driving an Xpra/X11 session inside a Hoody container. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first, and pick a `DID` (the active display id, e.g. `1`). The `display-1` in the kit URL is the kit instance, NOT the display id — `?displayId=N` (or `--display-id N`) selects the X server.

### 1. See-then-act loop — capture, click, re-capture, diff

**Goal:** snapshot the screen, click a coordinate, snapshot again, and use cheap metadata (`timestamp`) to detect that the second capture is fresh — typical inner loop for vision-driven agents.

**Step 1 — capture a baseline with `base64` so the bytes round-trip in JSON.**

```typescript
const before = await client.display.screenshots.captureMetadata({ displayId: 1 });
const tsBefore = before.data!.timestamp;
const shot = await client.display.screenshots.capture({ displayId: 1, base64: true });
const b64 = (shot.data as any).image.data;
```
**Step 2 — click at `(75, 50)`.** `clickAt` moves AND clicks in one call; default `button=1` (left).

```typescript
await client.display.input.clickAt({ x: 75, y: 50, button: 1 }, { displayId: 1 });
```
**Step 3 — cheap freshness check, then full re-capture only if the timestamp advanced.** `screenshot/info` returns metadata without the PNG bytes — much cheaper than a full capture for polling.

```typescript
const after = await client.display.screenshots.captureMetadata({ displayId: 1 });
if (after.data!.timestamp !== tsBefore) {
  const fresh = await client.display.screenshots.capture({ displayId: 1, base64: true });
  // … feed fresh.data!.image.data to your vision model
}
```
### 2. Find a window by name + focus it

**Goal:** locate the `xeyes` window without knowing its decimal `windowId`, then focus and confirm.

**Step 1 — `windowSearch` with a regex `pattern`.** The booleans control which X11 fields to match against (`name` = WM_NAME / `_NET_WM_NAME`, `class` / `classname` = WM_CLASS pair). Returns just an array of `windowId` integers.

```typescript
const search = await client.display.input.windowSearch(
  { pattern: 'xeyes', name: true, class: true, classname: true },
  { displayId: 1 },
);
const wid = String(search.data!.windows![0]);   // windows is number[]; the methods take windowId: string
```
**Step 2 — focus + confirm.** `windowActive` returns the currently focused id; compare with what you focused.

```typescript
await client.display.input.windowFocus({ windowId: wid }, { displayId: 1 });
const active = await client.display.input.windowActive({ displayId: 1 });
console.log('focused:', active.data!.windowId === wid);
```
### 3. Click sequence, then type into the focused window

**Goal:** focus an editable field (e.g. a text input at `(120, 80)`), type a string, with a small per-keystroke delay so the target app doesn't drop characters.

```typescript
await client.display.input.clickAt({ x: 120, y: 80 }, { displayId: 1 });
await client.display.input.keyboardType({ text: 'hello world', delay: 20 }, { displayId: 1 });
```
`typeAt` collapses click-then-type into one call when you only need plain ASCII at one point: `{ x, y, text, delay }`.

### 4. Drag from one position to another

**Goal:** smooth-drag from `(50, 50)` to `(200, 150)` over `steps=20` interpolated mouse positions (raise `steps` if the target app's drag-recogniser misses fast moves; cap is 1000).

```typescript
await client.display.input.drag(
  { startX: 50, startY: 50, endX: 200, endY: 150, steps: 20, button: 1 },
  { displayId: 1 },
);
```
If a drag aborts mid-way and the button stays "pressed" (next click misbehaves), see example 10 — `input.reset` releases stuck buttons + modifiers.

### 5. Clipboard hand-off — write text, paste with Ctrl+V

**Goal:** stage text in the X11 CLIPBOARD selection, then send Ctrl+V into the focused window so it's pasted natively. ⚠ See quirk: clipboard ops can fail with `CLIPBOARD_FAILED` / "no HOME directory" if the X session was launched without a writable `$HOME` for the kit user; verify by reading back the clipboard after the write.

**Step 1 — `setClipboard` to the standard CLIPBOARD buffer.** PRIMARY (middle-click paste) is a different selection — Ctrl+V reads CLIPBOARD only.

```typescript
await client.display.setClipboard({ text: 'pasted via hoody', selection: 'clipboard' }, { displayId: 1 });
const r = await client.display.getClipboard({ displayId: 1, selection: 'clipboard' });
console.log(r.data!.text);
```
**Step 2 — Ctrl+V into the focused window.** `keys` is an array — pass `["shift+Insert"]` instead if the target app paste-binds to PRIMARY.

```typescript
await client.display.input.keyboardKey({ keys: ['ctrl+v'] }, { displayId: 1 });
```
### 6. Read window properties (geometry + WM_CLASS + WM_NAME)

**Goal:** for an unknown window id `WID`, read its title, class hints, and pixel rectangle to decide where to click.

```typescript
const props = await client.display.getWindowProperties(wid, { displayId: 1 });
const geom = await client.display.input.windowGeometry(wid, { displayId: 1 });
console.log(props.data!.properties.wmClass, geom.data);
```
`windowId` accepts decimal or hex (`0x...`); the response always normalises to decimal.

### 7. List visible windows with `onlyVisible` filter

**Goal:** enumerate everything mapped on screen (not iconified / withdrawn), pick the one with a name matching `xeyes`, no regex.

```typescript
const r = await client.display.listWindows({ displayId: 1, onlyVisible: true });
const xeyes = r.data!.windows.find(w => w.name === 'xeyes');
console.log(xeyes);
```
Each item carries `windowId`, `name`, `class` (the WM_CLASS pair as 2 strings), `desktop`, a per-window geometry object (the JSON key is "geometry", shaped `{x,y,width,height}`), `focused`, `states`. Use `focusedWindowId` on the parent object to find the active window without a second call.

### 8. Batch input replay — one POST, many actions

**Goal:** replay a recorded interaction (move → wait → click → wait → type) atomically. `actions[]` cap is 50, each item is `{ action: "<service>/<verb>", params: {...} }`. The response lists every step with success/failure indexed back to the request order.

```typescript
await client.display.input.batch(
  {
    actions: [
      { action: 'mouse/move',    params: { x: 120, y: 80 } },
      { action: 'input/wait',    params: { ms: 150 } },
      { action: 'mouse/click',   params: { button: 1 } },
      { action: 'keyboard/type', params: { text: 'replayed', delay: 15 } },
    ],
  },
  { displayId: 1 },
);
```
`input.wait` standalone (`{ ms, screenshot }`) is the right way to insert pauses between separate calls if you don't want to use `batch`. `ms` floor 50, ceiling 30 000.

### 9. Get display information — geometry, screenshots, X server status

**Goal:** one call that returns the running PID/session-name, connected clients, the window list, and the recent screenshot list — then pair it with `input.geometry` for the X server's pixel size. Useful as a one-shot diagnostic before driving input.

```typescript
const info = await client.display.getInformation({ displayId: 1 });
const geom = await client.display.input.geometry({ displayId: 1 });
console.log(geom.data!.width, geom.data!.height);
```
Note: the geometry returned is the underlying Xvfb canvas (often `8192x4096` for Hoody Xpra sessions), not a physical monitor size. Click coordinates are in this canvas space.

### 10. Reset stuck modifiers / buttons after a misfired drag

**Goal:** after an aborted drag or a `keyboardKeyDown` you forgot to release, the X server still thinks Shift / Ctrl / Button-1 is held. Symptom: every subsequent click acts as Shift-click; typed letters arrive uppercase. `input.reset` releases everything in one call.

```typescript
await client.display.input.reset({ displayId: 1 });
```
Safe to call any time, even when nothing is stuck. Pair it with the start of every new automation run as a defensive default.

## Reference

**Accessor:** `client.display`  |  **Import:** `import * as display from 'hoody-sdk/display'`

### `client.display` (7) — Display information and management

#### `accessClient` — Access the HTML5 Display client interface

```typescript
client.display.accessClient(displayId?: integer, decorations?: boolean, toolbar?: boolean, menu?: boolean, maximize_new_windows?: boolean, readonly?: boolean, dark_mode?: boolean, node?: string, project_id?: string, container_id?: string, url_display_id?: string, ssl?: boolean, webtransport?: boolean, path?: string, action?: string, display?: string, encoding?: string, offscreen?: boolean, bandwidth_limit?: integer, override_width?: string, override_height?: string, vrefresh?: integer, suspend_inactive_tab?: boolean, sound?: boolean, audio_codec?: string, keyboard?: boolean, keyboard_layout?: string, swap_keys?: boolean, clipboard?: boolean, clipboard_preferred_format?: string, clipboard_poll?: boolean, printing?: boolean, file_transfer?: boolean, video?: boolean, mediasource_video?: boolean, open_url?: boolean, notification_server_url?: string, web_notifications?: boolean, display_notifications?: boolean, notification_connection_type?: string, sharing?: boolean, steal?: boolean, reconnect?: boolean, floating_menu?: boolean, clock?: boolean, scroll_reverse_y?: string, scroll_reverse_x?: boolean, title_show_hoody?: boolean, title_show_display_id?: boolean, app?: string, remote_logging?: boolean, insecure?: boolean, debug_main?: boolean, debug_keyboard?: boolean, debug_geometry?: boolean, debug_mouse?: boolean, debug_clipboard?: boolean, debug_draw?: boolean, debug_audio?: boolean, debug_network?: boolean, debug_file?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `decorations` | `boolean` | query | No | Show window decorations (title bar with close/minimize/maximize buttons). Set to false for headless/kiosk mode. |
| `toolbar` | `boolean` | query | No | Show entire toolbar/menu area (menu trigger + menu). Set to false to hide all menu UI elements. Takes precedence over the menu parameter. |
| `menu` | `boolean` | query | No | Show Hoody menu trigger icon. Set to false to hide menu completely. Note: toolbar parameter takes precedence over this. |
| `maximize_new_windows` | `boolean` | query | No | Open new top-level application windows maximized instead of centered at the default size (max 1024x1024). Only applies to windows that do not request their own position, and skips override-redirect windows, dialogs, other non-NORMAL window types, and windows the app itself marks undecorated via metadata (which would have no title bar to un-maximize from). Windows can still be un-maximized from their title bar. Combining with the global decorations=false parameter is honoured as explicit kiosk intent: windows open maximized without a title bar. |
| `readonly` | `boolean` | query | No | Enable read-only/view-only mode. Blocks all keyboard and mouse input from the client. Perfect for dashboards, monitoring, or demo scenarios. Works independently or combines with server readonly setting. |
| `dark_mode` | `boolean` | query | No | Enable dark mode theme |
| `node` | `string` | query | No | Hoody node identifier (e.g., example-1, example-2) |
| `project_id` | `string` | query | No | Hoody project ID |
| `container_id` | `string` | query | No | Hoody container ID |
| `url_display_id` | `string` | query | No | Display ID for URL construction |
| `ssl` | `boolean` | query | No | Use SSL/TLS for WebSocket connection |
| `webtransport` | `boolean` | query | No | Use WebTransport (HTTP3) instead of WebSocket |
| `path` | `string` | query | No | Connection path for the display server |
| `action` | `string` | query | No | Connection action type. - `connect` - Connect to existing session - `start` - Start new session - `shadow` - Shadow existing display |
| `display` | `string` | query | No | Display number to connect to |
| `encoding` | `string` | query | No | Video encoding type. Use auto for best automatic selection. |
| `offscreen` | `boolean` | query | No | Use offscreen canvas for rendering |
| `bandwidth_limit` | `integer` | query | No | Bandwidth limit in bits per second (0 = unlimited) |
| `override_width` | `string` | query | No | Override virtual desktop width (auto or numeric value) |
| `override_height` | `string` | query | No | Override virtual desktop height (auto or numeric value 480-4320) |
| `vrefresh` | `integer` | query | No | Vertical refresh rate in Hz. Use -1 for auto-detect. Minimum 30 when explicitly set. |
| `suspend_inactive_tab` | `boolean` | query | No | Suspend client updates when browser tab is inactive. Enables power saving by calling client.suspend() on tab hide and client.resume() on tab show. Recommended to keep enabled for better performance. |
| `sound` | `boolean` | query | No | Enable audio forwarding |
| `audio_codec` | `string` | query | No | Preferred audio codec |
| `keyboard` | `boolean` | query | No | Show on-screen virtual keyboard |
| `keyboard_layout` | `string` | query | No | Keyboard layout (us, gb, fr, de, etc.) |
| `swap_keys` | `boolean` | query | No | Swap Cmd/Ctrl keys (useful for macOS) |
| `clipboard` | `boolean` | query | No | Enable clipboard sharing |
| `clipboard_preferred_format` | `string` | query | No | Preferred clipboard format |
| `clipboard_poll` | `boolean` | query | No | Enable clipboard polling (browser-dependent default) |
| `printing` | `boolean` | query | No | Enable printing support |
| `file_transfer` | `boolean` | query | No | Enable file transfer support |
| `video` | `boolean` | query | No | Enable video encoding support |
| `mediasource_video` | `boolean` | query | No | Enable MediaSource API for video |
| `open_url` | `boolean` | query | No | Allow opening URLs from the remote session in the local browser |
| `notification_server_url` | `string` | query | No | External notification server URL for real-time notification integration.  **URL Format:** `https://{project}-{container}-n-{display}.{node}.containers.hoody.com/notification-client.js`  **Auto-detection:** If not provided, the client will attempt to auto-detect from the current hostname pattern. The client transforms the display URL pattern by replacing 'display' with 'n'.  **Examples:** - Manual: `?notification_server_url=https://my-project-container-n-6.node.containers.hoody.com/notification-client.js` - Auto-detected from: `https://my-project-container-display-6.node.containers.hoody.com`  **Integration:** The notification server (port 3999) provides: - Historical notification retrieval - Real-time WebSocket notification updates - Notification icons serving - Desktop notification triggering  See external notification server OpenAPI spec for complete API documentation. |
| `web_notifications` | `boolean` | query | No | Enable browser web notifications (native OS notifications) |
| `display_notifications` | `boolean` | query | No | Show notifications within display UI |
| `notification_connection_type` | `string` | query | No | Notification server connection type. - websocket: Real-time updates via WebSocket (recommended) - polling: Periodic HTTP polling (fallback) |
| `sharing` | `boolean` | query | No | Allow session sharing |
| `steal` | `boolean` | query | No | Steal existing sessions |
| `reconnect` | `boolean` | query | No | Auto-reconnect on connection loss |
| `floating_menu` | `boolean` | query | No | Show floating menu |
| `clock` | `boolean` | query | No | Show server clock |
| `scroll_reverse_y` | `string` | query | No | Reverse vertical scrolling direction (auto, true, false) |
| `scroll_reverse_x` | `boolean` | query | No | Reverse horizontal scrolling direction |
| `title_show_hoody` | `boolean` | query | No | Show "Hoody" in browser title |
| `title_show_display_id` | `boolean` | query | No | Show display ID in browser title |
| `app` | `string` | query | No | Target application to launch or focus. Can be an application name, a REGEX pattern, or a window ID. |
| `remote_logging` | `boolean` | query | No | Enable remote logging to the display server |
| `insecure` | `boolean` | query | No | Allow insecure authentication (not recommended for production) |
| `debug_main` | `boolean` | query | No | Enable main debug logging |
| `debug_keyboard` | `boolean` | query | No | Enable keyboard debug logging |
| `debug_geometry` | `boolean` | query | No | Enable geometry debug logging |
| `debug_mouse` | `boolean` | query | No | Enable mouse debug logging |
| `debug_clipboard` | `boolean` | query | No | Enable clipboard debug logging |
| `debug_draw` | `boolean` | query | No | Enable draw debug logging |
| `debug_audio` | `boolean` | query | No | Enable audio debug logging |
| `debug_network` | `boolean` | query | No | Enable network debug logging |
| `debug_file` | `boolean` | query | No | Enable file transfer debug logging |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/display/`
**CLI:** `hoody display access`

---

#### `getClipboard` — Read clipboard text

```typescript
client.display.getClipboard(displayId?: integer, selection?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `selection` | `string` | query | No | Clipboard buffer selection |

**Returns:** `display_ClipboardReadResult`  |  **HTTP:** `GET /api/v1/display/clipboard`
**CLI:** `hoody display clipboard get`

---

#### `getInformation` — Get display information and screenshots

```typescript
client.display.getInformation(displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_DisplayInfo`  |  **HTTP:** `GET /api/v1/display/info`
**CLI:** `hoody display info`

---

#### `getWindowProperties` — Get extended properties for a window

```typescript
client.display.getWindowProperties(displayId?: integer, windowId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `windowId` | `string` | path | Yes | Window ID (decimal or hex 0x...) |

**Returns:** `display_WindowPropertiesResult`  |  **HTTP:** `GET /api/v1/display/window/{windowId}/properties`
**CLI:** `hoody display windows properties`

---

#### `listScreenshots` — List all available screenshots

```typescript
client.display.listScreenshots(displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_ScreenshotsList`  |  **HTTP:** `GET /api/v1/display/screenshots`
**CLI:** `hoody display screenshots list`

---

#### `listWindows` — List windows on the current display

```typescript
client.display.listWindows(displayId?: integer, onlyVisible?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `onlyVisible` | `boolean` | query | No | If true, only include visible windows |

**Returns:** `display_WindowListResult`  |  **HTTP:** `GET /api/v1/display/windows`
**CLI:** `hoody display windows list`

---

#### `setClipboard` — Write clipboard text

```typescript
client.display.setClipboard(displayId?: integer, data: display_ClipboardWriteBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_ClipboardWriteBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/clipboard`
**CLI:** `hoody display clipboard set`

---

### `client.display.health` (1) — Server health and status endpoints

#### `check` — Service health check

```typescript
client.display.health.check()
```

**Returns:** `display_HealthResponse`  |  **HTTP:** `GET /api/v1/display/health`
**CLI:** `hoody display health`

---

### `client.display.input` (31) — Mouse, keyboard, and window control operations via xdotool

#### `act` — Execute one action with optional screenshot

```typescript
client.display.input.act(displayId?: integer, data: display_ActBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_ActBody` | body | Yes |  |

**Returns:** `display_ActionWithScreenshotResult`  |  **HTTP:** `POST /api/v1/display/input/act`
**CLI:** `hoody display input act`

---

#### `batch` — Execute a sequence of actions

```typescript
client.display.input.batch(displayId?: integer, data: display_BatchBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_BatchBody` | body | Yes |  |

**Returns:** `display_BatchResult`  |  **HTTP:** `POST /api/v1/display/input/batch`
**CLI:** `hoody display input batch`

---

#### `clickAt` — Move cursor and click

```typescript
client.display.input.clickAt(displayId?: integer, data: display_ClickAtBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_ClickAtBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/input/click-at`
**CLI:** `hoody display input click-at`

---

#### `drag` — Drag from one position to another

```typescript
client.display.input.drag(displayId?: integer, data: display_DragBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_DragBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/input/drag`
**CLI:** `hoody display input drag`

---

#### `geometry` — Get display dimensions

```typescript
client.display.input.geometry(displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_DisplayGeometryResult`  |  **HTTP:** `GET /api/v1/display/input/display-geometry`
**CLI:** `hoody display input geometry`

---

#### `keyboardKey` — Press key combinations

```typescript
client.display.input.keyboardKey(displayId?: integer, data: display_KeyboardKeyBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_KeyboardKeyBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/keyboard/key`
**CLI:** `hoody display keyboard key`

---

#### `keyboardKeyDown` — Hold a key down

```typescript
client.display.input.keyboardKeyDown(displayId?: integer, data: display_KeyboardKeyDownBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_KeyboardKeyDownBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/keyboard/key-down`
**CLI:** `hoody display keyboard key-down`

---

#### `keyboardKeyUp` — Release a held key

```typescript
client.display.input.keyboardKeyUp(displayId?: integer, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `object` | body | Yes |  |

**Body:** `{ key*: string, window: int | string }`

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/keyboard/key-up`
**CLI:** `hoody display keyboard key-up`

---

#### `keyboardType` — Type a string of text

```typescript
client.display.input.keyboardType(displayId?: integer, data: display_KeyboardTypeBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_KeyboardTypeBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/keyboard/type`
**CLI:** `hoody display keyboard type`

---

#### `mouseClick` — Click a mouse button

```typescript
client.display.input.mouseClick(displayId?: integer, data?: display_MouseClickBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_MouseClickBody` | body | No |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/mouse/click`
**CLI:** `hoody display mouse click`

---

#### `mouseDoubleClick` — Double-click a mouse button

```typescript
client.display.input.mouseDoubleClick(displayId?: integer, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `object` | body | No |  |

**Body:** `{ button: int=1, window: int | string }`

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/mouse/double-click`
**CLI:** `hoody display mouse double-click`

---

#### `mouseDown` — Press and hold a mouse button

```typescript
client.display.input.mouseDown(displayId?: integer, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `object` | body | No |  |

**Body:** `{ button: int=1, window: int | string, holdMs: int }`

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/mouse/down`
**CLI:** `hoody display mouse down`

---

#### `mouseLocation` — Get cursor position

```typescript
client.display.input.mouseLocation(displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_MouseLocationResult`  |  **HTTP:** `GET /api/v1/display/mouse/location`
**CLI:** `hoody display mouse location`

---

#### `mouseMove` — Move cursor to absolute position

```typescript
client.display.input.mouseMove(displayId?: integer, data: display_MouseMoveBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_MouseMoveBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/mouse/move`
**CLI:** `hoody display mouse move`

---

#### `mouseMoveRelative` — Move cursor by offset

```typescript
client.display.input.mouseMoveRelative(displayId?: integer, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `object` | body | Yes |  |

**Body:** `{ x*: int, y*: int, sync: bool }`

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/mouse/move-relative`
**CLI:** `hoody display mouse move-relative`

---

#### `mouseScroll` — Scroll in a direction

```typescript
client.display.input.mouseScroll(displayId?: integer, data: display_MouseScrollBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_MouseScrollBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/mouse/scroll`
**CLI:** `hoody display mouse scroll`

---

#### `mouseUp` — Release a mouse button

```typescript
client.display.input.mouseUp(displayId?: integer, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `object` | body | No |  |

**Body:** `{ button: int=1, window: int | string }`

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/mouse/up`
**CLI:** `hoody display mouse up`

---

#### `reset` — Emergency release all inputs

```typescript
client.display.input.reset(displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/input/reset`
**CLI:** `hoody display input reset`

---

#### `select` — Select a range via click + shift-click

```typescript
client.display.input.select(displayId?: integer, data: display_SelectBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_SelectBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/input/select`
**CLI:** `hoody display input select`

---

#### `typeAt` — Move, click, and type in one operation

```typescript
client.display.input.typeAt(displayId?: integer, data: display_TypeAtBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_TypeAtBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/input/type-at`
**CLI:** `hoody display input type-at`

---

#### `wait` — Wait for a duration with optional screenshot

```typescript
client.display.input.wait(displayId?: integer, data: display_WaitBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_WaitBody` | body | Yes |  |

**Returns:** `display_WaitResult`  |  **HTTP:** `POST /api/v1/display/input/wait`
**CLI:** `hoody display input wait`

---

#### `windowActive` — Get the active window ID

```typescript
client.display.input.windowActive(displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_ActiveWindowResult`  |  **HTTP:** `GET /api/v1/display/window/active`
**CLI:** `hoody display windows active`

---

#### `windowClose` — Close a window

```typescript
client.display.input.windowClose(displayId?: integer, data: display_WindowIdBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_WindowIdBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/window/close`
**CLI:** `hoody display windows close`

---

#### `windowFocus` — Focus/activate a window

```typescript
client.display.input.windowFocus(displayId?: integer, data: display_WindowIdBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_WindowIdBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/window/focus`
**CLI:** `hoody display windows focus`

---

#### `windowGeometry` — Get window position and size

```typescript
client.display.input.windowGeometry(displayId?: integer, windowId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `windowId` | `string` | path | Yes | Window ID (decimal or hex) |

**Returns:** `display_WindowGeometryResult`  |  **HTTP:** `GET /api/v1/display/window/{windowId}/geometry`
**CLI:** `hoody display windows geometry`

---

#### `windowMinimize` — Minimize a window

```typescript
client.display.input.windowMinimize(displayId?: integer, data: display_WindowIdBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_WindowIdBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/window/minimize`
**CLI:** `hoody display windows minimize`

---

#### `windowMove` — Move a window

```typescript
client.display.input.windowMove(displayId?: integer, data: display_WindowMoveBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_WindowMoveBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/window/move`
**CLI:** `hoody display windows move`

---

#### `windowName` — Get window title

```typescript
client.display.input.windowName(displayId?: integer, windowId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `windowId` | `string` | path | Yes | Window ID (decimal or hex) |

**Returns:** `display_WindowNameResult`  |  **HTTP:** `GET /api/v1/display/window/{windowId}/name`
**CLI:** `hoody display windows name`

---

#### `windowRaise` — Raise a window to the top

```typescript
client.display.input.windowRaise(displayId?: integer, data: display_WindowIdBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_WindowIdBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/window/raise`
**CLI:** `hoody display windows raise`

---

#### `windowResize` — Resize a window

```typescript
client.display.input.windowResize(displayId?: integer, data: display_WindowResizeBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_WindowResizeBody` | body | Yes |  |

**Returns:** `display_InputActionResponse`  |  **HTTP:** `POST /api/v1/display/window/resize`
**CLI:** `hoody display windows resize`

---

#### `windowSearch` — Search for windows by pattern

```typescript
client.display.input.windowSearch(displayId?: integer, data: display_WindowSearchBody)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `data` | `display_WindowSearchBody` | body | Yes |  |

**Returns:** `display_WindowSearchResult`  |  **HTTP:** `POST /api/v1/display/window/search`
**CLI:** `hoody display windows search`

---

### `client.display.screenshots` (5) — Screenshot capture and retrieval operations

#### `capture` — Capture a new screenshot

```typescript
client.display.screenshots.capture(base64?: boolean, displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `base64` | `boolean` | query | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data.  Accepted values: - `true`, `1`, `` (empty) - Return base64 JSON - `false`, `0` - Return binary (default) |
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_Base64ScreenshotResponse`  |  **HTTP:** `GET /api/v1/display/screenshot`
**CLI:** `hoody display screenshots capture`

---

#### `captureMetadata` — Capture screenshot and return metadata only

```typescript
client.display.screenshots.captureMetadata(displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_ScreenshotInfo`  |  **HTTP:** `GET /api/v1/display/screenshot/info`
**CLI:** `hoody display screenshots capture-metadata`

---

#### `getByTimestamp` — Retrieve a specific screenshot by timestamp

```typescript
client.display.screenshots.getByTimestamp(timestamp: string, base64?: boolean, displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `timestamp` | `string` | path | Yes | Unix timestamp of the screenshot. Use the `timestamp` field returned by screenshot metadata/list endpoints. Do not use `timestamp_human` for path queries. Must be numeric only for security. |
| `base64` | `boolean` | query | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data.  Accepted values: - `true`, `1`, `` (empty) - Return base64 JSON - `false`, `0` - Return binary (default) |
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_Base64ScreenshotResponse`  |  **HTTP:** `GET /api/v1/display/screenshot/{timestamp}`
**CLI:** `hoody display screenshots by-timestamp`

---

#### `getLatest` — Retrieve the most recent screenshot

```typescript
client.display.screenshots.getLatest(base64?: boolean, displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `base64` | `boolean` | query | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data.  Accepted values: - `true`, `1`, `` (empty) - Return base64 JSON - `false`, `0` - Return binary (default) |
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_Base64ScreenshotResponse`  |  **HTTP:** `GET /api/v1/display/screenshot/last`
**CLI:** `hoody display screenshots latest`

---

#### `getLatestMetadata` — Get metadata for the most recent screenshot

```typescript
client.display.screenshots.getLatestMetadata(displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_ScreenshotInfo`  |  **HTTP:** `GET /api/v1/display/screenshot/last/info`
**CLI:** `hoody display screenshots latest-metadata`

---

### `client.display.thumbnails` (3) — Thumbnail image operations

#### `capture` — Capture a new screenshot thumbnail

```typescript
client.display.thumbnails.capture(base64?: boolean, displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `base64` | `boolean` | query | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data.  Accepted values: - `true`, `1`, `` (empty) - Return base64 JSON - `false`, `0` - Return binary (default) |
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_Base64ScreenshotResponse`  |  **HTTP:** `GET /api/v1/display/thumbnail`
**CLI:** `hoody display thumbnails capture`

---

#### `getByTimestamp` — Retrieve a specific thumbnail by timestamp

```typescript
client.display.thumbnails.getByTimestamp(timestamp: string, base64?: boolean, displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `timestamp` | `string` | path | Yes | Unix timestamp of the screenshot. Use the `timestamp` field returned by screenshot metadata/list endpoints. Do not use `timestamp_human` for path queries. Must be numeric only for security. |
| `base64` | `boolean` | query | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data.  Accepted values: - `true`, `1`, `` (empty) - Return base64 JSON - `false`, `0` - Return binary (default) |
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_Base64ScreenshotResponse`  |  **HTTP:** `GET /api/v1/display/thumbnail/{timestamp}`
**CLI:** `hoody display thumbnails by-timestamp`

---

#### `getLatest` — Retrieve the most recent thumbnail

```typescript
client.display.thumbnails.getLatest(base64?: boolean, displayId?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `base64` | `boolean` | query | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data.  Accepted values: - `true`, `1`, `` (empty) - Return base64 JSON - `false`, `0` - Return binary (default) |
| `displayId` | `integer` | query | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

**Returns:** `display_Base64ScreenshotResponse`  |  **HTTP:** `GET /api/v1/display/thumbnail/last`
**CLI:** `hoody display thumbnails latest`


### Body schemas

- `display_ClipboardWriteBody` — `{ text*: string, selection: "clipboard" | "primary" | "secondary"="clipboard" }`
- `display_MouseClickBody` — `{ button: int=1, repeat: int=1, delay: int, window: int | string }`
- `display_MouseMoveBody` — `{ x*: int, y*: int, window: int | string, screen: int, sync: bool }`
- `display_MouseScrollBody` — `{ direction*: "up" | "down" | "left" | "right", clicks: int=5 }`
- `display_KeyboardTypeBody` — `{ text*: string, window: int | string, delay: int, clearModifiers: bool }`
- `display_KeyboardKeyBody` — `{ keys*: string[], window: int | string, delay: int, clearModifiers: bool }`
- `display_KeyboardKeyDownBody` — `{ key*: string, window: int | string, holdMs: int }`
- `display_WindowIdBody` — `{ windowId*: int | string }`
- `display_WindowMoveBody` — `{ windowId*: int | string, x*: int, y*: int, sync: bool, relative: bool }`
- `display_WindowResizeBody` — `{ windowId*: int | string, width*: int, height*: int, sync: bool, useHints: bool }`
- `display_WindowSearchBody` — `{ pattern*: string, name: bool, class: bool, classname: bool, onlyVisible: bool }`
- `display_ClickAtBody` — `{ x*: int, y*: int, button: int=1 }`
- `display_TypeAtBody` — `{ x*: int, y*: int, text*: string, delay: int }`
- `display_DragBody` — `{ startX*: int, startY*: int, endX*: int, endY*: int, button: int=1, steps: int }`
- `display_SelectBody` — `{ x*: int, y*: int, endX*: int, endY*: int }`
- `display_ActBody` — `{ action*: string, params: object, screenshot: bool=true, screenshotDelay: int=100, screenshotRegion: string }`
- `display_WaitBody` — `{ ms*: int, screenshot: bool=false }`
- `display_BatchBody` — `{ actions*: { action*: string, params: object }[] }`


---

<!-- ===== namespace: exec ===== -->

# `exec` — micro-services: any script or API as an instant HTTP endpoint

## Purpose

**Default tool when the user asks "write me an API" or "expose this script".** Drop a `.ts` / `.js` file in the scripts dir and it becomes a live HTTP handler — no framework, no build step, no deploy. The kit keeps it loaded and ready: each request is fast, supervised, schematized via magic comments / OpenAPI, log-streamed, metric-instrumented, and updateable by overwriting the file. Treat each script like a tiny microservice — single responsibility, simple in spirit, but it can shell out to anything (curl, ffmpeg, Python, native binaries, …) since it runs as a normal process inside the container.

**Routes only auto-mount for `.ts` / `.js` files.** Bare `.sh` / `.py` files dropped in the scripts dir are NOT exposed as HTTP — wrap them by writing a thin `.ts` handler that shells out via `Bun.$`. From a `.ts` handler you can run anything on `$PATH` (curl, ffmpeg, Python, native binaries) in one line.

To make a script **public**: create a `proxyAliases.create({ container_id: '<containerId>', program: 'exec', target_path: '/<your-script>', allow_path_override: false })` — get back `https://<alias>.{server_name}.containers.hoody.com` with no `containerId` leak in the URL. Layer Password / Token / JWT / IP gates via `proxyPermissionsContainer.*` exactly like any other kit URL.

**Trust model — read carefully.** Scripts run inside the container with full container privileges. They are NOT a sandbox for untrusted user code. Anyone who can invoke a script can do everything the script can do (read files, hit other kits, spawn processes). Use them for *your* APIs / cron logic / webhooks / ETL — don't expose them as an arbitrary code-execution surface to anonymous internet users without thinking through the gate stack first.

## When to use

- "Write me an API endpoint that does X" — default to a `hoody-exec` script before reaching for a Node app + framework.
- **"Build a workflow"** — multi-step pipelines (call agent A → validate with agent B → trigger action C, fan-out / fan-in, retry logic, conditional branches). One script = one orchestrator; each step is a function call inside the same Bun process, so no inter-service plumbing. State between requests via `bun:sqlite` (`Database`), the `sqlite` kit, the `files` kit, or in-process module-scope.
- Webhooks (GitHub / Stripe / your CI), ETL pipelines, batch transformers, AI agent tools.
- Wrap a bash one-liner or Python helper as an HTTP API with zero scaffolding.
- Schematize inputs / outputs via magic-comment annotations, auto-publish OpenAPI.
- Pin npm deps per script, stream logs/metrics, drop a `package.json` like any normal JS project.

## When NOT to use

- **Untrusted-input code execution** — it's not sandboxed. Use a fresh container (or a stricter runtime) per untrusted caller.
- Long-lived processes / supervisors → `daemon` (exec is request/response).
- Schedules outliving the kit → `cron` (`exec.schedules.*` is in-process and dies with the kit).
- File I/O outside the scripts dir → `files`. Interactive shells → `terminal`. Container lifecycle → `daemon`. Headless web → `browser`.

## Prerequisites

- Scripts dir `/hoody/storage/hoody-exec/scripts/{subdomain}/{instanceId}/` (subdomain defaults to `default`, e.g. `…/scripts/default/1/`) is service-managed; write only via `scripts.write`.
- **`require('hoody-sdk')` works with no install step** — the runtime auto-installs any `require()`d npm package on first execution (it is not pre-injected or bundled). Import from `'hoody-sdk'` (NOT `'hoody-sdk'`). The constructor takes an explicit config; `withContainer` is async and returns a container-scoped client. Calls go through the Hoody Proxy — no localhost bypass — so all the usual capability gates / request hooks / proxy logs apply (see § No local bypass in `SKILL-SDK.md`).
- The `hoody` CLI is also on `$PATH` if you'd rather shell out: `Bun.$\`hoody projects list\`` from the same script works end-to-end.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Write, validate, invoke

1. `scripts.write` — `path`, `content`, `createDirs:true`, `validate:true` (default); 400 + `validation` on fail.
2. `scripts.read` — confirm bytes.
3. `POST {EXEC_BASE_URL}/<path-without-extension>` — body parsed, return auto-serialised, output streamed.
4. `scripts.list` — verify.

### 2. Pin deps, iterate

1. `dependencies.check` → `dependencies.install` → `package.pinVersions`.
2. `validate.validateScript` + `validateMagicComments` (`// @description`, `// @cors`, `// @timeout`).
3. `scripts.write` (auto-validates unless `validate:false`).
4. `cache.clear` — drop compiled cache; ephemeral runtime, no state across recompiles.

### 3. Debug + OpenAPI

1. `logs.list` / `logs.search` / `logs.read` (streamed `--lines`/`--tail`).
2. `monitor.getActiveRequests` / `monitor.getStats` / `monitor.getScriptPerformance`.
3. `openapi.listScripts` → `openapi.generate` → `openapi.merge` → `openapi.serve` → `openapi.validateSchema`.

## Quirks & gotchas

- **Direct execution / top-level `return` is the canonical script shape**; `req`, `res`, `metadata`, `shared`, `console`, and `require` are auto-injected. `module.exports = handler` and many `export default` forms are accepted as compatibility inputs. The stored file is ALWAYS kept verbatim; the pattern-normaliser rewrites the code at load time on every request, independent of `validate`.
- Prefer top-level code with auto-injected `req`/`res` (or just `return …` from the script body); use `module.exports = handler` only as a compatibility style.
- `scripts.delete` needs literal `confirm=true`.
- `scripts.write` defaults `createDirs:true`, `validate:true`. `.md`/`.yaml`/`.env`/any other non-`.ts`/`.js`/`.json` extension skip; `.json` JSON.parse; only `.ts`/`.js` full pipeline.
- Invocation = bare path (`POST /greeting`), NOT `/api/v1/exec/...`.
- Proxy-alias uses `program: 'exec'`; `proxyDiscovery.listContainerProxyServices` returning `[]` is normal.
- `scripts.write`/`delete` accept optional `execId` (alias `exec_id`) + `subdomain`; query wins.
- **Built-in AI, zero setup — never wire up your own provider/key for AI in a script.** Every script gets pre-injected globals: `ai` (`ai.generate(prompt)` / `ai.stream(prompt)` / `ai.object({ schema, prompt })`), plus `openai` (provider factory), `model` (the default model instance), and `generateText`/`streamText`/`generateObject`. They are already wired to **Hoody AI** (`https://ai.hoody.com/api/v1`, default model **`hoody-ai/hoody-free`**). **No `require()`, no base URL, and no API key** — the key auto-defaults to `container-<hash>` and is a usage-tracking tag, NOT auth, so you specify nothing. **The default model is the free tier: it costs nothing and needs no wallet credit, so a script with no `// @ai-model` line runs on a brand-new account.** Overriding it with a catalog model (`GET https://api.hoody.com/api/v1/ai/models`) bills the wallet and is **refused outright** while `ai_limit` is `0.00` — check `GET /api/v1/wallet/balances/ai` before you name one. Override per-script with magic comments (`// @ai-model minimax/minimax-m3` — paid, `// @ai-temperature 0.7`, `// @ai-max-tokens 2048`, `// @ai-key <custom-tag>`); set a system prompt via a sibling `<script>.system.md` (or directory-level `_system.md`).

## Common errors

- `400 Script validation failed` — fix or `validate:false`.
- `400 confirm=true parameter required for safety`.
- `403 Path is excluded from access` — recheck `safePath`.
- `400 Invalid JSON` on `.json` — `validate:false` bypasses.

## Related namespaces

- `files` (FS outside scripts dir), `cron` (outlives kit; `exec.schedules.*` is in-process), `terminal`, `daemon` (`system.restartServer` = kit only), `proxyLogs` (edge vs kit logs).

## Examples

Every step in every example was live-tested against a real `exec-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first.

Two facts to keep in mind across every example:

- **Script invocation is bare path** — `POST /<basename-without-extension>`, NOT `/api/v1/exec/...`. Writing `echo.js` exposes `POST {KIT}/echo`.
- **Canonical shape: top-level code with auto-injected `req`/`res`/`metadata`/`shared`/`console`/`require` and `return …`.** `module.exports = (req, res) => res.json(...)` is also accepted (compatibility style, normalised at load time on every request regardless of `validate`). `req.query` does NOT exist on the raw request object — read query/route params from the auto-injected `metadata.query` (or `metadata.parameters`) instead.

### 1. One-line echo handler — write, invoke, read back

**Goal:** prove the loop end-to-end. Drop a 1-line CommonJS handler, hit its bare path, read it back to confirm the bytes.

**Step 1 — write `echo.js`.** `validate:true` is the default; the kit returns `validated:true` in the response when syntax + TS-transpile + dependency-check + magic-comment parse all pass.

```typescript
await client.exec.scripts.write({
  path: 'echo.js',
  content: 'module.exports = (req, res) => res.json({ ok: true, body: req.body });\n',
});
```
**Step 2 — invoke `POST /echo`** (bare path, NOT `/api/v1/exec/echo`). Body is auto-parsed; the return value of `res.json(...)` is the wire body.

```typescript
// `exec` kit accepts the bare kit URL as the bearer — NO Authorization header needed.
const r = await fetch(`https://${P}-${C}-exec-1.${N}.containers.hoody.com/echo`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ hello: 'world' }),
});
console.log(await r.json());
```
**Step 3 — read it back.** `scripts.read` returns the literal content + parsed `magicComments` + metadata.

```typescript
const r = await client.exec.scripts.read({ path: 'echo.js' });
console.log(r.data!.content);
```
### 2. Multi-step workflow — agent A → check with B → action C

**Goal:** one script orchestrates three steps as plain async functions. State lives in script-local closures, no inter-service plumbing. The externals are stubbed inline; in a real script swap them for `client.curl.execute(...)` / SDK calls.

```typescript
await client.exec.scripts.write({
  path: 'workflow.js',
  content: `module.exports = async (req, res) => {
  // Real version — the Hoody SDK is auto-loaded inside scripts:
  //   const { HoodyClient } = require('hoody-sdk');
  //   The kit injects NO HOODY_* variables — script env is process.env plus a `.env`
  //   companion file next to the script, so you must supply these yourself:
  //   const hoody = new HoodyClient({ baseURL: process.env.HOODY_API_URL!, token: process.env.HOODY_TOKEN });
  //   const c = await hoody.withContainer(process.env.HOODY_CONTAINER!);
  //   const a = await c.curl.execute({ url: 'https://agent-a/score', method: 'POST', data: req.body });
  const callA = async () => ({ score: 0.91, label: 'spam' });
  const checkB = async (a) => ({ verdict: a.score > 0.8 ? 'block' : 'allow' });
  const actC  = async (v) => ({ executed: v === 'block' ? 'quarantined' : 'delivered' });
  const a = await callA();
  const b = await checkB(a);
  res.json({ a, b, c: await actC(b.verdict) });
};
`,
});
```
### 3. Webhook receiver with HMAC signature verification

**Goal:** GitHub-style `X-Hub-Signature-256` verification using `crypto.timingSafeEqual`. Reject 401 on bad sig.

**Step 1 — write the verifier.** No `npm install` needed — `crypto` is a runtime built-in.

```typescript
const webhookCode = `const crypto = require('crypto');
const SECRET = process.env.WEBHOOK_SECRET || 'shhh-test';
module.exports = async (req, res) => {
  const sig = req.headers['x-hub-signature-256'] || '';
  const body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body || {});
  const expect = 'sha256=' + crypto.createHmac('sha256', SECRET).update(body).digest('hex');
  let ok = false;
  try { ok = sig.length === expect.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expect)); } catch {}
  if (!ok) return res.status(401).json({ error: 'bad sig' });
  res.json({ accepted: true, payload: req.body });
};
`;
await client.exec.scripts.write({ path: 'webhook.js', content: webhookCode });
```
**Step 2 — fire a signed request.** The Bun `req.body` is an *object* if Content-Type was JSON, so re-serialise before HMAC'ing on both sides for byte-stable signing.

### 4. Pin npm deps — `dependencies.check` → `dependencies.install` → `package.pinVersions`

**Goal:** add `lodash` to the kit's package.json, install it, then pin to an exact version so future installs are deterministic.

**Step 1 — check.** Returns `installed[]` and `missing[]` per module so you can decide what to install.

```typescript
const r = await client.exec.dependencies.check({ code: 'const _ = require("lodash");' });
console.log(r.data!.missing);
```
**Step 2 — install.** `modules` accepts a string or array; specs may pin (`"lodash@4.17.21"`).

```typescript
await client.exec.dependencies.install({ modules: ['lodash'] });
```
**Step 3 — pin to exact versions** (drops the leading `^`/`~`).

```typescript
await client.exec.package.pinVersions({ packages: ['lodash'] });
```
### 5. Validate-only flow + magic comments

**Goal:** lint a script (and its magic comments — `@cors`, `@timeout`, `@description`, `@schedule`, `@token`, `@websocket`, …) BEFORE writing it. Useful in CI / pre-commit / LLM-output gating. (`@method` / `@route` are not directives — HTTP method dispatch is per-handler logic.)

```typescript
const code = `// @cors *
// @timeout 5000
// @description Greeting handler
module.exports = (req, res) => res.json({ hi: 1 });`;
const v = await client.exec.validate.validateScript({ code });
const m = await client.exec.validate.validateMagicComments({ code });
console.log(v.data!.valid, m.data!.magicComments);
```
If `valid:true`, ship it via `scripts.write` (default `validate:true` will re-run the same checks server-side). If `valid:false`, the `results.{syntax,typescript,dependencies,magicComments}` slots tell you exactly which checker rejected it.

### 6. Auto-publish OpenAPI for your scripts

**Goal:** every script gets a route entry (and inferred request/response shapes from any handler-attached schema) in a single OpenAPI 3.0 document the proxy can serve.

**Step 1 — list what would be in the spec** (route paths derived from filenames):

```typescript
const r = await client.exec.openapi.listScripts();
console.log(r.data!.scripts);
```
**Step 2 — fetch the served spec** (what an OpenAPI viewer / SDK generator will see). `format=json|yaml`.

```typescript
const r = await client.exec.openapi.serve({ format: 'json' });
require('fs').writeFileSync('/tmp/user-scripts.openapi.json', JSON.stringify(r.data, null, 2));
```
**Step 3 — merge a hand-written spec layer** (auth / examples / hosts) on top of the auto-generated one with `openapi.merge`. `openapi.generate` rebuilds the on-disk spec from current scripts; `openapi.validateSchema` validates a script's companion `.openapi.json` file (the per-script schema sidecar in the `openapi-json` format), not the merged/served spec.

### 7. Tail script logs in real time

**Goal:** watch what your handler logged for the last N requests. Default `logs.list` returns kit-wide log files; `logs.read` slices a specific one.

```typescript
const list = await client.exec.logs.list();
const tail = await client.exec.logs.read({ file: logName, lines: 200, tail: true }  // logName from logs.list().logs[].name);
```
Access logging is ON by default (`@log-level` defaults to `standard`); the magic comments opt *out* — `// @log-level none` disables it.

### 8. Monitor active requests + per-script stats

**Goal:** "is anything stuck?" + "which script is the hot path?". `monitor.getStats` is a single snapshot; `monitor.getActiveRequests` lists in-flight HTTP/WS; `monitor.getScriptPerformance` aggregates by script.

```typescript
const stats = await client.exec.monitor.getStats();
const active = await client.exec.monitor.getActiveRequests();
const perf = await client.exec.monitor.getScriptPerformance({});
```
For Prometheus scraping, `GET /api/v1/exec/monitor/metrics` returns text/plain in standard exposition format.

### 9. Wrap a bash one-liner as an HTTP API with `Bun.$`

**Goal:** turn `df -h /` into a JSON HTTP endpoint with no scaffolding. `Bun.$` is in scope inside any script.

```typescript
await client.exec.scripts.write({
  path: 'disk-usage.js',
  content: `module.exports = async (req, res) => {
  const out = await Bun.$\`df -h --output=source,size,used,avail,target /\`.text();
  res.json({ disk: out.trim().split('\\n').slice(1).map(l => l.split(/\\s+/)) });
};
`,
});
```
Same pattern works for `python3 -c "..."`, `ffmpeg`, native binaries, anything on `$PATH`. The script handler runs inside the container as a normal process.

### 10. In-process schedule via `@schedule` directive

**Goal:** fire a script every 5 minutes WITHOUT the `cron` namespace. The schedule lives inside the kit; if the kit restarts, the schedule re-registers from disk on boot. (For schedules that must survive a kit-down — use the `cron` namespace instead.)

**Step 1 — write a script with `// @schedule`** (5-field cron or a nickname like `@daily`, UTC, one per file). `console.log` lines go to the kit log; `res.json` is what an HTTP `triggerSchedule` call returns.

```typescript
await client.exec.scripts.write({
  path: 'tick.js',
  content: `// @schedule */5 * * * *
// @description Heartbeat — fires every 5 minutes
module.exports = async (req, res) => {
  console.log('[tick] fired at', new Date().toISOString());
  res.json({ ok: true, ts: Date.now() });
};
`,
});
```
**Step 2 — confirm it registered.** Listing the schedules shows the parsed expression, the in-process timer state, and the absolute on-disk `scriptPath` you'll need to trigger it.

```typescript
const r = await client.exec.schedules.listSchedules();
console.log(r.data!.schedules);
```
**Step 3 — fire it on demand.** `scriptPath` accepts either the absolute path returned in step 2 OR a path relative to the kit's scripts dir (the handler resolves relative paths against `scriptDir`). `force:true` bypasses the `// @token` refusal so you can manually exercise scripts that gate cron-only.

```typescript
await client.exec.schedules.triggerSchedule({
  scriptPath: '/hoody/storage/hoody-exec/scripts/default/1/tick.js',
  force: true,
});
const hist = await client.exec.schedules.scheduleHistory({ limit: 5 });
```
**Stop the schedule** by deleting the script (`scripts.delete?path=tick.js&confirm=true`) or by editing the file to remove the `// @schedule` directive and calling `schedules.reloadSchedules`.

### 11. Use the built-in AI — zero setup (no key, no import)

**Goal:** call an LLM from a script with **zero AI boilerplate**. The runtime pre-injects `ai` (`ai.generate` / `ai.stream` / `ai.object`), plus `openai`, `model`, `generateText`, `streamText`, `generateObject` — already wired to **Hoody AI** (`https://ai.hoody.com/api/v1`). You never import anything, set a base URL, or pass an API key — the key auto-defaults to `container-<hash>` (a usage-tracking tag, not auth). Default model is **`hoody-ai/hoody-free`**, the free tier — no wallet credit needed, so the script below works on a brand-new account with `ai_limit: "0.00"`. Override per-script with `// @ai-model <provider/model>` **only when you need a paid catalog model and have confirmed the wallet has AI credit**, and set a system prompt with a sibling `summarize.system.md`.

**Step 1 — write the script. The only "AI" line is `ai.generate(...)`.** (Read query params from `metadata.query`, not `req.query`.)

```typescript
await client.exec.scripts.write({
  path: 'summarize.js',
  content: `// @description One-line summary via built-in Hoody AI (free model by default)
module.exports = async (req, res) => {
  const text = metadata.query.q || 'Say hello in one sentence.';
  const result = await ai.generate('Summarize in one line: ' + text); // ai/model/generateText all pre-injected
  res.json({ summary: result.text });
};
`,
});
```
**Step 2 — invoke it** (bare path on the exec kit URL). No key anywhere in the call:

```
https://{P}-{C}-exec-1.{N}.containers.hoody.com/summarize?q=Hoody+is+a+remote-first+computing+platform
# → {"summary":"..."}
```

For structured output use `ai.object({ schema, prompt })` (Zod), and to stream just `return (await ai.stream(prompt)).textStream`.

## Reference

**Accessor:** `client.exec`  |  **Import:** `import * as exec from 'hoody-sdk/exec'`

### `client.exec.cache` (1) — Cache

#### `clear` — Clear Cache

```typescript
client.exec.cache.clear(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ hostname: string, scriptPath: string, clearVm: bool=true, clearState: bool=false, clearAll: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/cache/clear`
**CLI:** `hoody exec system cache-clear`

---

### `client.exec.dependencies` (3) — Dependencies

#### `check` — Check Dependencies

```typescript
client.exec.dependencies.check(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ code: string, modules: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/dependencies/check`
**CLI:** `hoody exec packages check`

---

#### `install` — Install Dependencies

```typescript
client.exec.dependencies.install(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ modules*: string | string[], force: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/dependencies/install`
**CLI:** `hoody exec packages add-modules`

---

#### `listBundled` — List Bundled Dependencies

```typescript
client.exec.dependencies.listBundled()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/dependencies/bundled`
**CLI:** `hoody exec packages list`

---

### `client.exec.execution` (1) — Script Execution

#### `execute` — Execute Script (GET)

```typescript
client.exec.execution.execute(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Script path (supports Next.js-style routing) |

**Returns:** `any`  |  **HTTP:** `GET /{path}`

---

### `client.exec.health` (1) — Health

#### `check` — Health Check

```typescript
client.exec.health.check()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/health`
**CLI:** `hoody exec health`

---

### `client.exec.ids` (1) — List

#### `list` — List All Exec Ids

```typescript
client.exec.ids.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/list`
**CLI:** `hoody exec namespaces list`

---

### `client.exec.logs` (5) — Logs

#### `clear` — Clear Logs

```typescript
client.exec.logs.clear(file?: string, type?: string, olderThanDays?: string, confirm?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `file` | `string` | query | No | File query parameter |
| `type` | `string` | query | No | Type query parameter |
| `olderThanDays` | `string` | query | No | OlderThanDays query parameter |
| `confirm` | `string` | query | No | Confirm query parameter |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/exec/logs/clear`
**CLI:** `hoody exec logs clear`

---

#### `list` — List Logs

```typescript
client.exec.logs.list(type?: string, limit?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `type` | `string` | query | No | Type query parameter |
| `limit` | `string` | query | No | Limit query parameter |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/logs/list`
**CLI:** `hoody exec logs list`

---

#### `read` — Read Log

```typescript
client.exec.logs.read(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ file: string, executionId: string, lines: int=100, tail: bool=true, search: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/logs/read`
**CLI:** `hoody exec logs read`

---

#### `search` — Search Logs

```typescript
client.exec.logs.search(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ query: string, regex: string, files: any[], limit: int=1000, caseSensitive: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/logs/search`
**CLI:** `hoody exec logs search`

---

#### `stream` — Stream Logs

```typescript
client.exec.logs.stream(file: string, follow?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `file` | `string` | query | Yes | File query parameter |
| `follow` | `string` | query | No | Follow query parameter |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/exec/logs/stream`
**CLI:** `hoody exec logs stream`

---

### `client.exec.magic` (4) — Magic-comments

#### `bulkUpdate` — Bulk Update Magic Comments

```typescript
client.exec.magic.bulkUpdate(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ directory: string, execId: string, comments: string, extension: string=".ts", recursive: bool=true, dry_run: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/magic-comments/bulk-update`
**CLI:** `hoody exec magic-comments bulk-update`

---

#### `getSchema` — Get Magic Comments Schema

```typescript
client.exec.magic.getSchema()
```

**Returns:** `exec_MagicCommentSchemaDocument`  |  **HTTP:** `GET /api/v1/exec/magic-comments/schema`
**CLI:** `hoody exec magic-comments schema`

---

#### `read` — Read Magic Comments

```typescript
client.exec.magic.read(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | query | Yes | Path query parameter |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/magic-comments/read`
**CLI:** `hoody exec magic-comments read`

---

#### `updateHandler` — Update Magic Comments Handler

```typescript
client.exec.magic.updateHandler(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ path*: string, comments: string, dry_run: bool=false }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/exec/magic-comments/update`
**CLI:** `hoody exec magic-comments update`

---

### `client.exec.monitor` (5) — Monitor

#### `getActiveRequests` — Get Active Requests

```typescript
client.exec.monitor.getActiveRequests()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/monitor/active-requests`
**CLI:** `hoody exec system active-requests`

---

#### `getScriptPerformance` — Get Script Performance

```typescript
client.exec.monitor.getScriptPerformance(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/monitor/script-performance`
**CLI:** `hoody exec scripts performance`

---

#### `getStats` — Get Stats

```typescript
client.exec.monitor.getStats()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/monitor/stats`
**CLI:** `hoody exec system stats`

---

#### `listMonitorScripts` — List Monitor Scripts

```typescript
client.exec.monitor.listMonitorScripts(limit?: integer, sort?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No | Max number of scripts to return. Clamped to [1, 500]. Default 100. |
| `sort` | `string` | query | No | Sort key. `lastActivity` (default) sorts by most recent activity; other keys sort descending by the matching metric. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/monitor/scripts`

---

#### `prometheusExport` — Prometheus Export

```typescript
client.exec.monitor.prometheusExport()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/monitor/metrics`
**CLI:** `hoody exec system prometheus`

---

### `client.exec.openapi` (6) — User-openapi

#### `generate` — Generate User Open A P I

```typescript
client.exec.openapi.generate(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/user-openapi/generate`
**CLI:** `hoody exec openapi generate`

---

#### `listScripts` — List User Scripts

```typescript
client.exec.openapi.listScripts(directory?: string, dir?: string, subdomain?: string, execId?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `directory` | `string` | query | No | Script directory to list (absolute or relative to scripts-dir). Default: `scripts`. |
| `dir` | `string` | query | No | Alias of `directory`. Ignored when `directory` is provided. |
| `subdomain` | `string` | query | No | Limit scan to scripts under this subdomain. Falls back to the Host header when omitted. |
| `execId` | `string` | query | No | Limit scan to scripts under this execId. Falls back to the Host header when omitted. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/user-openapi/list`
**CLI:** `hoody exec scripts list-user`

---

#### `merge` — Merge Open A P I Specs

```typescript
client.exec.openapi.merge(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/user-openapi/merge`
**CLI:** `hoody exec openapi merge`

---

#### `serve` — Serve Generated Spec

```typescript
client.exec.openapi.serve(dir?: string, directory?: string, format?: string, subdomain?: string, execId?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `dir` | `string` | query | No | Script directory to scan (absolute or relative to scripts-dir). Default: `scripts`. |
| `directory` | `string` | query | No | Alias of `dir`. Ignored when `dir` is provided. |
| `format` | `string` | query | No | Output format. `json` (default) or `yaml`. |
| `subdomain` | `string` | query | No | Limit scan to scripts under this subdomain. Falls back to the Host header when omitted. |
| `execId` | `string` | query | No | Limit scan to scripts under this execId. Falls back to the Host header when omitted. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/user-openapi/spec`
**CLI:** `hoody exec openapi serve`

---

#### `serveSchema` — Serve Schema File

```typescript
client.exec.openapi.serveSchema(file?: string, path?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `file` | `string` | query | No | Absolute or scripts-dir-relative path to the target script (e.g. `default/api/users/[id].ts`). Either `file` or `path` must be provided. |
| `path` | `string` | query | No | Alias of `file`. Either `file` or `path` must be provided. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/user-openapi/schema`
**CLI:** `hoody exec openapi serve-schema`

---

#### `validateSchema` — Validate User Schema

```typescript
client.exec.openapi.validateSchema(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/user-openapi/validate`
**CLI:** `hoody exec validate user-schema`

---

### `client.exec.package` (6) — Package

#### `compare` — Compare Packages

```typescript
client.exec.package.compare(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/package/compare`
**CLI:** `hoody exec packages compare`

---

#### `initJson` — Init Package Json

```typescript
client.exec.package.initJson(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ name: string="hoody-exec-project", version: string="1.0.0", description: string="Hoody Exec project", force: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/package/init`
**CLI:** `hoody exec packages json init`

---

#### `install` — Install Packages

```typescript
client.exec.package.install(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ packages: any[], dev: bool=false, save: bool=true, force: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/package/install`
**CLI:** `hoody exec packages install`

---

#### `pinVersions` — Pin Versions

```typescript
client.exec.package.pinVersions(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ packages: any[] }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/package/pin`
**CLI:** `hoody exec packages pin`

---

#### `readJson` — Read Package Json

```typescript
client.exec.package.readJson()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/package/read`
**CLI:** `hoody exec packages json read`

---

#### `updateJson` — Update Package Json

```typescript
client.exec.package.updateJson(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ dependencies: string, scripts: string, metadata: object, remove: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/package/update`
**CLI:** `hoody exec packages json update`

---

### `client.exec.route` (3) — Route

#### `discover` — Discover Routes

```typescript
client.exec.route.discover(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ baseDir: string="", includeMetadata: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/route/discover`
**CLI:** `hoody exec routes discover`

---

#### `resolve` — Resolve Route

```typescript
client.exec.route.resolve(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/route/resolve`
**CLI:** `hoody exec routes resolve`

---

#### `test` — Test Route

```typescript
client.exec.route.test(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/route/test`
**CLI:** `hoody exec routes test`

---

### `client.exec.schedules` (4) — Schedules

#### `listSchedules` — List Schedules

```typescript
client.exec.schedules.listSchedules()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/schedules/list`
**CLI:** `hoody exec schedules list`

---

#### `reloadSchedules` — Reload Schedules

```typescript
client.exec.schedules.reloadSchedules(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ dry_run: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/schedules/reload`
**CLI:** `hoody exec schedules reload`

---

#### `scheduleHistory` — Schedule History

```typescript
client.exec.schedules.scheduleHistory(scriptPath?: string, since?: string, limit?: integer, includeRotated?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `scriptPath` | `string` | query | No | Filter entries to a specific script (relative to scripts-dir). Optional. |
| `since` | `string` | query | No | ISO 8601 lower bound on `ts`. Optional. |
| `limit` | `integer` | query | No | Max entries to return. Default 100, hard max 1000. |
| `includeRotated` | `boolean` | query | No | When true, also scan rotated fires.log.* files (slower). |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/schedules/history`
**CLI:** `hoody exec schedules history`

---

#### `triggerSchedule` — Trigger Schedule

```typescript
client.exec.schedules.triggerSchedule(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ scriptPath*: string, force: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/schedules/trigger`
**CLI:** `hoody exec schedules trigger`

---

### `client.exec.scripts` (6) — Scripts

#### `delete` — Delete Script

```typescript
client.exec.scripts.delete(path: string, confirm?: string, execId?: string, exec_id?: string, subdomain?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | query | Yes | Path query parameter |
| `confirm` | `string` | query | No | Confirm query parameter |
| `execId` | `string` | query | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | `string` | query | No | Alias for execId (snake_case). |
| `subdomain` | `string` | query | No | Optional subdomain namespace used with execId for path resolution. |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/exec/scripts/delete`
**CLI:** `hoody exec scripts delete`

---

#### `getTree` — Get Script Tree

```typescript
client.exec.scripts.getTree(execId?: string, exec_id?: string, subdomain?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `execId` | `string` | query | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | `string` | query | No | Alias for execId (snake_case). |
| `subdomain` | `string` | query | No | Optional subdomain namespace used with execId for path resolution. |
| `data` | `object` | body | No |  |

**Body:** `{ baseDir: string="", maxDepth: int=10, includeMetadata: bool=false, execId: string, exec_id: string, subdomain: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/scripts/tree`
**CLI:** `hoody exec scripts tree`

---

#### `list` — List Scripts

```typescript
client.exec.scripts.list(dir?: string, filter?: string, metadata?: string, label?: string, tags?: string, mode?: string, enabled?: string, websocket?: string, recursive?: string, include_comments?: string, execId?: string, exec_id?: string, subdomain?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `dir` | `string` | query | No | Dir query parameter |
| `filter` | `string` | query | No | Filter query parameter |
| `metadata` | `string` | query | No | Metadata query parameter |
| `label` | `string` | query | No | Label query parameter |
| `tags` | `string` | query | No | Tags query parameter |
| `mode` | `string` | query | No | Mode query parameter |
| `enabled` | `string` | query | No | Enabled query parameter |
| `websocket` | `string` | query | No | Websocket query parameter |
| `recursive` | `string` | query | No | Recursive query parameter |
| `include_comments` | `string` | query | No | Include_comments query parameter |
| `execId` | `string` | query | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | `string` | query | No | Alias for execId (snake_case). |
| `subdomain` | `string` | query | No | Optional subdomain namespace used with execId for path resolution. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/scripts/list`
**CLI:** `hoody exec scripts list`

---

#### `move` — Move Script

```typescript
client.exec.scripts.move(execId?: string, exec_id?: string, subdomain?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `execId` | `string` | query | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | `string` | query | No | Alias for execId (snake_case). |
| `subdomain` | `string` | query | No | Optional subdomain namespace used with execId for path resolution. |
| `data` | `object` | body | Yes |  |

**Body:** `{ from*: string, to*: string, overwrite: bool=false, execId: string, exec_id: string, subdomain: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/scripts/move`
**CLI:** `hoody exec scripts move`

---

#### `read` — Read Script

```typescript
client.exec.scripts.read(path: string, execId?: string, exec_id?: string, subdomain?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | query | Yes | Path query parameter |
| `execId` | `string` | query | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | `string` | query | No | Alias for execId (snake_case). |
| `subdomain` | `string` | query | No | Optional subdomain namespace used with execId for path resolution. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/scripts/read`
**CLI:** `hoody exec scripts read`

---

#### `write` — Write Script

```typescript
client.exec.scripts.write(execId?: string, exec_id?: string, subdomain?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `execId` | `string` | query | No | Optional execution scope. When provided, relative paths resolve under default/{execId}/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | `string` | query | No | Alias for execId (snake_case). |
| `subdomain` | `string` | query | No | Optional subdomain namespace used with execId for path resolution. |
| `data` | `object` | body | Yes |  |

**Body:** `{ path*: string, content*: string, createDirs: bool=true, validate: bool=true, execId: string, exec_id: string, subdomain: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/scripts/write`
**CLI:** `hoody exec scripts write`

---

### `client.exec.sdk` (4) — Sdk

#### `delete` — Delete S D K

```typescript
client.exec.sdk.delete(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Id parameter |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/exec/sdk/:id`
**CLI:** `hoody exec sdks delete`

---

#### `get` — Get S D K

```typescript
client.exec.sdk.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Id parameter |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/sdk/:id`
**CLI:** `hoody exec sdks get`

---

#### `importSDK` — Import S D K

```typescript
client.exec.sdk.importSDK(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ execId*: string, source_url*: string, source_auth: string, middleware: string, magic_comments: string, force: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/sdk/import`
**CLI:** `hoody exec sdks import`

---

#### `list` — List S D Ks

```typescript
client.exec.sdk.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/sdk/list`
**CLI:** `hoody exec sdks list`

---

### `client.exec.state` (3) — Shared-state

#### `clear` — Clear Shared State

```typescript
client.exec.state.clear(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ hostname*: string, path: string, clearAll: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/shared-state/clear`
**CLI:** `hoody exec state clear`

---

#### `get` — Get Shared State

```typescript
client.exec.state.get(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ hostname*: string, path: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/shared-state/get`
**CLI:** `hoody exec state get`

---

#### `set` — Set Shared State

```typescript
client.exec.state.set(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ hostname*: string, path: string, value*: any, merge: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/shared-state/set`
**CLI:** `hoody exec state set`

---

### `client.exec.system` (4) — System

#### `getOpenApiJson` — Get OpenAPI Specification (JSON)

```typescript
client.exec.system.getOpenApiJson()
```

**Returns:** `any`  |  **HTTP:** `GET /openapi.json`

---

#### `getOpenApiYaml` — Get OpenAPI Specification (YAML)

```typescript
client.exec.system.getOpenApiYaml()
```

**Returns:** `any`  |  **HTTP:** `GET /openapi.yaml`

---

#### `getRestartStatus` — Get Restart Status

```typescript
client.exec.system.getRestartStatus()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/system/restart-status`
**CLI:** `hoody exec system restart-status`

---

#### `restartServer` — Restart Server

```typescript
client.exec.system.restartServer(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ graceful: bool=true, drainTimeoutMs: int=5000, reason: string="API restart request" }`

**Returns:** `void`  |  **HTTP:** `POST /api/v1/exec/system/restart`
**CLI:** `hoody exec system restart`

---

### `client.exec.templates` (6) — Templates

#### `createCustom` — Create Custom Template

```typescript
client.exec.templates.createCustom(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/templates/create-custom`
**CLI:** `hoody exec templates create`

---

#### `deleteCustom` — Delete Custom Template

```typescript
client.exec.templates.deleteCustom(name: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Name parameter |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/exec/templates/delete-custom/:name`
**CLI:** `hoody exec templates delete`

---

#### `generate` — Generate From Template

```typescript
client.exec.templates.generate(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ name*: string, variables: object, outputPath: string, saveFile: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/templates/generate`
**CLI:** `hoody exec templates generate`

---

#### `list` — List Templates

```typescript
client.exec.templates.list(category?: string, includeBuiltin?: boolean, includeCustom?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `category` | `string` | query | No | Filter templates to a single metadata category (e.g. `api`, `utility`). Omit to list all categories. |
| `includeBuiltin` | `boolean` | query | No | Include built-in templates in the result set. Default `true`. Accepts `true`/`false`/`1`/`0`. |
| `includeCustom` | `boolean` | query | No | Include user-supplied templates (from `_hoody/templates/`) in the result set. Default `true`. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/templates/list`
**CLI:** `hoody exec templates list`

---

#### `preview` — Preview Template

```typescript
client.exec.templates.preview(name: string, variables?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | query | Yes | Name query parameter |
| `variables` | `string` | query | No | Variables query parameter |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/exec/templates/preview`
**CLI:** `hoody exec templates preview`

---

#### `updateCustom` — Update Custom Template

```typescript
client.exec.templates.updateCustom(name: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Name parameter |
| `data` | `object` | body | No |  |

**Body:** `{ code: string, metadata: object }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/exec/templates/update-custom/:name`
**CLI:** `hoody exec templates update`

---

### `client.exec.validate` (6) — Validate

#### `validateDependencies` — Validate Dependencies

```typescript
client.exec.validate.validateDependencies(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/validate/dependencies`
**CLI:** `hoody exec validate dependencies`

---

#### `validateMagicComments` — Validate Magic Comments

```typescript
client.exec.validate.validateMagicComments(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/validate/magic-comments`
**CLI:** `hoody exec validate magic-comments`

---

#### `validateReturnType` — Validate Return Type

```typescript
client.exec.validate.validateReturnType(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ typeDefinition*: string, value*: any }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/validate/return-type`
**CLI:** `hoody exec validate return-type`

---

#### `validateScript` — Validate Script

```typescript
client.exec.validate.validateScript(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/validate/script`
**CLI:** `hoody exec validate script`

---

#### `validateSyntax` — Validate Syntax

```typescript
client.exec.validate.validateSyntax(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/validate/syntax`
**CLI:** `hoody exec validate syntax`

---

#### `validateTypeScript` — Validate Type Script

```typescript
client.exec.validate.validateTypeScript(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ code*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/exec/validate/typescript`
**CLI:** `hoody exec validate types`


---

<!-- ===== namespace: files ===== -->

# `files` — container filesystem over HTTP, with automatic Git-like change history

## Purpose

**Default surface: the container's own filesystem, exposed over HTTP — with optional automatic mutation journaling when the kit is started with `--journal-path`.** Read, write, copy, move, delete, stat, chmod, list, glob, grep, archive-preview/extract, fetch URLs into the FS, resumable upload — all on absolute container paths (`/workspace/main.py`, `/etc/hostname`, `/hoody/databases/foo.db`). No backend flag needed.

**Headline feature when journaling is on — automatic change history (think Git, but for every file write).** With the journal enabled, every `PUT` / `PATCH` / `DELETE` / `MOVE` / `COPY` is appended to a per-container mutation log: monotonic sequence number, timestamp, path, op, size, hash. **You never lose a previous version of a journaled path.** The journal lets you:

- **Time-travel a single file** — `files.get` `?revision=<seq>` or `?at=<unix-ms>` returns the bytes as they were at that point.
- **List a file's history** — `files.get` `?history=1` walks every revision of the path.
- **Diff between revisions** — `files.get` `?diff=1&from_seq=<N>` returns a unified diff between revision `N` and current.
- **Replay / audit** — `journal.query` `?path=<p>&after_id=<seq>` streams every operation since seq `N`. Run `journal.flush` to force-persist before querying for the absolute latest.
- **Cross-replicate** — pipe the journal into another container as an event source for mirroring / fan-out / append-only sync.

When provisioned with `--journal-path`, no per-write setup is needed — every covered write is recorded (add `--allow-journal` to expose the journal query endpoints — history, revision, diff, stats, flush — over the API). Treat it as "always-on undo for the whole filesystem." Journaling is ON in the standard `hoody_kit: true` container image; raw kit deployments without those flags will accept writes but skip recording.

**Optional add-ons (per-request, opt-in):**
- **Remote backends** — append `?backend=<id>` (or `?type=<rclone-type>`) to operate against any of the 60+ rclone backend types you've connected (Mega, SFTP, S3, GDrive, Dropbox, Backblaze B2, WebDAV, Git, …) instead of the local FS. Requires `--allow-remote` server-side flag. Note: the journal records local-FS mutations; remote-backend ops go to the remote and aren't replayable from the journal.
- **FUSE mounts** — `mounts.create` to surface a remote backend AS a path in the local FS. Same `--allow-remote` requirement.
- **chmod / chown** — Unix-only, requires `--allow-chmod` / `--allow-chown` server flags.

## When to use

- **Local container FS (the 90% case)** — CRUD, archive entry / extract, cross-binary search (glob, grep), download a URL into a path, resumable upload. Local works out of the box.
- **Recover / inspect a previous version of any file** — `?history=1`, `?revision=<seq>`, `?at=<unix-ms>`, `?diff=1&from_seq=<N>`. Always available because every write is journaled.
- **Audit / replay every change to the filesystem** — `journal.query` for the full event stream (sequence, timestamp, path, op, size, hash).
- **Remote cloud / SSH / S3 / Git** — append `?backend=<id>` on `files.get` / `files.put` / `files.delete` / `files.patchApi` / `files.operate` calls (the methods that accept a `backend` option). The same endpoints transparently target the remote.
- **FUSE-mount a remote into the local FS** — when downstream code needs to read the remote as a regular path (`/mnt/s3/…`).

## When NOT to use

Run binaries -> `terminal`/`exec`, live events -> `watch`, TS/JS gen -> `exec`, indexed queries -> `sqlite`, traffic logs -> `proxyLogs`.

## Prerequisites

- **For plain read/write/stat/list (the default)**: nothing beyond the kit URL. Paths are absolute container paths; the namespace is not workspace-scoped, so use `/workspace/...`, `/home/user/...`, etc.
- **For glob / grep**: kit started with `--allow-search` / `--allow-grep` (403 "not allowed" otherwise; enabled in the standard hoody_kit container image).
- **For remote backends** (`?backend=` / `?type=` / FUSE mounts): the kit must be started with `--allow-remote`.
- **For chmod / chown**: kit started with `--allow-chmod` / `--allow-chown`, and the container is Unix.
- Cross-host file propagation (`/external/...` paths that route to another container) goes via SSHFS — that travels by container-to-container, not via this kit's API surface.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Read, search, write

1. `files.get` `?stat`/`?lines=10-50`.
2. `files.glob` `?pattern=**/*.ts`; `files.grep` `?pattern=TODO&context=2` (local only).
3. `files.put` `?append=true`; `files.delete`.

### 2. Download, extract, FUSE-mount

1. `downloads.fetch` `GET /{dir}?download=<url>`; `downloads.listActive` to poll.
2. `archives.preview` `?preview`; `archives.extract` `?extract=src/**&dest=work-src` (`?dest=` MUST be relative).
3. `backends.connectS3` (60+ `connect*`) -> `files.get('/remote/path', { backend: id })` for one-shot reads OR `mounts.create` -> `files.listDirectory('/hoody/mounts/...')` for a regular FS view -> `mounts.unmount`/`backends.disconnect`. (`files.listDirectory` itself has no `backend` option; `files.get`/`files.put`/`files.delete` do.)

### 3. Journal time-travel + TUS-like upload

1. `files.get` `?history=1`, `?revision=N`/`?at=<unix-ms>`, `?diff=1&from_seq=<N>`.
2. `journal.query` `?path=<p>&after_id=<seq>`; `journal.flush` first.
3. Resumable: `PUT /{path}` first chunk, then `PATCH /{path}` `X-Update-Range: append` per chunk.

## Quirks & gotchas

- Reserved sub-prefixes (stat, chmod, chown, realpath, glob, grep, copy, move, append) dispatch by URL prefix in `api/files.rs`, each scoped to a specific HTTP method (stat/grep/glob/realpath→GET, chmod/chown→PATCH, copy/move→POST, append→PUT) to prevent method bleeding -- so `DELETE /api/v1/files/stat/foo` does NOT trigger the stat dispatcher; it removes the literal path `/stat/foo`. (`/api/v1/files/health` is intercepted by a separate top-level route in `api/mod.rs:106` before this dispatcher sees it.)
- `glob`/`grep`/`realpath`/`?lines=` local-only; `?backend=` -> 400.
- `?backend=` and `?type=` need `--allow-remote`.
- `chmod`/`chown` Unix-only + `--allow-chmod`/`--allow-chown`.
- WebDAV verbs on `/{path}`: PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK, CHECKAUTH, LOGOUT.
- Cross-host propagation is SSHFS-only via `/external/...`.
- `realpath` follows symlinks even where blocked.
- **Resumable upload PATCH must use the WebDAV root route (`PATCH /{path}`), NOT `PATCH /api/v1/files/{path}`** — the api/v1 PATCH expects a JSON body and rejects raw bytes with `400 Invalid JSON body`. The WebDAV form takes `X-Update-Range: append` — or an HTTP-Range offset `bytes=<start>-<end>` (a Content-Range-style `/<size>` suffix is REJECTED with `400 Invalid X-Update-Range Header`) — plus the raw body, and returns `204 No Content` on success. An explicit offset must fall INSIDE the current file, so it can only overwrite, never extend past EOF; use `append` to extend. [live-verified]
- **Archive `?dest=` MUST be relative** (e.g. `?dest=extracted`), not absolute — sending `?dest=/abs/path` returns `400 Absolute destination path not allowed`. The directory is created relative to the archive's containing dir. [live-verified]
- **`?preview` query value matters** — pass `?preview` empty (no `=…`) for a full archive listing; `?preview=true` is parsed as the entry name `true` and returns `404 Entry not found in archive: true`. Same trap on `?contents`. [live-verified]
- **Bare `?extract_file=` is unhandled by the kit's GET dispatcher** — the request falls through to "send the raw archive file" and you get the **whole archive's bytes**, not the selected entry. The generated SDK and CLI work around this by ALSO sending `?extract=` — when both query params are present, the `extract` branch runs (writes to disk under `?dest=`). The OpenAPI-only `?extract_file=` form is effectively dead.
- **Journal default `max_file_size` is 2 MiB; oversized files still produce a journal entry with hash + size, but the body bytes are not stored**.
- **`journal.is_excluded(<relative-path>)` gates which paths are recorded.** Built-in dev-dir excludes (`node_modules`, `target`, `.next`, `.nuxt`, `.svelte-kit`, `.turbo`, `__pycache__`, `.venv`, `venv`, `env`, `__pypackages__`, `.tox`, `.nox`, `bower_components`, …) skip journaling unless you pass `--no-journal-ignore-dev-dirs`. `.git` is always excluded regardless of that flag (separate hardcoded check, not part of the toggleable list). Custom excludes via `--journal-exclude`. This is why "I wrote to `node_modules/x` and saw no journal entry" is expected.
- **Journal does NOT cover everything by default.** Live behaviour observed: a fresh `PUT` (create) and an overwriting `PUT` (write) on `/home/user/...` produce entries; URL downloads (`?download=`) and archive extraction are NOT journaled — those write through paths with no journal hook. `chmod`, `chown`, `touch`, `?append=true` and copy/move ARE recorded. Always call `journal.flush` then `journal.query` (or `?history=1`) to inspect what was actually recorded — don't assume coverage. [live-verified]
- **Built-in dev-dir exclude list always skips journaling** for `node_modules`, `__pycache__`, `.venv`, `target`, `.next`, `.nuxt`, etc. — even on `/home/user/...` paths. Disable these with `--no-journal-ignore-dev-dirs` at kit start. `.git` is hardcoded to ALWAYS be excluded and stays excluded even with `--no-journal-ignore-dev-dirs`.
- **`/tmp/...` paths appear NOT to be journaled** at all (live-verified — 0 entries for `/tmp/files-examples-*` writes). Use `/home/user/...` (or another non-`/tmp` path) when you need history.
- **`HEAD /api/v1/files/{path}` returns `405`**; the `getMetadata` operation in mappings is the WebDAV-style `HEAD /{path}` route (no `/api/v1/files/` prefix). Use `files.stat` (`GET /api/v1/files/stat/{path}`) for a JSON envelope instead. [live-verified]
- **`chown` to root is rejected** with `400 Cannot change ownership to root (UID 0)` (owner) or `400 Cannot change group to root (GID 0)` (group) — even if the kit has `--allow-chown`. Use a non-root user (`nobody`, `user`, …). [live-verified]
- **FUSE mount paths are confined to a configured mount-dir** (`/hoody/mounts` by default); arbitrary paths return `400 Mount path must be under the configured mount directory`. Override at kit start with `--mount-dir <path>`.
- **Listing-style query params (`?downloads`, `?download_history`, `?extractions`, `?extraction_history`) are honoured on the WebDAV root route, NOT on `/api/v1/files/...`** — calling `GET /api/v1/files/<dir>?downloads` returns a regular directory listing (the query is ignored). Use `GET /<dir>?downloads` (or `GET /?download_history` for the global feed). [live-verified]
- **`hoody-files` test container goes to sleep** between requests on a quiet kit; first hit may return `502` from the proxy with a `<!DOCTYPE html>…Error 502` body. Retry 2-3× with a few seconds backoff and the kit wakes up. [live-verified]
- **Mount the whole FS as a local drive on the USER's machine (client-side WebDAV).** Because the kit serves a WebDAV API at its URL root, the container's files mount natively as a drive/folder with no install: on **Windows** *Map network drive* to `https://{P}-{C}-files-1.{N}.containers.hoody.com/`, on **macOS** Finder → *Connect to Server* to the same URL; cross-platform/scripted, via `hoody mount <containerId> <localDir>` (WebDAV-backed remote mount; `--read-only`/`--background`/`--auth-token`/`--auth-password`/`--auth-ip` flags). This is the inverse of the server-side FUSE `mounts.*` family (which mounts remote backends INTO the container).

## Common errors

- 400 not supported with remote backends -- drop `?backend=`.
- 400 Cannot combine realpath with other ops.
- 400 Cannot preview a directory as archive.
- 400 Unknown operation -- POST needs one query op.
- 400 Missing query or body -- PATCH needs op or body.

## Related namespaces

- `terminal` -- run binaries.
- `exec` -- TS/JS gen.
- `watch` -- live events.
- `sqlite` -- indexed queries.
- `browser`/`curl` -- scripted HTTP.

## Examples

Every step in every example was live-tested against a real `files-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first. Paths under `/tmp/...` are NOT journaled by default — examples that need history use `/home/user/...`.

### 1. Read a file — full bytes, stat envelope, and a slice of lines

**Goal:** inspect an unknown text file three ways: get its metadata, peek the raw bytes, and slice an arbitrary line range without pulling the whole thing.

**Step 1 — stat first.** `files.stat` returns size, owner/group, octal permissions, mtime, plus a `revisions` count (how many journal entries exist for this path). Use this BEFORE downloading a file you don't know the size of.

```typescript
const s = await client.files.stat('/etc/hostname');
console.log(s.data!.size, s.data!.permissions);
```
**Step 2 — line slice.** `?lines=2-3` returns just lines 2 through 3 inclusive. Local-FS only (rejected with `400` when combined with `?backend=`).

```typescript
const slice = await client.files.get('/var/log/syslog', { lines: '2-3' });
```
**Step 3 — full body.** No query params, raw bytes (or `Content-Type: application/octet-stream` for binaries).

```typescript
const body = await client.files.get('/etc/hostname');
```
### 2. Find files then grep them — TODO scan across a tree

**Goal:** find every `.md` file under a directory, then grep them for `TODO` with surrounding context. Both ops are local-FS only.

**Step 1 — glob.** `pattern` matches relative to `path`; obeys `.gitignore` unless `no_ignore=true`.

```typescript
const r = await client.files.glob('/home/user', { pattern: '**/*.md' });
const paths = r.data!.entries.map(e => e.name);
```
**Step 2 — grep with context.** `?glob=` filters which files grep looks at; `?context=2` mirrors `grep -C 2`.

```typescript
const m = await client.files.grep('/home/user', { pattern: 'TODO', ignore_case: true, glob: '**/*.md', context: 2 });
console.log(m.data!.total_matches, m.data!.matches.length);
```
### 3. Resumable upload — split a payload into PUT-then-PATCH-append chunks

**Goal:** push a 16 MiB payload as 8 MiB chunks. Many CDN/proxy combos cap a single body at 10 MiB; chunked upload sidesteps that.

**Step 1 — first chunk via PUT.** Creates the file with the first slab.

```typescript
import { readFileSync } from 'fs';
await client.files.put('/home/user/upload-test.bin', readFileSync('/tmp/chunk1.bin'));
```
**Step 2 — append remaining chunks.** Send `PATCH /<path>` (the WebDAV root route, NOT `/api/v1/files/...` — that one expects JSON and 400s on raw bytes). Header `X-Update-Range: append` says "concatenate". Returns `204 No Content`.

```typescript
await client.files.patch('/home/user/upload-test.bin', readFileSync('/tmp/chunk2.bin') as any, { XUpdateRange: 'append' });
const s = await client.files.stat('/home/user/upload-test.bin');
```
**Step 3 — same shape for resume after a network drop.** Re-append the missing chunk with `X-Update-Range: append` if you tracked the cursor client-side. An explicit `bytes=<start>-<end>` offset (no `/<total>` suffix — that is rejected) only overwrites INSIDE the existing file and cannot extend it, so it is not a resume mechanism. The generated **SDK and CLI currently expose only `X-Update-Range: append`**; explicit byte ranges need raw HTTP.

### 4. Time-travel a single file — history → revision N → diff

**Goal:** roll back a config file by reading an earlier revision, comparing it to current, then writing the chosen revision back. Journal tracks every PUT under `/home/user/...` (NOT `/tmp/...` — see Quirks).

**Step 1 — write two revisions.**

```typescript
const path = '/home/user/config.toml';
await client.files.put(path, Buffer.from('verbose = false'));
await new Promise(r => setTimeout(r, 1000));
await client.files.put(path, Buffer.from('verbose = true'));
await client.files.journal.flush();
```
**Step 2 — list history.** `?history=1` returns the per-revision log: each entry has `seq`, `op` (`"create"` / `"write"` / `"delete"` / `"moved_from"`/`"moved_to"` / etc. — see Example 5 for the full enum), `ts`, hashes, and size deltas.

```typescript
// Empty-string sentinel for valueless query flags — generated SDK rejects '1'.
const h = await client.files.get(path, { history: '' });
```
**Step 3 — fetch revision N.** `?revision=1` returns the bytes of that point-in-time. (Use `?at=<unix-ms>` for an instant.)

```typescript
const old = await client.files.get(path, { revision: 1 });
const diff = await client.files.get(path, { diff: '', from_seq: 1 });
```
**Step 4 — restore by writing rev1 bytes back.** PUT the body returned by `?revision=1`.

### 5. Audit — stream every mutation since seq N via the journal

**Goal:** wire a SIEM / alerting pipeline. Get every FS mutation since the last cursor, including hashes for tamper detection. Don't forget `journal.flush` first.

**Step 1 — flush so buffered journal writes are on disk.**

```typescript
await client.files.journal.flush();
```
**Step 2 — query since cursor.** `after_id` is the last `id` you've seen. `op` enum is `"create"` / `"write"` / `"append"` / `"delete"` / `"touch"` / `"moved_from"` / `"moved_to"` / `"copied_from"` / `"copied_to"` / `"dir_moved_from"` / `"dir_moved_to"` / `"dir_copied_from"` / `"dir_copied_to"` / `"dir_deleted"` / `"mkdir"` / `"chmod"` / `"chown"` / `"gap"` (no bare `"move"`/`"copy"` — use the directional `"moved_from"`/`"moved_to"` etc. pair).

```typescript
const r = await client.files.journal.query({ limit: 200, after_id: 0 });
const next = r.data!.next_after_id;
```
**Step 3 — health check.** Watch `journal.getStats` for `writer_healthy:false`, `parse_failures`, or `skipped_overflow > 0` — any of those means the audit trail is degraded.

```typescript
const s = await client.files.journal.getStats();
```
### 6. Download a URL straight into the FS (no curl, no wget needed)

**Goal:** pull an asset from the public internet into a container path. The kit handles the actual HTTP fetch — useful for sandboxed containers without outbound HTTP libs.

**Step 1 — fire-and-poll.** `GET /<directory>?download=<url>&filename=<name>` (URL-encode the URL). Returns a `download_id` for tracking.

```typescript
const r = await client.files.downloads.fetch('/home/user/inbox', {
  download: 'https://httpbin.org/robots.txt',
  filename: 'robots.txt',
  timeout: 15,
});
```
**Step 2 — list active.** `?downloads` ONLY works on the WebDAV root route — `GET /api/v1/files/<dir>?downloads` ignores the flag and returns a normal listing (live-verified).

```typescript
await client.files.downloads.listActive('/home/user/inbox', { downloads: '' });
await client.files.downloads.getHistory({ download_history: '' });
```
### 7. Archive workflow — preview, then selective extract

**Goal:** extract just one subpath of a zip without unpacking the whole archive.

**Step 1 — preview** to see what's inside. `?preview` MUST be empty — `?preview=true` is parsed as the entry name `true` and 404s (live-verified).

```typescript
const p = await client.files.archives.preview('/home/user/inbox/hello.zip');
```
**Step 2 — extract a subset.** `?extract=<glob>` selects entries (empty = all). `?dest=` MUST be relative — absolute paths return `400 Absolute destination path not allowed`. Destination is created relative to the archive's parent dir.

```typescript
await client.files.archives.extract('/home/user/inbox/hello.zip', { extract: 'Hello-World-master/', dest: 'extracted' });
```
**Step 3 — extract a single file to disk.** Pass BOTH `?extract=<glob>` and `?extract_file=<path>` (the bare `?extract_file=` form falls through to "send whole archive"). The kit writes the matched entry under `?dest=` (or alongside the archive if omitted).

```typescript
// extract-file writes the chosen entry under `dest` and returns an extraction
// response; to get the bytes back, follow up with files.get(`${dest}/<entry>`).
const extracted = await client.files.archives.extractFile('/home/user/inbox/hello.zip', { extract: 'Hello-World-master/README', dest: 'extracted' });
```
### 8. Cross-directory copy + move + chmod (a one-shot deploy)

**Goal:** stage a config in a working dir, copy to the target, set permissions, move the staging file to a backup folder. Uses POST against the copy/move reserved-prefix routes (`/api/v1/files/copy/...`, `/move/...`) and PATCH for chmod/chown (`/api/v1/files/chmod/...`, `/chown/...`).

**Step 1 — copy.** Both `copy_to` and (for move) `move_to` are **query params**, NOT body fields. Add `?overwrite=true` to allow replacing an existing destination.

```typescript
await client.files.copy('/home/user/staging/app.toml', { copy_to: '/etc/myapp/app.toml', overwrite: 'true' });
```
**Step 2 — chmod.** Octal as a query param (`?chmod=600`). Requires `--allow-chmod` at kit start; Unix-only.

```typescript
await client.files.chmod('/etc/myapp/app.toml', { chmod: '600' });
```
**Step 3 — move staging → archive.** Same pattern as copy but no overwrite by default; `?move_to=` is required.

```typescript
await client.files.move('/home/user/staging/app.toml', { move_to: '/home/user/archive/app.toml' });
```
### 9. Connect a remote backend, mount it as a path, list through both surfaces

**Goal:** attach a remote backend (here: `local` for portability — swap in `s3`/`sftp`/`drive` etc.) and surface it as a regular FS path via FUSE. Requires `--allow-remote` at kit start.

**Step 1 — connect.** Each backend has its own `POST /api/v1/backends/<type>` with type-specific config. Returns `{id, type, vfs_backend_type}`.

```typescript
const conn = await client.files.backends.connectLocal({ description: 'local-passthrough' });
const bid = conn.data!.id;
await client.files.backends.testConnection(bid);
```
**Step 2 — mount.** `mount_path` MUST sit under the kit's configured mount-dir (default `/hoody/mounts/...`) — arbitrary paths return `400 Mount path must be under the configured mount directory`.

```typescript
const m = await client.files.mounts.create({
  backend_id: bid,
  label: 'local-mount',
  mount_path: '/hoody/mounts/local-test',
});
const mid = m.data!.id;
```
**Step 3 — read through the mount as a regular path.**

```typescript
await client.files.listDirectory('/hoody/mounts/local-test');
```
**Step 4 — tear down** (in order: unmount, then disconnect).

```typescript
await client.files.mounts.unmount(mid);
await client.files.backends.disconnect(bid);
```
### 10. Bulk delete a tree atomically — and verify nothing leaked

**Goal:** wipe a working directory and every file under it, then confirm via journal + listing that nothing remains.

**Step 1 — DELETE on the directory.** `DELETE /api/v1/files/<dir>` removes recursively. (Reserved-prefix trap: a path like `/api/v1/files/stat/foo` is interpreted as removing `/stat/foo`, NOT a stat call. Always use absolute container paths.)

```typescript
await client.files.delete('/home/user/files-examples-cleanup');
```
**Step 2 — verify.** A `404` from `files.stat` is what you want.

```typescript
try { await client.files.stat('/home/user/files-examples-cleanup'); }
catch (e: any) { if (e.status === 404) console.log('gone'); else throw e; }
```
**Step 3 — confirm via journal.** A successful tree-delete emits `op: "delete"` (or `op: "dir_deleted"` per child). Flush first.

```typescript
await client.files.journal.flush();
const r = await client.files.journal.query({ path: '/home/user/files-examples-cleanup', limit: 10 });
```

## Reference

**Accessor:** `client.files`  |  **Import:** `import * as files from 'hoody-sdk/files'`

### `client.files.archives` (8) — Archive operations - extract, preview, download directories as ZIP

#### `downloadAsZip` — Download directory as ZIP

```typescript
client.files.archives.downloadAsZip(directory: string, zip: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `directory` | `string` | path | Yes |  |
| `zip` | `string` | query | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /{directory}?zip`
**CLI:** `hoody files downloads zip`

---

#### `extract` — Extract archive

```typescript
client.files.archives.extract(archive: string, extract: string, dest?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `archive` | `string` | path | Yes |  |
| `extract` | `string` | query | Yes | Empty for full extraction; path for selective (e.g. "src/" or "lib/") |
| `dest` | `string` | query | No | Destination directory name (default: archive name) |

**Returns:** `files_ExtractionResult`  |  **HTTP:** `GET /{archive}?extract`
**CLI:** `hoody files extractions create`

---

#### `extractFile` — Extract file from archive

```typescript
client.files.archives.extractFile(archive: string, extract: string, dest?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `archive` | `string` | path | Yes | Path to archive file |
| `extract` | `string` | query | Yes | Path of the file or directory inside the archive to extract (e.g. "src/" or "lib/") |
| `dest` | `string` | query | No | Destination directory name (default: archive name) |

**Returns:** `files_ExtractionResult`  |  **HTTP:** `GET /{archive}?extract_file`
**CLI:** `hoody files extractions extract-file`

---

#### `getHistory` — Extraction history

```typescript
client.files.archives.getHistory(extraction_history: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `extraction_history` | `string` | query | Yes |  |

**Returns:** `files_ExtractionHistory`  |  **HTTP:** `GET /?extraction_history`
**CLI:** `hoody files extractions history`

---

#### `listActive` — List active extractions

```typescript
client.files.archives.listActive(extractions: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `extractions` | `string` | query | Yes |  |

**Returns:** `files_ActiveExtractions`  |  **HTTP:** `GET /?extractions`
**CLI:** `hoody files extractions active`

---

#### `listGlobal` — List active extractions

```typescript
client.files.archives.listGlobal()
```

**Returns:** `files_ActiveExtractions`  |  **HTTP:** `GET /api/v1/extractions`
**CLI:** `hoody files extractions all`

---

#### `preview` — Preview archive contents or read file

```typescript
client.files.archives.preview(archive: string, preview?: string, contents?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `archive` | `string` | path | Yes | Path to archive file |
| `preview` | `string` | query | No | Empty value lists archive contents; non-empty value reads a specific file from the archive (alias: ?contents) |
| `contents` | `string` | query | No | Alias for ?preview |

**Returns:** `files_ArchiveContents`  |  **HTTP:** `GET /{archive}?preview`
**CLI:** `hoody files archive preview`

---

#### `viewFile` — View file from archive

```typescript
client.files.archives.viewFile(archive: string, preview: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `archive` | `string` | path | Yes | Path to archive file |
| `preview` | `string` | query | Yes | Path of the file inside the archive to view (e.g. "src/" or "README.md") |

**Returns:** `any`  |  **HTTP:** `GET /{archive}?view_file`
**CLI:** `hoody files archive view`

---

### `client.files.authentication` (2) — Authentication operations - check auth status and logout

#### `checkAuth` — Check authentication status

```typescript
client.files.authentication.checkAuth(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `CHECKAUTH /{path}`

---

#### `logout` — Clear authentication

```typescript
client.files.authentication.logout(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |

**Returns:** `void`  |  **HTTP:** `LOGOUT /{path}`

---

### `client.files.backends` (67) — Backend management - connect, list, test, and disconnect remote cloud storage backends

#### `connectAlias` — Connect to alias backend

```typescript
client.files.backends.connectAlias(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="", remote*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/alias`
**CLI:** `hoody files backends connect alias`

---

#### `connectAzureblob` — Connect to azureblob backend

```typescript
client.files.backends.connectAzureblob(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_tier: string="", account: string="", archive_tier_delete: bool=false, chunk_size: string="4194304", client_certificate_password: string="", client_certificate_path: string="", client_id: string="", client_secret: string="", client_send_certificate_chain: bool=false, delete_snapshots: "" | "include" | "only"="", description: string="", directory_markers: bool=false, disable_checksum: bool=false, disable_instance_discovery: bool=false, encoding: string="21078018", endpoint: string="", env_auth: bool=false, key: string="", list_chunk: int=5000, memory_pool_flush_time: int=60, memory_pool_use_mmap: bool=false, msi_client_id: string="", msi_mi_res_id: string="", msi_object_id: string="", no_check_container: bool=false, no_head_object: bool=false, password: string="", public_access: "" | "blob" | "container"="", sas_url: string="", service_principal_file: string="", tenant: string="", upload_concurrency: int=16, upload_cutoff: string="", use_az: bool=false, use_emulator: bool=false, use_msi: bool=false, username: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/azureblob`
**CLI:** `hoody files backends connect azureblob`

---

#### `connectAzurefiles` — Connect to azurefiles backend

```typescript
client.files.backends.connectAzurefiles(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ account: string="", chunk_size: string="4194304", client_certificate_password: string="", client_certificate_path: string="", client_id: string="", client_secret: string="", client_send_certificate_chain: bool=false, connection_string: string="", description: string="", encoding: string="54634382", endpoint: string="", env_auth: bool=false, key: string="", max_stream_size: string="10737418240", msi_client_id: string="", msi_mi_res_id: string="", msi_object_id: string="", password: string="", sas_url: string="", service_principal_file: string="", share_name: string="", tenant: string="", upload_concurrency: int=16, use_msi: bool=false, username: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/azurefiles`
**CLI:** `hoody files backends connect azurefiles`

---

#### `connectB2` — Connect to b2 backend

```typescript
client.files.backends.connectB2(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ account*: string="", chunk_size: string="100663296", copy_cutoff: string="4294967296", description: string="", disable_checksum: bool=false, download_auth_duration: int=604800, download_url: string="", encoding: string="50438146", endpoint: string="", hard_delete: bool=false, key*: string="", lifecycle: int=0, memory_pool_flush_time: int=60, memory_pool_use_mmap: bool=false, test_mode: string="", upload_concurrency: int=4, upload_cutoff: string="209715200", version_at: string="0001-01-01T00:00:00Z", versions: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/b2`
**CLI:** `hoody files backends connect b2`

---

#### `connectBox` — Connect to box backend

```typescript
client.files.backends.connectBox(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_token: string="", auth_url: string="", box_config_file: string="", box_sub_type: "user" | "enterprise"="user", client_credentials: bool=false, client_id: string="", client_secret: string="", commit_retries: int=100, description: string="", encoding: string="52535298", impersonate: string="", list_chunk: int=1000, owned_by: string="", root_folder_id: string="0", token: string="", token_url: string="", upload_cutoff: string="52428800" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/box`
**CLI:** `hoody files backends connect box`

---

#### `connectCache` — Connect to cache backend

```typescript
client.files.backends.connectCache(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ chunk_clean_interval: int=60, chunk_no_memory: bool=false, chunk_path: string="/home/user/.cache/hoody-vfs/cache-backend", chunk_size: "1M" | "5M" | "10M"="5242880", chunk_total_size: "500M" | "1G" | "10G"="10737418240", db_path: string="/home/user/.cache/hoody-vfs/cache-backend", db_purge: bool=false, db_wait_time: int=1, description: string="", info_age: "1h" | "24h" | "48h"=21600, plex_insecure: string="", plex_password: string="", plex_token: string="", plex_url: string="", plex_username: string="", read_retries: int=10, remote*: string="", rps: int=-1, tmp_upload_path: string="", tmp_wait_time: int=15, workers: int=4, writes: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/cache`
**CLI:** `hoody files backends connect cache`

---

#### `connectChunker` — Connect to chunker backend

```typescript
client.files.backends.connectChunker(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ chunk_size: string="2147483648", description: string="", fail_hard: "true" | "false"=false, hash_type: "none" | "md5" | "sha1" | "md5all" | "sha1all" | "md5quick" | "sha1quick"="md5", meta_format: "none" | "simplejson"="simplejson", name_format: string="*.hoody-vfs_chunk.###", remote*: string="", start_from: int=1, transactions: "rename" | "norename" | "auto"="rename" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/chunker`
**CLI:** `hoody files backends connect chunker`

---

#### `connectCloudinary` — Connect to cloudinary backend

```typescript
client.files.backends.connectCloudinary(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ api_key*: string="", api_secret*: string="", cloud_name*: string="", description: string="", encoding: string="52543246", eventually_consistent_delay: int=0, upload_prefix: string="", upload_preset: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/cloudinary`
**CLI:** `hoody files backends connect cloudinary`

---

#### `connectCombine` — Connect to combine backend

```typescript
client.files.backends.connectCombine(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="", upstreams*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/combine`
**CLI:** `hoody files backends connect combine`

---

#### `connectCompress` — Connect to compress backend

```typescript
client.files.backends.connectCompress(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="", level: int=-1, mode: "gzip"="gzip", ram_cache_limit: string="20971520", remote*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/compress`
**CLI:** `hoody files backends connect compress`

---

#### `connectCrypt` — Connect to crypt backend

```typescript
client.files.backends.connectCrypt(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="", directory_name_encryption: "true" | "false"=true, filename_encoding: "base32" | "base64" | "base32768"="base32", filename_encryption: "standard" | "obfuscate" | "off"="standard", no_data_encryption: "true" | "false"=false, pass_bad_blocks: bool=false, password*: string="", password2: string="", remote*: string="", server_side_across_configs: bool=false, show_mapping: bool=false, strict_names: bool=false, suffix: string=".bin" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/crypt`
**CLI:** `hoody files backends connect crypt`

---

#### `connectDrive` — Connect to drive backend

```typescript
client.files.backends.connectDrive(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ acknowledge_abuse: bool=false, allow_import_name_change: bool=false, alternate_export: bool=false, auth_owner_only: bool=false, auth_url: string="", chunk_size: string="8388608", client_credentials: bool=false, client_id: string="", client_secret: string="", copy_shortcut_content: bool=false, description: string="", disable_http2: bool=true, encoding: string="16777216", env_auth: "false" | "true"=false, export_formats: string="docx,xlsx,pptx,svg", fast_list_bug_fix: bool=true, formats: string="", impersonate: string="", import_formats: string="", keep_revision_forever: bool=false, list_chunk: int=1000, metadata_labels: "off" | "read" | "write" | "failok" | "read,write"="0", metadata_owner: "off" | "read" | "write" | "failok" | "read,write"="1", metadata_permissions: "off" | "read" | "write" | "failok" | "read,write"="0", pacer_burst: int=100, pacer_min_sleep: int=0, resource_key: string="", root_folder_id: string="", scope: "drive" | "drive.readonly" | "drive.file" | "drive.appfolder" | "drive.metadata.readonly"="", server_side_across_configs: bool=false, service_account_credentials: string="", service_account_file: string="", shared_with_me: bool=false, show_all_gdocs: bool=false, size_as_quota: bool=false, skip_checksum_gphotos: bool=false, skip_dangling_shortcuts: bool=false, skip_gdocs: bool=false, skip_shortcuts: bool=false, starred_only: bool=false, stop_on_download_limit: bool=false, stop_on_upload_limit: bool=false, team_drive: string="", token: string="", token_url: string="", trashed_only: bool=false, upload_cutoff: string="8388608", use_created_date: bool=false, use_shared_date: bool=false, use_trash: bool=true, v2_download_min_size: string="-1" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/drive`
**CLI:** `hoody files backends connect drive`

---

#### `connectDropbox` — Connect to dropbox backend

```typescript
client.files.backends.connectDropbox(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", batch_commit_timeout: int=600, batch_mode: string="sync", batch_size: int=0, batch_timeout: int=0, chunk_size: string="50331648", client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="52469762", impersonate: string="", pacer_min_sleep: int=0, root_namespace: string="", shared_files: bool=false, shared_folders: bool=false, token: string="", token_url: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/dropbox`
**CLI:** `hoody files backends connect dropbox`

---

#### `connectFichier` — Connect to fichier backend

```typescript
client.files.backends.connectFichier(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ api_key: string="", cdn: bool=false, description: string="", encoding: string="52666494", file_password: string="", folder_password: string="", shared_folder: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/fichier`
**CLI:** `hoody files backends connect fichier`

---

#### `connectFilefabric` — Connect to filefabric backend

```typescript
client.files.backends.connectFilefabric(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="", encoding: string="50429954", permanent_token: string="", root_folder_id: string="", token: string="", token_expiry: string="", url*: "https://storagemadeeasy.com" | "https://eu.storagemadeeasy.com" | "https://yourfabric.smestorage.com"="", version: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/filefabric`
**CLI:** `hoody files backends connect filefabric`

---

#### `connectFilescom` — Connect to filescom backend

```typescript
client.files.backends.connectFilescom(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ api_key: string="", description: string="", encoding: string="60923906", password: string="", site: string="", username: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/filescom`
**CLI:** `hoody files backends connect filescom`

---

#### `connectFtp` — Connect to ftp backend

```typescript
client.files.backends.connectFtp(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ ask_password: bool=false, close_timeout: int=60, concurrency: int=0, description: string="", disable_epsv: bool=false, disable_mlsd: bool=false, disable_tls13: bool=false, disable_utf8: bool=false, encoding: "Asterisk,Ctl,Dot,Slash" | "BackSlash,Ctl,Del,Dot,RightSpace,Slash,SquareBracket" | "Ctl,LeftPeriod,Slash"="35749890", explicit_tls: bool=false, force_list_hidden: bool=false, host*: string="", idle_timeout: int=60, no_check_certificate: bool=false, no_check_upload: bool=false, pass: string="", port: int=21, shut_timeout: int=60, socks_proxy: string="", tls: bool=false, tls_cache_size: int=32, user: string="user", writing_mdtm: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/ftp`
**CLI:** `hoody files backends connect ftp`

---

#### `connectGofile` — Connect to gofile backend

```typescript
client.files.backends.connectGofile(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_token: string="", account_id: string="", description: string="", encoding: string="323331982", list_chunk: int=1000, root_folder_id: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/gofile`
**CLI:** `hoody files backends connect gofile`

---

#### `connectGoogleCloudStorage` — Connect to google cloud storage backend

```typescript
client.files.backends.connectGoogleCloudStorage(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_token: string="", anonymous: bool=false, auth_url: string="", bucket_acl: "authenticatedRead" | "private" | "projectPrivate" | "publicRead" | "publicReadWrite"="", bucket_policy_only: bool=false, client_credentials: bool=false, client_id: string="", client_secret: string="", decompress: bool=false, description: string="", directory_markers: bool=false, encoding: string="50348034", endpoint: string="", env_auth: "false" | "true"=false, location: "" | "asia" | "eu" | "us" | "asia-east1" | "asia-east2" | "asia-northeast1" | "asia-northeast2" | …(36 values)="", no_check_bucket: bool=false, object_acl: "authenticatedRead" | "bucketOwnerFullControl" | "bucketOwnerRead" | "private" | "projectPrivate" | "publicRead"="", project_number: string="", service_account_credentials: string="", service_account_file: string="", storage_class: "" | "MULTI_REGIONAL" | "REGIONAL" | "NEARLINE" | "COLDLINE" | "ARCHIVE" | "DURABLE_REDUCED_AVAILABILITY"="", token: string="", token_url: string="", user_project: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/google-cloud-storage`
**CLI:** `hoody files backends connect google-cloud-storage`

---

#### `connectGooglePhotos` — Connect to google photos backend

```typescript
client.files.backends.connectGooglePhotos(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", batch_commit_timeout: int=600, batch_mode: string="sync", batch_size: int=0, batch_timeout: int=0, client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="50348034", include_archived: bool=false, proxy: string="", read_only: bool=false, read_size: bool=false, start_year: int=2000, token: string="", token_url: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/google-photos`
**CLI:** `hoody files backends connect google-photos`

---

#### `connectHasher` — Connect to hasher backend

```typescript
client.files.backends.connectHasher(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auto_size: string="0", description: string="", hashes: string, max_age: int=0, remote*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/hasher`
**CLI:** `hoody files backends connect hasher`

---

#### `connectHdfs` — Connect to hdfs backend

```typescript
client.files.backends.connectHdfs(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ data_transfer_protection: "privacy"="", description: string="", encoding: string="50430082", namenode*: string, service_principal_name: string="", username: "root"="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/hdfs`
**CLI:** `hoody files backends connect hdfs`

---

#### `connectHidrive` — Connect to hidrive backend

```typescript
client.files.backends.connectHidrive(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", chunk_size: string="50331648", client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", disable_fetching_member_count: bool=false, encoding: string="33554434", endpoint: string="https://api.hidrive.strato.com/2.1", root_prefix: "/" | "root" | ""="/", scope_access: "rw" | "ro"="rw", scope_role: "user" | "admin" | "owner"="user", token: string="", token_url: string="", upload_concurrency: int=4, upload_cutoff: string="100663296" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/hidrive`
**CLI:** `hoody files backends connect hidrive`

---

#### `connectHttp` — Connect to http backend

```typescript
client.files.backends.connectHttp(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="", headers: string, no_escape: bool=false, no_head: bool=false, no_slash: bool=false, url*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/http`
**CLI:** `hoody files backends connect http`

---

#### `connectIclouddrive` — Connect to iclouddrive backend

```typescript
client.files.backends.connectIclouddrive(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ apple_id*: string="", client_id: string="d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", cookies: string="", description: string="", encoding: string="50438146", password*: string="", trust_token: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/iclouddrive`
**CLI:** `hoody files backends connect iclouddrive`

---

#### `connectImagekit` — Connect to imagekit backend

```typescript
client.files.backends.connectImagekit(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="", encoding: string="117553486", endpoint*: string="", only_signed: bool=false, private_key*: string="", public_key*: string="", upload_tags: string="", versions: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/imagekit`
**CLI:** `hoody files backends connect imagekit`

---

#### `connectInternetarchive` — Connect to internetarchive backend

```typescript
client.files.backends.connectInternetarchive(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_key_id: string="", description: string="", disable_checksum: bool=true, encoding: string="50446342", endpoint: string="https://s3.us.archive.org", front_endpoint: string="https://archive.org", secret_access_key: string="", wait_archive: int=0 }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/internetarchive`
**CLI:** `hoody files backends connect internetarchive`

---

#### `connectJottacloud` — Connect to jottacloud backend

```typescript
client.files.backends.connectJottacloud(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="50431886", hard_delete: bool=false, md5_memory_limit: string="10485760", no_versions: bool=false, token: string="", token_url: string="", trashed_only: bool=false, upload_resume_limit: string="10485760" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/jottacloud`
**CLI:** `hoody files backends connect jottacloud`

---

#### `connectKoofr` — Connect to koofr backend

```typescript
client.files.backends.connectKoofr(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="", encoding: string="50438146", endpoint*: string="", mountid: string="", password*: string="", provider: "koofr" | "digistorage" | "other"="", setmtime: bool=true, user*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/koofr`
**CLI:** `hoody files backends connect koofr`

---

#### `connectLinkbox` — Connect to linkbox backend

```typescript
client.files.backends.connectLinkbox(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="", token*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/linkbox`
**CLI:** `hoody files backends connect linkbox`

---

#### `connectLocal` — Connect to local backend

```typescript
client.files.backends.connectLocal(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ case_insensitive: bool=false, case_sensitive: bool=false, copy_links: bool=false, description: string="", encoding: string="33554434", links: bool=false, no_check_updated: bool=false, no_clone: bool=false, no_preallocate: bool=false, no_set_modtime: bool=false, no_sparse: bool=false, nounc: "true"=false, one_file_system: bool=false, skip_links: bool=false, time_type: "mtime" | "atime" | "btime" | "ctime"="0", unicode_normalization: bool=false, zero_size_links: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/local`
**CLI:** `hoody files backends connect local`

---

#### `connectMailru` — Connect to mailru backend

```typescript
client.files.backends.connectMailru(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", check_hash: "true" | "false"=true, client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="50440078", pass*: string="", quirks: string="", speedup_enable: "true" | "false"=true, speedup_file_patterns: "" | "*" | "*.mkv,*.avi,*.mp4,*.mp3" | "*.zip,*.gz,*.rar,*.pdf"="*.mkv,*.avi,*.mp4,*.mp3,*.zip,*.gz,*.rar,*.pdf", speedup_max_disk: "0" | "1G" | "3G"="3221225472", speedup_max_memory: "0" | "32M" | "256M"="33554432", token: string="", token_url: string="", user*: string="", user_agent: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/mailru`
**CLI:** `hoody files backends connect mailru`

---

#### `connectMega` — Connect to mega backend

```typescript
client.files.backends.connectMega(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ debug: bool=false, description: string="", encoding: string="50331650", hard_delete: bool=false, pass*: string="", use_https: bool=false, user*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/mega`
**CLI:** `hoody files backends connect mega`

---

#### `connectMemory` — Connect to memory backend

```typescript
client.files.backends.connectMemory(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ description: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/memory`
**CLI:** `hoody files backends connect memory`

---

#### `connectNetstorage` — Connect to netstorage backend

```typescript
client.files.backends.connectNetstorage(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ account*: string="", description: string="", host*: string="", protocol: "http" | "https"="https", secret*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/netstorage`
**CLI:** `hoody files backends connect netstorage`

---

#### `connectOnedrive` — Connect to onedrive backend

```typescript
client.files.backends.connectOnedrive(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_scopes: "Files.Read Files.ReadWrite Files.Read.All Files.ReadWrite.All Sites.Read.All offline_access" | "Files.Read Files.Read.All Sites.Read.All offline_access" | "Files.Read Files.ReadWrite Files.Read.All Files.ReadWrite.All offline_access", auth_url: string="", av_override: bool=false, chunk_size: string="10485760", client_credentials: bool=false, client_id: string="", client_secret: string="", delta: bool=false, description: string="", disable_site_permission: bool=false, drive_id: string="", drive_type: string="", encoding: string="57386894", expose_onenote_files: bool=false, hard_delete: bool=false, hash_type: "auto" | "quickxor" | "sha1" | "sha256" | "crc32" | "none"="auto", link_password: string="", link_scope: "anonymous" | "organization"="anonymous", link_type: "view" | "edit" | "embed"="view", list_chunk: int=1000, metadata_permissions: "off" | "read" | "write" | "read,write" | "failok"="0", no_versions: bool=false, region: "global" | "us" | "de" | "cn"="global", root_folder_id: string="", server_side_across_configs: bool=false, tenant: string="", token: string="", token_url: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/onedrive`
**CLI:** `hoody files backends connect onedrive`

---

#### `connectOpendrive` — Connect to opendrive backend

```typescript
client.files.backends.connectOpendrive(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ chunk_size: string="10485760", description: string="", encoding: string="62007182", password*: string="", username*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/opendrive`
**CLI:** `hoody files backends connect opendrive`

---

#### `connectOracleobjectstorage` — Connect to oracleobjectstorage backend

```typescript
client.files.backends.connectOracleobjectstorage(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ attempt_resume_upload: bool=false, chunk_size: string="5242880", compartment: string="", config_file: "~/.oci/config"="~/.oci/config", config_profile: "Default"="Default", copy_cutoff: string="4999610368", copy_timeout: int=60, description: string="", disable_checksum: bool=false, encoding: string="50331650", endpoint: string="", leave_parts_on_error: bool=false, max_upload_parts: int=10000, namespace*: string="", no_check_bucket: bool=false, provider*: "env_auth" | "user_principal_auth" | "instance_principal_auth" | "workload_identity_auth" | "resource_principal_auth" | "no_auth"="env_auth", region*: string="", sse_customer_algorithm: "" | "AES256"="", sse_customer_key: ""="", sse_customer_key_file: ""="", sse_customer_key_sha256: ""="", sse_kms_key_id: ""="", storage_tier: "Standard" | "InfrequentAccess" | "Archive"="Standard", upload_concurrency: int=10, upload_cutoff: string="209715200" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/oracleobjectstorage`
**CLI:** `hoody files backends connect oracleobjectstorage`

---

#### `connectPcloud` — Connect to pcloud backend

```typescript
client.files.backends.connectPcloud(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="50438146", hostname: "api.pcloud.com" | "eapi.pcloud.com"="api.pcloud.com", password: string="", root_folder_id: string="d0", token: string="", token_url: string="", username: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/pcloud`
**CLI:** `hoody files backends connect pcloud`

---

#### `connectPikpak` — Connect to pikpak backend

```typescript
client.files.backends.connectPikpak(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ chunk_size: string="5242880", description: string="", device_id: string="", encoding: string="56829838", hash_memory_limit: string="10485760", no_media_link: bool=false, pass*: string="", root_folder_id: string="", trashed_only: bool=false, upload_concurrency: int=5, use_trash: bool=true, user*: string="", user_agent: string="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/pikpak`
**CLI:** `hoody files backends connect pikpak`

---

#### `connectPixeldrain` — Connect to pixeldrain backend

```typescript
client.files.backends.connectPixeldrain(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ api_key: string="", api_url*: string="https://pixeldrain.com/api", description: string="", root_folder_id: string="me" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/pixeldrain`
**CLI:** `hoody files backends connect pixeldrain`

---

#### `connectPremiumizeme` — Connect to premiumizeme backend

```typescript
client.files.backends.connectPremiumizeme(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ api_key: string="", auth_url: string="", client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="50438154", token: string="", token_url: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/premiumizeme`
**CLI:** `hoody files backends connect premiumizeme`

---

#### `connectProtondrive` — Connect to protondrive backend

```typescript
client.files.backends.connectProtondrive(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ 2fa: string="", app_version: string="macos-drive@1.0.0-alpha.1+hoody-vfs", client_access_token: string="", client_refresh_token: string="", client_salted_key_pass: string="", client_uid: string="", description: string="", enable_caching: bool=true, encoding: string="52559874", mailbox_password: string="", original_file_size: bool=true, password*: string="", replace_existing_draft: bool=false, username*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/protondrive`
**CLI:** `hoody files backends connect protondrive`

---

#### `connectPutio` — Connect to putio backend

```typescript
client.files.backends.connectPutio(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="50438146", token: string="", token_url: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/putio`
**CLI:** `hoody files backends connect putio`

---

#### `connectQingstor` — Connect to qingstor backend

```typescript
client.files.backends.connectQingstor(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_key_id: string="", chunk_size: string="4194304", connection_retries: int=3, description: string="", encoding: string="16842754", endpoint: string="", env_auth: "false" | "true"=false, secret_access_key: string="", upload_concurrency: int=1, upload_cutoff: string="209715200", zone: "pek3a" | "sh1a" | "gd2a"="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/qingstor`
**CLI:** `hoody files backends connect qingstor`

---

#### `connectQuatrix` — Connect to quatrix backend

```typescript
client.files.backends.connectQuatrix(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ api_key*: string="", description: string="", effective_upload_time: string="4s", encoding: string="50438146", hard_delete: bool=false, host*: string="", maximal_summary_chunk_size: string="100000000", minimal_chunk_size: string="10000000", skip_project_folders: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/quatrix`
**CLI:** `hoody files backends connect quatrix`

---

#### `connectS3` — Connect to s3 backend

```typescript
client.files.backends.connectS3(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_key_id: string="", acl: "default" | "private" | "public-read" | "public-read-write" | "authenticated-read" | "bucket-owner-read" | "bucket-owner-full-control"="", bucket_acl: "private" | "public-read" | "public-read-write" | "authenticated-read"="", chunk_size: string="5242880", copy_cutoff: string="4999610368", decompress: bool=false, description: string="", directory_bucket: bool=false, directory_markers: bool=false, disable_checksum: bool=false, disable_http2: bool=false, download_url: string="", encoding: string="50331650", endpoint*: "objects-us-east-1.dream.io" | "syd1.digitaloceanspaces.com" | "sfo3.digitaloceanspaces.com" | "fra1.digitaloceanspaces.com" | "nyc3.digitaloceanspaces.com" | "ams3.digitaloceanspaces.com" | "sgp1.digitaloceanspaces.com" | "localhost:8333" | …(35 values)="", env_auth: "false" | "true"=false, force_path_style: bool=true, leave_parts_on_error: bool=false, list_chunk: int=1000, list_url_encode: string, list_version: int=0, location_constraint: string="", max_upload_parts: int=10000, memory_pool_flush_time: int=60, memory_pool_use_mmap: bool=false, might_gzip: string, no_check_bucket: bool=false, no_head: bool=false, no_head_object: bool=false, no_system_metadata: bool=false, profile: string="", provider: "AWS" | "Alibaba" | "ArvanCloud" | "Ceph" | "ChinaMobile" | "Cloudflare" | "DigitalOcean" | "Dreamhost" | …(34 values)="", region: "" | "other-v2-signature"="", requester_pays: bool=false, sdk_log_mode: string="0", secret_access_key: string="", server_side_encryption: "" | "AES256" | "aws:kms"="", session_token: string="", shared_credentials_file: string="", sse_customer_algorithm: "" | "AES256"="", sse_customer_key: ""="", sse_customer_key_base64: ""="", sse_customer_key_md5: ""="", sse_kms_key_id: "" | "arn:aws:kms:us-east-1:*"="", storage_class: "STANDARD" | "LINE" | "GLACIER" | "DEEP_ARCHIVE"="", sts_endpoint: string="", upload_concurrency: int=4, upload_cutoff: string="209715200", use_accelerate_endpoint: bool=false, use_accept_encoding_gzip: string, use_already_exists: string, use_dual_stack: bool=false, use_multipart_etag: string, use_multipart_uploads: string, use_presigned_request: bool=false, use_unsigned_payload: string, v2_auth: bool=false, version_at: string="0001-01-01T00:00:00Z", version_deleted: bool=false, versions: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/s3`
**CLI:** `hoody files backends connect s3`

---

#### `connectSeafile` — Connect to seafile backend

```typescript
client.files.backends.connectSeafile(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ 2fa: bool=false, auth_token: string="", create_library: bool=false, description: string="", encoding: string="16850954", library: string="", library_key: string="", pass: string="", url*: "https://cloud.seafile.com/"="", user*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/seafile`
**CLI:** `hoody files backends connect seafile`

---

#### `connectSftp` — Connect to sftp backend

```typescript
client.files.backends.connectSftp(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ ask_password: bool=false, chunk_size: string="32768", ciphers: string, concurrency: int=64, connections: int=0, copy_is_hardlink: bool=false, description: string="", disable_concurrent_reads: bool=false, disable_concurrent_writes: bool=false, disable_hashcheck: bool=false, host*: string="", host_key_algorithms: string, idle_timeout: int=60, key_exchange: string, key_file: string="", key_file_pass: string="", key_pem: string="", key_use_agent: bool=false, known_hosts_file: "~/.ssh/known_hosts"="", macs: string, md5sum_command: string="", pass: string="", path_override: string="", port: int=22, pubkey: string="", pubkey_file: string="", server_command: string="", set_env: string, set_modtime: bool=true, sha1sum_command: string="", shell_type: "none" | "unix" | "powershell" | "cmd"="", skip_links: bool=false, socks_proxy: string="", ssh: string, subsystem: string="sftp", use_fstat: bool=false, use_insecure_cipher: "false" | "true"=false, user: string="user" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/sftp`
**CLI:** `hoody files backends connect sftp`

---

#### `connectSharefile` — Connect to sharefile backend

```typescript
client.files.backends.connectSharefile(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", chunk_size: string="67108864", client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="57091982", endpoint: string="", root_folder_id: "" | "favorites" | "allshared" | "connectors" | "top"="", token: string="", token_url: string="", upload_cutoff: string="134217728" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/sharefile`
**CLI:** `hoody files backends connect sharefile`

---

#### `connectSia` — Connect to sia backend

```typescript
client.files.backends.connectSia(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ api_password: string="", api_url: string="http://127.0.0.1:9980", description: string="", encoding: string="50436354", user_agent: string="Sia-Agent" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/sia`
**CLI:** `hoody files backends connect sia`

---

#### `connectSmb` — Connect to smb backend

```typescript
client.files.backends.connectSmb(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ case_insensitive: bool=true, description: string="", domain: string="WORKGROUP", encoding: string="56698766", hide_special_share: bool=true, host*: string="", idle_timeout: int=60, pass: string="", port: int=445, spn: string="", user: string="user" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/smb`
**CLI:** `hoody files backends connect smb`

---

#### `connectStorj` — Connect to storj backend

```typescript
client.files.backends.connectStorj(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_grant: string="", api_key: string="", description: string="", passphrase: string="", provider: "existing" | "new"="existing", satellite_address: "us1.storj.io" | "eu1.storj.io" | "ap1.storj.io"="us1.storj.io" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/storj`
**CLI:** `hoody files backends connect storj`

---

#### `connectSugarsync` — Connect to sugarsync backend

```typescript
client.files.backends.connectSugarsync(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_key_id: string="", app_id: string="", authorization: string="", authorization_expiry: string="", deleted_id: string="", description: string="", encoding: string="50397186", hard_delete: bool=false, private_access_key: string="", refresh_token: string="", root_id: string="", user: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/sugarsync`
**CLI:** `hoody files backends connect sugarsync`

---

#### `connectSwift` — Connect to swift backend

```typescript
client.files.backends.connectSwift(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ application_credential_id: string="", application_credential_name: string="", application_credential_secret: string="", auth: "https://auth.api.rackspacecloud.com/v1.0" | "https://lon.auth.api.rackspacecloud.com/v1.0" | "https://identity.api.rackspacecloud.com/v2.0" | "https://auth.storage.memset.com/v1.0" | "https://auth.storage.memset.com/v2.0" | "https://auth.cloud.ovh.net/v3" | "https://authenticate.ain.net"="", auth_token: string="", auth_version: int=0, chunk_size: string="5368709120", description: string="", domain: string="", encoding: string="16777218", endpoint_type: "public" | "internal" | "admin"="public", env_auth: "false" | "true"=false, fetch_until_empty_page: bool=false, key: string="", leave_parts_on_error: bool=false, no_chunk: bool=false, no_large_objects: bool=false, partial_page_fetch_threshold: int=0, region: string="", storage_policy: "" | "pcs" | "pca"="", storage_url: string="", tenant: string="", tenant_domain: string="", tenant_id: string="", use_segments_container: string, user: string="", user_id: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/swift`
**CLI:** `hoody files backends connect swift`

---

#### `connectTardigrade` — Connect to tardigrade backend

```typescript
client.files.backends.connectTardigrade(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_grant: string="", api_key: string="", description: string="", passphrase: string="", provider: "existing" | "new"="existing", satellite_address: "us1.storj.io" | "eu1.storj.io" | "ap1.storj.io"="us1.storj.io" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/tardigrade`
**CLI:** `hoody files backends connect tardigrade`

---

#### `connectUlozto` — Connect to ulozto backend

```typescript
client.files.backends.connectUlozto(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ app_token: string="", description: string="", encoding: string="50438146", list_page_size: int=500, password: string="", root_folder_slug: string="", username: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/ulozto`
**CLI:** `hoody files backends connect ulozto`

---

#### `connectUnion` — Connect to union backend

```typescript
client.files.backends.connectUnion(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ action_policy: string="epall", cache_time: int=120, create_policy: string="epmfs", description: string="", min_free_space: string="1073741824", search_policy: string="ff", upstreams*: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/union`
**CLI:** `hoody files backends connect union`

---

#### `connectUptobox` — Connect to uptobox backend

```typescript
client.files.backends.connectUptobox(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ access_token: string="", description: string="", encoding: string="50561070", private: bool=false }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/uptobox`
**CLI:** `hoody files backends connect uptobox`

---

#### `connectWebdav` — Connect to webdav backend

```typescript
client.files.backends.connectWebdav(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_redirect: bool=false, bearer_token: string="", bearer_token_command: string="", description: string="", encoding: string="", headers: string, nextcloud_chunk_size: string="10485760", owncloud_exclude_mounts: bool=false, owncloud_exclude_shares: bool=false, pacer_min_sleep: int=0, pass: string="", unix_socket: string="", url*: string="", user: string="", vendor: "fastmail" | "nextcloud" | "owncloud" | "sharepoint" | "sharepoint-ntlm" | "hoody-vfs" | "other"="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/webdav`
**CLI:** `hoody files backends connect webdav`

---

#### `connectYandex` — Connect to yandex backend

```typescript
client.files.backends.connectYandex(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="50429954", hard_delete: bool=false, spoof_ua: bool=true, token: string="", token_url: string="" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/yandex`
**CLI:** `hoody files backends connect yandex`

---

#### `connectZoho` — Connect to zoho backend

```typescript
client.files.backends.connectZoho(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ auth_url: string="", client_credentials: bool=false, client_id: string="", client_secret: string="", description: string="", encoding: string="16875520", region: "com" | "eu" | "in" | "jp" | "com.cn" | "com.au"="", token: string="", token_url: string="", upload_cutoff: string="10485760" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/backends/zoho`
**CLI:** `hoody files backends connect zoho`

---

#### `disconnect` — Disconnect backend

```typescript
client.files.backends.disconnect(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/backends/{id}`
**CLI:** `hoody files backends disconnect`

---

#### `getDetails` — Get backend details

```typescript
client.files.backends.getDetails(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/backends/{id}`
**CLI:** `hoody files backends get`

---

#### `list` — List all backends

```typescript
client.files.backends.list()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/backends`
**CLI:** `hoody files backends list`

---

#### `testConnection` — Test backend connection

```typescript
client.files.backends.testConnection(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/backends/{id}/test`
**CLI:** `hoody files backends test`

---

#### `update` — Update backend credentials

```typescript
client.files.backends.update(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Backend ID (16-character hex string) |
| `data` | `object` | body | Yes |  |

**Body:** `{ [key: string]: string|null }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/backends/{id}`
**CLI:** `hoody files backends update`

---

### `client.files.directories` (1) — Directory operations - create, list, download as ZIP

#### `create` — Create directory

```typescript
client.files.directories.create(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `MKCOL /{path}`

---

### `client.files.downloads` (4) — Downloads

#### `fetch` — Download file from remote URL

```typescript
client.files.downloads.fetch(directory: string, download: string, filename?: string, timeout?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `directory` | `string` | path | Yes | Destination directory |
| `download` | `string` | query | Yes | URL to download from |
| `filename` | `string` | query | No | Custom filename for downloaded file |
| `timeout` | `integer` | query | No | Download timeout in seconds |

**Returns:** `files_DownloadResult`  |  **HTTP:** `GET /{directory}?download`
**CLI:** `hoody files downloads url`

---

#### `getHistory` — Download history

```typescript
client.files.downloads.getHistory(download_history: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `download_history` | `string` | query | Yes |  |

**Returns:** `files_DownloadHistory`  |  **HTTP:** `GET /?download_history`
**CLI:** `hoody files downloads history`

---

#### `listActive` — List active downloads

```typescript
client.files.downloads.listActive(directory: string, downloads: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `directory` | `string` | path | Yes |  |
| `downloads` | `string` | query | Yes |  |

**Returns:** `files_ActiveDownloads`  |  **HTTP:** `GET /{directory}?downloads`
**CLI:** `hoody files downloads active`

---

#### `listGlobal` — List active downloads

```typescript
client.files.downloads.listGlobal()
```

**Returns:** `files_ActiveDownloads`  |  **HTTP:** `GET /api/v1/downloads`
**CLI:** `hoody files downloads all`

---

### `client.files` (21) — File operations - upload, download, delete, list files

#### `append` — Append data to file

```typescript
client.files.append(path: string, owner?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File path |
| `owner` | `string` | query | No | Create-time owner (user[:group]/uid[:gid]) when this append creates a new file. Requires --allow-chown + allowlist; refuses root. Absent → server default. |

**Returns:** `files_AppendResponse`  |  **HTTP:** `PUT /api/v1/files/append/{path}`
**CLI:** `hoody files append`

---

#### `chmod` — Change file permissions

```typescript
client.files.chmod(path: string, chmod: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File or directory path |
| `chmod` | `string` | query | Yes | Octal permission mode (e.g., 755, 644, 0755) |

**Returns:** `files_ChmodResponse`  |  **HTTP:** `PATCH /api/v1/files/chmod/{path}`
**CLI:** `hoody files chmod`

---

#### `chown` — Change file ownership

```typescript
client.files.chown(path: string, chown: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File or directory path |
| `chown` | `string` | query | Yes | Owner and optional group (e.g., user:group, user,:group, or UID:GID) |

**Returns:** `files_ChownResponse`  |  **HTTP:** `PATCH /api/v1/files/chown/{path}`
**CLI:** `hoody files chown`

---

#### `copy` — Copy file or directory

```typescript
client.files.copy(path: string, copy_to: string, overwrite?: string, owner?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Source file or directory path |
| `copy_to` | `string` | query | Yes | Destination path to copy the file/directory to |
| `overwrite` | `string` | query | No | Allow overwriting existing destination (default: false) |
| `owner` | `string` | query | No | Create-time owner (user[:group]/uid[:gid]) for newly-created copies. Requires --allow-chown + allowlist; refuses root. Overwritten existing files preserve their owner. Absent → server default. |

**Returns:** `files_CopyResponse`  |  **HTTP:** `POST /api/v1/files/copy/{path}`
**CLI:** `hoody files copy`

---

#### `delete` — Delete file or directory

```typescript
client.files.delete(path: string, backend?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `backend` | `string` | query | No | Backend ID for remote file deletion |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/files/{path}`
**CLI:** `hoody files delete`

---

#### `deleteRecursive` — Delete file or directory

```typescript
client.files.deleteRecursive(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Path to file or directory to delete |

**Returns:** `any`  |  **HTTP:** `DELETE /{path}`
**CLI:** `hoody files delete-recursive`

---

#### `get` — List directory or download file

```typescript
client.files.get(path: string, backend?: string, hash?: string, sha256?: string, base64?: string, preview?: string, contents?: string, stat?: string, thumbnail?: string, grep?: string, ignore_case?: boolean, fixed_string?: boolean, glob?: string, context?: integer, max_count?: integer, max_matches?: integer, max_depth?: integer, max_filesize?: integer, timeout?: integer, no_ignore?: boolean, max_results?: integer, max_files_scanned?: integer, sort?: string, order?: string, lines?: string, history?: string, at?: string, revision?: integer, diff?: string, from_seq?: integer, from_ts?: string, to_seq?: integer, to_ts?: string, after_id?: integer, limit?: integer, zip?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File or directory path |
| `backend` | `string` | query | No | Backend ID for remote file access |
| `hash` | `string` | query | No | Get SHA256 hash of file |
| `sha256` | `string` | query | No | Get SHA256 hash of file (alias for hash) |
| `base64` | `string` | query | No | Get file content as base64 |
| `preview` | `string` | query | No | Preview archive contents (for zip/tar files). Alias: ?contents |
| `contents` | `string` | query | No | Alias for ?preview - list archive contents |
| `stat` | `string` | query | No | Get file/directory metadata (stat) without downloading content |
| `thumbnail` | `string` | query | No | Return a processed image (resize, format convert, blur, grayscale). Requires the service to be started with --allow-thumbnails; returns 403 when disabled. |
| `grep` | `string` | query | No | Search file/directory contents for regex pattern (or literal if fixed_string=true). Requires --allow-grep. |
| `ignore_case` | `boolean` | query | No | Case-insensitive grep matching |
| `fixed_string` | `boolean` | query | No | Treat grep pattern as literal string, not regex |
| `glob` | `string` | query | No | Find files matching glob pattern (e.g. '**/*.rs', 'src/**/*.{ts,tsx}'). Requires --allow-search. Directory paths only. |
| `context` | `integer` | query | No | Number of context lines before/after each grep match |
| `max_count` | `integer` | query | No | Max matches per file for grep |
| `max_matches` | `integer` | query | No | Total max matches across all files for grep |
| `max_depth` | `integer` | query | No | Directory recursion depth for grep |
| `max_filesize` | `integer` | query | No | Skip files larger than this (bytes) during grep |
| `timeout` | `integer` | query | No | Grep timeout in seconds |
| `no_ignore` | `boolean` | query | No | Bypass.gitignore filtering during grep |
| `max_results` | `integer` | query | No | Max entries returned for glob search |
| `max_files_scanned` | `integer` | query | No | Max filesystem entries scanned during glob search |
| `sort` | `string` | query | No | Sort glob results by: mtime (default), name, or size |
| `order` | `string` | query | No | Sort order for glob results. Default: desc for mtime, asc for name/size |
| `lines` | `string` | query | No | Extract specific lines from a file. Formats: '10-50' (range, 1-indexed inclusive), '100' (single line), '-20' (last 20 lines / tail), '50-' (line 50 to end). Returns text/plain with X-Line-Range header. X-Total-Lines header included when naturally known (scan reached EOF). Max 100,000 lines or 64MB per request. |
| `history` | `string` | query | No | List all revisions of a file. Returns JSON with revisions array, pagination via after_id. Mutually exclusive with at/revision/diff. |
| `at` | `string` | query | No | Read file content at a point in time. Accepts RFC3339 timestamp or Unix milliseconds. Mutually exclusive with history/revision/diff. Composable with ?lines, ?hash, ?base64. |
| `revision` | `integer` | query | No | Read file content by stable per-path sequence number. Mutually exclusive with history/at/diff. Composable with ?lines, ?hash, ?base64. |
| `diff` | `string` | query | No | Compute unified diff between two versions. Requires from_seq or from_ts. Optional to_seq or to_ts (defaults to current file). Mutually exclusive with history/at/revision. |
| `from_seq` | `integer` | query | No | Source revision seq number for ?diff. Mutually exclusive with from_ts. |
| `from_ts` | `string` | query | No | Source timestamp for ?diff (RFC3339 or Unix ms). Mutually exclusive with from_seq. |
| `to_seq` | `integer` | query | No | Target revision seq number for ?diff. Mutually exclusive with to_ts. Default: current file on disk. |
| `to_ts` | `string` | query | No | Target timestamp for ?diff (RFC3339 or Unix ms). Mutually exclusive with to_seq. |
| `after_id` | `integer` | query | No | Cursor for ?history pagination. Returns entries with id > after_id. |
| `limit` | `integer` | query | No | Max entries to return for ?history. |
| `zip` | `string` | query | No | Download a directory as a streaming zip archive (bare flag, e.g. ?zip). Local directories only; requires --allow-archive. Same behavior as the WebDAV-style /{directory}?zip. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/files/{path}`
**CLI:** `hoody files get`

---

#### `getMetadata` — Get file metadata

```typescript
client.files.getMetadata(path: string, history?: string, at?: string, revision?: integer, diff?: string, from_seq?: integer, from_ts?: string, to_seq?: integer, to_ts?: string, after_id?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `history` | `string` | query | No | List all revisions of a file. Returns JSON with revisions array, pagination via after_id. Mutually exclusive with at/revision/diff. |
| `at` | `string` | query | No | Read file content at a point in time. Accepts RFC3339 timestamp or Unix milliseconds. Mutually exclusive with history/revision/diff. Composable with ?lines, ?hash, ?base64. |
| `revision` | `integer` | query | No | Read file content by stable per-path sequence number. Mutually exclusive with history/at/diff. Composable with ?lines, ?hash, ?base64. |
| `diff` | `string` | query | No | Compute unified diff between two versions. Requires from_seq or from_ts. Optional to_seq or to_ts (defaults to current file). Mutually exclusive with history/at/revision. |
| `from_seq` | `integer` | query | No | Source revision seq number for ?diff. Mutually exclusive with from_ts. |
| `from_ts` | `string` | query | No | Source timestamp for ?diff (RFC3339 or Unix ms). Mutually exclusive with from_seq. |
| `to_seq` | `integer` | query | No | Target revision seq number for ?diff. Mutually exclusive with to_ts. Default: current file on disk. |
| `to_ts` | `string` | query | No | Target timestamp for ?diff (RFC3339 or Unix ms). Mutually exclusive with to_seq. |
| `after_id` | `integer` | query | No | Cursor for ?history pagination. Returns entries with id > after_id. |
| `limit` | `integer` | query | No | Max entries to return for ?history. |

**Returns:** `any`  |  **HTTP:** `HEAD /{path}`
**CLI:** `hoody files metadata`

---

#### `glob` — Find files by glob pattern

```typescript
client.files.glob(path: string, pattern: string, max_results?: integer, max_depth?: integer, max_files_scanned?: integer, timeout?: integer, no_ignore?: boolean, sort?: string, order?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Directory path to search within |
| `pattern` | `string` | query | Yes | Glob pattern (e.g. '**/*.rs', 'src/**/*.{ts,tsx}', '*.md') |
| `max_results` | `integer` | query | No | Maximum entries to return |
| `max_depth` | `integer` | query | No | Maximum directory recursion depth |
| `max_files_scanned` | `integer` | query | No | Maximum filesystem entries to scan |
| `timeout` | `integer` | query | No | Search timeout in seconds |
| `no_ignore` | `boolean` | query | No | Bypass.gitignore filtering |
| `sort` | `string` | query | No | Sort results by: mtime (modification time), name, or size |
| `order` | `string` | query | No | Sort order. Default: desc for mtime, asc for name/size |

**Returns:** `files_GlobResults`  |  **HTTP:** `GET /api/v1/files/glob/{path}`
**CLI:** `hoody files glob`

---

#### `grep` — Search file contents (grep)

```typescript
client.files.grep(path: string, pattern: string, ignore_case?: boolean, fixed_string?: boolean, glob?: string, context?: integer, max_count?: integer, max_matches?: integer, max_depth?: integer, max_filesize?: integer, timeout?: integer, no_ignore?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File or directory path to search |
| `pattern` | `string` | query | Yes | Search pattern (regex by default, literal if fixed_string=true) |
| `ignore_case` | `boolean` | query | No | Case-insensitive matching |
| `fixed_string` | `boolean` | query | No | Treat pattern as literal string, not regex |
| `glob` | `string` | query | No | Filter files by glob pattern (e.g. '*.rs', '*.{ts,tsx}') |
| `context` | `integer` | query | No | Number of context lines before and after each match |
| `max_count` | `integer` | query | No | Maximum matches per file |
| `max_matches` | `integer` | query | No | Total maximum matches across all files |
| `max_depth` | `integer` | query | No | Maximum directory recursion depth |
| `max_filesize` | `integer` | query | No | Skip files larger than this (bytes) |
| `timeout` | `integer` | query | No | Search timeout in seconds |
| `no_ignore` | `boolean` | query | No | Bypass.gitignore filtering |

**Returns:** `files_GrepResults`  |  **HTTP:** `GET /api/v1/files/grep/{path}`
**CLI:** `hoody files grep`

---

#### `listDirectory` — List directory contents or download file

```typescript
client.files.listDirectory(path: string, json?: string, simple?: string, sort?: string, order?: string, hash?: string, sha256?: string, base64?: string, edit?: string, view?: string, download?: string, content-type?: string, history?: string, at?: string, revision?: integer, diff?: string, from_seq?: integer, from_ts?: string, to_seq?: integer, to_ts?: string, after_id?: integer, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File or directory path |
| `json` | `string` | query | No | Return JSON format instead of HTML |
| `simple` | `string` | query | No | Return simple text listing |
| `sort` | `string` | query | No | Sort by field |
| `order` | `string` | query | No | Sort order |
| `hash` | `string` | query | No | Get SHA256 hash of file (returns plain text hash) |
| `sha256` | `string` | query | No | Get SHA256 hash of file (alias for hash) |
| `base64` | `string` | query | No | Get file content as base64 encoded string |
| `edit` | `string` | query | No | Open file in Web UI editor (requires allow-upload permission) |
| `view` | `string` | query | No | View file in Web UI (read-only mode) |
| `download` | `string` | query | No | For file paths only: force browser download (Content-Disposition: attachment). Accepted values: empty (?download), 1, or true. For directory paths, ?download is the URL download-manager operation. |
| `content-type` | `string` | query | No | Override Content-Type header for file downloads |
| `history` | `string` | query | No | List all revisions of a file. Returns JSON with revisions array, pagination via after_id. Mutually exclusive with at/revision/diff. |
| `at` | `string` | query | No | Read file content at a point in time. Accepts RFC3339 timestamp or Unix milliseconds. Mutually exclusive with history/revision/diff. Composable with ?lines, ?hash, ?base64. |
| `revision` | `integer` | query | No | Read file content by stable per-path sequence number. Mutually exclusive with history/at/diff. Composable with ?lines, ?hash, ?base64. |
| `diff` | `string` | query | No | Compute unified diff between two versions. Requires from_seq or from_ts. Optional to_seq or to_ts (defaults to current file). Mutually exclusive with history/at/revision. |
| `from_seq` | `integer` | query | No | Source revision seq number for ?diff. Mutually exclusive with from_ts. |
| `from_ts` | `string` | query | No | Source timestamp for ?diff (RFC3339 or Unix ms). Mutually exclusive with from_seq. |
| `to_seq` | `integer` | query | No | Target revision seq number for ?diff. Mutually exclusive with to_ts. Default: current file on disk. |
| `to_ts` | `string` | query | No | Target timestamp for ?diff (RFC3339 or Unix ms). Mutually exclusive with to_seq. |
| `after_id` | `integer` | query | No | Cursor for ?history pagination. Returns entries with id > after_id. |
| `limit` | `integer` | query | No | Max entries to return for ?history. |

**Returns:** `files_DirectoryListing`  |  **HTTP:** `GET /{path}`
**CLI:** `hoody files dir`

---

#### `move` — Move file or directory

```typescript
client.files.move(path: string, move_to: string, owner?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Source file or directory path |
| `move_to` | `string` | query | Yes | Destination path to move the file/directory to |
| `owner` | `string` | query | No | Create-time owner (user[:group]/uid[:gid]) for newly-created destination PARENT directories. Requires --allow-chown + --allowed-create-owners; refuses root. The moved inode itself preserves its existing owner. Absent → server default. |

**Returns:** `files_MoveResponse`  |  **HTTP:** `POST /api/v1/files/move/{path}`
**CLI:** `hoody files move`

---

#### `operate` — File operations (mkdir, extract, download, move, copy)

```typescript
client.files.operate(path: string, backend?: string, mkdir?: string, extract?: string, dest?: string, download_from?: string, move_to?: string, copy_to?: string, overwrite?: string, owner?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `backend` | `string` | query | No |  |
| `mkdir` | `string` | query | No | Create directory |
| `extract` | `string` | query | No | Extract archive. Empty value extracts all; non-empty value is a selective path to extract (e.g. "src/" or "lib/") |
| `dest` | `string` | query | No | Destination directory name for extraction (default: archive name without extension) |
| `download_from` | `string` | query | No | Download file from remote URL |
| `move_to` | `string` | query | No | Move file/directory to destination path |
| `copy_to` | `string` | query | No | Copy file/directory to destination path |
| `overwrite` | `string` | query | No | Allow overwriting existing destination (for copy) |
| `owner` | `string` | query | No | Create-time owner for newly-created inodes as user[:group] or uid[:gid]. Requires --allow-chown and must resolve to an entry in --allowed-create-owners; refuses root (uid/gid 0). Absent → the server default create owner. Applies to mkdir/extract/download_from/copy_to. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/files/{path}`
**CLI:** `hoody files operation`

---

#### `patch` — File operations

```typescript
client.files.patch(path: string, X-Update-Range?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `X-Update-Range` | `string` | header | No | Set to 'append' to append data to the end of the file. Perfect for logs and incremental writes. Example: curl -X PATCH -H 'X-Update-Range: append' --data-binary @data.txt http://server/file.log |
| `data` | `object` | body | No |  |

**Body:** `files_ChmodRequest | files_ChownRequest | files_RenameRequest`

**Returns:** `void`  |  **HTTP:** `PATCH /{path}`
**CLI:** `hoody files patch`

---

#### `patchApi` — Modify file properties or move/rename

```typescript
client.files.patchApi(path: string, backend?: string, owner?: string, chmod?: string, chown?: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File path |
| `backend` | `string` | query | No | Backend ID for remote file operations |
| `owner` | `string` | query | No | Create-time owner (user[:group]/uid[:gid]) for newly-created destination parent directories on a JSON-body move_to. Requires --allow-chown + --allowed-create-owners; cannot be root. The moved item keeps its own owner. Absent → server default. |
| `chmod` | `string` | query | No | Set file permissions using octal mode value (e.g., ?chmod=755) |
| `chown` | `string` | query | No | Set file ownership (e.g., ?chown=user:group or ?chown=user) |
| `data` | `object` | body | No |  |

**Body:** `files_MoveRequest | files_RenameRequest`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/files/{path}`
**CLI:** `hoody files modify-properties`

---

#### `put` — Upload or append file

```typescript
client.files.put(path: string, backend?: string, append?: string, owner?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `backend` | `string` | query | No | Backend ID for remote upload |
| `append` | `string` | query | No | Append body to end of existing file (create if missing) instead of overwriting |
| `owner` | `string` | query | No | Create-time owner (user[:group]/uid[:gid]) for a newly-created file. Requires --allow-chown + --allowed-create-owners; refuses root. Overwrites/appends to an existing file preserve its owner. Absent → server default. |

**Returns:** `files_AppendResponse`  |  **HTTP:** `PUT /api/v1/files/{path}`
**CLI:** `hoody files put`

---

#### `realpath` — Resolve canonical path (realpath)

```typescript
client.files.realpath(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File or directory path to resolve |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/files/realpath/{path}`
**CLI:** `hoody files realpath`

---

#### `search` — Search directory

```typescript
client.files.search(directory: string, q: string, json?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `directory` | `string` | path | Yes |  |
| `q` | `string` | query | Yes | Search query (case-insensitive filename match). Maximum 512 BYTES of UTF-8 after form/percent decoding, measured both before and after Unicode lowercasing — lowercasing can change a string's byte length in either direction. Longer queries are rejected with 400; they are not truncated. Note this is a byte limit, not a character limit, so it is deliberately not expressed as `maxLength`. |
| `json` | `string` | query | No | Return JSON format instead of HTML |

**Returns:** `files_DirectoryListing`  |  **HTTP:** `GET /{directory}?q`
**CLI:** `hoody files search`

---

#### `stat` — Get file metadata (stat)

```typescript
client.files.stat(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File or directory path |

**Returns:** `files_FileStatResponse`  |  **HTTP:** `GET /api/v1/files/stat/{path}`
**CLI:** `hoody files stat`

---

#### `touch` — Touch file (create or update mtime)

```typescript
client.files.touch(path: string, touch: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | File path to touch |
| `touch` | `string` | query | Yes | Flag to indicate touch operation |

**Returns:** `any`  |  **HTTP:** `PUT /{path}?touch`
**CLI:** `hoody files touch`

---

#### `upload` — Upload file

```typescript
client.files.upload(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Destination file path |

**Returns:** `any`  |  **HTTP:** `PUT /{path}`
**CLI:** `hoody files upload`

---

### `client.files.ftp` (1) — WebDAV-compatible API for FTP/FTPS

#### `access` — Access file via FTP

```typescript
client.files.ftp.access(path: string, type: string, server: string, user?: string, pass?: string, ftp_secure?: boolean, ftp_passive?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `type` | `string` | query | Yes |  |
| `server` | `string` | query | Yes |  |
| `user` | `string` | query | No |  |
| `pass` | `string` | query | No |  |
| `ftp_secure` | `boolean` | query | No | Use FTPS (FTP over TLS) |
| `ftp_passive` | `boolean` | query | No | Use passive mode |

**Returns:** `any`  |  **HTTP:** `GET /{path}?type=ftp`
**CLI:** `hoody files access ftp`

---

### `client.files.git` (1) — WebDAV-compatible API for Git repositories

#### `fetch` — Fetch file from Git repository

```typescript
client.files.git.fetch(path: string, type: string, url: string, ref?: string, pass?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `type` | `string` | query | Yes |  |
| `url` | `string` | query | Yes | Full GitHub/GitLab/Bitbucket URL or repository URL |
| `ref` | `string` | query | No | Branch, tag, or commit (defaults to HEAD or extracted from URL) |
| `pass` | `string` | query | No | Personal Access Token (base64 encoded) for private repos |

**Returns:** `any`  |  **HTTP:** `GET /{path}?type=git`
**CLI:** `hoody files fetch-from-git`

---

### `client.files.health` (1) — Health

#### `check` — Service health check

```typescript
client.files.health.check()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/files/health`
**CLI:** `hoody files health`

---

### `client.files.images` (1) — On-the-fly image conversion, resizing, and effects with format conversion (JPEG/PNG/WebP/GIF/BMP), multiple resize modes, quality control, blur, grayscale, and two-tier caching (memory + disk)

#### `process` — Process and convert images

```typescript
client.files.images.process(image: string, thumbnail: string, format?: string, size?: string, width?: integer, height?: integer, resize?: string, quality?: string, q?: integer, blur?: number, grayscale?: string, bg?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `image` | `string` | path | Yes | Path to image file |
| `thumbnail` | `string` | query | Yes | Enable image processing |
| `format` | `string` | query | No | Output format (default: jpeg) |
| `size` | `string` | query | No | Width×Height in pixels (max: 2000×2000) |
| `width` | `integer` | query | No | Width in pixels (height auto-calculated) |
| `height` | `integer` | query | No | Height in pixels (width auto-calculated) |
| `resize` | `string` | query | No | Resize mode: fit (preserve aspect, fit within), fill (exact size, crop), cover (cover area), exact (force dimensions) |
| `quality` | `string` | query | No | Resize algorithm quality: low (box filter), medium (bilinear), high (Lanczos3) |
| `q` | `integer` | query | No | JPEG/WebP quality (1-100, higher is better quality) |
| `blur` | `number` | query | No | Gaussian blur radius (0-50) |
| `grayscale` | `string` | query | No | Convert to grayscale/black-and-white |
| `bg` | `string` | query | No | Background color for transparency (hex RGB, e.g., 'ffffff' for white) |

**Returns:** `any`  |  **HTTP:** `GET /{image}?thumbnail`
**CLI:** `hoody files process-image`

---

### `client.files.journal` (3) — Journal

#### `flush` — Flush journal to disk

```typescript
client.files.journal.flush()
```

**Returns:** `any`  |  **HTTP:** `POST /api/v1/journal/flush`
**CLI:** `hoody files journal flush`

---

#### `getStats` — Get journal statistics

```typescript
client.files.journal.getStats()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/journal/stats`
**CLI:** `hoody files journal stats`

---

#### `query` — Query journal entries

```typescript
client.files.journal.query(path?: string, op?: string, since?: string, limit?: integer, after_id?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | query | No | Filter entries by path prefix |
| `op` | `string` | query | No | Filter by operation type(s), comma-separated (e.g. 'write,delete') |
| `since` | `string` | query | No | Filter entries since timestamp (RFC3339 or Unix ms) |
| `limit` | `integer` | query | No | Max entries to return |
| `after_id` | `integer` | query | No | Cursor: return entries with id > after_id |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/journal`
**CLI:** `hoody files journal query`

---

### `client.files.mounts` (5) — Mount management - create, list, and remove FUSE filesystem mounts for remote backends

#### `create` — Create persistent FUSE mount

```typescript
client.files.mounts.create(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ backend_id*: string, label: string, mount_path: string, vfs_config: { cache_max_age: int | string=3600, cache_max_size: int | string=10737418240, cache_mode: "off" | "minimal" | "writes" | "full"="writes", dir_cache_time: int | string=300 } }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/mounts`
**CLI:** `hoody files mounts create`

---

#### `getDetails` — Get mount details

```typescript
client.files.mounts.getDetails(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/mounts/{id}`
**CLI:** `hoody files mounts get`

---

#### `list` — List all mounts

```typescript
client.files.mounts.list(label?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `label` | `string` | query | No | Filter mounts by label. Only mounts with this exact label will be returned. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/mounts`
**CLI:** `hoody files mounts list`

---

#### `unmount` — Unmount filesystem

```typescript
client.files.mounts.unmount(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/mounts/{id}`
**CLI:** `hoody files mounts unmount`

---

#### `update` — Update mount VFS configuration

```typescript
client.files.mounts.update(id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Mount ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ vfs_config*: object }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/mounts/{id}`
**CLI:** `hoody files mounts update`

---

### `client.files.s3` (1) — WebDAV-compatible API for S3 storage

#### `access` — Access file from S3

```typescript
client.files.s3.access(path: string, type: string, server: string, s3_bucket: string, s3_region: string, user?: string, pass?: string, s3_endpoint?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `type` | `string` | query | Yes |  |
| `server` | `string` | query | Yes |  |
| `s3_bucket` | `string` | query | Yes | S3 bucket name |
| `s3_region` | `string` | query | Yes |  |
| `user` | `string` | query | No | AWS Access Key ID |
| `pass` | `string` | query | No | AWS Secret Key (base64 encoded) |
| `s3_endpoint` | `string` | query | No | Custom S3 endpoint for MinIO, etc. |

**Returns:** `any`  |  **HTTP:** `GET /{path}?type=s3`
**CLI:** `hoody files access s3`

---

### `client.files.ssh` (2) — WebDAV-compatible API for SSH/SFTP

#### `access` — Access file via SSH/SFTP

```typescript
client.files.ssh.access(path: string, type: string, server: string, user: string, pass?: string, key?: string, passphrase?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `type` | `string` | query | Yes |  |
| `server` | `string` | query | Yes | Server hostname:port |
| `user` | `string` | query | Yes | SSH username |
| `pass` | `string` | query | No | Password (base64 encoded) |
| `key` | `string` | query | No | Private key PEM (base64 encoded) |
| `passphrase` | `string` | query | No | Key passphrase (base64 encoded) |

**Returns:** `any`  |  **HTTP:** `GET /{path}?type=ssh`
**CLI:** `hoody files access ssh`

---

#### `upload` — Upload file via SSH/SFTP

```typescript
client.files.ssh.upload(path: string, server: string, user: string, pass?: string, key?: string, passphrase?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `server` | `string` | query | Yes | Server hostname:port |
| `user` | `string` | query | Yes | SSH username |
| `pass` | `string` | query | No | Password (base64 encoded) |
| `key` | `string` | query | No | Private key PEM (base64 encoded) |
| `passphrase` | `string` | query | No | Key passphrase (base64 encoded) |

**Returns:** `any`  |  **HTTP:** `PUT /{path}?type=ssh`
**CLI:** `hoody files access ssh-upload`

---

### `client.files.system` (1) — System endpoints - health checks, version info, and status monitoring

#### `getApiVersion` — Get API version

```typescript
client.files.system.getApiVersion()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/version`
**CLI:** `hoody files version`

---

### `client.files.webdav` (8) — WebDAV protocol operations - PROPFIND, PROPPATCH, COPY, MOVE, LOCK, UNLOCK, OPTIONS

#### `access` — Access file via WebDAV

```typescript
client.files.webdav.access(path: string, type: string, server: string, user?: string, pass?: string, webdav_path?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `type` | `string` | query | Yes |  |
| `server` | `string` | query | Yes |  |
| `user` | `string` | query | No |  |
| `pass` | `string` | query | No |  |
| `webdav_path` | `string` | query | No | WebDAV endpoint path |

**Returns:** `any`  |  **HTTP:** `GET /{path}?type=webdav`
**CLI:** `hoody files access webdav`

---

#### `copyResource` — Copy file or directory

```typescript
client.files.webdav.copyResource(path: string, Destination: string, Depth?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Source file or directory path |
| `Destination` | `string` | header | Yes | Destination URL for the copy |
| `Depth` | `string` | header | No | Copy depth: 0 (file only) or infinity (recursive for directories) |

**Returns:** `any`  |  **HTTP:** `COPY /{path}`

---

#### `getOptions` — Get allowed methods

```typescript
client.files.webdav.getOptions(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |

**Returns:** `void`  |  **HTTP:** `OPTIONS /{path}`
**CLI:** `hoody files options`

---

#### `lockResource` — Lock file (WebDAV compatibility)

```typescript
client.files.webdav.lockResource(path: string, Depth?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `Depth` | `string` | header | No |  |

**Returns:** `any`  |  **HTTP:** `LOCK /{path}`

---

#### `moveResource` — Move or rename file/directory

```typescript
client.files.webdav.moveResource(path: string, Destination: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Source file or directory path |
| `Destination` | `string` | header | Yes | Destination URL for the move |

**Returns:** `any`  |  **HTTP:** `MOVE /{path}`

---

#### `propfindResource` — Get WebDAV properties

```typescript
client.files.webdav.propfindResource(path: string, Depth?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `Depth` | `string` | header | No | Depth of property retrieval: 0 (resource only), 1 (immediate children), infinity (recursive) |

**Returns:** `void`  |  **HTTP:** `PROPFIND /{path}`

---

#### `proppatchResource` — Update WebDAV properties

```typescript
client.files.webdav.proppatchResource(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |

**Returns:** `void`  |  **HTTP:** `PROPPATCH /{path}`

---

#### `unlockResource` — Unlock file (WebDAV compatibility)

```typescript
client.files.webdav.unlockResource(path: string, Lock-Token: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes |  |
| `Lock-Token` | `string` | header | Yes | Lock token to release |

**Returns:** `void`  |  **HTTP:** `UNLOCK /{path}`


### Body schemas

- `files_MoveRequest` — `{ move_to*: string }`
- `files_RenameRequest` — `{ name*: string }`
- `files_ChmodRequest` — `{ mode*: string }`
- `files_ChownRequest` — `{ group: string, owner: string }`


---

<!-- ===== namespace: notes ===== -->

# `notes` — Collaborative notebooks, hierarchical nodes, documents, databases

## Purpose

Per-container notebooks of hierarchical nodes (sections, pages, channels, messages, databases, records) with rich-text bodies, comments, reactions, versions, collaborators, TUS attachments, WS mutation feed.

## When to use

Section→page wikis; typed `database`/`record` nodes; comments/reactions/versions/collaborators; TUS attachments; WS-driven UI.

## When NOT to use

SQL/KV → `sqlite`, container fs → `files`, desktop notifs → `notifications`, cross-container identity → `api`, scheduled writes → `cron`.

## Prerequisites

- `notebookId` per call; first `identity.get` auto-provisions notebook+user from `?username=` (default `user`).
- HTTP/raw fetch supports `X-Idempotency-Key` for retry-safe creates. **Most generated SDK service methods do NOT expose per-call request headers** (`requestOptions` has no `headers` field), so idempotency-keyed retries on those must use raw `fetch()` against the kit URL. **The exception is `documents.appendDocument`, which accepts the key as `options.XIdempotencyKey`** — so the recommended document-writing path is fully retry-safe from the SDK. Export `ticket` is HTML-export-only.
- **Writing a document needs editor-or-admin role on the node** — `documents.put`/`patch`/`appendDocument` resolve access via `getNodeAccess` and reject viewers/read-only collaborators with `403`. Documents attach only to `page` and `record` nodes (the node types that declare a `documentSchema`); `message`/`channel`/`database` nodes do not support documents.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

1. **Write a page (RECOMMENDED: append)** — the simplest, most reliable way to put content into a note. First get a page node id (use the auto-provisioned `Home` section: `nodes.list` `type:"section"` → pick `Home` → `nodes.create` `type:"page"`, `parentId:<sectionId>`, `attributes:{name}`). Then `documents.appendDocument` with `{text:"…", type:"paragraph"|"heading1"|…}` (one block from plain text) OR `{blocks:[{type,content,attrs}]}` (batch). **The server assigns each block's `id`, `parentId`, and `index`** — you never compute fractional indices or block ids, which is the part agents get wrong with `documents.put`. Creates the document if absent; pass `X-Idempotency-Key` (SDK `options.XIdempotencyKey`) for safe retries. See §Examples 1–2.
2. **Bootstrap identity + notebook** — `identity.get` → `{userId,username,role,notebookId}` (auto-provisions a notebook + `Home` section + starter pages). `notebooks.listNotebooks`/`create`/`get` open to any non-`none` member; `update`/`delete` are owner-gated.
3. **Build a structured document with `documents.put`** — use this only when you need full control over layout/ordering (append cannot create lists, tables, or nested blocks). `documents.put` OVERWRITES the whole document; `documents.patch` shallow-merges at the TOP level only (submitting `content.blocks` REPLACES the entire blocks map — it does NOT merge per-block). The body is `{content:{type:"rich_text",blocks:{<id>:<block>}}}`. **Use the real `EditorNodeTypes` strings and the `attrs` key, and remember container blocks (lists/tasks/blockquote/table cells) hold their text in a CHILD `paragraph` block** — see §Examples 0 (block-model cheat-sheet) and 3.
4. **Database CRUD** — `nodes.create` `type:"database"`; then `databases.create`/`databases.list`/`databases.search`/`databases.update` (merges `fields`)/`databases.delete`. Page with `page`/`count` on `databases.list` (count max 100). ⚠ SDK-only: the auto-pagination helpers (listIterator / listAll) are misconfigured upstream — they send `limit`/`offset` while the route accepts `page`/`count`; prefer manual paged list loops.
5. **Comments + versions** — `collaborators.add` (`admin`/`editor`/`collaborator`/`viewer`). `comments.create` (top-level, anchored, or reply); `edit`/`delete`/`resolve` accept optional `expectedVersion` for optimistic concurrency. `versions.create`/`list`/`get`/`restore`.
6. **TUS upload + download** — `files.tusCreateUpload` for `fileId`; `tusUploadChunk` `PATCH`+`Upload-Offset`; `tusCheckUpload` `HEAD` returns resume offset; `tusAbortUpload` cancels. `files.listIterator`, `files.download`.

## Quirks & gotchas

- **Use the real block `type` strings and the `attrs` key — wrong values store silently but render blank.** Valid block types are the `EditorNodeTypes` values: `paragraph`, `heading1`/`heading2`/`heading3`, `blockquote`, `bulletList`, `listItem`, `orderedList`, `taskList`, `taskItem`, `codeBlock`, `horizontalRule`, `table`/`tableRow`/`tableHeader`/`tableCell`, `page`, `file`, `folder`, `tempFile`, `drawing`, `grid`, plus the editor-extension blocks `embed` (block) and inline `mention`/`hardBreak`. There is NO `code`, `bullet_list_item`, or `quote` type, and block attributes live under `attrs` (NOT `props`); code language is `attrs.language`, a task's done-state is `attrs.checked`. The block schema is loose (`type:z.string()`, `attrs:z.record`), so a bad `type`/`props` is accepted with `200` and stored — the CRDT bridge validates with `safeParse` but writes the ORIGINAL object, persisting the junk key — the editor then has no renderer for it and the block shows blank. (A later full rewrite that omits the bad key reconciles it away.)
- **Container blocks hold NO direct text — their text lives in a CHILD `paragraph` block.** Only `paragraph`/`heading1-3`/`codeBlock` (and the text-less `horizontalRule`) are leaf blocks that carry `content:[{type:'text',text}]` directly. For `listItem`, `taskItem`, `blockquote`, `tableCell`, `tableHeader` you MUST add a child `paragraph` block whose `parentId` is the container's id; putting text directly on the container makes it render empty. See §Examples 0 and 3 for the exact nesting.
- **Prefer `documents.appendDocument` for adding content; it does NOT create the node.** Append server-assigns `id`/`parentId`/`index` and creates the document row if missing, but `404`s if the node is absent and `400`s for node types without a `documentSchema` (only `page`/`record`) — so create/find the page first. It rejects client-supplied `id`/`parentId`/`index` and reserved `attrs` keys (`id`,`parentId`,`index`,`type`,`__proto__`,`constructor`,`prototype`), accepts only `{type:'text'}` leaves (no inline `mention`/image), the `{text}` form does NOT split newlines (one literal block), and it caps at 100 blocks / 512 KiB per call. Appendable types: `paragraph`, `heading1-3`, `codeBlock`, `horizontalRule` (containers and `file` are rejected).
- **`documents.put` has no block/byte cap** (only the Fastify 10 MB body limit) and requires the node to exist, creating the document row if it has none; the 100-block / 512 KiB caps are append-only.
- **`nodes.create` with schema-invalid `attributes` for a KNOWN type returns `500 unknown`, not `400`** — `YDoc.update()` throws on the attribute `safeParse` before the create transaction's try/catch (unknown type / missing parent → `400`; permission/`canCreate` → `403`). A page needs `attributes.name` + `parentId` and cannot be root-level; a manually-created `section` must include `attributes.collaborators` with the creator as `admin` and is root-only — easiest is to reuse the auto-provisioned `Home` section.
- `notebookAuthenticator` re-anchors identity to URL `notebookId`; one bearer reaches any notebook the username joined.
- Cross-client convergence is **mutation-stream-driven** via the `mutations.sync` route + WS feed: each mutation type (`document.update`, `node.*`, etc.) is dispatched server-side to a SQL-backed lib function. `documents.put`/`patch` are last-writer-wins JSON overlays on top of the same store; two concurrent PATCHes will clobber each other unless drivers coordinate via the WS mutation feed.
- `identity.get?username=&role=` creates user+notebook and runs `initializeNotebookContent`. Priority Bearer → `ticket` → `?username=&role=`; invalid Bearer = 401 even with fallback. **Without any of the three, requests default to username `user` (NOT to a previously seen `?username=alex` query)** — re-pass `?username=` on every unauthenticated call, or attach Bearer / `ticket`. Username lowercased `/^[a-zA-Z0-9_-]+$/` 1–32. `role` ∈ `owner|admin|collaborator|guest|none`; `none` → `notebook_no_access`.
- **`Readonly` notebook gates writes only** — read endpoints still serve through; write routes (mutations, document.put/patch, record-create, etc.) are rejected with `403 notebook_readonly`.
- `X-Idempotency-Key` replay returns saved response; same key+different payload → 409.
- `databases.update` merges `fields`; access runs through `getNodeAccess(notebookId, databaseId, userId)`, which resolves your role from the `collaborations` row on the notebook **root** node, then walks the database's full ancestor chain and returns `403` if any ancestor is a private `section`, or a `channel` whose `attributes.collaborators` map omits you (notebook owner/admin bypasses the privacy walk). A root collaboration alone is therefore NOT sufficient. TUS validates `notebookId`/`fileId` against generated-id regex; free-form id → 400 `file_not_found`.
- `documents.get?output=html` needs single-use export `ticket` on `GET .../document`. `documents.put` overwrites; `documents.patch` top-level spreads the request body over current content — submitting `content.blocks` REPLACES the blocks map, it does not merge per-block. For per-block CRDT merging use the `mutations.sync` WS feed instead. `comments.edit`/`delete`/`resolve` accept optional `expectedVersion`.
- `databases.search` matches against record names AND field values (not just names).
- Text filter operators in `databases.list?filters=`: `is_equal_to` / `is_not_equal_to` / `contains` / `does_not_contain` / `starts_with` / `ends_with` / `is_empty` / `is_not_empty`. The bare `is` is NOT a valid operator — use `is_equal_to`; the bare `not_contains` is NOT either — use `does_not_contain`.
- TUS chunk uploads: `PATCH /api/v1/notes/notebooks/{n}/files/{id}/tus` is the byte-transfer call — send the raw chunk as the request body with `Upload-Offset`/`Tus-Resumable` headers (e.g. via `@tus/client`). SDK-only: the generated tusUploadChunk method takes no chunk-body or Upload-Offset parameter, so drop to raw `@tus/client`/`fetch` for the actual byte transfer.

## Common errors

- `400 bad_request` validation (`details[]`); `400 file_not_found` TUS id regex; `409` PK dupe or idempotency-key reused w/ different payload.
- `403 notebook_no_access`/`notebook_readonly`/`forbidden` (db needs `collaborations` or `canCreate`).
- `404 not_found` — node/file/comment/version missing or `notebook_id` mismatch. `500 unknown` — read-back failed or uncategorized.

## Related namespaces

`files`, `sqlite`, `notifications`, `api`, `exec`

## Examples

Every step in every example was live-tested against a real `notes-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first; bootstrap identity once with `GET /api/v1/notes/me?username=...&role=owner` to auto-provision `notebookId`.

### 0. Block model cheat-sheet — types, the `attrs` key, and container nesting

**Read this before hand-building any `documents.put` body.** A document is
`{ "content": { "type": "rich_text", "blocks": { "<blockId>": <block> } } }`. Each
block is `{ id, type, parentId, index, content?, attrs? }`:

- `type` is one of the real `EditorNodeTypes` strings. There is **no** `code`,
  `bullet_list_item`, `quote`, or `numbered_list_item`. Block attributes live under
  `attrs` (**never** `props`).
- **Leaf blocks** carry text directly in `content`: `paragraph`, `heading1`,
  `heading2`, `heading3`, `codeBlock` (language in `attrs.language`). `horizontalRule`
  is a leaf with no text/content.
- **Container blocks carry NO direct text** — each holds a child `paragraph`:
  `bulletList`/`orderedList` → `listItem` → `paragraph`; `taskList` → `taskItem`
  (`attrs.checked`) → `paragraph`; `blockquote` → `paragraph`; `table` → `tableRow` →
  `tableHeader`/`tableCell` → `paragraph`. The child's `parentId` is the container's id.
- `index` is a lexicographic ordering string per sibling group (server uses
  fractional indexing). For a brand-new document, monotonically increasing strings
  (`a0`,`a1`,`a2`,…) sort correctly. To INSERT between two existing blocks you need a
  key that sorts strictly between them — another reason to prefer append.
- Inline `content` leaves are `{ "type":"text", "text":"…", "marks?":[…] }`. Marks:
  `bold`, `italic`, `strike`, `underline`, `code` (no attrs); `link`
  (`attrs:{href,target,rel}`); `color` (`attrs:{color}`); `highlight`
  (`attrs:{highlight}`); `comment` (`attrs:{commentId}`). `mention` is an inline NODE
  (`{type:'mention',attrs:{id,target}}`), not a mark; `hardBreak`
  (`{type:'hardBreak'}`) forces a line break inside a paragraph.

Leaf blocks (text/code carry `content` directly):

```json
{ "h":  {"id":"h","type":"heading1","parentId":"PAGE","index":"a0","content":[{"type":"text","text":"Runbook"}]},
  "p":  {"id":"p","type":"paragraph","parentId":"PAGE","index":"a1","content":[{"type":"text","text":"Intro with ","marks":[]},{"type":"text","text":"bold","marks":[{"type":"bold"}]}]},
  "c":  {"id":"c","type":"codeBlock","parentId":"PAGE","index":"a2","attrs":{"language":"bash"},"content":[{"type":"text","text":"./deploy.sh prod"}]},
  "hr": {"id":"hr","type":"horizontalRule","parentId":"PAGE","index":"a3"} }
```

Bulleted list — `bulletList → listItem → paragraph` (use `orderedList` for numbered):

```json
{ "bl":  {"id":"bl","type":"bulletList","parentId":"PAGE","index":"a0"},
  "li1": {"id":"li1","type":"listItem","parentId":"bl","index":"a0"},
  "li1p":{"id":"li1p","type":"paragraph","parentId":"li1","index":"a0","content":[{"type":"text","text":"First item"}]},
  "li2": {"id":"li2","type":"listItem","parentId":"bl","index":"a1"},
  "li2p":{"id":"li2p","type":"paragraph","parentId":"li2","index":"a0","content":[{"type":"text","text":"Second item"}]} }
```

Task list (`attrs.checked` on the item) and blockquote:

```json
{ "tl":  {"id":"tl","type":"taskList","parentId":"PAGE","index":"a0"},
  "ti1": {"id":"ti1","type":"taskItem","parentId":"tl","index":"a0","attrs":{"checked":false}},
  "ti1p":{"id":"ti1p","type":"paragraph","parentId":"ti1","index":"a0","content":[{"type":"text","text":"Open task"}]},
  "bq":  {"id":"bq","type":"blockquote","parentId":"PAGE","index":"a1"},
  "bqp": {"id":"bqp","type":"paragraph","parentId":"bq","index":"a0","content":[{"type":"text","text":"Quoted line"}]} }
```

Table — `table → tableRow → tableHeader/tableCell → paragraph`:

```json
{ "tbl": {"id":"tbl","type":"table","parentId":"PAGE","index":"a0"},
  "r1":  {"id":"r1","type":"tableRow","parentId":"tbl","index":"a0"},
  "h1":  {"id":"h1","type":"tableHeader","parentId":"r1","index":"a0"},
  "h1p": {"id":"h1p","type":"paragraph","parentId":"h1","index":"a0","content":[{"type":"text","text":"Col A"}]},
  "r2":  {"id":"r2","type":"tableRow","parentId":"tbl","index":"a1"},
  "c1":  {"id":"c1","type":"tableCell","parentId":"r2","index":"a0"},
  "c1p": {"id":"c1p","type":"paragraph","parentId":"c1","index":"a0","content":[{"type":"text","text":"Val 1"}]} }
```

### 1. Bootstrap identity, create a page, and append the first content (recommended)

**Goal:** stand up a fresh notebook from scratch, attach a page under the auto-created Home section, give it a one-block document.

**Step 1 — bootstrap identity & create notebook.** First call to `identity.get` with `?username=&role=` auto-provisions a default notebook + Home section + Welcome page; pass it once per `username`. Then `notebooks.create` for a second, named one.

```typescript
// identity.get takes no args; auto-provision via the HTTP path with ?username=&role= query
// once on first contact, then call identity.get() in the SDK after the user/notebook exist.
const me = await client.notes.identity.get();
const nb = await client.notes.notebooks.create({ name: 'team-wiki', description: 'engineering docs' });
const nbId = nb.data!.id;
```
**Step 2 — find the auto-created Home section and add a page under it.** Every fresh notebook ships with a `section` named `Home`; `nodes.create` with `type:"page"` needs that section as `parentId`. POST returns `201` (NOT 200 — generic retry helpers that only accept 200 will treat success as failure).

```typescript
const list = await client.notes.nodes.list(nbId, { limit: 100 });
const sec = list.data!.nodes.find(n => n.type === 'section')!.id;
const page = await client.notes.nodes.create(nbId, {
  type: 'page',
  parentId: sec,
  attributes: { name: 'Runbook' },
});
const pageId = page.data!.id;
```
**Step 3 — append the first content (recommended).** `documents.appendDocument`
appends to the END of the page's document and **the server assigns each block's
`id`, `parentId`, and `index`** — so you never compute fractional indices or block
ids. Send EITHER `{text, type?}` (one block from plain text; `type` defaults to
`paragraph`) OR `{blocks:[{type, content?, attrs?}]}` (a batch of flat blocks).
Appendable types are `paragraph`, `heading1`–`heading3`, `codeBlock`,
`horizontalRule` only; containers (lists/tables) need `documents.put` (Example 3).
If the document doesn't exist yet it is created. `X-Idempotency-Key` makes retries
safe.

```typescript
// One block from plain text. The idempotency key is passed via options.XIdempotencyKey
// (appendDocument is the one generated method that exposes a per-call header).
await client.notes.documents.appendDocument(
  nbId, pageId,
  { type: 'heading1', text: 'Runbook' },
  { XIdempotencyKey: 'runbook-h1' },
);
// A batch of flat blocks:
await client.notes.documents.appendDocument(nbId, pageId, {
  blocks: [
    { type: 'paragraph', content: [{ type: 'text', text: 'Run the deploy script:' }] },
    { type: 'codeBlock', attrs: { language: 'bash' },
      content: [{ type: 'text', text: './deploy.sh prod' }] },
  ],
});
```
### 2. Build a structured document with `documents.put` — leaf blocks + a bulleted list

**Goal:** lay out a page with a header, prose, a fenced code block, and a 2-item
bulleted list, in one full-document write. Use PUT (not append) when you need
containers or precise ordering. ⚠ Two traps this example fixes: (1) use the REAL type
strings — `codeBlock` (not `code`) with the language under `attrs` (not `props`),
and a `bulletList`→`listItem`→`paragraph` nest (there is no `bullet_list_item`); a
wrong type/`props` is stored silently and renders blank. (2) `documents.patch` does
NOT merge by block id — it REPLACES the entire `blocks` map (live-verified). To add to
an existing doc, `GET` the current blocks, mutate locally, `PUT` the union back (or
just use `appendDocument`).

```typescript
const mk = () => randomBytes(12).toString('hex');
const [b1, b2, b3, bl, li1, li1p, li2, li2p] = Array.from({ length: 8 }, mk);
await client.notes.documents.put(nbId, pageId, {
  content: { type: 'rich_text', blocks: {
    [b1]: { id: b1, parentId: pageId, index: 'a0', type: 'heading1',
            content: [{ type: 'text', text: 'Deploy Steps' }] },
    [b2]: { id: b2, parentId: pageId, index: 'a1', type: 'paragraph',
            content: [{ type: 'text', text: 'Run the script below, then verify.' }] },
    [b3]: { id: b3, parentId: pageId, index: 'a2', type: 'codeBlock',
            attrs: { language: 'bash' },
            content: [{ type: 'text', text: './deploy.sh prod' }] },
    // bulleted list: bulletList -> listItem -> paragraph (text lives in the paragraph)
    [bl]:   { id: bl,   parentId: pageId, index: 'a3', type: 'bulletList' },
    [li1]:  { id: li1,  parentId: bl,  index: 'a0', type: 'listItem' },
    [li1p]: { id: li1p, parentId: li1, index: 'a0', type: 'paragraph',
              content: [{ type: 'text', text: 'Smoke-test /healthz' }] },
    [li2]:  { id: li2,  parentId: bl,  index: 'a1', type: 'listItem' },
    [li2p]: { id: li2p, parentId: li2, index: 'a0', type: 'paragraph',
              content: [{ type: 'text', text: 'Tag the release' }] },
  }},
});
```
### 3. Update one block's content + reorder by changing `index`

**Goal:** rewrite a paragraph and move it to the top of the page. Because PUT is full-overwrite, you read the current doc, mutate the target block, and write the full map back.

**Step 1 — read current blocks.**

```typescript
const doc = await client.notes.documents.get(nbId, pageId);
const blocks = (doc.data!.content as any).blocks;
```
**Step 2 — mutate locally + PUT back.** Set the target block's `index` to a key that sorts FIRST (e.g. prefix `Z` → swap to `9`, or use a fresh small string like `_a0`); rewrite its `content`.

```typescript
for (const b of Object.values(blocks) as any[]) {
  if (b.type === 'paragraph') {
    b.index = '_a0';
    b.content = [{ type: 'text', text: 'Updated intro paragraph (now first).' }];
  }
}
await client.notes.documents.put(nbId, pageId, { content: { type: 'rich_text', blocks } });
```
### 4. Delete a block + verify ordering survives

**Goal:** drop a single block from the doc. Same overwrite trick — `delete blocks[b3]` locally, PUT remaining map back, then GET to verify the survivors keep their `index` order.

```typescript
delete blocks[b3];
await client.notes.documents.put(nbId, pageId, { content: { type: 'rich_text', blocks } });
const after = await client.notes.documents.get(nbId, pageId);
const order = Object.values((after.data!.content as any).blocks)
  .sort((a: any, b: any) => a.index.localeCompare(b.index)).map((b: any) => b.type);
```
### 5. Create a database (Tasks) with typed columns + add records

**Goal:** make a database node with `text`, `number`, `boolean` fields, then create a few records. ⚠ `nodes.create` for `type:"database"` REQUIRES `attributes.fields` populated — without it the kit returns `500` (the attribute `safeParse` throws inside `YDoc.update` before the create transaction's try/catch — a `canCreate` failure would be a `403`). Each field needs `id` (matching `^[a-zA-Z0-9_-]+$`), `type`, `name`, `index`.

```typescript
const db = await client.notes.nodes.create(nbId, {
  type: 'database', parentId: sec,
  attributes: {
    name: 'Tasks',
    fields: {
      f_status:   { id: 'f_status',   type: 'text',    name: 'Status',   index: 'a0' },
      f_priority: { id: 'f_priority', type: 'number',  name: 'Priority', index: 'a1' },
      f_done:     { id: 'f_done',     type: 'boolean', name: 'Done',     index: 'a2' },
    },
  },
});
const dbId = db.data!.id;
for (let i = 1; i <= 3; i++) {
  await client.notes.databases.create(nbId, dbId, {
    name: `Task ${i}`,
    fields: {
      f_status:   { type: 'text',    value: 'todo' },
      f_priority: { type: 'number',  value: i },
      f_done:     { type: 'boolean', value: false },
    },
  });
}
```
### 6. Query records — filter + sort

**Goal:** find records with `priority > 1` sorted descending. Both `filters` and `sorts` are JSON-encoded query strings. ⚠ `filters` MUST be a **JSON array** (not an object) of `{ id, type:"field", fieldId, operator, value }`; sending an object returns `400 "filters" query parameter must be a JSON array.` Operators are field-type-specific: numbers use `is_equal_to`/`is_not_equal_to`/`is_greater_than`/`is_less_than`/`is_greater_than_or_equal_to`/`is_less_than_or_equal_to`, text uses `is_equal_to`/`is_not_equal_to`/`contains`/`does_not_contain`/`starts_with`/`ends_with`/`is_empty`/`is_not_empty`, booleans use `is_true`/`is_false`. Sort entries are `{ id, fieldId, direction:"asc"|"desc" }` (also array).

```typescript
const r = await client.notes.databases.list(nbId, dbId, {
  filters: JSON.stringify([
    { id: 'f1', type: 'field', fieldId: 'f_priority', operator: 'is_greater_than', value: 1 },
  ]),
  sorts: JSON.stringify([
    { id: 's1', fieldId: 'f_priority', direction: 'desc' },
  ]),
  count: 50,
});
```
A simpler full-text alternative is `databases.search?q=...` — no array shape, just a query string; matches against record `name` AND field values.

### 7. Update a record by id — partial-merge fields

**Goal:** mark Task 1 as done. `databases.update` PATCH MERGES `fields` (live-verified: sending only `f_status` + `f_done` left `f_priority` untouched). Each field value must be the typed wrapper `{ type: <type>, value: <v> }` matching the column type.

```typescript
const list = await client.notes.databases.list(nbId, dbId, { count: 50 });
const recordId = (list.data as any).records.find((r: any) => r.name === 'Task 1').id;
await client.notes.databases.update(nbId, dbId, recordId, {
  fields: {
    f_status: { type: 'text',    value: 'done' },
    f_done:   { type: 'boolean', value: true },
  },
});
```
### 8. Bulk import records from a CSV

**Goal:** load a list of imports into the Tasks database in a loop. There is no single-call bulk-create endpoint; loop `databases.create` per row. ⚠ Records DO NOT auto-deduplicate by `name` — re-running the same import doubles your data. If you need idempotency over HTTP/raw fetch, set the request header `X-Idempotency-Key` to a deterministic per-row key (replay returns the saved response; same key + different payload returns `409`). The **generated SDK service methods do not expose per-call headers**, so idempotency keys must be sent via raw `fetch()` (or `client.api.http.*` low-level if available).

```typescript
// Generated SDK has no per-call header hook — drop down to raw fetch for X-Idempotency-Key.
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
const rows = readFileSync('/tmp/tasks.csv', 'utf8').trim().split('\n').slice(1);
for (const row of rows) {
  const [name, pri, stat] = row.split(',');
  const key = createHash('sha256').update(`import-2026-05-07:${name}`).digest('hex');
  await fetch(`${kitUrl}/api/v1/notes/notebooks/${nbId}/databases/${dbId}/records`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Idempotency-Key': key,
    },
    body: JSON.stringify({
      name,
      fields: {
        f_priority: { type: 'number',  value: Number(pri) },
        f_status:   { type: 'text',    value: stat },
        f_done:     { type: 'boolean', value: false },
      },
    }),
  });
}
// (`client.notes.databases.create(nbId, dbId, body)` cannot carry per-call headers —
// drop to raw `fetch()` for `X-Idempotency-Key`.)
```
### 9. Export a page to HTML — single-use ticket flow

**Goal:** publish a static HTML snapshot of a page. `documents.get?output=html` requires a single-use export `ticket` (markdown via `?output=md` does NOT — it returns text directly with no ticket). Tickets default to 3 uses and expire in ~2 minutes (live-verified). Anyone with the kit URL + ticket can download until it expires.

**Step 1 — create a ticket.**

```typescript
const t = await client.notes.documents.createExportTicket(nbId, pageId, {
  output: 'html', themeMode: 'light', includeComments: 'appendix',
});
const ticket = t.data!.ticket;
```
**Step 2 — fetch the HTML.** Same kit URL; pass `ticket=` in the query.

```typescript
const html = await client.notes.documents.get(nbId, pageId, { output: 'html', ticket });
```
### 10. Tear down — delete the database, then the section (cascade), then the notebook

**Goal:** clean up everything you created. Order matters: deleting a `section` cascades to every descendant page/database/record under it (live-verified — one DELETE on the section emptied the notebook). Then `notebooks.delete` removes the notebook itself.

`notebooks.delete` returns `200` immediately after soft-deleting the notebook (flips `status` to `Inactive`); the caller must be `owner`. A background `notebook.clean` job then recursively purges child rows asynchronously — re-list via `notebooks.listNotebooks` to confirm the notebook no longer appears (the list filters out `Inactive` status).

```typescript
const recs = await client.notes.databases.list(nbId, dbId, { count: 100 });
for (const r of recs.data!.records) {
  await client.notes.databases.delete(nbId, dbId, r.id);
}
await client.notes.nodes.delete(nbId, dbId);
await client.notes.nodes.delete(nbId, sec);
try { await client.notes.notebooks.delete(nbId); }
catch { await client.notes.notebooks.update(nbId, { name: 'team-wiki-DELETED' }); }
```

## Reference

**Accessor:** `client.notes`  |  **Import:** `import * as notes from 'hoody-sdk/notes'`

### `client.notes.avatars` (2) — avatars

#### `download` — Download an avatar image

```typescript
client.notes.avatars.download(avatarId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `avatarId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/avatars/{avatarId}`

---

#### `upload` — Upload an avatar image

```typescript
client.notes.avatars.upload()
```

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/avatars`

---

### `client.notes.collaborators` (4) — collaborators

#### `add` — Add a collaborator

```typescript
client.notes.collaborators.add(notebookId: string, nodeId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ collaboratorId*: string, role*: "admin" | "editor" | "collaborator" | "viewer" }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators`
**CLI:** `hoody notes collab add`

---

#### `list` — List collaborators

```typescript
client.notes.collaborators.list(notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators`
**CLI:** `hoody notes collab list`

---

#### `remove` — Remove a collaborator

```typescript
client.notes.collaborators.remove(notebookId: string, nodeId: string, collaboratorId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `collaboratorId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators/{collaboratorId}`
**CLI:** `hoody notes collab remove`

---

#### `update` — Update collaborator role

```typescript
client.notes.collaborators.update(notebookId: string, nodeId: string, collaboratorId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `collaboratorId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ role*: "admin" | "editor" | "collaborator" | "viewer" }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators/{collaboratorId}`
**CLI:** `hoody notes collab update`

---

### `client.notes.comments` (7) — comments

#### `create` — Create a comment

```typescript
client.notes.comments.create(notebookId: string, nodeId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ content*: string, parentId: string, anchorBlockId: string, anchor: object }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments`
**CLI:** `hoody notes comment create`

---

#### `delete` — Delete a comment

```typescript
client.notes.comments.delete(expectedVersion?: integer, notebookId: string, nodeId: string, commentId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `expectedVersion` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `commentId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}`
**CLI:** `hoody notes comment delete`

---

#### `edit` — Edit a comment

```typescript
client.notes.comments.edit(notebookId: string, nodeId: string, commentId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `commentId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ content*: string, expectedVersion: int }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}`
**CLI:** `hoody notes comment edit`

---

#### `list` — List comments

```typescript
client.notes.comments.list(limit?: integer, offset?: integer, cursor?: string, notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `cursor` | `string` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments`
**CLI:** `hoody notes comment list`

---

#### `listAnchors` — List comment anchors

```typescript
client.notes.comments.listAnchors(limit?: integer, offset?: integer, cursor?: string, notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `cursor` | `string` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comment-anchors`
**CLI:** `hoody notes comment anchors`

---

#### `reanchor` — Re-anchor a comment thread

```typescript
client.notes.comments.reanchor(notebookId: string, nodeId: string, commentId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `commentId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ anchor*: object, expectedVersion: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/reanchor`

---

#### `resolve` — Resolve a comment

```typescript
client.notes.comments.resolve(notebookId: string, nodeId: string, commentId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `commentId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ expectedVersion: int }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/resolve`
**CLI:** `hoody notes comment resolve`

---

### `client.notes.databases` (8) — databases

#### `create` — Create a database record

```typescript
client.notes.databases.create(notebookId: string, databaseId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `databaseId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ id: string, name: string="Untitled", avatar: string | null, fields: { [key: string]: any } }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records`
**CLI:** `hoody notes db create`

---

#### `delete` — Delete a database record

```typescript
client.notes.databases.delete(notebookId: string, databaseId: string, recordId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `databaseId` | `string` | path | Yes |  |
| `recordId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/{recordId}`
**CLI:** `hoody notes db delete`

---

#### `get` — Get a database record

```typescript
client.notes.databases.get(notebookId: string, databaseId: string, recordId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `databaseId` | `string` | path | Yes |  |
| `recordId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/{recordId}`
**CLI:** `hoody notes db get`

---

#### `list` — List database records

```typescript
client.notes.databases.list(filters?: string, sorts?: string, page?: integer, count?: integer, notebookId: string, databaseId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `filters` | `string` | query | No |  |
| `sorts` | `string` | query | No |  |
| `page` | `integer` | query | No |  |
| `count` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `databaseId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records`
**CLI:** `hoody notes db list`

---

#### `listAll` — List database records (collect all pages)

```typescript
client.notes.databases.listAll(filters?: string, sorts?: string, page?: integer, count?: integer, notebookId: string, databaseId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `filters` | `string` | query | No |  |
| `sorts` | `string` | query | No |  |
| `page` | `integer` | query | No |  |
| `count` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `databaseId` | `string` | path | Yes |  |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records`
**CLI:** `hoody notes db list`

---

#### `listIterator` — List database records (async iterator)

```typescript
client.notes.databases.listIterator(filters?: string, sorts?: string, page?: integer, count?: integer, notebookId: string, databaseId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `filters` | `string` | query | No |  |
| `sorts` | `string` | query | No |  |
| `page` | `integer` | query | No |  |
| `count` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `databaseId` | `string` | path | Yes |  |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records`
**CLI:** `hoody notes db list`

---

#### `search` — Search database records

```typescript
client.notes.databases.search(q?: string, exclude?: string, notebookId: string, databaseId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `q` | `string` | query | No |  |
| `exclude` | `string` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `databaseId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/search`
**CLI:** `hoody notes db search`

---

#### `update` — Update a database record

```typescript
client.notes.databases.update(notebookId: string, databaseId: string, recordId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `databaseId` | `string` | path | Yes |  |
| `recordId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ name: string, avatar: string | null, fields: { [key: string]: object } }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/{recordId}`
**CLI:** `hoody notes db update`

---

### `client.notes.documents` (6) — documents

#### `appendDocument` — Append blocks to a document

```typescript
client.notes.documents.appendDocument(notebookId: string, nodeId: string, X-Idempotency-Key?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `X-Idempotency-Key` | `string` | header | No | Optional idempotency key (max 256 chars). Reusing the same key with an identical request body and node replays the original response; reusing it with a different body or node returns 409. |
| `data` | `object` | body | Yes |  |

**Body:** `{ text*: string, type: "paragraph" | "heading1" | "heading2" | "heading3" | "codeBlock"="paragraph", attrs: object | null } | { blocks*: object[] }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document/append`

---

#### `createExportTicket` — Create secure HTML export ticket

```typescript
client.notes.documents.createExportTicket(notebookId: string, nodeId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ output: "html"="html", includeComments: "none" | "appendix"="none", includeBackground: bool=true, themeMode: "light" | "dark"="dark", themeId: string | null, themeVariables: { [key: string]: string }, fileName: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/export-ticket`

---

#### `exportBlockSvg` — Export drawing block as SVG

```typescript
client.notes.documents.exportBlockSvg(bg?: string, scale?: number, notebookId: string, nodeId: string, blockId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `bg` | `string` | query | No |  |
| `scale` | `number` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `blockId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/blocks/{blockId}/svg`

---

#### `get` — Get document content

```typescript
client.notes.documents.get(blockIds?: string, lines?: string, output?: string, includeComments?: string, ticket?: string, notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `blockIds` | `string` | query | No |  |
| `lines` | `string` | query | No |  |
| `output` | `string` | query | No |  |
| `includeComments` | `string` | query | No |  |
| `ticket` | `string` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document`
**CLI:** `hoody notes doc get`

---

#### `patch` — Merge document content

```typescript
client.notes.documents.patch(notebookId: string, nodeId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ content*: { [key: string]: any } }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document`
**CLI:** `hoody notes doc patch`

---

#### `put` — Create or replace document

```typescript
client.notes.documents.put(notebookId: string, nodeId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ content*: { [key: string]: any } }`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document`
**CLI:** `hoody notes doc put`

---

### `client.notes.files` (8) — files

#### `download` — Download a file

```typescript
client.notes.files.download(fileId: string, notebookId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `fileId` | `string` | path | Yes |  |
| `notebookId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/files/{fileId}`
**CLI:** `hoody notes file download`

---

#### `list` — List all uploaded files

```typescript
client.notes.files.list(limit?: integer, offset?: integer, notebookId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/files`
**CLI:** `hoody notes file list`

---

#### `listAll` — List all uploaded files (collect all pages)

```typescript
client.notes.files.listAll(limit?: integer, offset?: integer, notebookId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/files`
**CLI:** `hoody notes file list`

---

#### `listIterator` — List all uploaded files (async iterator)

```typescript
client.notes.files.listIterator(limit?: integer, offset?: integer, notebookId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/files`
**CLI:** `hoody notes file list`

---

#### `tusAbortUpload` — Abort a TUS upload

```typescript
client.notes.files.tusAbortUpload(notebookId: string, fileId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `fileId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus`

---

#### `tusCheckUpload` — Check a TUS upload's offset (for resuming)

```typescript
client.notes.files.tusCheckUpload(notebookId: string, fileId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `fileId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `HEAD /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus`

---

#### `tusCreateUpload` — Create a resumable (TUS) upload

```typescript
client.notes.files.tusCreateUpload(notebookId: string, fileId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `fileId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus`

---

#### `tusUploadChunk` — Upload a chunk to a TUS upload

```typescript
client.notes.files.tusUploadChunk(notebookId: string, fileId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `fileId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus`

---

### `client.notes.health` (1) — health

#### `check` — Service health and runtime info

```typescript
client.notes.health.check()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/health`

---

### `client.notes.identity` (1) — identity

#### `get` — Get current identity

```typescript
client.notes.identity.get()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/me`
**CLI:** `hoody notes whoami`

---

### `client.notes.interactions` (2) — interactions

#### `markOpened` — Mark node as opened

```typescript
client.notes.interactions.markOpened(notebookId: string, nodeId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ openedAt: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/interactions/opened`

---

#### `markSeen` — Mark node as seen

```typescript
client.notes.interactions.markSeen(notebookId: string, nodeId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ seenAt: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/interactions/seen`

---

### `client.notes.mutations` (1) — mutations

#### `sync` — Sync client mutations

```typescript
client.notes.mutations.sync(notebookId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ mutations*: object[] }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/mutations`

---

### `client.notes.nodes` (7) — nodes

#### `create` — Create a node

```typescript
client.notes.nodes.create(notebookId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ id: string, type*: string, parentId: string, attributes*: { [key: string]: any } }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes`
**CLI:** `hoody notes node create`

---

#### `delete` — Delete a node

```typescript
client.notes.nodes.delete(notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}`
**CLI:** `hoody notes node delete`

---

#### `get` — Get a node

```typescript
client.notes.nodes.get(notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}`
**CLI:** `hoody notes node get`

---

#### `getByAlias` — Resolve page by alias

```typescript
client.notes.nodes.getByAlias(notebookId: string, alias: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `alias` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/alias/{alias}`
**CLI:** `hoody notes node get-by-alias`

---

#### `list` — List nodes

```typescript
client.notes.nodes.list(type?: string, parentId?: string, rootId?: string, limit?: integer, offset?: integer, notebookId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `type` | `string` | query | No |  |
| `parentId` | `string` | query | No |  |
| `rootId` | `string` | query | No |  |
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes`
**CLI:** `hoody notes node list`

---

#### `listChildren` — List child nodes

```typescript
client.notes.nodes.listChildren(limit?: integer, offset?: integer, notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/children`
**CLI:** `hoody notes node children`

---

#### `update` — Update a node

```typescript
client.notes.nodes.update(notebookId: string, nodeId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ attributes*: { [key: string]: any } }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}`
**CLI:** `hoody notes node update`

---

### `client.notes.notebooks` (5) — notebooks

#### `create` — Create a notebook

```typescript
client.notes.notebooks.create(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ name*: string, description: string | null, avatar: string | null }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks`
**CLI:** `hoody notes notebook create`

---

#### `delete` — Delete a notebook

```typescript
client.notes.notebooks.delete(notebookId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/notes/notebooks/{notebookId}`
**CLI:** `hoody notes notebook delete`

---

#### `get` — Get notebook details

```typescript
client.notes.notebooks.get(notebookId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}`
**CLI:** `hoody notes notebook get`

---

#### `listNotebooks` — List notebooks

```typescript
client.notes.notebooks.listNotebooks()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks`
**CLI:** `hoody notes notebook list`

---

#### `update` — Update notebook settings

```typescript
client.notes.notebooks.update(notebookId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ name*: string, description: string | null, avatar: string | null }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/notes/notebooks/{notebookId}`
**CLI:** `hoody notes notebook update`

---

### `client.notes.reactions` (3) — reactions

#### `add` — Add a reaction

```typescript
client.notes.reactions.add(notebookId: string, nodeId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ reaction*: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/reactions`
**CLI:** `hoody notes reaction add`

---

#### `list` — List reactions

```typescript
client.notes.reactions.list(notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/reactions`
**CLI:** `hoody notes reaction list`

---

#### `remove` — Remove a reaction

```typescript
client.notes.reactions.remove(notebookId: string, nodeId: string, reaction: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `reaction` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/reactions/{reaction}`
**CLI:** `hoody notes reaction remove`

---

### `client.notes.sockets` (2) — sockets

#### `init` — Initialize a WebSocket session

```typescript
client.notes.sockets.init()
```

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/sockets`

---

#### `open` — Open a WebSocket connection

```typescript
client.notes.sockets.open(socketId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `socketId` | `string` | path | Yes |  |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/notes/sockets/{socketId}`

---

### `client.notes.users` (2) — users

#### `invite` — Invite users to notebook

```typescript
client.notes.users.invite(notebookId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ users*: ({ username*: string, role*: "owner" | "admin" | "collaborator" | "guest" | "none" })[] }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/users`

---

#### `updateRole` — Update user role

```typescript
client.notes.users.updateRole(notebookId: string, userId: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `userId` | `string` | path | Yes |  |
| `data` | `object` | body | Yes |  |

**Body:** `{ role*: "owner" | "admin" | "collaborator" | "guest" | "none" }`

**Returns:** `any`  |  **HTTP:** `PATCH /api/v1/notes/notebooks/{notebookId}/users/{userId}/role`
**CLI:** `hoody notes user set-role`

---

### `client.notes.versions` (5) — versions

#### `create` — Create a document version snapshot

```typescript
client.notes.versions.create(notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions`
**CLI:** `hoody notes version create`

---

#### `delete` — Delete a document version

```typescript
client.notes.versions.delete(notebookId: string, nodeId: string, versionId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `versionId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions/{versionId}`
**CLI:** `hoody notes version delete`

---

#### `get` — Get a specific document version

```typescript
client.notes.versions.get(notebookId: string, nodeId: string, versionId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `versionId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions/{versionId}`
**CLI:** `hoody notes version get`

---

#### `list` — List document versions

```typescript
client.notes.versions.list(limit?: integer, offset?: integer, notebookId: string, nodeId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions`
**CLI:** `hoody notes version list`

---

#### `restore` — Restore a document version

```typescript
client.notes.versions.restore(notebookId: string, nodeId: string, versionId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `notebookId` | `string` | path | Yes |  |
| `nodeId` | `string` | path | Yes |  |
| `versionId` | `string` | path | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions/{versionId}/restore`
**CLI:** `hoody notes version restore`


---

<!-- ===== namespace: notifications ===== -->

# `notifications` — Trigger and consume desktop notifications inside a container

## Purpose

Two jobs in one namespace. First and most useful: **remotely inform the human operator** — an agent fires a notification from inside a container and it pops on the user's phone, desktop, or smartwatch even when they're away from the session. Second: drive and read the container's own desktop (`notify-send`) toasts on an X display. The user opens the kit's web page once (see § Capability URL), grants browser-notification permission, and leaves it backgrounded; from then on every notification the agent fires is delivered to them as a real OS notification, and is also queryable and streamable over the HTTP API.

## When to use

- **Tell the human something happened while they're away** — "build finished", "needs your input", "deploy failed" — and have it reach their phone/desktop/smartwatch via the backgrounded web page. This is the supported way for an agent to reach its operator out-of-band.
- Surface progress or alerts from a long-running agent task as desktop toasts on a container display (`:1`, `:2`, …).
- Pull the notification log after a task; subscribe (WS/SSE) to react to new entries in real time.
- Dismiss handled entries (or restore them); fetch a notification's icon.

## When NOT to use

- Status you'll read yourself in the same session → just read the command output (→ see `terminal` / `exec`); a notification is for reaching a human who isn't watching.
- Account-level inbox, email, SMS, or verification mail → see `api` (the control-plane account inbox, unrelated to this kit).
- Cross-process or agent-to-agent message passing with no human and no display → see `pipe`.
- Reacting to file changes rather than pushing a message to someone → see `watch`.
- Managing the X display or windows themselves → see `display`.

## Prerequisites

- Target display exists.
- Required: `display`+`summary` on `trigger`; `display` on `list`; `displays` on `connectStream`.

## Capability URL

Kit slug is `n` (not `notifications`): `https://{P}-{C}-n-1.{N}.containers.hoody.com`. The HTTP API lives under `/api/v1/notifications/...`. **The root of that URL is a user-facing web page**: open `https://{P}-{C}-n-1.{N}.containers.hoody.com/?displays=all` in any browser and it requests notification permission, then turns every streamed entry into a real OS notification (works backgrounded; auto-reconnects, polling fallback). The hostname itself is the credential, so no token or header goes in the URL — hand the human that exact URL with `{P}`/`{C}`/`{N}` filled in from `containers.get`. → See `SKILL-SDK.md § Proxy URLs` for the slug table and capability-token rules.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Fire a notification

`notify.trigger` body: `display`+`summary` (req); optional `body`, `urgency`, `icon`, `category`, `expire_time`. CLI: `hoody notifications trigger --display :0 --summary "Build done"`.

### 2. Read recent notifications

`list` — `display`: `":0"`, `"0"`, `"0,:1,2"`, or `"all"`. Optional `limit` (1–1000, default 100), `since` (ms), `username`, `session`. **`after_id` is supported by the kit HTTP route but is NOT in the generated SDK options** (`notifications.service.generated.ts:340-422`); use raw HTTP/curl when you need cursor pagination. The SDK additionally exposes `listAll` (one-shot fetch-all) and `listIterator` (async page iterator) on top of plain list; the CLI exposes only a single `list` subcommand under `hoody notifications`. CLI: see CLI-broken caveat in Quirks.

### 3. Subscribe to events

`connectStream` — `displays`: `"all"`, `"*"`, or 1–3-digit IDs comma list. WS if `Upgrade`; else SSE (`connected`, 15 s `heartbeat`, `notification`). WS sends `{"type":"subscribe"|"unsubscribe","displays":[...]}`.

### 4. Dismiss / restore

`list` → int `id`s → `dismiss` POST `{"notificationIds":[<int>,…],"displayId":":0"}`. `clearDismissed` DELETE (optional `displayId`) restores. CLI: `hoody notifications dismiss --display-id 1 --notification-ids 12,13`.

### 5. Fetch an icon

`list` → `icon` → `icons.get` by `iconId`. Honours `If-None-Match`/`If-Modified-Since`.

### 6. Remotely notify the human operator

Reach a human who isn't watching the session — on their phone, desktop, or smartwatch. One-time human setup: they open the kit web page with `?displays=all` (see § Capability URL), grant browser-notification permission, and leave the tab backgrounded. Agent side: `notify.trigger` with a `display` (any real display number — the kit auto-ensures it, so you do NOT have to set up X first), a `summary`, and optional `body`/`urgency`. The kit records the entry and broadcasts it on its stream; the operator's page renders it as an OS notification. Full copy-paste recipe in Example 11.

## Quirks & gotchas

- Kit slug is `n`, not `notifications`.
- `dismiss.notificationIds` MUST be integers (strings → `400`); `displayId` strips leading `:`.
- `trigger` limits: `summary` ≤200, `body` ≤1000, `category` ≤50, `expire_time` 0–300000; `urgency` ∈ `low|normal|critical`.
- `list.display` numeric or `"all"`; `connectStream.displays` accepts 1–5-digit IDs (`20001` is valid; 6+ digits rejected) (`"1000"` rejected).
- `list.limit` `[1,1000]` def 100; cursor `after_id`; `username`/`session` 1–100 ASCII alnum.
- `iconId` ext whitelist `jpg|jpeg|png|webp|avif|gif|bmp`; traversal rejected.
- WS only with `Upgrade`; else SSE+15 s heartbeat. WS: per-IP caps, origin allow-list, drops after 2 missed pongs.
- `clearDismissed`=DELETE, `dismiss`=POST, same path.
- **There are two distinct `notifications` surfaces; this namespace is the kit one.** This file documents the per-container kit (`hoody-notifications`, kit slug `n`) — `/api/v1/notifications/{display}`, `notify-send`, icons, WS/SSE stream. The control-plane *account inbox* lives at `client.api.notifications.*` (`GET /api/v1/notifications/`, `PUT /:id/read`, `read-all`) and is unrelated.
- The CLI uses `namespace: 'notifications'`, which routes through `normalizeKitProgram` to the kit slug `n` and builds `https://{P}-{C}-n-{N}.{server}.containers.hoody.com/api/v1/notifications/...` — `hoody --container <C> notifications {list|dismiss|icon|trigger}` reaches the kit correctly.
- `notifications stream` mapping has no `cli_stream` flag — the generated CLI buffers SSE events forever instead of streaming. Use SDK `connectStream` (returns a WebSocket wrapper, not void) or hit `/api/v1/notifications/stream` directly with `EventSource`/`fetch` for live feeds.
- `connectStream` returns a `Promise<NotificationsConnectNotificationStreamWebSocket>` wrapper. Wire callbacks first (`wrapper.onNotification(cb)` / `onHeartbeat(cb)` / `onDisconnect(cb)` / `onError(cb)`), then `await wrapper.connect()`. Close with `wrapper.close()`. There is NO `onMessage`/`onClose` — those names are wrong. `displays` is typed optional in the TS signature but is required at runtime — the SDK throws `ValidationError('displays is required')` if omitted, so always pass it.
- The kit serves a **browser client at `/`** (and at the `/api/v1/notifications` alias): it connects to the stream and raises a browser `Notification` per entry, with a polling fallback and auto-reconnect. Mount it as `?displays=all` to catch every display. This is the supported path for delivering an agent's notifications to a human's device; no token goes in the URL (the hostname is the capability).
- `notify.trigger` does NOT require you to pre-create an X display: when display-ensure is enabled (the kit default), the kit runs `hoody-terminal create --terminal-id N --display-id N --wait-for-display --wait-timeout S` (where `S` is the display-ensure timeout, default 30 s; results are cached 60 s) to bring the target display up before calling `notify-send`, so firing to e.g. `:1` works on demand. It still returns `500 "No D-Bus session available"` if the display genuinely can't be brought up — pick another display instead of retrying the same one.

## Common errors

- `400 "Notification dispatch failed"` w/ details; non-`Invalid` → `500`.
- WS `1008` origin-deny; `1001` heartbeat timeout. `429` on `trigger` / `icons.get`; both are enforced by the shared per-IP rate-limit middleware.
- `/health` 200 ≠ authorised endpoints reachable.

## Related namespaces

- `display`, `api` (account inbox), `pipe`, `exec`, `watch`.

## Examples

Every step in every example was live-tested against a real `n-1` kit (kit slug is `n`, NOT `notifications` — the long form returns DNS NXDOMAIN). Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first, and tag your test traffic with a distinctive `category` like `sdk-doc-*` so cleanup can find it.

### 1. Fire a notification on a display + read it back

**Goal:** post a toast on display `:2`, then confirm the kit recorded it. ⚠ Display `:0` typically has no D-Bus session in headless containers (`500 "No D-Bus session available for display :0"`); use a display that an X session is actually attached to (`:2` and `3` were live on the test container).

**Step 1 — trigger.** Body is `application/json`; `display` + `summary` are required, the rest are optional. The kit responds `{"success":true,"message":"Notification sent successfully"}` — note: NO `id` is returned here, so step 2 has to recover the per-display id by listing.

```typescript
await client.notifications.notify.trigger({
  display: '2',
  summary: 'Build done',
  body: 'v1.4.2 deployed',
  urgency: 'normal',
  category: 'sdk-doc-build',
});
```
**Step 2 — read it back.** `id` in the response is **per-display**, not global; the dismiss compound key is `(displayId, id)`. Capture both.

```typescript
const list = await client.notifications.list('2', { limit: 10 });
const mine = (list.data as any).notifications.find((n: any) => n.category === 'sdk-doc-build');
const { id, display_id } = mine; // id is per-display
```
### 2. Fan out the same notification to multiple displays

**Goal:** ship one alert to every X session attached to the container. The trigger endpoint takes ONE display per call — you fire N times, the response carries no id, and per-display id sequences are independent (display `:2` and `:3` each have their own `id=1`, `id=2`…).

```typescript
const displays = ['2', '3'];
await Promise.all(displays.map(d => client.notifications.notify.trigger({
  display: d, summary: 'Maintenance in 5m', urgency: 'critical', category: 'sdk-doc-fanout',
})));
const r = await client.notifications.list('2,3', { limit: 20 });
const mine = (r.data as any).notifications.filter((n: any) => n.category === 'sdk-doc-fanout');
```
### 3. Subscribe to the live SSE stream and react to new notifications

**Goal:** keep a long-lived consumer that processes every new notification as it arrives. Default is SSE (no `Upgrade` header); WebSocket activates only with an `Upgrade: websocket` request. Frames: `connected` (once), `heartbeat` (every 15 s), `notification` (on new entry).

```typescript
const stream = await client.notifications.connectStream({ displays: 'all' });
stream.onNotification((msg) => console.log('new:', msg.data));
stream.onHeartbeat(() => { /* keepalive every 15s */ });
stream.onDisconnect((code, reason) => { /* socket closed: code=${code} reason=${reason} */ });
stream.onError((err) => console.error(err));
await stream.connect();
// later:
stream.close();
```
### 4. Dismiss a list of notifications, scoped to one display

**Goal:** after handling a batch of toasts in your UI, hide them on display `:2` without affecting display `:3`. ⚠ `notificationIds` MUST be **integers** — strings return `400 "notificationIds must contain valid integer IDs"`. ⚠ Scoped dismiss (with `displayId`) hides the entries from `GET /:displayId` but they remain visible from `GET /all`; for cross-display hide, omit `displayId` (see example 5).

```typescript
const r = await client.notifications.list('2', { limit: 50 });
const ids = (r.data as any).notifications.map((n: any) => n.id); // numbers, NOT strings
await client.notifications.dismiss({ notificationIds: ids, displayId: '2' });
```
### 5. Restore everything you just dismissed

**Goal:** undo step 4 — bring dismissed items back into the listing. `clearDismissed` is `DELETE /dismiss` (same path as POST `dismiss`); pass `displayId` to scope, or omit it for global restore.

```typescript
// SDK signature is clearDismissed(options?: { displayId?: string; ... })
await client.notifications.clearDismissed({ displayId: '2' });   // scoped
await client.notifications.clearDismissed();                     // global
```
### 6. Fetch a notification icon by id with revalidation

**Goal:** download the icon a notification carried, then revalidate cheaply via `If-None-Match`. `iconId` looks like `6_10_1749024932903.png`; the kit rejects unknown extensions and any path traversal. Unknown/unresolvable `iconId` returns `400` (`{"error":"Icon not found"}` / `Icon not found or path invalid`); an unsupported extension or path traversal returns `400 "Icon ID is invalid or has an unsupported extension."`, and an existing-but-unreadable icon returns `500` — NOT 404, despite the openapi.yaml documenting a 404 for this case.

```typescript
const r = await client.notifications.list('2', { limit: 10 });
const withIcon = (r.data as any).notifications.find((n: any) => n.has_icon);
const iconId = withIcon.icon_url.split('/').pop();
const r2 = await client.notifications.icons.get(iconId);
const blob = r2.data; // icons.get returns ApiResponse<unknown>; the bytes live under .data
```
### 7. Filter a listing by display, time window, and cursor

**Goal:** "give me everything new on display `:2` since the last poll, with a stable cursor for the next call." `since` is **Unix milliseconds**, `after_id` is the per-display integer cursor (HTTP only — neither the SDK nor the CLI exposes `after_id`). `display_id` in responses is a `DisplayIdValue` — either a number or a string depending on the entry source — but the path/CLI accepts `"2"`, `":2"`, or even `"all"`.

```typescript
const since = Date.now() - 60_000;
// `after_id` is NOT exposed on the generated SDK list options; for cursor pagination
// drop down to raw fetch (see HTTP example above) or use listIterator/listAll.
const r = await client.notifications.list('2', { limit: 100, since });
const items = (r.data as any).notifications;
const lastId = items.length ? Math.max(...items.map((n: any) => n.id)) : null;
```
### 8. Page through history — newest 50, then walk backwards

**Goal:** the listing default is 100 newest; you want clean pagination. The kit hands back items already in newest-first order. There's no `before_id` flag — you walk by re-querying with `after_id` for forward catch-up, or take page slices client-side. The SDK adds two helpers on top of plain list: a one-shot fetch-all and a streaming iterator (both shown in the SDK example below).

```typescript
// AsyncIterableIterator — pulls every page transparently
for await (const n of client.notifications.listIterator('all', { limit: 50 } as any)) {
  if ((n as any).category === 'sdk-doc-fanout') process(n);
}
// Or one-shot fetch-all:
const all = await client.notifications.listAll('all', { limit: 50 } as any);
```
### 9. Audit notifications by username / session

**Goal:** "show me everything user `alex` saw in session `sessabc`." `username` and `session` are filter query params on `list`; pure ASCII alnum 1–100 chars — hyphens/underscores are rejected with `400` (e.g. `sess-abc` would fail validation). ⚠ Unknown values DO NOT 404 — the kit returns `count` of whatever it has from the wider context (live-confirmed: `username=root` returned non-empty even though notifications never had that field set), so always combine with `display`+`since` to keep the result tight.

```typescript
const r = await client.notifications.list('all', {
  limit: 200,
  since: Date.now() - 86_400_000,
  username: 'alex',
  session: 'sessabc',
} as any);
```
### 10. Survive a 429 rate-limit burst on `notify.trigger`

**Goal:** you're shipping a flood of toasts (CI, monitoring, …) and the kit pushes back with `429 Too Many Requests`. The kit per-IP rate-limits both `notify.trigger` and `icons.get`. Strategy: cap concurrency client-side, exponential-backoff on `429`, and never retry on `400` (validation — fix the body instead). On `500 "No D-Bus session available"`, the display has no live X session — pick another display, don't retry the same one.

```typescript
async function triggerWithBackoff(req: any, max = 5) {
  let delay = 500;
  for (let i = 0; i < max; i++) {
    try { return await client.notifications.notify.trigger(req); }
    catch (e: any) {
      if (e?.status === 429) { await new Promise(r => setTimeout(r, delay)); delay *= 2; continue; }
      throw e; // 400 / 500 — don't retry blindly
    }
  }
  throw new Error('rate-limited after retries');
}
```
### 11. Notify a human on their phone / desktop / watch (remote operator alert)

**Goal:** the agent finished something — or needs input — and the human isn't watching the session. Deliver a real OS notification to whatever device they left the page open on. Two parts: a one-time human action (open the page, allow notifications) and the agent firing the alert.

**One-time, human side** — open this URL in a browser, click "Allow" when prompted, and leave the tab in the background (phone, desktop, or a browser that mirrors notifications to a smartwatch). `{P}`/`{C}`/`{N}` come from `containers.get`; `?displays=all` catches notifications fired on any display:

```
https://{P}-{C}-n-1.{N}.containers.hoody.com/?displays=all
```

The page asks for notification permission on first load, reconnects on its own, and falls back to polling if WebSocket is blocked. No token goes in the URL — the hostname is the credential.

**Agent side** — fire the alert. Any real display number works; the kit auto-ensures it, so you don't need to set up X first.

```typescript
await client.notifications.notify.trigger({
  display: '1',
  summary: 'Build finished',
  body: 'v1.4.2 is deployed — review when you can',
  urgency: 'normal',
});
```

## Reference

**Accessor:** `client.notifications`  |  **Import:** `import * as notifications from 'hoody-sdk/notifications'`

### `client.notifications.health` (2) — Server health check

#### `check` — Service health check

```typescript
client.notifications.health.check()
```

**Returns:** `notifications_HealthResponse`  |  **HTTP:** `GET /api/v1/notifications/health`
**CLI:** `hoody notifications health`

---

#### `getMetrics` — Prometheus-compatible metrics endpoint

```typescript
client.notifications.health.getMetrics()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notifications/metrics`
**CLI:** `hoody notifications metrics`

---

### `client.notifications.icons` (1) — Serve notification icons

#### `get` — Get notification icon

```typescript
client.notifications.icons.get(iconId: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `iconId` | `string` | path | Yes | The unique identifier for the icon (e.g., "6_10_1749024932903.png") |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notifications/icons/{iconId}`
**CLI:** `hoody notifications icon`

---

### `client.notifications` (6) — Retrieve historical notifications

#### `clearDismissed` — Clear dismissed notifications

```typescript
client.notifications.clearDismissed(displayId?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displayId` | `string` | query | No | Optional display ID to scope the clear operation |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/notifications/dismiss`
**CLI:** `hoody notifications clear-dismissed`

---

#### `connectStream` — Real-time notification stream via WebSocket

```typescript
client.notifications.connectStream(displays: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `displays` | `string` | query | Yes | Comma-separated display IDs to subscribe to (e.g., "1,:2,3"), or "all" to receive notifications from every display. |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/notifications/stream`
**CLI:** `hoody notifications stream`

---

#### `dismiss` — Dismiss notifications

```typescript
client.notifications.dismiss(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ displayId: string, notificationIds*: int[] }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notifications/dismiss`
**CLI:** `hoody notifications dismiss`

---

#### `list` — Get notifications for specified display(s)

```typescript
client.notifications.list(display: string, limit?: integer, since?: integer, username?: string, session?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `display` | `string` | path | Yes | A single display ID (e.g., "1" or ":1"), a comma-separated list (e.g., "1,:2,3"), or "all" to fetch from all displays |
| `limit` | `integer` | query | No | Maximum number of notifications to return |
| `since` | `integer` | query | No | Unix timestamp in milliseconds to get notifications after this time |
| `username` | `string` | query | No | Filter notifications by username |
| `session` | `string` | query | No | Filter notifications by session ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/notifications/{display}`
**CLI:** `hoody notifications list`

---

#### `listAll` — Get notifications for specified display(s) (collect all pages)

```typescript
client.notifications.listAll(display: string, limit?: integer, since?: integer, username?: string, session?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `display` | `string` | path | Yes | A single display ID (e.g., "1" or ":1"), a comma-separated list (e.g., "1,:2,3"), or "all" to fetch from all displays |
| `limit` | `integer` | query | No | Maximum number of notifications to return |
| `since` | `integer` | query | No | Unix timestamp in milliseconds to get notifications after this time |
| `username` | `string` | query | No | Filter notifications by username |
| `session` | `string` | query | No | Filter notifications by session ID |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/notifications/{display}`
**CLI:** `hoody notifications list`

---

#### `listIterator` — Get notifications for specified display(s) (async iterator)

```typescript
client.notifications.listIterator(display: string, limit?: integer, since?: integer, username?: string, session?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `display` | `string` | path | Yes | A single display ID (e.g., "1" or ":1"), a comma-separated list (e.g., "1,:2,3"), or "all" to fetch from all displays |
| `limit` | `integer` | query | No | Maximum number of notifications to return |
| `since` | `integer` | query | No | Unix timestamp in milliseconds to get notifications after this time |
| `username` | `string` | query | No | Filter notifications by username |
| `session` | `string` | query | No | Filter notifications by session ID |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/notifications/{display}`
**CLI:** `hoody notifications list`

---

### `client.notifications.notify` (1) — Trigger new notifications

#### `trigger` — Trigger a new desktop notification

```typescript
client.notifications.notify.trigger(data: notifications_NotifyRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `notifications_NotifyRequest` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/notifications/notify`
**CLI:** `hoody notifications trigger`


### Body schemas

- `notifications_NotifyRequest` — `{ body: string, category: string, display*: string, expire_time: int, icon: string, summary*: string, urgency: "low" | "normal" | "critical"="normal" }`


---

<!-- ===== namespace: pipe ===== -->

# `pipe` — Zero-storage streaming HTTP transfers

## Purpose

HTTP rendezvous. Sender POST/PUTs a path; receivers GET it; bytes fan out
in-memory, zero server storage. Paths exist only while pending/active.

## When to use

- Endpoint-to-endpoint bytes without staging.
- Fan-out (`?n=<count>`, N ≤ 256).
- Live video via `?video`.
- Telemetry via `?progress`.
- Browser upload UI via `/noscript` or `/`.

## When NOT to use

Persist → `files`, HTTP client → `curl`, shell → `terminal`; no replay/queue (no storage).

## Prerequisites

- All peers share kit URL, `{path}`, `n`.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

Drive via SDK (`client.pipe.send` / `client.pipe.receive`), `curl` against the kit URL, or the hand-written `hoody pipe` CLI surface (subcommands `send`, `receive`, `progress`, `url`, `forward-tcp`, `health`, `help-cheatsheet` — a hand-written CLI surface, NOT part of the auto-generated command mapping).

### 1. One-to-one

`pipe.receive` with `path` (blocks); `pipe.send` same `path`. `n=1`.

### 2. Fan-out (N ≤ 256)

All peers call with same `path` + `n=<N>`. Mismatch → 400.

### 3. Download / inline per receiver

`pipe.receive` with `download=1` and/or `filename=<n>` → attachment; `download=0`
→ inline.

### 4. Watch via `?progress`

`pipe.receive` with `progress=1`. `Accept: text/event-stream` → SSE; `text/html`
→ dashboard. No receiver slot.

### 5. Video via `?video`

`pipe.receive` with `video=1` + `Accept: text/html` → MSE player; non-browser →
raw bytes. Sender: WebM / fMP4 / MPEG-TS.

## Quirks & gotchas

- Generated SDK + HTTP plus a hand-written `hoody pipe` CLI; the file does NOT cover its subcommands — they are a hand-written CLI surface.
- `/api/v1/pipe/{path}` ≡ bare `/{path}`.
- Reserved: `/`, `/noscript`, `/help`, `/favicon.ico`, `/robots.txt` (alias-hardened).
- `n` ≤ 256; peers must agree. Caps 1000 pending + 1000 active.
- Path ≤ 1024 enforced by kit; URL ≤ 4096 is a deployment-layer cap (proxy/Cloudflare) — `MAX_URL_LENGTH = 4096` is declared but not enforced in the kit validator. Control/backslash/%-slash → 400.
- Half-pipes / idle active evicted after 5 min.
- Dangerous sender MIME (HTML/SVG/JS) → `text/plain`; `nosniff` forced.
- Forwarded sender→receiver headers: `Content-Type` (sanitized — dangerous MIME → `text/plain`), `Content-Length` (when set), `X-Piping`, `X-Hoody-Pipe` (each ≤8 KiB, CRLF-stripped). `Content-Disposition` is rebuilt per-receiver from sender metadata + receiver `?download`/`?filename` params.
- `?download` enum (SDK-validated): `"true"`/`"false"`/`"yes"`/`"no"`/`"1"`/`"0"` (attach / inline). The kit is more permissive — bare `?download` (no value) and any non-`false`/`no`/`0` string are treated as truthy. `?filename=<v>` implies attach, sanitised (255 chars, RFC 5987).
- `?video` HTML player only on `Accept: text/html`; no receiver slot.
- `?progress` no receiver slot. Caps: 50/path, 500 groups, 30 min TTL.
- `Service-Worker: script` → 400.
- `Content-Range` on POST/PUT → 400.
- Multipart: only **first file part** used, 30s deadline.

## Common errors

- 400 — active / sender attached / `n` mismatch / slots full / `n`>256 / reserved / forbidden chars / `Service-Worker` / `Content-Range`.
- 405 — method (HEAD only on reserved).
- 408 — receivers timeout.
- 414 — path > 1024.
- 429 — pending/active/spectator cap.

## Related namespaces

`files` persist · `curl` HTTP client · `tunnel` long-lived bidi · `terminal`/`exec` shell.

## Examples

`pipe` examples below use HTTP and the generated SDK. A separate hand-written `hoody pipe` CLI also exists (subcommands `send`, `receive`, `progress`, `url`, `forward-tcp`, `health`, `help-cheatsheet`) — hand-written, NOT auto-generated. Every step in every example
was live-tested against a real `pipe-1` kit. Set `P`, `C`, `N` (project id,
container id, server name) from `containers.get` first, then
`KIT="https://${P}-${C}-pipe-1.${N}.containers.hoody.com"`.

Pipe paths are reservations: receivers and senders rendezvous on the same path.
Pick a unique path (e.g. `transfer-$(openssl rand -hex 4)`) per transfer — once
it's claimed by a sender or receiver, the same path can't host another transfer
until the first one finishes or the 5-min idle TTL evicts it.

### 1. One-to-one transfer — receiver waits, sender pushes bytes

**Goal:** stream a payload from one endpoint to another with zero staging. Receiver opens the GET first; the connection blocks until the sender POSTs to the same path.

**Step 1 — start the receiver in the background.** It blocks until the sender connects.

```typescript
const pathName = `transfer-${crypto.randomBytes(4).toString('hex')}`;
const recvPromise = client.pipe.receive(pathName, { responseType: 'arrayBuffer' });
```
**Step 2 — send the bytes.** Sender's response is a streamed `[INFO]` log:
```
[INFO] Waiting for 1 receiver(s) to connect...
[INFO] 1 receiver(s) already connected.
[INFO] Streaming to 1 receiver(s)...
[INFO] Upload complete.
[INFO] Transfer complete.
```

```typescript
// Generated SDK types `data` as `object` — wrap raw text/bytes in a Blob.
await client.pipe.send(pathName, new Blob(['hello pipe!'], { type: 'text/plain' }));
const received = await recvPromise;
console.log(new TextDecoder().decode(received.data as ArrayBuffer)); // → hello pipe!
```
### 2. Fan-out 1-to-3 — one sender, three receivers

**Goal:** broadcast the same bytes to three endpoints in lockstep. All four parties (3 receivers + 1 sender) must agree on `n=3`; mismatch → 400.

**Step 1 — open three receivers.** Each must pass `?n=3`.

```typescript
const pathName = `broadcast-${crypto.randomBytes(4).toString('hex')}`;
const receivers = [1, 2, 3].map(() =>
  client.pipe.receive(pathName, { n: 3, responseType: 'arrayBuffer' })
);
```
**Step 2 — send once; all three receivers get an identical copy.** Lockstep fan-out: the slowest receiver paces the transfer.

```typescript
await client.pipe.send(pathName, new Blob(['fan-out-payload'], { type: 'text/plain' }), { n: 3 });
const all = await Promise.all(receivers);
// all three .data buffers are byte-identical
```
### 3. Force a download with a custom filename

**Goal:** make the browser save the response to disk with a specific name, regardless of what (or whether) the sender provided a `Content-Disposition`. `?filename=<v>` implies `?download` and overrides any sender-supplied filename.

**Live-tested response header:** `content-disposition: attachment; filename="report.bin"`.

```typescript
const pathName = `dl-${crypto.randomBytes(4).toString('hex')}`;
const recv = client.pipe.receive(pathName, {
  download: '1',
  filename: 'report.bin',
  responseType: 'blob',
});
await client.pipe.send(pathName, new Blob(['BINARYPAYLOAD'], { type: 'application/octet-stream' }));
const r = await recv;
// r exposes the response Blob; in a browser, save with FileSaver or anchor[download]
```
### 4. Force inline display, overriding a sender's `attachment`

**Goal:** the sender (e.g. a legacy script) marks every payload as `Content-Disposition: attachment; filename="leaked.txt"`, but you want to render it inline in your app. `?download=0` strips Content-Disposition entirely on the receiver side — per receiver, not globally.

**Live-tested:** sender sent `Content-Disposition: attachment; filename="leaked.txt"`; receiver got `content-type: text/plain` only — no Content-Disposition.

```typescript
const pathName = `inline-${crypto.randomBytes(4).toString('hex')}`;
const recv = client.pipe.receive(pathName, { download: '0' });
await client.pipe.send(pathName, new Blob(['inline body'], { type: 'text/plain' }));
await recv;
// Generated `ApiResponse<T>` exposes only { statusCode, message, data } — no `headers`.
// To inspect Content-Disposition, drop down to raw `fetch(KIT_URL + '?download=0')`
// against the kit URL and read `res.headers.get('content-disposition')`.
```
### 5. Watch a transfer with `?progress` (SSE)

**Goal:** monitor live state + bytes/sec from a third process, without consuming a receiver slot. `?progress=1` with `Accept: text/event-stream` returns SSE events; the spectator never blocks the transfer.

**Live-tested SSE stream** for a 50 KB payload:
```
event: state    data: {"state":"idle",...}
event: state    data: {"state":"waiting","hasSender":true,"activeReceivers":1,...}
event: state    data: {"state":"streaming",...}
event: progress data: {"bytesTransferred":50000,"totalBytes":50000,...}
event: done     data: {"state":"complete","bytesTransferred":50000,"avgSpeed":7142857}
```

```typescript
// Browser/Node: use EventSource with the kit URL directly
const url = `${kitUrl}/api/v1/pipe/${pathName}?progress=1`;
const es = new EventSource(url, { withCredentials: false });
es.addEventListener('state',    e => console.log('state', JSON.parse((e as MessageEvent).data)));
es.addEventListener('progress', e => console.log('progress', JSON.parse((e as MessageEvent).data)));
es.addEventListener('done',     e => { console.log('done', (e as MessageEvent).data); es.close(); });
```
### 6. Embed an HTML transfer dashboard

**Goal:** give a non-technical user a live dashboard view of an in-flight transfer. Same `?progress=1` endpoint, but `Accept: text/html` returns a self-contained HTML page (CSP nonces, EventSource client baked in). Pop it in an `<iframe>` or open it in a new tab.

**Live-tested:** ~6 KB HTML page titled `"Hoody Pipe — Transfer Progress"` with `EventSource` wired to the same path. Dashboard never consumes a receiver slot.

```typescript
// Just point the browser at the URL — no SDK call needed:
const dashUrl = `${kitUrl}/api/v1/pipe/${pathName}?progress=1`;
window.open(dashUrl, '_blank');
// Or embed:
//   <iframe src={dashUrl} sandbox="allow-scripts" />
```
### 7. Stream a screen-recording to an MSE video player

**Goal:** stream live MPEG-TS / fMP4 / WebM from `ffmpeg` and watch it in a browser without serving a separate frontend. `?video=1` + `Accept: text/html` returns an HTML page that auto-detects the codec from the first bytes; non-browser clients (VLC, mpv, ffplay) fall through to the raw stream automatically.

**Live-tested:** ~7.7 KB HTML page titled `"Hoody Pipe — Video"` with `MediaSource` + `data-path` baked in.

```typescript
// Player URL is just a browser navigation — open it directly:
const playerUrl = `${kitUrl}/api/v1/pipe/${pathName}?video=1`;
window.open(playerUrl, '_blank');
// To push the encode from Node, stream a ReadableStream into pipe.send (raw fetch
// preserves backpressure better than the SDK wrapper for unbounded uploads).
```
### 8. Multipart upload — only the first file part is forwarded

**Goal:** accept an HTML form upload. Pipe extracts the **first file part** of a `multipart/form-data` body, drains the rest, and forwards the file's `Content-Type` + `Content-Disposition` (auto-upgraded to `attachment`).

**Live-tested:** sent `field1=ignored` + `file=@a.txt` + `extra=@b.txt`. Receiver got body `first-file-content` only, with headers `content-type: text/plain` and `content-disposition: attachment; filename="a.txt"` — second file silently dropped.

```typescript
const pathName = `upload-${crypto.randomBytes(4).toString('hex')}`;
const recv = client.pipe.receive(pathName);
const fd = new FormData();
fd.append('field1', 'ignored-form-field');
fd.append('file', new Blob(['first-file-content'], { type: 'text/plain' }), 'a.txt');
fd.append('extra', new Blob(['second-file-content'], { type: 'text/plain' }), 'b.txt');
await client.pipe.send(pathName, fd as unknown as object);
const r = await recv;
// `r.data` carries first-file bytes; b.txt is dropped server-side.
// Generated ApiResponse has no `headers`; for Content-Disposition use raw fetch
// (`(await fetch(kitUrl)).headers.get('content-disposition')`) instead.
```
### 9. Path / URL length limits — 1024-char path is the cap

**Goal:** know what blows up at the validator. Path > 1024 chars → `414 Path too long`; URL > 4096 chars → 414 from the proxy before the kit even sees it.

**Live-tested:**
- 1100-char path POST: kit returned `414` with body `[ERROR] Path too long (max 1024 characters).`
- 1024-char path: accepted by the path validator (sender hangs waiting for receiver as expected).

```typescript
// SDK enforces this at runtime BEFORE hitting the network:
try {
  await client.pipe.send('x'.repeat(1100), new Blob(['data'], { type: 'text/plain' }));
} catch (e: any) {
  console.log(e.message); // → "path must be at most 1024 characters"
}
```
### 10. Sidechannel metadata via `X-Hoody-Pipe`

**Goal:** attach commit / build / job metadata to the transfer without polluting the body. The kit forwards `X-Hoody-Pipe` and `X-Piping` (≤ 8 KiB each, CRLF-stripped) to receivers and exposes them via `Access-Control-Expose-Headers` so browsers can read them.

**Live-tested response headers** with `X-Hoody-Pipe: build-id=42; commit=abc1234` + `X-Piping: legacy-meta=true`:
```
access-control-expose-headers: X-Piping, X-Hoody-Pipe
x-hoody-pipe: build-id=42; commit=abc1234
x-piping: legacy-meta=true
```

```typescript
// The generated SDK methods do NOT expose per-call request/response headers
// (`ApiResponse<T>` has only { statusCode, message, data }), so the metadata
// sidechannel is not reachable through `client.pipe.{send,receive}`. Drop down
// to raw `fetch()` against the kit URL — same wire format, full header access:

const KIT = `https://${P}-${C}-pipe-1.${N}.containers.hoody.com`;
const pathName = `meta-${crypto.randomBytes(4).toString('hex')}`;
const recv = fetch(`${KIT}/api/v1/pipe/${pathName}`);
await fetch(`${KIT}/api/v1/pipe/${pathName}`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/octet-stream',
    'X-Hoody-Pipe': 'build-id=42; commit=abc1234',
    'X-Piping':     'legacy-meta=true',
  },
  body: 'metadata payload',
});
const res = await recv;
console.log(res.headers.get('x-hoody-pipe')); // → "build-id=42; commit=abc1234"
console.log(res.headers.get('x-piping'));     // → "legacy-meta=true"
```

## Reference

**Accessor:** `client.pipe`  |  **Import:** `import * as pipe from 'hoody-sdk/pipe'`

### `client.pipe.health` (1) — Health

#### `check` — Service health check

```typescript
client.pipe.health.check()
```

**Returns:** `pipe_HealthResponse`  |  **HTTP:** `GET /api/v1/pipe/health`

---

### `client.pipe.info` (1) — info

#### `getHelp` — Get help text with curl examples

```typescript
client.pipe.info.getHelp()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/pipe/help`

---

### `client.pipe` (3) — pipe

#### `corsPreflight` — CORS preflight

```typescript
client.pipe.corsPreflight(path: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Any path — OPTIONS is handled identically for all paths |

**Returns:** `any`  |  **HTTP:** `OPTIONS /api/v1/pipe/{path}`

---

#### `receive` — Receive data from a pipe

```typescript
client.pipe.receive(path: string, n?: integer, download?: string, filename?: string, video?: string, progress?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Pipe path name to receive from — must match the path used by the sender.  Reserved paths (`/help`, `/noscript`, etc.) return their own content on GET instead of acting as pipe receivers. |
| `n` | `integer` | query | No | Expected number of receivers. Must match the sender's `n` value exactly — a mismatch returns 400.  When `n > 1`, the pipe waits for all `n` receivers and the sender before streaming. |
| `download` | `string` | query | No | Control whether the response triggers a browser download.  - `?download` (bare), `?download=true`, `?download=yes`, `?download=1` — force `Content-Disposition: attachment` (triggers download). Uses sender's filename if available, otherwise pipe path basename. - `?download=false`, `?download=no`, `?download=0` — suppress `Content-Disposition` entirely, even if sender set one (forces inline display). - Absent — passthrough sender's Content-Disposition as-is. Multipart `form-data` dispositions are auto-converted to `attachment`.  Works per-receiver — with `n=2`, one receiver can have `?download` and the other can display inline. |
| `filename` | `string` | query | No | Set a custom download filename. Implies `?download` — the response will have `Content-Disposition: attachment; filename="<value>"`.  **Priority:** `?filename` overrides any filename from the sender's Content-Disposition header.  **Sanitization:** Null bytes, CRLF, path separators (`/`, `\`), leading dots, and control characters are stripped. Truncated to 255 characters. Non-ASCII filenames use RFC 5987 `filename*=UTF-8''...` encoding.  Filenames that sanitize to empty fall back to bare `attachment`. |
| `video` | `string` | query | No | Return an HTML page with an embedded MSE (MediaSource Extensions) video player instead of raw pipe data. The player page fetches the raw stream internally — no pipe receiver slot is consumed by the page itself.  **Browser detection:** Only serves the HTML player when the client sends `Accept: text/html` (i.e. a browser). Non-browser clients (VLC, mpv, curl, ffplay) with `?video` fall through to normal pipe receiver behavior and get the raw stream — ensuring automatic compatibility with media players.  **Auto-detection:** The player detects the container/codec from the stream's first bytes: - WebM (VP8/VP9/AV1 + Opus/Vorbis) - MP4/fMP4 (H.264/H.265/VP09/AV01 + AAC) - MPEG-TS  **UI features:** - Click to unmute (autoplay requires muted) - Right-click to pause/resume - Status overlay: "Waiting for stream…", "Connected", "Stream ended" - Buffer trimming (>30s behind currentTime removed)  **Values:** `?video` (bare), `?video=true`, `?video=yes`, `?video=1` → show player. `?video=false`, `?video=no`, `?video=0` → normal pipe receiver.  **Security:** CSP with nonces (`script-src`, `style-src`), `connect-src 'self'`, `media-src blob:`, `default-src 'none'`. Pipe path HTML-escaped in `data-path` attribute. |
| `progress` | `string` | query | No | Return real-time transfer progress as a Server-Sent Events (SSE) stream or HTML dashboard. Does NOT consume a pipe receiver slot — spectators are completely independent of the transfer.  **Accept header routing:** - `Accept: text/event-stream` → SSE stream (EventSource, curl) - `Accept: text/html` → HTML dashboard page (browser) - `Accept: */*` or missing → SSE stream (default to data, not markup)  **SSE event types:** - `state` — State transitions: idle → waiting → streaming → complete/failed - `progress` — During streaming (throttled 250ms): bytesTransferred, speed, ETA, receivers - `done` — Terminal event: final stats (bytesTransferred, duration, avgSpeed)  **State machine:** `idle` (no pipe) → `waiting` (sender/receivers connecting) → `streaming` (data flowing) → `complete` or `failed`  **DoS protections:** Max 50 spectators per path, 500 total groups, 30-min connection TTL, 30s post-transfer linger.  **Values:** `?progress` (bare), `?progress=true`, `?progress=yes`, `?progress=1` → show progress. `?progress=false`, `?progress=no`, `?progress=0` → normal pipe receiver.  **Security:** HTML dashboard uses CSP with nonces. Pipe path HTML-escaped. SSE includes `X-Accel-Buffering: no` for Nginx compatibility. |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/pipe/{path}`

---

#### `send` — Send data to a pipe

```typescript
client.pipe.send(path: string, n?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | path | Yes | Unique pipe path name. Must not be a reserved path (`/`, `/help`, `/noscript`, `/favicon.ico`, `/robots.txt`).  Examples: `myfile`, `transfer123`, `secret.png`, `logs/today` |
| `n` | `integer` | query | No | Number of receivers to wait for before starting the transfer. All receivers get identical copies of the data (fan-out). Must be a positive integer, max 256. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/pipe/{path}`

---

### `client.pipe.ui` (2) — ui

#### `getIndex` — Index page (web UI)

```typescript
client.pipe.ui.getIndex()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/pipe`

---

#### `getNoScript` — No-JavaScript upload page

```typescript
client.pipe.ui.getNoScript(path?: string, mode?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | query | No | Pre-fill the pipe path. Only URL-safe characters allowed. |
| `mode` | `string` | query | No | Input mode: `file` for file picker, `text` for textarea |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/pipe/noscript`


---

<!-- ===== namespace: proxyLogs ===== -->

# `proxyLogs` — Per-container request/response/event log query, stats, and SSE tail

## Purpose

Read-only access to the container reverse-proxy log store: query, stats, SSE tail.

## When to use

- Debug 4xx/5xx on a kit subdomain.
- Live-tail during deploys.
- Status-code mix; filter by `kind`/`level`/`method`/`serviceName`.

## When NOT to use

App stdout/stderr → `exec`/`daemon`, file events → `watch`, user SQLite → `sqlite`, shell → `terminal`; clearing logs not exposed.

## Prerequisites

- Project + container running; logging automatic.
- Matrix needs `"logs": true`. `"*"` does NOT grant.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. List recent

- `logs.list` with `last: N` or `limit`+`offset` (SNI-bound).
- Sweep: `logs.listAll` / `logs.listIterator`.

### 2. Drill into 5xx

- `logs.list` `level: "error"` (single value — 5xx auto-promote to `error`), `includeResponseBody: true`; filter `serviceName` client-side (kit/SNI list ignores it).
- Paginate with `limit`/`offset` (DB rowids); `afterId` on the kit URL hits the ring buffer where `id: 0`.

### 3. Live-tail with resume

- `logs.streamLogs` — SSE; live frames carry `id: <ringSeq>` (initial replay frame is a data-only array with no `id:` line).
- Reconnect with `Last-Event-ID`; replays from ~5000-entry ring.
- `event: reset` → drop cursor, reconnect. `event: scope-destroyed` → close.

### 4. Status snapshot

- `logs.getStats` — totals + status breakdown.

### 5. Bodies for a slice

- `logs.list` + `includeRequestBody`/`includeResponseBody: true` (off by default).

## Quirks & gotchas

- Kit slug `logs`; only `/`, `/_logs`, `/_logs/stream`, `/_logs/stats` reachable.
- `projectId`/`containerId` on `list` ignored — SNI auto-scopes.
- `cursor` is only accepted when the deployment enables cursor pagination.
- `level` accepts ONE value at a time; despite the mapping description claiming comma-separated, `level=warn,error` returns `total: 0` — query each level separately and union client-side.
- `serviceName` handling is path-dependent: the kit/SNI `/_logs` LIST handler silently ignores `serviceName`, but the kit/SNI `/_logs/stream` BOUNCE forwards `serviceName`/`source` to the management port; the management-port `/_logs` handler always honours it. The generated SDK `list()` sends the param either way, but on a kit-URL list it is a no-op — scan + client-side filter. The generated **streamLogs** SDK method does not expose `serviceName` at all. `traceId` is not exposed as a query param anywhere; scan with `limit/afterId` and filter client-side.
- Response shape switches on which paging param you use: `limit/offset` → `{entries,total,limit,offset}`; `last=N` and `afterId` → flat `LogEntry[]` (and `id: 0` placeholders on the `last=N` shape) — but only when bodies are NOT requested; with `includeRequestBody`/`includeResponseBody=true` even the flat-paging paths fall through to the wrapped `{entries,total,limit,offset}` shape.
- `includeRequestBody`/`includeResponseBody` default `false`.
- Ring ~5000 (~50s @ 100/s); longer gaps lose rows.
- `event: reset` rebases ringSeq ≥10000 — drop cursor.
- `logs` strict-boolean; `"*"` does NOT grant.
- `kind` = `request`/`response`/`event`.

## Common errors

- 403 — missing `logs: true` (permission / blocked-path gate).
- 429 — `AUDIT_SNI_RATE_LIMIT_PER_MIN` (default 30) per-scope read/stat/stream rate limit tripped on any SNI read (list, getStats, streamLogs) → `{error:"rate_limited"}` with `Retry-After: 2`; back off.
- 404 on `/_logs/{config,health,export,db/*}`.
- 405 on `DELETE /_logs`.
- 410 NDJSON `snapshot_expired` (admin `cursor`); restart no-cursor.
- Desync if `event:` lines unparsed; reset on `reset`.

## Related namespaces

- `exec`/`daemon` — app logs.
- `terminal` — reproduce.
- `watch` — fs events.
- `api` — lifecycle.

## Examples

Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first.

> **CLI caveat — `hoody proxy logs …` does NOT target the kit.** `list`/`stats` declare `namespace: 'api'` and `stream` builds from the client base URL, so `--container` is IGNORED and the call goes to your configured API/management base URL (`--base-url`), where no `/_logs` route exists. The kit-URL behaviours documented below (flat `last=N` array, `id: 0` ring rows) apply to the HTTP and SDK forms only.

`proxyLogs` is read-only (no destructive writes — clear/reset/repair are not exposed via the kit URL), so the surface is small. We picked **7** end-to-end recipes that exercise every working filter, both response shapes, the stats endpoint, and SSE resume. Three additional scenarios from the suggestion list — *filter by program*, *filter by source IP*, and *search by alias hostname* — were dropped because the kit does **not** filter on those fields server-side (`serviceName`, `clientIp`, alias-hostname are *not* honoured as query params on `GET /_logs`); the only way to scope by them is client-side `.filter()` after a paged scan, which is already shown in §2 (status) and §5 (traceId).

### 1. Tail the last N requests across every kit

**Goal:** glance at the most recent ~50 requests handled by the container's edge proxy. Uses `last=N`, which returns a flat ARRAY (no `entries` wrapper) ordered **oldest-first within the returned last-N slice** with `id: 0` placeholders — the cheapest call you can make.

```typescript
const r = await client.proxyLogs.logs.list({ last: 50 });
const rows = r.data as any[];               // `last` returns an array, not a LogQueryResult
for (const e of rows) {
  console.log(e.tsIso, e.kind, e.level, e.serviceName, e.method, e.url, e.status ?? '—');
}
```
### 2. Triage 4xx/5xx — pull a level and post-filter by status

**Goal:** find the entries the edge auto-promoted — `level: error` is what **5xx** become, `level: warn` is what **4xx** become — then narrow client-side to a specific status range. The server-side `level` param honours **one** value at a time — `level=warn,error` returns 0 rows; query each level separately and union locally.

```typescript
const r = await client.proxyLogs.logs.list({ level: 'error', limit: 200 });
const fivexx = (r.data as any).entries.filter((e: any) => e.status >= 500 && e.status < 600);
console.log(`${fivexx.length} 5xx of ${(r.data as any).total} error entries`);
```
### 3. Walk the full window with `limit`/`offset` paging (oldest → newest)

**Goal:** sweep every entry without skipping or double-reading rows. On the kit URL **do not** cursor-page by `id`: a bare `afterId`/`last` (no bodies) routes to the in-memory ring buffer where every entry is `id: 0`, so `max(id)` is always `0` and `afterId=0` never advances. Page with `limit`/`offset` instead — that path queries the DB, returns the wrapped `{entries,total,limit,offset}` shape, and carries real rowids. Walk pages until `entries` is empty.

```typescript
let offset = 0;
for (;;) {
  const page = await client.proxyLogs.logs.list({ limit: 500, offset });
  const rows = (page.data as any).entries as any[];
  if (rows.length === 0) break;
  for (const e of rows) console.log(e.id, e.tsIso, e.serviceName, e.status);
  offset += rows.length;
}
// SDK convenience: `client.proxyLogs.logs.listIterator({ limit: 500 })` works because limit/offset returns the wrapped shape; do NOT pass `afterId`/`last` to listIterator — itemsPath is hardcoded to `data.entries` (flat-array path returns nothing, and on the kit URL its ids are 0).
```
### 4. Status snapshot — total, level mix, per-service breakdown

**Goal:** one call to summarise log volume and where errors are clustering. `/_logs/stats` returns `{ total, byLevel, byProject, byContainer, byService }` — perfect for a dashboard tile.

```typescript
const s = await client.proxyLogs.logs.getStats();
const d = s.data as any;
const noisiest = Object.entries(d.byService)
  .sort((a, b) => (b[1] as number) - (a[1] as number))
  .slice(0, 3);
console.log(`${d.total} entries, ${d.byLevel.warn} warns, hot:`, noisiest);
```
### 5. Trace one request across kits via `traceId`

**Goal:** one HTTP request that fans out to multiple internal kits shares a single `traceId` (UUID for edge entries; 32-char hex for backend hops). The kit does not filter on `traceId` server-side, so scan a recent window and group client-side. Each `traceId` typically yields a `kind: "request"` (edge) + one or more `kind: "request"`/`"response"` (backend) frames.

```typescript
const tid = '354a5a0222e7107c46ae2851ded57fa6';
const r = await client.proxyLogs.logs.list({
  limit: 1000, includeRequestBody: true, includeResponseBody: true,
});
const chain = (r.data as any).entries
  .filter((e: any) => e.traceId === tid)
  .sort((a: any, b: any) => a.tsMs - b.tsMs);
console.log(`${chain.length} hops in trace ${tid}`);
```
### 6. Live-tail with SSE and resume after disconnect

**Goal:** stream new log entries as they happen, and pick up exactly where you left off after a network blip. Live frames carry `id: <ringSeq>`; the initial replay frame may be `data: [...]` with no `id:` line, so seed your cursor only after you see the first `id:` line. Resume by sending `Last-Event-ID: <last>` on reconnect. On `event: reset` clear your cursor and reconnect fresh; on `event: scope-destroyed` exit cleanly — the container is gone.

```typescript
// `client.proxyLogs.logs.streamLogs(...)` is request/response-shaped — it returns
// `Promise<ApiResponse<unknown>>`, NOT an async iterator. For live SSE in the SDK,
// drop down to raw `fetch()` against the kit URL and parse `response.body` yourself.
const KIT = `https://${P}-${C}-logs-1.${N}.containers.hoody.com`;
let lastSeen = 0;
const res = await fetch(`${KIT}/_logs/stream?level=warn`, {
  headers: { 'Accept': 'text/event-stream', 'Last-Event-ID': String(lastSeen) },
});
const reader = res.body!.getReader();
const td = new TextDecoder();
let buf = '';
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += td.decode(value, { stream: true });
  let sep;
  while ((sep = buf.indexOf('\n\n')) !== -1) {
    const frame = buf.slice(0, sep); buf = buf.slice(sep + 2);
    if (frame.startsWith(':')) continue; // heartbeat (every 30s)
    let id: number | null = null, data = '', event: string | null = null;
    for (const line of frame.split('\n')) {
      if (line.startsWith('id: ')) id = Number(line.slice(4));
      else if (line.startsWith('data: ')) data += line.slice(6);
      else if (line.startsWith('event: ')) event = line.slice(7);
    }
    if (event === 'reset') { lastSeen = 0; continue; }
    if (event === 'scope-destroyed') {
      await reader.cancel().catch(() => undefined);
      break;
    }
    if (id !== null) lastSeen = id; // first replay frame has no id; seed only on live frames
    const payload = JSON.parse(data);
    if (Array.isArray(payload)) for (const e of payload) console.log(e.tsIso, e.serviceName, e.status, e.url);
    else console.log(payload.tsIso, payload.serviceName, payload.status, payload.url);
  }
}
```
### 7. Capture request + response bodies for a debug slice

**Goal:** body payloads are off by default. Turn them on for a narrow window (e.g. recent warns) to inspect what the upstream actually sent or received. Bodies are capped at `maxBodySize` (default 65 536 B) and content-types in the kit's `excludeContentTypes` (`image/`, `video/`, `audio/`, `application/octet-stream`, `font/`) are skipped — `bodyTruncated: true` flags both cases.

```typescript
const r = await client.proxyLogs.logs.list({
  level: 'warn', limit: 20,
  includeRequestBody: true, includeResponseBody: true,
});
for (const e of (r.data as any).entries) {
  if (e.bodyTruncated) continue;
  console.log(e.id, e.status, e.url, e.requestBody, e.responseBody);
}
```

## Reference

**Accessor:** `client.proxyLogs`  |  **Import:** `import * as proxyLogs from 'hoody-sdk/proxyLogs'`

### `client.proxyLogs.logs` (5) — logs

#### `getStats` — Get log statistics

```typescript
client.proxyLogs.logs.getStats()
```

**Returns:** `proxyLogs_LogStats`  |  **HTTP:** `GET /_logs/stats`
**CLI:** `hoody proxy logs stats`

---

#### `list` — Query centralized logs

```typescript
client.proxyLogs.logs.list(limit?: integer, offset?: integer, projectId?: string, containerId?: string, serviceName?: string, level?: string, includeRequestBody?: boolean, includeResponseBody?: boolean, last?: integer, afterId?: integer, cursor?: string, kind?: string, method?: string, source?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `projectId` | `string` | query | No |  |
| `containerId` | `string` | query | No |  |
| `serviceName` | `string` | query | No |  |
| `level` | `string` | query | No | Comma-separated levels (debug,info,warn,error) |
| `includeRequestBody` | `boolean` | query | No |  |
| `includeResponseBody` | `boolean` | query | No |  |
| `last` | `integer` | query | No | Return only the last N entries |
| `afterId` | `integer` | query | No | Return entries with SQLite row ID greater than this (ASC cursor) |
| `cursor` | `string` | query | No | pagination cursor (signed opaque base64). |
| `kind` | `string` | query | No |  |
| `method` | `string` | query | No |  |
| `source` | `string` | query | No |  |

**Returns:** `proxyLogs_LogQueryResult`  |  **HTTP:** `GET /_logs`
**CLI:** `hoody proxy logs list`

---

#### `listAll` — Query centralized logs (collect all pages)

```typescript
client.proxyLogs.logs.listAll(limit?: integer, offset?: integer, projectId?: string, containerId?: string, serviceName?: string, level?: string, includeRequestBody?: boolean, includeResponseBody?: boolean, last?: integer, afterId?: integer, cursor?: string, kind?: string, method?: string, source?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `projectId` | `string` | query | No |  |
| `containerId` | `string` | query | No |  |
| `serviceName` | `string` | query | No |  |
| `level` | `string` | query | No | Comma-separated levels (debug,info,warn,error) |
| `includeRequestBody` | `boolean` | query | No |  |
| `includeResponseBody` | `boolean` | query | No |  |
| `last` | `integer` | query | No | Return only the last N entries |
| `afterId` | `integer` | query | No | Return entries with SQLite row ID greater than this (ASC cursor) |
| `cursor` | `string` | query | No | pagination cursor (signed opaque base64). |
| `kind` | `string` | query | No |  |
| `method` | `string` | query | No |  |
| `source` | `string` | query | No |  |

**Returns:** `proxyLogs_LogQueryResult[]`  |  **HTTP:** `GET /_logs`
**CLI:** `hoody proxy logs list`

---

#### `listIterator` — Query centralized logs (async iterator)

```typescript
client.proxyLogs.logs.listIterator(limit?: integer, offset?: integer, projectId?: string, containerId?: string, serviceName?: string, level?: string, includeRequestBody?: boolean, includeResponseBody?: boolean, last?: integer, afterId?: integer, cursor?: string, kind?: string, method?: string, source?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `limit` | `integer` | query | No |  |
| `offset` | `integer` | query | No |  |
| `projectId` | `string` | query | No |  |
| `containerId` | `string` | query | No |  |
| `serviceName` | `string` | query | No |  |
| `level` | `string` | query | No | Comma-separated levels (debug,info,warn,error) |
| `includeRequestBody` | `boolean` | query | No |  |
| `includeResponseBody` | `boolean` | query | No |  |
| `last` | `integer` | query | No | Return only the last N entries |
| `afterId` | `integer` | query | No | Return entries with SQLite row ID greater than this (ASC cursor) |
| `cursor` | `string` | query | No | pagination cursor (signed opaque base64). |
| `kind` | `string` | query | No |  |
| `method` | `string` | query | No |  |
| `source` | `string` | query | No |  |

**Returns:** `AsyncIterableIterator<proxyLogs_LogQueryResult>`  |  **HTTP:** `GET /_logs`
**CLI:** `hoody proxy logs list`

---

#### `streamLogs` — Live-tail logs over Server-Sent Events (v8 SSE contract)

```typescript
client.proxyLogs.logs.streamLogs(projectId?: string, containerId?: string, kind?: string, level?: string, Last-Event-ID?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `projectId` | `string` | query | No | Filter to a single project |
| `containerId` | `string` | query | No | Filter to a single container |
| `kind` | `string` | query | No |  |
| `level` | `string` | query | No |  |
| `Last-Event-ID` | `string` | header | No | numeric ringSeq of the last event received. Server skips entries ≤ this value from the ring buffer on reconnect. |

**Returns:** `any`  |  **HTTP:** `GET /_logs/stream`
**CLI:** `hoody proxy logs stream`


---

<!-- ===== namespace: run ===== -->

# `run` — resolve apps to shell commands

## Purpose

Hoody Run — HTTP resolver across package sources (trusted-list, system-path, nixpkgs, pkgx, AppImage, OCI, manifests). Returns ranked candidates (each carrying a `kind`) or a resolved `shell_command`. Resolve produces a command plus a preview; it never launches the app itself.

## When to use

- Resolve `firefox`/`react`/`owner/repo` to a command.
- Cross-provider candidates with stable `set_id`.
- Preview the resolved command via `print_curl` / `preflight`.
- Batch via `runBatch`; persist profiles/recipes.

## When NOT to use

Not for: command known → `terminal`, long-lived process → `daemon`, one-shot remote exec → `exec`, arbitrary HTTP → `curl`.

## Prerequisites

- Kit slug `run`.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Search then pick

1. `client.run.searchCandidates({ app, os?, kind?, arch?, tags?, source? })` → `{ set_id, candidates[] }`.
2. `client.run.resolve({ ...selector, set_id, pick:"index", pick_index:N })` → `shell_command`.

### 2. Preflight

1. `client.run.preflightRun(Selector)` → `recommended_mode`, `missing_requirements`, `effective_policy`.
2. `client.run.resolve(Selector)` → resolved command + preview.

### 3. Cursor-paged search

`client.run.searchCandidatesPaged` → `{ set_id, total_count, items, next_cursor }` (note `items`, not `candidates`). The cursor-paged endpoint returns one page per call; drain manually by carrying `next_cursor` forward until it's null/absent.
The SDK exposes `client.run.searchCandidatesPagedAll` and `client.run.searchCandidatesPagedIterator`, but they currently only return/yield the **first page** — the generated paginator can't thread the cursor back into a cursor-paged call, so it stops after one page. To collect all candidates reliably, carry `next_cursor` forward by hand until it's null/absent.
### 4. Batch

`client.run.runBatch({ items: [{ request_id, mode, selector }] })`. `mode:"run"` resolves each item to a command.

### 5. Recipes

`recipes.createRecipe({ name, selector_template, allowed_overrides })`; invoke via `recipes.runRecipe(name, data)` — generated SDK takes `name` positional + a body, NOT a single options object.

## Quirks & gotchas

- Kit slug/URL/HTTP prefix all `run`. The resolve endpoint is `GET|POST /api/v1/run/resolve` (operations `resolveGet` / `resolve`).
- Every candidate carries a `kind` — `gui` | `cli` | `any` (`any` means the source doesn't classify it). Pass `kind` in the selector to narrow (`kind:'cli'`, `kind:'gui'`), or `kind:'any'` for no filter.
- `limit` default 25, clamped 1..=100.
- 30s query cache; `HOODY_RUN_QUERY_CACHE_TTL_MS`.
- `set_id` expires 300s.
- Selector requires `app`. Aliases `q`/`name` are accepted ONLY by the urlencoded query-string parser (GET / form-style); the JSON `Selector` model has only `app`, so JSON POST / SDK calls must use `app:`.
- Resolve is command-only: the response `status` is `"dry-run"` (a single picked candidate) or `"resolved"` (an unpicked candidate set), and carries a `handoff` object whose `state` is `"preview"`. When the resolved candidate has display/terminal surfaces, `preview_display_url` / `preview_terminal_url` are populated. The kit never launches the app or executes anything.
- `runBatch` only knows `mode: "search" | "run"` (no `"preflight"`); `"run"` resolves to a command.
- `recipes.runRecipe` / `recipes.searchRecipe` reject disallowed overrides with `400 "recipe override not allowed: <field>"` — they are NOT silently dropped.
- `selected.run_plan` carries `command`/`env`/`cwd`; `argv` is on `selected.execution_plan`.
- `/go/...` are alias routes for bookmarkable resolve URLs — `client.run.runPathBased` (selector parsed from path segments) and `client.run.runTerminalAnchored` (terminal id baked into the path prefix). Both are public and supported, but the canonical machine-facing entrypoint is `client.run.resolve` (`GET|POST /api/v1/run/resolve`) — prefer it for programmatic callers.
## Common errors

- `400 INVALID_PICK` — bad `pick_index`/`candidate_id`.
- `409 cursor set expired` — re-search.
- `502 SOURCE_RESOLUTION_FAILED` — a source (e.g. `nix`/`pkgx`) failed or is missing.

## Related namespaces

- `terminal` — run a known command / interactive shell. `exec` — one-shot remote exec. `daemon` — supervised process. `display` — GUI X11.

## Examples

Each example below has a copy-pasteable code block in the mode you're reading (curl for HTTP, TypeScript for SDK, or the CLI). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first. These examples reflect the resolve/preview contract — re-verify against a live `run-1` kit before relying on exact response shapes.

### 1. Resolve `firefox` to a shell command — search, then pick the top hit

**Goal:** turn the user's typed `firefox` into a runnable shell command. The default `pick` mode is `ask` (returns candidates, no selection); use `first` to auto-pick the highest-ranked one.

**Step 1 — search.** Returns a `set_id` (binds your follow-up `pick` against this exact candidate list — `set_id` expires after ~300s) and `candidates[]` ranked by score. Each candidate carries a `kind` (`gui`/`cli`/`any`).

```typescript
const r = await client.run.searchCandidates({ app: 'firefox', kind: 'any', limit: 5 });
const setId = (r.data as any).set_id;
const top = (r.data as any).candidates[0];
console.log(top.candidate_id, top.kind, top.score);
```
**Step 2 — resolve to a command.** `pick: 'first'` returns `shell_command` for the top candidate.

```typescript
const run = await client.run.resolve({
  app: 'firefox', kind: 'any', pick: 'first',
});
console.log((run.data as any).shell_command);
```
### 2. Resolve to a command and read the execution plan

**Goal:** get the exact command plus its structured plan. Lightweight CLI app (`echo`) used so we don't leak GUI state.

The response carries `shell_command` plus the full selected entry: `run_plan.{command,env,cwd}` (the resolved shell-form) and `execution_plan.{argv,env,cwd}` (the argv-form). When the candidate has display/terminal surfaces, `preview_display_url` / `preview_terminal_url` are populated so a caller can open the preview; resolve itself never launches anything.

```typescript
const r = await client.run.resolve({
  app: 'echo', kind: 'cli', pick: 'first',
});
console.log('argv:', (r.data as any).selected.execution_plan.argv);
console.log('preview:', (r.data as any).preview_terminal_url);
```
### 3. Pick a non-default candidate by index when multiple match

**Goal:** `git` resolves to `system-path:/usr/bin/git` by default, but you specifically want the `pkgx` candidate at index 2. Bind the pick to a `set_id` to avoid the candidate list shifting under you.

**Step 1 — list candidates with `set_id`.**

```typescript
const list = await client.run.searchCandidates({ app: 'git', kind: 'cli', limit: 5 });
(list.data as any).candidates.forEach((c, i) =>
  console.log(i, c.candidate_id, c.provider, c.score));
const setId = (list.data as any).set_id;
```
**Step 2 — pick by index against the captured `set_id`.** Out-of-range raises `400 pick_index out of range: <N>`.

```typescript
const r = await client.run.resolve({
  app: 'git', kind: 'cli', set_id: setId, pick: 'index', pick_index: 2,
});
console.log((r.data as any).shell_command);
```
**Pick by id alternative** — when you know the exact candidate, use `pick: 'id'` + `candidate_id`:

```typescript
await client.run.resolve({
  app: 'git', kind: 'cli', pick: 'id',
  candidate_id: 'system-path:/usr/bin/git',
});
```
### 4. Filter by os / arch / kind / source / tags

**Goal:** narrow candidates to Linux x86_64 CLI tools sourced only from the system PATH (skip `nix`/`pkgx`/`appimage`). Useful when you don't want long resolver tails.

`source` is repeatable on `GET` (`source=system&source=registry`); it's an array on the JSON body. Empty / absent → no filter. `kind` narrows by classification (`cli` / `gui` / `any`).

```typescript
const r = await client.run.searchCandidates({
  app: 'jq', os: 'linux', arch: 'amd64', kind: 'cli', source: ['system'], limit: 5,
});
const providers = new Set((r.data as any).candidates.map(c => c.provider));
// providers = Set { 'system' }
```
**Tags** are free-form ranking hints (e.g. `tags: ['portable']` boosts AppImage / pkgx candidates). Combine with `kind: 'gui'` for X11 apps, or `os: 'windows'` to filter the catalog to Wine-runnable variants.

### 5. Preflight before resolving — check requirements + policy

**Goal:** before resolving a GUI app, learn whether the kit thinks it'll succeed. `preflight` returns `recommended_mode`, `missing_requirements`, and the `effective_policy` (verify, integrity, deny-lists).

```typescript
const pf = await client.run.preflightRun({
  app: 'xeyes', kind: 'gui', pick: 'first',
});
if ((pf.data as any).missing_requirements?.length) {
  console.error('Missing:', (pf.data as any).missing_requirements);
}
```
### 6. Pagination — walk a long candidate list with `searchCandidatesPaged`

**Goal:** the `git` query returns many candidates across providers. Fetch them in pages of 3 without re-running expensive nix/pkgx queries.

`POST /api/v1/run/search/paged` returns `{ set_id, total_count, items, next_cursor }`. (Note: response field is `items`, not `candidates`.) Pass `next_cursor` back to get the next page; bound to the original `set_id` so the candidate set is stable.

**Step 1 — first page.**

```typescript
const page1 = await client.run.searchCandidatesPaged({
  selector: { app: 'git', kind: 'cli' }, page_size: 3,
});
console.log((page1.data as any).total_count, (page1.data as any).items.length);
const cursor = (page1.data as any).next_cursor;
```
**Step 2 — fetch all pages.** ⚠ The `search/paged` endpoint returns one page per call — there is no built-in drain-all behavior, so walk it manually by carrying `next_cursor` between calls until it's null/absent.
⚠ The SDK's `client.run.searchCandidatesPagedAll(...)` and `client.run.searchCandidatesPagedIterator(...)` helpers currently return/yield only the **first page** (the generated paginator can't advance the cursor on a cursor-paged op) — relying on them silently drops later pages, so use the manual `next_cursor` loop above to collect all candidates.
```typescript
// ⚠ searchCandidatesPagedAll / searchCandidatesPagedIterator currently return
// only the FIRST page (the paginator can't advance the cursor). Drain by hand:
let cursor2 = (page1.data as any).next_cursor;
const all = [...((page1.data as any).items as any[])];
while (cursor2) {
  const p = await client.run.searchCandidatesPaged({
    selector: { app: 'git', kind: 'cli' }, page_size: 3, cursor: cursor2,
  });
  all.push(...((p.data as any).items as any[]));
  cursor2 = (p.data as any).next_cursor;
}
console.log('drained:', all.length);
```
⚠ `409 cursor set expired` after ~300s — re-run the initial search with `selector` (no cursor) to get a fresh `set_id`.

### 7. Async search via job queue — for slow nix/pkgx queries

**Goal:** searching `firefox` across nixpkgs can take 15+s synchronously. Submit as a job, do other work, fetch result later.

⚠ `POST /api/v1/run/search/jobs` body is a **flat Selector** (NOT `{selector: ...}` like `search/paged`) — the wrapped form returns `Failed to deserialize ... missing field 'app'`.

**Step 1 — submit.**

```typescript
const sub = await client.run.jobs.createSearchJob({ app: 'firefox', kind: 'any' });
const jid = (sub.data as any).job_id;
```
**Step 2 — poll.** Status transitions `queued → running → done` (or `error`). Search-resolve jobs have a 10-minute TTL refreshed on every status/result read, so polling keeps them alive — but plan to read the result the moment status flips to `done`/`error` rather than relying on the TTL.

```typescript
let status = '';
while (!['done', 'error'].includes(status)) {
  await new Promise(r => setTimeout(r, 1000));
  const s = await client.run.jobs.getJobStatus(jid);
  status = (s.data as any).status;
}
const final = await client.run.jobs.getJobStatus(jid); // read result inline
```
### 8. Batch — resolve N apps in a single round-trip

**Goal:** the agent decided on three apps at once (`ls`, `echo`, `git`); resolve all to commands without three separate HTTP hits.

`runBatch` accepts items with `mode: 'search' | 'run'` (NOT `'preflight'`). Each item has its own `request_id` for correlation; results come back in the same order with one of `result: 'search'` (full search response) or `result: 'run'` (with `selected` + `shell_command`).

```typescript
const batch = await client.run.runBatch({
  items: [
    { request_id: 'a', mode: 'run', selector: { app: 'ls', kind: 'cli', pick: 'first' } },
    { request_id: 'b', mode: 'run', selector: { app: 'echo', kind: 'cli', pick: 'first' } },
    { request_id: 'c', mode: 'search', selector: { app: 'git', kind: 'cli', limit: 3 } },
  ],
});
for (const it of (batch.data as any).items) {
  if (it.result === 'run')    console.log(it.request_id, it.run.shell_command);
  if (it.result === 'search') console.log(it.request_id, it.search.candidates.length, 'candidates');
}
```
### 9. Save a recipe — reusable selector template with override allow-list

**Goal:** the team often resolves "give me a JS runtime" with a fixed set of filters. Save it once as a recipe; teammates run it by name and only override approved fields.

**Step 1 — create.** `allowed_overrides` is a whitelist; any `overrides.*` outside it is rejected with `400 "recipe override not allowed: <field>"` on `recipes.runRecipe` — update the recipe to widen the allow-list.

```typescript
await client.run.recipes.createRecipe({
  name: 'team-js-runtime',
  description: 'Resolve a JS runtime; team-default = node CLI',
  selector_template: {
    app: 'node', kind: 'cli', os: 'linux', arch: 'amd64', pick: 'first',
  },
  allowed_overrides: ['app', 'version', 'tags'],
});
```
**Step 2 — list / get / update / delete.**

```typescript
const list = await client.run.recipes.listRecipes();
const one  = await client.run.recipes.getRecipe('team-js-runtime');
await client.run.recipes.updateRecipe('team-js-runtime', {
  description: 'Updated: now also resolves bun/deno via override',
});
await client.run.recipes.deleteRecipe('team-js-runtime');
```
### 10. Invoke a recipe with overrides — `recipes.runRecipe(name, { overrides })`

**Goal:** teammate uses the `team-js-runtime` recipe but wants `bun` instead of the default `node`. They override only the allow-listed `app` field; other selector fields stay locked.

**Step 1 — run with overrides.** Returns the same envelope as `resolve` (`{ status, shell_command, selected, ... }`).

```typescript
const r = await client.run.recipes.runRecipe('team-js-runtime', {
  overrides: { app: 'bun' },
});
console.log((r.data as any).shell_command, (r.data as any).selected.provider);
```
**Step 2 — search through the recipe** (same selector, but stop at candidate listing instead of resolving) via `recipes.searchRecipe`:

```typescript
const s = await client.run.recipes.searchRecipe('team-js-runtime', {
  overrides: { app: 'node' },
});
console.log((s.data as any).candidates.length, 'candidates across',
  new Set((s.data as any).candidates.map(c => c.provider)));
```
⚠ Overrides outside `allowed_overrides` are **rejected** with `400 "recipe override not allowed: <field>"` — they are NOT silently dropped. Use `recipes.updateRecipe` to widen the allow-list.

## Reference

**Accessor:** `client.run`  |  **Import:** `import * as run from 'hoody-sdk/run'`

### `client.run.configuration` (1) — APIs for retrieving consolidated runtime configuration state including active profile selection

#### `getConfig` — Get full runtime configuration

```typescript
client.run.configuration.getConfig()
```

**Returns:** `run_ConfigFile`  |  **HTTP:** `GET /api/v1/run/config`

---

### `client.run.documentation` (2) — Self-documenting specification endpoints in JSON and YAML formats

#### `getOpenApiJson` — OpenAPI specification (JSON)

```typescript
client.run.documentation.getOpenApiJson()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/run/openapi.json`

---

#### `getOpenApiYaml` — OpenAPI specification (YAML)

```typescript
client.run.documentation.getOpenApiYaml()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/run/openapi.yaml`

---

### `client.run.jobs` (2) — APIs for tracking async job status with optional long-polling support for sync and background operations

#### `createSearchJob` — Start an async search job

```typescript
client.run.jobs.createSearchJob(data: run_Selector)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_Selector` | body | Yes |  |

**Returns:** `run_Job`  |  **HTTP:** `POST /api/v1/run/search/jobs`

---

#### `getJobStatus` — Get job status

```typescript
client.run.jobs.getJobStatus(job_id: string, wait?: string, timeout_ms?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `job_id` | `string` | path | Yes | Job identifier (UUID) |
| `wait` | `string` | query | No | Set to 'done' to long-poll until job completes |
| `timeout_ms` | `integer` | query | No | Long-poll timeout in milliseconds (default 0, max 120000) |

**Returns:** `run_Job`  |  **HTTP:** `GET /api/v1/run/jobs/{job_id}`

---

### `client.run.profiles` (5) — APIs for managing user profiles and defaults including source overrides, pick mode, and display preferences

#### `createProfile` — Create a new profile

```typescript
client.run.profiles.createProfile(data: run_ProfileConfig)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_ProfileConfig` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/run/profiles`

---

#### `deleteProfile` — Delete a profile

```typescript
client.run.profiles.deleteProfile(profile: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `profile` | `string` | path | Yes | Profile name |

**Returns:** `void`  |  **HTTP:** `DELETE /api/v1/run/profiles/{profile}`

---

#### `listProfiles` — List all profiles

```typescript
client.run.profiles.listProfiles()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/run/profiles`

---

#### `selectProfile` — Select the active profile

```typescript
client.run.profiles.selectProfile(profile: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `profile` | `string` | path | Yes | Profile name to select |

**Returns:** `run_SelectedProfileResponse`  |  **HTTP:** `POST /api/v1/run/profiles/{profile}/select`

---

#### `updateProfile` — Update a profile

```typescript
client.run.profiles.updateProfile(profile: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `profile` | `string` | path | Yes | Profile name |
| `data` | `object` | body | Yes |  |

**Returns:** `run_ProfileConfig`  |  **HTTP:** `PATCH /api/v1/run/profiles/{profile}`

---

### `client.run.recipes` (7) — APIs for managing saved selector templates and invoking them with controlled overrides

#### `createRecipe` — Create a saved recipe

```typescript
client.run.recipes.createRecipe(data: run_RecipeConfig)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_RecipeConfig` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/run/recipes`

---

#### `deleteRecipe` — Delete a saved recipe

```typescript
client.run.recipes.deleteRecipe(name: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Recipe name |

**Returns:** `void`  |  **HTTP:** `DELETE /api/v1/run/recipes/{name}`

---

#### `getRecipe` — Get a saved recipe

```typescript
client.run.recipes.getRecipe(name: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Recipe name |

**Returns:** `run_RecipeConfig`  |  **HTTP:** `GET /api/v1/run/recipes/{name}`

---

#### `listRecipes` — List saved launch recipes

```typescript
client.run.recipes.listRecipes()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/run/recipes`

---

#### `runRecipe` — Run using a saved recipe

```typescript
client.run.recipes.runRecipe(name: string, data: run_RecipeExecutionRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Recipe name |
| `data` | `run_RecipeExecutionRequest` | body | Yes |  |

**Returns:** `run_RunResponse`  |  **HTTP:** `POST /api/v1/run/recipes/{name}/run`

---

#### `searchRecipe` — Search using a saved recipe

```typescript
client.run.recipes.searchRecipe(name: string, data: run_RecipeExecutionRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Recipe name |
| `data` | `run_RecipeExecutionRequest` | body | Yes |  |

**Returns:** `run_SearchResponse`  |  **HTTP:** `POST /api/v1/run/recipes/{name}/search`

---

#### `updateRecipe` — Update a saved recipe

```typescript
client.run.recipes.updateRecipe(name: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `name` | `string` | path | Yes | Recipe name |
| `data` | `object` | body | Yes |  |

**Returns:** `run_RecipeConfig`  |  **HTTP:** `PATCH /api/v1/run/recipes/{name}`

---

### `client.run` (11) — APIs for searching and running applications across multiple package sources with automatic candidate ranking and selection

#### `healthCheck` — Service health check

```typescript
client.run.healthCheck()
```

**Returns:** `run_HealthResponse`  |  **HTTP:** `GET /api/v1/run/health`

---

#### `preflightRun` — Preflight a run request

```typescript
client.run.preflightRun(data: run_Selector)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_Selector` | body | Yes |  |

**Returns:** `run_PreflightResponse`  |  **HTTP:** `POST /api/v1/run/preflight`

---

#### `resolve` — Resolve an application via JSON body

```typescript
client.run.resolve(data: run_Selector)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_Selector` | body | Yes |  |

**Returns:** `run_RunResponse`  |  **HTTP:** `POST /api/v1/run/resolve`

---

#### `resolveGet` — Resolve an application and return exact shell command

```typescript
client.run.resolveGet(app: string, os?: string, source?: array, kind?: string, arch?: string, tags?: array, profile?: string, channel?: string, version?: string, variant?: string, publisher?: string, repo?: string, release?: string, asset?: string, pick?: string, pick_index?: integer, candidate_id?: string, set_id?: string, terminal_id?: integer, display?: string, origin?: string, dry_run?: boolean, print_curl?: string, format?: string, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `app` | `string` | query | Yes | Primary name query |
| `os` | `string` | query | No | Target OS filter |
| `source` | `array` | query | No | Source kind filter (repeatable) |
| `kind` | `string` | query | No | App kind filter |
| `arch` | `string` | query | No | Target CPU architecture filter |
| `tags` | `array` | query | No | Free-form tags for filtering and ranking (repeatable) |
| `profile` | `string` | query | No | Named profile for default preferences |
| `channel` | `string` | query | No | Release channel hint |
| `version` | `string` | query | No | Exact version or provider-defined version constraint |
| `variant` | `string` | query | No | Provider-specific variant hint |
| `publisher` | `string` | query | No | Publisher hint for curated registries |
| `repo` | `string` | query | No | Repository hint such as owner/name |
| `release` | `string` | query | No | Release hint such as a tag name |
| `asset` | `string` | query | No | Desired asset name or pattern |
| `pick` | `string` | query | No | Candidate selection mode (ask, first, index, id) |
| `pick_index` | `integer` | query | No | Candidate index (required when pick=index) |
| `candidate_id` | `string` | query | No | Specific candidate ID (required when pick=id) |
| `set_id` | `string` | query | No | Bind pick to a specific candidate set |
| `terminal_id` | `integer` | query | No | Terminal session ID (default 1) |
| `display` | `string` | query | No | X11 DISPLAY number |
| `origin` | `string` | query | No | Origin identifier for observability propagation |
| `dry_run` | `boolean` | query | No | If true, force command-only response (hoody-run never executes) |
| `print_curl` | `string` | query | No | Generate curl command (hoody-run) |
| `format` | `string` | query | No | Output format (json or html) |
| `limit` | `integer` | query | No | Max candidates (default 25) |

**Returns:** `run_RunResponse`  |  **HTTP:** `GET /api/v1/run/resolve`

---

#### `runBatch` — Execute a batch of search or run requests

```typescript
client.run.runBatch(data: run_BatchRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_BatchRequest` | body | Yes |  |

**Returns:** `run_BatchResponse`  |  **HTTP:** `POST /api/v1/run/batch`

---

#### `runPathBased` — Path-based resolve (positional or key-value)

```typescript
client.run.runPathBased(rest: string, os?: string, source?: array, kind?: string, arch?: string, tags?: array, profile?: string, channel?: string, version?: string, variant?: string, publisher?: string, repo?: string, release?: string, asset?: string, pick?: string, pick_index?: integer, candidate_id?: string, set_id?: string, terminal_id?: integer, display?: string, origin?: string, dry_run?: boolean, print_curl?: string, format?: string, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `rest` | `string` | path | Yes | Path segments for positional or key-value app specification |
| `os` | `string` | query | No | Target OS filter when not supplied in the path |
| `source` | `array` | query | No | Source kind filter (repeatable) |
| `kind` | `string` | query | No | App kind filter when not supplied in the path |
| `arch` | `string` | query | No | Target CPU architecture filter |
| `tags` | `array` | query | No | Free-form tags for filtering and ranking (repeatable) |
| `profile` | `string` | query | No | Named profile for default preferences |
| `channel` | `string` | query | No | Release channel hint |
| `version` | `string` | query | No | Exact version or provider-defined version constraint |
| `variant` | `string` | query | No | Provider-specific variant hint |
| `publisher` | `string` | query | No | Publisher hint for curated registries |
| `repo` | `string` | query | No | Repository hint such as owner/name |
| `release` | `string` | query | No | Release hint such as a tag name |
| `asset` | `string` | query | No | Desired asset name or pattern |
| `pick` | `string` | query | No | Candidate selection mode (ask, first, index, id) |
| `pick_index` | `integer` | query | No | Candidate index (required when pick=index) |
| `candidate_id` | `string` | query | No | Specific candidate ID (required when pick=id) |
| `set_id` | `string` | query | No | Bind pick to a specific candidate set |
| `terminal_id` | `integer` | query | No | Terminal session ID when not supplied in the path |
| `display` | `string` | query | No | X11 DISPLAY number |
| `origin` | `string` | query | No | Origin identifier for observability propagation |
| `dry_run` | `boolean` | query | No | If true, force command-only response (hoody-run never executes) |
| `print_curl` | `string` | query | No | Generate curl command (hoody-run) |
| `format` | `string` | query | No | Output format (json or html) |
| `limit` | `integer` | query | No | Max candidates (default 25) |

**Returns:** `run_RunResponse`  |  **HTTP:** `GET /api/v1/run/go/{rest}`

---

#### `runTerminalAnchored` — Terminal-anchored path-based resolve

```typescript
client.run.runTerminalAnchored(terminal_id: integer, rest: string, os?: string, source?: array, kind?: string, arch?: string, tags?: array, profile?: string, channel?: string, version?: string, variant?: string, publisher?: string, repo?: string, release?: string, asset?: string, pick?: string, pick_index?: integer, candidate_id?: string, set_id?: string, display?: string, origin?: string, dry_run?: boolean, print_curl?: string, format?: string, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `integer` | path | Yes | Terminal session ID (1-65535) |
| `rest` | `string` | path | Yes | Path segments for app specification |
| `os` | `string` | query | No | Target OS filter when not supplied in the path |
| `source` | `array` | query | No | Source kind filter (repeatable) |
| `kind` | `string` | query | No | App kind filter when not supplied in the path |
| `arch` | `string` | query | No | Target CPU architecture filter |
| `tags` | `array` | query | No | Free-form tags for filtering and ranking (repeatable) |
| `profile` | `string` | query | No | Named profile for default preferences |
| `channel` | `string` | query | No | Release channel hint |
| `version` | `string` | query | No | Exact version or provider-defined version constraint |
| `variant` | `string` | query | No | Provider-specific variant hint |
| `publisher` | `string` | query | No | Publisher hint for curated registries |
| `repo` | `string` | query | No | Repository hint such as owner/name |
| `release` | `string` | query | No | Release hint such as a tag name |
| `asset` | `string` | query | No | Desired asset name or pattern |
| `pick` | `string` | query | No | Candidate selection mode (ask, first, index, id) |
| `pick_index` | `integer` | query | No | Candidate index (required when pick=index) |
| `candidate_id` | `string` | query | No | Specific candidate ID (required when pick=id) |
| `set_id` | `string` | query | No | Bind pick to a specific candidate set |
| `display` | `string` | query | No | X11 DISPLAY number |
| `origin` | `string` | query | No | Origin identifier for observability propagation |
| `dry_run` | `boolean` | query | No | If true, force command-only response (hoody-run never executes) |
| `print_curl` | `string` | query | No | Generate curl command (hoody-run) |
| `format` | `string` | query | No | Output format (json or html) |
| `limit` | `integer` | query | No | Max candidates (default 25) |

**Returns:** `run_RunResponse`  |  **HTTP:** `GET /api/v1/run/t/{terminal_id}/go/{rest}`

---

#### `searchCandidates` — Search for app candidates

```typescript
client.run.searchCandidates(app: string, os?: string, source?: array, kind?: string, arch?: string, tags?: array, profile?: string, channel?: string, version?: string, variant?: string, publisher?: string, repo?: string, release?: string, asset?: string, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `app` | `string` | query | Yes | Primary name query (aliases q, name) |
| `os` | `string` | query | No | Target OS filter |
| `source` | `array` | query | No | Source kind filter (repeatable) |
| `kind` | `string` | query | No | App kind filter (gui, cli, any) |
| `arch` | `string` | query | No | Target CPU architecture filter |
| `tags` | `array` | query | No | Free-form tags for filtering and ranking (repeatable) |
| `profile` | `string` | query | No | Named profile for default preferences |
| `channel` | `string` | query | No | Release channel hint (for example stable or beta) |
| `version` | `string` | query | No | Exact version or provider-defined version constraint |
| `variant` | `string` | query | No | Provider-specific variant hint (for example portable or headless) |
| `publisher` | `string` | query | No | Publisher hint for curated registries |
| `repo` | `string` | query | No | Repository hint such as owner/name |
| `release` | `string` | query | No | Release hint such as a tag name |
| `asset` | `string` | query | No | Desired asset name or pattern |
| `limit` | `integer` | query | No | Max candidates to return (default 25) |

**Returns:** `run_SearchResponse`  |  **HTTP:** `GET /api/v1/run/search`

---

#### `searchCandidatesPaged` — Search for app candidates with cursor pagination

```typescript
client.run.searchCandidatesPaged(data: run_PagedSearchRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_PagedSearchRequest` | body | Yes |  |

**Returns:** `run_PagedSearchResponse`  |  **HTTP:** `POST /api/v1/run/search/paged`

---

#### `searchCandidatesPagedAll` — Search for app candidates with cursor pagination (collect all pages)

```typescript
client.run.searchCandidatesPagedAll(data: run_PagedSearchRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_PagedSearchRequest` | body | Yes |  |

**Returns:** `run_PagedSearchResponse[]`  |  **HTTP:** `POST /api/v1/run/search/paged`

---

#### `searchCandidatesPagedIterator` — Search for app candidates with cursor pagination (async iterator)

```typescript
client.run.searchCandidatesPagedIterator(data: run_PagedSearchRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_PagedSearchRequest` | body | Yes |  |

**Returns:** `AsyncIterableIterator<run_PagedSearchResponse>`  |  **HTTP:** `POST /api/v1/run/search/paged`

---

### `client.run.sources` (7) — APIs for managing package sources including CRUD operations, enable/disable, priority control, and sync triggers

#### `createSource` — Create a new package source

```typescript
client.run.sources.createSource(data: run_SourceConfig)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `run_SourceConfig` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/run/sources`

---

#### `deleteSource` — Delete a package source

```typescript
client.run.sources.deleteSource(source_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `source_id` | `string` | path | Yes | Source identifier |

**Returns:** `void`  |  **HTTP:** `DELETE /api/v1/run/sources/{source_id}`

---

#### `getSourceDiagnostics` — Get runtime diagnostics for a source

```typescript
client.run.sources.getSourceDiagnostics(source_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `source_id` | `string` | path | Yes | Source identifier |

**Returns:** `run_SourceDiagnostics`  |  **HTTP:** `GET /api/v1/run/sources/{source_id}/diagnostics`

---

#### `listSources` — List all package sources

```typescript
client.run.sources.listSources()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/run/sources`

---

#### `syncAllSources` — Sync all sources

```typescript
client.run.sources.syncAllSources()
```

**Returns:** `run_Job`  |  **HTTP:** `POST /api/v1/run/sources/sync`

---

#### `syncSource` — Sync a single source

```typescript
client.run.sources.syncSource(source_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `source_id` | `string` | path | Yes | Source identifier |

**Returns:** `run_Job`  |  **HTTP:** `POST /api/v1/run/sources/{source_id}/sync`

---

#### `updateSource` — Update a package source

```typescript
client.run.sources.updateSource(source_id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `source_id` | `string` | path | Yes | Source identifier |
| `data` | `object` | body | Yes |  |

**Returns:** `run_SourceConfig`  |  **HTTP:** `PATCH /api/v1/run/sources/{source_id}`


### Body schemas

- `run_PagedSearchRequest` — `{ selector*: run_Selector, cursor: string, page_size: int }`
- `run_Selector` — `{ app*: string, os: run_Os, kind: run_AppKind, source: run_SourceKind[], arch: run_Arch, tags: string[], profile: string, channel: string, version: string, variant: string, publisher: string, repo: string, release: string, asset: string, pick: run_PickMode, pick_index: int, candidate_id: string, set_id: string, terminal_id: int, display: string, origin: string, format: run_OutputFormat, dry_run: bool, print_curl: run_PrintCurlMode, limit: int }`
- `run_BatchRequest` — `{ items: run_BatchItemRequest[] }`
- `run_SourceConfig` — `{ source_id*: string, enabled*: bool, priority*: int, provider*: run_SourceKind, source_type*: run_SourceType, pin: run_SourcePin, config: object }`
- `run_ProfileConfig` — `{ name*: string, description: string, defaults: run_ProfileDefaults, sources_mode: run_ProfileSourceMode, sources: run_ProfileSourceOverride[], policy: run_PolicyConfig }`
- `run_RecipeConfig` — `{ name*: string, description: string, selector_template: run_SelectorTemplate, allowed_overrides: string[] }`
- `run_RecipeExecutionRequest` — `{ overrides: run_SelectorTemplate }`
- `run_Os` — `"linux" | "windows" | "any"`
- `run_AppKind` — `"gui" | "cli" | "any"`
- `run_SourceKind` — `"nix" | "pkgx" | "appimage" | "oci" | "registry" | "system" | "any"`
- `run_Arch` — `"amd64" | "arm64" | "any"`
- `run_PickMode` — `"ask" | "first" | "index" | "id"`
- `run_OutputFormat` — `"json" | "html"`
- `run_PrintCurlMode` — `"hoody-run"`
- `run_BatchItemRequest` — `{ request_id*: string, mode*: run_BatchMode, selector*: run_Selector }`
- `run_SourceType` — `"nix-pkgs" | "nix-flake" | "pkgx" | "app-image-pinned" | "app-image-git-hub-releases" | "app-image-catalog" | "oci-local-images" | "manifest-registry" | …(11 values)`
- `run_SourcePin` — `{ url*: string, sha256: string, author_pubkey_ed25519: string, sig_ed25519: string }`
- `run_ProfileDefaults` — `{ os: run_Os, kind: run_AppKind, source: run_SourceKind[], pick: run_PickMode, terminal_id: int, display: string, limit: int }`
- `run_ProfileSourceMode` — `"inherit" | "allowlist"`
- `run_ProfileSourceOverride` — `{ source_id*: string, enabled: bool, priority: int }`
- `run_PolicyConfig` — `{ require_verified: bool, require_integrity: bool, deny_providers: run_SourceKind[], deny_source_ids: string[] }`
- `run_SelectorTemplate` — `{ app: string, os: run_Os, kind: run_AppKind, source: run_SourceKind[], arch: run_Arch, tags: string[], profile: string, channel: string, version: string, variant: string, publisher: string, repo: string, release: string, asset: string, pick: run_PickMode, pick_index: int, candidate_id: string, set_id: string, terminal_id: int, display: string, origin: string, format: run_OutputFormat, dry_run: bool, print_curl: run_PrintCurlMode, limit: int }`
- `run_BatchMode` — `"search" | "run"`


---

<!-- ===== namespace: sqlite ===== -->

# `sqlite` — SQLite HTTP API

## Purpose

hoody-sqlite: SQL tx, JSON KV, history, time-travel. Keyed by `db` query param. No workspace scoping.

## When to use

- Durable structured state without Postgres.
- KV: TTL, CAS, atomic incr/decr/push/pop, JSON-path, per-key history.
- Multi-statement SQL tx over HTTP; time-travel rollback by N ops or timestamp.

## When NOT to use

Blobs → `files`, supervisors → `daemon`, notebooks → `notes`, control-plane → `api`.

## Prerequisites

- Outside `/hoody/databases` needs `--allow-any-absolute-db-path`.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### DB + SQL tx

`database.create` `path` (bare/`./name`/abs under `/hoody/databases`), `init_kv: true` for KV table → `database.executeTransaction` `{ transaction: [{ statement, values?|valuesBatch? }] }` (`create_db_if_missing: true` skips create) → `history.list`.

### KV CRUD + CAS + counters

- `kvStore.set` — `ttl`, `if_match` (CAS), `path`, `history`.
- `kvStore.get` — `path`, `at_timestamp`. `exists` takes only `db`/`table`; `delete` takes only `db`/`table`/`history` (`history` keeps the tombstone).
- `kvStore.incr`/`decr`/`push`/`pop`/`removeElement` — atomic, `path`-aware.

### Time-travel (needs `history: true`)

- `kvStore.getHistory` (default 50, max 1000); `kvStore.getSnapshot` at `op_number`.
- `kvStore.getTableSnapshot` / `kvStore.compareSnapshots` — Unix `timestamp` / diff.
- `kvStore.rollback` last N; `kvStore.rollbackTable`: `dry_run` (query) then `confirm: 'yes'` (query — NOT body field). Body is **required** by SDK/CLI; pass `{}` for full-table rollback or `{"keys":[...]}` / `{"exclude_keys":[...]}` to scope.

### Bulk + shareable

- `kvStore.batchSet`/`batchGet`/`batchDelete` — single SQLite tx.
- `query.executeShareable` — GET, URL-safe base64 `sql`, read-only. `health.getHealth`/`getHealthCache`.

## Quirks & gotchas

- **Bare-URL auth (no claim/token headers).** Like every kit (including `agent`), the `sqlite` kit accepts the bare per-container kit URL — no `X-Hoody-Container-Claim` or `X-Hoody-Token` headers required. The capability URL itself is the bearer.
- **Tx item key matters: `"query"` vs `statement` are NOT interchangeable.** Each `transaction[i]` MUST carry exactly one of `"query"` (returns rows) or `statement` (rows-affected only). If you put a SELECT under `statement`, you silently get back `rowsUpdated: 0` and no data. The `sql` alias maps to `statement`.
- Path resolution: bare names auto-resolve under `/hoody/databases/` (with `.db` appended if no extension). Relative paths containing `/` or `\` are **rejected**, NOT auto-absoluted; only literal absolute paths (e.g. `/hoody/databases/app.db`) are treated as absolute. Outside `/hoody/databases` needs `--allow-any-absolute-db-path`. Symlinks followed; outward targets rejected. `:memory:` databases are rejected.
- Directory mode requires absolute db (`directory-mode: invalid path input: path must be absolute`).
- Tx items: `statement` or alias `sql`. `executeTransaction` caps: 10k items, 100k rows/`valuesBatch`, 1M total rows. `values` and `valuesBatch` are mutually exclusive on a single item; `"query"` items cannot use `valuesBatch`.
- **GET `/query` rejects mutations**: INSERT/UPDATE/DELETE, `RETURNING` on writes, multi-statement (semicolons), PRAGMA writes, VACUUM, ATTACH/DETACH. Use `executeTransaction` with `statement:` items for writes.
- **SELECT result-row cap is 10 000** (responses set `truncated: true` when hit) on both transaction `"query"` items and GET `/query`; further rows silently truncated. Paginate explicitly for larger result sets.
- **`kvStore.set` body is a JSON-encoded STRING**, not an object. Generated SDK type is `string`; encode objects yourself before sending (e.g. JSON-stringify the value). Same for the `batchSet` per-item `value`.
- Time-travel **history is opt-out, not opt-in**: write handlers default `history: true`. Pass `history: false` to skip recording — but later `kvStore.getHistory` / snapshot / time-travel reads will see gaps (`has_gaps`, `gap_keys`, `candidate_truncated` fields). Per-key history reconstruction is capped at 50 000 ops.
- `create_db_if_missing`/`auto_create` aliases; mismatch → `conflicting flags`.
- `kvStore.list` w/ `at_timestamp` → time-travel handler (different envelope, ignores `offset`). `getHistory.limit`: 0→50, >1000→1000.
- `query.executeShareable` `sql` accepts URL-safe base64 (`+`→`-`, `/`→`_`); both padded and unpadded forms are accepted. Inputs that do not decode to a SELECT/WITH query are treated as raw SQL. No workspace scoping — the kit URL alone is the credential, share carefully.
- CLI: `hoody db` (aliases `sql`, `sqlite`); KV under `hoody kv`.

## Common errors

- `412 Value mismatch for CAS` (`if_match` mismatch) / `412 Key does not exist for CAS` (both CAS failures are 412).
- `400 directory-mode: invalid path input: path must be absolute`.
- `400 absolute database paths outside /hoody/databases are disallowed`.
- `400 in-memory databases are not supported`.
- `400 conflicting flags: create_db_if_missing and auto_create must match`.
- `400 GET /query only accepts read-only SELECT/WITH queries; use POST /db for mutating SQL` (returned for non-SELECT input; a non-base64 `sql` value is not an error — it is interpreted as raw SQL).
- `400 Invalid JSON body` on `batchSet` — wire shape requires each `value` to be a JSON-encoded string, not an object.
- `409 time-travel chain gap` when the requested timestamp falls inside a `history: false` window.
- Per-statement tx errors → `{ reqIdx, error }`; HTTP 200 if tx parsed.

## Related namespaces

`files` `.db` in `/hoody/databases/` · `exec` in-container · `notes` notebooks · `cron` schedule maintenance.

## Examples

Every step in every example was live-tested against a real `sqlite-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first, then choose a `DB` path. Bare names (`./mydb`) auto-resolve under `/hoody/databases/`; absolute paths outside that tree need the kit's `--allow-any-absolute-db-path` flag (`/tmp/...` works on dev kits).

**Two SQL field names that are NOT interchangeable:** in a transaction item, the `"query":"..."` key is for SELECT (returns `resultSet`/`resultHeaders`) and the `"statement":"..."` key is for DDL/DML (returns `rowsUpdated` only). Putting a SELECT under `"statement"` runs it but throws away rows — `rowsUpdated:0` even when matches exist. The `"sql"` alias maps to `"statement"`, not `"query"`.

### 1. Schema setup with idempotent multi-statement transaction

**Goal:** create a fresh database under `/tmp/`, install a 3-statement schema (table + index + seed row) atomically, then read it back. Every statement is `IF NOT EXISTS` / parameterised so the whole step is replay-safe.

**Step 1 — create the db file** with the kv table pre-seeded so KV ops on the same db don't have to bootstrap separately.

```typescript
const db = `/tmp/sqlite-examples-${Math.random().toString(36).slice(2)}.db`;
await client.sqlite.database.create({ path: db, init_kv: true });
```
**Step 2 — install schema** in a single transaction. Returns `{results:[...]}` with one entry per statement; `rowsUpdated:1` on the final INSERT confirms the seed landed.

```typescript
await client.sqlite.database.executeTransaction(
  { transaction: [
    { statement: 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE, created_at INTEGER)' },
    { statement: 'CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)' },
    { statement: 'INSERT OR IGNORE INTO users (name, email, created_at) VALUES (?, ?, ?)', values: ['Ada', 'ada@example.com', 1778191500] },
  ]},
  { db },
);
```
**Step 3 — read back using a SELECT under a transaction item with the `"query":"..."` key (NOT the `"statement":"..."` key — `statement` returns `rowsUpdated` only and silently drops rows).** The response carries `resultHeaders` + `resultSet` of column→value objects.

```typescript
const r = await client.sqlite.database.executeTransaction(
  { transaction: [{ query: 'SELECT id, name, email FROM users' }] },
  { db },
);
const rows = (r.data as any).results[0].resultSet;
```
### 2. KV CRUD with TTL — short-lived session token

**Goal:** store a per-user session blob with a 60-second TTL, prove `exists` flips to 404 after expiry, then explicitly delete.

**Step 1 — set with TTL.** The PUT body is the raw JSON value (string, object, array — kit infers `content_type`); query params carry `ttl` in seconds.

```typescript
await client.sqlite.kvStore.set('session:alex', JSON.stringify({ user_id: 'd6ec...', scopes: ['read', 'write'] }), { db, ttl: 60 });
```
**Step 2 — `HEAD` for existence** (zero-body, cheap). Returns `200` while live, `404` once TTL elapses.

```typescript
// The SDK THROWS on any non-2xx — a 404 never returns an inspectable object.
let present = true;
try { await client.sqlite.kvStore.exists('session:alex', { db }); }
catch (e: any) { if (e?.status !== 404) throw e; present = false; }
```
**Step 3 — explicit delete** (don't wait for TTL). `kvStore.delete` is NOT idempotent: deleting a missing key returns `404 Key not found`; on a hit it returns `{success:true,deleted:true}`. Wrap with try/catch or pre-check via `kvStore.get`.

```typescript
await client.sqlite.kvStore.delete('session:alex', { db });
```
### 3. Compare-and-swap on a versioned config blob

**Goal:** roll a config doc forward only when the current value matches what we last read. CAS uses `if_match` carrying the **literal raw value** (URL-encoded), not a hash — wrong value → `412 Value mismatch for CAS`.

**Step 1 — initial set** (no `if_match` needed; CAS only protects subsequent updates).

```typescript
await client.sqlite.kvStore.set('config', JSON.stringify({ version: 1, feature_x: false }), { db });
```
**Step 2 — read current**, then send the next version with `if_match` set to the exact JSON bytes you just read. Mismatched expected → `412`, request body is rejected.

```typescript
// CAS compares byte-exact against the stored bytes. SDK auto-parses the GET body
// into `cur.data` (an object), so `JSON.stringify(cur.data)` may diverge from
// what was stored (key order, whitespace). Use `rawResponse:true` to capture
// the exact stored bytes, OR remember the bytes you wrote and reuse them.
const curRaw = await client.sqlite.kvStore.get('config', { db, rawResponse: true, responseType: 'text' });
const ifMatch = curRaw as unknown as string;
await client.sqlite.kvStore.set('config', JSON.stringify({ version: 2, feature_x: true }), { db, if_match: ifMatch });
```
**Step 3 — observe a conflict** by sending stale `if_match`. Expect `HTTP 412 {"error":"Value mismatch for CAS"}` — the write is rejected without modifying the stored value.

```typescript
try {
  await client.sqlite.kvStore.set('config', JSON.stringify({ version: 99 }), { db, if_match: 'stale' });
} catch (e: any) { if (e?.status !== 412) throw e; /* CAS rejected */ }
```
### 4. Atomic counter for per-user rate limiting

**Goal:** hot-path increment/decrement without a transaction round-trip. `incr`/`decr` are server-side atomic and create the key on first hit; `delta` must be a POSITIVE integer (`delta <= 0` → `400 delta must be a positive integer`) — use `decr` for the negative direction. Useful for request quotas, login-attempt counters, work-queue depth.

**Step 1 — increment by 1** on each request. First call materialises the key as `text/plain` integer.

```typescript
const { data: r } = await client.sqlite.kvStore.incr('rate:alex:hour', { db, delta: 1 });
const count = (r as any).value;
```
**Step 2 — bulk-add 10** in one shot (e.g. credit refund). `delta` must be a positive integer; use `decr` to go the other way — a negative `delta` is rejected with `400`.

```typescript
await client.sqlite.kvStore.incr('rate:alex:hour', { db, delta: 10 });
```
**Step 3 — burn down by 3** (e.g. consume 3 quota units). Final value is plain text — `kvStore.get` returns the integer body directly, not wrapped.

```typescript
await client.sqlite.kvStore.decr('rate:alex:hour', { db, delta: 3 });
const final = await client.sqlite.kvStore.get('rate:alex:hour', { db }); // body = "8"
```
### 5. JSON-path read & partial update on a nested doc

**Goal:** stash a user-prefs document, read **one** field with `path=`, then mutate **only** that field without rewriting the whole blob. The path applies to both reads and writes.

**Step 1 — seed full document.**

```typescript
await client.sqlite.kvStore.set('profile', JSON.stringify({ name: 'Ada', prefs: { theme: 'dark', lang: 'en' } }), { db });
```
**Step 2 — read just `prefs.theme`**: returns the leaf value (`"dark"`), not the parent object.

```typescript
const { data: theme } = await client.sqlite.kvStore.get('profile', { db, path: 'prefs.theme' });
```
**Step 3 — patch one leaf**. The PUT body is the **new leaf value** (here `"light"`), not the full document. `lang` and `name` are untouched.

```typescript
await client.sqlite.kvStore.set('profile', '"light"', { db, path: 'prefs.theme' });
```
### 6. Time-travel — record three states of a feature flag, roll back two

**Goal:** undo the last two writes on a key without losing earlier history. Requires `history=true` on every write you want to be reversible.

**Step 1 — three sequential states** with history recording.

```typescript
for (const v of [{chat:false,voice:false},{chat:true,voice:false},{chat:true,voice:true}]) {
  await client.sqlite.kvStore.set('feature-flags', JSON.stringify(v), { db, history: true });
}
```
**Step 2 — inspect history** (`getHistory` returns newest first; each entry has `op_number` and `operation` — `operation.raw_old_value` / `operation.raw_new_value` are base64 and appear only for non-JSON content types).

```typescript
const { data: h } = await client.sqlite.kvStore.getHistory('feature-flags', { db, limit: 10 });
```
**Step 3 — roll back the last two ops** so `feature-flags` returns to `{chat:false,voice:false}`. Only the chosen key is affected.

```typescript
await client.sqlite.kvStore.rollback('feature-flags', { db, steps: 2 });
const { data: now } = await client.sqlite.kvStore.get('feature-flags', { db });
```
### 7. Snapshot at op-number, then diff against current

**Goal:** prove what a key looked like right after creation, then summarise every key that changed in a window. Uses `getSnapshot` (per-key, by `op_number`) and `compareSnapshots` (whole table, by Unix timestamps).

**Step 1 — fetch the per-key snapshot at `op_number=1`** (= the first state).

```typescript
const { data: s1 } = await client.sqlite.kvStore.getSnapshot('feature-flags', { db, op_number: 1 });
```
**Step 2 — record `from` and `to` timestamps** around a write window, then mutate so there is something to diff.

```typescript
const from = Math.floor(Date.now() / 1000); await new Promise(r => setTimeout(r, 1000));
await client.sqlite.kvStore.set('cmp-test', JSON.stringify({ v: 1 }), { db, history: true });
await new Promise(r => setTimeout(r, 1000));
const to = Math.floor(Date.now() / 1000);
```
**Step 3 — diff the table** between the two timestamps. `stats.created/modified/deleted` summarises; `changes[]` enumerates per-key.

```typescript
const { data: diff } = await client.sqlite.kvStore.compareSnapshots({ db, from, to });
```
### 8. Bulk batch — set / get / delete in single round-trips

**Goal:** seed three KV pairs, fetch them in one request (with one missing key to see the null payload), then drop them all. Each call wraps in a single SQLite tx; cap is 100 items per batch.

**Important wire-format detail:** in `batchSet`, every `value` must be a **string** (a JSON-encoded scalar/object). Sending a raw object → `400 Invalid JSON body`.

**Step 1 — bulk set with TTL on one item.**

```typescript
await client.sqlite.kvStore.batchSet({ items: [
  { key: 'u:1', value: JSON.stringify({ name: 'alice' }), content_type: 'application/json' },
  { key: 'u:2', value: JSON.stringify({ name: 'bob' }),   content_type: 'application/json' },
  { key: 'u:3', value: JSON.stringify({ name: 'carol' }), content_type: 'application/json', ttl: 3600 },
] } as any, { db });
```
**Step 2 — bulk get** (missing keys come back as `null`, present ones as `{value, content_type}`).

```typescript
const { data: got } = await client.sqlite.kvStore.batchGet({ keys: ['u:1','u:2','u:3','u:404'] } as any, { db });
```
**Step 3 — bulk delete.** Returns `{deleted: <count>, success: true}`. Missing keys silently no-op.

```typescript
await client.sqlite.kvStore.batchDelete({ keys: ['u:1','u:2','u:3'] } as any, { db });
```
### 9. Shareable read-only SQL via base64-encoded GET

**Goal:** mint a one-shot URL that runs a SELECT — safe to embed in dashboards / logs because the kit enforces read-only. `sql` is **URL-safe base64** (`+`→`-`, `/`→`_`); padding is optional — both padded and unpadded forms are accepted (the kit falls back to `base64.RawURLEncoding`).

**Step 1 — encode** the query.

```typescript
const sql = 'SELECT id, name, email FROM users LIMIT 10';
// Kit accepts URL-safe base64, padded or unpadded (falls back to
// base64.RawURLEncoding). Node's 'base64url' (unpadded) works as-is.
const sqlB64 = Buffer.from(sql).toString('base64url');
```
**Step 2 — issue the GET.** Response includes `columns`, `resultSet`, `rowCount`, `truncated`. A non-base64 `sql` value is not an error — it is interpreted as raw SQL; a non-SELECT query → `400 GET /query only accepts read-only SELECT/WITH queries; use POST /db for mutating SQL`.

```typescript
const { data: r } = await client.sqlite.query.executeShareable({ db, sql: sqlB64 });
```
**Step 3 — paste-able URL** (e.g. dashboard link). The kit URL itself is the auth grant — guard who you share it with.

```typescript
const url = `${kitUrl}/api/v1/sqlite/query?db=${encodeURIComponent(db)}&sql=${sqlB64}`;
```
### 10. Bulk insert via `valuesBatch` — one statement, many rows

**Goal:** load 3 rows (or 100k) through one prepared statement instead of one tx item per row. `valuesBatch` is an array of value-arrays positionally aligned with the `?` placeholders. Caps: 100k rows per `valuesBatch`, 1M rows per tx.

**Step 1 — bulk insert.** Response carries `rowsUpdatedBatch:[1,1,1]` — one entry per row.

```typescript
await client.sqlite.database.executeTransaction({ transaction: [{
  statement: 'INSERT INTO users (name, email, created_at) VALUES (?, ?, ?)',
  valuesBatch: [
    ['Bob','bob@example.com',1778000000],
    ['Carol','carol@example.com',1778000100],
    ['Dan','dan@example.com',1778000200],
  ],
}] }, { db });
```
**Step 2 — verify count** by sending a SELECT inside a transaction item with the `"query":"..."` key (NOT the `"statement":"..."` key).

```typescript
const r = await client.sqlite.database.executeTransaction(
  { transaction: [{ query: 'SELECT COUNT(*) AS n FROM users' }] }, { db });
const n = (r.data as any).results[0].resultSet[0].n;
```
**Step 3 — clean up** (drop the throwaway db file via `files`, or just leave under `/tmp/` for the next reboot to reclaim).

```typescript
await client.files.delete(db);
```

## Reference

**Accessor:** `client.sqlite`  |  **Import:** `import * as sqlite from 'hoody-sdk/sqlite'`

### `client.sqlite.database` (2) — Database

#### `create` — Create new SQLite database

```typescript
client.sqlite.database.create(path: string, init_kv?: boolean, kv_table?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `path` | `string` | query | Yes | Database path (absolute path, bare name, or./name shorthand resolved to /hoody/databases/*.db) |
| `init_kv` | `boolean` | query | No | Initialize KV store tables |
| `kv_table` | `string` | query | No | Custom KV table name |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/db/create`
**CLI:** `hoody db create`

---

#### `executeTransaction` — Execute SQL transaction

```typescript
client.sqlite.database.executeTransaction(db: string, create_db_if_missing?: boolean, data: sqlite_main.request)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database path (absolute path, bare name, or./name shorthand resolved to /hoody/databases/*.db) |
| `create_db_if_missing` | `boolean` | query | No | Create database file if it is missing |
| `data` | `sqlite_main.request` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/db`
**CLI:** `hoody db exec-transaction`

---

### `client.sqlite.docs` (2) — API documentation

#### `getJson` — Get OpenAPI specification (JSON redirect)

```typescript
client.sqlite.docs.getJson()
```

**Returns:** `void`  |  **HTTP:** `GET /api/v1/sqlite/openapi.json`

---

#### `getYaml` — Get OpenAPI specification (YAML)

```typescript
client.sqlite.docs.getYaml()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/openapi.yaml`

---

### `client.sqlite.health` (2) — Health

#### `getHealth` — Health check

```typescript
client.sqlite.health.getHealth()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/health`

---

#### `getHealthCache` — Cache health snapshot

```typescript
client.sqlite.health.getHealthCache()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/health/cache`

---

### `client.sqlite.history` (4) — Query execution history and statistics

#### `clear` — Clear query history

```typescript
client.sqlite.history.clear(db: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/sqlite/history`
**CLI:** `hoody db history clear`

---

#### `deleteEntry` — Delete history entry

```typescript
client.sqlite.history.deleteEntry(index: integer, db: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `index` | `integer` | path | Yes | History entry ID |
| `db` | `string` | query | Yes | Database file path |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/sqlite/history/{index}`
**CLI:** `hoody db history delete`

---

#### `getStats` — Get history statistics

```typescript
client.sqlite.history.getStats(db: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/history/stats`
**CLI:** `hoody db history stats`

---

#### `list` — Get query history

```typescript
client.sqlite.history.list(db: string, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |
| `limit` | `integer` | query | No | Maximum number of entries to return |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/history`
**CLI:** `hoody db history list`

---

### `client.sqlite.kvStore` (21) — Key-Value store operations with TTL and namespaces

#### `batchDelete` — Batch delete multiple keys

```typescript
client.sqlite.kvStore.batchDelete(db: string, table?: string, data: sqlite_main.kvBatchDeleteRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `data` | `sqlite_main.kvBatchDeleteRequest` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/batch/delete`
**CLI:** `hoody kv batch delete`

---

#### `batchGet` — Batch get multiple keys

```typescript
client.sqlite.kvStore.batchGet(db: string, table?: string, data: sqlite_main.kvBatchGetRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `data` | `sqlite_main.kvBatchGetRequest` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/batch/get`
**CLI:** `hoody kv batch get`

---

#### `batchSet` — Batch set multiple keys

```typescript
client.sqlite.kvStore.batchSet(db: string, table?: string, data: sqlite_main.kvBatchSetRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `data` | `sqlite_main.kvBatchSetRequest` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/batch/set`
**CLI:** `hoody kv batch set`

---

#### `compareSnapshots` — Compare table snapshots

```typescript
client.sqlite.kvStore.compareSnapshots(db: string, table?: string, from: integer, to: integer, keys?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `from` | `integer` | query | Yes | Starting timestamp (Unix) |
| `to` | `integer` | query | Yes | Ending timestamp (Unix) |
| `keys` | `string` | query | No | Comma-separated list of keys to compare (optional) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/kv/diff`
**CLI:** `hoody kv snapshots compare-table`

---

#### `decr` — Atomic decrement

```typescript
client.sqlite.kvStore.decr(key: string, db: string, table?: string, delta?: integer, path?: string, history?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `delta` | `integer` | query | No | Amount to decrement |
| `path` | `string` | query | No | JSON path to nested numeric value |
| `history` | `boolean` | query | No | Enable history tracking |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/{key}/decr`
**CLI:** `hoody kv decr`

---

#### `delete` — Delete key

```typescript
client.sqlite.kvStore.delete(key: string, db: string, table?: string, history?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path or directory |
| `table` | `string` | query | No | Custom table name |
| `history` | `boolean` | query | No | Enable history tracking |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/sqlite/kv/{key}`
**CLI:** `hoody kv delete`

---

#### `exists` — Check if key exists

```typescript
client.sqlite.kvStore.exists(key: string, db: string, table?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path or directory |
| `table` | `string` | query | No | Custom table name |

**Returns:** `any`  |  **HTTP:** `HEAD /api/v1/sqlite/kv/{key}`
**CLI:** `hoody kv exists`

---

#### `get` — Get value by key

```typescript
client.sqlite.kvStore.get(key: string, db: string, table?: string, path?: string, at_timestamp?: integer, rebuild?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name (supports / for hierarchical keys) |
| `db` | `string` | query | Yes | Database file path or directory |
| `table` | `string` | query | No | Custom table name |
| `path` | `string` | query | No | JSON path for nested value extraction |
| `at_timestamp` | `integer` | query | No | Unix timestamp for time-travel query (selects handleKVAtTimestamp) |
| `rebuild` | `boolean` | query | No | Rebuild cache (directory mode only) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/kv/{key}`
**CLI:** `hoody kv get`

---

#### `getHistory` — Get key operation history

```typescript
client.sqlite.kvStore.getHistory(key: string, db: string, table?: string, limit?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `limit` | `integer` | query | No | Maximum number of operations to return (0 → default 50, clamped to maximum 1000) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/kv/{key}/history`
**CLI:** `hoody kv history`

---

#### `getSnapshot` — Get key snapshot at operation

```typescript
client.sqlite.kvStore.getSnapshot(key: string, db: string, table?: string, op_number: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `op_number` | `integer` | query | Yes | Operation number to reconstruct from |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/kv/{key}/snapshot`
**CLI:** `hoody kv snapshots get-key`

---

#### `getTableSnapshot` — Get table snapshot at timestamp

```typescript
client.sqlite.kvStore.getTableSnapshot(db: string, table?: string, timestamp: integer, limit?: integer, prefix?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `timestamp` | `integer` | query | Yes | Unix timestamp to reconstruct from |
| `limit` | `integer` | query | No | Maximum number of keys to return |
| `prefix` | `string` | query | No | Filter keys by prefix |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/kv/snapshot`
**CLI:** `hoody kv snapshots get-table`

---

#### `incr` — Atomic increment

```typescript
client.sqlite.kvStore.incr(key: string, db: string, table?: string, delta?: integer, path?: string, history?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `delta` | `integer` | query | No | Amount to increment |
| `path` | `string` | query | No | JSON path to nested numeric value |
| `history` | `boolean` | query | No | Enable history tracking |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/{key}/incr`
**CLI:** `hoody kv incr`

---

#### `list` — List keys

```typescript
client.sqlite.kvStore.list(db: string, table?: string, prefix?: string, limit?: integer, offset?: integer, at_timestamp?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path or directory |
| `table` | `string` | query | No | Custom table name |
| `prefix` | `string` | query | No | Filter keys by prefix |
| `limit` | `integer` | query | No | Maximum number of results |
| `offset` | `integer` | query | No | Skip N results for pagination (regular LIST only; ignored when at_timestamp is set) |
| `at_timestamp` | `integer` | query | No | Unix timestamp for time-travel LIST (selects handleKVListAtTimestamp; returns a different envelope and ignores offset) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/kv`
**CLI:** `hoody kv list`

---

#### `listAll` — List keys (collect all pages)

```typescript
client.sqlite.kvStore.listAll(db: string, table?: string, prefix?: string, limit?: integer, offset?: integer, at_timestamp?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path or directory |
| `table` | `string` | query | No | Custom table name |
| `prefix` | `string` | query | No | Filter keys by prefix |
| `limit` | `integer` | query | No | Maximum number of results |
| `offset` | `integer` | query | No | Skip N results for pagination (regular LIST only; ignored when at_timestamp is set) |
| `at_timestamp` | `integer` | query | No | Unix timestamp for time-travel LIST (selects handleKVListAtTimestamp; returns a different envelope and ignores offset) |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/sqlite/kv`
**CLI:** `hoody kv list`

---

#### `listIterator` — List keys (async iterator)

```typescript
client.sqlite.kvStore.listIterator(db: string, table?: string, prefix?: string, limit?: integer, offset?: integer, at_timestamp?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path or directory |
| `table` | `string` | query | No | Custom table name |
| `prefix` | `string` | query | No | Filter keys by prefix |
| `limit` | `integer` | query | No | Maximum number of results |
| `offset` | `integer` | query | No | Skip N results for pagination (regular LIST only; ignored when at_timestamp is set) |
| `at_timestamp` | `integer` | query | No | Unix timestamp for time-travel LIST (selects handleKVListAtTimestamp; returns a different envelope and ignores offset) |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/sqlite/kv`
**CLI:** `hoody kv list`

---

#### `pop` — Remove from array end

```typescript
client.sqlite.kvStore.pop(key: string, db: string, table?: string, path?: string, history?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `path` | `string` | query | No | JSON path to nested array |
| `history` | `boolean` | query | No | Enable history tracking |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/{key}/pop`
**CLI:** `hoody kv arrays pop`

---

#### `push` — Append to array

```typescript
client.sqlite.kvStore.push(key: string, db: string, table?: string, path?: string, history?: boolean, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `path` | `string` | query | No | JSON path to nested array |
| `history` | `boolean` | query | No | Enable history tracking |
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/{key}/push`
**CLI:** `hoody kv arrays push`

---

#### `removeElement` — Remove array element

```typescript
client.sqlite.kvStore.removeElement(key: string, db: string, table?: string, path?: string, index?: integer, history?: boolean, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `path` | `string` | query | No | JSON path to nested array |
| `index` | `integer` | query | No | Array index to remove |
| `history` | `boolean` | query | No | Enable history tracking |
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/{key}/remove`
**CLI:** `hoody kv arrays delete`

---

#### `rollback` — Rollback key operations

```typescript
client.sqlite.kvStore.rollback(key: string, db: string, table?: string, steps?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `steps` | `integer` | query | No | Number of operations to rollback |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/{key}/rollback`
**CLI:** `hoody kv rollback`

---

#### `rollbackTable` — Rollback entire table

```typescript
client.sqlite.kvStore.rollbackTable(db: string, table?: string, to_timestamp: integer, dry_run?: boolean, confirm?: string, data: sqlite_main.kvTableRollbackRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `to_timestamp` | `integer` | query | Yes | Target timestamp to rollback to (Unix) |
| `dry_run` | `boolean` | query | No | Preview changes without applying |
| `confirm` | `string` | query | No | Must be 'yes' to execute actual rollback |
| `data` | `sqlite_main.kvTableRollbackRequest` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/kv/rollback`
**CLI:** `hoody kv rollback-table`

---

#### `set` — Set value for key

```typescript
client.sqlite.kvStore.set(key: string, db: string, table?: string, path?: string, ttl?: integer, if_match?: string, history?: boolean, create_db_if_missing?: boolean, data: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `key` | `string` | path | Yes | Key name |
| `db` | `string` | query | Yes | Database file path |
| `table` | `string` | query | No | Custom table name |
| `path` | `string` | query | No | JSON path for nested value update |
| `ttl` | `integer` | query | No | Time-to-live in seconds |
| `if_match` | `string` | query | No | Current value for compare-and-swap |
| `history` | `boolean` | query | No | Enable history tracking |
| `create_db_if_missing` | `boolean` | query | No | Create database file if it is missing |
| `data` | `string` | body | Yes |  |

**Body:** `string`

**Returns:** `any`  |  **HTTP:** `PUT /api/v1/sqlite/kv/{key}`
**CLI:** `hoody kv set`

---

### `client.sqlite.query` (1) — Query

#### `executeShareable` — Execute shareable SQL query

```typescript
client.sqlite.query.executeShareable(db: string, sql: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database file path |
| `sql` | `string` | query | Yes | Base64-encoded SQL query |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/sqlite/query`
**CLI:** `hoody db exec-shareable`

---

### `client.sqlite.sql` (1) — SQL database operations

#### `runMaintenance` — Run a database maintenance operation

```typescript
client.sqlite.sql.runMaintenance(db: string, timeout?: integer, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `db` | `string` | query | Yes | Database path (absolute path, bare name, or./name shorthand resolved to /hoody/databases/*.db) |
| `timeout` | `integer` | query | No | Request deadline in seconds (clamped to [1, 300]) |
| `data` | `object` | body | Yes |  |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/sqlite/maintenance`


### Body schemas

- `sqlite_main.request` — `{ resultFormat: string, transaction: sqlite_main.requestItem[] }`
- `sqlite_main.kvBatchDeleteRequest` — `{ keys*: string[] }`
- `sqlite_main.kvBatchGetRequest` — `{ keys*: string[] }`
- `sqlite_main.kvBatchSetRequest` — `{ items*: sqlite_main.kvBatchSetItem[] }`
- `sqlite_main.kvTableRollbackRequest` — `{ exclude_keys: string[], keys: string[] }`
- `sqlite_main.requestItem` — `{ noFail: bool, query: string, statement: string, values: int[], valuesBatch: int[][], sql: string }`
- `sqlite_main.kvBatchSetItem` — `{ content_type: string, key*: string, ttl: int, value: string }`


---

<!-- ===== namespace: terminal ===== -->

# `terminal` — Persistent multiplayer PTY sessions over HTTP and WebSocket

## Purpose

Real PTY per container, numeric `terminal_id` (1–65535). REST + WebSocket. Multiplayer; sessions persist.

## When to use

- **Interactive TUIs** that paint the screen (Claude Code, Codex, vim, htop, less, fzf, ssh, etc.) — these need a real PTY; only `terminal` provides one.
- **Durable / long-lived programs that you may need to interact with later** (the agent you spawned, a coding assistant, a chat REPL) — pin a stable `terminal_id` (1–39999) and reattach over WS or REST. Daemon-supervised processes have no TTY, so they can't host these.
- Sequenced commands sharing shell state, keystroke automation, screen capture, regex-search the rendered buffer.
- SSH / SOCKS5 sessions (`ssh_*` / `socks5_*`).
- Host introspection (`system.*`).

**Pin a unique `terminal_id` per program.** Re-using the same `terminal_id` for multiple programs writes both into the same PTY (output interleaves, prompts collide). Pick a distinct id per concurrent process — the `display-<terminal_id>` kit URL also pairs by id, so reusing breaks GUI routing too. **Never start a durable program with `ephemeral=true`** — ephemeral terminals auto-allocate from `40000–65535`, run the command, then evict at 300 s; your Claude Code / Codex session would die when the timer fires.

## When NOT to use

- Headless background process you don't need to interact with → `daemon` (supervised, log-captured, auto-restart).
- One-shot synchronous request/response → `exec`.
- File I/O → `files`. GUI rendering → `display`. Schedule → `cron`.

## Prerequisites

- Container with `terminal` kit running; capability URL.
- SSH needs reachable `ssh_host:ssh_port`; automation needs existing `terminal_id`.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Persistent interactive session

`sessions.create` (pin `terminal_id` or `ephemeral=true`) → `execution.execute` (`wait=true` is the default → sync; pass `wait=false` for async `command_id`; shares shell state) → `execution.getResult` → `sessions.getRawOutput`/`captureScreenshot` → `sessions.delete`.

### 2. Ephemeral one-off execute

`execution.execute` `ephemeral=true`, `wait=true` — auto ID 40000–65535, runs `cmd`, cleans up. Later: `getResult` before ephemeral-result-timeout (300s).

### 3. Automate a TUI

`sessions.create` (or `execute` to launch) → `pressTerminalKeys` (`Down`/`Enter`/`F2`; `listSupportedKeys`) → `pasteTerminalText` (`bracketed=true`) → `waitForTerminal` (`stable` or `pattern`) → `getTerminalSnapshot`/`findInTerminal`.

### 4. Live stream — WebSocket

`sessions.connectWebSocket` at `/api/v1/terminal/ws?terminal_id=…`. Multiple WS clients attach simultaneously; writes broadcast to PTY. Inject from REST via `client.terminal.write` or `pressTerminalKeys`. `abort` interrupts by `command_id`.

### 5. Container introspection

`system.listProcesses`, `getProcess`, `sendSignal`, `listPorts`, `getResources`, `getDisplayInfo`, `getDaemonConfig`, `reboot`/`shutdown`. Both `listProcesses` and `listPorts` also have streamed `*Iterator` variants for paginated traversal.

### 6. Spawn a durable agent CLI (Claude Code, Codex, …) and reattach later

For interactive coding agents and other long-lived TUIs the user may detach from and come back to:

1. Pick an unused `terminal_id` (1–39999, **never** the ephemeral range 40000–65535, **never** re-use one another program is on).
2. `sessions.create` with that pinned id, `ephemeral: false`, `shell: '/bin/bash'`, `cwd: '/workspace'` (or wherever).
3. `execution.execute` `cmd: 'claude code'` (or `codex`, `aider`, `gemini …`) with `wait: false` so the agent stays alive in the PTY rather than being treated as a sync request.
4. Reattach any time: `sessions.connectWebSocket` (multiplayer — multiple viewers / scripts can attach to the same PTY simultaneously), or REST via `pressTerminalKeys` / `pasteTerminalText` to drive it.
5. Tear down only when really done: `sessions.delete <terminal_id>`. The session persists until explicitly deleted or hit by `terminal-idle-timeout` (300 s default with zero attached clients and no running process). Sessions are in-memory only — a container reboot kills the PTY and drops the session; re-create after a reboot.

Two concurrent agents → two distinct `terminal_id`s (e.g. Claude Code on `1`, Codex on `2`). The `display-<id>` kit URL pairs by id, so non-collision keeps GUI routing clean too.

### 7. Launch a GUI app + control it via the display kit

Spin up a terminal, launch any X11 program, then drive it from the paired `display-<N>` kit. **You must explicitly pair the IDs** — the kit injects `DISPLAY` from the JSON `display` field on `sessions.create`; there is no automatic `terminal_id ⇒ DISPLAY=:N` mapping.

1. `sessions.create` with a pinned `terminal_id` (e.g. `1`) AND a matching `display: "1"` field (string in the SDK type), so the kit exports `DISPLAY=:1` into the PTY.
2. From that session: `execution.execute` `command: 'xeyes &'` (or `firefox &`, `gimp &`, `chromium-browser &`, `code &`, `xterm &`, …). The `&` returns the PTY immediately so the shell stays interactive; the GUI continues under `display-1`.
3. Open the matching display kit URL — `https://{projectId}-{containerId}-display-1.{node}.containers.hoody.com` (read `projectId`, `containerId`, and `server_name` from `containers.get`; the SDK exposes `getKitUrl('display', container, 1)` and `getKitUrls(container)` if you want to skip the string manipulation).
4. Drive the GUI via the `display` namespace:
   - `display.screenshots.capture` — see what's on screen (use `base64=true` for vision agents).
   - `display.input.clickAt` `{ x, y, button }` — left/right click; `display.input.typeAt` `{ text }` — keyboard.
   - `display.input.windowSearch` `{ name | class }` → `windowFocus` / `windowGeometry` / `windowActive` to focus + locate.
   - `display.input.batch` — bulk input replay; `display.input.wait` between actions.
5. Tear down: kill the X process via `system.sendSignal` from the terminal session, or `sessions.delete` to drop the whole shell + its child GUIs.

**Opening several GUI apps? Give each its own `terminal_id` + `display`.** Don't pile multiple apps onto one display — pair each app with a distinct id (`terminal_id=1`↔`display:":1"`, `terminal_id=2`↔`display:":2"`, …). Each then has its own `display-<N>` kit URL: a dedicated full-surface stream you can screenshot, embed / iframe, and drive input to **independently per window**, with no window-search/focus juggling. One display per app is almost always the right call; share a display only when you deliberately want them composited together.

For a turnkey full desktop instead of a single window, swap step 3 for the `desktop-<N>` alias (XFCE / MATE in a browser tab — see § Desktop alias in `SKILL-SDK.md`). The desktop alias auto-spawns the DE for you; this recipe is for spawning **specific** apps under your own control.

## Quirks & gotchas

- **Sharing a terminal URL = handing out root.** A `terminal-N` kit URL (or any alias pointed at it) lets anyone who can render it run arbitrary commands as root: read env / tokens / vault, exfiltrate files, install backdoors, mutate state. Capability-token semantics treat the URL itself as the credential — there is no per-recipient gate beyond what's configured in `proxyPermissionsContainer`. Share only with people you'd trust with `ssh root@…`. For wider audiences, gate (`setPasswordGroup` / `setTokenGroup` / `setIpGroup`), set an alias `expires_at`, watch `proxyLogs`, and prefer a constrained `exec` script or a read-only `display` stream over a live PTY.
- `terminal_id` numeric **1–65535**. **40000–65535 reserved for ephemeral**; pin manual IDs in 1–39999.
- `terminal_id=0` = sentinel "treat as absent".
- **Display pairing.** GUI apps run with the `DISPLAY` value supplied by the JSON `display` field on `sessions.create`. There is no automatic `terminal_id=N ⇒ DISPLAY=:N` mapping — if you want X11 rendering on display `:N`, pass `display` explicitly (either `"N"` or `":N"` — the kit normalises a bare number to `:N`). The `display-N` kit URL surface is independent of session id.
- `ephemeral=true` strips `DISPLAY`, skips display/dbus init — X11 won't render.
- `defer_pid` returns `/execute` immediately even with `wait=true`; queues until named PID exits (TUI-safe).
- **`/execute` body field is `command` (NOT `cmd`); request fails `400 Missing 'command' field` if you send `cmd`. The value is sent as RAW UTF-8 (written straight into the PTY unmodified); only the URL-form `?cmd=<base64>` is base64-decoded.**
- **`/execute` REQUIRES `?terminal_id=<n>` as a query parameter** unless `?ephemeral=true`; missing/non-numeric returns `400`. A body-only `terminal_id` is rejected by the validator before routing.
- Completion via `COMMAND_COMPLETED_MARKER_{id}` tail; stripped before `/result/{id}`. Programs swallowing it hang `wait=true`.
- **`wait=false` returns `status:"queued"` or `"running"` immediately** (NOT `"completed"`) — the kit only tracks the prompt-marker, not the underlying PID. Re-check actual output via `sessions.getRawOutput` / `getTerminalSnapshot`.
- **Screenshot `?format=` accepts `png | jpeg | jpg | gif`** at the kit level — `json` is invalid. (Note: the generated SDK type only allows `png | jpeg | gif`, so `jpg` works only via raw HTTP.)
- **`sendSignal` with `{name}` targets EVERY process matching that name** (returns `affected_pids`); use `{pid}` for surgical kills.
- Idle reaping: `terminal-idle-timeout` **300s**; `ephemeral-result-timeout` 300s (min 10s).
- `hoody pty` (and `hoody ssh`, `hoody run`) rewrite to `hoody shell` — an interactive PTY, or a one-shot via a POSITIONAL command (`hoody pty <cid> -- uname -a`). There is no `--command` flag and no `--ephemeral` flag on `shell`. For pinned ids on the automation surface use `terminal sessions exec --terminal-id <n>`.

## Common errors

- `400 Invalid terminal_id (must be numeric 1-65535)` on a non-numeric or out-of-range id; the lower-level validator logs a near-identical `0-65535` warning.
- `400` config-error on `sessions.create` — SSH/SOCKS5 partial validation (e.g. `ssh_user` without `ssh_host`, `socks5_port` out of range). The kit does NOT enforce mutual exclusion of `ssh_password` + `ssh_key`; both can coexist on a single session.
- `404` on `getResult` after ephemeral-result-timeout — buffer GC'd.
- "Unknown program name" on `proxyAliases.create` → use `program=exec`.

## Related namespaces

`exec`, `display`, `files`, `daemon`, `notifications`.

## Examples

Every step in every example was live-tested against a real `terminal-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first.

⚠ The HTTP routes take **`terminal_id` as a query parameter on `/execute`**, not in the body — a `terminal_id` field in the JSON body is silently ignored (the body parser only consumes the `command`, `id`, `timeout`, the boolean wait sync flag, `cwd` and `env` keys); missing the query param returns 400 `terminal_id parameter required` unless `?ephemeral=true`. Always pass `?terminal_id=N`. The `command` body field is sent as **raw UTF-8** (written straight into the PTY unmodified; only the URL form `?cmd=<base64>` is base64-decoded). `wait=true` returns when the kit sees the completion marker; programs that swallow the marker or only background-fork can return `status:"completed"` with empty stdout — re-check via `sessions.getRawOutput` if in doubt. SDK callers pass `terminal_id` / `ephemeral` / `defer_pid` / `display` / `ssh_*` in the **options object** (2nd arg), NOT in the body: `client.terminal.execution.execute({ command: 'echo hi' }, { terminal_id: '100', wait: true })`.

### 1. Persistent interactive session — create, run, capture, tear down

**Goal:** pin a stable PTY at `terminal_id=100`, run a command, fetch the result by `command_id`, then delete the session. All four steps live-verified.

**Step 1 — create the session.** `terminal_id` is required in the body; pin in `1–39999`.

```typescript
await client.terminal.sessions.create({ terminal_id: '100', shell: '/bin/bash', cols: 120, rows: 30 });
```
**Step 2 — execute** with `wait=true`. The body's `command` field is **raw UTF-8** (no base64).

```typescript
const r = await client.terminal.execution.execute(
  { command: 'echo HELLO; uname -a' },
  { terminal_id: '100', wait: true },
);
const commandId = r.data!.command_id;
```
**Step 3 — re-fetch the result later** via `getResult`. This is a pinned (non-ephemeral) session, so the result stays available until the session is idle-reaped at `terminal-idle-timeout` (300 s default).

```typescript
const out = await client.terminal.execution.getResult(commandId);
```
**Step 4 — clean up.** Always delete the session you created — autostart may re-spawn id 1, so explicit delete keeps your pinned ids tidy.

```typescript
await client.terminal.sessions.delete('100');
```
### 2. Ephemeral one-off — run a command without pinning anything

**Goal:** behave like `child_process.exec` — auto-allocated PTY, runs, evicts. No need to track a terminal_id.

```typescript
const r = await client.terminal.execution.execute(
  { command: 'date -u +%FT%TZ; uname -m' },
  { ephemeral: true, wait: true },
);
console.log(r.data!.stdout);
```
⚠ Ephemeral allocates from `40000–65535` and strips `DISPLAY` — never use it for GUI programs or anything you need to attach back to.

### 3. Automate a TUI — paste, press, wait, snapshot, find

**Goal:** drive an interactive program (here a simple shell echo, but the same recipe works for `htop`, `vim`, `fzf`, …). All five automation calls live-verified against a real session.

**Step 1 — create the session and paste a line** (raw, not base64; bracketed-paste optional).

```typescript
await client.terminal.sessions.create({ terminal_id: '101' });
// terminal_id is a QUERY param — it goes in the 2nd options arg, NOT the body.
await client.terminal.terminalAutomation.pasteTerminalText(
  { text: 'echo PASTED_TEXT', bracketed: false },
  { terminal_id: '101' },
);
```
**Step 2 — press Enter, wait for the screen to go stable, snapshot + regex-find.**

```typescript
await client.terminal.terminalAutomation.pressTerminalKeys({ keys: ['enter'] }, { terminal_id: '101' });
await client.terminal.terminalAutomation.waitForTerminal(
  { mode: 'stable', debounce_ms: 500, timeout_ms: 3000 },
  { terminal_id: '101' },
);
const snap = await client.terminal.terminalAutomation.getTerminalSnapshot({ terminal_id: '101' });
const hits = await client.terminal.terminalAutomation.findInTerminal({ terminal_id: '101', pattern: 'PASTED' });
```
**Step 3 — discover what keys you can press** (named keys differ per kit build):

```typescript
const keys = await client.terminal.terminalAutomation.listSupportedKeys();
```
Cleanup: `DELETE /api/v1/terminal/101`.

### 4. WebSocket attach for live streaming

**Goal:** subscribe to a PTY for live output while still driving it from REST. Multiple clients can attach; writes broadcast.

**Step 1 — create or reuse a session, then connect WS.** `wss://` URL, `terminal_id` in query. Inject input via REST `/write` or `/press`; the WS receives the rendered bytes.

```typescript
await client.terminal.sessions.create({ terminal_id: '102' });
// Generated WS wrapper exposes connect()/sendInput()/onOutput()/onDisconnect()/onError()/close()
const ws = await client.terminal.sessions.connectWebSocket({ terminal_id: '102', readonly: true });
ws.onOutput((buf) => process.stdout.write(buf));   // callback receives Uint8Array directly
await ws.connect();
await client.terminal.write({ input: 'echo VIA_WRITE\n' }, { terminal_id: '102' });
```
`readonly=true` blocks input from this client only; other attached clients keep their write rights. Cleanup: `sessions.delete(102)`.

### 5. Container introspection — processes, ports, resources, displays, daemon-config

**Goal:** one-call situational awareness. All five endpoints live-verified.

```typescript
const res = await client.terminal.system.getResources();
const procs = await client.terminal.system.listProcesses({ limit: 5 });
const ports = await client.terminal.system.listPorts();
const disp = await client.terminal.system.getDisplayInfo();
const cfg = await client.terminal.system.getDaemonConfig();
const init = await client.terminal.system.getProcess(1);
```
⚠ `system.reboot` and `system.shutdown` exist on the same surface — don't call them on a shared dev container, they wipe in-memory state.

### 6. Launch a GUI app + verify it's running on the paired display

**Goal:** start `xeyes &` from `terminal_id=10`, then read `display-10` in the `display` namespace to see the window. **You must pair the ids explicitly**: pass `display: 10` on `sessions.create` so the kit exports `DISPLAY=:10` (the kit does NOT auto-derive DISPLAY from `terminal_id`).

**Step 1 — create the session with display pairing, launch the GUI** (background it with `&` so the PTY stays free):

```typescript
await client.terminal.sessions.create({ terminal_id: '10', display: '10' });
await client.terminal.execution.execute({ command: 'xeyes &' }, { terminal_id: '10', wait: true });
```
**Step 2 — verify display-10 actually has a window** — query system displays from the same kit, then drive it from the `display-10` URL:

```typescript
const displays = await client.terminal.system.getDisplayInfo();
const ten = displays.data!.find(d => d.display === 10);
// then drive via client.display.* targeting display-10
```
Cleanup: kill `xeyes` via `system.sendSignal { name: 'xeyes', signal: 'SIGTERM' }` or just `sessions.delete(10)` (drops the shell + child GUIs).

### 7. SSH session through the terminal kit

**Goal:** open an SSH PTY to a remote host through the container's network. The kit's `/create` accepts `ssh_*` fields and the resulting session looks like any other PTY (paste/press/snapshot/WS all work the same). The kit accepts both `ssh_password` and `ssh_key` together (the underlying `ssh` client picks key first, then password) — there is no mutual-exclusion error.

```typescript
await client.terminal.sessions.create({
  terminal_id: '11', shell: 'ssh',
  ssh_host: '10.0.0.42', ssh_user: 'deploy', ssh_port: '22', ssh_password: process.env.SSH_PASSWORD,
});
await client.terminal.execution.execute({ command: 'hostname; whoami' }, { terminal_id: '11', wait: true });
```
For SOCKS5, swap to `socks5_host` / `socks5_port` / `socks5_user` / `socks5_pass`. Common 400 config-error triggers: `ssh_user` without `ssh_host`, `socks5_port` out of range; `ssh_password` and `ssh_key` may be sent together (no mutual-exclusion error).

### 8. Spawn a durable agent CLI and reattach over WS

**Goal:** start a long-running TUI (Claude Code, Codex, vim, …) at a pinned `terminal_id`, walk away, come back later from a different host.

**Step 1 — pin id, create, launch with `wait=false`** so the request returns instantly while the agent stays alive in the PTY:

```typescript
await client.terminal.sessions.create({
  terminal_id: '50', shell: 'bash', cwd: '/workspace',
});
await client.terminal.execution.execute(
  { command: 'sleep 600; echo agent-stopped' },
  { terminal_id: '50', wait: false, cwd_auto_create: true },
);
```
**Step 2 — reattach later** — same `terminal_id`, WS or REST, multiplayer:

```typescript
const ws = await client.terminal.sessions.connectWebSocket({ terminal_id: '50' });
const snap = await client.terminal.terminalAutomation.getTerminalSnapshot({ terminal_id: '50' });
```
⚠ `wait=false` returns `status:"queued"` or `"running"` immediately (NOT `"completed"`) because the kit only tracks the prompt marker, not the underlying PID — that's expected; the agent keeps running. Re-check actual output via `sessions.getRawOutput` / `getTerminalSnapshot`. **Never** start a durable agent with `ephemeral=true`: the 300 s sweep would kill it.

### 9. `defer_pid` — schedule a command to run after a parent process exits

**Goal:** queue command B so it only fires after pid `<PID>` finishes. Useful when you want to chain "after this build finishes, run tests" without watching the process from outside.

```typescript
await client.terminal.sessions.create({ terminal_id: '60' });
await client.terminal.execution.execute(
  { command: 'echo build-finished; ./run-tests.sh' },
  { terminal_id: '60', defer_pid: 12345, wait: true },
);
```
`defer_pid` returns `/execute` immediately even with `wait=true` (TUI-safe — see Quirks); it queues the body and runs it once the named PID exits. Pair `defer_start_time_ticks` to disambiguate PID reuse.

### 10. Cancel a running command + kill misbehaving processes

**Goal:** abort a hung `/execute` by `command_id`, then escalate to a process-level signal if the underlying program ignored SIGINT.

**Step 1 — submit async (`wait=false`), capture `command_id`.**

```typescript
await client.terminal.sessions.create({ terminal_id: '70' });
const r = await client.terminal.execution.execute({ command: 'sleep 120' }, { terminal_id: '70', wait: false });
const cid = r.data!.command_id;
```
**Step 2 — abort** the command tracker. Add `force:true` to send SIGKILL; default sends SIGINT.

```typescript
await client.terminal.abort(cid, { force: true });
```
**Step 3 — if the program survives** (ignored SIGINT, double-fork'd, etc.), escalate via `system.sendSignal` by name. Targets every process matching the name.

```typescript
await client.terminal.system.sendSignal({ name: 'sleep', signal: 'SIGTERM' });
```
Cleanup: `sessions.delete(70)`. ⚠ Never call `system.shutdown` / `system.reboot` to recover from a hung command — they wipe the entire container.

## Reference

**Accessor:** `client.terminal`  |  **Import:** `import * as terminal from 'hoody-sdk/terminal'`

### `client.terminal.docs` (2) — Self-documenting API specification endpoints in JSON and YAML formats

#### `getJson` — Get OpenAPI specification in JSON format

```typescript
client.terminal.docs.getJson()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/openapi.json`

---

#### `getYaml` — Get OpenAPI specification in YAML format

```typescript
client.terminal.docs.getYaml()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/openapi.yaml`

---

### `client.terminal.execution` (2) — APIs for executing commands in terminal sessions and retrieving their results

#### `execute` — Execute command in terminal session

```typescript
client.terminal.execution.execute(terminal_id?: string, ephemeral?: boolean, defer_pid?: integer, defer_start_time_ticks?: string, defer_timeout_ms?: integer, defer_poll_ms?: integer, reset?: boolean, cwd?: string, cwd_auto_create?: boolean, shell?: string, user?: string, cmd?: string, env?: string, skip_display_wait?: boolean, display_wait_timeout?: integer, display?: string, ssh_host?: string, ssh_user?: string, ssh_port?: string, ssh_password?: string, socks5_host?: string, socks5_port?: string, socks5_user?: string, ssh_key?: string, socks5_pass?: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | No | Terminal session ID (numeric 1-65535). Use terminal_id=0 as an explicit sentinel meaning "no terminal ID" (treated as absent, useful when a reverse proxy always injects a terminal_id). Required unless ephemeral=true, in which case it is auto-generated if not provided |
| `ephemeral` | `boolean` | query | No | When true, auto-generates a unique terminal_id (if not provided), skips display/dbus initialization, and applies aggressive cleanup. Designed for programmatic CLI command execution like child_process.exec (default: false). WARNING: Do NOT use ephemeral=true for GUI applications that require a display. Ephemeral sessions strip the DISPLAY environment variable, which means X11/GUI applications will not work. Use a regular terminal session with an explicit terminal_id and display parameter instead for GUI workloads |
| `defer_pid` | `integer` | query | No | Defer command injection until this PID exits (TUI-safe). If set, the API returns immediately regardless of wait=true |
| `defer_start_time_ticks` | `string` | query | No | Optional /proc/<pid>/stat field 22 (starttime in clock ticks since boot) to avoid PID reuse bugs. If it mismatches, command executes immediately |
| `defer_timeout_ms` | `integer` | query | No | Max time to wait for defer_pid exit before failing (default: 60000) |
| `defer_poll_ms` | `integer` | query | No | Poll interval while waiting for defer_pid exit (default: 50, minimum: 10) |
| `reset` | `boolean` | query | No | Reset existing session and reconfigure (kills current process, clears state, allows switching from bash to SSH or changing any parameter) - Use 'true', '1', or no value |
| `cwd` | `string` | query | No | Working directory for local bash sessions (ignored for SSH) |
| `cwd_auto_create` | `boolean` | query | No | Auto-create cwd when the requested working directory does not exist yet. Only applies when cwd is explicitly provided for a new or reset local session. Enable with 'true', '1', or no value (default: false) |
| `shell` | `string` | query | No | Shell to use for local sessions: bash (case-insensitive), zsh, fish, sh, etc. (default: server startup command, only applies to new sessions or after reset) |
| `user` | `string` | query | No | System user to spawn shell as (requires su permissions, only applies to new sessions or after reset) |
| `cmd` | `string` | query | No | Base64-encoded command to execute automatically (works with both new and active shells, executes every time URL is visited) |
| `env` | `string` | query | No | Environment variable in KEY=VALUE format (can be repeated for multiple variables, e.g., ?env=DEBUG=1&env=API_KEY=abc) |
| `skip_display_wait` | `boolean` | query | No | Skip waiting for Hoody Display readiness before executing command. By default, if a DISPLAY is configured, the endpoint blocks until the display server on port 4000+display_num is ready (default: false) |
| `display_wait_timeout` | `integer` | query | No | Timeout in seconds for display readiness wait (default: 10, capped at 10 seconds to prevent event-loop pin; values <=0 or malformed also map to the 10-second cap). Ignored if skip_display_wait=true |
| `display` | `string` | query | No | DISPLAY environment variable for X11 applications (auto-formats:display if number provided, e.g., ?display=1 becomes DISPLAY=:1) |
| `ssh_host` | `string` | query | No | SSH server hostname or IP address (creates SSH session if provided with ssh_user) |
| `ssh_user` | `string` | query | No | SSH username (required if ssh_host is provided) |
| `ssh_port` | `string` | query | No | SSH port number (default: 22) |
| `ssh_password` | `string` | query | No | SSH password for authentication (use with caution, prefer key-based auth) |
| `socks5_host` | `string` | query | No | SOCKS5 proxy hostname for SSH connection |
| `socks5_port` | `string` | query | No | SOCKS5 proxy port (default: 1080) |
| `socks5_user` | `string` | query | No | SOCKS5 proxy username for authentication |
| `ssh_key` | `string` | query | No | Base64-encoded SSH private key for key-based authentication (prefer over password-based auth) |
| `socks5_pass` | `string` | query | No | SOCKS5 proxy password for authentication |
| `data` | `object` | body | Yes |  |

**Body:** `{ command*: string, id: string, timeout: int, wait: bool, cwd: string, env: object }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/execute`
**CLI:** `hoody terminal sessions exec`

---

#### `getResult` — Get command result

```typescript
client.terminal.execution.getResult(command_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `command_id` | `string` | path | Yes | Command ID returned from /api/v1/terminal/execute (numeric 1-65535) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/result/{command_id}`
**CLI:** `hoody terminal sessions command-result`

---

### `client.terminal.health` (1) — Health

#### `check` — Service health check

```typescript
client.terminal.health.check()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/health`
**CLI:** `hoody terminal health`

---

### `client.terminal.sessions` (11) — APIs for managing terminal sessions, retrieving output, and viewing session history

#### `captureScreenshot` — Capture terminal screenshot

```typescript
client.terminal.sessions.captureScreenshot(terminal_id: string, format?: string, foreground?: string, background?: string, fontsize?: integer, save?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID (numeric 1-65535) |
| `format` | `string` | query | No | Output format: png, jpeg, gif (default: png) |
| `foreground` | `string` | query | No | Foreground color: black, red, green, yellow, blue, magenta, cyan, white, or RGB (R,G,B,A) (default: white) |
| `background` | `string` | query | No | Background color: same as foreground options (default: black) |
| `fontsize` | `integer` | query | No | Font size in pixels (default: 20) |
| `save` | `boolean` | query | No | Save to storage directory (default: true) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/screenshot`
**CLI:** `hoody terminal sessions screenshot`

---

#### `connectWebSocket` — WebSocket terminal connection

```typescript
client.terminal.sessions.connectWebSocket(terminal_id?: string, readonly?: boolean, cwd?: string, cwd_auto_create?: boolean, shell?: string, user?: string, cmd?: string, env?: string, display?: string, pid?: integer, ssh_host?: string, ssh_user?: string, ssh_port?: string, ssh_password?: string, socks5_host?: string, socks5_port?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | No | Terminal session ID (numeric 1-65535, auto-generated if not provided) - Multiple clients can share by using same ID |
| `readonly` | `boolean` | query | No | Enable read-only mode for this client (blocks keyboard input) - Use 'true', '1', or no value |
| `cwd` | `string` | query | No | Working directory for new sessions |
| `cwd_auto_create` | `boolean` | query | No | Auto-create cwd when the requested working directory does not exist yet. Only applies when cwd is explicitly provided for a new local session. Enable with 'true', '1', or no value (default: false) |
| `shell` | `string` | query | No | Shell to use (bash, zsh, fish, tmux, ssh, etc.) |
| `user` | `string` | query | No | System user to spawn shell as (requires permissions) |
| `cmd` | `string` | query | No | Base64-encoded command to auto-execute on spawn |
| `env` | `string` | query | No | Environment variable KEY=VALUE (repeatable) |
| `display` | `string` | query | No | DISPLAY variable for X11 apps (auto-formats:N) |
| `pid` | `integer` | query | No | Attach to existing process PID for monitoring |
| `ssh_host` | `string` | query | No | SSH server hostname/IP for remote connections |
| `ssh_user` | `string` | query | No | SSH username (required if ssh_host provided) |
| `ssh_port` | `string` | query | No | SSH port (default: 22) |
| `ssh_password` | `string` | query | No | SSH password (use with caution) |
| `socks5_host` | `string` | query | No | SOCKS5 proxy for SSH |
| `socks5_port` | `string` | query | No | SOCKS5 port (default: 1080) |

**Returns:** `void`  |  **HTTP:** `GET /api/v1/terminal/ws`
**CLI:** `hoody terminal sessions connect`

---

#### `create` — Create a terminal session

```typescript
client.terminal.sessions.create(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ terminal_id: string, ephemeral: bool, display: string, shell: string, user: string, cwd: string, startup_script: string, welcome: bool, debug: bool, desktop: bool, desktop_env: string, cols: int, rows: int, wait_until_display: bool, wait_timeout: int, ssh_host: string, ssh_user: string, ssh_port: string, ssh_password: string, ssh_key: string, socks5_host: string, socks5_port: string, socks5_user: string, socks5_pass: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/create`
**CLI:** `hoody terminal sessions create`

---

#### `delete` — Delete a terminal session

```typescript
client.terminal.sessions.delete(terminal_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | path | Yes | Terminal session ID to delete (numeric 1-65535) |

**Returns:** `any`  |  **HTTP:** `DELETE /api/v1/terminal/{terminal_id}`
**CLI:** `hoody terminal sessions delete`

---

#### `getRawOutput` — Get raw terminal output

```typescript
client.terminal.sessions.getRawOutput(terminal_id?: string, format?: string, tail?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | No | Terminal session ID (numeric 1-65535, defaults to "1" if not provided) |
| `format` | `string` | query | No | Output format: download, text, or html (defaults to "download" if not provided) |
| `tail` | `integer` | query | No | Return only the last N lines of output |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/raw`
**CLI:** `hoody terminal sessions raw-output`

---

#### `list` — List all terminal sessions

```typescript
client.terminal.sessions.list(history_limit?: integer, history_lines?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `history_limit` | `integer` | query | No | Max command_history entries to include per session (default: 50, max: 1000) |
| `history_lines` | `integer` | query | No | Alias of history_limit |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/sessions`
**CLI:** `hoody terminal sessions list`

---

#### `listAll` — List all terminal sessions (collect all pages)

```typescript
client.terminal.sessions.listAll(history_limit?: integer, history_lines?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `history_limit` | `integer` | query | No | Max command_history entries to include per session (default: 50, max: 1000) |
| `history_lines` | `integer` | query | No | Alias of history_limit |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/terminal/sessions`
**CLI:** `hoody terminal sessions list`

---

#### `listHistory` — Get terminal command history

```typescript
client.terminal.sessions.listHistory(terminal_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | path | Yes | Terminal session ID (numeric 1-65535, can also be provided as query parameter) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/history/{terminal_id}`
**CLI:** `hoody terminal sessions history`

---

#### `listHistoryAll` — Get terminal command history (collect all pages)

```typescript
client.terminal.sessions.listHistoryAll(terminal_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | path | Yes | Terminal session ID (numeric 1-65535, can also be provided as query parameter) |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/terminal/history/{terminal_id}`
**CLI:** `hoody terminal sessions history`

---

#### `listHistoryIterator` — Get terminal command history (async iterator)

```typescript
client.terminal.sessions.listHistoryIterator(terminal_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | path | Yes | Terminal session ID (numeric 1-65535, can also be provided as query parameter) |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/terminal/history/{terminal_id}`
**CLI:** `hoody terminal sessions history`

---

#### `listIterator` — List all terminal sessions (async iterator)

```typescript
client.terminal.sessions.listIterator(history_limit?: integer, history_lines?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `history_limit` | `integer` | query | No | Max command_history entries to include per session (default: 50, max: 1000) |
| `history_lines` | `integer` | query | No | Alias of history_limit |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/terminal/sessions`
**CLI:** `hoody terminal sessions list`

---

### `client.terminal.system` (15) — APIs for monitoring system resources, processes, network ports, and controlling system state

#### `freezeProcess` — Freeze (SIGSTOP) a process or process tree

```typescript
client.terminal.system.freezeProcess(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ pid: int, name: string, include_descendants: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/system/processes/freeze`

---

#### `getDaemonConfig` — Get daemon programs configuration

```typescript
client.terminal.system.getDaemonConfig()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/system/daemon`
**CLI:** `hoody terminal system daemon-config`

---

#### `getDisplayInfo` — Get display information

```typescript
client.terminal.system.getDisplayInfo()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/system/displays`
**CLI:** `hoody terminal system display-info`

---

#### `getProcess` — Get process details by PID

```typescript
client.terminal.system.getProcess(pid: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `pid` | `integer` | path | Yes | Process ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/system/processes/{pid}`
**CLI:** `hoody terminal processes get`

---

#### `getResources` — Get system resources and statistics

```typescript
client.terminal.system.getResources()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/system/resources`
**CLI:** `hoody terminal system resources`

---

#### `listPorts` — List all listening network ports

```typescript
client.terminal.system.listPorts(protocol?: string, user?: string, port?: integer, ip?: string, skip_program?: string, http_only?: boolean, hoody_only?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `protocol` | `string` | query | No | Filter by protocol: tcp, udp, or comma-separated list |
| `user` | `string` | query | No | Filter by user (exact match) |
| `port` | `integer` | query | No | Filter by specific port number |
| `ip` | `string` | query | No | Filter by IP address (comma-separated list) |
| `skip_program` | `string` | query | No | Exclude specific programs (comma-separated list) |
| `http_only` | `boolean` | query | No | Only return HTTP services |
| `hoody_only` | `boolean` | query | No | Only return Hoody Kit services |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/system/ports`
**CLI:** `hoody terminal system ports`

---

#### `listPortsAll` — List all listening network ports (collect all pages)

```typescript
client.terminal.system.listPortsAll(protocol?: string, user?: string, port?: integer, ip?: string, skip_program?: string, http_only?: boolean, hoody_only?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `protocol` | `string` | query | No | Filter by protocol: tcp, udp, or comma-separated list |
| `user` | `string` | query | No | Filter by user (exact match) |
| `port` | `integer` | query | No | Filter by specific port number |
| `ip` | `string` | query | No | Filter by IP address (comma-separated list) |
| `skip_program` | `string` | query | No | Exclude specific programs (comma-separated list) |
| `http_only` | `boolean` | query | No | Only return HTTP services |
| `hoody_only` | `boolean` | query | No | Only return Hoody Kit services |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/system/ports`
**CLI:** `hoody terminal system ports`

---

#### `listPortsIterator` — List all listening network ports (async iterator)

```typescript
client.terminal.system.listPortsIterator(protocol?: string, user?: string, port?: integer, ip?: string, skip_program?: string, http_only?: boolean, hoody_only?: boolean)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `protocol` | `string` | query | No | Filter by protocol: tcp, udp, or comma-separated list |
| `user` | `string` | query | No | Filter by user (exact match) |
| `port` | `integer` | query | No | Filter by specific port number |
| `ip` | `string` | query | No | Filter by IP address (comma-separated list) |
| `skip_program` | `string` | query | No | Exclude specific programs (comma-separated list) |
| `http_only` | `boolean` | query | No | Only return HTTP services |
| `hoody_only` | `boolean` | query | No | Only return Hoody Kit services |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/system/ports`
**CLI:** `hoody terminal system ports`

---

#### `listProcesses` — List all system processes

```typescript
client.terminal.system.listProcesses(sort?: string, limit?: integer, filter?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `sort` | `string` | query | No | Sort by field: cpu, memory, pid, name (default: pid) |
| `limit` | `integer` | query | No | Maximum number of processes to return (default: all) |
| `filter` | `string` | query | No | Filter by process name (substring match, case-insensitive) |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/system/processes`
**CLI:** `hoody terminal processes list`

---

#### `listProcessesAll` — List all system processes (collect all pages)

```typescript
client.terminal.system.listProcessesAll(sort?: string, limit?: integer, filter?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `sort` | `string` | query | No | Sort by field: cpu, memory, pid, name (default: pid) |
| `limit` | `integer` | query | No | Maximum number of processes to return (default: all) |
| `filter` | `string` | query | No | Filter by process name (substring match, case-insensitive) |

**Returns:** `any[]`  |  **HTTP:** `GET /api/v1/system/processes`
**CLI:** `hoody terminal processes list`

---

#### `listProcessesIterator` — List all system processes (async iterator)

```typescript
client.terminal.system.listProcessesIterator(sort?: string, limit?: integer, filter?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `sort` | `string` | query | No | Sort by field: cpu, memory, pid, name (default: pid) |
| `limit` | `integer` | query | No | Maximum number of processes to return (default: all) |
| `filter` | `string` | query | No | Filter by process name (substring match, case-insensitive) |

**Returns:** `AsyncIterableIterator<any>`  |  **HTTP:** `GET /api/v1/system/processes`
**CLI:** `hoody terminal processes list`

---

#### `reboot` — Reboot the system

```typescript
client.terminal.system.reboot(delay?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `delay` | `integer` | query | No | Delay in seconds before reboot, 0..86400 (default: 0 for immediate). shutdown(8) schedules in whole minutes, so the server rounds UP to the nearest minute and reports the actual scheduled value as `effective_minutes` in the response. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/system/reboot`
**CLI:** `hoody terminal system reboot`

---

#### `sendSignal` — Send signal to process(es)

```typescript
client.terminal.system.sendSignal(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ pid: int, name: string, signal: string|integer, force: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/system/process/signal`
**CLI:** `hoody terminal processes signal`

---

#### `shutdown` — Shutdown the system

```typescript
client.terminal.system.shutdown(delay?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `delay` | `integer` | query | No | Delay in seconds before shutdown, 0..86400 (default: 0 for immediate). shutdown(8) schedules in whole minutes, so the server rounds UP to the nearest minute and reports the actual scheduled value as `effective_minutes` in the response. |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/system/shutdown`
**CLI:** `hoody terminal system shutdown`

---

#### `unfreezeProcess` — Unfreeze (SIGCONT) a process or process tree

```typescript
client.terminal.system.unfreezeProcess(data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | Yes |  |

**Body:** `{ pid: int, name: string, include_descendants: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/system/processes/unfreeze`

---

### `client.terminal` (2) — Terminal

#### `abort` — Abort a running command

```typescript
client.terminal.abort(command_id: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `command_id` | `string` | path | Yes | The command ID returned by the execute endpoint |
| `data` | `object` | body | No |  |

**Body:** `{ force: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/execute/{command_id}/abort`
**CLI:** `hoody terminal sessions abort`

---

#### `write` — Write input to terminal

```typescript
client.terminal.write(terminal_id: string, data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID to write to |
| `data` | `object` | body | No |  |

**Body:** `{ input*: string, enter: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/write`
**CLI:** `hoody terminal sessions write`

---

### `client.terminal.terminalAutomation` (9) — Agent-facing automation primitives: screen snapshot, regex find, named key presses, text paste, and async wait conditions backed by a server-side libvterm parser

#### `findInTerminal` — Search terminal screen with regex

```typescript
client.terminal.terminalAutomation.findInTerminal(terminal_id: string, pattern: string, scope?: string, limit?: integer, case_insensitive?: boolean, scroll_offset?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID |
| `pattern` | `string` | query | Yes | PCRE2 regex pattern to search for (max 1024 bytes) |
| `scope` | `string` | query | No | Search scope: screen (default), scrollback, or all |
| `limit` | `integer` | query | No | Maximum number of hits to return (default 100, max 1000) |
| `case_insensitive` | `boolean` | query | No | Case-insensitive matching. Default: false |
| `scroll_offset` | `integer` | query | No | Scrollback offset for screen scope (0 = live viewport). Default: 0 |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/find`
**CLI:** `hoody terminal sessions find`

---

#### `getAutomationMetrics` — Get terminal automation metrics

```typescript
client.terminal.terminalAutomation.getAutomationMetrics()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/automation/metrics`
**CLI:** `hoody terminal automation metrics`

---

#### `getSessionAutomationState` — Get per-session automation state

```typescript
client.terminal.terminalAutomation.getSessionAutomationState(terminal_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | path | Yes | Terminal session ID |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/{terminal_id}/automation`
**CLI:** `hoody terminal sessions automation-state`

---

#### `getTerminalSnapshot` — Get rendered terminal snapshot

```typescript
client.terminal.terminalAutomation.getTerminalSnapshot(terminal_id: string, include_colors?: boolean, include_highlights?: boolean, scroll_offset?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID (numeric 1-65535) |
| `include_colors` | `boolean` | query | No | Include ANSI SGR colored_lines array alongside plain text lines. Default: false |
| `include_highlights` | `boolean` | query | No | Include reverse-video highlight spans. Default: true |
| `scroll_offset` | `integer` | query | No | Lines into scrollback (0 = live viewport). Default: 0 |

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/snapshot`
**CLI:** `hoody terminal sessions snapshot`

---

#### `listSupportedKeys` — List supported key names for /press endpoint

```typescript
client.terminal.terminalAutomation.listSupportedKeys()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/terminal/keys`
**CLI:** `hoody terminal automation keys`

---

#### `pasteTerminalText` — Paste text into terminal

```typescript
client.terminal.terminalAutomation.pasteTerminalText(terminal_id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ text*: string, bracketed: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/paste`
**CLI:** `hoody terminal sessions paste`

---

#### `pressTerminalKeys` — Send named key presses to terminal

```typescript
client.terminal.terminalAutomation.pressTerminalKeys(terminal_id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ keys: any[], key: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/press`
**CLI:** `hoody terminal sessions press`

---

#### `sendTerminalMouseEvents` — Send cell-based mouse events to terminal

```typescript
client.terminal.terminalAutomation.sendTerminalMouseEvents(terminal_id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ event: terminal_TerminalMouseEvent, events: terminal_TerminalMouseEvent[] } (exactly one of: event | events required)`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/mouse`

---

#### `waitForTerminal` — Wait for terminal condition

```typescript
client.terminal.terminalAutomation.waitForTerminal(terminal_id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID |
| `data` | `object` | body | Yes |  |

**Body:** `{ mode: string, debounce_ms: int, pattern: string, timeout_ms: int, search_scope: string, include_colors: bool, include_highlights: bool }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/wait`
**CLI:** `hoody terminal sessions wait`

---

### `client.terminal.terminalDragAndDrop` (4) — Terminal Drag-and-Drop

#### `beginTerminalDrop` — Begin a drag-and-drop staging transaction

```typescript
client.terminal.terminalDragAndDrop.beginTerminalDrop(terminal_id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID (numeric 1-65535) |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/drop-begin`

---

#### `commitTerminalDrop` — Finalize a drop and inject the OSC frame

```typescript
client.terminal.terminalDragAndDrop.commitTerminalDrop(terminal_id: string, drop: string, token: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID (numeric 1-65535) |
| `drop` | `string` | query | Yes | Drop id from /drop-begin |
| `token` | `string` | query | Yes | Drop token from /drop-begin |
| `data` | `object` | body | Yes |  |

**Body:** `{ ctx*: string, r: int, c: int, cr: string, items*: any[] }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/drop-commit`

---

#### `oneShotTerminalDrop` — One-shot drop (begin + stage + commit)

```typescript
client.terminal.terminalDragAndDrop.oneShotTerminalDrop(terminal_id: string, data: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID (numeric 1-65535) |
| `data` | `object` | body | Yes |  |

**Body:** `{ ctx*: string, r: int, c: int, items*: any[] }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/drop`

---

#### `uploadTerminalDropSlice` — Upload a raw file slice into a drop

```typescript
client.terminal.terminalDragAndDrop.uploadTerminalDropSlice(terminal_id: string, drop: string, token: string, path: string, offset: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | Yes | Terminal session ID (numeric 1-65535) |
| `drop` | `string` | query | Yes | Drop id from /drop-begin |
| `token` | `string` | query | Yes | Drop token from /drop-begin |
| `path` | `string` | query | Yes | Sanitized relative path of the staged file (no `..`, not absolute) |
| `offset` | `integer` | query | Yes | Byte offset to write at (must equal the current staged size) |

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/upload`

---

### `client.terminal.terminalState` (1) — Client render/connection diagnostics beacon

#### `postTerminalState` — Client render/connection diagnostics beacon

```typescript
client.terminal.terminalState.postTerminalState(data?: object)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `object` | body | No |  |

**Body:** `{ build_id: string, renderer: string, reason: string }`

**Returns:** `any`  |  **HTTP:** `POST /api/v1/terminal/state`

---

### `client.terminal.web` (1) — Web-based terminal interface with customizable display and session parameters

#### `get` — Get web terminal interface

```typescript
client.terminal.web.get(terminal_id?: string, cwd?: string, cwd_auto_create?: boolean, shell?: string, user?: string, cmd?: string, readonly?: boolean, title?: string, fontSize?: integer, backgroundColor?: string, panel?: string, panel-visible?: boolean, panel-position?: string, panel-width?: string, panel-resizable?: boolean, hide-toolbar?: boolean, ssh_host?: string, ssh_user?: string, ssh_port?: string, ssh_password?: string, socks5_host?: string, socks5_port?: string, socks5_user?: string, socks5_pass?: string, desktop?: boolean, desktop_env?: string, redirect?: string, redirect_delay?: integer, arg?: string, welcome?: boolean, debug?: boolean, reset?: boolean, pid?: integer, env?: string, display?: string, env_inject?: boolean, startup_script?: string, ssh_key?: string, panel-height?: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `terminal_id` | `string` | query | No | Terminal session ID (numeric 1-65535, auto-generated if not provided) - Allows multiple clients to share the same terminal session |
| `cwd` | `string` | query | No | Initial working directory for new terminal sessions (only applied when session is first created) |
| `cwd_auto_create` | `boolean` | query | No | Auto-create cwd when the requested working directory does not exist yet. Only applies when cwd is explicitly provided for a new session. Enable with 'true', '1', or no value (default: false) |
| `shell` | `string` | query | No | Shell to use: bash, zsh, fish, sh, etc. (default: server startup command, only applies to new sessions) |
| `user` | `string` | query | No | System user to spawn shell as (requires su permissions, only applies to new sessions, user must exist on system) |
| `cmd` | `string` | query | No | Base64-encoded command to execute automatically on spawn (executes once when shell starts) |
| `readonly` | `boolean` | query | No | Enable read-only mode (blocks keyboard input, allows viewing only) - Use 'true', '1', or no value |
| `title` | `string` | query | No | Browser window/tab title (default: application default) - HTML tags removed, max 200 characters, useful for organizing multiple terminal tabs |
| `fontSize` | `integer` | query | No | Terminal font size in pixels (default: 13, range: 8-72) - Accepts 'px' suffix (e.g., 16px), applied immediately when terminal loads |
| `backgroundColor` | `string` | query | No | Terminal background color (default: #2b2b2b) - Supports hex colors (#RGB, #RRGGBB, #RRGGBBAA) or CSS named colors (black, white, red, blue, green, navy, etc.) |
| `panel` | `string` | query | No | URL to display in side panel iframe (enables panel feature) |
| `panel-visible` | `boolean` | query | No | Show panel on load (default: true if panel URL provided, false otherwise) |
| `panel-position` | `string` | query | No | Panel position: 'left' or 'right' (default: right) |
| `panel-width` | `string` | query | No | Initial panel width in pixels or percentage (default: 400px) |
| `panel-resizable` | `boolean` | query | No | Allow panel resizing via drag handle (default: true) |
| `hide-toolbar` | `boolean` | query | No | Hide the terminal toolbar (default: false) |
| `ssh_host` | `string` | query | No | SSH server hostname or IP address (creates SSH session if provided with ssh_user) |
| `ssh_user` | `string` | query | No | SSH username (required if ssh_host is provided) |
| `ssh_port` | `string` | query | No | SSH port number (default: 22) |
| `ssh_password` | `string` | query | No | SSH password for authentication (use with caution, prefer key-based auth) |
| `socks5_host` | `string` | query | No | SOCKS5 proxy hostname for SSH connection |
| `socks5_port` | `string` | query | No | SOCKS5 proxy port (default: 1080) |
| `socks5_user` | `string` | query | No | SOCKS5 proxy username for authentication |
| `socks5_pass` | `string` | query | No | SOCKS5 proxy password for authentication |
| `desktop` | `boolean` | query | No | Enable Hoody Display desktop mode. Provides a full desktop environment instead of seamless individual windows (default: false) |
| `desktop_env` | `string` | query | No | Desktop environment to launch (implies desktop=true). Starts the specified DE session after the display is ready. Valid values: xfce, mate |
| `redirect` | `string` | query | No | Redirect mode. When set to "display", creates/ensures the terminal session, waits for X11 display readiness, then returns HTTP 302 redirect to the display URL. Requires terminal_id and display params |
| `redirect_delay` | `integer` | query | No | Extra delay in seconds after display is ready before redirecting. Only used when redirect=display (default: 0) |
| `arg` | `string` | query | No | Command-line arguments to pass to shell (requires --url-arg server option, can be repeated) |
| `welcome` | `boolean` | query | No | Show welcome message on startup (default: false). Supports ?welcome=true, ?welcome=1, or ?welcome (no value = true) |
| `debug` | `boolean` | query | No | Enable debug output in wrapper script (default: false) |
| `reset` | `boolean` | query | No | Kill existing terminal process and reconfigure session (default: false). Use to switch shell, user, or from shell to SSH |
| `pid` | `integer` | query | No | Attach to an existing process by PID instead of spawning a new shell. Implies reset |
| `env` | `string` | query | No | Inject environment variable as KEY=VALUE. Can be repeated for multiple variables (e.g., ?env=FOO=bar&env=BAZ=qux) |
| `display` | `string` | query | No | X11 display number for GUI applications. Accepts number (e.g., 1) or:number (e.g.,:1). Shorthand for ?env=DISPLAY=:N |
| `env_inject` | `boolean` | query | No | Inject HOODY_* environment variables into shell session (default: true). Set to false to disable |
| `startup_script` | `string` | query | No | Path to startup script to execute before shell launch (only applied on first session creation) |
| `ssh_key` | `string` | query | No | Base64-encoded SSH private key for key-based authentication (prefer over password-based auth) |
| `panel-height` | `string` | query | No | Initial panel height for top/bottom positioned panels (default: 300px) |

**Returns:** `any`  |  **HTTP:** `GET /`
**CLI:** `hoody terminal sessions web`


### Body schemas

- `terminal_TerminalMouseEvent` — `{ type*: "move" | "down" | "up" | "click" | "scroll", row*: int, col*: int, button: int, amount: int, direction: "up" | "down", modifiers: ("shift" | "alt" | "meta" | "ctrl" | "control")[] }`


---

<!-- ===== namespace: tunnel ===== -->

# `tunnel` — reverse tunnels for HTTP/WS/TCP via container relay

## Purpose

**Mental model: ngrok, but built into every container, with the rest of the platform glued in for free.** Same job — reverse tunnel laptop ↔ container — but the public URL lives on the container's own `*.containers.hoody.com` host, so it inherits everything the proxy already does:

- **Capability gates** (`proxyPermissionsContainer.*`) apply unchanged — Password / Token / JWT / IP groups gate the tunnel URL the same way they gate any kit URL.
- **Request hooks (MITM)** — wire `proxyHooks.*` rules to inspect, transform, redirect, or block requests before they reach the tunnel; same engine as the rest of the platform.
- **Proxy logs** — every tunnel request is captured by `proxyLogs.*` automatically (status, latency, headers, source IP). No extra setup.
- **Friendly aliases** — point a `<alias>.{server_name}.containers.hoody.com` at the tunnel via `proxyAliases.create({ program: 'tunnel', … })` so the public URL hides `containerId`.

Two surfaces:
- **EXPOSE**: publish laptop HTTP/1.1 (+WS) on container's public domain. (ngrok `http`)
- **PULL**: project laptop TCP onto container loopback. (ngrok `tcp` reverse)

Each surface has:
- **Data plane** (open the tunnel) — long-running WebSocket process. Lives in a separate driver (see Quirks).
- **Control plane** (inspect / kill) — short request/response: `tunnel.listTunnels`, `tunnel.listSessions`, `tunnel.listBindings`, `tunnel.getMetrics`, `tunnel.health.check`, `tunnel.killSession`.

## When to use

- Publish laptop HTTP/WS on `*.containers.hoody.com`.
- Project laptop TCP onto container `127.0.0.1:<port>`.
- Inspect, scrape metrics, kill sessions.

## When NOT to use

Container-hosted HTTP → `exec`, browser → `browser`, one-shot HTTP → `curl`, edge logs → `proxyLogs`, container↔container TCP not supported.

## Prerequisites

- `hoody-tunnel` kit running; base port reserved.
- EXPOSE URL needs `HOODY_TUNNEL_PUBLIC_URL_PATTERN`.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Inspect

`tunnel.listTunnels` → sessions, bindings, streams, orphans, FD budget. Drill via `tunnel.listSessions` / `tunnel.listBindings`. `tunnel.health.check`; `tunnel.getMetrics` → Prometheus.

### 2. Kill stuck session

1. `tunnel.listSessions` → `sessionId`.
2. `tunnel.killSession` with `grace_ms` 0–5000 (default 50). Returns `202`. Live → GOAWAY then close; orphans drop now (parking skipped).
3. Re-list before re-binding.

### 3. MITM / log / gate the tunnel

Tunnel traffic flows through the same proxy as every other kit URL, so:

- **Logs**: `client.proxyLogs.logs.list({ serviceName: 'tunnel' })` returns every request that hit the tunnel — status, latency, source IP, headers — and `client.proxyLogs.logs.streamLogs(...)` for live tail; the generated SDK stream method does **not** expose `serviceName`, so use `list({ serviceName: 'tunnel', ... })` for tunnel-scoped polling. (proxyLogs query params on `list` are `limit` / `offset` / `projectId` / `containerId` / `serviceName` / `level` / `includeRequestBody` / `includeResponseBody` / `last` / `afterId` / `cursor` / `kind` / `method` / `source`; there is no `program` filter.) No agent on the laptop needed.
- **Hooks (MITM)**: register a `proxyHooks` rule scoped to `program: 'tunnel'` (or by alias hostname) to inspect / rewrite / inject / block requests before they reach the WS data plane. Same hook DSL as for any kit. Useful for: stripping a bearer header on the way through, redacting PII, rate-limiting, swapping bodies on the fly, fault-injecting for tests.
- **Gates**: layer `proxyPermissionsContainer.set{Password,Token,Jwt,Ip}Group` on the container; the tunnel URL becomes auth-gated without the laptop ever needing to handle it.

## Quirks & gotchas

- The data-plane (open / pull) is a long-running driver process, not a request/response call. Runtime: Bun 1.3+ or Node 20+ (the listening-server form is Bun-only). The main `tunnel` namespace covers only the read/observability + admin surface (`listTunnels`, `listSessions`, `listBindings`, `getMetrics`, `killSession`).
- `BIND_OK.publicUrl=null` without `HOODY_TUNNEL_PUBLIC_URL_PATTERN`.
- `grace_ms` capped at 5000ms; over → `400`.
- Ports `<80` rejected; `80..=1023` need `--allow-privileged-expose`/`--allow-privileged-pull`.
- PULL loopback-only. EXPOSE has atomic takeover (`takeover:true`); old owner gets `RESET(BIND_TAKEOVER)`+`BIND_REVOKED`. PULL takeover → `BIND_ERR(INVALID_KIND)`.
- Idle reaping needs zero streams AND zero bindings. Orphans with parked bindings wait 60s takeover-grace.
- v1 vs v2 subprotocols share `/connect` (`hoody-tunnel.v1` for single-WS sessions, `hoody-tunnel.v2` for multi-WS shard pools); `isV2` on `listSessions` reports the shape. Both subprotocols support graceful resume via `resume.sessionId` in HELLO; `isV2:false` does NOT mean "no resume".
- Multi-WS (v2) drop semantics: dropping the **primary** socket closes the whole session; dropping a **secondary** shard makes the driver close streams pinned to that shard while the kit detaches the shard and the session continues.
- Pre-auth connection cap defaults to `--max-pre-auth-connections=32`; exceeding it closes the socket before HELLO (no explicit close code). HELLO timeout defaults to 5 s.
- **No UDP support.** EXPOSE is HTTP/1.1+WS only; PULL is TCP only.
- `tunnelConnect` is the WS-upgrade endpoint of the data plane. The generated SDK exposes it as a plain `http.get` returning an `ApiResponse<unknown>` — that is NOT a working tunnel attach. Use the driver helpers (`tunnelExpose` / `tunnelPull` / `tunnelServe`) re-exported from the main SDK package, which handle the WS subprotocol, HELLO frame, and resume; do not call `client.tunnel.tunnelConnect` directly.

## Common errors

- `404` on kill — session gone; no retry.
- `403` — SSRF guard; not via Hoody Proxy.
- Upgrade `400` — missing/unsupported subprotocol; WS `1002` — HELLO rejected after upgrade; plain socket close — HELLO timeout or pre-auth cap reached.
- `BIND_ERR` codes: `ALREADY_BOUND` (retry `takeover:true`), `PORT_IN_USE`, `RESERVED_PORT`, `INVALID_HOST`, `PRIVILEGED_PORT`, `BIND_CAP_EXCEEDED`, `INVALID_KIND` (unsupported `(kind, mode)` combo, or `takeover:true` on PULL), `INTERNAL` (server-side, e.g. random-port exhaustion).
- `GOAWAY(IDLE_TIMEOUT)` — no PONG in 60s; reconnect via `resume.sessionId`.
- `503`+`Retry-After:5` at visitor URL — orphan takeover-grace window (set by `expose` driver kill, NOT by admin `killSession` which skips orphan parking).

## Related namespaces

- `proxyLogs` — every tunnel request appears here automatically; filter by `serviceName: 'tunnel'` (there is no `program` filter on the proxy-log API).
- `api` — `proxyHooks.*` (MITM rules), `proxyPermissionsContainer.*` (capability gates), `proxyAliases.create({ program: 'tunnel' })` (friendly hostnames hiding `containerId`).
- `exec` — for one-off HTTP handlers hosted directly inside the container (no laptop). `curl` — outbound HTTP from the container. `browser` — full headless Chromium. `daemon` — supervise long-running processes.

## Examples

The `tunnel` namespace covers only the **observability + admin** surface — `health.check`, `listTunnels`, `listSessions`, `listBindings`, `getMetrics`, `killSession`. The data plane (open / pull) is a long-running WebSocket driver that lives in a separate package; it is intentionally out of scope here, so these 7 examples assume *somebody else* (a teammate's tunnel expose/pull session, your CI machine's tunnel session, a test rig) is currently holding the tunnel. You're the operator: inspecting it, scraping metrics, killing it. Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first.

Read-only steps were live-attempted against the test container; on this deployment the tunnel kit process was not running on that container at the moment of writing (502); the admin endpoints serve independently of any session. Schemas, status codes, response shapes and CLI flags are verified against `generated/openapi.public.json`, `docs/reference/CLI-COMMANDS.md`.

### 1. Health probe — kit alive, FD budget not exhausted

**Goal:** before any other call, confirm the tunnel kit is reachable and not saturated. Response includes `pid`, `started`, `userAgent`, `fds` (Unix-only file-descriptor count when available), and `memory.rss`.

```typescript
const r = await client.tunnel.health.check();
const h = r.data!;
console.log({ status: h.status, pid: h.pid, fds: h.fds, rss: h.memory?.rss });
```
If the response is HTML / `Error 502` instead of JSON, the kit base listener isn't reachable through the proxy (kit crashed / not installed / proxy mis-route) — the admin endpoints are designed to stay live independent of any active session. Lack of an active session shows up as `sessions: []`, not 502.

### 2. List every active tunnel — combined sessions + bindings + FD budget

**Goal:** "what's currently tunneling on this container?" One call returns `sessions[]` (each with `peerAddr`, `protocol`, `connectionsGranted`, `activeStreams`, `exposeBindings[]`, `pullBindings[]`), `orphanedSessions` count, `totalStreams`, `totalBindings`, and `fdPermitsAvailable`.

```typescript
const r = await client.tunnel.listTunnels();
const t = r.data!;
console.log({
  active: t.sessions.length,
  orphans: t.orphanedSessions,
  streams: t.totalStreams,
  binds: t.totalBindings,
  fdBudget: t.fdPermitsAvailable,
  ids: t.sessions.map(s => s.sessionId),
});
```
`listTunnels` is the one-shot overview. For per-session detail (peer addr, max-stream cap, v2 flag) drill in via `listSessions` (example #3). Note: `protocol` is per-session and reflects the negotiated control-plane protocol, NOT the upstream — for the "is this an EXPOSE or PULL" answer, look at which of `exposeBindings` / `pullBindings` is non-empty.

### 3. Drill into one session — peer addr, stream load, capacity

**Goal:** you got a `sessionId` from #2; now you want the operator-facing detail (who's connected, how loaded). Returns `peerAddr` (`<ip>:<port>` of the laptop holding the tunnel), `connectionsGranted` (lifetime), `activeStreams` (right now), `maxStreams` (negotiated cap), `isV2` (control-plane protocol), and `bindings[]`.

```typescript
const r = await client.tunnel.listSessions();
const s = r.data!.sessions.find(x => x.sessionId === sid);
if (s) console.log({
  peer: s.peerAddr,
  v2: s.isV2,
  load: `${s.activeStreams}/${s.maxStreams}`,
  lifetime: s.connectionsGranted,
  binds: s.bindings.map(b => `${b.kind}/${b.mode}:${b.containerPort}#${b.bindId}`),
});
```
`activeStreams / maxStreams` is the headroom number — a session sitting at `48/50` is one curl away from `STREAM_LIMIT`. `isV2:false` means the session negotiated the single-WebSocket v1 control plane; resume is still supported via `resume.sessionId` while the orphan is in takeover grace.

### 4. List bindings — which ports are exposed across every session

**Goal:** answer "what container ports are tunnels eating right now?". `listBindings` flattens across one row per active binding — `port`, `kind` (`http` / `tcp`), `mode` (`expose` / `pull`). EXPOSE rows include the owning `sessionId`/`bindId`; current PULL rows report `sessionId: ""` and `bindId: 0`.

```typescript
const r = await client.tunnel.listBindings();
const byMode = r.data!.bindings.reduce<Record<string, number[]>>((acc, b) => {
  (acc[b.mode] ||= []).push(b.port);
  return acc;
}, {});
console.log(byMode);  // { expose: [3000, 8080], pull: [5432] }
```
Useful pre-flight check before someone tries to bind another port — `BIND_ERR(PORT_IN_USE)` is one of the most common BIND failures. Also: the wire field is `port` here but `containerPort` inside the per-session `bindings[]` array of #3 — same value, different name (verified against `tunnel_BindingDetail` vs `tunnel_BindingInfo` in the openapi spec).

### 5. Scrape Prometheus metrics — sessions, bindings, FD permits

**Goal:** wire the tunnel kit into your scrape job. Endpoint emits Prometheus text (one of the few endpoints that's not JSON). Three live-verified counters: `hoody_tunnel_sessions_active`, `hoody_tunnel_bindings_active`, `hoody_tunnel_fd_permits_available`.

```typescript
const r = await client.tunnel.getMetrics();
const text = r.data as unknown as string; // text/plain payload
// metric lines may carry a Prometheus label set, and bindings_active is split
// across expose+pull series — tolerate `{...}` and sum every matching line.
const get = (name: string) => {
  const re = new RegExp(`^${name}(?:\\{[^}]*\\})?\\s+(\\S+)`, 'gm');
  let sum = 0, hit = false;
  for (const m of text.matchAll(re)) { sum += Number(m[1]); hit = true; }
  return hit ? sum : NaN;
};
console.log({
  sessions: get('hoody_tunnel_sessions_active'),
  bindings: get('hoody_tunnel_bindings_active'),
  fdPermits: get('hoody_tunnel_fd_permits_available'),
});
```
For a dashboard, register the kit URL as a Prometheus scrape target via `proxyAliases.create({ program: 'tunnel' })` so the scrape config doesn't carry `containerId`, then gate it with `proxyPermissionsContainer.setIpGroup` so only your monitoring VPC can hit `/api/v1/tunnel/metrics`.

### 6. Kill a stuck session (recipe — needs a real session)

**Goal:** a teammate's tunnel expose session is wedged; you want it gone without restarting the kit. `killSession` returns `202` with `{sessionId, status}`. `grace_ms` ∈ [0, 5000] (default 50, anything above 5000 → `400`); the kit sends GOAWAY then waits up to that many ms for in-flight streams before closing. Orphan sessions skip the parking grace window and drop immediately.

⚠ Don't run this in the doc as live verification — it kills whoever's actually connected. Recipe only.

```typescript
const list = await client.tunnel.listSessions();
const stuck = list.data!.sessions.find(s => s.peerAddr.startsWith('203.0.113.'));
if (!stuck) throw new Error('no matching session');
const r = await client.tunnel.killSession(stuck.sessionId, { grace_ms: 1000 });
console.log(r.data); // { sessionId, status: 'closing' }
```
After a non-admin driver disconnect, visitors of an orphaned `expose` URL see `503 Retry-After:5` during takeover grace. `killSession` (admin) skips orphan parking and tears bindings down, so do **not** expect that 60 s 503 window from an admin kill — bindings drop immediately. PULL bindings drop instantly in both paths.

### 7. Auto-discover orphans + low-FD alert (monitoring recipe)

**Goal:** one cron-able script that watches both the orphan count (parked bindings whose laptop dropped) and the FD permits remaining; pages on either. `listTunnels` carries both numbers.

```typescript
const r = await client.tunnel.listTunnels();
const { orphanedSessions, fdPermitsAvailable } = r.data!;
if (fdPermitsAvailable < 64 || orphanedSessions > 0) {
  console.warn(`tunnel kit ${C}: orphans=${orphanedSessions} fds=${fdPermitsAvailable}`);
}
```
For a continuous live tail of `GOAWAY` / `IDLE_TIMEOUT` / `BIND_REVOKED` events as they happen, attach with the tunnel driver helpers (`TunnelSession` / `tunnelExpose` / `tunnelPull`, re-exported from the main SDK package) to `/api/v1/tunnel/connect` — that's the data-plane driver's surface and lives outside this namespace; do **not** use generated `client.tunnel.tunnelConnect` for a real attach. The polling recipe above stays inside the request/response admin surface.

## Reference

**Accessor:** `client.tunnel`  |  **Import:** `import * as tunnel from 'hoody-sdk/tunnel'`

### `client.tunnel.health` (1) — Health

#### `check` — Kit health

```typescript
client.tunnel.health.check()
```

**Returns:** `tunnel_HealthResponse`  |  **HTTP:** `GET /api/v1/tunnel/health`
**CLI:** `hoody tunnel health`

---

### `client.tunnel` (6) — Tunnel control plane (WebSocket + health + management)

#### `getMetrics` — Prometheus metrics

```typescript
client.tunnel.getMetrics()
```

**Returns:** `any`  |  **HTTP:** `GET /api/v1/tunnel/metrics`
**CLI:** `hoody tunnel metrics`

---

#### `killSession` — Terminate an active tunnel session

```typescript
client.tunnel.killSession(session_id: string, grace_ms?: integer)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `session_id` | `string` | path | Yes | Session ID as returned by GET /sessions |
| `grace_ms` | `integer` | query | No | GOAWAY drain budget in ms (0-5000, default 50) |

**Returns:** `tunnel_KillResponse`  |  **HTTP:** `DELETE /api/v1/tunnel/sessions/{session_id}`
**CLI:** `hoody tunnel sessions kill`

---

#### `listBindings` — List active bindings across all sessions

```typescript
client.tunnel.listBindings()
```

**Returns:** `tunnel_BindingsResponse`  |  **HTTP:** `GET /api/v1/tunnel/bindings`
**CLI:** `hoody tunnel bindings list`

---

#### `listSessions` — List active tunnel sessions

```typescript
client.tunnel.listSessions()
```

**Returns:** `tunnel_SessionsResponse`  |  **HTTP:** `GET /api/v1/tunnel/sessions`
**CLI:** `hoody tunnel sessions list`

---

#### `listTunnels` — List all active tunnels (combined sessions + bindings)

```typescript
client.tunnel.listTunnels()
```

**Returns:** `tunnel_TunnelOverview`  |  **HTTP:** `GET /api/v1/tunnel/tunnels`
**CLI:** `hoody tunnel list`

---

#### `tunnelConnect` — Tunnel WebSocket control plane

```typescript
client.tunnel.tunnelConnect()
```

**Returns:** `void`  |  **HTTP:** `GET /api/v1/tunnel/connect`


---

<!-- ===== namespace: watch ===== -->

# `watch` — Linux inotify file-change streams with replay history

## Purpose

Per-container filesystem-event service. Configure watchers (paths, globs, ignore-dirs, coalesce window); consume events via paginated history, SSE, or WebSocket. Bounded in-memory replay buffer supports `since_id` resume.

## When to use

- Live-tail FS changes inside a container (build, hot-reload, log tail)
- Detect `created | modified | removed | renamed | metadata` events on paths/trees
- Audit writes by filtering `kinds`
- Resume after disconnect via `since_id` / `since_timestamp`

## When NOT to use

- One-shot listing/stat, file-content tail, shell-process lifecycle, cross-container aggregation (see §Related namespaces).

## Prerequisites

- Container with `hoody-watch` kit (Linux only); see `SKILL-SDK.md` for auth + URL routing.

## Capability URL

→ See `SKILL-SDK.md § Proxy URLs`.

**Reaching a service you host on a container port** (any port, any namespace):

- `https://{projectId}-{containerId}-http-<port>.{node}.containers.hoody.com` — proxy speaks HTTP to `localhost:<port>`.
- `https://{projectId}-{containerId}-https-<port>.{node}.containers.hoody.com` — proxy speaks HTTPS to `localhost:<port>` (target needs TLS).

Edge is always `https://`. No alias, firewall edit, or proxy registration needed; capability-token gates still apply.

## Common workflows

### 1. Provision and verify

1. `client.watch.watchers.create` — paths plus optional `recursive`, `include`, `exclude`, `kinds`, `ignore_dirs`, `skip_hidden`, `coalesce_ms`, `history_size`
2. `client.watch.watchers.get` — read back `id`, `WatcherConfigView`, `WatcherStats`

### 2. SSE live-tail with resume

1. `client.watch.watchers.create`
2. `client.watch.streams.streamSse` — each event carries monotonic `id`
3. Reconnect with `since_id` = last seen id
4. On HTTP 409 `HISTORY_GAP` or inline `event: lag` — treat as data loss; rebuild from fresh listing

### 3. Bulk replay via pagination

Bulk replay: `listEventsAll`/`Iterator` with `since_id`; persist highest `id`.

### 4. WebSocket consumer

1. `client.watch.watchers.create`
2. `client.watch.streams.streamWs` — respond to server `Ping` within ~20s or socket closes
3. `{"type":"lag",...}` text frame = same handling as SSE lag

### 5. Inventory and teardown

List/inspect/delete via `watchers.listIterator`/`get`/`delete`.

## Quirks & gotchas

- Linux only; on non-Linux hosts the binary exits before opening the listener (no HTTP served) — typically with code 0 via the container-path check, or code 1 if those paths exist. The 501 `UNSUPPORTED_PLATFORM` mapping in `api.rs` is a defensive branch that is unreachable from a normal startup.
- No kit-level auth header; do not add `Authorization` on direct kit calls
- `recursive` defaults `true`; `coalesce_ms` defaults `100`
- `ignore_dirs` default: `node_modules, .git, target, __pycache__, .hg, .svn, .cache, dist, .next, .nuxt, vendor, bower_components`. Pass `[]` to disable; `null` falls back to default
- Hard limits: 128 watchers/kit, 32 paths/watcher, 64 stream clients/watcher, replay buffer 100 000 events or 16 MiB (first hit)
- Watcher ids are UUIDs (`Path<Uuid>`); non-UUID segment yields 400 from axum extractor
- `since_id` and `since_timestamp` mutually exclusive — both = 400 `INVALID_CURSOR`
- `since_timestamp` accepts RFC3339, unix seconds, or millis (switches to ms when `|n| >= 100_000_000_000`)
- WS message cap 64 KiB (`ws_max_message_bytes`); server pings every 20s, disconnects on missed pong
- `kind` (wire field name): `created | modified | removed | renamed | metadata | overflow | other` (snake_case enum); `overflow` = inotify dropped events

## Common errors

- `400 INVALID_PAGINATION` — `page < 1` or `limit ∉ [1,200]`; defaults `page=1, limit=50`
- `400 INVALID_REQUEST` — empty `paths`, invalid/missing path, or glob compile fail (`EmptyPaths`, `InvalidPath`, `PathNotFound`, `InvalidPattern`, `InvalidIgnoreDir`)
- `400 INVALID_CURSOR` — both cursor fields, or unparseable timestamp
- `404 WATCHER_NOT_FOUND` — UUID syntactically valid but no watcher; also raised pre-upgrade on stream endpoints
- `409 LIMIT_EXCEEDED` — `paths > 32` or `active_watchers >= 128` on create
- `409 HISTORY_GAP` — cursor older than oldest retained; body `details` carries `oldest_available_id` / `newest_available_id`
- `429 MAX_CLIENTS_REACHED` — >64 concurrent SSE+WS on one watcher; capacity incremented after checks pass (no slot leak)
- `503 SHUTTING_DOWN` — only on `streamSse`/`streamWs` while draining; history reads still work
- Mid-stream `event: lag` (SSE) / `{"type":"lag",...}` (WS) — broadcast lagged AND replay buffer cannot fill gap; connection closed after lag frame

## Related namespaces

- `files` — read/write watched paths
- `exec` — run command on event (rebuild on save)
- `daemon` — supervise the consumer process
- `pipe` — fan SSE stream into another container/process

## Examples

Every step in every example was live-tested against a real `watch-1` kit. Each step has a copy-pasteable code block in the mode you're reading (curl for HTTP, `hoody` for CLI, TypeScript for SDK). Set `P`, `C`, `N` (project id, container id, server name) from `containers.get` first. **There is no `update` endpoint** — change a watcher's config = delete + recreate.

### 1. Provision a recursive watcher and verify it sees events

**Goal:** watch `/tmp/wt` for any change; confirm the inotify subscriptions are wired by mutating a file and reading the history.

**Step 1 — create the watcher.** Capture `id`. `recursive` defaults to `true`; tighten `coalesce_ms` from the default 100 ms to 50 ms for snappier debounce.

```typescript
const r = await client.watch.watchers.create({
  paths: ['/tmp/wt'],
  recursive: true,
  coalesce_ms: 50,
});
const wid = r.data!.id;
```
**Step 2 — read back stats** (response carries `config` + `stats`; `events_seen > 0` after the first FS touch confirms the inotify watch is live).

```typescript
const view = await client.watch.watchers.get(wid);
console.log(view.data!.stats.events_seen);
```
### 2. SSE live-tail with `since_id` resume after disconnect

**Goal:** subscribe to live events; on disconnect, replay everything missed.

**Step 1 — open the SSE stream.** Each frame is `id: <n>\nevent: file_event\ndata: {…json…}\n\n`. The `id` is monotonic; persist it as your resume cursor.

```typescript
// Generated streamSse returns Promise<ApiResponse<unknown>> (single http.get),
// NOT an async iterator. For live consumption use the URL with EventSource /
// fetch+ReadableStream, or poll the cursor:
let lastId = 0;
for (;;) {
  const page = await client.watch.streams.listEvents(wid, { since_id: lastId, limit: 200 });
  const items = (page.data as any)?.items ?? [];
  for (const ev of items) {
    lastId = ev.id;
    console.log(ev.kind, ev.path);
  }
  if (items.length === 0) await new Promise(r => setTimeout(r, 500));
}
```
**Step 2 — reconnect with `since_id`.** Server replays from the buffer; if the buffer rolled past your cursor you get **HTTP 409 `HISTORY_GAP`** with `details` (a JSON-encoded string) holding `oldest_available_id` / `newest_available_id` / `requested_cursor`. Treat that as data loss and rebuild from a fresh listing.

```typescript
// streamSse takes (id, options?, _templateVars?); since_id goes in options.
const resumed = await client.watch.streams.streamSse(wid, { since_id: lastId });
```
### 3. WebSocket consumer — replay buffer + live events on one socket

**Goal:** alternative to SSE when the consumer needs binary frames or bidirectional control. Server sends Ping every ~20 s; miss the pong and the socket closes.

```typescript
// Generated streamWs is a plain http.get returning Promise<ApiResponse<unknown>>,
// NOT a WebSocket client. For an actual WS connection, use the kit URL with a
// browser/Node `WebSocket` and pass the bearer cookie/header per § Auth model.
import WebSocket from 'ws';
// Kit WS path is mounted at the kit root, NOT under /api/v1/watch.
const ws = new WebSocket(
  `wss://${P}-${C}-watch-1.${N}.containers.hoody.com/watchers/${wid}/events/ws?since_id=37000`,
);
ws.on('message', (raw) => {
  const ev = JSON.parse(raw.toString());
  if (ev.type === 'lag') { /* drop & rebuild from history */ return; }
  console.log(ev.kind, ev.path);
});
```
A lag frame is `{"type":"lag", …}` (text); after it, the server closes the socket — same handling as the SSE inline `event: lag`.

### 4. Bulk replay history via paginated listing

**Goal:** cursor-walk every event since a known id, persist offline, then resume from `newest_available_id`. Useful for batch consumers (cron, periodic syncers) that don't want to hold a stream.

**Step 1 — page through.** `limit ∈ [1,200]`; out-of-range = **400 `INVALID_PAGINATION`**. Response carries `oldest_available_id` / `newest_available_id` / `oldest_available_timestamp` / `newest_available_timestamp` so you can detect buffer churn between pages.

```typescript
// listEventsIterator yields individual events (NOT pages), so just push.
// Signature is (id, options?, _templateVars?) — since_id / limit go in options.
const collected: any[] = [];
for await (const ev of client.watch.streams.listEventsIterator(wid, { since_id: lastId, limit: 200 })) {
  collected.push(ev);
}
const resume = collected.at(-1)?.id ?? lastId;
```
### 5. Filter by event kind — only writes, ignore creates / removes / metadata

**Goal:** trigger a rebuild only on real content edits, not on file creation noise. `kinds` accepts a subset of `created | modified | removed | renamed | metadata | overflow | other`.

```typescript
const r = await client.watch.watchers.create({
  paths: ['/tmp/wt'], kinds: ['modified'], coalesce_ms: 50,
});
const events = await client.watch.streams.listEvents(r.data!.id, { limit: 50 });
```
⚠ A single `writeFile` triggers two `inotify` events on Linux: the `created` (size 0) and a follow-up `modified` once data is written. With `kinds: ["modified"]` you skip the empty-file noise and only see real writes — live-verified.

### 6. Glob include/exclude — watch logs but ignore secret rotations

**Goal:** stream `*.log` events but exclude `secret-*.log` rotations a security agent doesn't need to see. `exclude` takes precedence over `include`.

```typescript
const r = await client.watch.watchers.create({
  paths: ['/tmp/wt'],
  include: ['**/*.log'],
  exclude: ['**/secret-*.log'],
});
```
### 7. Inventory — list every watcher with its event-counter and active-clients

**Goal:** an audit screen that shows every watcher in the kit, what it watches, and whether it has live consumers. `stats.events_seen` is the inotify-side counter (post-coalesce); `active_clients` counts live SSE+WS connections.

```typescript
// listIterator yields individual watcher records (NOT pages); signature is
// (options?, _templateVars?) — pagination params live in options.
const all: any[] = [];
for await (const w of client.watch.watchers.listIterator({ limit: 200 })) {
  all.push(w);
}
```
### 8. Change a watcher's config — there is no `update`, you delete and recreate

**Goal:** widen `kinds` from `["modified"]` to `["modified","removed"]`. The kit exposes only `create | get | delete` — there is **no** `PATCH /watchers/{id}`. Pattern: snapshot the old config, delete, create new, hand off the resume cursor.

```typescript
const old = await client.watch.watchers.get(wid);
await client.watch.watchers.delete(wid);
const fresh = await client.watch.watchers.create({
  ...old.data!.config,
  kinds: ['modified', 'removed'],
});
```
⚠ Event ids are process-global, not per-watcher; the `since_id` cursor remains numerically valid across the swap, but the new watcher's first event will have an id ≥ the global counter, so any gap below that is from the old watcher's stream — treat HISTORY_GAP as the trigger for a full re-list, not the swap itself.

### 9. Tear down on shutdown + verify events stop

**Goal:** clean up. After delete, both `GET /watchers/{id}` and `/events` return **404 `WATCHER_NOT_FOUND`**, and any open SSE/WS sockets close. The DELETE response body is `{ id, deleted: true }`.

```typescript
await client.watch.watchers.delete(wid);
try { await client.watch.watchers.get(wid); }
catch (e: any) { /* e.status === 404, e.code === 'WATCHER_NOT_FOUND' */ }
```
### 10. Recent history without a stream — `since_timestamp` for one-shot tail

**Goal:** a forensics caller wants every event in the last 5 min without holding a connection. `since_timestamp` accepts RFC3339, unix seconds, or unix milliseconds (auto-detected when `|n| >= 100_000_000_000`). It is **mutually exclusive** with `since_id` — pass both and you get **400 `INVALID_CURSOR`**.

```typescript
const r = await client.watch.streams.listEvents(wid, {
  since_timestamp: new Date(Date.now() - 5 * 60_000).toISOString(),
  limit: 200,
});
```
For a rename the kit relies on the notify backend's combined `RenameMode::Both` event, which carries both paths: it emits **one** `renamed` event with `(path=new, old_path=old)`. Renames the backend reports as separate from/to halves (e.g. across mount boundaries) fall through to one event per side with `old_path: null`. Filter on `old_path != null` to keep only the paired form.

## Reference

**Accessor:** `client.watch`  |  **Import:** `import * as watch from 'hoody-sdk/watch'`

### `client.watch.health` (1) — Health

#### `check` — Health Check

```typescript
client.watch.health.check()
```

**Returns:** `watch_HealthResponse`  |  **HTTP:** `GET /api/v1/watch/health`
**CLI:** `hoody watch health`

---

### `client.watch.streams` (5) — Real-time event streams

#### `listEvents` — List Watcher Events

```typescript
client.watch.streams.listEvents(id: string, since_id?: integer|null, since_timestamp?: string|null, page?: integer|null, limit?: integer|null)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Watcher id |
| `since_id` | `integer|null` | query | No | Replay events strictly after this event id. |
| `since_timestamp` | `string|null` | query | No | Replay events strictly after this timestamp. Accepted formats: - RFC3339 (e.g. 2026-02-11T15:30:00Z) - Unix seconds (e.g. 1739287800) - Unix milliseconds (e.g. 1739287800123) |
| `page` | `integer|null` | query | No | Page number (1-based). |
| `limit` | `integer|null` | query | No | Items per page (1-200). |

**Returns:** `watch_EventHistoryResponse`  |  **HTTP:** `GET /watchers/{id}/events`
**CLI:** `hoody watch events list`

---

#### `listEventsAll` — List Watcher Events (collect all pages)

```typescript
client.watch.streams.listEventsAll(id: string, since_id?: integer|null, since_timestamp?: string|null, page?: integer|null, limit?: integer|null)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Watcher id |
| `since_id` | `integer|null` | query | No | Replay events strictly after this event id. |
| `since_timestamp` | `string|null` | query | No | Replay events strictly after this timestamp. Accepted formats: - RFC3339 (e.g. 2026-02-11T15:30:00Z) - Unix seconds (e.g. 1739287800) - Unix milliseconds (e.g. 1739287800123) |
| `page` | `integer|null` | query | No | Page number (1-based). |
| `limit` | `integer|null` | query | No | Items per page (1-200). |

**Returns:** `watch_EventHistoryResponse[]`  |  **HTTP:** `GET /watchers/{id}/events`
**CLI:** `hoody watch events list`

---

#### `listEventsIterator` — List Watcher Events (async iterator)

```typescript
client.watch.streams.listEventsIterator(id: string, since_id?: integer|null, since_timestamp?: string|null, page?: integer|null, limit?: integer|null)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Watcher id |
| `since_id` | `integer|null` | query | No | Replay events strictly after this event id. |
| `since_timestamp` | `string|null` | query | No | Replay events strictly after this timestamp. Accepted formats: - RFC3339 (e.g. 2026-02-11T15:30:00Z) - Unix seconds (e.g. 1739287800) - Unix milliseconds (e.g. 1739287800123) |
| `page` | `integer|null` | query | No | Page number (1-based). |
| `limit` | `integer|null` | query | No | Items per page (1-200). |

**Returns:** `AsyncIterableIterator<watch_EventHistoryResponse>`  |  **HTTP:** `GET /watchers/{id}/events`
**CLI:** `hoody watch events list`

---

#### `streamSse` — Stream Watcher Events Sse

```typescript
client.watch.streams.streamSse(id: string, since_id?: integer|null, since_timestamp?: string|null)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Watcher id |
| `since_id` | `integer|null` | query | No | Replay events strictly after this event id. |
| `since_timestamp` | `string|null` | query | No | Replay events strictly after this timestamp. Accepted formats: - RFC3339 (e.g. 2026-02-11T15:30:00Z) - Unix seconds (e.g. 1739287800) - Unix milliseconds (e.g. 1739287800123) |

**Returns:** `any`  |  **HTTP:** `GET /watchers/{id}/events/sse`
**CLI:** `hoody watch events stream`

---

#### `streamWs` — Stream Watcher Events Ws

```typescript
client.watch.streams.streamWs(id: string, since_id?: integer|null, since_timestamp?: string|null)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Watcher id |
| `since_id` | `integer|null` | query | No | Replay events strictly after this event id. |
| `since_timestamp` | `string|null` | query | No | Replay events strictly after this timestamp. Accepted formats: - RFC3339 (e.g. 2026-02-11T15:30:00Z) - Unix seconds (e.g. 1739287800) - Unix milliseconds (e.g. 1739287800123) |

**Returns:** `void`  |  **HTTP:** `GET /watchers/{id}/events/ws`

---

### `client.watch.system` (2) — System endpoints

#### `getOpenApiJson` — Get Open Api Json

```typescript
client.watch.system.getOpenApiJson()
```

**Returns:** `any`  |  **HTTP:** `GET /openapi.json`

---

#### `getOpenApiYaml` — Get Open Api Yaml

```typescript
client.watch.system.getOpenApiYaml()
```

**Returns:** `any`  |  **HTTP:** `GET /openapi.yaml`

---

### `client.watch.watchers` (6) — Watcher management

#### `create` — Create Watcher

```typescript
client.watch.watchers.create(data: watch_CreateWatcherRequest)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `data` | `watch_CreateWatcherRequest` | body | Yes |  |

**Returns:** `watch_WatcherResponse`  |  **HTTP:** `POST /watchers`
**CLI:** `hoody watch create`

---

#### `delete` — Delete Watcher

```typescript
client.watch.watchers.delete(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Watcher id |

**Returns:** `watch_DeleteWatcherResponse`  |  **HTTP:** `DELETE /watchers/{id}`
**CLI:** `hoody watch delete`

---

#### `get` — Get Watcher

```typescript
client.watch.watchers.get(id: string)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `id` | `string` | path | Yes | Watcher id |

**Returns:** `watch_WatcherResponse`  |  **HTTP:** `GET /watchers/{id}`
**CLI:** `hoody watch get`

---

#### `list` — List Watchers

```typescript
client.watch.watchers.list(page?: integer|null, limit?: integer|null)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer|null` | query | No | Page number (1-based). |
| `limit` | `integer|null` | query | No | Items per page (1-200). |

**Returns:** `watch_WatcherListResponse`  |  **HTTP:** `GET /watchers`
**CLI:** `hoody watch list`

---

#### `listAll` — List Watchers (collect all pages)

```typescript
client.watch.watchers.listAll(page?: integer|null, limit?: integer|null)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer|null` | query | No | Page number (1-based). |
| `limit` | `integer|null` | query | No | Items per page (1-200). |

**Returns:** `watch_WatcherListResponse[]`  |  **HTTP:** `GET /watchers`
**CLI:** `hoody watch list`

---

#### `listIterator` — List Watchers (async iterator)

```typescript
client.watch.watchers.listIterator(page?: integer|null, limit?: integer|null)
```

| Parameter | Type | In | Required | Description |
|-----------|------|------|----------|-------------|
| `page` | `integer|null` | query | No | Page number (1-based). |
| `limit` | `integer|null` | query | No | Items per page (1-200). |

**Returns:** `AsyncIterableIterator<watch_WatcherListResponse>`  |  **HTTP:** `GET /watchers`
**CLI:** `hoody watch list`


### Body schemas

- `watch_CreateWatcherRequest` — `{ coalesce_ms: int|null, exclude: string[]|null, history_size: int|null, ignore_dirs: string[]|null, include: string[]|null, kinds: watch_WatchEventKind[]|null, paths*: string[], recursive: bool|null, skip_hidden: bool|null }`
- `watch_WatchEventKind` — `"created" | "modified" | "removed" | "renamed" | "metadata" | "overflow" | "other"`

