puras
v0.4.0
Published
Puras JavaScript/TypeScript SDK — call your deployed Puras skills from Node, React, and React Native / Expo.
Maintainers
Readme
puras
JavaScript/TypeScript SDK for Puras — call your deployed
Puras skills from Node, React, and React Native / Expo. Zero
dependencies, works everywhere fetch does.
The Python counterpart (pip install puras) additionally ships the in-skill
runtime and the deploy CLI; this package is the client half only: submit
jobs, wait for them, read results.
npm install purasQuickstart (Node / backend)
import { Puras } from "puras";
const puras = new Puras(); // reads PURAS_API_KEY from env
// Fully-qualified skill path, copyable from the skill's page:
const ad = await puras.run("acme/ugc-ads/ugc-ad", {
product: "https://example.com/product",
duration: 15,
});
console.log(ad.video); // signed media URLFor your own skills, set a default skillpack once and call skills by name:
const puras = new Puras({ apiKey: process.env.PURAS_API_KEY, skillpack: "ugc-ads" });
const ad = await puras.run("ugc-ad", { product: url });React / Expo (client apps)
Use a publishable key (puras_pub_…), minted in the dashboard under
API keys → Publishable. Publishable keys are designed to ship inside an app
bundle:
- they can only submit jobs and read the jobs they submitted — no drive, no skillpack management, no job-history enumeration;
- you can restrict them to a skill allowlist at creation;
- you can set a daily spend cap so an extracted key can't drain your balance.
Never embed a secret key (puras_live_…) in an app.
import { Puras } from "puras";
const puras = new Puras({ apiKey: "puras_pub_XXXXXXXX.YYYY..." });
async function generateVideo(photoUrl: string) {
const job = await puras.submit("acme/ugc-ads/ugc-ad", { product: photoUrl });
// Poll — media skills take minutes. Update your UI as you go:
const done = await puras.wait(job.id, { pollIntervalMs: 3000, timeoutMs: 15 * 60_000 });
if (done.status !== "succeeded") throw new Error(done.error ?? done.status);
return done.result;
}Three ways to consume a job
- One await —
await puras.run(skill, inputs)submits and polls to completion, returning the skill'sresult. (For short jobs,submit(…, { wait: true })holds the HTTP request open server-side instead.) - Async —
await puras.submit(skill, inputs)returns the job id immediately; check back later withget()/wait(). - Live events (SSE) —
puras.events(jobId)is an async iterator over the job's event stream (step,tool_call,log, …), for progress UIs:
const job = await puras.submit("acme/ugc-ads/ugc-ad", { product: url });
for await (const ev of puras.events(job.id)) {
console.log(ev.type, ev.payload);
}
const done = await puras.get(job.id); // terminal by nowTransport is picked automatically: true SSE where fetch can stream (Node,
browsers), polling fallback where it can't (React Native / Expo). Force one
with { transport: "sse" | "poll" }.
API
new Puras(options?)
| option | default | |
| --- | --- | --- |
| apiKey | PURAS_API_KEY env (Node) | puras_live_… or puras_pub_… |
| skillpack | — | default skillpack (slug or UUID) for bare skill names |
| apiBase | https://api.puras.co | |
| timeoutMs | 120000 | per-request timeout |
| fetch | global fetch | custom implementation (tests, polyfills) |
puras.run(skill, inputs?, options?) → Promise<result>
Submit + wait, returning the skill's result. Throws JobError (carrying
the full job) if the job failed, was cancelled, or is still running when
timeoutMs (default 10 min) elapses. Waiting is client-side polling
(pollIntervalMs, default 2 s) so it works on React Native and survives
multi-minute media jobs.
puras.submit(skill, inputs?, options?) → Promise<Job>
Fire-and-return — gives you the job {id, status, …} immediately. Options:
skillpack, version (pin a deployment), wait + timeoutSeconds
(server-side wait, for short jobs).
puras.get(jobId) → Promise<Job> / puras.wait(jobId, options?) → Promise<Job>
Fetch a job once, or poll it to a terminal status.
puras.feedback(jobId, options?) → Promise<JobFeedback>
Record end-user feedback on a job's result — a thumb and/or a comment — so you
can later see which skills run well and which don't. Options: rating
(1 good, -1 bad, 0 comment-only), comment (free-form text), endUserId
(opaque id of the end user; re-posting with the same (job, endUserId)
overwrites their rating/comment). At least one of a non-zero rating or a
comment is required.
const result = await puras.run("acme/ugc-ads/ugc-ad", { product: url });
// …user reacts in your UI…
await puras.feedback(job.id, { rating: 1, endUserId: user.id });puras.events(jobId, options?) → AsyncGenerator<JobEvent>
Stream the job's events live (SSE with automatic polling fallback); the
iterator ends when the job reaches a terminal status. Options: transport
("auto" | "sse" | "poll"), afterId (resume), pollIntervalMs, signal.
puras.chat(skill, options?) → Promise<Conversation>
Open a multi-turn chat against a mode: chat skill. The skill's SKILL.md
is the system prompt and its tools are the agent's tools; each message runs one
turn and resolves with the model's reply, with the thread remembered server-side.
const chat = await puras.chat("acme/support-desk/refund-concierge");
console.log(await chat.send("Hi, my order AMZ-1001 arrived defective"));
console.log(await chat.send("It's the earbuds, EARBUDS-X2")); // continues the threadoptions: skillpack, version (pin a deployment), model (pin a text-model
slug for every turn), title.
A Conversation has:
send(message, options?) → Promise<string>— run a turn, resolve with the reply.optionsis the same wait/poll budget aswait(). ThrowsJobErroron a failed/cancelled turn.sendJob(message) → Promise<Job>— run a turn but return the job immediately (stream it withpuras.events(job.id); read the reply frompuras.get(job.id).result.reply).stream(message, options?) → AsyncGenerator<JobEvent>— send + stream the turn's events live; read the reply after withhistory().history() → Promise<{role, text}[]>— the rendered transcript so far.delete() → Promise<void>.id/skill— resume later withnew Conversation(...)semantics via the id.
Lower-level: createConversation, getConversation, listConversations,
sendMessage, deleteConversation.
Works with either key kind: a publishable key (puras_pub_…) can open
conversations and send turns directly from a web/mobile app — scoped, like jobs,
to the conversations it created and bounded by its allowed-skills fence + daily
spend cap. A secret key (puras_live_…) is unrestricted in a backend.
(listConversations is the one exception — it stays secret/JWT-only so an app
key can't enumerate the workspace's chats.)
Errors
PurasAPIError— non-2xx API response (.status,.detail).JobError— terminal-but-not-succeeded job (.job).
Skill addressing
skill accepts the same forms as the Python SDK and the dashboard:
| form | example | resolves against |
| --- | --- | --- |
| workspace/skillpack/skill | acme/ugc-ads/ugc-ad | any public skillpack (or your own) |
| skillpack/skill | ugc-ads/ugc-ad | your workspace |
| skill | ugc-ad | the default skillpack you configured |
License
MIT
