fetch.
Polling
// sai.mjs
const API = process.env.SAI_API_URL ?? "https://simular-cloud-api-149671192594.us-central1.run.app";
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"));
SAI_API_KEY=sapi_... node sai.mjs
api("POST", "/abort", { sessionId }).
Uploading a file
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.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);
POST /approve from another request; the stream stays open while it waits.
