@rool-dev/sdk 2.0.0-dev.36748aa → 2.0.0-dev.6381fab

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
- # Rool SDK
1
+ <p align="center" class="sdk-brand"><a href="https://rool.dev"><img class="sdk-brand-logo" src="./assets/rool-logo.svg" alt="Rool logo" width="44" height="44"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/rool-wordmark-dark.svg"><img class="sdk-brand-wordmark" src="./assets/rool-wordmark.svg" alt="Rool" width="128"></picture></a></p>
2
2
 
3
- TypeScript SDK for Rool Machines.
3
+ <h1 align="center" class="sdk-title">TypeScript SDK</h1>
4
+
5
+ The official TypeScript SDK for building apps and automations with [Rool Machines](https://rool.dev).
6
+
7
+ A Rool Machine is a persistent cloud VM with snapshots, files, and embedded AI agents. The SDK provides one typed interface for authentication, machine management, file access, agent conversations, and live updates.
4
8
 
5
9
  ## Installation
6
10
 
@@ -8,652 +12,108 @@ TypeScript SDK for Rool Machines.
8
12
  npm install @rool-dev/sdk
9
13
  ```
10
14
 
11
- ## User and session API
12
-
13
- ```typescript
14
- import { RoolClient } from "@rool-dev/sdk";
15
-
16
- const client = new RoolClient({
17
- getTokens: () => ({
18
- accessToken: currentAccessToken,
19
- roolToken: currentRoolToken,
20
- }),
21
- });
22
-
23
- const session = await client.getSession();
24
- const account = await client.getAccount();
25
- const profile = await client.getProfile();
26
- const userAppData = await client.getUserAppData();
27
- const greeting = await client.getGreeting("en");
28
-
29
- await client.replaceProfile({
30
- name: "Ada",
31
- marketingOptIn: true,
32
- });
33
- await client.setUserAppData("theme", "dark");
34
- await client.deleteUserAppData("theme");
35
- await client.deleteAccount();
36
- ```
37
-
38
- `UserAppData` is an opaque cross-device JSON object. App data is changed one key at a time so unrelated settings cannot overwrite each other.
39
-
40
- ## Account events
41
-
42
- ```typescript
43
- const unsubscribe = client.events.subscribe(async (event) => {
44
- if (event.type === "session") {
45
- renderSession(event.session);
46
- } else if (event.type === "account_changed") {
47
- renderAccount(await client.getAccount());
48
- } else if (event.type === "profile_changed") {
49
- renderProfile(await client.getProfile());
50
- } else if (event.type === "user_app_data_changed") {
51
- renderUserAppData(await client.getUserAppData());
52
- } else if (event.type === "machines_changed") {
53
- renderMachines(await client.listMachines());
54
- } else if (event.type === "machine_members_changed") {
55
- renderMembers(
56
- event.machineId,
57
- await client.machine(event.machineId).members.list(),
58
- );
59
- }
60
- });
61
-
62
- unsubscribe();
63
- ```
64
-
65
- Account events tell the client when something changed. Fetch the relevant route to get the latest data. The SDK starts with `/v2/session`, then long-polls for changes using its account sync token. Empty polls and network retries do not produce events or refresh the session. If a token expires, the SDK fetches a new session and sends another `session` event.
15
+ ## Connect from Node.js
66
16
 
67
- `getTokens` may return tokens synchronously or asynchronously. The SDK sends the access token as `Authorization: Bearer …` and the Rool token as `X-Rool-Token`. Bearer-only integrations can use `getAccessToken` instead. Use `apiUrl` to target a non-production server and `fetch` to provide a custom transport.
68
-
69
- ## Authentication
70
-
71
- Auth clients own login, credential storage, and refresh. `RoolClient` only asks one for tokens and tells it when authentication is invalidated.
72
-
73
- ### Browser authentication
17
+ `NodeAuth` opens the user's browser for login, stores their credentials locally, and refreshes them when needed.
74
18
 
75
19
  ```typescript
76
- import { BrowserAuth, RoolClient } from "@rool-dev/sdk";
77
-
78
- const auth = new BrowserAuth();
79
- const client = new RoolClient({
80
- getTokens: auth.getTokens,
81
- onAuthInvalidated: auth.logout,
82
- });
20
+ import { RoolClient } from "@rool-dev/sdk";
21
+ import { NodeAuth } from "@rool-dev/sdk/node";
83
22
 
84
- auth.onAuthStateChanged(renderSignedInState);
23
+ const auth = new NodeAuth();
85
24
  if (!(await auth.initialize())) {
86
- await auth.login("My App");
25
+ await auth.login("My app");
87
26
  }
88
- ```
89
27
 
90
- `initialize()` processes an auth callback in the URL. Tokens are stored in endpoint-scoped browser storage and refreshed when requested.
91
-
92
- ### Native authentication
93
-
94
- `NativeAuth` uses system-browser PKCE for Google and Apple while also supporting password and magic-link sign-in.
95
-
96
- ```typescript
97
- import { App } from "@capacitor/app";
98
- import { Browser } from "@capacitor/browser";
99
- import { NativeAuth, RoolClient } from "@rool-dev/sdk";
100
-
101
- const auth = new NativeAuth({
102
- redirectUri: "roolandroidauth://auth/callback",
103
- defaultProvider: "google",
104
- openExternal: (url) => Browser.open({ url }),
105
- });
106
28
  const client = new RoolClient({
107
29
  getTokens: auth.getTokens,
108
30
  onAuthInvalidated: auth.logout,
109
31
  });
110
-
111
- App.addListener("appUrlOpen", async ({ url }) => {
112
- await Browser.close();
113
- await auth.handleRedirect(url);
114
- });
115
-
116
- if (!(await auth.initialize())) await auth.login("My App");
117
32
  ```
118
33
 
119
- The redirect URI must exactly match the app setup and auth server allowlist. Pass `{ provider: "apple" }` to `login()` or `signup()` to override the default provider.
34
+ The package also includes:
120
35
 
121
- ```typescript
122
- const result = await auth.signInWithPassword(email, password);
123
- if (result.status === "verify_required") showCheckYourEmailMessage();
124
-
125
- await auth.requestMagicLink(email);
126
- await auth.verify(verifyToken);
127
- ```
128
-
129
- On native, an HTTPS magic link only returns to the app when Universal Links or App Links are configured for that domain.
36
+ - `BrowserAuth` for web apps using browser redirects and browser storage.
37
+ - `NativeAuth` for mobile apps that open sign-in in the system browser and return through a deep link. It also supports passwords and magic links.
130
38
 
131
- ### Password and account methods
39
+ ## Put a machine to work
132
40
 
133
- Browser and native auth clients also provide:
134
-
135
- - `setPassword(password)`
136
- - `requestEmailChange(newEmail)`
137
- - `verify(token)`
138
- - `logout()`
139
- - `isAuthenticated()`
140
-
141
- ### Node.js authentication
41
+ This example creates a machine, gives an agent a job, uploads a receipt, and asks the agent to file it. The file API writes to the machine's WebDAV storage, so the agent and the app see the same files.
142
42
 
143
43
  ```typescript
144
- import { RoolClient } from "@rool-dev/sdk";
145
- import { NodeAuth } from "@rool-dev/sdk/node";
44
+ import { readFile } from "node:fs/promises";
146
45
 
147
- const auth = new NodeAuth();
148
- if (!(await auth.initialize())) await auth.login("My CLI");
46
+ const created = await client.createMachine({ name: "Receipts" });
47
+ const machine = client.machine(created.id);
149
48
 
150
- const client = new RoolClient({
151
- getTokens: auth.getTokens,
152
- onAuthInvalidated: auth.logout,
49
+ const agent = await machine.agents.create("receipt-filer", {
50
+ system:
51
+ "File receipt attachments under /rool-drive/receipts. " +
52
+ "Choose useful subfolders based on the receipt's contents.",
153
53
  });
154
- ```
155
-
156
- `NodeAuth` opens the system browser for login and stores endpoint-scoped credentials under `~/.config/rool/`. It refreshes the access and Rool tokens when requested. Pass `apiUrl` to select another deployed environment; the corresponding auth URL is derived from it. When the API uses a loopback URL, pass `authUrl` explicitly.
157
-
158
- Use `profile` or `credentialsPath` when an application needs multiple independent accounts:
159
-
160
- ```typescript
161
- const auth = new NodeAuth({
162
- apiUrl: "https://api.example.com",
163
- profile: "automation",
54
+ const conversation = await agent.createConversation({
55
+ name: "Receipt inbox",
56
+ visibility: "private",
164
57
  });
165
- ```
166
-
167
- API errors are thrown as `RoolProblem` with the server's stable `code`, HTTP `status`, `title`, and `detail`.
168
-
169
- ## Machine API
170
-
171
- ```typescript
172
- const created = await client.createMachine({ name: "Research" });
173
- const machine = client.machine(created.id);
174
-
175
- const details = await machine.get();
176
- await machine.settings.replace({ name: "Field research" });
177
- const copy = await machine.duplicate({ name: "Research copy" });
178
- const response = await machine.fetchUrl("https://example.com/data.json");
179
- await client.machine(copy.id).delete();
180
- ```
181
-
182
- `client.machine(id)` returns a stable machine handle that owns machine-scoped APIs and synchronization state. Creation, listing, sessions, duplication, and `machine.get()` all return the same point-in-time `MachineSummary`; bind its ID to a handle before performing machine operations. Summaries include the machine's inbound email address, lifecycle `state`, and an opaque `meta` JSON object. `fetchUrl()` returns the upstream `Response`, including non-success statuses.
183
-
184
- ## Machine checkpoints
185
-
186
- ```typescript
187
- const history = await machine.checkpoints.list();
188
- const checkpoint = history.checkpoints.at(-1);
189
- if (checkpoint) {
190
- await machine.checkpoints.restore(checkpoint.id);
191
- }
192
- ```
193
-
194
- The checkpoint collection contains the currently restorable timeline and the `baseCheckpointId` underlying the live filesystem. The live filesystem can contain newer uncheckpointed changes. Restoring a checkpoint preserves those changes as a new checkpoint and causes a watched machine file tree to reconcile completely. Moving backward does not discard later checkpoints, but modifying the filesystem from that earlier position replaces the later timeline.
195
-
196
- ## Machine files
197
58
 
198
- ```typescript
199
- const files = machine.files;
200
- const path = "/rool-drive/documents/report.pdf";
201
-
202
- const storage = await files.getStorageUsage();
203
- console.log(storage.usedBytes, storage.availableBytes);
204
- await files.createDirectory("/rool-drive/documents");
205
- const written = await files.write(path, reportBlob, {
59
+ const receiptPath = "/rool-drive/receipts/unsorted/receipt.pdf";
60
+ await machine.files.write(receiptPath, await readFile("./receipt.pdf"), {
206
61
  contentType: "application/pdf",
207
- ifNoneMatch: "*",
208
- onUploadProgress: ({ transferredBytes, totalBytes }) => {
209
- if (totalBytes) renderUploadProgress(transferredBytes / totalBytes);
210
- },
211
- });
212
- await files.write(
213
- "/rool-drive/documents/archive.bin",
214
- () => createArchiveStream(),
215
- { contentType: "application/octet-stream" },
216
- );
217
- const info = await files.stat(path);
218
- const documents = await files.list("/rool-drive/documents");
219
- const response = await files.read(path, {
220
- range: { start: 0, end: 1023 },
221
- ifMatch: info.etag,
62
+ createParents: true,
222
63
  });
223
- const hydrated = await files.readMultiple([
224
- "/space/.meta.json",
225
- "/rool-drive/documents/notes.json",
226
- ]);
227
- await files.copy(path, "/rool-drive/documents/report-backup.pdf", {
228
- overwrite: false,
229
- ifMatch: written.etag,
230
- });
231
- await files.move(path, "/rool-drive/documents/final-report.pdf");
232
- const deleted = await files.deleteMultiple([
233
- "/rool-drive/documents/final-report.pdf",
234
- { path: "/rool-drive/documents/notes.json", ifMatch: '"notes-etag"' },
235
- ]);
236
- for (const result of deleted) {
237
- if (!result.ok)
238
- console.error(`Failed to delete ${result.path}`, result.error);
239
- }
240
- ```
241
-
242
- Paths are absolute machine paths under `/space` or `/rool-drive`. `list()` without a path enumerates those storage roots; pass `{ recursive: true }` to enumerate a complete subtree. File and directory metadata has a discriminating `kind` field. Reads return the native `Response` so callers can stream the body. `readMultiple()` hydrates ordered small files in one request and returns an `ok` result with binary-safe bytes and validators, or a per-file HTTP failure. A batch accepts at most 128 paths, 2 MiB per successful file, and 16 MiB across successful files. Writes accept any `BodyInit`, including `Blob` and `ReadableStream`, and return the same complete file metadata as `stat()` and `list()`. Pass a function that creates a fresh `ReadableStream` when an upload must be replayable after a machine route change; a directly passed stream remains one-shot. `onUploadProgress` reports transferred bytes and includes the total size when it is known; successful completion is confirmed by the `write()` promise. Copy and move operations overwrite by default; pass `{ overwrite: false }` for create-only behavior.
243
-
244
- `deleteMultiple()` sends independent DAV requests with at most eight in flight. It accepts plain paths and targets carrying their own HTTP preconditions, and returns one ordered success or failure result per target. The requests are not atomic. A directory target recursively deletes its contents, so callers should omit redundant descendants; duplicate and overlapping targets otherwise remain independent and can race.
245
-
246
- Every file and directory has protected `access` metadata. `currentUser` says whether the requesting user can read or write it. `readableBy` and `writableBy` describe its filesystem audiences as `resource-owner`, `machine-admins`, `machine-editors`, and `machine-members`. For a file, write means changing its contents. For a directory, write means adding, removing, or renaming entries. Members without read access to a directory do not receive that directory through listing, direct lookup, or synchronization.
247
-
248
- `getStorageUsage()` reports the used and writable bytes on the machine's complete live persistent filesystem, including public files and private runtime or system state. It excludes checkpoint history and the ephemeral operating-system overlay.
249
-
250
- Watch the files when an application needs a live machine file tree:
251
64
 
252
- ```typescript
253
- await files.watch();
254
-
255
- const unsubscribe = files.tree.subscribe(({ reset, changed, deleted }) => {
256
- renderFileChanges({ reset, changed, deleted });
65
+ await conversation.prompt("File this receipt.", {
66
+ attachments: [receiptPath],
257
67
  });
258
-
259
- const cached = files.tree.get("/rool-drive/documents/final-report.pdf");
260
- const etag = files.tree.etag("/rool-drive/documents/final-report.pdf");
261
- const allDocuments = files.tree.list("/rool-drive/documents", {
262
- recursive: true,
68
+ await conversation.follow({
69
+ onEvent(event) {
70
+ if (event.type === "output.delta" && event.content.type === "text") {
71
+ process.stdout.write(event.content.text);
72
+ }
73
+ },
263
74
  });
264
-
265
- unsubscribe();
266
- files.unwatch();
267
75
  ```
268
76
 
269
- `files.watch()` performs a complete `sync-collection`, then keeps the machine's file metadata and ETag cache current with long-poll incremental reports. DAV writes and guest-program changes enter the same tree. An invalid or expired sync token causes an atomic complete reconciliation and a change event with `reset: true`. Transient sync errors are retried and available as `files.watchError`; `files.unwatch()` aborts the active long poll.
77
+ Prompting is asynchronous: `prompt()` returns when Rool accepts the work, while the agent keeps running on the machine. An app can follow the current run, leave, and reconnect later. UI clients can instead watch a conversation and receive a current view as its saved turns and live output change.
270
78
 
271
- ## Rool Object Collections
79
+ ## Give agents data your app understands
272
80
 
273
- Objects are JSON stored under `/space`. Collections are directories with a `.schema.json` definition and objects are schema-checked JSON files.
81
+ A machine can hold records as well as documents. A collection defines the fields in each record, and Rool rejects writes that do not match. For example, an app can keep an index alongside the receipt files:
274
82
 
275
83
  ```typescript
276
- await machine.files.watch();
277
-
278
- const task = await machine.collections.create("task", {
84
+ await machine.collections.create("receipts", {
279
85
  fields: [
280
- { name: "title", type: { kind: "string" } },
281
- { name: "done", type: { kind: "boolean" } },
86
+ { name: "vendor", type: { kind: "string" } },
87
+ { name: "amount", type: { kind: "number" } },
88
+ { name: "currency", type: { kind: "string" } },
89
+ { name: "document", type: { kind: "string" } },
282
90
  ],
283
91
  });
284
92
 
285
- const first = await machine.objects.create("/space/task/first.json", {
286
- title: "First task",
287
- done: false,
93
+ await machine.objects.create("/space/receipts/cafe.json", {
94
+ vendor: "Cafe",
95
+ amount: 12,
96
+ currency: "EUR",
97
+ document: receiptPath,
288
98
  });
289
- const objectPaths = machine.objects.list({ collection: "task" });
290
- const object = await machine.objects.get(first.path);
291
- const [sameObject, missing] = await machine.objects.getMultiple([
292
- first.path,
293
- "/space/task/missing.json",
294
- ]);
295
-
296
- await machine.objects.patch(first.path, { done: true });
297
- await machine.objects.move(first.path, "/space/task/renamed.json");
298
- const [removal] = await machine.objects.removeMultiple([
299
- "/space/task/renamed.json",
300
- ]);
301
- if (!removal) throw new Error("Object removal returned no result");
302
- if (!removal.ok) throw removal.error;
303
- await machine.collections.remove(task.name);
304
99
  ```
305
100
 
306
- `machine.objects.list()` returns object paths from the synchronized file tree without reading their bodies. `get()` reads one object and `getMultiple()` preserves the input positions and returns `undefined` for missing objects. Every call reads the current bodies from DAV, with multiple reads using bounded `read-multiple` batches. Collection schemas are read in the same way and schema replacement follows the guest's lazy-migration rule: existing objects are checked again only when edited.
101
+ The record is also a normal JSON file at `/space/receipts/cafe.json`. The app can work with it through `machine.objects`, while agents and programs inside the machine can use normal file tools. Changes from either side enter the same watched file tree.
307
102
 
308
- Creates are create-only. Metadata, schema, and object replacements plus object patches, moves, and removals use ETags and report status `412` when state changed concurrently. `removeMultiple()` uses a separate conditional DAV request for each object and returns ordered per-object results. Removing a collection recursively deletes its contents. Object moves do not overwrite by default; pass `{ overwrite: true }` explicitly. Patch values of `null` or `undefined` remove fields.
103
+ Collections can power task lists, catalogues, contact records, or a memory view with links between people, projects, and notes. The app gets predictable fields for its UI without hiding the data from the agent.
309
104
 
310
- The semantic APIs use the machine's shared file tree and sync loop. They do not create separate synchronization state. Body reads and mutations also work without `machine.files.watch()` by reading current DAV state directly; `objects.list()` reflects the shared file tree, so watch the machine's files before enumerating paths.
105
+ ## What the SDK handles
311
106
 
312
- ## Agents
107
+ - **Machines:** create, configure, duplicate, checkpoint, share, and delete persistent VMs.
108
+ - **Files:** upload, stream, move, and watch files under `/space` and `/rool-drive`.
109
+ - **Agents:** define agents, keep conversation history, attach machine files, stream runs, and request structured output.
110
+ - **Live state:** learn when account data changes, keep a file tree current, and stream conversation updates.
111
+ - **Shared app data:** define records that apps, agents, and programs inside the machine can all read and edit.
313
112
 
314
- Agents and conversations use stable JSON routes; their private machine files are not part of the SDK. Prompting does not require `machine.files.watch()`.
315
-
316
- ```typescript
317
- const defaultAgent = await machine.agents.get("rool");
318
- if (!defaultAgent) throw new Error("Rool agent is unavailable");
319
-
320
- const conversation = defaultAgent.conversation("research-chat");
321
- const stopWatching = conversation.watch((view) => {
322
- renderConversation({
323
- turns: view.turns,
324
- output: view.output,
325
- isRunning: view.isRunning,
326
- loading: view.loading,
327
- error: view.error,
328
- });
329
- });
330
-
331
- await conversation.prompt("Explain the result.", { effort: "reasoning" });
332
-
333
- // When this conversation leaves the UI:
334
- stopWatching();
335
- ```
336
-
337
- `prompt()` starts the conversation's current run and resolves once the server accepts it. The agent runs as a detached job. A conversation can only have one current run; call `cancel()` and wait for `follow()` to finish before prompting again. The `readOnly` option is accepted for compatibility with legacy prompting but currently has no effect.
338
-
339
- `watch()` is the normal UI API. It fetches only turns after the last turn it has seen, follows the current run, and refreshes the durable turns when that stream ends. `turns` contains saved history through the current user message while a run is active; `output` contains that run's replayed and live output. The first listener starts the work and removing the last listener stops it. Saved turns remain cached on the conversation handle for the next listener.
340
-
341
- `follow()` is the lower-level streaming API. It performs one `GET` of the conversation's current run. It receives the complete current-run output and then continues with new events until that response ends. Tool calls and their results arrive as `output.delta` events with matching IDs. A tool result contains nested content parts and an optional `error` flag. `follow()` returns `false` when there is no current run. Aborting `follow()` only stops that request; call `cancel()` to stop the detached job.
342
-
343
- The SDK uses `conversation_changed` account events to wake active watchers. A watcher also refreshes after prompting, cancellation, stream completion, connection failure, and account event-token replacement.
344
-
345
- Prompt attachments are existing `/space` or `/rool-drive` paths. Pass a durable user turn's `id` as `replaceTurnId` to replace that message and everything after it. This supports edits and rerolls, including the first message. A replacement gets a new user turn ID; use that ID to edit it again.
346
-
347
- Pass a JSON Schema as `responseSchema` to request structured output. Tools are skipped for that run. The successful assistant turn contains one JSON content part; the value is JSON directly, not a JSON string.
348
-
349
- ```typescript
350
- await conversation.prompt("Return the number of records.", {
351
- responseSchema: {
352
- type: "object",
353
- properties: { count: { type: "integer" } },
354
- required: ["count"],
355
- additionalProperties: false,
356
- },
357
- });
358
- await conversation.follow();
359
-
360
- const turns = await conversation.listTurns();
361
- const part = turns.at(-1)?.content[0];
362
- if (part?.type !== "json") throw new Error("No structured result");
363
- console.log(part.value); // { count: ... }
364
- ```
365
-
366
- Custom agent definitions currently contain one plain system prompt. The server owns the executable agent implementation.
367
-
368
- ```typescript
369
- const researcher = await machine.agents.create("researcher", {
370
- system: "Investigate carefully and distinguish facts from uncertainty.",
371
- });
372
- const customConversation = await researcher.createConversation({
373
- name: "Climate report",
374
- visibility: "private",
375
- });
376
- await customConversation.prompt("Investigate this claim.");
377
- ```
378
-
379
- Agents expose `replace()` and `delete()`. Conversations expose metadata replacement, listing, durable turn reads, rename, and deletion. Listed and fetched conversation metadata includes server-managed ISO 8601 `createdAt` and `updatedAt` timestamps plus `isRunning`, which can drive a running indicator without opening every run stream. Visibility defaults to private. The built-in `rool` agent cannot be replaced or deleted.
380
-
381
- ## Members and invites
382
-
383
- ```typescript
384
- const invite = await machine.invites.create({
385
- role: "editor",
386
- maxUses: 1,
387
- });
388
- const token = invite.url.split("/").at(-1)!;
389
-
390
- await client.getInvitePreview(token);
391
- await client.redeemInvite(token);
392
-
393
- const members = await machine.members.list();
394
- console.log(members[1].name ?? members[1].email);
395
- await machine.members.replaceRole(members[1].userId, { role: "viewer" });
396
- await machine.members.remove(members[1].userId);
397
- await machine.invites.revoke(invite.id);
398
- ```
113
+ Long-running work belongs to the machine rather than the client connection. File changes made by the app, a user, or an agent enter the same synchronized file tree, while account events tell the app when to fetch fresh account or machine data.
399
114
 
400
- Invite URLs contain a secret token and are returned only when an invite is created. Invites can optionally be bound to an email address. Role replacement never creates membership or transfers ownership.
401
-
402
- ## Gifts
403
-
404
- A gift carries something of value in a short code. Users receive gifts from Rool and give the codes away themselves. Claiming a gift is single-use, and claiming your own gift is allowed.
405
-
406
- ```typescript
407
- const { gifts } = await client.listGifts();
408
- for (const gift of gifts) {
409
- console.log(gift.code, gift.url, gift.description, gift.claimedAt);
410
- }
411
-
412
- const preview = await client.previewGift(code); // no auth required
413
- console.log(
414
- `${preview.holderName ?? "Someone"} gave you ${preview.description}`,
415
- );
416
-
417
- const { gift } = await client.claimGift(code);
418
- if (gift.kind === "credits") console.log(`+${gift.credits} credits`);
419
- ```
420
-
421
- Codes are case-insensitive and the dash is optional. Prefer the server-rendered `description` for display. Narrow `gift.kind` when using the structured payload because new gift kinds may be added.
422
-
423
- A holder can add a note, archive a gift, or replace an unclaimed gift's code. These actions do not change what the gift grants. Archiving only hides it from the holder's normal view.
424
-
425
- ```typescript
426
- await client.updateGift(giftId, { note: "sent to Peter" });
427
- await client.updateGift(giftId, { archived: true });
428
- await client.updateGift(giftId, { note: null });
429
- const updated = await client.rotateGiftCode(giftId);
430
- ```
431
-
432
- Gift failures are `RoolProblem` errors. `gift_invalid` means the code or gift is unavailable to the caller. `gift_claimed` means it was already claimed.
433
-
434
- ## API problems
435
-
436
- Each problem `type` links to its entry below.
437
-
438
- <a id="problem-authentication_required"></a>
439
-
440
- ### `authentication_required`
441
-
442
- **Documentation placeholder.**
443
-
444
- <a id="problem-invalid_authentication"></a>
445
-
446
- ### `invalid_authentication`
447
-
448
- **Documentation placeholder.**
449
-
450
- <a id="problem-email_unverified"></a>
451
-
452
- ### `email_unverified`
453
-
454
- **Documentation placeholder.**
455
-
456
- <a id="problem-account_suspended"></a>
457
-
458
- ### `account_suspended`
459
-
460
- **Documentation placeholder.**
461
-
462
- <a id="problem-invalid_profile"></a>
463
-
464
- ### `invalid_profile`
465
-
466
- **Documentation placeholder.**
467
-
468
- <a id="problem-invalid_user_app_data"></a>
469
-
470
- ### `invalid_user_app_data`
471
-
472
- **Documentation placeholder.**
473
-
474
- <a id="problem-invalid_json"></a>
475
-
476
- ### `invalid_json`
477
-
478
- **Documentation placeholder.**
479
-
480
- <a id="problem-payload_too_large"></a>
481
-
482
- ### `payload_too_large`
483
-
484
- **Documentation placeholder.**
485
-
486
- <a id="problem-user_app_data_too_large"></a>
487
-
488
- ### `user_app_data_too_large`
489
-
490
- **Documentation placeholder.**
491
-
492
- <a id="problem-not_found"></a>
493
-
494
- ### `not_found`
495
-
496
- **Documentation placeholder.**
497
-
498
- <a id="problem-checkpoint_not_found"></a>
499
-
500
- ### `checkpoint_not_found`
501
-
502
- **Documentation placeholder.**
503
-
504
- <a id="problem-sync_token_required"></a>
505
-
506
- ### `sync_token_required`
507
-
508
- The account event route requires the sync token returned by `/v2/session`.
509
-
510
- <a id="problem-invalid_sync_token"></a>
511
-
512
- ### `invalid_sync_token`
513
-
514
- The account event history no longer contains everything after this token. Fetch `/v2/session` and continue with its new token.
515
-
516
- <a id="problem-invalid_wait_preference"></a>
517
-
518
- ### `invalid_wait_preference`
519
-
520
- The account event wait must be an integer from 0 through 50 seconds.
521
-
522
- <a id="problem-internal_error"></a>
523
-
524
- ### `internal_error`
525
-
526
- **Documentation placeholder.**
527
-
528
- <a id="problem-server_misconfigured"></a>
529
-
530
- ### `server_misconfigured`
531
-
532
- **Documentation placeholder.**
533
-
534
- <a id="problem-current_run_exists"></a>
535
-
536
- ### `current_run_exists`
537
-
538
- The conversation is already running. Cancel or follow that run before prompting again. This also applies to prompts with `replaceTurnId`.
539
-
540
- <a id="problem-replace_turn_not_found"></a>
541
-
542
- ### `replace_turn_not_found`
543
-
544
- The user turn supplied as `replaceTurnId` is no longer in the conversation. Fetch the turns again before retrying the edit.
545
-
546
- <a id="problem-invalid_member_role"></a>
547
-
548
- ### `invalid_member_role`
549
-
550
- **Documentation placeholder.**
551
-
552
- <a id="problem-role_not_replaceable"></a>
553
-
554
- ### `role_not_replaceable`
555
-
556
- **Documentation placeholder.**
557
-
558
- <a id="problem-membership_not_removable"></a>
559
-
560
- ### `membership_not_removable`
561
-
562
- **Documentation placeholder.**
563
-
564
- <a id="problem-invalid_invite"></a>
565
-
566
- ### `invalid_invite`
567
-
568
- **Documentation placeholder.**
569
-
570
- <a id="problem-invite_invalid"></a>
571
-
572
- ### `invite_invalid`
573
-
574
- **Documentation placeholder.**
575
-
576
- <a id="problem-invite_expired"></a>
577
-
578
- ### `invite_expired`
579
-
580
- **Documentation placeholder.**
581
-
582
- <a id="problem-invite_revoked"></a>
583
-
584
- ### `invite_revoked`
585
-
586
- **Documentation placeholder.**
587
-
588
- <a id="problem-invite_exhausted"></a>
589
-
590
- ### `invite_exhausted`
591
-
592
- **Documentation placeholder.**
593
-
594
- <a id="problem-invite_email_mismatch"></a>
595
-
596
- ### `invite_email_mismatch`
597
-
598
- **Documentation placeholder.**
599
-
600
- <a id="problem-gift_invalid"></a>
601
-
602
- ### `gift_invalid`
603
-
604
- The code is not valid, or the requested gift does not belong to the current user.
605
-
606
- <a id="problem-gift_claimed"></a>
607
-
608
- ### `gift_claimed`
609
-
610
- The gift has already been claimed. A claimed gift cannot be claimed again or given a new code.
611
-
612
- <a id="problem-invalid_input"></a>
613
-
614
- ### `invalid_input`
615
-
616
- The gift update is empty or contains an invalid note or archived value.
617
-
618
- ## Development
619
-
620
- ```bash
621
- pnpm build
622
- pnpm typecheck
623
- ```
624
-
625
- The user-route smoke test must target a dedicated non-production account. It replaces profile and app data, then schedules and cancels account deletion. The member-route smoke test requires a second non-production account to exercise invite redemption and membership changes. The local gift fixture accounts are deleted after the gift smoke test.
626
-
627
- Copy `.env.example` to the ignored `.env` file and configure the local endpoints and expected primary account ID.
628
-
629
- | Variable | Purpose |
630
- | ------------------------------- | ---------------------------------------------------------------------- |
631
- | `ROOL_TEST_API_URL` | API origin. HTTPS is required except for loopback development servers. |
632
- | `ROOL_TEST_AUTH_URL` | Auth endpoint override required for a loopback API. |
633
- | `ROOL_TEST_ROUTER_URL` | Local machine-router origin. |
634
- | `ROOL_TEST_USER_ID` | Expected primary account ID, preventing mutation of the wrong account. |
635
- | `ROOL_TEST_GIFT_HOLDER_EMAIL` | Local fixture account that holds a gift. |
636
- | `ROOL_TEST_GIFT_CLAIMANT_EMAIL` | Local fixture account that claims the gift. |
637
- | `ROOL_TEST_INTERNAL_SECRET` | Local rool-server secret used to create and remove fixtures. |
638
-
639
- Log the `sdk-v2-primary` and `sdk-v2-member` Node profiles into two dedicated accounts:
640
-
641
- ```bash
642
- node --env-file=.env --import tsx test/integration/v2/login.ts
643
- ```
644
-
645
- Then run the complete v2 smoke-test suite:
646
-
647
- ```bash
648
- pnpm test:v2
649
- ```
650
-
651
- To run one smoke test manually, invoke its script directly, for example:
652
-
653
- ```bash
654
- node --env-file=.env --import tsx test/integration/v2/machine-routes.test.ts
655
- ```
115
+ The package exports its public TypeScript types, so editor autocomplete shows the detailed options and results. See [docs.rool.dev](https://docs.rool.dev/) for the published documentation.
656
116
 
657
117
  ## License
658
118
 
659
- MIT — see [LICENSE](../../LICENSE).
119
+ MIT — see [LICENSE](./LICENSE).