@rool-dev/sdk 0.12.5 → 2.0.0-dev.36748aa

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.
Files changed (108) hide show
  1. package/README.md +436 -784
  2. package/dist/agents.d.ts +138 -0
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/agents.js +563 -0
  5. package/dist/agents.js.map +1 -0
  6. package/dist/auth-base.d.ts +43 -88
  7. package/dist/auth-base.d.ts.map +1 -1
  8. package/dist/auth-base.js +178 -302
  9. package/dist/auth-base.js.map +1 -1
  10. package/dist/auth-browser.d.ts +7 -25
  11. package/dist/auth-browser.d.ts.map +1 -1
  12. package/dist/auth-browser.js +45 -82
  13. package/dist/auth-browser.js.map +1 -1
  14. package/dist/auth-native.d.ts +9 -56
  15. package/dist/auth-native.d.ts.map +1 -1
  16. package/dist/auth-native.js +88 -177
  17. package/dist/auth-native.js.map +1 -1
  18. package/dist/auth-node.d.ts +32 -36
  19. package/dist/auth-node.d.ts.map +1 -1
  20. package/dist/auth-node.js +338 -265
  21. package/dist/auth-node.js.map +1 -1
  22. package/dist/auth-url.d.ts +6 -0
  23. package/dist/auth-url.d.ts.map +1 -0
  24. package/dist/auth-url.js +30 -0
  25. package/dist/auth-url.js.map +1 -0
  26. package/dist/client.d.ts +35 -290
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +323 -635
  29. package/dist/client.js.map +1 -1
  30. package/dist/events.d.ts +40 -0
  31. package/dist/events.d.ts.map +1 -0
  32. package/dist/events.js +175 -0
  33. package/dist/events.js.map +1 -0
  34. package/dist/files.d.ts +174 -0
  35. package/dist/files.d.ts.map +1 -0
  36. package/dist/files.js +846 -0
  37. package/dist/files.js.map +1 -0
  38. package/dist/index.d.ts +16 -19
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +7 -20
  41. package/dist/index.js.map +1 -1
  42. package/dist/machine.d.ts +51 -0
  43. package/dist/machine.d.ts.map +1 -0
  44. package/dist/machine.js +142 -0
  45. package/dist/machine.js.map +1 -0
  46. package/dist/problem.d.ts +11 -0
  47. package/dist/problem.d.ts.map +1 -0
  48. package/dist/problem.js +20 -0
  49. package/dist/problem.js.map +1 -0
  50. package/dist/structured.d.ts +84 -0
  51. package/dist/structured.d.ts.map +1 -0
  52. package/dist/structured.js +405 -0
  53. package/dist/structured.js.map +1 -0
  54. package/dist/types.d.ts +119 -520
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +11 -16
  57. package/dist/auth.d.ts +0 -99
  58. package/dist/auth.d.ts.map +0 -1
  59. package/dist/auth.js +0 -217
  60. package/dist/auth.js.map +0 -1
  61. package/dist/client-info.d.ts +0 -18
  62. package/dist/client-info.d.ts.map +0 -1
  63. package/dist/client-info.js +0 -25
  64. package/dist/client-info.js.map +0 -1
  65. package/dist/event-emitter.d.ts +0 -45
  66. package/dist/event-emitter.d.ts.map +0 -1
  67. package/dist/event-emitter.js +0 -86
  68. package/dist/event-emitter.js.map +0 -1
  69. package/dist/graphql.d.ts +0 -90
  70. package/dist/graphql.d.ts.map +0 -1
  71. package/dist/graphql.js +0 -471
  72. package/dist/graphql.js.map +0 -1
  73. package/dist/logger.d.ts +0 -13
  74. package/dist/logger.d.ts.map +0 -1
  75. package/dist/logger.js +0 -17
  76. package/dist/logger.js.map +0 -1
  77. package/dist/path.d.ts +0 -6
  78. package/dist/path.d.ts.map +0 -1
  79. package/dist/path.js +0 -47
  80. package/dist/path.js.map +0 -1
  81. package/dist/reroute.d.ts +0 -22
  82. package/dist/reroute.d.ts.map +0 -1
  83. package/dist/reroute.js +0 -70
  84. package/dist/reroute.js.map +0 -1
  85. package/dist/rest.d.ts +0 -54
  86. package/dist/rest.d.ts.map +0 -1
  87. package/dist/rest.js +0 -171
  88. package/dist/rest.js.map +0 -1
  89. package/dist/router.d.ts +0 -20
  90. package/dist/router.d.ts.map +0 -1
  91. package/dist/router.js +0 -54
  92. package/dist/router.js.map +0 -1
  93. package/dist/space-session.d.ts +0 -30
  94. package/dist/space-session.d.ts.map +0 -1
  95. package/dist/space-session.js +0 -83
  96. package/dist/space-session.js.map +0 -1
  97. package/dist/space.d.ts +0 -161
  98. package/dist/space.d.ts.map +0 -1
  99. package/dist/space.js +0 -618
  100. package/dist/space.js.map +0 -1
  101. package/dist/subscription.d.ts +0 -36
  102. package/dist/subscription.d.ts.map +0 -1
  103. package/dist/subscription.js +0 -419
  104. package/dist/subscription.js.map +0 -1
  105. package/dist/webdav.d.ts +0 -171
  106. package/dist/webdav.d.ts.map +0 -1
  107. package/dist/webdav.js +0 -481
  108. package/dist/webdav.js.map +0 -1
package/README.md CHANGED
@@ -1,15 +1,6 @@
1
1
  # Rool SDK
2
2
 
3
- TypeScript SDK for programmatic access to [Rool Machines](https://rool.dev) — private AI computers holding objects, files, conversations, and memory, with AI-assisted editing built in.
4
-
5
- > **Naming note:** the SDK predates the Rool Machine branding and calls a Machine a **space** (`createSpace`, `RoolSpace`, `/space/...` paths). The API keeps this legacy naming until v2; read "space" as "Machine" throughout.
6
-
7
- Core primitives:
8
-
9
- - **Spaces** — Machines: containers for objects, schema, metadata, conversations, collaborators, and files.
10
- - **Conversations** — independent interaction histories in a space.
11
- - **Objects** — JSON records addressed by object paths such as `/space/article/welcome.json`.
12
- - **Files** — user-visible files stored under `/rool-drive/...` through WebDAV.
3
+ TypeScript SDK for Rool Machines.
13
4
 
14
5
  ## Installation
15
6
 
@@ -17,991 +8,652 @@ Core primitives:
17
8
  npm install @rool-dev/sdk
18
9
  ```
19
10
 
20
- ## Quick Start
11
+ ## User and session API
21
12
 
22
13
  ```typescript
23
- import { RoolClient } from '@rool-dev/sdk';
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.
24
39
 
25
- async function main() {
26
- const client = new RoolClient();
40
+ ## Account events
27
41
 
28
- if (!(await client.initialize())) {
29
- await client.login('My App');
30
- // Browser auth redirects away. Run startup again after the auth callback.
31
- return;
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
+ );
32
59
  }
60
+ });
33
61
 
34
- const space = await client.createSpace('Solar System');
35
- const conversation = space.conversation('main');
62
+ unsubscribe();
63
+ ```
36
64
 
37
- await space.createCollection('body', [
38
- { name: 'name', type: { kind: 'string' } },
39
- { name: 'mass', type: { kind: 'string' } },
40
- { name: 'radius', type: { kind: 'string' } },
41
- { name: 'orbits', type: { kind: 'maybe', inner: { kind: 'ref' } } },
42
- ]);
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.
43
66
 
44
- const { object: sun } = await space.putObject('/space/body/sun.json', {
45
- name: 'Sun',
46
- mass: '1 solar mass',
47
- radius: '696,340 km',
48
- });
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.
49
68
 
50
- const { object: earth } = await space.putObject('/space/body/earth.json', {
51
- name: 'Earth',
52
- mass: '1 Earth mass',
53
- radius: '6,371 km',
54
- orbits: sun.path,
55
- });
69
+ ## Authentication
56
70
 
57
- const { message, objects } = await conversation.prompt(
58
- 'Add the other planets in our solar system, each referencing the Sun.'
59
- );
71
+ Auth clients own login, credential storage, and refresh. `RoolClient` only asks one for tokens and tells it when authentication is invalidated.
60
72
 
61
- console.log(message);
62
- console.log(`Modified ${objects.length} objects`);
73
+ ### Browser authentication
63
74
 
64
- const loadedEarth = await space.getObject(earth.path);
65
- console.log(loadedEarth?.body.name);
75
+ ```typescript
76
+ import { BrowserAuth, RoolClient } from "@rool-dev/sdk";
66
77
 
67
- space.close();
68
- }
78
+ const auth = new BrowserAuth();
79
+ const client = new RoolClient({
80
+ getTokens: auth.getTokens,
81
+ onAuthInvalidated: auth.logout,
82
+ });
69
83
 
70
- void main();
84
+ auth.onAuthStateChanged(renderSignedInState);
85
+ if (!(await auth.initialize())) {
86
+ await auth.login("My App");
87
+ }
71
88
  ```
72
89
 
73
- ## Paths and Resource URIs
74
-
75
- Most SDK methods take plain path strings:
90
+ `initialize()` processes an auth callback in the URL. Tokens are stored in endpoint-scoped browser storage and refreshed when requested.
76
91
 
77
- - Object paths: `/space/<collection>/<name>.json` (exactly three segments; no dotfile collection or object names)
78
- - File paths: `/rool-drive/<path/to/file>`
92
+ ### Native authentication
79
93
 
80
- `rool-machine:/...` URIs are the user-facing/canonical form for resource references in prompt attachments and interaction history. The exported helpers normalize between these forms when you need them.
94
+ `NativeAuth` uses system-browser PKCE for Google and Apple while also supporting password and magic-link sign-in.
81
95
 
82
96
  ```typescript
83
- import { machinePath, machineUri, isObjectPath } from '@rool-dev/sdk';
84
-
85
- machinePath('rool-machine:/rool-drive/docs/read%20me.md');
86
- // '/rool-drive/docs/read me.md'
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
+ });
87
110
 
88
- machineUri('/space/article/welcome.json');
89
- // 'rool-machine:/space/article/welcome.json'
111
+ App.addListener("appUrlOpen", async ({ url }) => {
112
+ await Browser.close();
113
+ await auth.handleRedirect(url);
114
+ });
90
115
 
91
- isObjectPath('/space/article/welcome.json'); // true
116
+ if (!(await auth.initialize())) await auth.login("My App");
92
117
  ```
93
118
 
94
- Object APIs require full object paths. References between objects are ordinary body fields containing object paths:
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.
95
120
 
96
121
  ```typescript
97
- {
98
- path: '/space/body/earth.json',
99
- body: { name: 'Earth', orbits: '/space/body/sun.json' },
100
- }
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);
101
127
  ```
102
128
 
103
- ## Authentication
129
+ On native, an HTTPS magic link only returns to the app when Universal Links or App Links are configured for that domain.
104
130
 
105
- ### Browser
131
+ ### Password and account methods
106
132
 
107
- The default auth provider stores tokens in browser storage and redirects to the Rool auth page.
133
+ Browser and native auth clients also provide:
108
134
 
109
- ```typescript
110
- async function start() {
111
- const client = new RoolClient();
135
+ - `setPassword(password)`
136
+ - `requestEmailChange(newEmail)`
137
+ - `verify(token)`
138
+ - `logout()`
139
+ - `isAuthenticated()`
112
140
 
113
- if (!(await client.initialize())) {
114
- await client.login('My App');
115
- // Browser auth redirects; stop startup until the callback reloads the app.
116
- return;
117
- }
141
+ ### Node.js authentication
118
142
 
119
- // Use the authenticated client here.
120
- }
143
+ ```typescript
144
+ import { RoolClient } from "@rool-dev/sdk";
145
+ import { NodeAuth } from "@rool-dev/sdk/node";
121
146
 
122
- void start();
147
+ const auth = new NodeAuth();
148
+ if (!(await auth.initialize())) await auth.login("My CLI");
149
+
150
+ const client = new RoolClient({
151
+ getTokens: auth.getTokens,
152
+ onAuthInvalidated: auth.logout,
153
+ });
123
154
  ```
124
155
 
125
- ### Node.js
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.
126
157
 
127
- Use the Node auth provider for CLIs and scripts. It stores endpoint-scoped credentials under `~/.config/rool/` by default (for example, `credentials-<hash>.json`) and opens a browser for login.
158
+ Use `profile` or `credentialsPath` when an application needs multiple independent accounts:
128
159
 
129
160
  ```typescript
130
- import { RoolClient } from '@rool-dev/sdk';
131
- import { NodeAuthProvider } from '@rool-dev/sdk/node';
132
-
133
- const client = new RoolClient({ authProvider: new NodeAuthProvider() });
134
- let authenticated = await client.initialize();
135
-
136
- if (!authenticated) {
137
- await client.login('My CLI Tool');
138
- // Re-run initialize after the non-redirect login to hydrate currentUser,
139
- // user storage, and client-level event subscriptions.
140
- authenticated = await client.initialize();
141
- }
142
-
143
- if (!authenticated) throw new Error('Login required');
161
+ const auth = new NodeAuth({
162
+ apiUrl: "https://api.example.com",
163
+ profile: "automation",
164
+ });
144
165
  ```
145
166
 
146
- ### Native (Capacitor, Cordova, Tauri, ...)
167
+ API errors are thrown as `RoolProblem` with the server's stable `code`, HTTP `status`, `title`, and `detail`.
147
168
 
148
- Use the native PKCE provider for JS app shells that sign in through an external
149
- system browser. `login()`/`signup()` open the auth server's `/authorize` page
150
- via the `openExternal` callback you supply; when the deep link returns, feed it
151
- to `client.handleAuthRedirect(url)` from your platform's deep-link handler. The
152
- code is exchanged for a session at `/token` and tokens are stored like the
153
- browser provider.
169
+ ## Machine API
154
170
 
155
171
  ```typescript
156
- import { RoolClient, NativePkceAuthProvider } from '@rool-dev/sdk';
157
- import { Browser } from '@capacitor/browser';
158
- import { App } from '@capacitor/app';
159
-
160
- const client = new RoolClient({
161
- authProvider: new NativePkceAuthProvider({
162
- redirectUri: 'roolandroidauth://auth/callback', // must match the server allowlist
163
- defaultProvider: 'google', // 'google' | 'apple'
164
- openExternal: (url) => Browser.open({ url }),
165
- }),
166
- });
172
+ const created = await client.createMachine({ name: "Research" });
173
+ const machine = client.machine(created.id);
167
174
 
168
- // Complete sign-in when the OS hands the app its deep link.
169
- App.addListener('appUrlOpen', async ({ url }) => {
170
- if (await client.handleAuthRedirect(url)) {
171
- await Browser.close();
172
- // Now authenticated — refresh your UI.
173
- }
174
- });
175
-
176
- if (!(await client.initialize())) {
177
- // Opens the system browser; completion arrives via the listener above.
178
- await client.login('My App'); // pass { provider: 'apple' } to override the default
179
- }
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
180
  ```
181
181
 
182
- #### Email + password and magic links (native)
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
183
 
184
- The native provider also supports email/password sign-in and magic links —
185
- no system browser, the server returns the token set as JSON directly.
184
+ ## Machine checkpoints
186
185
 
187
186
  ```typescript
188
- // Password sign-in
189
- const result = await client.signInWithPassword(email, password);
190
- if (result.status === 'signed_in') {
191
- // authenticated — refresh your UI
192
- } else {
193
- // status === 'verify_required': the email isn't verified yet and the server
194
- // has emailed a magic link. Tell the user to check their inbox.
187
+ const history = await machine.checkpoints.list();
188
+ const checkpoint = history.checkpoints.at(-1);
189
+ if (checkpoint) {
190
+ await machine.checkpoints.restore(checkpoint.id);
195
191
  }
196
-
197
- // Or request a magic link explicitly
198
- await client.requestMagicLink(email);
199
192
  ```
200
193
 
201
- The magic link carries a `?verify=<token>` param; complete sign-in by passing
202
- that token to `client.verify(token)` once the link lands back in the app.
203
-
204
- > **⚠️ Magic links open the website, not the app, until Universal Links / App
205
- > Links are configured.** The emailed link is an `https://` URL for the Rool
206
- > web app. On native, an `https` link only re-opens your app if you've set up
207
- > [iOS Universal Links](https://developer.apple.com/ios/universal-links/) /
208
- > [Android App Links](https://developer.android.com/training/app-links) for that
209
- > domain (custom-scheme deep links don't apply to email links). **Without that
210
- > setup the magic link completes sign-in in the browser/website, not in the
211
- > native app** — so for now treat magic links on native as a website hand-off,
212
- > and prefer password or social sign-in for an in-app experience.
213
-
214
- ### Auth API
215
-
216
- | Method | Description |
217
- | --- | --- |
218
- | `initialize(): Promise<boolean>` | Call on startup. Initializes auth, refreshes user/storage state, and starts client events when authenticated. |
219
- | `login(appName, params?): Promise<void>` | Start login flow. |
220
- | `signup(appName, params?): Promise<void>` | Start signup flow. |
221
- | `verify(token): Promise<boolean>` | Complete email verification token flow; returns `false` when the active auth provider does not implement verification. |
222
- | `handleAuthRedirect(url): Promise<boolean>` | Complete a native PKCE sign-in from a deep-link callback URL. Returns `false` when the active auth provider does not implement it. |
223
- | `signInWithPassword(email, password): Promise<PasswordSignInResult>` | Email + password sign-in (native provider). Resolves `{ status: 'signed_in' }`, or `{ status: 'verify_required' }` when the email is unverified (a magic link was emailed). Rejects on bad credentials. |
224
- | `requestMagicLink(email): Promise<void>` | Email the user a magic sign-in link (native provider). See the caveat below — on native the link opens the **website**, not the app, until Universal Links / App Links are configured. |
225
- | `logout(): void` | Clear auth state and close open spaces. |
226
- | `isAuthenticated(): Promise<boolean>` | Whether credentials are held locally. No network call — a server outage does not read as logged out. |
227
- | `getAuthUser(): AuthUser` | Return auth identity decoded from the token. |
228
- | `setPassword(password): Promise<void>` | Set/change password and sign out the user's other sessions. |
229
- | `requestEmailChange(newEmail): Promise<void>` | Start an email address change. The server mails a confirmation link to the new address; the change applies when that link is clicked. |
230
-
231
- ### Changing email
232
-
233
- `requestEmailChange` starts the flow; nothing changes until the user clicks the confirmation link mailed to the new address. After confirmation the current session no longer belongs to the account — the next API call fails with a 401, which ends the session via `authStateChanged(false)`. Have the user sign in again with the new address.
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
234
197
 
235
198
  ```typescript
236
- import { EmailChangeError } from '@rool-dev/sdk';
237
-
238
- try {
239
- await client.requestEmailChange('new@example.com');
240
- // Tell the user to check the new address's inbox.
241
- } catch (error) {
242
- if (error instanceof EmailChangeError) {
243
- showFormError(error.message); // user-facing message
244
- }
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
+ 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,
222
+ });
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);
245
239
  }
246
240
  ```
247
241
 
248
- Refusals throw `EmailChangeError` with a `code` and a user-facing `message`. Codes worth branching on:
249
-
250
- | Code | Meaning |
251
- | --- | --- |
252
- | `invalid_email` | The new address is not a valid email. |
253
- | `same_email` | The new address is the account's current address. |
254
- | `email_in_use` | Another account already uses the new address. |
255
- | `send_failed` | The confirmation email could not be sent. |
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.
256
243
 
257
- ### Offline behavior
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.
258
245
 
259
- A temporarily unreachable server never reads as "logged out". `initialize()` reports authentication from stored credentials, so on an offline start it can return `true` while `currentUser` is still `null` and user storage is empty — the SDK keeps reconnecting in the background and hydrates both automatically once the server is reachable, emitting `currentUserChanged`. Only a hard auth failure ends the session an invalid or expired refresh token, or a 401 from the API (the identity was retired, e.g. after an email change, or the account deleted) — via `authStateChanged(false)`.
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.
260
247
 
261
- ## Spaces and Conversations
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.
262
249
 
263
- Open a space to receive live events and manage objects, schema, metadata, collaborators, file storage, and conversations. Use a conversation handle for conversation metadata and AI.
250
+ Watch the files when an application needs a live machine file tree:
264
251
 
265
252
  ```typescript
266
- const space = await client.openSpace('space-id');
253
+ await files.watch();
267
254
 
268
- const conversation = space.conversation('main');
269
- space.on('filesChanged', () => console.log('files changed'));
255
+ const unsubscribe = files.tree.subscribe(({ reset, changed, deleted }) => {
256
+ renderFileChanges({ reset, changed, deleted });
257
+ });
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,
263
+ });
270
264
 
271
- await conversation.prompt('Summarize this space');
265
+ unsubscribe();
266
+ files.unwatch();
272
267
  ```
273
268
 
274
- Conversation IDs must be 1–32 characters and contain only letters, numbers, `_`, and `-`.
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.
275
270
 
276
- ## Object Operations
271
+ ## Rool Object Collections
277
272
 
278
- Objects are JSON files under `/space`. Create the collection before writing objects in it.
273
+ Objects are JSON stored under `/space`. Collections are directories with a `.schema.json` definition and objects are schema-checked JSON files.
279
274
 
280
275
  ```typescript
281
- await space.createCollection('article', [
282
- { name: 'title', type: { kind: 'string' } },
283
- { name: 'status', type: { kind: 'string' } },
284
- ]);
276
+ await machine.files.watch();
285
277
 
286
- // Create or replace an exact object path
287
- const { object } = await space.putObject('/space/article/welcome.json', {
288
- title: 'Welcome',
289
- status: 'draft',
278
+ const task = await machine.collections.create("task", {
279
+ fields: [
280
+ { name: "title", type: { kind: "string" } },
281
+ { name: "done", type: { kind: "boolean" } },
282
+ ],
290
283
  });
291
284
 
292
- // Patch fields; null or undefined deletes a field
293
- await space.patchObject(object.path, {
294
- data: { status: 'published', obsoleteField: null },
285
+ const first = await machine.objects.create("/space/task/first.json", {
286
+ title: "First task",
287
+ done: false,
295
288
  });
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
+ ]);
296
295
 
297
- // Read one or many objects
298
- await space.getObject('/space/article/welcome.json');
299
- await space.getObjects([
300
- '/space/article/welcome.json',
301
- '/space/article/intro.json',
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",
302
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
+ ```
303
305
 
304
- // Rename or move an object
305
- await space.moveObject(
306
- '/space/article/welcome.json',
307
- '/space/article/hello-world.json'
308
- );
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.
309
307
 
310
- // Delete objects
311
- await space.deleteObjects(['/space/article/hello-world.json']);
312
- ```
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.
313
309
 
314
- | Method | Description |
315
- | --- | --- |
316
- | `getObject(path): Promise<RoolObject | undefined>` | Fetch one object by object path. |
317
- | `getObjects(paths): Promise<GetObjectsResult>` | Fetch objects in bulk; returns `objects` and `missing`. |
318
- | `space.putObject(path, body): Promise<{ object, message }>` | Create or replace an object at an exact path. |
319
- | `space.patchObject(path, { data }): Promise<{ object, message }>` | Patch an object's body; `null`/`undefined` deletes fields. |
320
- | `space.moveObject(from, to, options?): Promise<{ object, message }>` | Rename or relocate an object; `options.body` can replace the body after moving. |
321
- | `space.deleteObjects(paths): Promise<void>` | Delete object files. |
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.
322
311
 
323
- ## AI Agent
312
+ ## Agents
324
313
 
325
- `prompt()` invokes the AI agent. The agent can inspect space context and, unless `readOnly` or a read-only effort is used, create/modify/move/delete objects.
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()`.
326
315
 
327
316
  ```typescript
328
- const { message, objects } = await conversation.prompt(
329
- 'Create a topic node for the solar system, then child nodes for each planet.'
330
- );
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" });
331
332
 
332
- console.log(message);
333
- console.log(objects.map((object) => object.path));
333
+ // When this conversation leaves the UI:
334
+ stopWatching();
334
335
  ```
335
336
 
336
- ### Prompt Options
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.
337
338
 
338
- | Option | Description |
339
- | --- | --- |
340
- | `responseSchema` | Request structured JSON text matching a JSON-schema-like shape. |
341
- | `effort` | `'QUICK'` (fast/read-only), `'STANDARD'` (default), `'REASONING'`, or `'RESEARCH'`. |
342
- | `ephemeral` | Do not record the prompt in interaction history. |
343
- | `readOnly` | Disable mutation tools. |
344
- | `parentInteractionId` | Conversation-tree parent. Omit to fetch and continue from the current default leaf; pass `null` for a new root branch. |
345
- | `attachments` | Existing object/file paths or `rool-machine:/...` URIs, plus local files (`File`, `Blob`, or `{ data, contentType, filename? }`). |
346
- | `signal` | AbortSignal used to request that the server stop an in-flight prompt. |
347
- | `eventName` | Optional telemetry event name. Defaults to `'prompt_user'`. |
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.
348
340
 
349
- ```typescript
350
- // Read-only quick question
351
- await conversation.prompt('What topics are covered?', {
352
- effort: 'QUICK', // fast/read-only
353
- });
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.
354
342
 
355
- // Focus on existing objects and files
356
- await conversation.prompt('Compare these resources', {
357
- attachments: [
358
- '/space/article/intro.json',
359
- 'rool-machine:/rool-drive/docs/report.pdf',
360
- ],
361
- });
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.
362
344
 
363
- // Upload a local file as an attachment
364
- await conversation.prompt('Describe this image', {
365
- attachments: [fileInput.files![0]],
366
- });
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.
367
348
 
368
- // Structured response
369
- const { message } = await conversation.prompt('Categorize these items', {
349
+ ```typescript
350
+ await conversation.prompt("Return the number of records.", {
370
351
  responseSchema: {
371
- type: 'object',
372
- properties: {
373
- categories: { type: 'array', items: { type: 'string' } },
374
- summary: { type: 'string' },
375
- },
352
+ type: "object",
353
+ properties: { count: { type: "integer" } },
354
+ required: ["count"],
355
+ additionalProperties: false,
376
356
  },
377
357
  });
378
- const result = JSON.parse(message);
358
+ await conversation.follow();
379
359
 
380
- // Stop a long prompt with a signal (when the caller holds the controller)
381
- const ac = new AbortController();
382
- const promptPromise = conversation.prompt('Do a deep analysis', {
383
- effort: 'RESEARCH',
384
- signal: ac.signal,
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",
385
375
  });
386
- ac.abort(); // asks the server to stop the in-flight interaction
387
- await promptPromise;
376
+ await customConversation.prompt("Investigate this claim.");
388
377
  ```
389
378
 
390
- ### Stopping a conversation
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.
391
380
 
392
- Use `signal` when the same call site cancels the prompt. When the Stop button
393
- lives elsewhere — a different component, after a reload, or a prompt another
394
- client started — stop the conversation itself. A conversation processes one
395
- run at a time, so no interaction ID is needed. Stopping is best-effort: the
396
- server halts the agent loop and closes the stream, but an LLM turn already in
397
- flight keeps generating server-side and is billed.
381
+ ## Members and invites
398
382
 
399
383
  ```typescript
400
- // Stop whatever is running in a conversation. Returns whether anything was
401
- // actually running.
402
- await space.stopConversation('thread-42');
384
+ const invite = await machine.invites.create({
385
+ role: "editor",
386
+ maxUses: 1,
387
+ });
388
+ const token = invite.url.split("/").at(-1)!;
403
389
 
404
- // Conversation handles stop their own running work.
405
- const thread = space.conversation('thread-42');
406
- await thread.stop();
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);
407
398
  ```
408
399
 
409
- | Method | Description |
410
- | --- | --- |
411
- | `stopConversation(conversationId): Promise<boolean>` | Stop whatever is running in a conversation. |
412
- | `conversation.stop(): Promise<boolean>` | Stop this conversation's running work. |
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.
413
401
 
414
- ## Conversations
402
+ ## Gifts
415
403
 
416
- Use `space.conversation(id)` for an imperative API scoped to one conversation. The handle retains no conversation contents or branch cursor; fetch contents when needed. With no explicit parent, `prompt()` fetches the current conversation and continues from its default leaf.
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.
417
405
 
418
406
  ```typescript
419
- import { conversationBranch, defaultConversationLeaf } from '@rool-dev/sdk';
407
+ const { gifts } = await client.listGifts();
408
+ for (const gift of gifts) {
409
+ console.log(gift.code, gift.url, gift.description, gift.claimedAt);
410
+ }
420
411
 
421
- const thread = space.conversation('thread-42');
422
- await thread.prompt('Hello from another thread');
423
- await thread.setSystemInstruction('Answer in haiku');
412
+ const preview = await client.previewGift(code); // no auth required
413
+ console.log(
414
+ `${preview.holderName ?? "Someone"} gave you ${preview.description}`,
415
+ );
424
416
 
425
- const conversation = await thread.get();
426
- const leaf = defaultConversationLeaf(conversation);
427
- const branch = conversationBranch(conversation, leaf);
417
+ const { gift } = await client.claimGift(code);
418
+ if (gift.kind === "credits") console.log(`+${gift.credits} credits`);
428
419
  ```
429
420
 
430
- | Method/property | Description |
431
- | --- | --- |
432
- | `space.conversation(id): ConversationHandle` | Get a stateless conversation-scoped API handle. |
433
- | `conversation.get(): Promise<Conversation | null>` | Fetch current conversation contents. |
434
- | `conversation.prompt(text, options?)` | Prompt the conversation. Pass `parentInteractionId` to choose a branch explicitly. |
435
- | `conversation.setSystemInstruction(value)` | Set or clear the system instruction. |
436
- | `conversation.rename(name)` / `conversation.delete()` / `conversation.stop()` | Manage the scoped conversation. |
437
- | `listConversations(): Promise<ConversationMeta[]>` | Fetch the conversation roster. |
438
- | `createConversation(agent, visibility): Promise<string>` | Create a conversation under an agent; returns the server-minted conversation ID. |
439
- | `deleteConversation(id): Promise<void>` | Delete a conversation by ID. |
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.
440
422
 
441
- ### Agents
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.
442
424
 
443
- Conversations belong to an agent. Every space has the stock agent `rool`; spaces can also host custom agents. `createConversation` creates a conversation under any agent — including `rool` — with a server-minted ID and the visibility you choose; prompt the returned ID through a normal handle. Stock (`rool`) conversations can also spring into existence from a client-minted ID on first prompt (as in the examples above); custom-agent conversations are only created explicitly.
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
+ ```
444
431
 
445
- Each conversation has a visibility: `'shared'` (every space member), `'private'` (only you), or `'temporary'` (private, and auto-deleted after sitting idle). A conversation's agent and visibility are reported on its `ConversationMeta`.
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.
446
433
 
447
- ```typescript
448
- const agents = await space.listAgents(); // always includes 'rool'
434
+ ## API problems
449
435
 
450
- const id = await space.createConversation('research-bot', 'private');
451
- await space.conversation(id).prompt('Hello');
436
+ Each problem `type` links to its entry below.
452
437
 
453
- await space.deleteAgent('research-bot'); // removes the agent and all its conversations
454
- ```
438
+ <a id="problem-authentication_required"></a>
455
439
 
456
- | Method | Description |
457
- | --- | --- |
458
- | `listAgents(): Promise<string[]>` | The space's agents. Always includes the stock agent `rool`. |
459
- | `createConversation(agent, visibility): Promise<string>` | Create a conversation under an agent with the given visibility. |
460
- | `deleteAgent(agent): Promise<void>` | Delete a custom agent and all its conversations. The stock agent `rool` cannot be deleted. |
440
+ ### `authentication_required`
461
441
 
462
- ## Schema and Metadata
442
+ **Documentation placeholder.**
463
443
 
464
- Collections define the schema visible to the AI agent. Hidden body fields whose names start with `_` are useful for app/UI state that should not be considered by AI.
444
+ <a id="problem-invalid_authentication"></a>
465
445
 
466
- ```typescript
467
- await space.createCollection('article', {
468
- schemaOrgType: 'Article',
469
- fields: [
470
- { name: 'title', type: { kind: 'string' } },
471
- { name: 'status', type: { kind: 'enum', values: ['draft', 'published'] } },
472
- { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } },
473
- { name: 'author', type: { kind: 'ref' } },
474
- ],
475
- });
446
+ ### `invalid_authentication`
476
447
 
477
- const schema = await space.readSchema();
448
+ **Documentation placeholder.**
478
449
 
479
- await space.alterCollection('article', [
480
- { name: 'title', type: { kind: 'string' } },
481
- { name: 'status', type: { kind: 'string' } },
482
- ]);
450
+ <a id="problem-email_unverified"></a>
483
451
 
484
- await space.writeMeta({ viewport: { x: 0, y: 0, zoom: 1 } });
485
- const meta = await space.readMeta();
486
- ```
452
+ ### `email_unverified`
487
453
 
488
- | Method | Description |
489
- | --- | --- |
490
- | `readSchema(): Promise<SpaceSchema>` | Collection definitions, read from `/space/<name>/.schema.json`. |
491
- | `createCollection(name, fieldsOrDef, options?): Promise<CollectionDef>` | Create a collection. |
492
- | `alterCollection(name, fieldsOrDef, options?): Promise<CollectionDef>` | Replace a collection definition. |
493
- | `dropCollection(name): Promise<void>` | Remove a collection and its object directory. |
494
- | `readMeta(): Promise<Record<string, unknown>>` | Read metadata from `/space/.meta.json`. |
495
- | `writeMeta(meta): Promise<void>` | Write the full metadata blob to `/space/.meta.json`. |
454
+ **Documentation placeholder.**
496
455
 
497
- Field kinds: `string`, `number`, `boolean`, `ref`, `enum`, `literal`, `array`, and `maybe`.
456
+ <a id="problem-account_suspended"></a>
498
457
 
499
- ## Undo/Redo
458
+ ### `account_suspended`
500
459
 
501
- Undo/redo works over the whole space. Checkpoints are managed automatically by the server, so you don't need to create them yourself — just call `undo()`/`redo()`.
460
+ **Documentation placeholder.**
502
461
 
503
- ```typescript
504
- await space.deleteObjects(['/space/article/welcome.json']);
462
+ <a id="problem-invalid_profile"></a>
505
463
 
506
- if (await space.canUndo()) {
507
- await space.undo();
508
- }
509
- ```
464
+ ### `invalid_profile`
510
465
 
511
- | Method | Description |
512
- | --- | --- |
513
- | `canUndo(): Promise<boolean>` | Check whether undo is available. |
514
- | `canRedo(): Promise<boolean>` | Check whether redo is available. |
515
- | `undo(): Promise<boolean>` | Restore the latest checkpoint. |
516
- | `redo(): Promise<boolean>` | Reapply undone work. |
466
+ **Documentation placeholder.**
517
467
 
518
- Undo/redo availability is scoped to the space.
468
+ <a id="problem-invalid_user_app_data"></a>
519
469
 
520
- ## File Storage and WebDAV
470
+ ### `invalid_user_app_data`
521
471
 
522
- Every space has authenticated WebDAV storage. WebDAV methods take SDK machine paths such as `/space/...`, `/rool-drive/...`, or `/` for the root collection.
472
+ **Documentation placeholder.**
523
473
 
524
- ```typescript
525
- const webdav = space.webdav;
474
+ <a id="problem-invalid_json"></a>
526
475
 
527
- await webdav.mkcol('/rool-drive/docs');
528
- await webdav.put('/rool-drive/docs/readme.md', '# Hello', {
529
- contentType: 'text/markdown',
530
- ifNoneMatch: '*',
531
- });
476
+ ### `invalid_json`
532
477
 
533
- const listing = await webdav.propfind('/rool-drive/docs', {
534
- depth: '1',
535
- props: ['displayname', 'getcontentlength', 'getcontenttype', 'getetag'],
536
- });
478
+ **Documentation placeholder.**
537
479
 
538
- const response = await webdav.get('/rool-drive/docs/readme.md');
539
- console.log(await response.text());
480
+ <a id="problem-payload_too_large"></a>
540
481
 
541
- const file = await space.fetchPath('/rool-drive/docs/readme.md');
542
- console.log(file.headers.get('Content-Type'));
482
+ ### `payload_too_large`
543
483
 
544
- ```
484
+ **Documentation placeholder.**
545
485
 
546
- ### Real-time file sync
486
+ <a id="problem-user_app_data_too_large"></a>
547
487
 
548
- Object and file changes are announced at the space level. Use WebDAV `syncCollection()` to reconcile changes.
488
+ ### `user_app_data_too_large`
549
489
 
550
- ```typescript
551
- let token: string | null = null;
490
+ **Documentation placeholder.**
552
491
 
553
- async function syncFiles() {
554
- const result = await space.webdav.syncCollection('/', {
555
- token,
556
- level: 'infinite',
557
- props: ['displayname', 'getetag', 'getlastmodified', 'resourcetype'],
558
- });
559
- token = result.token;
560
- updateFileTree(result.responses);
561
- }
492
+ <a id="problem-not_found"></a>
562
493
 
563
- space.on('filesChanged', syncFiles);
564
- space.on('filesReset', () => {
565
- token = null;
566
- void syncFiles();
567
- });
494
+ ### `not_found`
568
495
 
569
- await syncFiles();
570
- ```
496
+ **Documentation placeholder.**
571
497
 
572
- | Method | Description |
573
- | --- | --- |
574
- | `webdav.href(path)` / `webdav.url(path)` | Return WebDAV href/URL for an absolute SDK path. |
575
- | `webdav.options(path)` | Send `OPTIONS`. |
576
- | `webdav.propfind(path, options)` | Read properties/list collections. `depth` is required. |
577
- | `webdav.syncCollection(path, options)` | WebDAV `REPORT sync-collection`; returns changed responses and next token. |
578
- | `webdav.get(path, options?)` / `webdav.head(path)` | Read a file; `get` supports byte ranges. |
579
- | `webdav.put(path, body, options?)` | Write a file/object at an exact path. Parent collection must exist unless `createParents: true` is passed, which creates missing parent collections atomically (intended for `/rool-drive` paths). |
580
- | `webdav.mkcol(path)` | Create one collection. |
581
- | `webdav.copy(source, destination, options?)` | Copy a file or collection. |
582
- | `webdav.move(source, destination, options?)` | Move a file or collection. |
583
- | `webdav.delete(path, options?)` | Delete a file or collection. |
584
- | `webdav.lock(path, options)` / `refreshLock(path, token)` / `unlock(token)` | WebDAV write locks. |
585
- | `webdav.request(method, path, init?)` | Raw authenticated WebDAV request. |
586
- | `space.fetchPath(path, options?)` | Fetch a `/rool-drive/...` file path or `rool-machine:` file URI. |
498
+ <a id="problem-checkpoint_not_found"></a>
587
499
 
588
- High-level WebDAV methods that validate response status throw `WebDAVError` with `status`, `statusText`, and `body`; raw `request()` and `options()` return `Response`.
500
+ ### `checkpoint_not_found`
589
501
 
590
- ## Collaboration
502
+ **Documentation placeholder.**
591
503
 
592
- New members join a space by redeeming an invite. Owners and admins mint invites; the returned `url` contains the secret token and is only available at mint time.
504
+ <a id="problem-sync_token_required"></a>
593
505
 
594
- ```typescript
595
- // Shareable invite link
596
- const invite = await space.createInvite('editor', { expiresInDays: 7 });
597
- console.log(invite.url);
598
-
599
- // Email-guarded invite: single-use, locked to that address, sent by mail
600
- const emailed = await space.createInvite('viewer', { email: 'colleague@example.com' });
601
- if (emailed.emailStatus !== 'sent') {
602
- // Mail did not go out (e.g. no mail provider configured) — share emailed.url yourself
603
- }
506
+ ### `sync_token_required`
604
507
 
605
- // Manage outstanding invites
606
- const invites = await space.listInvites();
607
- await space.revokeInvite(invites[0].inviteId);
508
+ The account event route requires the sync token returned by `/v2/session`.
608
509
 
609
- // Change an existing member's role, or remove them
610
- await space.setUserRole(userId, 'admin');
611
- await space.removeUser(userId);
612
- ```
510
+ <a id="problem-invalid_sync_token"></a>
613
511
 
614
- On the join page, look up the invite before sign-in and redeem it once authenticated:
512
+ ### `invalid_sync_token`
615
513
 
616
- ```typescript
617
- const preview = await client.previewInvite(token); // no auth required
618
- console.log(preview.spaceName, preview.role, preview.inviterName);
514
+ The account event history no longer contains everything after this token. Fetch `/v2/session` and continue with its new token.
619
515
 
620
- const result = await client.redeemInvite(token);
621
- console.log(result.spaceId, result.status); // 'joined' | 'upgraded' | 'already_member'
622
- ```
516
+ <a id="problem-invalid_wait_preference"></a>
623
517
 
624
- Invalid, expired, revoked, exhausted, or email-mismatched invites throw `InviteError` with a `code` of `'INVITE_INVALID' | 'INVITE_EXPIRED' | 'INVITE_REVOKED' | 'INVITE_EXHAUSTED' | 'INVITE_EMAIL_MISMATCH'`.
518
+ ### `invalid_wait_preference`
625
519
 
626
- ```typescript
627
- import { InviteError } from '@rool-dev/sdk';
520
+ The account event wait must be an integer from 0 through 50 seconds.
628
521
 
629
- try {
630
- await client.redeemInvite(token);
631
- } catch (error) {
632
- if (error instanceof InviteError && error.code === 'INVITE_EXPIRED') {
633
- // Ask for a fresh invite
634
- }
635
- }
636
- ```
522
+ <a id="problem-internal_error"></a>
637
523
 
638
- Roles (invites grant `admin`, `editor`, or `viewer` — never `owner`):
524
+ ### `internal_error`
639
525
 
640
- | Role | Capabilities |
641
- | --- | --- |
642
- | `owner` | Full control. |
643
- | `admin` | Editor capabilities plus user/link management. |
644
- | `editor` | Create, modify, move, and delete objects/files. |
645
- | `viewer` | Read-only access. |
526
+ **Documentation placeholder.**
646
527
 
647
- ## Gifts
528
+ <a id="problem-server_misconfigured"></a>
648
529
 
649
- A gift is something of value carried by a short code, e.g. `K7M2-9QRT`. Users are issued gifts server-side and give them away in their own words; whoever claims one gets what it holds. Claiming your own gift is a legitimate use. A gift is unclaimed or claimed, and claiming is single-use.
530
+ ### `server_misconfigured`
650
531
 
651
- What a gift grants is in its `gift` field, discriminated by `kind`. Every gift also carries a server-rendered `description` such as `"10,000 AI credits"`, so UI can display any gift without knowing its kind. Prefer `description` unless you need the numbers.
532
+ **Documentation placeholder.**
652
533
 
653
- Narrow on `kind` before reading any other payload field. Kinds are added over time, and un-narrowed access becomes a compile error when the next one lands.
534
+ <a id="problem-current_run_exists"></a>
654
535
 
655
- Users hold no gifts by default, so `listGifts()` returning an empty list is the normal state for most accounts, not an error.
536
+ ### `current_run_exists`
656
537
 
657
- ```typescript
658
- // The current user's gifts, spent and unspent
659
- const { gifts } = await client.listGifts();
660
- for (const gift of gifts) {
661
- console.log(gift.code, gift.url, gift.description, gift.claimedAt);
662
- }
663
- ```
538
+ The conversation is already running. Cancel or follow that run before prompting again. This also applies to prompts with `replaceTurnId`.
664
539
 
665
- On the claim page, look up the gift before sign-up and claim it once authenticated:
540
+ <a id="problem-replace_turn_not_found"></a>
666
541
 
667
- ```typescript
668
- const preview = await client.previewGift(code); // no auth required
669
- console.log(`${preview.holderName} gave you ${preview.description}`);
542
+ ### `replace_turn_not_found`
670
543
 
671
- const { gift } = await client.claimGift(code);
672
- if (gift.kind === 'credits') console.log(`+${gift.credits} credits`);
673
- ```
544
+ The user turn supplied as `replaceTurnId` is no longer in the conversation. Fetch the turns again before retrying the edit.
674
545
 
675
- Codes are case-insensitive, and the dash is optional on input.
546
+ <a id="problem-invalid_member_role"></a>
676
547
 
677
- When a gift is claimed, `claimedByName` carries the claimer's display name — the reciprocal of the claim page showing the holder's name to the claimer. The claim page tells the claimer this will happen.
548
+ ### `invalid_member_role`
678
549
 
679
- A holder can annotate a gift with a note ("sent this to Peter") and archive it to hide it from their default view. Both are bookkeeping only: an archived gift's code still claims.
550
+ **Documentation placeholder.**
680
551
 
681
- A holder can also rotate a gift's code: a fresh code is minted and the old one stops working. Use it to take back a code that was given away but never claimed — the gift itself is untouched.
552
+ <a id="problem-role_not_replaceable"></a>
682
553
 
683
- ```typescript
684
- await client.updateGift(gift.id, { note: 'sent to Peter' });
685
- await client.updateGift(gift.id, { archived: true });
686
- await client.updateGift(gift.id, { note: null }); // clear the note
554
+ ### `role_not_replaceable`
687
555
 
688
- const updated = await client.rotateGiftCode(gift.id);
689
- console.log(updated.code, updated.url); // the old code and link are dead
690
- ```
556
+ **Documentation placeholder.**
691
557
 
692
- Failed gift operations throw `GiftError` with a `code`:
558
+ <a id="problem-membership_not_removable"></a>
693
559
 
694
- | Code | Meaning |
695
- | --- | --- |
696
- | `gift_invalid` | No gift matches that code. |
697
- | `gift_claimed` | The gift has already been claimed. |
560
+ ### `membership_not_removable`
698
561
 
699
- ```typescript
700
- import { GiftError } from '@rool-dev/sdk';
562
+ **Documentation placeholder.**
701
563
 
702
- try {
703
- await client.claimGift(code);
704
- } catch (error) {
705
- if (error instanceof GiftError && error.code === 'gift_claimed') {
706
- // Offer a plain signup instead
707
- }
708
- }
709
- ```
564
+ <a id="problem-invalid_invite"></a>
710
565
 
711
- ## RoolClient API
566
+ ### `invalid_invite`
712
567
 
713
- ### Constructor config
568
+ **Documentation placeholder.**
714
569
 
715
- ```typescript
716
- const client = new RoolClient({
717
- apiUrl: 'https://api.rool.dev',
718
- authUrl: 'https://rool.dev/auth',
719
- graphqlUrl: 'https://api.rool.dev/graphql',
720
- client: {
721
- appName: 'com.example.app',
722
- appVersion: '1.4.2',
723
- osVersion: 'iOS 17.5',
724
- },
725
- logger: console,
726
- });
727
- ```
570
+ <a id="problem-invite_invalid"></a>
728
571
 
729
- `apiUrl` defaults to `https://api.rool.dev`; `authUrl` is derived by stripping the `api.` hostname prefix unless provided. `baseUrl` is still accepted as a deprecated alias for `apiUrl`. Pass `authProvider` for Node.js, Electron, or custom auth flows.
730
-
731
- `client` is optional application identity sent on requests alongside the SDK package name/version. Compatibility is based only on the SDK version.
732
-
733
- | Method/property | Description |
734
- | --- | --- |
735
- | `currentUser: CurrentUser | null` | Cached user profile from initialization/fetch. |
736
- | `getCurrentUser(): Promise<CurrentUser>` | Fetch current user. |
737
- | `updateCurrentUser(input): Promise<CurrentUser>` | Update `name`, `slug`, or `marketingOptIn`. |
738
- | `deleteCurrentUser(): Promise<void>` | Mark account for deletion and log out. |
739
- | `previewInvite(token): Promise<InvitePreview>` | Look up an invite link without redeeming it. No auth required. |
740
- | `redeemInvite(token): Promise<InviteRedeemResult>` | Redeem an invite, joining (or upgrading in) its space. |
741
- | `previewGift(code): Promise<GiftPreview>` | Look up a gift without claiming it. No auth required. |
742
- | `claimGift(code): Promise<GiftClaimResult>` | Claim a gift, granting what it holds to the current account. |
743
- | `listGifts(): Promise<GiftList>` | The current user's gifts, spent and unspent. |
744
- | `updateGift(giftId, changes): Promise<Gift>` | Set or clear a gift's note, or change its archived state. |
745
- | `rotateGiftCode(giftId): Promise<Gift>` | Mint a new code for a gift; the old code and link stop working. |
746
- | `listSpaces(): Promise<RoolSpaceInfo[]>` | List accessible spaces. |
747
- | `openSpace(id): Promise<RoolSpace>` | Open/cached live space handle. |
748
- | `createSpace(name): Promise<RoolSpace>` | Create and open a space. |
749
- | `duplicateSpace(sourceId, name): Promise<RoolSpace>` | Duplicate a space. |
750
- | `deleteSpace(id): Promise<void>` | Permanently delete a space. |
751
- | `importArchive(name, archive): Promise<RoolSpace>` | Import a zip archive as a new space. |
752
- | `getUserStorage<T>(key): T | undefined` | Sync read from user-storage cache. |
753
- | `setUserStorage(key, value): void` | Update user storage; `null`/`undefined` deletes. |
754
- | `getAllUserStorage(): Record<string, unknown>` | Copy all cached user storage. |
755
- | `reportEvent(event, url?): void` | Fire-and-forget telemetry event. |
756
- | `destroy(): void` | Close subscriptions, spaces, auth resources, and listeners. |
757
- | `generateId(): string` | Generate a unique ID suitable for conversation IDs. |
758
-
759
- ### Client events
572
+ ### `invite_invalid`
760
573
 
761
- ```typescript
762
- client.on('authStateChanged', (authenticated) => void 0);
763
- client.on('currentUserChanged', (user) => void 0); // CurrentUser | null; null on sign-out
764
- client.on('spaceAdded', (space) => void 0);
765
- client.on('spaceRemoved', (spaceId) => void 0);
766
- client.on('spaceRenamed', (spaceId, newName) => void 0);
767
- client.on('userStorageChanged', ({ key, value, source }) => void 0);
768
- client.on('connectionStateChanged', (state) => void 0);
769
- client.on('error', (error, context) => void 0);
770
- client.on('serverInfoChanged', (info) => void 0);
771
- client.on('unsupported', (info) => void 0); // SDK older than server minimum
772
- ```
574
+ **Documentation placeholder.**
773
575
 
774
- ## RoolSpace API
775
-
776
- Properties: `id`, `name`, `role`, `memberCount`, `openSpaceResult`, `route`, `webdav`.
777
-
778
- | Method | Description |
779
- | --- | --- |
780
- | `conversation(conversationId): ConversationHandle` | Get a stateless conversation-scoped handle. |
781
- | `getObject`, `getObjects` | Read object data. |
782
- | `listConversations`, `createConversation`, `deleteConversation` | Fetch, create, and delete conversations. |
783
- | `listAgents`, `deleteAgent` | List the space's agents and delete a custom agent. |
784
- | `readMeta`, `writeMeta`, `readSchema` | Read and write WebDAV-backed metadata and schema. |
785
- | `canUndo`, `canRedo`, `undo`, `redo` | Space history controls. |
786
- | `stopConversation(conversationId): Promise<boolean>` | Stop whatever is running in a conversation. |
787
- | `fetch(url, init?): Promise<Response>` | Proxy an external HTTP request through the server to bypass browser CORS. |
788
- | `close(): void` | Stop the space subscription. |
789
- | `rename(newName): Promise<void>` | Rename the space. |
790
- | `delete(): Promise<void>` | Permanently delete the space. |
791
- | `listUsers(): Promise<SpaceMember[]>` | List collaborators. |
792
- | `setUserRole(userId, role): Promise<void>` | Change an existing member's role. |
793
- | `removeUser(userId): Promise<void>` | Remove collaborator. |
794
- | `createInvite(role, options?): Promise<SpaceInviteCreated>` | Mint an invite link; `options` takes `email`, `expiresInDays`, `maxUses`. |
795
- | `listInvites(): Promise<SpaceInvite[]>` | List currently redeemable invites. |
796
- | `revokeInvite(inviteId): Promise<boolean>` | Revoke an invite so its link stops working. |
797
- | `exportArchive(): Promise<Blob>` | Export a space archive. |
798
- | `refresh(): Promise<OpenSpaceResult>` | Refresh identity, access, member count, and conversation metadata. |
799
- | `fetchPath(path, options?): Promise<Response>` | Fetch a `/rool-drive/...` file. |
800
-
801
- Events:
576
+ <a id="problem-invite_expired"></a>
802
577
 
803
- ```typescript
804
- space.on('conversationUpdated', ({ conversationId, conversation, source, timestamp }) => void 0);
805
- space.on('syncError', (error) => void 0);
806
- space.on('filesChanged', ({ spaceId, source, timestamp }) => void 0);
807
- space.on('filesReset', ({ spaceId, source, timestamp }) => void 0);
808
- space.on('connectionStateChanged', (state) => void 0);
809
- ```
810
- ## Import/Export
578
+ ### `invite_expired`
811
579
 
812
- ```typescript
813
- const archive = await space.exportArchive();
814
- const imported = await client.importArchive('Imported Data', archive);
815
- ```
580
+ **Documentation placeholder.**
816
581
 
817
- Archives include objects, metadata, conversations, and file storage.
582
+ <a id="problem-invite_revoked"></a>
818
583
 
819
- ## Data Types
584
+ ### `invite_revoked`
820
585
 
821
- ```typescript
822
- // Outcome of signInWithPassword. 'verify_required' means the account's email
823
- // isn't verified yet and the server has emailed a magic link.
824
- type PasswordSignInResult = { status: 'signed_in' | 'verify_required' };
825
-
826
- type FieldType =
827
- | { kind: 'string' }
828
- | { kind: 'number' }
829
- | { kind: 'boolean' }
830
- | { kind: 'array'; inner?: FieldType }
831
- | { kind: 'maybe'; inner: FieldType }
832
- | { kind: 'enum'; values: string[] }
833
- | { kind: 'literal'; value: string | number | boolean }
834
- | { kind: 'ref' };
835
-
836
- interface FieldDef {
837
- name: string;
838
- type: FieldType;
839
- }
586
+ **Documentation placeholder.**
840
587
 
841
- interface CollectionDef {
842
- fields: FieldDef[];
843
- schemaOrgType?: string;
844
- }
588
+ <a id="problem-invite_exhausted"></a>
845
589
 
846
- type SpaceSchema = Record<string, CollectionDef>;
590
+ ### `invite_exhausted`
847
591
 
848
- interface RoolObject {
849
- path: string;
850
- body: Record<string, unknown>;
851
- }
592
+ **Documentation placeholder.**
852
593
 
853
- interface GetObjectsResult {
854
- objects: RoolObject[];
855
- missing: string[];
856
- }
594
+ <a id="problem-invite_email_mismatch"></a>
857
595
 
858
- // Who may see a conversation: 'shared' is every space member; 'private' is
859
- // owner-only; 'temporary' is private plus auto-delete once it sits idle.
860
- type ConversationVisibility = 'shared' | 'private' | 'temporary';
861
-
862
- // Lightweight conversation roster entry (no interaction bodies), returned by
863
- // openSpace/listConversations. `updatedAt` drives last-activity display.
864
- interface ConversationMeta {
865
- id: string;
866
- agent: string; // owning agent ('rool' is the stock agent)
867
- visibility: ConversationVisibility;
868
- name: string | null;
869
- systemInstruction: string | null;
870
- createdAt: number;
871
- createdBy: string;
872
- interactionCount: number;
873
- updatedAt: number;
874
- }
596
+ ### `invite_email_mismatch`
875
597
 
876
- type InviteRole = 'admin' | 'editor' | 'viewer';
877
-
878
- interface SpaceInvite {
879
- inviteId: string;
880
- spaceId: string;
881
- role: InviteRole;
882
- email: string | null;
883
- createdBy: string;
884
- createdAt: string;
885
- expiresAt: string;
886
- maxUses: number | null;
887
- useCount: number;
888
- }
598
+ **Documentation placeholder.**
889
599
 
890
- // Outcome of the invite email send. Null when no email was involved (open link).
891
- // The invite is always minted and its `url` is usable regardless of this value;
892
- // only the email delivery is reflected here.
893
- // - 'sent': email dispatched
894
- // - 'not_configured': server has no mail provider (local dev)
895
- // - 'failed': provider rejected the send
896
- // - 'cooldown': a recent invite to this same address was already emailed
897
- // - 'rate_limited': the inviter hit their daily email-invite cap
898
- // Treat unknown values as not sent.
899
- type InviteEmailStatus =
900
- | 'sent'
901
- | 'not_configured'
902
- | 'failed'
903
- | 'cooldown'
904
- | 'rate_limited'
905
- | (string & {});
906
-
907
- interface SpaceInviteCreated {
908
- inviteId: string;
909
- spaceId: string;
910
- role: InviteRole;
911
- email: string | null;
912
- expiresAt: string;
913
- maxUses: number | null;
914
- url: string; // contains the secret token; only available at mint time
915
- emailStatus: InviteEmailStatus | null;
916
- }
600
+ <a id="problem-gift_invalid"></a>
917
601
 
918
- interface InvitePreview {
919
- spaceId: string;
920
- spaceName: string;
921
- role: InviteRole;
922
- email: string | null;
923
- inviterName: string | null;
924
- }
602
+ ### `gift_invalid`
925
603
 
926
- interface InviteRedeemResult {
927
- spaceId: string;
928
- role: RoolUserRole;
929
- status: 'joined' | 'upgraded' | 'already_member';
930
- }
604
+ The code is not valid, or the requested gift does not belong to the current user.
931
605
 
932
- type GiftPayload = { kind: 'credits'; credits: number };
933
-
934
- interface Gift {
935
- id: string;
936
- code: string; // display form, e.g. "K7M2-9QRT"
937
- url: string; // claim URL carrying the code
938
- gift: GiftPayload;
939
- description: string; // e.g. "10,000 AI credits"
940
- claimedAt: string | null; // null while unspent
941
- claimedByName: string | null; // claimer's display name, when they have one
942
- createdAt: string;
943
- note: string | null; // the holder's own reminder
944
- archivedAt: string | null; // set when hidden; the code still claims
945
- }
606
+ <a id="problem-gift_claimed"></a>
946
607
 
947
- interface GiftUpdate {
948
- note?: string | null; // undefined leaves as-is, null clears
949
- archived?: boolean;
950
- }
608
+ ### `gift_claimed`
951
609
 
952
- interface GiftList {
953
- gifts: Gift[];
954
- }
610
+ The gift has already been claimed. A claimed gift cannot be claimed again or given a new code.
955
611
 
956
- interface GiftPreview {
957
- holderName: string | null;
958
- gift: GiftPayload;
959
- description: string;
960
- }
612
+ <a id="problem-invalid_input"></a>
961
613
 
962
- interface GiftClaimResult {
963
- gift: GiftPayload;
964
- description: string;
965
- }
614
+ ### `invalid_input`
966
615
 
967
- type PromptAttachment =
968
- | File
969
- | Blob
970
- | { data: string; contentType: string; filename?: string }
971
- | string;
972
-
973
- type PromptEffort = 'QUICK' | 'STANDARD' | 'REASONING' | 'RESEARCH';
974
-
975
- interface PromptOptions {
976
- responseSchema?: Record<string, unknown>;
977
- effort?: PromptEffort;
978
- parentInteractionId?: string | null;
979
- ephemeral?: boolean;
980
- readOnly?: boolean;
981
- attachments?: PromptAttachment[];
982
- signal?: AbortSignal;
983
- eventName?: string;
984
- }
616
+ The gift update is empty or contains an invalid note or archived value.
985
617
 
986
- type InteractionStatus = 'pending' | 'streaming' | 'done' | 'error';
987
-
988
- interface Interaction {
989
- id: string;
990
- parentId: string | null;
991
- timestamp: number;
992
- userId: string;
993
- userName?: string | null;
994
- operation: 'prompt' | 'putObject' | 'patchObject' | 'moveObject' | 'deleteObjects' | 'deletePaths' | string;
995
- input: string;
996
- output: string | null;
997
- status: InteractionStatus;
998
- ai: boolean;
999
- modifiedObjectPaths: string[];
1000
- toolCalls: ToolCall[];
1001
- attachments?: string[];
1002
- }
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
1003
655
  ```
1004
656
 
1005
657
  ## License
1006
658
 
1007
- MIT - see [LICENSE](../../LICENSE) for details.
659
+ MIT see [LICENSE](../../LICENSE).