# SDKs and CLI reference

> Every method of the TypeScript and Python SDKs, and every command of the CLI.

## TypeScript

`import { Vestra } from '@vestra/sdk'`, then `new Vestra()`: each line of the card is a method, and `vestra.user(id)` acts for one of your users.

```bash
npm install https://vestra.ai/developers/kit/vestra-sdk-0.1.8.tgz && npm pkg set type=module
```

### The client

- `new Vestra(opts?: VestraOptions): Vestra`: Your company's door to Bash: your key from VESTRA_API_KEY, the base URL from VESTRA_BASE_URL, and your tools.
- `vestra.user(id: string): UserBash`: One of your users' Bash, by your own id for them. They are created the first time you act for them.

### `vestra.init` (the [`init`](https://vestra.ai/developers/init.md) line)

- `vestra.init(opts: InitOptions): Draft`: Bash interviews you and drafts vestra.yaml: iterate draft.questions(), then await draft.yaml().
- `vestra.init.closing(deployment: string): Promise<InterviewClosing | null>`: The closing words of an interview run elsewhere (over MCP, say), by its deployment: as Draft.closing() gives them.

### `vestra.deploy` (the [`deploy`](https://vestra.ai/developers/deploy.md) line)

- `vestra.deploy(path?: string, opts?: DeployOptions): Promise<DeployResult>`: Apply vestra.yaml: a new version, promoted in the key's mode once its tests pass; .rollback(name).
- `vestra.deploy.rollback(name: string, opts?: { version?: number; }): Promise<…>`: Put an earlier version back live, at once: the one before, or { version }. Needs a live key.

### `vestra.test` (the [`test`](https://vestra.ai/developers/test.md) line)

- `vestra.test(target?: string, opts?: TestOptions): Promise<TestSummary>`: Run vestra.yaml's tests and wait for the verdict: an edited file is applied first as a version of its own, not promoted (a live key applies nothing), and the summary's note says so; .generate(name) drafts more.
- `vestra.test.wait(runId: string, opts?: { interval_ms?: number; }): Promise<TestSummary>`: A run already started, followed to its verdict: the one a deploy started (its tests.id), which promotes when it passes, so the summary says what it promoted.
- `vestra.test.generate(target?: string, opts?: { count?: number; version?: number; }): Promise<…>`: Scenarios drafted from the deployment's instructions, as vestra.yaml's tests: entries; from `version`, or else the version test mode runs.

### `vestra.ask` (the [`ask`](https://vestra.ai/developers/ask.md) line)

- `vestra.ask(message: string, opts?: AskOptions): Run`: A job in plain words. Returns a Run at once: iterate its events, stop() it, read text(), output() and cost().
- `vestra.ask(items: BatchInput[], opts?: BatchOptions): Batch`: Many users at once: a batch, one conversation each.

### `vestra.routines` (the [`routines`](https://vestra.ai/developers/routines.md) line)

- `vestra.routines.create(input: RoutineInput): Promise<…>`: A job Bash runs on a schedule in plain words (every), as cron (schedule), or whenever your system calls (on: 'call').
- `vestra.routines.list(q?: { user?: string; cursor?: string; limit?: number; }): Promise<Page<…>>`: A user's routines (or your company's own Bash's), with when each runs next.
- `vestra.routines.get(id: string): Promise<…>`: One routine: what it asks, when it runs next, and when it last ran.
- `vestra.routines.update(id: string, patch: Partial<Pick<…>>): Promise<…>`: Change what it asks, when it runs, its filters or its name.
- `vestra.routines.pause(id: string): Promise<…>`: Stop its runs, and keep it.
- `vestra.routines.resume(id: string): Promise<…>`: Start its runs again.
- `vestra.routines.delete(id: string): Promise<{ id: string; object: "routine"; deleted: true; }>`: Delete it: it never runs again.
- `vestra.routines.run(id: string): Promise<{ … }>`: Run it once, now.
- `vestra.routines.trigger(id: string, payload?: Record<string, unknown>): Promise<{ … }>`: Fire a routine that runs on a call, with your system's payload.
- `vestra.routines.runs(id: string, q?: { cursor?: string; limit?: number; }): Promise<Page<…>>`: Its runs, newest first, each with the conversation it ran in.

### `vestra.answer` (the [`answer`](https://vestra.ai/developers/answer.md) line)

- `vestra.answer(id: string, body: { … } | { answers: string[]; } | { done: true; } | { could_not: true; } | { not_now: true; } | { output: unknown; } | { error: string; }): Promise<{ … }>`: Answer what Bash waits on, by its id, with the body its type takes: approve, answers, done, or your tool's output.
- `vestra.answer.list(q?: InteractionQuery): Promise<Page<…>>`: What Bash waits on: every user's, or one user's with `user`.
- `vestra.answer.get(id: string): Promise<…>`: One interaction: what Bash waits on, its questions, its nonce, and how it settled.
- `vestra.answer.cancel(id: string, reason?: string): Promise<…>`: Withdraw it: the task carries on without that action.

### `vestra.watch` (the [`watch`](https://vestra.ai/developers/watch.md) line)

- `vestra.watch(opts?: { control?: boolean; conversation?: string; }): Promise<LiveSession>`: Its screen, live: watch only, or { control: true } so a person can take the wheel.

### `vestra.talk` (the [`talk`](https://vestra.ai/developers/talk.md) line)

- `vestra.talk(opts?: { conversation?: string; voice?: string; }): Promise<LiveSession>`: A voice call with Bash: open session.url and the page joins the call.

### `vestra.events` (the [`events`](https://vestra.ai/developers/events.md) line)

- `vestra.events(id: string, opts?: { after?: number; signal?: AbortSignal; }): AsyncGenerator<VestraEvent, any, any>`: A conversation's events (conv_…), or one task's (task_…), as they happen: its kept rows first, then live. Stop by breaking out.
- `vestra.events.trace(task: string): Promise<…>`: Every step of a task, afterwards, with tokens and cost per turn.
- `vestra.events.to(url: string, opts?: { events?: string[]; description?: string; }): Promise<{ … }>`: Send events to your server, signed. The signing secret is in the answer, shown once.
- `vestra.events.list(q?: EventLogQuery): Promise<{ data: VestraEvent[]; next_cursor: string | null; }>`: The record: the events your webhooks get, newest first.
- `vestra.events.tail(q?: TailQuery): AsyncGenerator<VestraEvent, any, any>`: The record from now on, oldest first, read every interval_ms.
- `vestra.events.handler(fn: (event: VestraEvent) => unknown, opts?: HandlerOptions): WebhookHandler`: A webhook handler: checks the signature, answers your tools' calls itself, and hands every other event to fn.
- `vestra.events.verify(payload: string | Uint8Array<ArrayBufferLike>, header: string | null | undefined, secret?: string): Promise<VestraEvent | null>`: One webhook checked by hand, from its raw body and its Vestra-Signature header.

### `vestra.files` (the [`files`](https://vestra.ai/developers/files.md) line)

- `vestra.files.list(q?: FileQuery): Promise<Page<{ … }>>`: A user's files (or your company's own Bash's), one task's outputs, or one conversation's.
- `vestra.files.get(id: string): Promise<{ … }>`: Its details, with a download link good for a short while.
- `vestra.files.download(id: string): Promise<Uint8Array<ArrayBufferLike>>`: Its bytes.
- `vestra.files.upload(file: Upload, opts?: { user?: string; filename?: string; type?: string; }): Promise<{ … }>`: Hand Bash a file (10 MB at most) for this user, or for your company's own Bash.
- `vestra.files.delete(id: string): Promise<{ id: string; object: "file"; deleted: true; }>`: Delete a file.

### `vestra.connect` (the [`connect`](https://vestra.ai/developers/connect.md) line)

- `vestra.connect(app: string, opts?: { … }): Promise<…>`: A sign-in link for one app: send it to your user, or open it yourself for your company's own Bash.
- `vestra.connect.list(q?: { user?: string; cursor?: string; limit?: number; }): Promise<Page<{ … }>>`: The apps a user has connected (or your company's own Bash has), with each one's status.
- `vestra.connect.get(id: string, opts?: { user?: string; }): Promise<{ … }>`: One connected app and its status.
- `vestra.connect.remove(id: string, opts?: { user?: string; }): Promise<{ object: "connection"; id: string; deleted: true; }>`: Disconnect the app: Bash can no longer use it for this user.
- `vestra.connect.catalog(q?: { … }): Promise<Page<{ … }>>`: Every app your users may connect, after your allowlist.

### `vestra.memory` (the [`memory`](https://vestra.ai/developers/memory.md) line)

- `vestra.memory.list(q?: { user?: string; cursor?: string; limit?: number; }): Promise<…>`: A written profile, and each memory, newest first.
- `vestra.memory.search(q: string, opts?: { user?: string; }): Promise<…>`: Ask what Bash remembers: { found, text }.
- `vestra.memory.add(text: string, opts?: { user?: string; kind?: MemoryKind; }): Promise<{ … }>`: Tell Bash a fact once.
- `vestra.memory.forget(id: string, opts?: { user?: string; }): Promise<{ id: string; deleted: true; }>`: Forget one memory.
- `vestra.memory.forgetAll(opts?: { user?: string; }): Promise<{ … }>`: Forget everything about this user, or your company's own Bash.

### `vestra.embed` (the [`embed`](https://vestra.ai/developers/embed.md) line)

- `vestra.embed(opts?: { … }): Promise<LiveSession>`: Bash inside your app: chat, approvals, the live screen and voice. memory: true adds the memory panel (PATCH /v1/org { memory_panel: true } first).

### `vestra.users` (the [`users`](https://vestra.ai/developers/users.md) line)

- `vestra.users.list(q?: { cursor?: string; limit?: number; }): Promise<Page<…>>`: Your users, with this month's spend.
- `vestra.users.create(user: string, profile?: UserProfile): Promise<…>`: Set a user up before first use (a name, a default deployment, a cap). Safe to repeat.

### `vestra.usage` (the [`usage`](https://vestra.ai/developers/usage.md) line)

- `vestra.usage(q?: UsageQuery): Promise<…>`: What was spent between two dates, grouped by day, user, deployment, conversation, task, model or category.
- `vestra.usage.lineItems(q: { … }): Promise<Page<{ … }>>`: The receipt: the charges behind one conversation or one task.
- `vestra.usage.caps(patch: { … }): Promise<…>`: Set the day and month caps and the alert thresholds.
- `vestra.usage.limits(): Promise<…>`: The caps and alert thresholds, with what is spent against each.

### `vestra.keys` (the [`keys`](https://vestra.ai/developers/keys.md) line)

- `vestra.keys.list(): Promise<Page<{ … }>>`: Your keys: names, modes, scopes and the last four characters, never the secret.
- `vestra.keys.create(opts?: { … }): Promise<{ … }>`: A new key in this key's mode (a test key makes test keys). Its secret is shown once.
- `vestra.keys.roll(id: string, opts?: { grace_seconds?: number; }): Promise<…>`: Replace a key; the old one keeps working for grace_seconds (a day by default).
- `vestra.keys.revoke(id: string): Promise<{ … }>`: Revoke a key: it stops working at once.
- `vestra.keys.me(): Promise<…>`: Your company, and the key in use.

### `vestra.secrets` (the [`secrets`](https://vestra.ai/developers/secrets.md) line)

- `vestra.secrets.set(name: string, value: string, opts?: { user?: string; }): Promise<{ … }>`: Store it, write-only: your company's, or one user's own. Storing it again replaces it.
- `vestra.secrets.list(q?: { user?: string; cursor?: string; limit?: number; }): Promise<Page<{ … }>>`: Names only, never values.
- `vestra.secrets.delete(name: string, opts?: { user?: string; }): Promise<{ … }>`: Delete a secret: your company's, or one user's own.

### `vestra.sessions`

- `vestra.sessions.get(id: string): Promise<{ … }>`: A session's state: open or ended, and what it shows.
- `vestra.sessions.end(id: string): Promise<{ … }>`: End a session now: its token stops working at once.

### `vestra.raw`

- `vestra.raw.GET<P>(path: P, init?: RawInit<Exclude<paths[P]["get"], undefined>>): Promise<RawResult<Ok<Exclude<paths[P]["get"], undefined>>>>`: Read any route by its path, typed from the contract: { data, error, response }.
- `vestra.raw.POST<P>(path: P, init?: RawInit<Exclude<paths[P]["post"], undefined>>): Promise<RawResult<Ok<Exclude<paths[P]["post"], undefined>>>>`: Write to any route by its path, typed from the contract: { data, error, response }.
- `vestra.raw.PUT<P>(path: P, init?: RawInit<Exclude<paths[P]["put"], undefined>>): Promise<RawResult<Ok<Exclude<paths[P]["put"], undefined>>>>`: Put to any route by its path, typed from the contract: { data, error, response }.
- `vestra.raw.PATCH<P>(path: P, init?: RawInit<Exclude<paths[P]["patch"], undefined>>): Promise<RawResult<Ok<Exclude<paths[P]["patch"], undefined>>>>`: Change through any route by its path, typed from the contract: { data, error, response }.
- `vestra.raw.DELETE<P>(path: P, init?: RawInit<Exclude<paths[P]["delete"], undefined>>): Promise<RawResult<Ok<Exclude<paths[P]["delete"], undefined>>>>`: Delete through any route by its path, typed from the contract: { data, error, response }.

### `vestra.user(id)`

- `vestra.user(id).id: string`
- `vestra.user(id).get(): Promise<…>`: Their profile, their spend this month and what they may still spend.
- `vestra.user(id).update(patch: { name?: string | null; email?: string | null; locale?: string | null; timezone?: string | null; metadata?: { [key: string]: unknown; }; spend_cap_usd?: number | null; monthly_cap_usd?: number | null; default_deployment?: string | null; status?: "active" | "disabled"; }): Promise<…>`: Change their name, email, locale, time zone, default deployment, cap or metadata, or switch them off.
- `vestra.user(id).export(): Promise<{ … }>`: Everything held for them, as one download link (a data-rights request).
- `vestra.user(id).delete(): Promise<{ … }>`: Erase their Bash, computer, memory and files. It runs as a job; user.deleted says when it is done.
- `vestra.user(id).warm(): Promise<{ user_id: string; status: "ready" | "waking"; }>`: Wake their computer now, so their next task starts at once.
- `vestra.user(id).screenshot(): Promise<Uint8Array<ArrayBufferLike>>`: What their computer's screen shows now, as a JPEG.

### `run`

An ask, as it happens: iterate it with for await to read each event, and answer what Bash waits on as it comes.

- `run.conversation: string | null`: The conversation (conv_…), once the server has named it.
- `run.status: RunStatus | null`: How it ended: completed, failed or stopped. Null while it runs, or when it failed before it started.
- `run.stop(): Promise<{ … } | null>`: Stop the reply and every piece of work the conversation started.
- `run.text(): Promise<string>`: The answer, once the run has ended, trimmed: when Bash went to work, the work's answer, its final reply (task.completed's answer; from an older server without one, its summary, which joins every step's words); otherwise, and when the work ended with no final reply, the reply's answer, its final words (message.completed's answer; from an older server without one, the whole reply). Throws the turn's error when the turn failed (as output() does), so a failed turn never reads as an empty answer.
- `run.output<T>(): Promise<T>`: The result you asked for with `output`. Throws the turn's error when the turn failed or the answer did not fit it.
- `run.cost(): Promise<RunCost>`: What this run cost, read once it has ended.
- `run.ask(message: string, opts?: Omit<AskOptions, "user" | "conversation" | "deployment">): Run`: Carry on in the same conversation, once this run has ended, so the next message never meets a turn still running.
- `run.unsentAnswers(ms?: number): Promise<ToolCall[]>`: Waits up to `ms` for the answers this run is still sending to your tools' calls (each function's result, then the write that carries it), and returns the calls whose answers had not landed by then: Bash may still be waiting on those, so answer them with vestra.answer. The run never waits on its own answers, so await this before your process exits, on every way out, an error or an abort included.

### `batch`

Many users at once, one conversation each: follow it with wait(), and read each item's result with items().

- `batch.created: Promise<…>`: The batch as the server answered its creation.
- `batch.id: string | null`: bat_…, once the batch exists.
- `batch.get(): Promise<…>`: The batch now: its status and how many items are queued, running, done or failed.
- `batch.items(): AsyncGenerator<…>`: Every item, with its conversation, task and result, page by page.
- `batch.cancel(): Promise<…>`: Start no new item; the running ones finish.
- `batch.wait(opts?: { interval_ms?: number; signal?: AbortSignal; }): Promise<…>`: Until the batch is no longer running; each read rides out a lost connection (a redeploy) for up to 5 minutes.

### `session`

A session opened for a browser: send its url to the page, and end it when you are done.

- `session.id: string`
- `session.url: string`: Open this in a tab or an iframe: chat, approvals, the screen and voice, as the session was opened.
- `session.token: string`: The session's own token: it reaches this user (and this conversation) only, and never money.
- `session.expires_at: string`
- `session.user_id: string | null`
- `session.conversation_id: string | null`
- `session.view: { … }`: The whole answer of POST /v1/sessions.
- `session.end(): Promise<{ … }>`: End it now: its token stops working at once, and its streams and calls close.
- `session.take(): Promise<…>`: Take the wheel (a session opened with control).
- `session.giveBack(): Promise<…>`: Give the wheel back to Bash.
- `session.screen(): Promise<…>`: The screen's stream credentials, made per call, good for 5 minutes.

### `draft`

vestra.init(): Bash interviews you (POST /v1/deployments/interview) and drafts vestra.yaml.

- `draft.started: Promise<…>`
- `draft.questions(opts?: { timeout_ms?: number; }): AsyncGenerator<InterviewQuestion, any, any>`: Bash's questions, one reply at a time. Answer each to go on; it ends once the draft is saved, or at a question left unanswered. An interview that says nothing for timeout_ms (INTERVIEW_WAIT_MS) ends with interview_failed.
- `draft.isDrafted(): Promise<boolean>`: Whether the interview has saved its draft (the draft moved past version 1).
- `draft.yaml(opts?: { interval_ms?: number; timeout_ms?: number; }): Promise<string>`: vestra.yaml, as the interview drafted it (?format=yaml), once the draft is saved.
- `draft.closing(): Promise<InterviewClosing | null>`: The interview's closing words, once its draft is saved: what the draft sets Bash up to do, the tools your code answers, the spending cap per task, the starter tests, and the next steps. Show them where init ends. Null when the draft version carries none.

### `http`

The one door every call goes through: your key, the base URL, retries and errors. vestra.raw uses it too.

- `http.apiKey: string`
- `http.baseUrl: string`
- `http.fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>`: The fetch every call goes through: yours from the options, or the global one.
- `http.fetch(input: string | Request | URL, init?: RequestInit): Promise<Response>`: The fetch every call goes through: yours from the options, or the global one.
- `http.mode: "live" | "test"`: The key's mode, from its prefix: a test key sees test users, test deployments and test money.
- `http.url(path: string, query?: Query): string`: A route's whole URL, with its query.
- `http.json<T>(method: string, path: string, opts?: CallOptions): Promise<T>`: A call whose answer is JSON, or nothing for a 204.
- `http.readThrough<T>(path: string, opts?: CallOptions): Promise<T>`: A GET whose answer is JSON, read on through a lost connection (Reconnects): what a wait polls.
- `http.writeThrough<T>(method: string, path: string, opts?: CallOptions): Promise<T>`: A write whose answer is JSON, sent under one Idempotency-Key and sent again under it through a lost connection (Reconnects): a 503 while the server checks your key never ends it, and the server takes the write once.
- `http.send(method: string, path: string, opts?: CallOptions, accept?: string): Promise<Response>`: A call whose answer the caller reads (a stream of events, or a file). Throws VestraError unless it is 2xx.
- `http.request(method: string, path: string, opts?: CallOptions, accept?: string): Promise<Response>`: One call as every call is made (vestra.raw's too), with the key, the user agent and the tries above: its answer as it came, 2xx or not.

### Functions

- `errorOf(res: Response): Promise<VestraError>`: A non-2xx answer as a VestraError, from its /v1 error body when it has one.
- `lostConnection(err: unknown): string | null`: Why a failed call is the connection lost: a redeploy's 502, 503 or 504, a transport failure, or a first try that died mid-call and left its key in progress (409 idempotency_in_progress: the server says retry in a moment, with the same key). Null for an answer to act on, an abort, or a bug.
- `eventFromStream(row: StreamRow, where: { user: string | null; conversation_id: string | null; }): RawEvent | null`: A stream row, read for one user's conversation, as the one shape.
- `eventFromWebhook(body: WebhookBody): RawEvent | null`: A webhook's body (or an item of GET /v1/events) as the one shape.
- `runTool(fn: ToolFunction, args: unknown, call: ToolCall): Promise<ToolAnswer>`: Your function's answer as the answer route takes it: { output } or { error }.
- `tool(def: { … }): Tool`: A tool your code answers: its name, what it does, its input in plain words or JSON Schema, and the function that runs it. It waits for your user's yes unless you mark it read_only (a lookup) or run_without_asking.
- `toJsonSchema(shape: Record<string, unknown>): JsonSchema`: A shape in plain words, as vestra.yaml's tools write their input, as JSON Schema: each field's type is string, number, integer, boolean or object, with [] for a list and ? for optional, and a nested map for a nested object: { pnr: 'string', seats: 'integer?', legs: 'string[]', trip: { from: 'string' } }. An input with type: 'object', properties or $schema is JSON Schema and is kept as it is.
- `nameFrom(describe: string): string`: A deployment name from the start of a description: lowercase words and dashes, at most 63.
- `plainClosing<C>(c: C): C`: The closing words in plain words: the model wrote the summary and the tool descriptions, and the next steps name them. The server cleans them where it writes them; this is the second layer, where the developer reads them.
- `testRunOutcome(run: { … } & RunPromotion): string`: A test run's status in plain words: an error says why, from its error_code, and a run a restart interrupted says so: resumed, or interrupted once too often.
- `skillBundleSha256(files: readonly BundleFile[]): string`: sha256 over each file's path, its byte length and its bytes (UTF-8), in path order: the server's Skill.sha256.
- `parseSseFrames(buffer: string): { frames: VestraStreamEvent[]; rest: string; }`: Split an SSE buffer into complete frames and the unfinished rest. Comment lines are skipped.
- `streamEvents(opts: StreamOptions): AsyncGenerator<VestraStreamEvent, any, any>`: Follow a /v1 event stream. A dropped or closed connection, or one with nothing for 45 s, not even the server's 15 s ping, reconnects with Last-Event-ID, so nothing is missed and nothing repeats: through a redeploy too, for up to 5 minutes without a new event (RECONNECT), then connection_lost. Stop by breaking out of the loop or aborting the signal. A 410 means the resume point is gone (older than retention, or from another stream): read the resource, start over.
- `signPayload(secret: string, timestampSec: number, payload: string): string`: Hex HMAC-SHA256 of `${timestampSec}.${payload}` with the endpoint's signing secret.
- `verifyWebhook(input: { … }): unknown`: Check a webhook before trusting it. `payload` is the raw request body, exactly as received (never re-serialized JSON), as text or as bytes (read as UTF-8). Returns the parsed body.

### Reconnects

- `new Reconnects(clock?: { … }): Reconnects`: One lost connection after another: `ok()` when it is back, `lost(why)` waits before the next try, or throws connection_lost once the budget is spent.
- `Reconnects.ok(): void`: The connection is back (a new event, or an answer): the next loss starts a fresh 5 minutes, and short waits.
- `Reconnects.opened(): void`: A 2xx connection stayed up past RECONNECT.heldOpenMs and said something: the next loss starts a fresh 5 minutes; the waits keep growing until a frame comes. One that answers and closes at once, or never says a word, does not count, so a server that only does that is given up on.
- `Reconnects.lost(why: string, signal?: AbortSignal): Promise<void>`: Lost again: wait before the next try (1 s doubling to 15 s, with jitter), or throw connection_lost once 5 minutes have passed.

### VestraError

- `new VestraError(status: number, error?: ErrorBody): VestraError`: The /v1 error body, { error: { type, code, message, param?, request_id } }, as an exception.
- `VestraError.status: number`
- `VestraError.type: string`
- `VestraError.code: string`
- `VestraError.param: string | undefined`
- `VestraError.requestId: string | undefined`
- `VestraError.names: string[] | undefined`: secret_missing: the secrets to store before the call can succeed.
- `VestraError.deployments: string[] | undefined`: mcp_server_shared: the other deployments that run the MCP server the file would change.

### StreamExpiredError

- `new StreamExpiredError(message?: string): StreamExpiredError`: The events after your resume point are no longer kept: read the conversation again, and follow it from now.
- `new StreamExpiredError(message?: string, options?: ErrorOptions): StreamExpiredError`: The events after your resume point are no longer kept: read the conversation again, and follow it from now.
- `StreamExpiredError.code: "stream_expired"`

### StreamHttpError

- `new StreamHttpError(status: number): StreamHttpError`: The stream answered an error that retrying cannot fix: a bad key, no access, no such stream, or a 429 that outlasted its waits.
- `StreamHttpError.status: number`

### WebhookVerificationError

- `new WebhookVerificationError(code: "missing_header" | "bad_header" | "too_old" | "bad_signature", message: string): WebhookVerificationError`: A webhook failed its check: no signature header, a bad one, too old, or a signature that does not match.
- `WebhookVerificationError.code: "missing_header" | "bad_header" | "too_old" | "bad_signature"`

### Constants

- `DEFAULT_BASE_URL: "https://api.vestra.ai"`: The API's address when neither baseUrl nor VESTRA_BASE_URL is given.
- `RECONNECT: { … }`: How a reader rides out a lost connection (a redeploy answers 502 for a minute or two, or refuses the connection): waits that double from 1 s to at most 15 s, each with jitter, for up to 5 minutes in a row. Past that, it gives up with connection_lost.
- `SDK_VERSION: "0.1.8"`: This SDK's version, sent in its user agent.
- `EVENT_DELIVERY: { … }`: Where each type is delivered: the stream carries what happens in a conversation, webhooks the durable events.
- `EVENT_TYPES: readonly ["message.delta", "message.completed", "task.started", "task.progress", "task.completed", "task.failed", "task.stopped", "interaction.created", "interaction.resolved", "interaction.expired", "screen.control", "session.started", "session.ended", "file.created", "routine.fired", "usage.updated", "usage.threshold_reached", "usage.cap_reached", "conversation.status", "user.created", "user.deleted", "connection.completed", "connection.failed", "connection.removed", "memory.observed", "batch.completed", "deployment.version_created", "deployment.promoted", "deployment.rolled_back", "test_run.completed", "key.created", "key.revoked", "error", "ping"]`
- `INTERVIEW_WAIT_MS: 600000`: How long the interview may say nothing before init gives up (questions()), and how long yaml() waits for the draft.
- `SIGNATURE_HEADER: "vestra-signature"`
- `CONTRACT_TYPES_RESOLVE: true`: Compile-time proof that every name above is a real route's answer: a moved route or a wrong status makes one `never`, and this line stops compiling.

## Python

`from vestra import Vestra`, then `Vestra()`: it blocks, and each call returns its answer. `AsyncVestra` has the same names, each awaited.

```bash
pip install https://vestra.ai/developers/kit/vestra-0.1.8-py3-none-any.whl
```

### The client

- `Vestra(api_key: Optional[str] = None, base_url: Optional[str] = None, tools: Optional[Tools] = None, transport: Optional[Transport] = None, max_retries: int = 2)`: api_key defaults to VESTRA_API_KEY, base_url to VESTRA_BASE_URL. tools answers Bash's calls to your tools.
- `vestra.close() -> None`: End the client: the work still on its way is cancelled, its connections closed, and its loop stopped.
- `AsyncVestra(api_key: Optional[str] = None, base_url: Optional[str] = None, tools: Optional[Tools] = None, transport: Optional[Transport] = None, max_retries: int = 2)`: Your company's door to Bash, awaited: the same lines as Vestra. api_key defaults to VESTRA_API_KEY, base_url to VESTRA_BASE_URL.
- `vestra.aclose() -> None`: Close the client's connections. `async with AsyncVestra() as vestra:` closes them for you.
- `vestra.user(user_id: str) -> AsyncUserBash`: One of your users' Bash, by your own id for them.

### `vestra.init` (the [`init`](https://vestra.ai/developers/init.md) line)

- `vestra.init(describe: str, for_: str = 'customers', name: Optional[str] = None) -> AsyncDraft`: Bash interviews you and drafts vestra.yaml: iterate draft.questions(), then await draft.yaml().

### `vestra.deploy` (the [`deploy`](https://vestra.ai/developers/deploy.md) line)

- `vestra.deploy(path: str = 'vestra.yaml', live: bool = False, promote: str = 'after_tests', shared: str = 'write') -> Data`: Apply vestra.yaml: a new version, promoted in the key's mode once its tests pass (promote='now' or 'no' to choose).
- `vestra.deploy.rollback(name: str, version: Optional[int] = None) -> Data`: Put an earlier version back live, at once: the one before, or version=. Needs a live key.

### `vestra.test` (the [`test`](https://vestra.ai/developers/test.md) line)

- `vestra.test(target: str = 'vestra.yaml', version: Optional[int] = None, user_secrets: Optional[Mapping[str, str]] = None, interval: float = 3.0) -> Data`: Run the tests and wait for the verdict: an edited vestra.yaml is applied first as a version of its own, not promoted (a live key applies nothing), and the summary's note says so.
- `vestra.test.generate(target: str = 'vestra.yaml', count: Optional[int] = None, version: Optional[int] = None) -> List[Data]`: Scenarios drafted from the deployment's instructions, as vestra.yaml's tests: entries; from `version`, or else the version test mode runs.
- `vestra.test.wait(run_id: str, interval: float = 3.0) -> Data`: A run already started, followed to its verdict: the one a deploy started (its tests.id), which promotes when it passes, so the summary says what it promoted.

### `vestra.ask` (the [`ask`](https://vestra.ai/developers/ask.md) line)

- `vestra.ask(what: Union[str, Sequence[Mapping[str, Any]]], **opts: Any) -> Union[AsyncRun, AsyncBatch]`: A job in plain words (an AsyncRun; user= names whose Bash), or a list of {"user", "message"} for many users at once (an AsyncBatch).

### `vestra.routines` (the [`routines`](https://vestra.ai/developers/routines.md) line)

- `vestra.routines.create(ask: str, *, every: Optional[str] = None, timezone: Optional[str] = None, schedule: Optional[Mapping[str, str]] = None, on: Optional[str] = None, filters: Optional[Mapping[str, Any]] = None, name: Optional[str] = None, conversation: Optional[str] = None, deployment: Optional[str] = None, enabled: Optional[bool] = None, user: Optional[str] = None) -> Data`: every: "weekday at 08:00", "mon, wed, fri at 18:30", "30 minutes". on="call": it runs whenever your system posts to its trigger_url.
- `vestra.routines.delete(routine_id: str) -> Data`: Delete it: it never runs again.
- `vestra.routines.get(routine_id: str) -> Data`: One routine: what it asks, when it runs next, and when it last ran.
- `vestra.routines.list(user: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: A user's routines (or your company's own Bash's), with when each runs next.
- `vestra.routines.pause(routine_id: str) -> Data`: Stop its runs, and keep it.
- `vestra.routines.resume(routine_id: str) -> Data`: Start its runs again.
- `vestra.routines.run(routine_id: str) -> Data`: Run it once, now.
- `vestra.routines.runs(routine_id: str, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: Its runs, newest first, each with the conversation it ran in.
- `vestra.routines.trigger(routine_id: str, payload: Optional[Mapping[str, Any]] = None) -> Data`: Fire a routine that runs on a call, with your system's payload.
- `vestra.routines.update(routine_id: str, **patch: Any) -> Data`: Change what it asks, when it runs, its filters or its name.

### `vestra.answer` (the [`answer`](https://vestra.ai/developers/answer.md) line)

- `vestra.answer(interaction_id: str, **body: Any) -> Data`: Answer what Bash waits on, by its id, with the body its type takes: approve=, answers=, done=, or your tool's output=.
- `vestra.answer.cancel(interaction_id: str, reason: Optional[str] = None) -> Data`: Withdraw it: the task carries on without that action.
- `vestra.answer.get(interaction_id: str) -> Data`: One interaction: what Bash waits on, its questions, its nonce, and how it settled.
- `vestra.answer.list(user: Optional[str] = None, conversation: Optional[str] = None, task: Optional[str] = None, type: Optional[str] = None, status: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: What Bash waits on (pending, unless you ask for another status): every user's, or one user's.

### `vestra.watch` (the [`watch`](https://vestra.ai/developers/watch.md) line)

- `vestra.watch(control: bool = False, conversation: Optional[str] = None) -> LiveSession`: Its screen, live: watch only, or control=True so a person can take the wheel.

### `vestra.talk` (the [`talk`](https://vestra.ai/developers/talk.md) line)

- `vestra.talk(conversation: Optional[str] = None, voice: Optional[str] = None) -> LiveSession`: A voice call with Bash: open session.url and the page joins the call.

### `vestra.events` (the [`events`](https://vestra.ai/developers/events.md) line)

- `vestra.events(id: str, after: int = 0) -> AsyncIterator[Event]`: A conversation's events (conv_...) or one task's (task_...), its kept rows first, then live. Stop by breaking out.
- `vestra.events.handler(fn: Callable[[Event], Any], secret: Optional[str] = None, tolerance: int = 300) -> 'WebhookHandler'`: A webhook handler: checks the signature, answers your tools' calls itself, and hands every other event to fn.
- `vestra.events.list(type: Optional[str] = None, user: Optional[str] = None, task: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: The record: the events your webhooks get, newest first, as the one shape (lists leave `interaction` None: read it with answer.get).
- `vestra.events.tail(type: Optional[str] = None, user: Optional[str] = None, after: Optional[str] = None, interval: float = 2.0) -> AsyncIterator[Event]`: The record from now on (or after an evt_ id), oldest first, read every `interval` seconds. Stop by breaking out.
- `vestra.events.to(url: str, events: Optional[Sequence[str]] = None, description: Optional[str] = None) -> Data`: Send events to your server, signed. The signing secret is in the answer, shown once.
- `vestra.events.trace(task_id: str) -> Data`: Every step of a task, afterwards, with tokens and cost per turn.
- `vestra.events.verify(payload: Union[str, bytes], header: Optional[str], secret: Optional[str] = None) -> Optional[Event]`: One webhook checked by hand, from its raw body and its Vestra-Signature header.

### `vestra.files` (the [`files`](https://vestra.ai/developers/files.md) line)

- `vestra.files.delete(file_id: str) -> Data`: Delete a file.
- `vestra.files.download(file_id: str) -> bytes`: Its bytes.
- `vestra.files.get(file_id: str) -> Data`: Its details, with a download link good for a short while.
- `vestra.files.list(user: Optional[str] = None, conversation: Optional[str] = None, task: Optional[str] = None, source: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: A user's files (or your company's own Bash's), one task's outputs, or one conversation's.
- `vestra.files.upload(file: Any, filename: Optional[str] = None, content_type: Optional[str] = None, user: Optional[str] = None) -> Data`: Hand Bash a file (10 MB at most): bytes, a path, or an open file.

### `vestra.connect` (the [`connect`](https://vestra.ai/developers/connect.md) line)

- `vestra.connect(app: str, user: Optional[str] = None, return_url: Optional[str] = None, deployment: Optional[str] = None) -> Data`: A sign-in link for one app: send it to your user, or open it yourself for your company's own Bash.
- `vestra.connect.catalog(search: Optional[str] = None, category: Optional[str] = None, deployment: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: Every app your users may connect, after your allowlist.
- `vestra.connect.get(connection_id: str, user: Optional[str] = None) -> Data`: One connected app and its status.
- `vestra.connect.list(user: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: The apps a user has connected (or your company's own Bash has), with each one's status.
- `vestra.connect.remove(connection_id: str, user: Optional[str] = None) -> Data`: Disconnect the app: Bash can no longer use it for this user.

### `vestra.memory` (the [`memory`](https://vestra.ai/developers/memory.md) line)

- `vestra.memory.add(text: str, kind: Optional[str] = None, user: Optional[str] = None) -> Data`: Tell Bash a fact once.
- `vestra.memory.forget(memory_id: str, user: Optional[str] = None) -> Data`: Forget one memory.
- `vestra.memory.forget_all(user: Optional[str] = None) -> Data`: Forget everything about this user, or your company's own Bash.
- `vestra.memory.list(user: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: A written profile (.profile), and each memory, newest first.
- `vestra.memory.search(q: str, user: Optional[str] = None) -> Data`: Ask what Bash remembers: .found and .text.

### `vestra.embed` (the [`embed`](https://vestra.ai/developers/embed.md) line)

- `vestra.embed(memory: bool = False, conversation: Optional[str] = None, **opts: Any) -> LiveSession`: Bash inside your app; chat=, screen= and voice= choose what the page shows.

### `vestra.users` (the [`users`](https://vestra.ai/developers/users.md) line)

- `vestra.users.create(user: str, **profile: Any) -> Data`: Set a user up before first use (a name, a default deployment, a cap). Safe to repeat.
- `vestra.users.list(cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: Your users, with this month's spend.

### `vestra.usage` (the [`usage`](https://vestra.ai/developers/usage.md) line)

- `vestra.usage(from_: Optional[str] = None, to: Optional[str] = None, group_by: Optional[str] = None, user: Optional[str] = None, deployment: Optional[str] = None, conversation: Optional[str] = None) -> Data`: from_ and to are dates (2026-10-01); group_by is day, user, deployment, conversation, task, model or category.
- `vestra.usage.caps(**patch: Any) -> Data`: Set the caps and the alert thresholds: org_daily_usd, org_monthly_usd, user_daily_usd, user_monthly_usd, alert_thresholds.
- `vestra.usage.limits() -> Data`: The caps and alert thresholds, with what is spent against each.
- `vestra.usage.line_items(conversation: Optional[str] = None, task: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: The receipt: the charges behind one conversation or one task.

### `vestra.keys` (the [`keys`](https://vestra.ai/developers/keys.md) line)

- `vestra.keys.create(name: Optional[str] = None, scopes: Optional[Sequence[str]] = None, expires_at: Optional[str] = None, mode: Optional[str] = None) -> Data`: A new key in this key's mode (a test key makes test keys). Its secret is shown once.
- `vestra.keys.list() -> Data`: Your keys: names, modes, scopes and the last four characters, never the secret.
- `vestra.keys.me() -> Data`: Your company, and the key in use.
- `vestra.keys.revoke(key_id: str) -> Data`: Revoke a key: it stops working at once.
- `vestra.keys.roll(key_id: str, grace_seconds: Optional[int] = None) -> Data`: Replace a key; the old one keeps working for grace_seconds (a day by default).

### `vestra.secrets` (the [`secrets`](https://vestra.ai/developers/secrets.md) line)

- `vestra.secrets.delete(name: str, user: Optional[str] = None) -> Data`: Delete a secret: your company's, or one user's own.
- `vestra.secrets.list(user: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: Names only, never values.
- `vestra.secrets.set(name: str, value: str, user: Optional[str] = None) -> Data`: Store it, write-only: your company's, or one user's own. Storing it again replaces it.

### `vestra.raw`

- `vestra.raw`: Every /v1 route as a generated function: vestra._generated's AuthenticatedClient, sent through this client as every call is (your key, the user agent, a rate limit waited out). Use its asyncio functions. Needs httpx and attrs.

### `vestra.sessions`

- `vestra.sessions.end(session_id: str) -> Data`: End a session now: its token stops working at once.
- `vestra.sessions.get(session_id: str) -> Data`: A session's state: open or ended, and what it shows.

### `vestra.user(id)`

- `vestra.user(id).delete() -> Any`: Erase their Bash, computer, memory and files. It runs as a job; user.deleted says when it is done.
- `vestra.user(id).export() -> Any`: Everything held for them, as one download link (a data-rights request).
- `vestra.user(id).get() -> Any`: Their profile, their spend this month and what they may still spend.
- `vestra.user(id).screenshot() -> bytes`: What their computer's screen shows now, as JPEG bytes.
- `vestra.user(id).update(**patch: Any) -> Any`: Change their name, email, locale, time zone, default deployment, cap or metadata, or switch them off.
- `vestra.user(id).warm() -> Any`: Wake their computer now, so their next task starts at once.

### `run`

An ask, as it happens: iterate it with async for to read each event, and answer what Bash waits on as it comes.

- `run.ask(message: str, **opts: Any) -> 'AsyncRun'`: Carry on in the same conversation, once this run has ended, so the next message never meets a turn still running.
- `run.cost() -> RunCost`: What this run cost, read once it has ended.
- `run.output() -> Any`: The result you asked for with output=. Raises the turn's error when the turn failed or the answer did not fit it.
- `run.stop() -> Optional[Data]`: Stop the reply and every piece of work the conversation started.
- `run.text() -> str`: The answer, once the run has ended, trimmed: when Bash went to work, the work's answer, its final reply (task.completed's answer; from an older server without one, its summary, which joins every step's words); otherwise, and when the work ended with no final reply, the reply's answer, its final words (message.completed's answer; from an older server without one, the whole reply). Raises the turn's error when the turn failed (as output() does), so a failed turn never reads as an empty answer.
- `run.unsent_answers(timeout: float = 10.0) -> List[ToolCall]`: Waits up to `timeout` seconds for the answers this run is still sending to your tools' calls (each function's result, then the write that carries it), and returns the calls whose answers had not landed by then: Bash may still be waiting on those, so answer them with vestra.answer. The run never waits on its own answers, so await this before your process exits, on every way out, an error or a cancel included.

### `batch`

One batch: read it, its items, cancel it, or wait for it.

- `batch.cancel() -> Data`: Start no new item; the running ones finish.
- `batch.get() -> Data`: The batch now: its status and how many items are queued, running, done or failed.
- `batch.id`: bat_..., once the batch exists.
- `batch.items() -> AsyncIterator[Data]`: Every item, with its conversation, task and result, page by page.
- `batch.wait(interval: float = 5.0) -> Data`: Until the batch is no longer running; each read rides out a lost connection (a redeploy) for up to 5 minutes.

### `session`

One session: open .url in a tab or an iframe. Its token reaches this user only, and never money.

- `session.end() -> Data`: End it now: its token stops working at once, and its streams and calls close.
- `session.give_back() -> Data`: Give the wheel back to Bash.
- `session.screen() -> Data`: The screen's stream credentials, made per call, good for 5 minutes.
- `session.take() -> Data`: Take the wheel (a session opened with control).

### `draft`

vestra.init(): Bash interviews you (POST /v1/deployments/interview) and drafts vestra.yaml.

- `draft.closing() -> Optional[Data]`: The interview's closing words, once its draft is saved: what the draft sets Bash up to do, the tools your code answers, the spending cap per task, the starter tests, and the next steps. Show them where init ends. None when the draft version carries none.
- `draft.is_drafted() -> bool`: Whether the interview has saved its draft (the draft moved past version 1).
- `draft.questions(timeout: float = 600.0) -> AsyncIterator[InterviewQuestion]`: Bash's questions, one reply at a time. Answer each to go on; it ends once the draft is saved, or at a question left unanswered. An interview that says nothing for timeout seconds ends with interview_failed.
- `draft.yaml(interval: Optional[float] = None, timeout: float = 600.0) -> str`: vestra.yaml, as the interview drafted it (?format=yaml), once the draft is saved: looked for every interval seconds, or from 2 s growing to 5 s when none is given.

### `event`

One event, the same shape from the stream and from a webhook: its type, your user, its conversation and task, and its data.

- `event.answer(**body: Any) -> Any`: Answer what Bash waits on with the body its type takes: approve= (with present_user= or message=), answers=, done=, could_not=, not_now=, output= or error=. An approval that approves gets the interaction's nonce when it needs one.

### Functions

- `tool(*, description: str, input: Optional[Mapping[str, Any]] = None, name: Optional[str] = None, timeout_s: Optional[int] = None, read_only: bool = False, run_without_asking: bool = False) -> Callable[[Callable[..., Any]], Tool]`: @tool(description="Live status of a PNR", input={"pnr": "string"}, read_only=True) over a function makes it a declared tool. Unmarked, its call waits for your user's yes; read_only (a lookup) and run_without_asking don't.
- `verify_webhook(payload: Union[str, bytes], header: Optional[str], secret: str, tolerance: int = 300, now: Optional[int] = None) -> object`: Verify the raw body against the Vestra-Signature header and return it parsed.
- `sign_payload(secret: str, timestamp: int, payload: Union[str, bytes]) -> str`: Hex HMAC-SHA256 of '<timestamp>.<payload>' with the endpoint's signing secret.

### VestraError

- `VestraError(status: int, error: Optional[Mapping[str, Any]] = None)`: The /v1 error body, {"error": {type, code, message, param?, request_id}}, as an exception.

### WebhookVerificationError

- `WebhookVerificationError(code: str, message: str)`: A webhook failed its check: no signature header, a bad one, too old, or a signature that does not match.

### ToolCall

- `ToolCall(id: str, name: str, conversation: Optional[str], task: Optional[str], user: Optional[str]) -> None`: Who and what a call is for, handed to your function as `call` when it takes one.

### Constants

- `EVENT_TYPES: tuple`
- `EVENT_DELIVERY: dict`
- `SIGNATURE_HEADER: str`

## CLI

Every line takes `--user <id>` to act for one of your users, and `--json` to print the API's answer. `vestra <line> --help` says the rest.

```bash
npm install -g https://vestra.ai/developers/kit/vestra-cli-0.1.8.tgz
```

### `vestra init`

Bash interviews you and writes vestra.yaml.

```text
vestra init ["<what Bash should do>"] [--for customers|us] [--name <slug>] [--force] [--answers <file> | --answers -] [--yes]

Bash asks a few questions right here (an empty line sends your answer), then writes vestra.yaml in this
folder. With --for us (your company's own work) it offers to connect the apps the file names (--yes: each
one, without asking). Routines go into the file by hand: the interview writes none. Needs a test key.

With no one at the terminal (a coding agent, CI), --answers gives your answers as text, from a file or
from stdin (-): they answer its questions, and a question after them is told to use the defaults.
```

### `vestra deploy`

Apply vestra.yaml to test mode (--live: to your users).

```text
vestra deploy [vestra.yaml] [--live] [--now | --no-promote] [--no-wait] [--secrets-from-env]
vestra deploy --rollback [<name>] [--version <n>]

A new version, promoted once its tests pass: in test mode with a test key, or with --live and a live key.
It waits for the tests here, each verdict as it lands, and exits 0 only once the version runs: 1 when a
test fails, the run ends in an error (an interrupted one too) or the promotion is refused, 2 when the
command is written wrong. A version already being tested (a retried deploy) has that run followed.
Ctrl-C stops the waiting (exit 130), not the run: it still promotes when it passes. --no-wait returns
as the tests start, naming the run to follow with vestra events.
Skill folders and knowledge files upload by content hash, so unchanged ones are skipped. A secret the
file names that is not stored yet is asked for here (with --secrets-from-env, read from the environment
variable of its name), stored, and the deploy tried again.
Skills, knowledge and company MCP servers are not versioned yet: every version reads them as they are,
live included. So while a deployment runs live, a test key that would change one (a new MCP server URL
included) exits 2 with nothing written, naming it; apply it with vestra deploy --live.
```

### `vestra test`

Run vestra.yaml's tests (--generate: draft more).

```text
vestra test [vestra.yaml | <name>] [--version <n>]   run the tests; a failed one fails the command (for CI)
vestra test --generate [--count 5]                    draft tests from the instructions into vestra.yaml

With a test key, a vestra.yaml that changed since the latest version is applied first, as vestra deploy
--no-promote applies it, and that new version is tested: a test after an edit tests the edit. Once it
passes, vestra deploy promotes it without testing it again. It applies only what a version owns: an edit
to a skill or knowledge, which every version reads as they are (live too), exits 2 with nothing written,
for vestra deploy to apply. A live key applies nothing. An unchanged file tests the latest version; a
deployment by name, or --version, tests what the API holds. A secret the file names is asked for, as
deploy asks for it (--secrets-from-env).
--generate applies an edited file first too, the same way, so its drafts read the edit.
```

### `vestra ask "..."`

Ask Bash to do something (--user, --watch, --users).

```text
vestra ask "<message>" [--user <id>] [--conversation <conv_id>] [--deployment <slug>]
                       [--watch] [--tools ./tools.mjs] [--hand-off] [--present-user] [--no-open]
vestra ask "<message>" --users <file.csv> [--deployment <slug>]   many users at once, one conversation each
vestra ask --stop <conv_id>                                        stop a conversation and the work it started

Approvals and questions are asked right here. Ctrl-C stops the conversation; twice leaves it running.
A yes that needs the person present: with a test key, your y is the test user's; with a live key, it asks
once more whether that person is with you, saying yes themselves. With no one at the terminal, nothing is
answered: it says how, with vestra answer.
--watch opens the live screen. --tools answers calls to your tools from a local module.
--hand-off sends the message straight to work, as a batch item goes, instead of letting Bash reply first.
--present-user: approve a payment, or one of your own tools, for one of your users only while they are with you.
```

### `vestra routines`

Jobs on a schedule in plain words, or on a call (add, run, pause).

```text
vestra routines [--user <id>]
vestra routines add "<ask>" --every "weekday at 08:00" [--timezone Asia/Kolkata] [--name "..."]
                            [--conversation <conv_id>] [--deployment <slug>] [--user <id>]
vestra routines add "<ask>" --on call [--user <id>]   runs whenever your system posts to its trigger_url
vestra routines get|run|pause|resume|delete|runs <rtn_id>

every reads "weekday at 08:00", "mon, wed, fri at 18:30", "30 minutes", "month on the 1st at 09:00",
in the user's own time zone unless you give --timezone.
```

### `vestra answer <id>`

Answer what Bash waits on (--approve, --answer, --done).

```text
vestra answer [--user <id>] [--conversation <conv_id>]      what Bash waits on
vestra answer <int_id> --approve [--present-user] | --deny [--message "..."]
vestra answer <int_id> --answer "..." | --answers '["...", "..."]' | --skip
vestra answer <int_id> --done | --could-not | --not-now
vestra answer <call_id> --output '<json>' | --error "..."
vestra answer <id>                                         asks you here

--present-user: approve a payment, or one of your own tools, only while the person it is for is with you and
says yes themselves (your user, or on your company's own Bash, a person at your company). Asked here, a y
with a test key is the test user's; with a live key it asks once more whether that person is saying it.
```

### `vestra watch`

Bash's live screen, as a link (--control to take the wheel).

```text
vestra watch [--user <id>] [--conversation <conv_id>] [--control] [--no-open]

A link to the live screen, good for 15 minutes. With --control, whoever opens it can take the wheel.
```

### `vestra talk`

A voice call with Bash, as a link.

```text
vestra talk [--user <id>] [--conversation <conv_id>] [--voice <name>] [--no-open]

What is said lands in the conversation.
```

### `vestra events <id>`

Every step: a conversation, a task, a batch or a test run (--to).

```text
vestra events [--type task.completed] [--user <id>]        the record your webhooks get, newest first
vestra events <conv_id> [--after <seq>] [--follow]         a conversation as it happens, until it is idle
vestra events <task_id> [--follow]                         a task afterwards: each step and its cost
vestra events <bat_id>                                     a batch, item by item as each finishes
vestra events <trun_id>                                    a test run, scenario by scenario
vestra events --to https://your.app/vestra [--type ...]    send events to your server, signed
vestra events --to localhost:3000/vestra [--secret ...]    forward them to this computer while you build

A conversation is followed until a read of it says it is idle with no open work, approval or call to your
tools: every event up to then is printed, and the command ends (exit 0). --follow keeps watching it after
that, until Ctrl-C, as a task's --follow watches the task as it happens. A lost connection is read again for
up to 5 minutes; past that the command fails (exit 1), naming the --after to pick up from.
```

### `vestra files`

What Bash made or was given (get, add, delete).

```text
vestra files [<conv_id>|<task_id>] [--user <id>] [--source upload|output]
vestra files get <file_id> [--out <path>] [--force]     download it here
vestra files add <path> [--user <id>]                   hand Bash a file, 10 MB at most
vestra files delete <file_id>
```

### `vestra connect <app>`

A sign-in link for an app (gmail, google-sheets, slack...).

```text
vestra connect [--user <id>]                   what is connected, for your company's own Bash or one user
vestra connect <app> [--user <id>]             a sign-in link: yours opens in your browser; a user's is for them
vestra connect --remove <con_id> [--user <id>]
vestra connect --catalog [--search <words>]    every app your users may connect
```

### `vestra memory`

What Bash remembers (search, add, forget).

```text
vestra memory [--user <id>]                                   the profile, and each memory
vestra memory search "<question>" [--user <id>]
vestra memory add "<fact>" [--kind preference] [--user <id>]  kinds: identity, relationship, commitment, preference, project_state, other
vestra memory forget <mem_id> [--user <id>]
vestra memory forget --all [--user <id>] [--yes]
```

### `vestra embed`

Bash inside your app: chat, approvals, the screen, voice.

```text
vestra embed [--user <id>] [--memory] [--conversation <conv_id>] [--no-open]

A preview of what your users see. In your app, make one session per visit on your server.
```

### `vestra users`

Your users (export, delete, --cap).

```text
vestra users                                  your users
vestra users <id> [--cap <usd>|none]          one user: spend and what is left; --cap sets their monthly cap
vestra users export <id>                      everything held for them, as one download link
vestra users delete <id> [--yes]              their Bash, computer, memory and files
vestra users warm <id>                        wake their computer now
```

### `vestra usage`

What the work cost (caps, items).

```text
vestra usage [--from 2026-10-01] [--to 2026-10-31] [--by day|user|deployment|conversation|task|model|category]
             [--user <id>] [--deployment <slug>] [--conversation <conv_id>]
vestra usage items <conv_id>|<task_id>        the receipt: every charge behind it
vestra usage caps [--daily <usd>] [--monthly <usd>] [--customer-daily <usd>] [--customer-monthly <usd>]
                  [--alerts 50,80]            none clears a cap; with no flag, the caps as they are
```

### `vestra keys`

Your API keys (create, roll, revoke).

```text
vestra keys                                   your keys
vestra keys create [--name ci] [--scopes runtime,setup,admin]
vestra keys roll <key_id> [--grace <seconds>]  a new key; the old one works for a day, or --grace
vestra keys revoke <key_id>

A new key takes the mode of the key that makes it: use a test key to make test keys.
```

### `vestra secrets`

Values Bash uses but never shows (set, delete).

```text
vestra secrets [--user <id>]                  the names, never the values
vestra secrets set <NAME> [--user <id>]       type the value when asked
vestra secrets set <NAME> --value - [--user <id>]                the value on stdin, piped in
vestra secrets set <NAME> --value-from-env <VAR> [--user <id>]   the value of VAR in your environment
vestra secrets delete <NAME> [--user <id>]

With no --user, a secret is your company's: ${NAME} in vestra.yaml names it. stdin is read only with
--value -, and a value is never a word on the command line, where it would stay in your shell history.
```

### `vestra mcp`

A local MCP server for your editor, over stdio.

```text
vestra mcp

A local MCP server for your editor, over stdio. Add it to your editor as a stdio server:
  command: vestra    args: ["mcp"]
It uses the key vestra login saved, or VESTRA_API_KEY. Every tool takes an optional user; with none it
acts for your company's own Bash. Approvals are asked in the editor when it supports MCP elicitation.
init, deploy and test read and write vestra.yaml in the folder the editor started it in. search_docs and
read_doc read the developer docs of the API's own site (VESTRA_DOCS_URL to read another).
```

### `vestra login`

Save your key on this computer.

```text
vestra login [--key vk_test_... | --key -] [--base-url https://api-staging.vestra.ai]

Checks the key with the API, then saves it in ~/.config/vestra/config.json, readable by you alone.
VESTRA_API_KEY and VESTRA_BASE_URL win over the file when they are set. With no one at the terminal
(a coding agent, CI), pass --key, or --key - to read it from stdin.
```

### `vestra dev`

Redeploy on every save, answer your tools, forward events.

```text
vestra dev [vestra.yaml] [--tools ./tools.mjs] [--to localhost:3000/vestra] [--secrets-from-env]

Deploys vestra.yaml to test mode now and on every save, answers Bash's calls to your tools with the
module's functions, and forwards events to your local app, signed. Ctrl-C stops. Needs a test key.
```

### `vestra agent setup`

Set up your coding tools: the Vestra skill and vestra mcp.

```text
vestra agent setup [--yes] [--dry-run] [--print] [--force]
vestra agent skill                              the Vestra skill, to save by hand

Sets up the coding tools on this computer to build with Vestra: it puts the Vestra skill where each
reads skills, and adds vestra mcp to each as an MCP server with the tool's own command (a tool whose
command is not on PATH is left to you, with the step to take; a tool with no such command has its file
edited, after a backup beside it). It shows the plan and asks first: --yes skips the question, --dry-run
only shows the plan, and --print says how to do it by hand. An entry of your own is replaced only when
you say so (--force), and no key is written anywhere: vestra mcp reads the key vestra login saved.
```

## HTTP

Over HTTP there is nothing to install: each line of the card is a route, and [the API reference](https://vestra.ai/developers/reference/api.md) has every one, with every field it takes and answers.

## MCP

Over MCP there is nothing to install: one endpoint, `/v1/mcp`, that any MCP client can call with your key, and `tools/list` names what it offers. [The mcp line](https://vestra.ai/developers/mcp.md) says how to connect.
