@rool-dev/sdk 2.0.0-dev.4adc2c2 → 2.0.0-dev.69ff8af

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,18 @@
1
- # Rool SDK
1
+ <p align="center" class="sdk-brand">
2
+ <a href="https://rool.dev">
3
+ <img class="sdk-brand-logo" src="./assets/rool-logo.svg" alt="Rool logo" width="44" height="44">
4
+ <picture>
5
+ <source media="(prefers-color-scheme: dark)" srcset="./assets/rool-wordmark-dark.svg">
6
+ <img class="sdk-brand-wordmark" src="./assets/rool-wordmark.svg" alt="Rool" width="128">
7
+ </picture>
8
+ </a>
9
+ </p>
2
10
 
3
- TypeScript SDK for Rool Machines.
11
+ <h1 align="center" class="sdk-title">TypeScript SDK</h1>
12
+
13
+ The official TypeScript SDK for building apps and automations with [Rool Machines](https://rool.dev).
14
+
15
+ 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
16
 
5
17
  ## Installation
6
18
 
@@ -8,144 +20,18 @@ TypeScript SDK for Rool Machines.
8
20
  npm install @rool-dev/sdk
9
21
  ```
10
22
 
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.
66
-
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
74
-
75
- ```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
- });
83
-
84
- auth.onAuthStateChanged(renderSignedInState);
85
- if (!(await auth.initialize())) {
86
- await auth.login("My App");
87
- }
88
- ```
89
-
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
- const client = new RoolClient({
107
- getTokens: auth.getTokens,
108
- onAuthInvalidated: auth.logout,
109
- });
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
- ```
118
-
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.
120
-
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.
130
-
131
- ### Password and account methods
23
+ ## Connect from Node.js
132
24
 
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
25
+ `NodeAuth` opens the user's browser for login, stores their credentials locally, and refreshes them when needed.
142
26
 
143
27
  ```typescript
144
28
  import { RoolClient } from "@rool-dev/sdk";
145
29
  import { NodeAuth } from "@rool-dev/sdk/node";
146
30
 
147
31
  const auth = new NodeAuth();
148
- if (!(await auth.initialize())) await auth.login("My CLI");
32
+ if (!(await auth.initialize())) {
33
+ await auth.login("My app");
34
+ }
149
35
 
150
36
  const client = new RoolClient({
151
37
  getTokens: auth.getTokens,
@@ -153,502 +39,88 @@ const client = new RoolClient({
153
39
  });
154
40
  ```
155
41
 
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.
42
+ The package also includes:
157
43
 
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",
164
- });
165
- ```
44
+ - `BrowserAuth` for web apps using browser redirects and browser storage.
45
+ - `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.
166
46
 
167
- API errors are thrown as `RoolProblem` with the server's stable `code`, HTTP `status`, `title`, and `detail`.
47
+ ## Put a machine to work
168
48
 
169
- ## Machine API
49
+ 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.
170
50
 
171
51
  ```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
-
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, {
206
- contentType: "application/pdf",
207
- ifNoneMatch: "*",
208
- onUploadProgress: ({ transferredBytes, totalBytes }) => {
209
- if (totalBytes) renderUploadProgress(transferredBytes / totalBytes);
210
- },
211
- });
212
- const info = await files.stat(path);
213
- const documents = await files.list("/rool-drive/documents");
214
- const response = await files.read(path, {
215
- range: { start: 0, end: 1023 },
216
- ifMatch: info.etag,
217
- });
218
- const hydrated = await files.readMultiple([
219
- "/space/.meta.json",
220
- "/rool-drive/documents/notes.json",
221
- ]);
222
- await files.copy(path, "/rool-drive/documents/report-backup.pdf", {
223
- overwrite: false,
224
- ifMatch: written.etag,
225
- });
226
- await files.move(path, "/rool-drive/documents/final-report.pdf");
227
- const deleted = await files.deleteMultiple([
228
- "/rool-drive/documents/final-report.pdf",
229
- { path: "/rool-drive/documents/notes.json", ifMatch: '"notes-etag"' },
230
- ]);
231
- for (const result of deleted) {
232
- if (!result.ok)
233
- console.error(`Failed to delete ${result.path}`, result.error);
234
- }
235
- ```
236
-
237
- 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()`. `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.
238
-
239
- `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.
240
-
241
- 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.
52
+ import { readFile } from "node:fs/promises";
242
53
 
243
- `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.
244
-
245
- Watch the files when an application needs a live machine file tree:
246
-
247
- ```typescript
248
- await files.watch();
54
+ const created = await client.createMachine({ name: "Receipts" });
55
+ const machine = client.machine(created.id);
249
56
 
250
- const unsubscribe = files.tree.subscribe(({ reset, changed, deleted }) => {
251
- renderFileChanges({ reset, changed, deleted });
57
+ const agent = await machine.agents.create("receipt-filer", {
58
+ system:
59
+ "File receipt attachments under /rool-drive/receipts. " +
60
+ "Choose useful subfolders based on the receipt's contents.",
252
61
  });
253
-
254
- const cached = files.tree.get("/rool-drive/documents/final-report.pdf");
255
- const etag = files.tree.etag("/rool-drive/documents/final-report.pdf");
256
- const allDocuments = files.tree.list("/rool-drive/documents", {
257
- recursive: true,
62
+ const conversation = await agent.createConversation({
63
+ name: "Receipt inbox",
64
+ visibility: "private",
258
65
  });
259
66
 
260
- unsubscribe();
261
- files.unwatch();
262
- ```
263
-
264
- `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.
265
-
266
- ## Rool Object Collections
267
-
268
- Objects are JSON stored under `/space`. Collections are directories with a `.schema.json` definition and objects are schema-checked JSON files.
269
-
270
- ```typescript
271
- await machine.files.watch();
272
-
273
- const task = await machine.collections.create("task", {
274
- fields: [
275
- { name: "title", type: { kind: "string" } },
276
- { name: "done", type: { kind: "boolean" } },
277
- ],
67
+ const receiptPath = "/rool-drive/receipts/unsorted/receipt.pdf";
68
+ await machine.files.write(receiptPath, await readFile("./receipt.pdf"), {
69
+ contentType: "application/pdf",
70
+ createParents: true,
278
71
  });
279
72
 
280
- const first = await machine.objects.create("/space/task/first.json", {
281
- title: "First task",
282
- done: false,
73
+ await conversation.prompt("File this receipt.", {
74
+ attachments: [receiptPath],
283
75
  });
284
- const objectPaths = machine.objects.list({ collection: "task" });
285
- const object = await machine.objects.get(first.path);
286
- const [sameObject, missing] = await machine.objects.getMultiple([
287
- first.path,
288
- "/space/task/missing.json",
289
- ]);
290
-
291
- await machine.objects.patch(first.path, { done: true });
292
- await machine.objects.move(first.path, "/space/task/renamed.json");
293
- const [removal] = await machine.objects.removeMultiple([
294
- "/space/task/renamed.json",
295
- ]);
296
- if (!removal) throw new Error("Object removal returned no result");
297
- if (!removal.ok) throw removal.error;
298
- await machine.collections.remove(task.name);
299
- ```
300
-
301
- `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.
302
-
303
- 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.
304
-
305
- 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.
306
-
307
- ## Agents
308
-
309
- Agents and conversations use stable JSON routes; their private machine files are not part of the SDK. Prompting does not require `machine.files.watch()`.
310
-
311
- ```typescript
312
- const defaultAgent = await machine.agents.get("rool");
313
- if (!defaultAgent) throw new Error("Rool agent is unavailable");
314
-
315
- const conversation = defaultAgent.conversation("research-chat");
316
- await conversation.prompt("Explain the result.", { effort: "reasoning" });
317
-
318
- renderSettled(await conversation.listTurns());
319
- clearUnsettled();
320
76
  await conversation.follow({
321
- onEvent: (event) => {
322
- if (event.type !== "output.delta") return;
323
- if (event.content.type === "text") {
324
- renderUnsettledText(event.content.text);
325
- } else if (event.content.type === "tool_call") {
326
- showRunningTool(event.content.id, event.content.name);
327
- } else if (event.content.type === "tool_result") {
328
- showToolResult(event.content.id, event.content.content);
77
+ onEvent(event) {
78
+ if (event.type === "output.delta" && event.content.type === "text") {
79
+ process.stdout.write(event.content.text);
329
80
  }
330
81
  },
331
82
  });
332
- renderSettled(await conversation.listTurns());
333
83
  ```
334
84
 
335
- `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.
336
-
337
- `follow()` performs one `GET` of the conversation's current run. It receives the complete unsettled part of the conversation 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. A client can always render the conversation from its durable turns plus the events from its latest `follow()` call.
338
-
339
- A `conversation_changed` account event tells clients to fetch the durable turns and follow the current run again. Aborting `follow()` only stops watching. Call `cancel()` to stop the detached job.
85
+ 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.
340
86
 
341
- 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.
87
+ ## Give agents data your app understands
342
88
 
343
- 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.
89
+ 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:
344
90
 
345
91
  ```typescript
346
- await conversation.prompt("Return the number of records.", {
347
- responseSchema: {
348
- type: "object",
349
- properties: { count: { type: "integer" } },
350
- required: ["count"],
351
- additionalProperties: false,
352
- },
92
+ await machine.collections.create("receipts", {
93
+ fields: [
94
+ { name: "vendor", type: { kind: "string" } },
95
+ { name: "amount", type: { kind: "number" } },
96
+ { name: "currency", type: { kind: "string" } },
97
+ { name: "document", type: { kind: "string" } },
98
+ ],
353
99
  });
354
- await conversation.follow();
355
100
 
356
- const turns = await conversation.listTurns();
357
- const part = turns.at(-1)?.body.content[0];
358
- if (part?.type !== "json") throw new Error("No structured result");
359
- console.log(part.value); // { count: ... }
360
- ```
361
-
362
- Custom agent definitions currently contain one plain system prompt. The server owns the executable agent implementation.
363
-
364
- ```typescript
365
- const researcher = await machine.agents.create("researcher", {
366
- system: "Investigate carefully and distinguish facts from uncertainty.",
101
+ await machine.objects.create("/space/receipts/cafe.json", {
102
+ vendor: "Cafe",
103
+ amount: 12,
104
+ currency: "EUR",
105
+ document: receiptPath,
367
106
  });
368
- const customConversation = await researcher.createConversation({
369
- name: "Climate report",
370
- visibility: "private",
371
- });
372
- await customConversation.prompt("Investigate this claim.");
373
107
  ```
374
108
 
375
- 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.
109
+ 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.
376
110
 
377
- ## Members and invites
111
+ 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.
378
112
 
379
- ```typescript
380
- const invite = await machine.invites.create({
381
- role: "editor",
382
- maxUses: 1,
383
- });
384
- const token = invite.url.split("/").at(-1)!;
113
+ ## What the SDK handles
385
114
 
386
- await client.getInvitePreview(token);
387
- await client.redeemInvite(token);
115
+ - **Machines:** create, configure, duplicate, checkpoint, share, and delete persistent VMs.
116
+ - **Files:** upload, stream, move, and watch files under `/space` and `/rool-drive`.
117
+ - **Agents:** define agents, keep conversation history, attach machine files, stream runs, and request structured output.
118
+ - **Live state:** learn when account data changes, keep a file tree current, and stream conversation updates.
119
+ - **Shared app data:** define records that apps, agents, and programs inside the machine can all read and edit.
388
120
 
389
- const members = await machine.members.list();
390
- console.log(members[1].name ?? members[1].email);
391
- await machine.members.replaceRole(members[1].userId, { role: "viewer" });
392
- await machine.members.remove(members[1].userId);
393
- await machine.invites.revoke(invite.id);
394
- ```
395
-
396
- 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.
397
-
398
- ## Gifts
399
-
400
- 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.
401
-
402
- ```typescript
403
- const { gifts } = await client.listGifts();
404
- for (const gift of gifts) {
405
- console.log(gift.code, gift.url, gift.description, gift.claimedAt);
406
- }
407
-
408
- const preview = await client.previewGift(code); // no auth required
409
- console.log(
410
- `${preview.holderName ?? "Someone"} gave you ${preview.description}`,
411
- );
412
-
413
- const { gift } = await client.claimGift(code);
414
- if (gift.kind === "credits") console.log(`+${gift.credits} credits`);
415
- ```
416
-
417
- 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.
418
-
419
- 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.
420
-
421
- ```typescript
422
- await client.updateGift(giftId, { note: "sent to Peter" });
423
- await client.updateGift(giftId, { archived: true });
424
- await client.updateGift(giftId, { note: null });
425
- const updated = await client.rotateGiftCode(giftId);
426
- ```
427
-
428
- Gift failures are `RoolProblem` errors. `gift_invalid` means the code or gift is unavailable to the caller. `gift_claimed` means it was already claimed.
429
-
430
- ## API problems
431
-
432
- Each problem `type` links to its entry below.
433
-
434
- <a id="problem-authentication_required"></a>
435
-
436
- ### `authentication_required`
437
-
438
- **Documentation placeholder.**
439
-
440
- <a id="problem-invalid_authentication"></a>
441
-
442
- ### `invalid_authentication`
443
-
444
- **Documentation placeholder.**
445
-
446
- <a id="problem-email_unverified"></a>
447
-
448
- ### `email_unverified`
449
-
450
- **Documentation placeholder.**
451
-
452
- <a id="problem-account_suspended"></a>
453
-
454
- ### `account_suspended`
455
-
456
- **Documentation placeholder.**
457
-
458
- <a id="problem-invalid_profile"></a>
459
-
460
- ### `invalid_profile`
461
-
462
- **Documentation placeholder.**
463
-
464
- <a id="problem-invalid_user_app_data"></a>
465
-
466
- ### `invalid_user_app_data`
467
-
468
- **Documentation placeholder.**
469
-
470
- <a id="problem-invalid_json"></a>
471
-
472
- ### `invalid_json`
473
-
474
- **Documentation placeholder.**
475
-
476
- <a id="problem-payload_too_large"></a>
477
-
478
- ### `payload_too_large`
479
-
480
- **Documentation placeholder.**
481
-
482
- <a id="problem-user_app_data_too_large"></a>
483
-
484
- ### `user_app_data_too_large`
485
-
486
- **Documentation placeholder.**
487
-
488
- <a id="problem-not_found"></a>
489
-
490
- ### `not_found`
491
-
492
- **Documentation placeholder.**
493
-
494
- <a id="problem-checkpoint_not_found"></a>
495
-
496
- ### `checkpoint_not_found`
497
-
498
- **Documentation placeholder.**
499
-
500
- <a id="problem-sync_token_required"></a>
501
-
502
- ### `sync_token_required`
503
-
504
- The account event route requires the sync token returned by `/v2/session`.
505
-
506
- <a id="problem-invalid_sync_token"></a>
507
-
508
- ### `invalid_sync_token`
509
-
510
- The account event history no longer contains everything after this token. Fetch `/v2/session` and continue with its new token.
511
-
512
- <a id="problem-invalid_wait_preference"></a>
513
-
514
- ### `invalid_wait_preference`
515
-
516
- The account event wait must be an integer from 0 through 50 seconds.
517
-
518
- <a id="problem-internal_error"></a>
519
-
520
- ### `internal_error`
521
-
522
- **Documentation placeholder.**
121
+ 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.
523
122
 
524
- <a id="problem-server_misconfigured"></a>
525
-
526
- ### `server_misconfigured`
527
-
528
- **Documentation placeholder.**
529
-
530
- <a id="problem-current_run_exists"></a>
531
-
532
- ### `current_run_exists`
533
-
534
- The conversation is already running. Cancel or follow that run before prompting again. This also applies to prompts with `replaceTurnId`.
535
-
536
- <a id="problem-replace_turn_not_found"></a>
537
-
538
- ### `replace_turn_not_found`
539
-
540
- The user turn supplied as `replaceTurnId` is no longer in the conversation. Fetch the turns again before retrying the edit.
541
-
542
- <a id="problem-invalid_member_role"></a>
543
-
544
- ### `invalid_member_role`
545
-
546
- **Documentation placeholder.**
547
-
548
- <a id="problem-role_not_replaceable"></a>
549
-
550
- ### `role_not_replaceable`
551
-
552
- **Documentation placeholder.**
553
-
554
- <a id="problem-membership_not_removable"></a>
555
-
556
- ### `membership_not_removable`
557
-
558
- **Documentation placeholder.**
559
-
560
- <a id="problem-invalid_invite"></a>
561
-
562
- ### `invalid_invite`
563
-
564
- **Documentation placeholder.**
565
-
566
- <a id="problem-invite_invalid"></a>
567
-
568
- ### `invite_invalid`
569
-
570
- **Documentation placeholder.**
571
-
572
- <a id="problem-invite_expired"></a>
573
-
574
- ### `invite_expired`
575
-
576
- **Documentation placeholder.**
577
-
578
- <a id="problem-invite_revoked"></a>
579
-
580
- ### `invite_revoked`
581
-
582
- **Documentation placeholder.**
583
-
584
- <a id="problem-invite_exhausted"></a>
585
-
586
- ### `invite_exhausted`
587
-
588
- **Documentation placeholder.**
589
-
590
- <a id="problem-invite_email_mismatch"></a>
591
-
592
- ### `invite_email_mismatch`
593
-
594
- **Documentation placeholder.**
595
-
596
- <a id="problem-gift_invalid"></a>
597
-
598
- ### `gift_invalid`
599
-
600
- The code is not valid, or the requested gift does not belong to the current user.
601
-
602
- <a id="problem-gift_claimed"></a>
603
-
604
- ### `gift_claimed`
605
-
606
- The gift has already been claimed. A claimed gift cannot be claimed again or given a new code.
607
-
608
- <a id="problem-invalid_input"></a>
609
-
610
- ### `invalid_input`
611
-
612
- The gift update is empty or contains an invalid note or archived value.
613
-
614
- ## Development
615
-
616
- ```bash
617
- pnpm build
618
- pnpm typecheck
619
- ```
620
-
621
- 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.
622
-
623
- Copy `.env.example` to the ignored `.env` file and configure the local endpoints and expected primary account ID.
624
-
625
- | Variable | Purpose |
626
- | ------------------------------- | ---------------------------------------------------------------------- |
627
- | `ROOL_TEST_API_URL` | API origin. HTTPS is required except for loopback development servers. |
628
- | `ROOL_TEST_AUTH_URL` | Auth endpoint override required for a loopback API. |
629
- | `ROOL_TEST_ROUTER_URL` | Local machine-router origin. |
630
- | `ROOL_TEST_USER_ID` | Expected primary account ID, preventing mutation of the wrong account. |
631
- | `ROOL_TEST_GIFT_HOLDER_EMAIL` | Local fixture account that holds a gift. |
632
- | `ROOL_TEST_GIFT_CLAIMANT_EMAIL` | Local fixture account that claims the gift. |
633
- | `ROOL_TEST_INTERNAL_SECRET` | Local rool-server secret used to create and remove fixtures. |
634
-
635
- Log the `sdk-v2-primary` and `sdk-v2-member` Node profiles into two dedicated accounts:
636
-
637
- ```bash
638
- node --env-file=.env --import tsx test/integration/v2/login.ts
639
- ```
640
-
641
- Then run the complete v2 smoke-test suite:
642
-
643
- ```bash
644
- pnpm test:v2
645
- ```
646
-
647
- To run one smoke test manually, invoke its script directly, for example:
648
-
649
- ```bash
650
- node --env-file=.env --import tsx test/integration/v2/machine-routes.test.ts
651
- ```
123
+ The package exports its public TypeScript types, so editor autocomplete shows the detailed options and results. See [docs.rool.dev](https://docs.rool.dev/sdk/) for the published documentation.
652
124
 
653
125
  ## License
654
126