
# JavaScript SDK

`@cestra/sdk` runs on Node 18 and newer and has zero dependencies. It ships with TypeScript types. `run()` receives events over a WebSocket on Node 22+, and falls back to REST long-polling elsewhere.

```sh
npm install https://docs.cestra.ai/dl/cestra-sdk.tgz
```

```js
import { Agent } from '@cestra/sdk';

const agent = new Agent({ key: process.env.CESTRA_KEY });
await agent.send('Hello from Cestra');
```

`new Agent()` with no options reads `CESTRA_KEY` and `CESTRA_URL` from the environment.

| Option | Default | |
| --- | --- | --- |
| `key` | `process.env.CESTRA_KEY` | Your `cs_live_` key |
| `baseUrl` | `process.env.CESTRA_URL` or `https://api.cestra.ai` | |
| `maxRetries` | `4` | Retries for 429, 5xx and network errors |
| `timeoutMs` | `30000` | Per request. Long-polls add their wait time |
| `transport` | `'auto'` | `'websocket'`, `'poll'` or `'auto'` (WebSocket, then long-polling) |

## Messages and notices

```js
await agent.send('Build **passed** in 3m 12s');           // Markdown
await agent.notice('Backup done', 'success');            // info | success | warning | error
await agent.send('Quiet update', { notify: false });     // no push notification
```

## Asks

```js
const ask = await agent.ask('Commit to main?', ['Approve', { label: 'Discard', style: 'destructive' }], {
  expiresIn: 3600,     // seconds, 60 s … 30 days
  allowText: false,    // true adds a text box
  detail: '3 files changed',
});

const reply = await ask.wait({ timeoutSec: 600 });
// { status: 'answered' | 'pending' | 'expired' | 'cancelled', answer, optionId, text, answeredBy, answeredAt }
if (reply.optionId === 'o1') { /* approved */ }
```

Options get ids `o1`, `o2`, … in order. `wait()` never throws on timeout. It returns the ask with `status: 'pending'`. Call `ask.refresh()` or `ask.cancel()` later. To pick an ask back up after a restart, use `agent.getAsk(id)`.

## Jobs

```js
const job = await agent.job('Reviewing spec', ['Read', 'Compare', 'Write up']);
await job.progress(40);
await job.step(0, 'done');
await job.step(1, 'running');
await job.done();                 // or: await job.fail('Spec file missing')
```

`job.update({ title, pct, status, steps, step, error })` is the raw form. When someone presses **Stop**, you get a `job.stop_requested` event, and later updates fail with a 409 because the job is now `cancelled`.

## Artifacts

```js
await agent.artifact({
  type: 'table',
  title: 'Coverage matrix',
  key: 'matrix',                       // same key again = version 2, 3, …
  data: { columns: ['Requirement', 'Status'], rows: [['Login', { value: 'ok', tone: 'ok' }]] },
});
```

Schemas for `table`, `chart`, `markdown`, `form` and `html` are on [Artifacts](/artifacts).

## Files

```js
await agent.file('report.pdf', { caption: 'Quote' });                // path, Buffer, Uint8Array or Blob, ≤ 50 MB
const bytes = await agent.download(fileId);                          // Buffer
const { name, mime, data } = await agent.downloadFile(fileId);
```

## Receiving events

```js
agent.on('message', async (e) => {
  if (e.data.file) await agent.download(e.data.file.id);
  else await agent.send(`You said: ${e.data.text}`);
});
agent.on('ask.answered', (e) => console.log(e.data.ask_id, e.data.answer));
agent.on('job.stop_requested', (e) => stopWork(e.data.job_id));
agent.on('*', (e) => console.log(e.seq, e.type));

await agent.run();      // resolves after agent.stop()
```

`run()` acknowledges each event after its handlers resolve, so delivery is at-least-once. It reconnects with backoff. It rejects with a `CestraError` (status 401) if the key is invalid or revoked. `on()` returns an unsubscribe function. There are also lifecycle events: `connected`, `disconnected` and `error`.

Event types and payloads are listed under [REST API → Events](/rest-api#event-types).

## Errors

Everything throws `CestraError` with `status`, `code` and `message`. Creates carry an automatic `clientId`, so retries never duplicate anything. Pass your own `clientId` to stay idempotent across restarts.

## Other calls

`agent.me()`, `agent.setStatus('working' | 'online')`, `agent.disconnect()`, `agent.events({ after, wait })`, `agent.ack(seq)`, `agent.jobById(id)`.
