@rool-dev/sdk 0.10.2-dev.eeb9773 → 0.11.0-dev.59f195f

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,15 +1,14 @@
1
1
  # Rool SDK
2
2
 
3
- The TypeScript SDK for Rool, a persistent and collaborative environment for organizing objects.
3
+ TypeScript SDK for Rool, a persistent collaborative workspace for objects, AI-assisted editing, and per-space files.
4
4
 
5
- The SDK manages authentication, real-time synchronization, and per-space file storage. Core primitives:
5
+ Core primitives:
6
6
 
7
- - **Spaces** — Containers for objects, schema, metadata, channels, and files
8
- - **Channels** — Named contexts within a space. All object and AI operations go through a channel.
9
- - **Conversations** — Independent interaction histories within a channel.
10
- - **Objects** — Records addressed by a **location** path (`/space/<collection>/<basename>.json`). The body holds user-defined fields. References between objects are body fields whose values are location strings.
11
- - **AI operations** — Create, update, or query objects using natural language and `{{placeholders}}`
12
- - **File storage** — Every space has WebDAV file storage
7
+ - **Spaces** — containers for objects, schema, metadata, channels, collaborators, and files.
8
+ - **Channels** — named contexts within a space for object operations and AI conversations.
9
+ - **Conversations** — independent interaction histories within a channel.
10
+ - **Objects** — JSON records addressed by object paths such as `/space/article/welcome.json`.
11
+ - **Files** — user-visible files stored under `/rool-drive/...` through WebDAV.
13
12
 
14
13
  ## Installation
15
14
 
@@ -22,1174 +21,611 @@ npm install @rool-dev/sdk
22
21
  ```typescript
23
22
  import { RoolClient } from '@rool-dev/sdk';
24
23
 
25
- const client = new RoolClient();
26
- const authenticated = await client.initialize();
24
+ async function main() {
25
+ const client = new RoolClient();
27
26
 
28
- if (!authenticated) {
29
- client.login('My App'); // Redirects to auth page, shows "Sign in to My App"
30
- }
31
-
32
- // Create a new space, then open a channel on it
33
- const space = await client.createSpace('Solar System');
34
- const channel = await space.openChannel('main');
35
-
36
- // Define the schema — what types of objects exist and their fields
37
- await channel.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
- ]);
43
-
44
- // Create objects with AI-generated content using {{placeholders}}.
45
- // First arg is the collection, second is the body.
46
- const { object: sun } = await channel.createObject('body', {
47
- name: 'Sun',
48
- mass: '{{mass in solar masses}}',
49
- radius: '{{radius in km}}',
50
- }, { basename: 'sun' });
51
-
52
- const { object: earth } = await channel.createObject('body', {
53
- name: 'Earth',
54
- mass: '{{mass in Earth masses}}',
55
- radius: '{{radius in km}}',
56
- orbits: sun.location, // Reference to the sun via its location
57
- });
58
-
59
- // Use the AI agent to work with your data
60
- const { message, objects } = await channel.prompt(
61
- 'Add the other planets in our solar system, each referencing the Sun'
62
- );
63
- console.log(message); // AI explains what it did
64
- console.log(`Modified ${objects.length} objects`);
65
-
66
- // Read an object by location
67
- const loadedEarth = await channel.getObject(earth.location);
68
- console.log(loadedEarth?.body.name);
69
-
70
- // Clean up
71
- channel.close();
72
- ```
73
-
74
- ## Core Concepts
75
-
76
- ### Spaces and Channels
77
-
78
- A **space** is a container that holds objects, schema, metadata, channels, and files. A **channel** is a named context within a space — it's the handle you use for all object and AI operations. Each channel contains one or more **conversations**, each with independent interaction history.
79
-
80
- There are two main handles:
81
- - **`RoolSpace`** — Live handle with SSE subscription for user management, link access, channel management, file storage, export, and channel lifecycle events. Extends `EventEmitter`.
82
- - **`RoolChannel`** — Full real-time handle for objects, AI prompts, schema, and undo/redo.
83
-
84
- ```typescript
85
- // Open a space — live handle with SSE subscription
86
- const space = await client.openSpace('space-id');
87
- await space.addUser(userId, 'editor');
88
- await space.setLinkAccess('viewer');
89
-
90
- // React to channel changes in real-time
91
- space.on('channelCreated', (channel) => console.log('New channel:', channel.id));
92
- space.on('channelUpdated', (channel) => console.log('Updated:', channel.id));
93
- space.on('channelDeleted', (channelId) => console.log('Deleted:', channelId));
94
-
95
- // Open a channel for object and AI operations
96
- const channel = await space.openChannel('my-channel');
97
- await channel.prompt('Create some planets');
27
+ if (!(await client.initialize())) {
28
+ await client.login('My App');
29
+ // Browser auth redirects away. Run startup again after the auth callback.
30
+ return;
31
+ }
98
32
 
99
- // Open another channel on the same space
100
- const channel2 = await space.openChannel('research');
101
- await channel2.prompt('Analyze the data'); // Independent channel
33
+ const space = await client.createSpace('Solar System');
34
+ const channel = await space.openChannel('main');
102
35
 
103
- // Clean up — stops subscription and closes all open channels
104
- space.close();
105
- ```
36
+ await channel.createCollection('body', [
37
+ { name: 'name', type: { kind: 'string' } },
38
+ { name: 'mass', type: { kind: 'string' } },
39
+ { name: 'radius', type: { kind: 'string' } },
40
+ { name: 'orbits', type: { kind: 'maybe', inner: { kind: 'ref' } } },
41
+ ]);
106
42
 
107
- The `channelId` is fixed when you open a channel and cannot be changed. To use a different channel, open a new one. Channels to the same space share the same objects and schema.
43
+ const { object: sun } = await channel.putObject('/space/body/sun.json', {
44
+ name: 'Sun',
45
+ mass: '1 solar mass',
46
+ radius: '696,340 km',
47
+ });
108
48
 
109
- **Channel ID constraints:**
110
- - 1–32 characters
111
- - Only alphanumeric characters, hyphens (`-`), and underscores (`_`)
49
+ const { object: earth } = await channel.putObject('/space/body/earth.json', {
50
+ name: 'Earth',
51
+ mass: '1 Earth mass',
52
+ radius: '6,371 km',
53
+ orbits: sun.path,
54
+ });
112
55
 
113
- ### Conversations
56
+ const { message, objects } = await channel.prompt(
57
+ 'Add the other planets in our solar system, each referencing the Sun.'
58
+ );
114
59
 
115
- A **conversation** is a named interaction history within a channel. By default, all operations use the `'default'` conversation — most apps never need to think about conversations at all.
60
+ console.log(message);
61
+ console.log(`Modified ${objects.length} objects`);
116
62
 
117
- For apps that need multiple independent interaction threads (e.g., a chat sidebar with multiple threads), use `channel.conversation()` to get a handle scoped to a specific conversation:
63
+ const loadedEarth = await channel.getObject(earth.path);
64
+ console.log(loadedEarth?.body.name);
118
65
 
119
- ```typescript
120
- // Default conversation — most apps use this
121
- const space = await client.openSpace('space-id');
122
- const channel = await space.openChannel('main');
123
- await channel.prompt('Hello'); // Uses 'default' conversation
66
+ space.close();
67
+ }
124
68
 
125
- // Conversation handle — for multi-thread UIs
126
- const thread = channel.conversation('thread-42');
127
- await thread.prompt('Hello'); // Uses 'thread-42' conversation
128
- thread.getInteractions(); // Interactions for thread-42 only
69
+ void main();
129
70
  ```
130
71
 
131
- Each conversation has its own interaction history and optional system instruction. Conversations are auto-created on first interaction — no explicit create step needed. The 200-interaction cap applies per conversation. All conversations share one SSE connection per channel.
72
+ ## Paths and Resource URIs
132
73
 
133
- ```typescript
134
- // System instructions are per-conversation
135
- const thread = channel.conversation('research');
136
- await thread.setSystemInstruction('Respond in haiku');
137
-
138
- // List all conversations in this channel
139
- const conversations = channel.getConversations();
140
-
141
- // Delete a conversation (cannot delete 'default')
142
- await channel.deleteConversation('old-thread');
74
+ Most SDK methods take plain path strings:
143
75
 
144
- // Rename a conversation
145
- await thread.rename('Research Thread');
146
- ```
147
-
148
- ### Branching Conversations
76
+ - Object paths: `/space/<collection>/<name>.json` (exactly three segments; no dotfile collection or object names)
77
+ - File paths: `/rool-drive/<path/to/file>`
149
78
 
150
- The conversation history is a **tree**, not a flat list. Each interaction has a `parentId` pointing to the interaction it continues from. When you call `prompt()`, the SDK automatically continues from the current active leaf. To branch (edit/reroll), pass a different `parentInteractionId`:
79
+ `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.
151
80
 
152
81
  ```typescript
153
- const thread = channel.conversation('chat');
82
+ import { machinePath, machineUri, isObjectPath } from '@rool-dev/sdk';
154
83
 
155
- // Normal conversation — each prompt auto-continues from the last
156
- await thread.prompt('My favorite color is blue. Say OK.');
157
- await thread.prompt('What is my favorite color?'); // Sees "blue"
84
+ machinePath('rool-machine:/rool-drive/docs/read%20me.md');
85
+ // '/rool-drive/docs/read me.md'
158
86
 
159
- // Branch: go back to the first message and say something different
160
- const firstLeaf = thread.activeLeafId; // ID of the "blue" interaction
161
- const tree = thread.getTree();
162
- const firstInteractionId = tree[firstLeaf!].parentId!; // The root
87
+ machineUri('/space/article/welcome.json');
88
+ // 'rool-machine:/space/article/welcome.json'
163
89
 
164
- await thread.prompt('My favorite color is red. Say OK.', {
165
- parentInteractionId: firstInteractionId, // Sibling of "blue"
166
- });
167
- await thread.prompt('What is my favorite color?'); // Sees "red", not "blue"
168
-
169
- // Switch back to the blue branch
170
- thread.setActiveLeaf(firstLeaf!);
171
- thread.getInteractions(); // Returns the blue branch (root → leaf)
172
- ```
173
-
174
- **Key concepts:**
175
- - `getInteractions()` returns the active branch as a flat `Interaction[]` (root → leaf)
176
- - `getTree()` returns the full `Record<string, Interaction>` for branch navigation UI
177
- - `activeLeafId` is the tip of the branch the user is currently viewing
178
- - `setActiveLeaf(id)` switches branches (emits `conversationUpdated` so reactive UIs refresh)
179
- - `prompt()` with no `parentInteractionId` auto-continues from `activeLeafId`
180
- - `prompt()` with `parentInteractionId: null` starts a new root-level branch
181
-
182
- ### Objects, Locations, and References
183
-
184
- Every object lives at a **location** — a path of the form `/space/<collection>/<basename>.json`. The collection is the parent directory, the basename is the filename without `.json`, and together they fully identify the object.
185
-
186
- ```typescript
187
- {
188
- location: '/space/article/welcome.json',
189
- collection: 'article',
190
- basename: 'welcome',
191
- body: { title: 'Hello World', status: 'draft' },
192
- }
90
+ isObjectPath('/space/article/welcome.json'); // true
193
91
  ```
194
92
 
195
- The **body** holds the user-defined data.
196
-
197
- **References** between objects are body fields whose values are location strings:
93
+ Object APIs require full object paths. References between objects are ordinary body fields containing object paths:
198
94
 
199
95
  ```typescript
200
- // A planet references a star
201
96
  {
202
- location: '/space/body/earth.json',
203
- collection: 'body',
204
- basename: 'earth',
97
+ path: '/space/body/earth.json',
205
98
  body: { name: 'Earth', orbits: '/space/body/sun.json' },
206
99
  }
207
-
208
- // An array of references
209
- {
210
- location: '/space/team/alpha.json',
211
- collection: 'team',
212
- basename: 'alpha',
213
- body: {
214
- name: 'Alpha',
215
- members: [
216
- '/space/user/alice.json',
217
- '/space/user/bob.json',
218
- '/space/user/carol.json',
219
- ],
220
- },
221
- }
222
100
  ```
223
101
 
224
- References are just data — no special API is needed to create or remove them. Set a field to a location string to create a reference; clear it to remove it.
225
-
226
- #### Location helpers
227
-
228
- ```typescript
229
- import { loc, parseLocation, normalizeLocation } from '@rool-dev/sdk';
230
-
231
- loc('article', 'welcome'); // '/space/article/welcome.json'
232
- parseLocation('/space/article/welcome.json'); // { collection: 'article', basename: 'welcome' }
233
-
234
- // normalizeLocation accepts canonical or short form and returns canonical
235
- normalizeLocation('article/welcome'); // '/space/article/welcome.json'
236
- normalizeLocation('/space/article/welcome.json'); // unchanged
237
- ```
238
-
239
- SDK methods that accept a location (`getObject`, `updateObject`, `deleteObjects`, `moveObject`, etc.) accept either form and normalize internally. SDK return values always use the canonical full form.
240
-
241
- #### Machine resource links
242
-
243
- ```typescript
244
- import { resolveMachineResource } from '@rool-dev/sdk';
245
-
246
- const objectResource = resolveMachineResource('/space/article/welcome.json');
247
- // { kind: 'object', path: '/space/article/welcome.json' }
248
-
249
- const fileResource = resolveMachineResource('/rool-drive/docs/readme.md');
250
- // { kind: 'file', path: '/rool-drive/docs/readme.md' }
251
- ```
252
-
253
- `rool-machine:` is the canonical URI scheme for user-visible resources from the Rool machine filesystem. `resolveMachineResource()` accepts either canonical `rool-machine:/...` URIs or bare machine paths such as `/rool-drive/...`, and returns the resource kind plus machine path. Fetch file resources through `space.fetchMachineResource(resource)`.
102
+ ## Authentication
254
103
 
255
- ### AI Placeholder Pattern
104
+ ### Browser
256
105
 
257
- Use `{{description}}` in body field values to have AI generate content:
106
+ The default auth provider stores tokens in browser storage and redirects to the Rool auth page.
258
107
 
259
108
  ```typescript
260
- // Create with AI-generated content
261
- await channel.createObject('article', {
262
- headline: '{{catchy headline about coffee}}',
263
- body: '{{informative paragraph}}',
264
- });
265
-
266
- // Update existing content with AI
267
- await channel.updateObject('/space/article/welcome.json', {
268
- prompt: 'Make the body shorter and more casual'
269
- });
270
-
271
- // Add new AI-generated field to existing object
272
- await channel.updateObject('/space/article/welcome.json', {
273
- data: { summary: '{{one-sentence summary}}' }
274
- });
275
- ```
276
-
277
- When resolving placeholders, the agent has access to the full body and the surrounding space context (except for `_`-prefixed fields). Placeholders are instructions, not templates, and do not need to repeat information already present in other fields.
278
-
279
- Placeholders are resolved by the AI during the mutation and replaced with concrete values. The `{{...}}` syntax is never stored — it only guides the agent while creating or updating the object.
109
+ async function start() {
110
+ const client = new RoolClient();
280
111
 
281
- ### Checkpoints & Undo/Redo
282
-
283
- Undo/redo works on **checkpoints**, not individual operations. Call `checkpoint()` before making changes to create a restore point. Each checkpoint stores a snapshot of the entire space.
284
-
285
- ```typescript
286
- // Create a checkpoint before user action
287
- await channel.checkpoint('Delete object');
288
- await channel.deleteObjects([location]);
112
+ if (!(await client.initialize())) {
113
+ await client.login('My App');
114
+ // Browser auth redirects; stop startup until the callback reloads the app.
115
+ return;
116
+ }
289
117
 
290
- // User can now undo back to the checkpoint
291
- if (await channel.canUndo()) {
292
- await channel.undo(); // Restores the deleted object
118
+ // Use the authenticated client here.
293
119
  }
294
120
 
295
- // Redo reapplies the undone action
296
- if (await channel.canRedo()) {
297
- await channel.redo(); // Deletes the object again
298
- }
121
+ void start();
299
122
  ```
300
123
 
301
- Checkpoints are **space-wide**: one shared stack across all channels and users. `undo()` restores the entire space — including any work others did since the checkpoint. Stacks are capped at 25 entries; identical-content checkpoints are deduped; a new checkpoint clears the redo stack.
302
-
303
- ### Hidden Fields
124
+ ### Node.js
304
125
 
305
- Body fields starting with `_` (e.g., `_ui`, `_cache`) are hidden from AI and ignored by the schema — you can add them to any object regardless of its collection definition. Otherwise they behave like normal fields: they sync in real-time, persist to the server, support undo/redo, and are visible to all users of the space. Use them for UI state, positions, or other data the AI shouldn't see or modify:
126
+ 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.
306
127
 
307
128
  ```typescript
308
- await channel.createObject('article', {
309
- title: 'My Article',
310
- author: 'John Doe',
311
- _ui: { x: 100, y: 200, collapsed: false }
312
- });
313
- ```
314
-
315
- ### Real-time Sync
316
-
317
- Object and file reactivity is WebDAV-based. Listen for space-level file change notifications, then reconcile with `webdav.syncCollection()` using your sync token. This covers both object files under `/space` and user files under `/rool-drive`.
129
+ import { RoolClient } from '@rool-dev/sdk';
130
+ import { NodeAuthProvider } from '@rool-dev/sdk/node';
318
131
 
319
- ```typescript
320
- let token: string | null = null;
132
+ const client = new RoolClient({ authProvider: new NodeAuthProvider() });
133
+ let authenticated = await client.initialize();
321
134
 
322
- async function syncFiles() {
323
- const result = await space.webdav.syncCollection('/', {
324
- token,
325
- level: 'infinite',
326
- props: ['displayname', 'getetag', 'getlastmodified', 'resourcetype'],
327
- });
328
- token = result.token;
329
- updateFileTree(result.responses);
135
+ if (!authenticated) {
136
+ await client.login('My CLI Tool');
137
+ // Re-run initialize after the non-redirect login to hydrate currentUser,
138
+ // user storage, and client-level event subscriptions.
139
+ authenticated = await client.initialize();
330
140
  }
331
141
 
332
- space.on('filesChanged', syncFiles);
333
- space.on('filesReset', () => { token = null; syncFiles(); });
334
- await syncFiles();
142
+ if (!authenticated) throw new Error('Login required');
335
143
  ```
336
144
 
337
- ### Locations & Basenames
145
+ ### Auth API
338
146
 
339
- By default, `createObject` mints a 6-character alphanumeric basename. Provide your own via `options.basename` for meaningful identifiers:
147
+ | Method | Description |
148
+ | --- | --- |
149
+ | `initialize(): Promise<boolean>` | Call on startup. Initializes auth, refreshes user/storage state, and starts client events when authenticated. |
150
+ | `login(appName, params?): Promise<void>` | Start login flow. |
151
+ | `signup(appName, params?): Promise<void>` | Start signup flow. |
152
+ | `verify(token): Promise<boolean>` | Complete email verification token flow; returns `false` when the active auth provider does not implement verification. |
153
+ | `logout(): void` | Clear auth state and close open spaces. |
154
+ | `isAuthenticated(): Promise<boolean>` | Validate current auth state. |
155
+ | `getAuthUser(): AuthUser` | Return auth identity decoded from the token. |
156
+ | `setPassword(password): Promise<void>` | Set/change password for the current user. |
340
157
 
341
- ```typescript
342
- await channel.createObject('article',
343
- { title: 'The Meaning of Life' },
344
- { basename: 'meaning-of-life' },
345
- );
346
- // → location: /space/article/meaning-of-life.json
347
- ```
158
+ ## Spaces and Channels
348
159
 
349
- **Why pin a basename?**
350
- - **Fire-and-forget creation** — Know the location immediately without awaiting the response.
351
- - **Meaningful identifiers** — Use domain-specific names like `welcome` or `2026-budget` for easier debugging and external references.
160
+ Open a space to receive live events and manage collaborators, file storage, and channels. Open a channel to work with objects, schema, metadata, and AI.
352
161
 
353
162
  ```typescript
354
- // Fire-and-forget: create and reference without waiting
355
- const basename = 'idea-seed';
356
- const location = loc('note', basename);
357
-
358
- channel.createObject('note', { text: '{{expand this idea}}' }, { basename });
359
- channel.updateObject(parentLocation, {
360
- data: { notes: [...existingNotes, location] },
361
- }); // Add reference immediately
362
- ```
163
+ const space = await client.openSpace('space-id');
363
164
 
364
- **Basename constraints:**
365
- - Must start with an alphanumeric character.
366
- - Other characters may be alphanumeric, hyphens (`-`), or underscores (`_`).
367
- - Must be unique within its collection (throws if the location already exists).
165
+ space.on('channelCreated', (channel) => console.log(channel.id));
166
+ space.on('filesChanged', () => console.log('files changed'));
368
167
 
369
- Use `moveObject` to rename an object or move it to a different collection — see [Moving and Renaming](#moving-and-renaming).
168
+ const channel = await space.openChannel('main');
169
+ await channel.prompt('Summarize this space');
170
+ ```
370
171
 
371
- ## Authentication
172
+ Channel IDs must be 1–32 characters and contain only letters, numbers, `_`, and `-`.
372
173
 
373
- ### Browser (Default)
174
+ ## Object Operations
374
175
 
375
- No configuration needed. Uses localStorage for tokens, redirects to login page.
176
+ Objects are JSON files under `/space`. Create the collection before writing objects in it.
376
177
 
377
178
  ```typescript
378
- const client = new RoolClient();
379
- const authenticated = await client.initialize();
380
-
381
- if (!authenticated) {
382
- client.login('My App'); // Redirect to the auth page
383
- }
384
- ```
179
+ await channel.createCollection('article', [
180
+ { name: 'title', type: { kind: 'string' } },
181
+ { name: 'status', type: { kind: 'string' } },
182
+ ]);
385
183
 
386
- ### Node.js
184
+ // Create or replace an exact object path
185
+ const { object } = await channel.putObject('/space/article/welcome.json', {
186
+ title: 'Welcome',
187
+ status: 'draft',
188
+ });
387
189
 
388
- For CLI tools and scripts. Stores credentials in `~/.config/rool/`, opens browser for login.
190
+ // Patch fields; null or undefined deletes a field
191
+ await channel.patchObject(object.path, {
192
+ data: { status: 'published', obsoleteField: null },
193
+ });
389
194
 
390
- ```typescript
391
- import { NodeAuthProvider } from '@rool-dev/sdk/node';
195
+ // Read one or many objects
196
+ await channel.getObject('/space/article/welcome.json');
197
+ await channel.getObjects([
198
+ '/space/article/welcome.json',
199
+ '/space/article/intro.json',
200
+ ]);
392
201
 
393
- const client = new RoolClient({ authProvider: new NodeAuthProvider() });
394
- const authenticated = await client.initialize();
202
+ // Rename or move an object
203
+ await channel.moveObject(
204
+ '/space/article/welcome.json',
205
+ '/space/article/hello-world.json'
206
+ );
395
207
 
396
- if (!authenticated) {
397
- await client.login('My CLI Tool'); // Opens browser, waits for callback
398
- }
208
+ // Delete objects
209
+ await channel.deleteObjects(['/space/article/hello-world.json']);
399
210
  ```
400
211
 
401
- ### Auth Methods
402
-
403
212
  | Method | Description |
404
- |--------|-------------|
405
- | `initialize(): Promise<boolean>` | **Call on app startup.** Processes auth callback from URL, sets up token refresh, returns auth state. Returns `false` if not authenticated. Throws if authenticated but account fetch fails (e.g. network error or invalid token). |
406
- | `login(appName, params?): void` | Redirect to login page. The app name is displayed on the auth page ("Sign in to {appName}"). Optional `params` are added as query parameters to the auth URL. |
407
- | `signup(appName, params?): void` | Redirect to signup page. The app name is displayed on the auth page ("Sign up for {appName}"). Optional `params` are added as query parameters to the auth URL. |
408
- | `verify(token): Promise<boolean>` | Sign in using a verification token (from a `?verify=<token>` email link). Used by the official Rool app most integrations won't need this. |
409
- | `logout(): void` | Clear tokens and state |
410
- | `isAuthenticated(): Promise<boolean>` | Check auth status (validates token) |
411
- | `getAuthUser(): AuthUser` | Get auth identity from JWT (`{ email, name }`) |
412
- | `setPassword(password): Promise<void>` | Set or change the current user's password. Requires an authenticated session. Password must be at least 8 characters and contain both letters and either digits or symbols. Throws with a human-readable message on validation or server failure. |
213
+ | --- | --- |
214
+ | `getObject(path): Promise<RoolObject | undefined>` | Fetch one object by object path. |
215
+ | `getObjects(paths): Promise<GetObjectsResult>` | Fetch objects in bulk; returns `objects` and `missing`. |
216
+ | `stat(path): RoolObjectStat | undefined` | Cached audit info for an object. |
217
+ | `putObject(path, body): Promise<{ object, message }>` | Create or replace an object at an exact path. |
218
+ | `patchObject(path, { data }): Promise<{ object, message }>` | Patch an object's body; `null`/`undefined` deletes fields. |
219
+ | `moveObject(from, to, options?): Promise<{ object, message }>` | Rename or relocate an object; `options.body` can replace the body after moving. |
220
+ | `deleteObjects(paths): Promise<void>` | Delete object files. |
413
221
 
414
222
  ## AI Agent
415
223
 
416
- The `prompt()` method is the primary way to invoke the AI agent. The agent has editor-level capabilities it can create, modify, move, and delete objects but cannot see or modify `_`-prefixed fields.
224
+ `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.
417
225
 
418
226
  ```typescript
419
227
  const { message, objects } = await channel.prompt(
420
- "Create a topic node for the solar system, then child nodes for each planet."
228
+ 'Create a topic node for the solar system, then child nodes for each planet.'
421
229
  );
422
- console.log(`AI: ${message}`);
423
- console.log(`Modified ${objects.length} objects:`, objects);
424
- ```
425
-
426
- Use `checkpoint()` before prompting to make operations undoable.
427
230
 
428
- ### Method Signature
429
-
430
- ```typescript
431
- prompt(text: string, options?: PromptOptions): Promise<{ message: string; objects: RoolObject[] }>
231
+ console.log(message);
232
+ console.log(objects.map((object) => object.path));
432
233
  ```
433
234
 
434
- Returns a message (the AI's response) and the list of objects that were created or modified.
435
-
436
- ### Options
235
+ ### Prompt Options
437
236
 
438
237
  | Option | Description |
439
- |--------|-------------|
440
- | `responseSchema` | Request structured JSON instead of text summary |
441
- | `effort` | Effort level: `'QUICK'`, `'STANDARD'` (default), `'REASONING'`, or `'RESEARCH'` |
442
- | `ephemeral` | If true, don't record in interaction history (useful for tab completion) |
443
- | `readOnly` | If true, disable mutation tools (create, update, move, delete). Use for questions. |
444
- | `parentInteractionId` | Parent interaction in the conversation tree. Omit to auto-continue from the active leaf. Pass `null` to start a new root-level branch. Pass a specific ID to branch from that point (edit/reroll). |
445
- | `attachments` | Machine resources to focus the AI on, plus local files to upload (`File`, `Blob`, or `{ data, contentType, filename? }`). Pass object resources (`/space/...`) for object context and file resources (`/rool-drive/...`) for existing WebDAV files/folders. Local files are uploaded to authenticated space file storage first. The interaction stores canonical `rool-machine:/...` refs for UI rendering. The AI can interpret images (JPEG, PNG, GIF, WebP, SVG), PDFs, text-based files (plain text, Markdown, CSV, HTML, XML, JSON), and DOCX documents. Other file types are stored but the AI cannot natively consume their contents, only use shell tools on them. |
446
- | `signal` | `AbortSignal` to stop the prompt mid-flight. When aborted, the agent loop halts and the streaming response closes. Note that any LLM turn already in flight on Vertex keeps generating server-side and is billed. |
447
-
448
- ### Effort Levels
449
-
450
- | Level | Description |
451
- |-------|-------------|
452
- | `QUICK` | Fast, lightweight model. Best for simple questions. |
453
- | `STANDARD` | Default behavior with balanced capabilities. |
454
- | `REASONING` | Extended reasoning for complex tasks. |
455
- | `RESEARCH` | Most thorough mode with deep analysis. Slowest and most credit-intensive. |
456
-
457
- ### Examples
458
-
459
- ```typescript
460
- // Reorganize existing objects
461
- const { objects } = await channel.prompt(
462
- "Group these notes by topic and create a parent node for each group."
463
- );
464
-
465
- // Work with specific objects
466
- const intro = resolveMachineResource('/space/article/intro.json');
467
- const conclusion = resolveMachineResource('/space/article/conclusion.json');
468
- if (!intro || !conclusion) throw new Error('invalid resource');
469
- const result = await channel.prompt(
470
- "Summarize these articles",
471
- { attachments: [intro, conclusion] }
472
- );
473
-
474
- // Quick question without mutations (fast model + read-only)
475
- const { message } = await channel.prompt(
476
- "What topics are covered?",
477
- { effort: 'QUICK', readOnly: true }
478
- );
479
-
480
- // Complex analysis with extended reasoning
481
- await channel.prompt(
482
- "Analyze relationships and reorganize",
483
- { effort: 'REASONING' }
484
- );
485
-
486
- // Attach existing WebDAV files/folders or local uploads
487
- const report = resolveMachineResource('/rool-drive/docs/report.pdf');
488
- if (!report) throw new Error('invalid resource');
489
- const file = fileInput.files[0]; // from <input type="file">
490
- await channel.prompt(
491
- "Compare this report with the uploaded photo",
492
- { attachments: [report, file] }
493
- );
494
-
495
- // Cancel a long-running prompt
496
- const ac = new AbortController();
497
- cancelButton.onclick = () => ac.abort();
498
- await channel.prompt("Do a deep analysis...", {
499
- effort: 'RESEARCH',
500
- signal: ac.signal,
238
+ | --- | --- |
239
+ | `responseSchema` | Request structured JSON text matching a JSON-schema-like shape. |
240
+ | `effort` | `'QUICK'` (fast/read-only), `'STANDARD'` (default), `'REASONING'`, or `'RESEARCH'`. |
241
+ | `ephemeral` | Do not record the prompt in interaction history. |
242
+ | `readOnly` | Disable mutation tools. |
243
+ | `parentInteractionId` | Conversation-tree parent. Omit to continue from the active leaf; pass `null` for a new root branch. |
244
+ | `attachments` | Existing object/file paths or `rool-machine:/...` URIs, plus local files (`File`, `Blob`, or `{ data, contentType, filename? }`). |
245
+ | `signal` | AbortSignal used to request that the server stop an in-flight prompt. |
246
+ | `eventName` | Optional telemetry event name. Defaults to `'prompt_user'`. |
247
+
248
+ ```typescript
249
+ // Read-only quick question
250
+ await channel.prompt('What topics are covered?', {
251
+ effort: 'QUICK', // fast/read-only
501
252
  });
502
- ```
503
-
504
- ### Structured Responses
505
253
 
506
- Use `responseSchema` to get structured JSON instead of a text message:
254
+ // Focus on existing objects and files
255
+ await channel.prompt('Compare these resources', {
256
+ attachments: [
257
+ '/space/article/intro.json',
258
+ 'rool-machine:/rool-drive/docs/report.pdf',
259
+ ],
260
+ });
507
261
 
508
- ```typescript
509
- const resources = [
510
- '/space/item/widget.json',
511
- '/space/item/gadget.json',
512
- '/space/item/gizmo.json',
513
- ].map((path) => {
514
- const resource = resolveMachineResource(path);
515
- if (!resource) throw new Error(`invalid resource: ${path}`);
516
- return resource;
262
+ // Upload a local file as an attachment
263
+ await channel.prompt('Describe this image', {
264
+ attachments: [fileInput.files![0]],
517
265
  });
518
266
 
519
- const { message } = await channel.prompt("Categorize these items", {
520
- attachments: resources,
267
+ // Structured response
268
+ const { message } = await channel.prompt('Categorize these items', {
521
269
  responseSchema: {
522
270
  type: 'object',
523
271
  properties: {
524
- categories: {
525
- type: 'array',
526
- items: { type: 'string' }
527
- },
528
- summary: { type: 'string' }
529
- }
530
- }
272
+ categories: { type: 'array', items: { type: 'string' } },
273
+ summary: { type: 'string' },
274
+ },
275
+ },
531
276
  });
532
-
533
277
  const result = JSON.parse(message);
534
- console.log(result.categories, result.summary);
535
- ```
536
-
537
- ### Context Flow
538
-
539
- AI operations automatically receive context:
540
- - **Interaction history** — Previous interactions and their results from this channel
541
- - **Recently modified objects** — Objects created or changed recently
542
- - **Attached resources** — Object resources passed via `attachments` are given primary focus; file resources are surfaced as `/rool-drive/...` paths
543
-
544
- This context flows automatically — no configuration needed. The AI sees enough history to maintain coherent interactions while respecting the `_`-prefixed field hiding rules.
545
-
546
- ## Collaboration
547
-
548
- ### Adding Users to a Space
549
-
550
- To add a user to a space, you need their user ID. Use `searchUser()` to find them by email:
551
-
552
- ```typescript
553
- // Find the user by email
554
- const user = await client.searchUser('colleague@example.com');
555
- if (!user) {
556
- throw new Error('User not found');
557
- }
558
-
559
- // Add them to the space
560
- const space = await client.openSpace('space-id');
561
- await space.addUser(user.id, 'editor');
562
- ```
563
-
564
- ### Roles
565
-
566
- | Role | Capabilities |
567
- |------|--------------|
568
- | `owner` | Full control, can delete space and manage all users |
569
- | `admin` | All editor capabilities, plus can manage users (except other admins/owners) |
570
- | `editor` | Can create, modify, move, and delete objects |
571
- | `viewer` | Read-only access (can query with `prompt` and read objects/files) |
572
-
573
- ### Space Collaboration Methods
574
-
575
- These methods are available on `RoolSpace`:
576
-
577
- | Method | Description |
578
- |--------|-------------|
579
- | `listUsers(): Promise<SpaceMember[]>` | List users with access |
580
- | `addUser(userId, role): Promise<void>` | Add user to space (requires owner or admin role) |
581
- | `removeUser(userId): Promise<void>` | Remove user from space (requires owner or admin role) |
582
- | `setLinkAccess(linkAccess): Promise<void>` | Set URL sharing level (requires owner or admin role) |
583
-
584
- ### URL Sharing
585
-
586
- Enable public URL access to allow anyone with the space URL to access it:
587
-
588
- ```typescript
589
- const space = await client.openSpace('space-id');
590
-
591
- // Allow anyone with the URL to view
592
- await space.setLinkAccess('viewer');
593
-
594
- // Allow anyone with the URL to edit
595
- await space.setLinkAccess('editor');
596
-
597
- // Disable URL access (default)
598
- await space.setLinkAccess('none');
599
-
600
- // Check current setting
601
- console.log(space.linkAccess); // 'none' | 'viewer' | 'editor'
602
- ```
603
-
604
- When a user accesses a space via URL, they're granted the corresponding role (`viewer` or `editor`) based on the space's `linkAccess` setting.
605
-
606
- ### Client User Methods
607
-
608
- | Method | Description |
609
- |--------|-------------|
610
- | `currentUser: CurrentUser \| null` | Cached user profile from `initialize()`. Use for sync access to user info (id, email, name, etc.). Returns `null` before `initialize()` is called. |
611
- | `getCurrentUser(): Promise<CurrentUser>` | Fetch fresh user profile from server (id, email, name, photoUrl, slug, plan, creditsBalance, totalCreditsUsed, createdAt, lastActivity, processedAt, storage) |
612
- | `updateCurrentUser(input): Promise<CurrentUser>` | Update the current user's profile (`name`, `slug`). Returns the updated user. Slug must be 3–32 chars, start with a letter, and contain only lowercase alphanumeric characters, hyphens, and underscores. |
613
- | `deleteCurrentUser(): Promise<void>` | Mark the current user's account for deletion (10-minute grace period before irreversible). Logs out the client. |
614
- | `searchUser(email): Promise<UserResult \| null>` | Find user by exact email address (no partial matching) |
615
-
616
- ### Real-time Collaboration
617
-
618
- When multiple users have a space open, object and file changes are announced by `space.on('filesChanged')` and reconciled through WebDAV `syncCollection()`. Channel/conversation state still emits channel events; filesystem state does not use channel object events.
619
278
 
620
- See [Real-time Sync](#real-time-sync) for a WebDAV sync-token example.
621
-
622
- ## RoolClient API
623
-
624
- ### Logging
625
-
626
- By default the SDK logs errors to the console. Pass a `logger` to see more or customize output:
627
-
628
- ```typescript
629
- // Default — errors only
630
- const client = new RoolClient();
631
-
632
- // Log everything to console
633
- const client = new RoolClient({ logger: console });
634
-
635
- // Bring your own logger (pino, winston, etc.)
636
- const client = new RoolClient({
637
- logger: myLogger // any object with { debug, info, warn, error }
279
+ // Stop a long prompt with a signal (when the caller holds the controller)
280
+ const ac = new AbortController();
281
+ const promptPromise = channel.prompt('Do a deep analysis', {
282
+ effort: 'RESEARCH',
283
+ signal: ac.signal,
638
284
  });
285
+ ac.abort(); // asks the server to stop the in-flight interaction
286
+ await promptPromise;
639
287
  ```
640
288
 
641
- ### Space & Channel Lifecycle
289
+ ### Stopping interactions
642
290
 
643
- | Method | Description |
644
- |--------|-------------|
645
- | `listSpaces(): Promise<RoolSpaceInfo[]>` | List available spaces |
646
- | `openSpace(spaceId): Promise<RoolSpace>` | Open a space with live SSE subscription. Caches and reuses open spaces. Call `space.openChannel(channelId)` to get a channel. |
647
- | `createSpace(name): Promise<RoolSpace>` | Create a new space, returns live handle with SSE subscription |
648
- | `duplicateSpace(sourceSpaceId, name): Promise<RoolSpace>` | Duplicate an existing space. Returns a handle to the new space. |
649
- | `deleteSpace(id): Promise<void>` | Permanently delete a space (cannot be undone) |
650
- | `importArchive(name, archive): Promise<RoolSpace>` | Import from a zip archive, creating a new space |
651
-
652
- ### Channel Management
653
-
654
- Manage channels on the `RoolSpace` handle:
655
-
656
- | Method | Description |
657
- |--------|-------------|
658
- | `space.channels: ChannelInfo[]` | Live channel list (auto-updates via SSE) |
659
- | `space.renameChannel(channelId, name): Promise<void>` | Rename a channel |
660
- | `space.deleteChannel(channelId): Promise<void>` | Delete a channel and its interaction history |
661
- | `channel.rename(name): Promise<void>` | Rename the current open channel |
662
-
663
- ### User Storage
664
-
665
- Server-side key-value storage for user preferences, UI state, and other persistent data. Replaces browser localStorage with cross-device, server-synced storage.
666
-
667
- **Features:**
668
- - Fresh data fetched from server on `initialize()` — cache is authoritative after init
669
- - Sync reads from local cache (fast, no network round-trip)
670
- - Automatic sync to server and across tabs/devices via SSE
671
- - `userStorageChanged` event fires on all changes (local or remote)
672
- - Total storage limited to 10MB per user
673
-
674
- | Method | Description |
675
- |--------|-------------|
676
- | `getUserStorage<T>(key): T \| undefined` | Get a value (sync, from cache) |
677
- | `setUserStorage(key, value): void` | Set a value (updates cache, syncs to server) |
678
- | `getAllUserStorage(): Record<string, unknown>` | Get all stored data (sync, from cache) |
291
+ Use `signal` when the same call site cancels the prompt. When the Stop button
292
+ lives elsewhere — a different component, after a reload, or a prompt another
293
+ client started stop by ID or stop the conversation's active interaction
294
+ instead. Both are best-effort: the server halts the agent loop and closes the
295
+ stream, but an LLM turn already in flight keeps generating server-side and is
296
+ billed.
679
297
 
680
298
  ```typescript
681
- // After initialize(), storage is fresh from server
682
- const authenticated = await client.initialize();
299
+ // Stop whatever is in flight on this channel's (default) conversation.
300
+ // No-op returning false when nothing is running.
301
+ await channel.stop();
683
302
 
684
- // Sync reads are now trustworthy
685
- const theme = client.getUserStorage<string>('theme');
686
- applyTheme(theme ?? 'light');
303
+ // Stop a specific interaction by ID (e.g. from channel.activeLeafId or
304
+ // the interactions list). Returns whether the server stopped it.
305
+ await channel.stopInteraction(channel.activeLeafId!);
687
306
 
688
- // Write - updates immediately, syncs to server in background
689
- client.setUserStorage('theme', 'dark');
690
- client.setUserStorage('sidebar', { collapsed: true, width: 280 });
691
-
692
- // Delete a key
693
- client.setUserStorage('theme', null);
694
-
695
- // Listen for changes from other tabs/devices
696
- client.on('userStorageChanged', ({ key, value, source }) => {
697
- // source: 'local' (this client) or 'remote' (server/other client)
698
- if (key === 'theme') applyTheme(value as string);
699
- });
307
+ // Conversation handles stop their own in-flight interaction.
308
+ const thread = channel.conversation('thread-42');
309
+ await thread.stop();
700
310
  ```
701
311
 
702
- ### Utilities
703
-
704
312
  | Method | Description |
705
- |--------|-------------|
706
- | `RoolClient.generateId(): string` | Generate a unique 6-character alphanumeric ID. |
707
- | `destroy(): void` | Clean up resources |
313
+ | --- | --- |
314
+ | `stop(): Promise<boolean>` | Stop the in-flight interaction on the default conversation; `false` if none. |
315
+ | `stopInteraction(id): Promise<boolean>` | Ask the server to stop a specific interaction by ID. |
316
+ | `conversation.stop(): Promise<boolean>` | Stop a specific conversation's in-flight interaction. |
708
317
 
709
- ### Client Events
318
+ ## Conversations
710
319
 
711
- ```typescript
712
- client.on('authStateChanged', (authenticated: boolean) => void)
713
- client.on('spaceAdded', (space: RoolSpaceInfo) => void) // Space created or access granted
714
- client.on('spaceRemoved', (spaceId: string) => void) // Space deleted or access revoked
715
- client.on('spaceRenamed', (spaceId: string, newName: string) => void)
716
- client.on('channelCreated', (spaceId: string, channel: ChannelInfo) => void)
717
- client.on('channelUpdated', (spaceId: string, channel: ChannelInfo) => void)
718
- client.on('channelDeleted', (spaceId: string, channelId: string) => void)
719
- client.on('userStorageChanged', ({ key, value, source }: UserStorageChangedEvent) => void)
720
- client.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)
721
- client.on('error', (error: Error, context?: string) => void)
722
- ```
320
+ Every channel has a default conversation. Use `channel.conversation(id)` for independent histories (for example, multiple chat threads). Conversations are represented as trees: interactions point at a `parentId`, and the SDK tracks an active leaf for each conversation.
723
321
 
724
- Channel events on the client (`channelCreated`, `channelUpdated`, `channelDeleted`) are pass-throughs from space events for backwards compatibility. Prefer listening on the space handle directly for new code.
725
-
726
- **Space list management pattern:**
727
322
  ```typescript
728
- const spaces = new Map<string, RoolSpaceInfo>();
729
-
730
- client.on('spaceAdded', (space) => spaces.set(space.id, space));
731
- client.on('spaceRemoved', (id) => spaces.delete(id));
732
- client.on('spaceRenamed', (id, name) => {
733
- const space = spaces.get(id);
734
- if (space) spaces.set(id, { ...space, name });
735
- });
736
- ```
737
-
738
- ## RoolSpace API
739
-
740
- A space handle with a live SSE subscription. Extends `EventEmitter`. Manages user access, link sharing, channels, file storage, and export. The `channels` property auto-updates via SSE, and channel lifecycle events fire in real-time.
741
-
742
- `openSpace()` caches and reuses open spaces — calling it twice with the same ID returns the same instance. Call `close()` when done to stop the subscription and close all open channels.
743
-
744
- ### Properties
323
+ await channel.prompt('Hello'); // default conversation
745
324
 
746
- | Property | Description |
747
- |----------|-------------|
748
- | `id: string` | Space ID |
749
- | `name: string` | Space name |
750
- | `role: RoolUserRole` | User's role |
751
- | `linkAccess: LinkAccess` | URL sharing level |
752
- | `memberCount: number` | Number of users with access to the space |
753
- | `channels: ChannelInfo[]` | Live channel list (auto-updates via SSE) |
754
- | `webdav: RoolWebDAV` | WebDAV client for this space's file storage |
755
-
756
- ### Methods
325
+ const thread = channel.conversation('thread-42');
326
+ await thread.prompt('Hello from another thread');
327
+ await thread.setSystemInstruction('Answer in haiku');
757
328
 
758
- | Method | Description |
759
- |--------|-------------|
760
- | `openChannel(channelId): Promise<RoolChannel>` | Open a channel on this space |
761
- | `close(): void` | Stop SSE subscription and close all open channels |
762
- | `rename(newName): Promise<void>` | Rename this space |
763
- | `delete(): Promise<void>` | Permanently delete this space |
764
- | `listUsers(): Promise<SpaceMember[]>` | List users with access |
765
- | `addUser(userId, role): Promise<void>` | Add user to space |
766
- | `removeUser(userId): Promise<void>` | Remove user from space |
767
- | `setLinkAccess(linkAccess): Promise<void>` | Set URL sharing level |
768
- | `renameChannel(channelId, name): Promise<void>` | Rename a channel |
769
- | `deleteChannel(channelId): Promise<void>` | Delete a channel |
770
- | `exportArchive(): Promise<Blob>` | Export space as zip archive |
771
- | `getStorageUsage(): Promise<SpaceFileStorageUsage>` | Get WebDAV quota usage for this space |
772
- | `fetchMachineResource(resource): Promise<Response>` | Fetch a resolved file `MachineResource` through this space |
773
- | `refresh(): Promise<void>` | Refresh space data from server |
774
-
775
- ### Space Events
329
+ const branch = thread.getInteractions(); // active branch, root → leaf
330
+ const tree = thread.getTree(); // full interaction tree
776
331
 
777
- ```typescript
778
- space.on('channelCreated', (channel: ChannelInfo) => void) // New channel added
779
- space.on('channelUpdated', (channel: ChannelInfo) => void) // Channel metadata changed
780
- space.on('channelDeleted', (channelId: string) => void) // Channel removed
781
- space.on('filesChanged', ({ source, timestamp }) => void) // WebDAV file storage changed; call webdav.syncCollection()
782
- space.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)
332
+ if (thread.activeLeafId) {
333
+ thread.setActiveLeaf(thread.activeLeafId);
334
+ }
783
335
  ```
784
336
 
785
- ## RoolChannel API
786
-
787
- A channel is a named context within a space. All object operations, AI prompts, and real-time sync go through a channel. The `channelId` is fixed at open time — to use a different channel, open a new one.
788
-
789
- ### Properties
790
-
791
- | Property | Description |
792
- |----------|-------------|
793
- | `id: string` | Space ID |
794
- | `name: string` | Space name |
795
- | `role: RoolUserRole` | User's role (`'owner' \| 'admin' \| 'editor' \| 'viewer'`) |
796
- | `linkAccess: LinkAccess` | URL sharing level (`'none' \| 'viewer' \| 'editor'`) |
797
- | `userId: string` | Current user's ID |
798
- | `channelId: string` | Channel ID (read-only, fixed at open time) |
799
- | `isReadOnly: boolean` | True if viewer role |
800
-
801
- ### Lifecycle
802
-
803
- | Method | Description |
804
- |--------|-------------|
805
- | `close(): void` | Clean up resources and stop receiving updates |
806
- | `rename(name): Promise<void>` | Rename this channel |
807
- | `conversation(conversationId): ConversationHandle` | Get a handle scoped to a specific conversation (see [Conversations](#conversations)) |
808
-
809
- ### Object Operations
810
-
811
- Objects are records addressed by location (`/space/<collection>/<basename>.json`). Every object must belong to a collection — create the collection first (see [Collection Schema](#collection-schema)). The body holds the user-defined fields.
337
+ | Method/property | Description |
338
+ | --- | --- |
339
+ | `channel.conversation(id): ConversationHandle` | Get a conversation-scoped handle. |
340
+ | `getInteractions(): Interaction[]` | Active branch as a flat list. |
341
+ | `getTree(): Record<string, Interaction>` | Full interaction tree. |
342
+ | `activeLeafId` | Current branch tip. |
343
+ | `setActiveLeaf(id): void` | Switch branches. |
344
+ | `getSystemInstruction()` / `setSystemInstruction(value)` | Manage conversation system instruction. Pass `null` to clear. |
345
+ | `getConversations(): ConversationInfo[]` | List channel conversations (on `RoolChannel`). |
346
+ | `deleteConversation(id): Promise<void>` | Delete a non-active conversation. |
347
+ | `renameConversation(name): Promise<void>` | Rename the current/default conversation (on `RoolChannel`). |
348
+ | `conversation.rename(name): Promise<void>` | Rename a specific conversation handle. |
812
349
 
813
- All methods that accept a location accept either the canonical form or the short form (`collection/basename`).
350
+ `ConversationHandle` also supports conversation-scoped `putObject`, `patchObject`, `moveObject`, `deleteObjects`, `prompt`, `stop`, collection-schema methods, and `setMetadata`.
814
351
 
815
- | Method | Description |
816
- |--------|-------------|
817
- | `getObject(location): Promise<RoolObject \| undefined>` | Get an object, or undefined if not found. |
818
- | `stat(location): RoolObjectStat \| undefined` | Get audit info for an object: when it was last modified, by whom, and where (channel/conversation/interaction). Sync read from local cache. |
819
- | `createObject(collection, body, options?): Promise<{ object, message }>` | Create a new object in `collection`. The SDK mints a random basename unless you pass `options.basename`. |
820
- | `updateObject(location, options): Promise<{ object, message }>` | Update an existing object's body. |
821
- | `moveObject(from, to, options?): Promise<{ object, message }>` | Rename or relocate an object. See [Moving and Renaming](#moving-and-renaming). |
822
- | `deleteObjects(locations): Promise<void>` | Delete objects by location. Other objects' refs become stale. |
352
+ ## Schema and Metadata
823
353
 
824
- #### createObject
354
+ 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.
825
355
 
826
356
  ```typescript
827
- // Auto-generated basename
828
- const { object } = await channel.createObject('article', {
829
- title: 'Hello',
830
- body: 'World',
357
+ await channel.createCollection('article', {
358
+ schemaOrgType: 'Article',
359
+ fields: [
360
+ { name: 'title', type: { kind: 'string' } },
361
+ { name: 'status', type: { kind: 'enum', values: ['draft', 'published'] } },
362
+ { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } },
363
+ { name: 'author', type: { kind: 'ref' } },
364
+ ],
831
365
  });
832
- // → object.location: '/space/article/X7kQ9p.json'
833
366
 
834
- // Pinned basename
835
- await channel.createObject('article',
836
- { title: 'Welcome' },
837
- { basename: 'welcome' },
838
- );
839
- // → location: '/space/article/welcome.json'
840
-
841
- // AI placeholders
842
- await channel.createObject('article', {
843
- headline: '{{catchy headline}}',
844
- body: '{{long-form intro}}',
845
- });
846
- ```
847
-
848
- | Option | Description |
849
- |--------|-------------|
850
- | `basename` | Specific basename to use. If omitted, the SDK generates a random 6-char one. |
851
- | `ephemeral` | If true, the operation won't be recorded in interaction history. |
852
- | `parentInteractionId` | Conversation tree parent. Omit to auto-continue; pass `null` for a new root. |
853
-
854
- #### updateObject
855
-
856
- ```typescript
857
- // Add/update fields
858
- await channel.updateObject('/space/article/welcome.json', {
859
- data: { status: 'published' },
860
- });
367
+ const schema = channel.getSchema();
861
368
 
862
- // Delete a field (pass null)
863
- await channel.updateObject('/space/article/welcome.json', {
864
- data: { draft: null },
865
- });
369
+ await channel.alterCollection('article', [
370
+ { name: 'title', type: { kind: 'string' } },
371
+ { name: 'status', type: { kind: 'string' } },
372
+ ]);
866
373
 
867
- // AI-driven rewrite
868
- await channel.updateObject('/space/article/welcome.json', {
869
- prompt: 'Tighten the intro by 30%.',
870
- });
374
+ channel.setMetadata('viewport', { x: 0, y: 0, zoom: 1 });
375
+ const viewport = channel.getMetadata('viewport');
871
376
  ```
872
377
 
873
- | Option | Description |
874
- |--------|-------------|
875
- | `data` | Body fields to add, update, or delete. `null` removes the field. Use `{{placeholder}}` for AI-generated content. Fields prefixed with `_` are hidden from AI. |
876
- | `prompt` | Natural language instruction for AI to modify content. |
877
- | `ephemeral` | If true, the operation won't be recorded in interaction history. |
878
- | `parentInteractionId` | Conversation tree parent. Omit to auto-continue; pass `null` for a new root. |
378
+ | Method | Description |
379
+ | --- | --- |
380
+ | `getSchema(): SpaceSchema` | Get collection definitions. |
381
+ | `createCollection(name, fieldsOrDef, options?): Promise<CollectionDef>` | Create a collection. |
382
+ | `alterCollection(name, fieldsOrDef, options?): Promise<CollectionDef>` | Replace a collection definition. |
383
+ | `dropCollection(name): Promise<void>` | Remove a collection and its object directory. |
384
+ | `setMetadata(key, value): void` | Set space metadata (fire-and-forget sync). |
385
+ | `getMetadata(key): unknown` | Read metadata from local cache. |
386
+ | `getAllMetadata(): Record<string, unknown>` | Read all metadata from local cache. |
879
387
 
880
- Use `moveObject` to change an object's location (collection or basename).
388
+ Field kinds: `string`, `number`, `boolean`, `ref`, `enum`, `literal`, `array`, and `maybe`.
881
389
 
882
- #### Moving and Renaming
390
+ ## Undo/Redo
883
391
 
884
- `moveObject` is how you rename an object (new basename in the same collection) or move it across collections. Pass `options.body` to atomically rewrite the body as part of the move.
392
+ Undo/redo uses checkpoints for the current channel ID. A checkpoint captures space state; call `checkpoint()` before a user action you want to make undoable.
885
393
 
886
394
  ```typescript
887
- // Rename within the same collection
888
- await channel.moveObject(
889
- '/space/article/welcome.json',
890
- '/space/article/hello-world.json',
891
- );
892
-
893
- // Move into a different collection
894
- await channel.moveObject(
895
- '/space/draft/post-42.json',
896
- '/space/article/post-42.json',
897
- );
395
+ await channel.checkpoint('Delete article');
396
+ await channel.deleteObjects(['/space/article/welcome.json']);
898
397
 
899
- // Move and replace body in one go
900
- await channel.moveObject(from, to, {
901
- body: { title: 'Hello, world', status: 'published' },
902
- });
398
+ if (await channel.canUndo()) {
399
+ await channel.undo();
400
+ }
903
401
  ```
904
402
 
905
- | Option | Description |
906
- |--------|-------------|
907
- | `body` | Replace the body atomically as part of the move. If omitted, the body is preserved. |
908
- | `ephemeral` | If true, the operation won't be recorded in interaction history. |
909
- | `parentInteractionId` | Conversation tree parent. Omit to auto-continue; pass `null` for a new root. |
910
-
911
- ### Undo/Redo
912
-
913
- | Method | Description |
914
- |--------|-------------|
915
- | `checkpoint(label?): Promise<string>` | Call before mutations. Saves current state for undo. |
916
- | `canUndo(): Promise<boolean>` | Check if undo available |
917
- | `canRedo(): Promise<boolean>` | Check if redo available |
918
- | `undo(): Promise<boolean>` | Undo to previous checkpoint |
919
- | `redo(): Promise<boolean>` | Redo undone action |
920
- | `clearHistory(): Promise<void>` | Clear undo/redo stack |
921
-
922
- See [Checkpoints & Undo/Redo](#checkpoints--undoredo) for semantics.
923
-
924
- ### Space Metadata
925
-
926
- Store arbitrary data alongside the space without it being part of an object's body (e.g., viewport state, user preferences).
927
-
928
403
  | Method | Description |
929
- |--------|-------------|
930
- | `setMetadata(key, value): void` | Set space-level metadata |
931
- | `getMetadata(key): unknown` | Get metadata value, or undefined if key not set |
932
- | `getAllMetadata(): Record<string, unknown>` | Get all metadata |
404
+ | --- | --- |
405
+ | `checkpoint(label?): Promise<string>` | Save current space state. |
406
+ | `canUndo(): Promise<boolean>` | Check whether undo is available. |
407
+ | `canRedo(): Promise<boolean>` | Check whether redo is available. |
408
+ | `undo(): Promise<boolean>` | Restore the latest checkpoint. |
409
+ | `redo(): Promise<boolean>` | Reapply undone work. |
410
+ | `clearHistory(): Promise<void>` | Clear checkpoint history. |
933
411
 
934
- ### Space File Storage
412
+ Undo/redo availability and history are scoped to the channel handle (`channel.channelId`).
935
413
 
936
- Every space has authenticated file storage. WebDAV is the SDK surface for that storage: paths are relative to the space root and collection operations use WebDAV collection semantics. Human/AI file links use `rool-machine:/rool-drive/...`; resolve those links with `resolveMachineResource()` and fetch file resources with `space.fetchMachineResource(resource)`.
414
+ ## File Storage and WebDAV
937
415
 
938
- Open a space, then use `space.webdav`.
416
+ Every space has authenticated WebDAV storage. WebDAV methods take SDK machine paths such as `/space/...`, `/rool-drive/...`, or `/` for the root collection.
939
417
 
940
418
  ```typescript
941
- import { resolveMachineResource } from '@rool-dev/sdk';
942
-
943
- const space = await client.openSpace('space-id');
944
419
  const webdav = space.webdav;
945
420
 
946
- await webdav.mkcol('docs');
947
- await webdav.put('docs/readme.md', '# Hello', {
421
+ await webdav.mkcol('/rool-drive/docs');
422
+ await webdav.put('/rool-drive/docs/readme.md', '# Hello', {
948
423
  contentType: 'text/markdown',
949
424
  ifNoneMatch: '*',
950
425
  });
951
426
 
952
- const listing = await webdav.propfind('docs/', {
427
+ const listing = await webdav.propfind('/rool-drive/docs', {
953
428
  depth: '1',
954
429
  props: ['displayname', 'getcontentlength', 'getcontenttype', 'getetag'],
955
430
  });
956
431
 
957
- const file = await webdav.get('docs/readme.md');
958
- console.log(await file.text());
432
+ const response = await webdav.get('/rool-drive/docs/readme.md');
433
+ console.log(await response.text());
959
434
 
960
- const resource = resolveMachineResource('/rool-drive/docs/read me.md');
961
- if (!resource || resource.kind !== 'file') throw new Error('not a file');
962
- const sameFile = await space.fetchMachineResource(resource);
435
+ const file = await space.fetchPath('/rool-drive/docs/readme.md');
436
+ console.log(file.headers.get('Content-Type'));
963
437
 
964
438
  const usage = await space.getStorageUsage();
965
- console.log(usage.usedBytes);
966
- console.log(usage.availableBytes); // null means unlimited
967
- console.log(usage.limitBytes); // null means unlimited
968
-
969
- const rootProps = await webdav.propfind('', {
970
- depth: '0',
971
- props: ['sync-token', 'supported-report-set'],
972
- });
973
- let syncToken = rootProps.responses[0]?.props.syncToken ?? null;
974
-
975
- space.on('filesChanged', async () => {
976
- const delta = await space.webdav.syncCollection('', {
977
- token: syncToken,
978
- level: 'infinite',
979
- });
980
- syncToken = delta.token;
981
- console.log('Changed file responses:', delta.responses);
982
- });
439
+ console.log(usage.usedBytes, usage.availableBytes, usage.limitBytes);
983
440
  ```
984
441
 
985
- Paths are space-relative (`docs/readme.md`, not `/docs/readme.md`). WebDAV methods accept WebDAV paths only. User-facing file links should use `rool-machine:/rool-drive/...`; resolve either that URI or a bare `/rool-drive/...` machine path with `resolveMachineResource()` and fetch the resulting file resource with `space.fetchMachineResource(resource)`. `PUT` writes an exact path and does not create parent collections; create parents with `mkcol()` first. Helpers preserve WebDAV status semantics: non-success responses throw `WebDAVError` with `status`, `statusText`, and `body`.
442
+ ### Real-time file sync
986
443
 
987
- | Method | Description |
988
- |--------|-------------|
989
- | `space.webdav` | WebDAV client for an open space |
990
- | `space.getStorageUsage()` | Get WebDAV quota usage for an open space |
991
- | `webdav.getStorageUsage()` | Get WebDAV quota usage through the WebDAV client |
992
- | `webdav.path(path)` | Normalize a WebDAV path |
993
- | `webdav.propfind(path, options)` | Read properties/list collections; explicit `depth` required. Supports `sync-token` and `supported-report-set` props. |
994
- | `webdav.syncCollection(path, options)` | Reconcile WebDAV changes with `REPORT sync-collection`. Pass the previous `token` (or `null`), `level: '1' \| 'infinite'`, optional `props`/`limit`; returns changed responses plus the next `token`. |
995
- | `webdav.get(path, options?)` / `webdav.head(path)` | Read a file, including optional byte ranges for `get` |
996
- | `webdav.put(path, body, options?)` | Write an exact file path; parents must already exist |
997
- | `webdav.mkcol(path)` | Create one collection |
998
- | `webdav.copy(source, destination, options?)` | Copy a file or collection within the same space |
999
- | `webdav.move(source, destination, options?)` | Move a file or collection within the same space |
1000
- | `webdav.delete(path, options?)` | Delete a file or collection |
1001
- | `webdav.lock(path, options)` / `webdav.refreshLock(path, token)` / `webdav.unlock(token)` | WebDAV Class 2 write locks |
1002
- | `webdav.request(method, path, init?)` | Raw authenticated WebDAV request escape hatch |
1003
-
1004
- > **Note**: `resolveMachineResource()` returns either a file resource or an object resource. File resources point at user-visible files in the space's WebDAV storage and can be fetched with `space.fetchMachineResource(resource)`. Object resources identify records inside the space. They're not interchangeable.
1005
-
1006
- #### File references from AI responses
1007
-
1008
- When an agent refers to a user-visible file, the SDK contract is `rool-machine:/rool-drive/path/to/file.ext`. That prefix makes a file reference unambiguous without exposing the authenticated WebDAV URL. In free text, ambiguous characters such as spaces are percent-encoded (`rool-machine:/rool-drive/docs/read%20me.md`).
444
+ Object and file changes are announced at the space level. Use WebDAV `syncCollection()` to reconcile changes.
1009
445
 
1010
446
  ```typescript
1011
- const resource = resolveMachineResource('rool-machine:/rool-drive/docs/readme.md');
1012
- if (!resource || resource.kind !== 'file') throw new Error('not a file');
1013
- const response = await space.fetchMachineResource(resource);
1014
- const blob = await response.blob();
1015
- img.src = URL.createObjectURL(blob);
1016
- ```
1017
-
1018
- Plain relative strings like `docs/readme.md` are valid WebDAV paths when you already know you are working with file storage. In user text or agent output, use `rool-machine:/rool-drive/docs/readme.md` so clients do not have to guess whether a string is a file.
1019
-
1020
- ### Proxied Fetch
1021
-
1022
- Fetch external URLs via the server, bypassing CORS restrictions. Requires editor role or above. Private/internal IP ranges are blocked (SSRF protection).
447
+ let token: string | null = null;
1023
448
 
1024
- | Method | Description |
1025
- |--------|-------------|
1026
- | `fetch(url, init?): Promise<Response>` | Fetch a URL via the server proxy. `init` accepts `method`, `headers`, and `body`. |
449
+ async function syncFiles() {
450
+ const result = await space.webdav.syncCollection('/', {
451
+ token,
452
+ level: 'infinite',
453
+ props: ['displayname', 'getetag', 'getlastmodified', 'resourcetype'],
454
+ });
455
+ token = result.token;
456
+ updateFileTree(result.responses);
457
+ }
1027
458
 
1028
- ```typescript
1029
- // GET request
1030
- const response = await channel.fetch('https://api.example.com/data');
1031
- const data = await response.json();
1032
-
1033
- // POST with headers and body
1034
- const response = await channel.fetch('https://api.example.com/submit', {
1035
- method: 'POST',
1036
- headers: { 'Content-Type': 'application/json' },
1037
- body: { key: 'value' },
459
+ space.on('filesChanged', syncFiles);
460
+ space.on('filesReset', () => {
461
+ token = null;
462
+ void syncFiles();
1038
463
  });
1039
- ```
1040
-
1041
- ### Collection Schema
1042
464
 
1043
- Collections are the types you use to group objects in a space. Every object belongs to exactly one collection: the collection is the parent directory of its location, and the server validates the object's body against that collection's definition. Renaming a collection changes the location of every object bound to it; dropping a collection is blocked while any object still lives there.
465
+ await syncFiles();
466
+ ```
1044
467
 
1045
- Collections make up the schema and are stored in the space data, syncing in real time. The schema is visible to the AI agent so it knows which collections exist and what fields they contain, producing more consistent objects.
468
+ | Method | Description |
469
+ | --- | --- |
470
+ | `webdav.href(path)` / `webdav.url(path)` | Return WebDAV href/URL for an absolute SDK path. |
471
+ | `webdav.options(path)` | Send `OPTIONS`. |
472
+ | `webdav.propfind(path, options)` | Read properties/list collections. `depth` is required. |
473
+ | `webdav.syncCollection(path, options)` | WebDAV `REPORT sync-collection`; returns changed responses and next token. |
474
+ | `webdav.get(path, options?)` / `webdav.head(path)` | Read a file; `get` supports byte ranges. |
475
+ | `webdav.put(path, body, options?)` | Write a file/object at an exact path. Parent collection must exist. |
476
+ | `webdav.mkcol(path)` | Create one collection. |
477
+ | `webdav.copy(source, destination, options?)` | Copy a file or collection. |
478
+ | `webdav.move(source, destination, options?)` | Move a file or collection. |
479
+ | `webdav.delete(path, options?)` | Delete a file or collection. |
480
+ | `webdav.lock(path, options)` / `refreshLock(path, token)` / `unlock(token)` | WebDAV write locks. |
481
+ | `webdav.request(method, path, init?)` | Raw authenticated WebDAV request. |
482
+ | `space.fetchPath(path, options?)` | Fetch a `/rool-drive/...` file path or `rool-machine:` file URI. |
483
+ | `space.getStorageUsage()` / `webdav.getStorageUsage()` | Storage quota usage. |
484
+
485
+ High-level WebDAV methods that validate response status throw `WebDAVError` with `status`, `statusText`, and `body`; raw `request()` and `options()` return `Response`.
1046
486
 
487
+ ## Collaboration
1047
488
 
1048
489
  ```typescript
1049
- // Define a collection with typed fields
1050
- await channel.createCollection('article', [
1051
- { name: 'title', type: { kind: 'string' } },
1052
- { name: 'status', type: { kind: 'enum', values: ['draft', 'published', 'archived'] } },
1053
- { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } },
1054
- { name: 'author', type: { kind: 'ref' } },
1055
- ]);
1056
-
1057
- // Read the current schema
1058
- const schema = channel.getSchema();
1059
- console.log(schema.article.fields); // FieldDef[]
1060
-
1061
- // Modify an existing collection's fields
1062
- await channel.alterCollection('article', [
1063
- { name: 'title', type: { kind: 'string' } },
1064
- { name: 'status', type: { kind: 'enum', values: ['draft', 'review', 'published', 'archived'] } },
1065
- { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } },
1066
- { name: 'author', type: { kind: 'ref' } },
1067
- { name: 'wordCount', type: { kind: 'number' } },
1068
- ]);
490
+ const user = await client.searchUser('colleague@example.com');
491
+ if (user) {
492
+ await space.addUser(user.id, 'editor');
493
+ }
1069
494
 
1070
- // Remove a collection
1071
- await channel.dropCollection('article');
495
+ await space.setLinkAccess('viewer'); // 'none' | 'viewer' | 'editor'
1072
496
  ```
1073
497
 
1074
- | Method | Description |
1075
- |--------|-------------|
1076
- | `getSchema(): SpaceSchema` | Get all collection definitions |
1077
- | `createCollection(name, fields): Promise<CollectionDef>` | Add a new collection to the schema |
1078
- | `alterCollection(name, fields): Promise<CollectionDef>` | Replace a collection's field definitions |
1079
- | `dropCollection(name): Promise<void>` | Remove a collection from the schema |
498
+ Roles:
1080
499
 
1081
- #### Field Types
1082
-
1083
- | Kind | Description | Example |
1084
- |------|-------------|---------|
1085
- | `string` | Text value | `{ kind: 'string' }` |
1086
- | `number` | Numeric value | `{ kind: 'number' }` |
1087
- | `boolean` | True/false | `{ kind: 'boolean' }` |
1088
- | `ref` | Reference to another object (location string) | `{ kind: 'ref' }` |
1089
- | `enum` | One of a set of values | `{ kind: 'enum', values: ['a', 'b'] }` |
1090
- | `literal` | Exact value | `{ kind: 'literal', value: 'fixed' }` |
1091
- | `array` | List of values | `{ kind: 'array', inner: { kind: 'string' } }` |
1092
- | `maybe` | Optional (nullable) | `{ kind: 'maybe', inner: { kind: 'number' } }` |
1093
-
1094
- ### Import/Export
500
+ | Role | Capabilities |
501
+ | --- | --- |
502
+ | `owner` | Full control. |
503
+ | `admin` | Editor capabilities plus user/link management. |
504
+ | `editor` | Create, modify, move, and delete objects/files. |
505
+ | `viewer` | Read-only access. |
1095
506
 
1096
- Export and import space data as zip archives for backup, portability, or migration:
507
+ ## RoolClient API
1097
508
 
1098
- | Method | Description |
1099
- |--------|-------------|
1100
- | `space.exportArchive(): Promise<Blob>` | Export objects, metadata, channels, and files as a zip archive |
1101
- | `client.importArchive(name, archive): Promise<RoolSpace>` | Import from a zip archive, creating a new space |
509
+ ### Constructor config
1102
510
 
1103
- **Export:**
1104
511
  ```typescript
1105
- const space = await client.openSpace('space-id');
1106
- const archive = await space.exportArchive();
1107
- // Save as .zip file
1108
- const url = URL.createObjectURL(archive);
512
+ const client = new RoolClient({
513
+ apiUrl: 'https://api.rool.dev',
514
+ authUrl: 'https://rool.dev/auth',
515
+ graphqlUrl: 'https://api.rool.dev/graphql',
516
+ logger: console,
517
+ });
1109
518
  ```
1110
519
 
1111
- **Import:**
1112
- ```typescript
1113
- const space = await client.importArchive('Imported Data', archiveBlob);
1114
- const channel = await space.openChannel('main');
520
+ `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.
521
+
522
+ | Method/property | Description |
523
+ | --- | --- |
524
+ | `currentUser: CurrentUser | null` | Cached user profile from initialization/fetch. |
525
+ | `getCurrentUser(): Promise<CurrentUser>` | Fetch current user. |
526
+ | `updateCurrentUser(input): Promise<CurrentUser>` | Update `name`, `slug`, or `marketingOptIn`. |
527
+ | `deleteCurrentUser(): Promise<void>` | Mark account for deletion and log out. |
528
+ | `searchUser(email): Promise<UserResult | null>` | Exact email lookup. |
529
+ | `listSpaces(): Promise<RoolSpaceInfo[]>` | List accessible spaces. |
530
+ | `openSpace(id): Promise<RoolSpace>` | Open/cached live space handle. |
531
+ | `createSpace(name): Promise<RoolSpace>` | Create and open a space. |
532
+ | `duplicateSpace(sourceId, name): Promise<RoolSpace>` | Duplicate a space. |
533
+ | `deleteSpace(id): Promise<void>` | Permanently delete a space. |
534
+ | `importArchive(name, archive): Promise<RoolSpace>` | Import a zip archive as a new space. |
535
+ | `getUserStorage<T>(key): T | undefined` | Sync read from user-storage cache. |
536
+ | `setUserStorage(key, value): void` | Update user storage; `null`/`undefined` deletes. |
537
+ | `getAllUserStorage(): Record<string, unknown>` | Copy all cached user storage. |
538
+ | `reportEvent(event, url?): void` | Fire-and-forget telemetry event. |
539
+ | `destroy(): void` | Close subscriptions, spaces, auth resources, and listeners. |
540
+ | `generateId(): string` | Generate a 6-character alphanumeric ID. |
541
+
542
+ ### Client events
543
+
544
+ ```typescript
545
+ client.on('authStateChanged', (authenticated) => void 0);
546
+ client.on('spaceAdded', (space) => void 0);
547
+ client.on('spaceRemoved', (spaceId) => void 0);
548
+ client.on('spaceRenamed', (spaceId, newName) => void 0);
549
+ client.on('channelCreated', (spaceId, channel) => void 0);
550
+ client.on('channelUpdated', (spaceId, channel) => void 0);
551
+ client.on('channelDeleted', (spaceId, channelId) => void 0);
552
+ client.on('userStorageChanged', ({ key, value, source }) => void 0);
553
+ client.on('connectionStateChanged', (state) => void 0);
554
+ client.on('error', (error, context) => void 0);
1115
555
  ```
1116
556
 
1117
- The archive bundles `data.json` (objects, metadata, and channels) together with the space file storage. File references are rewritten to relative paths within the archive and restored on import.
557
+ ## RoolSpace API
1118
558
 
1119
- ### Channel Events
559
+ Properties: `id`, `name`, `role`, `linkAccess`, `memberCount`, `channels`, `route`, `webdav`.
1120
560
 
1121
- Channel events are for channel/conversation state. Object and file reactivity goes through `space.on('filesChanged' | 'filesReset')` plus WebDAV `syncCollection()`.
561
+ | Method | Description |
562
+ | --- | --- |
563
+ | `openChannel(channelId): Promise<RoolChannel>` | Open/create a channel. |
564
+ | `close(): void` | Stop subscription and close open channels. |
565
+ | `rename(newName): Promise<void>` | Rename the space. |
566
+ | `delete(): Promise<void>` | Permanently delete the space. |
567
+ | `listUsers(): Promise<SpaceMember[]>` | List collaborators. |
568
+ | `addUser(userId, role): Promise<void>` | Add collaborator. |
569
+ | `removeUser(userId): Promise<void>` | Remove collaborator. |
570
+ | `setLinkAccess(linkAccess): Promise<void>` | Set URL sharing level. |
571
+ | `renameChannel(channelId, name): Promise<void>` | Rename a channel. |
572
+ | `deleteChannel(channelId): Promise<void>` | Delete a channel and history. |
573
+ | `exportArchive(): Promise<Blob>` | Export a space archive. |
574
+ | `refresh(): Promise<void>` | Refresh cached space data. |
575
+ | `fetchPath(path, options?): Promise<Response>` | Fetch a `/rool-drive/...` file. |
576
+ | `getStorageUsage(): Promise<SpaceFileStorageUsage>` | File-storage quota usage. |
577
+
578
+ Events:
579
+
580
+ ```typescript
581
+ space.on('channelCreated', (channel) => void 0);
582
+ space.on('channelUpdated', (channel) => void 0);
583
+ space.on('channelDeleted', (channelId) => void 0);
584
+ space.on('filesChanged', ({ spaceId, source, timestamp }) => void 0);
585
+ space.on('filesReset', ({ spaceId, source, timestamp }) => void 0);
586
+ space.on('connectionStateChanged', (state) => void 0);
587
+ ```
1122
588
 
1123
- ```typescript
1124
- // Channel metadata updated
1125
- channel.on('channelUpdated', ({ channelId, source }) => void)
589
+ ## RoolChannel API
1126
590
 
1127
- // Conversation interaction history updated
1128
- channel.on('conversationUpdated', ({ conversationId, channelId, source }) => void)
591
+ Properties: `id` (space ID), `name` (space name), `role`, `linkAccess`, `userId`, `channelId`, `channelName`, `conversationId`, `isReadOnly`, `activeLeafId`.
1129
592
 
1130
- // Space metadata / schema compatibility events
1131
- channel.on('metadataUpdated', ({ metadata, source }) => void)
1132
- channel.on('schemaUpdated', ({ schema, source }) => void)
593
+ | Area | Methods |
594
+ | --- | --- |
595
+ | Lifecycle | `close()`, `rename(name)`, `conversation(id)` |
596
+ | Objects | `getObject`, `getObjects`, `stat`, `putObject`, `patchObject`, `moveObject`, `deleteObjects` |
597
+ | Schema | `getSchema`, `createCollection`, `alterCollection`, `dropCollection` |
598
+ | Metadata | `setMetadata`, `getMetadata`, `getAllMetadata` |
599
+ | Conversations | `getInteractions`, `getTree`, `setActiveLeaf`, `getConversations`, `deleteConversation`, `getSystemInstruction`, `setSystemInstruction`, `renameConversation` |
600
+ | AI | `prompt`, `stop`, `stopInteraction` |
601
+ | Undo/redo | `checkpoint`, `canUndo`, `canRedo`, `undo`, `redo`, `clearHistory` |
602
+ | Utilities | `fetch(url, init?)` server-side proxied fetch |
1133
603
 
1134
- // Full state replacement (undo/redo, resync after error)
1135
- channel.on('reset', ({ source }) => void)
604
+ Channel events:
1136
605
 
1137
- // Sync error occurred
1138
- channel.on('syncError', (error: Error) => void)
606
+ ```typescript
607
+ channel.on('metadataUpdated', ({ metadata, source }) => void 0);
608
+ channel.on('schemaUpdated', ({ schema, source }) => void 0);
609
+ channel.on('channelUpdated', ({ channelId, source }) => void 0);
610
+ channel.on('conversationUpdated', ({ conversationId, channelId, source }) => void 0);
611
+ channel.on('reset', ({ source }) => void 0);
612
+ channel.on('syncError', (error) => void 0);
1139
613
  ```
1140
614
 
1141
- ### Error Handling
615
+ `channel.fetch(url, init?)` proxies external HTTP requests through the server to bypass browser CORS.
1142
616
 
1143
- AI operations may fail due to rate limiting or other transient errors. Check `error.message` for user-friendly error text:
617
+ ## Import/Export
1144
618
 
1145
619
  ```typescript
1146
- try {
1147
- await channel.updateObject(location, { prompt: 'expand this' });
1148
- } catch (error) {
1149
- if (error.message.includes('temporarily unavailable')) {
1150
- showToast('Service busy, please try again in a moment');
1151
- } else {
1152
- showToast(error.message);
1153
- }
1154
- }
620
+ const archive = await space.exportArchive();
621
+ const imported = await client.importArchive('Imported Data', archive);
1155
622
  ```
1156
623
 
1157
- ## Interaction History
1158
-
1159
- Each channel contains one or more conversations, each with its own interaction history. History is stored as a tree (interactions linked by `parentId`) in the space data and syncs in real-time. Capped at 200 interactions per conversation.
1160
-
1161
- ### Conversation History Methods
1162
-
1163
- | Method | Description |
1164
- |--------|-------------|
1165
- | `getInteractions(): Interaction[]` | Get the active branch as a flat array (root → leaf) |
1166
- | `getTree(): Record<string, Interaction>` | Get the full interaction tree for branch navigation |
1167
- | `activeLeafId: string \| undefined` | The tip of the currently active branch |
1168
- | `setActiveLeaf(id: string): void` | Switch to a different branch (emits `conversationUpdated`) |
1169
- | `getSystemInstruction(): string \| undefined` | Get system instruction for the default conversation |
1170
- | `setSystemInstruction(instruction): Promise<void>` | Set system instruction for the default conversation. Pass `null` to clear. |
1171
- | `getConversations(): ConversationInfo[]` | List all conversations in this channel |
1172
- | `deleteConversation(conversationId): Promise<void>` | Delete a conversation (cannot delete `'default'`) |
1173
- | `renameConversation(name): Promise<void>` | Rename the default conversation |
1174
-
1175
- Channel management (listing, renaming, deleting channels) is done via the client — see [Channel Management](#channel-management).
1176
-
1177
- ### The ai Field
1178
-
1179
- The `ai` field in interactions distinguishes AI-generated responses from synthetic confirmations:
1180
- - `ai: true` — AI processed this operation (prompt, or createObject/updateObject with placeholders)
1181
- - `ai: false` — System confirmation only (e.g., "Created object /space/note/welcome.json")
1182
-
1183
- ### Tool Calls
1184
-
1185
- The `toolCalls` array captures what the AI agent did during execution. The `conversationUpdated` event fires when each tool starts and completes. A tool call with `status: 'running'` has no result; once `status: 'done'`, `result` contains the truncated result string.
624
+ Archives include objects, metadata, channels/conversations, and file storage.
1186
625
 
1187
626
  ## Data Types
1188
627
 
1189
- ### Schema Types
1190
-
1191
628
  ```typescript
1192
- // Allowed field types
1193
629
  type FieldType =
1194
630
  | { kind: 'string' }
1195
631
  | { kind: 'number' }
@@ -1207,144 +643,66 @@ interface FieldDef {
1207
643
 
1208
644
  interface CollectionDef {
1209
645
  fields: FieldDef[];
646
+ schemaOrgType?: string;
1210
647
  }
1211
648
 
1212
- // Full schema — collection names to definitions
1213
649
  type SpaceSchema = Record<string, CollectionDef>;
1214
- ```
1215
650
 
1216
- ### Object Data
1217
-
1218
- ```typescript
1219
- // An object addressed by location. References between objects are body
1220
- // fields whose values are location strings.
1221
651
  interface RoolObject {
1222
- location: string; // "/space/<collection>/<basename>.json"
1223
- collection: string;
1224
- basename: string;
652
+ path: string;
1225
653
  body: Record<string, unknown>;
1226
654
  }
1227
655
 
1228
- // Object stat — audit information returned by channel.stat()
656
+ interface GetObjectsResult {
657
+ objects: RoolObject[];
658
+ missing: string[];
659
+ }
660
+
1229
661
  interface RoolObjectStat {
1230
- location: string;
662
+ path: string;
1231
663
  modifiedAt: number;
1232
664
  modifiedBy: string;
1233
665
  modifiedByName: string | null;
1234
- modifiedInChannel: string; // Channel ID where the last modification happened
1235
- modifiedInConversation: string | null; // Conversation ID, or null if not conversation-scoped
1236
- modifiedInInteraction: string | null; // Interaction ID, or null for ephemeral or non-AI writes
1237
- }
1238
- ```
1239
-
1240
- ### Channels and Conversations
1241
-
1242
- ```typescript
1243
- // Conversation — holds interaction tree and optional system instruction
1244
- interface Conversation {
1245
- name?: string; // Conversation name (optional)
1246
- systemInstruction?: string; // Custom system instruction for AI
1247
- createdAt: number; // Timestamp when conversation was created
1248
- createdBy: string; // User ID who created the conversation
1249
- interactions: Record<string, Interaction>; // Interaction tree (keyed by ID, linked by parentId)
666
+ modifiedInChannel: string;
667
+ modifiedInConversation: string | null;
668
+ modifiedInInteraction: string | null;
1250
669
  }
1251
670
 
1252
- // Conversation summary info (returned by channel.getConversations())
1253
- interface ConversationInfo {
1254
- id: string;
1255
- name: string | null;
1256
- systemInstruction: string | null;
1257
- createdAt: number;
1258
- createdBy: string;
1259
- interactionCount: number;
1260
- }
671
+ type PromptAttachment =
672
+ | File
673
+ | Blob
674
+ | { data: string; contentType: string; filename?: string }
675
+ | string;
1261
676
 
1262
- // Channel container with metadata and conversations
1263
- interface Channel {
1264
- name?: string; // Channel name (optional)
1265
- createdAt: number; // Timestamp when channel was created
1266
- createdBy: string; // User ID who created the channel
1267
- createdByName?: string; // Display name at time of creation
1268
- conversations: Record<string, Conversation>; // Keyed by conversation ID
1269
- }
677
+ type PromptEffort = 'QUICK' | 'STANDARD' | 'REASONING' | 'RESEARCH';
1270
678
 
1271
- // Channel summary info (used by space.channels)
1272
- interface ChannelInfo {
1273
- id: string;
1274
- name: string | null;
1275
- createdAt: number;
1276
- createdBy: string;
1277
- createdByName: string | null;
1278
- interactionCount: number;
679
+ interface PromptOptions {
680
+ responseSchema?: Record<string, unknown>;
681
+ effort?: PromptEffort;
682
+ parentInteractionId?: string | null;
683
+ ephemeral?: boolean;
684
+ readOnly?: boolean;
685
+ attachments?: PromptAttachment[];
686
+ signal?: AbortSignal;
687
+ eventName?: string;
1279
688
  }
1280
- ```
1281
-
1282
- Note: `Channel` and `ChannelInfo` are data types describing the stored channel metadata. The `Channel` interface is the wire format; `RoolChannel` is the live SDK class you interact with.
1283
-
1284
- ### Interaction Types
1285
-
1286
- ```typescript
1287
- type ToolCall =
1288
- | {
1289
- id: string;
1290
- name: string; // Tool name (e.g., "create_object", "update_object", "search_web")
1291
- input: unknown; // Arguments passed to the tool
1292
- status: 'running';
1293
- }
1294
- | {
1295
- id: string;
1296
- name: string;
1297
- input: unknown;
1298
- status: 'done';
1299
- result: string; // Truncated result
1300
- };
1301
689
 
1302
690
  type InteractionStatus = 'pending' | 'streaming' | 'done' | 'error';
1303
691
 
1304
692
  interface Interaction {
1305
- id: string; // Unique ID for this interaction
1306
- parentId: string | null; // Parent in conversation tree (null = root)
693
+ id: string;
694
+ parentId: string | null;
1307
695
  timestamp: number;
1308
- userId: string; // Who performed this interaction
1309
- userName?: string | null; // Display name at time of interaction
1310
- operation: 'prompt' | 'createObject' | 'updateObject' | 'moveObject' | 'deleteObjects';
1311
- input: string; // What the user did: prompt text or action description
1312
- output: string | null; // AI response or confirmation message (may be partial when streaming)
1313
- status: InteractionStatus; // Lifecycle status (pending → streaming → done/error)
1314
- ai: boolean; // Whether AI was invoked (vs synthetic confirmation)
1315
- modifiedObjectLocations: string[]; // Locations of objects affected by this interaction
1316
- toolCalls: ToolCall[]; // Tools called during this interaction (for AI prompts)
1317
- attachments?: string[]; // canonical rool-machine:/... resource refs attached by the user
1318
- }
1319
- ```
1320
-
1321
- ### Info Types
1322
-
1323
- ```typescript
1324
- type RoolUserRole = 'owner' | 'admin' | 'editor' | 'viewer';
1325
- type LinkAccess = 'none' | 'viewer' | 'editor';
1326
-
1327
- interface RoolSpaceInfo { id: string; name: string; inboundEmailAddress: string; role: RoolUserRole; ownerId: string; size: number; createdAt: string; updatedAt: string; linkAccess: LinkAccess; memberCount: number; }
1328
- interface SpaceMember { id: string; email: string; role: RoolUserRole; photoUrl: string | null; }
1329
- interface UserResult { id: string; email: string; name: string | null; photoUrl: string | null; }
1330
- interface CurrentUser { id: string; email: string; name: string | null; photoUrl: string | null; slug: string; plan: string; creditsBalance: number; totalCreditsUsed: number; createdAt: string; lastActivity: string; processedAt: string; storage: Record<string, unknown>; }
1331
- type ChangeSource = 'local_user' | 'remote_user' | 'remote_agent' | 'system';
1332
- ```
1333
-
1334
- ### Prompt Options
1335
-
1336
- ```typescript
1337
- type PromptEffort = 'QUICK' | 'STANDARD' | 'REASONING' | 'RESEARCH';
1338
- type PromptAttachment = File | Blob | { data: string; contentType: string; filename?: string } | MachineResource;
1339
-
1340
- interface PromptOptions {
1341
- responseSchema?: Record<string, unknown>;
1342
- effort?: PromptEffort; // Effort level (default: 'STANDARD')
1343
- ephemeral?: boolean; // Don't record in interaction history
1344
- readOnly?: boolean; // Disable mutation tools (default: false)
1345
- parentInteractionId?: string | null; // Branch from a specific interaction (omit to auto-continue)
1346
- attachments?: PromptAttachment[]; // Machine resources or local files to upload
1347
- signal?: AbortSignal; // Cancel an in-flight prompt
696
+ userId: string;
697
+ userName?: string | null;
698
+ operation: 'prompt' | 'putObject' | 'patchObject' | 'moveObject' | 'deleteObjects' | 'deletePaths' | string;
699
+ input: string;
700
+ output: string | null;
701
+ status: InteractionStatus;
702
+ ai: boolean;
703
+ modifiedObjectPaths: string[];
704
+ toolCalls: ToolCall[];
705
+ attachments?: string[];
1348
706
  }
1349
707
  ```
1350
708