new Vestra(opts?: VestraOptions): VestraYour company's door to Bash: your key from VESTRA_API_KEY, the base URL from VESTRA_BASE_URL, and your tools.
Every sample on every page follows this. Change it any time.
Every method of the TypeScript and Python SDKs, and every command of the CLI, read from their source. Each line of the card is one method in each, and one command. The page shows your language: change it at the top.
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.
npm install https://vestra.ai/developers/kit/vestra-sdk-0.1.8.tgz && npm pkg set type=modulenew Vestra(opts?: VestraOptions): VestraYour 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): UserBashOne of your users' Bash, by your own id for them. They are created the first time you act for them.
vestra.initthe init linevestra.init(opts: InitOptions): DraftBash 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.deploythe deploy linevestra.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.testthe test linevestra.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.askthe ask linevestra.ask(message: string, opts?: AskOptions): RunA 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): BatchMany users at once: a batch, one conversation each.
vestra.routinesthe routines linevestra.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.answerthe answer linevestra.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.watchthe watch linevestra.watch(opts?: { control?: boolean; conversation?: string; }): Promise<LiveSession>Its screen, live: watch only, or { control: true } so a person can take the wheel.
vestra.talkthe talk linevestra.talk(opts?: { conversation?: string; voice?: string; }): Promise<LiveSession>A voice call with Bash: open session.url and the page joins the call.
vestra.eventsthe events linevestra.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): WebhookHandlerA 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.filesthe files linevestra.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.connectthe connect linevestra.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.memorythe memory linevestra.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.embedthe embed linevestra.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.usersthe users linevestra.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.usagethe usage linevestra.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.keysthe keys linevestra.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.secretsthe secrets linevestra.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.sessionsvestra.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.rawvestra.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: stringvestra.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.
runAn 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 | nullThe conversation (conv_…), once the server has named it.
run.status: RunStatus | nullHow 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">): RunCarry 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.
batchMany 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 | nullbat_…, 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.
sessionA session opened for a browser: send its url to the page, and end it when you are done.
session.id: stringsession.url: stringOpen this in a tab or an iframe: chat, approvals, the screen and voice, as the session was opened.
session.token: stringThe session's own token: it reaches this user (and this conversation) only, and never money.
session.expires_at: stringsession.user_id: string | nullsession.conversation_id: string | nullsession.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.
draftvestra.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.
httpThe one door every call goes through: your key, the base URL, retries and errors. vestra.raw uses it too.
http.apiKey: stringhttp.baseUrl: stringhttp.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): stringA 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.
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 | nullWhy 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 | nullA stream row, read for one user's conversation, as the one shape.
eventFromWebhook(body: WebhookBody): RawEvent | nullA 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: { … }): ToolA 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>): JsonSchemaA 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): stringA deployment name from the start of a description: lowercase words and dashes, at most 63.
plainClosing<C>(c: C): CThe 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): stringA 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[]): stringsha256 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): stringHex HMAC-SHA256 of ${timestampSec}.${payload} with the endpoint's signing secret.
verifyWebhook(input: { … }): unknownCheck 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.
new Reconnects(clock?: { … }): ReconnectsOne 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(): voidThe connection is back (a new event, or an answer): the next loss starts a fresh 5 minutes, and short waits.
Reconnects.opened(): voidA 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.
new VestraError(status: number, error?: ErrorBody): VestraErrorThe /v1 error body, { error: { type, code, message, param?, request_id } }, as an exception.
VestraError.status: numberVestraError.type: stringVestraError.code: stringVestraError.param: string | undefinedVestraError.requestId: string | undefinedVestraError.names: string[] | undefinedsecret_missing: the secrets to store before the call can succeed.
VestraError.deployments: string[] | undefinedmcp_server_shared: the other deployments that run the MCP server the file would change.
new StreamExpiredError(message?: string): StreamExpiredErrorThe events after your resume point are no longer kept: read the conversation again, and follow it from now.
new StreamExpiredError(message?: string, options?: ErrorOptions): StreamExpiredErrorThe events after your resume point are no longer kept: read the conversation again, and follow it from now.
StreamExpiredError.code: "stream_expired"new StreamHttpError(status: number): StreamHttpErrorThe 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: numbernew WebhookVerificationError(code: "missing_header" | "bad_header" | "too_old" | "bad_signature", message: string): WebhookVerificationErrorA 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"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: 600000How 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: trueCompile-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.
from vestra import Vestra, then Vestra(): it blocks, and each call returns its answer. AsyncVestra has the same names, each awaited.
pip install https://vestra.ai/developers/kit/vestra-0.1.8-py3-none-any.whlVestra(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() -> NoneEnd 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() -> NoneClose the client's connections. async with AsyncVestra() as vestra: closes them for you.
vestra.user(user_id: str) -> AsyncUserBashOne of your users' Bash, by your own id for them.
vestra.initthe init linevestra.init(describe: str, for_: str = 'customers', name: Optional[str] = None) -> AsyncDraftBash interviews you and drafts vestra.yaml: iterate draft.questions(), then await draft.yaml().
vestra.deploythe deploy linevestra.deploy(path: str = 'vestra.yaml', live: bool = False, promote: str = 'after_tests', shared: str = 'write') -> DataApply 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) -> DataPut an earlier version back live, at once: the one before, or version=. Needs a live key.
vestra.testthe test linevestra.test(target: str = 'vestra.yaml', version: Optional[int] = None, user_secrets: Optional[Mapping[str, str]] = None, interval: float = 3.0) -> DataRun 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) -> DataA 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.askthe ask linevestra.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.routinesthe routines linevestra.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) -> Dataevery: "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) -> DataDelete it: it never runs again.
vestra.routines.get(routine_id: str) -> DataOne 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) -> DataA user's routines (or your company's own Bash's), with when each runs next.
vestra.routines.pause(routine_id: str) -> DataStop its runs, and keep it.
vestra.routines.resume(routine_id: str) -> DataStart its runs again.
vestra.routines.run(routine_id: str) -> DataRun it once, now.
vestra.routines.runs(routine_id: str, cursor: Optional[str] = None, limit: Optional[int] = None) -> DataIts runs, newest first, each with the conversation it ran in.
vestra.routines.trigger(routine_id: str, payload: Optional[Mapping[str, Any]] = None) -> DataFire a routine that runs on a call, with your system's payload.
vestra.routines.update(routine_id: str, **patch: Any) -> DataChange what it asks, when it runs, its filters or its name.
vestra.answerthe answer linevestra.answer(interaction_id: str, **body: Any) -> DataAnswer 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) -> DataWithdraw it: the task carries on without that action.
vestra.answer.get(interaction_id: str) -> DataOne 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) -> DataWhat Bash waits on (pending, unless you ask for another status): every user's, or one user's.
vestra.watchthe watch linevestra.watch(control: bool = False, conversation: Optional[str] = None) -> LiveSessionIts screen, live: watch only, or control=True so a person can take the wheel.
vestra.talkthe talk linevestra.talk(conversation: Optional[str] = None, voice: Optional[str] = None) -> LiveSessionA voice call with Bash: open session.url and the page joins the call.
vestra.eventsthe events linevestra.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) -> DataThe 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) -> DataSend events to your server, signed. The signing secret is in the answer, shown once.
vestra.events.trace(task_id: str) -> DataEvery 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.filesthe files linevestra.files.delete(file_id: str) -> DataDelete a file.
vestra.files.download(file_id: str) -> bytesIts bytes.
vestra.files.get(file_id: str) -> DataIts 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) -> DataA 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) -> DataHand Bash a file (10 MB at most): bytes, a path, or an open file.
vestra.connectthe connect linevestra.connect(app: str, user: Optional[str] = None, return_url: Optional[str] = None, deployment: Optional[str] = None) -> DataA 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) -> DataEvery app your users may connect, after your allowlist.
vestra.connect.get(connection_id: str, user: Optional[str] = None) -> DataOne connected app and its status.
vestra.connect.list(user: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> DataThe 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) -> DataDisconnect the app: Bash can no longer use it for this user.
vestra.memorythe memory linevestra.memory.add(text: str, kind: Optional[str] = None, user: Optional[str] = None) -> DataTell Bash a fact once.
vestra.memory.forget(memory_id: str, user: Optional[str] = None) -> DataForget one memory.
vestra.memory.forget_all(user: Optional[str] = None) -> DataForget 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) -> DataA written profile (.profile), and each memory, newest first.
vestra.memory.search(q: str, user: Optional[str] = None) -> DataAsk what Bash remembers: .found and .text.
vestra.embedthe embed linevestra.embed(memory: bool = False, conversation: Optional[str] = None, **opts: Any) -> LiveSessionBash inside your app; chat=, screen= and voice= choose what the page shows.
vestra.usersthe users linevestra.users.create(user: str, **profile: Any) -> DataSet 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) -> DataYour users, with this month's spend.
vestra.usagethe usage linevestra.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) -> Datafrom_ and to are dates (2026-10-01); group_by is day, user, deployment, conversation, task, model or category.
vestra.usage.caps(**patch: Any) -> DataSet the caps and the alert thresholds: org_daily_usd, org_monthly_usd, user_daily_usd, user_monthly_usd, alert_thresholds.
vestra.usage.limits() -> DataThe 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) -> DataThe receipt: the charges behind one conversation or one task.
vestra.keysthe keys linevestra.keys.create(name: Optional[str] = None, scopes: Optional[Sequence[str]] = None, expires_at: Optional[str] = None, mode: Optional[str] = None) -> DataA new key in this key's mode (a test key makes test keys). Its secret is shown once.
vestra.keys.list() -> DataYour keys: names, modes, scopes and the last four characters, never the secret.
vestra.keys.me() -> DataYour company, and the key in use.
vestra.keys.revoke(key_id: str) -> DataRevoke a key: it stops working at once.
vestra.keys.roll(key_id: str, grace_seconds: Optional[int] = None) -> DataReplace a key; the old one keeps working for grace_seconds (a day by default).
vestra.secretsthe secrets linevestra.secrets.delete(name: str, user: Optional[str] = None) -> DataDelete 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) -> DataNames only, never values.
vestra.secrets.set(name: str, value: str, user: Optional[str] = None) -> DataStore it, write-only: your company's, or one user's own. Storing it again replaces it.
vestra.rawvestra.rawEvery /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.sessionsvestra.sessions.end(session_id: str) -> DataEnd a session now: its token stops working at once.
vestra.sessions.get(session_id: str) -> DataA session's state: open or ended, and what it shows.
vestra.user(id)vestra.user(id).delete() -> AnyErase their Bash, computer, memory and files. It runs as a job; user.deleted says when it is done.
vestra.user(id).export() -> AnyEverything held for them, as one download link (a data-rights request).
vestra.user(id).get() -> AnyTheir profile, their spend this month and what they may still spend.
vestra.user(id).screenshot() -> bytesWhat their computer's screen shows now, as JPEG bytes.
vestra.user(id).update(**patch: Any) -> AnyChange their name, email, locale, time zone, default deployment, cap or metadata, or switch them off.
vestra.user(id).warm() -> AnyWake their computer now, so their next task starts at once.
runAn 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() -> RunCostWhat this run cost, read once it has ended.
run.output() -> AnyThe 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() -> strThe 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.
batchOne batch: read it, its items, cancel it, or wait for it.
batch.cancel() -> DataStart no new item; the running ones finish.
batch.get() -> DataThe batch now: its status and how many items are queued, running, done or failed.
batch.idbat_..., 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) -> DataUntil the batch is no longer running; each read rides out a lost connection (a redeploy) for up to 5 minutes.
sessionOne session: open .url in a tab or an iframe. Its token reaches this user only, and never money.
session.end() -> DataEnd it now: its token stops working at once, and its streams and calls close.
session.give_back() -> DataGive the wheel back to Bash.
session.screen() -> DataThe screen's stream credentials, made per call, good for 5 minutes.
session.take() -> DataTake the wheel (a session opened with control).
draftvestra.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() -> boolWhether 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) -> strvestra.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.
eventOne 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) -> AnyAnswer 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.
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) -> objectVerify the raw body against the Vestra-Signature header and return it parsed.
sign_payload(secret: str, timestamp: int, payload: Union[str, bytes]) -> strHex HMAC-SHA256 of '<timestamp>.<payload>' with the endpoint's signing secret.
VestraError(status: int, error: Optional[Mapping[str, Any]] = None)The /v1 error body, {"error": {type, code, message, param?, request_id}}, as an exception.
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(id: str, name: str, conversation: Optional[str], task: Optional[str], user: Optional[str]) -> NoneWho and what a call is for, handed to your function as call when it takes one.
EVENT_TYPES: tupleEVENT_DELIVERY: dictSIGNATURE_HEADER: strEvery 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.
npm install -g https://vestra.ai/developers/kit/vestra-cli-0.1.8.tgzvestra 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 [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 [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 "<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 [--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 [--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 [--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 [--user <id>] [--conversation <conv_id>] [--voice <name>] [--no-open] What is said lands in the conversation.
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 [<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 [--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 [--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 [--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 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 [--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 arevestra 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 [--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. 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 [--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 [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 [--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.
Over HTTP there is nothing to install: each line of the card is a route, and the API reference has every one, with every field it takes and answers.
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 says how to connect.