Docs/Node
Examples

Node

The loop with built-in fetch. No dependencies.

View as Markdown

Requires Node.js 18 or newer for global fetch.

Polling

javascript
// sai.mjs
const API = process.env.SAI_API_URL ?? 'https://api.simular.ai'
const HEADERS = {
  Authorization: `Bearer ${process.env.SAI_API_KEY}`,
  'Content-Type': 'application/json',
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

async function api(method, path, body) {
  const res = await fetch(`${API}/v1/agents${path}`, {
    method,
    headers: HEADERS,
    body: body ? JSON.stringify(body) : undefined,
  })
  if (res.status === 429) {
    await sleep(60_000)
    return api(method, path, body)
  }
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
  return res.json()
}

async function run(task, machineId) {
  const { sessionId } = await api('POST', '/message', {
    message: task,
    wait: false,
    ...(machineId && { machineId }),
  })

  let cursor
  for (;;) {
    const qs = new URLSearchParams({ sessionId, wait: '30' })
    if (cursor) qs.set('since', cursor)
    const page = await api('GET', `/events?${qs}`)
    cursor = page.cursor

    for (const ev of page.events) {
      if (ev.type === 'data-progress') console.log('  ', ev.data.text)
    }

    switch (page.status) {
      case 'running':
        continue
      case 'needs_approval': {
        const a = page.approval
        if (a.isLinkOnly) {
          console.log('Open in a browser:', a.approvalUrl)
          await sleep(15_000)
        } else if (a.approvalType === 'choice') {
          const selections = a.questions.map((q) => [q.options[0].value])
          await api('POST', '/approve', { approvalId: a.approvalId, response: 'yes', selections })
        } else {
          console.log('Approving:', a.command ?? a.title)
          await api('POST', '/approve', { approvalId: a.approvalId, response: 'yes' })
        }
        continue
      }
      case 'idle':
        return page.text
      default: {
        const err = page.events.find((e) => e.type === 'error')
        throw new Error(err?.errorText ?? 'task failed')
      }
    }
  }
}

console.log(await run('Open Notepad, type hello, save it to the desktop as hello.txt'))
bash
SAI_API_KEY=sapi_... node sai.mjs

The choice handler picks the first option. Replace it with your own logic. To stop a task early, api("POST", "/abort", { sessionId }).

Uploading a file

javascript
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'

async function upload(path) {
  const res = await fetch(`${API}/v1/agents/upload`, {
    method: 'POST',
    headers: {
      Authorization: HEADERS.Authorization,
      'x-filename': encodeURIComponent(basename(path)),
    },
    body: await readFile(path),
  })
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
  return res.json()
}

const attachment = await upload('./report.pdf')
await api('POST', '/message', {
  message: 'Summarise the attached PDF',
  wait: false,
  attachments: [attachment],
})

Streaming

For a long-lived process, read the SSE body directly.

javascript
const res = await fetch(`${API}/v1/agents/message`, {
  method: 'POST',
  headers: { ...HEADERS, Accept: 'text/event-stream' },
  body: JSON.stringify({ message: 'What is on the screen right now?' }),
})

const decoder = new TextDecoder()
let buffer = ''
let text = ''
for await (const chunk of res.body) {
  buffer += decoder.decode(chunk, { stream: true })
  let idx
  while ((idx = buffer.indexOf('\n\n')) !== -1) {
    const frame = buffer.slice(0, idx)
    buffer = buffer.slice(idx + 2)
    const data = frame.replace(/^data: /, '')
    if (data === '[DONE]') break
    const ev = JSON.parse(data)
    if (ev.type === 'text-delta') text += ev.delta
    if (ev.type === 'data-approval-request') console.log('approval:', ev.data.approvalId)
    if (ev.type === 'error') throw new Error(ev.errorText)
  }
}
console.log(text)

Answer approvals on the streaming path with POST /approve from another request; the stream stays open while it waits.