@rool-dev/sdk 0.8.2-dev.02b70e5 → 0.8.2-dev.d82ea25

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 (56) hide show
  1. package/README.md +465 -1005
  2. package/dist/channel.d.ts +93 -248
  3. package/dist/channel.d.ts.map +1 -1
  4. package/dist/channel.js +410 -577
  5. package/dist/channel.js.map +1 -1
  6. package/dist/client.d.ts +14 -46
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +31 -124
  9. package/dist/client.js.map +1 -1
  10. package/dist/graphql.d.ts +11 -36
  11. package/dist/graphql.d.ts.map +1 -1
  12. package/dist/graphql.js +72 -311
  13. package/dist/graphql.js.map +1 -1
  14. package/dist/index.d.ts +4 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/path.d.ts +6 -0
  19. package/dist/path.d.ts.map +1 -0
  20. package/dist/path.js +47 -0
  21. package/dist/path.js.map +1 -0
  22. package/dist/reroute.d.ts +22 -0
  23. package/dist/reroute.d.ts.map +1 -0
  24. package/dist/reroute.js +61 -0
  25. package/dist/reroute.js.map +1 -0
  26. package/dist/rest.d.ts +27 -0
  27. package/dist/rest.d.ts.map +1 -0
  28. package/dist/rest.js +78 -0
  29. package/dist/rest.js.map +1 -0
  30. package/dist/router.d.ts.map +1 -1
  31. package/dist/router.js +25 -10
  32. package/dist/router.js.map +1 -1
  33. package/dist/space.d.ts +23 -16
  34. package/dist/space.d.ts.map +1 -1
  35. package/dist/space.js +111 -78
  36. package/dist/space.js.map +1 -1
  37. package/dist/subscription.d.ts.map +1 -1
  38. package/dist/subscription.js +47 -40
  39. package/dist/subscription.js.map +1 -1
  40. package/dist/types.d.ts +85 -224
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js +0 -4
  43. package/dist/types.js.map +1 -1
  44. package/dist/webdav.d.ts +176 -0
  45. package/dist/webdav.d.ts.map +1 -0
  46. package/dist/webdav.js +495 -0
  47. package/dist/webdav.js.map +1 -0
  48. package/package.json +2 -1
  49. package/dist/apps.d.ts +0 -30
  50. package/dist/apps.d.ts.map +0 -1
  51. package/dist/apps.js +0 -81
  52. package/dist/apps.js.map +0 -1
  53. package/dist/media.d.ts +0 -76
  54. package/dist/media.d.ts.map +0 -1
  55. package/dist/media.js +0 -249
  56. package/dist/media.js.map +0 -1
package/README.md CHANGED
@@ -1,16 +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
- > **Building a new Rool extension?** Start with [`@rool-dev/extension`](/extension/) — it handles hosting, dev server, and gives you a reactive Svelte channel out of the box. This SDK is for advanced use cases: integrating Rool into an existing application, building Node.js scripts, or working outside the extension sandbox.
5
+ Core primitives:
6
6
 
7
- The SDK manages authentication, real-time synchronization, and media storage. Core primitives:
8
-
9
- - **Spaces** — Containers for objects, schema, metadata, and channels
10
- - **Channels** — Named contexts within a space. All object and AI operations go through a channel.
11
- - **Conversations** — Independent interaction histories within a channel.
12
- - **Objects** — Key-value records with any fields you define. References between objects are data fields whose values are object IDs.
13
- - **AI operations** — Create, update, or query objects using natural language and `{{placeholders}}`
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.
14
12
 
15
13
  ## Installation
16
14
 
@@ -23,1074 +21,611 @@ npm install @rool-dev/sdk
23
21
  ```typescript
24
22
  import { RoolClient } from '@rool-dev/sdk';
25
23
 
26
- const client = new RoolClient();
27
- const authenticated = await client.initialize();
24
+ async function main() {
25
+ const client = new RoolClient();
28
26
 
29
- if (!authenticated) {
30
- client.login('My App'); // Redirects to auth page, shows "Sign in to My App"
31
- }
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
+ }
32
32
 
33
- // Create a new space, then open a channel on it
34
- const space = await client.createSpace('Solar System');
35
- const channel = await space.openChannel('main');
33
+ const space = await client.createSpace('Solar System');
34
+ const channel = await space.openChannel('main');
36
35
 
37
- // Define the schema — what types of objects exist and their fields
38
- await channel.createCollection('body', [
39
- { name: 'name', type: { kind: 'string' } },
40
- { name: 'mass', type: { kind: 'string' } },
41
- { name: 'radius', type: { kind: 'string' } },
42
- { name: 'orbits', type: { kind: 'maybe', inner: { kind: 'ref' } } },
43
- ]);
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
+ ]);
44
42
 
45
- // Create objects with AI-generated content using {{placeholders}}
46
- const { object: sun } = await channel.createObject({
47
- data: {
48
- type: 'body', // Must match a collection name
43
+ const { object: sun } = await channel.putObject('/space/body/sun.json', {
49
44
  name: 'Sun',
50
- mass: '{{mass in solar masses}}',
51
- radius: '{{radius in km}}'
52
- }
53
- });
45
+ mass: '1 solar mass',
46
+ radius: '696,340 km',
47
+ });
54
48
 
55
- const { object: earth } = await channel.createObject({
56
- data: {
57
- type: 'body',
49
+ const { object: earth } = await channel.putObject('/space/body/earth.json', {
58
50
  name: 'Earth',
59
- mass: '{{mass in Earth masses}}',
60
- radius: '{{radius in km}}',
61
- orbits: sun.id // Reference to the sun object
62
- }
63
- });
64
-
65
- // Use the AI agent to work with your data
66
- const { message, objects } = await channel.prompt(
67
- 'Add the other planets in our solar system, each referencing the Sun'
68
- );
69
- console.log(message); // AI explains what it did
70
- console.log(`Created ${objects.length} objects`);
71
-
72
- // Query with natural language
73
- const { objects: innerPlanets } = await channel.findObjects({
74
- prompt: 'planets closer to the sun than Earth'
75
- });
76
-
77
- // Clean up
78
- channel.close();
79
- ```
51
+ mass: '1 Earth mass',
52
+ radius: '6,371 km',
53
+ orbits: sun.path,
54
+ });
80
55
 
81
- ## Core Concepts
56
+ const { message, objects } = await channel.prompt(
57
+ 'Add the other planets in our solar system, each referencing the Sun.'
58
+ );
82
59
 
83
- ### Spaces and Channels
84
-
85
- A **space** is a container that holds objects, schema, metadata, and channels. 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.
86
-
87
- There are two main handles:
88
- - **`RoolSpace`** — Live handle with SSE subscription for user management, link access, channel management, export, and channel lifecycle events. Extends `EventEmitter`.
89
- - **`RoolChannel`** — Full real-time handle for objects, AI prompts, media, schema, and undo/redo.
90
-
91
- ```typescript
92
- // Open a space — live handle with SSE subscription
93
- const space = await client.openSpace('space-id');
94
- await space.addUser(userId, 'editor');
95
- await space.setLinkAccess('viewer');
60
+ console.log(message);
61
+ console.log(`Modified ${objects.length} objects`);
96
62
 
97
- // React to channel changes in real-time
98
- space.on('channelCreated', (channel) => console.log('New channel:', channel.id));
99
- space.on('channelUpdated', (channel) => console.log('Updated:', channel.id));
100
- space.on('channelDeleted', (channelId) => console.log('Deleted:', channelId));
63
+ const loadedEarth = await channel.getObject(earth.path);
64
+ console.log(loadedEarth?.body.name);
101
65
 
102
- // Open a channel for object and AI operations
103
- const channel = await space.openChannel('my-channel');
104
- await channel.prompt('Create some planets');
105
-
106
- // Open another channel on the same space
107
- const channel2 = await space.openChannel('research');
108
- await channel2.prompt('Analyze the data'); // Independent channel
66
+ space.close();
67
+ }
109
68
 
110
- // Clean up — stops subscription and closes all open channels
111
- space.close();
69
+ void main();
112
70
  ```
113
71
 
114
- 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.
115
-
116
- **Channel ID constraints:**
117
- - 1–32 characters
118
- - Only alphanumeric characters, hyphens (`-`), and underscores (`_`)
72
+ ## Paths and Resource URIs
119
73
 
120
- ### Conversations
74
+ Most SDK methods take plain path strings:
121
75
 
122
- 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.
123
-
124
- 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:
125
-
126
- ```typescript
127
- // Default conversation — most apps use this
128
- const space = await client.openSpace('space-id');
129
- const channel = await space.openChannel('main');
130
- await channel.prompt('Hello'); // Uses 'default' conversation
131
-
132
- // Conversation handle — for multi-thread UIs
133
- const thread = channel.conversation('thread-42');
134
- await thread.prompt('Hello'); // Uses 'thread-42' conversation
135
- thread.getInteractions(); // Interactions for thread-42 only
136
- ```
76
+ - Object paths: `/space/<collection>/<name>.json` (exactly three segments; no dotfile collection or object names)
77
+ - File paths: `/rool-drive/<path/to/file>`
137
78
 
138
- 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.
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.
139
80
 
140
81
  ```typescript
141
- // System instructions are per-conversation
142
- const thread = channel.conversation('research');
143
- await thread.setSystemInstruction('Respond in haiku');
82
+ import { machinePath, machineUri, isObjectPath } from '@rool-dev/sdk';
144
83
 
145
- // List all conversations in this channel
146
- const conversations = channel.getConversations();
84
+ machinePath('rool-machine:/rool-drive/docs/read%20me.md');
85
+ // '/rool-drive/docs/read me.md'
147
86
 
148
- // Delete a conversation (cannot delete 'default')
149
- await channel.deleteConversation('old-thread');
87
+ machineUri('/space/article/welcome.json');
88
+ // 'rool-machine:/space/article/welcome.json'
150
89
 
151
- // Rename a conversation
152
- await thread.rename('Research Thread');
90
+ isObjectPath('/space/article/welcome.json'); // true
153
91
  ```
154
92
 
155
- ### Branching Conversations
156
-
157
- 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`:
93
+ Object APIs require full object paths. References between objects are ordinary body fields containing object paths:
158
94
 
159
95
  ```typescript
160
- const thread = channel.conversation('chat');
161
-
162
- // Normal conversation each prompt auto-continues from the last
163
- await thread.prompt('My favorite color is blue. Say OK.');
164
- await thread.prompt('What is my favorite color?'); // Sees "blue"
165
-
166
- // Branch: go back to the first message and say something different
167
- const firstLeaf = thread.activeLeafId; // ID of the "blue" interaction
168
- const tree = thread.getTree();
169
- const firstInteractionId = tree[firstLeaf!].parentId!; // The root
170
-
171
- await thread.prompt('My favorite color is red. Say OK.', {
172
- parentInteractionId: firstInteractionId, // Sibling of "blue"
173
- });
174
- await thread.prompt('What is my favorite color?'); // Sees "red", not "blue"
175
-
176
- // Switch back to the blue branch
177
- thread.setActiveLeaf(firstLeaf!);
178
- thread.getInteractions(); // Returns the blue branch (root → leaf)
96
+ {
97
+ path: '/space/body/earth.json',
98
+ body: { name: 'Earth', orbits: '/space/body/sun.json' },
99
+ }
179
100
  ```
180
101
 
181
- **Key concepts:**
182
- - `getInteractions()` returns the active branch as a flat `Interaction[]` (root → leaf)
183
- - `getTree()` returns the full `Record<string, Interaction>` for branch navigation UI
184
- - `activeLeafId` is the tip of the branch the user is currently viewing
185
- - `setActiveLeaf(id)` switches branches (emits `conversationUpdated` so reactive UIs refresh)
186
- - `prompt()` with no `parentInteractionId` auto-continues from `activeLeafId`
187
- - `prompt()` with `parentInteractionId: null` starts a new root-level branch
102
+ ## Authentication
188
103
 
189
- ### Objects & References
104
+ ### Browser
190
105
 
191
- **Objects** are plain key-value records. `id` and `type` are reserved; everything else is application-defined. Every object must include a `type` field whose value names a collection in the schema that binds the object to that collection and determines how it's validated. Create the collection first (see [Collection Schema](#collection-schema)).
106
+ The default auth provider stores tokens in browser storage and redirects to the Rool auth page.
192
107
 
193
108
  ```typescript
194
- { id: 'abc123', type: 'article', title: 'Hello World', status: 'draft' }
195
- ```
196
-
197
- **References** between objects are data fields whose values are object IDs. The system detects these statistically — any string field whose value matches an existing object ID is recognized as a reference.
109
+ async function start() {
110
+ const client = new RoolClient();
198
111
 
199
- ```typescript
200
- // A planet references a star via the 'orbits' field
201
- { id: 'earth', type: 'body', name: 'Earth', orbits: 'sun-01' }
202
-
203
- // An array of references
204
- { id: 'team-a', type: 'team', name: 'Alpha', members: ['user-1', 'user-2', 'user-3'] }
205
- ```
206
-
207
- References are just data — no special API is needed to create or remove them. Set a field to an object ID to create a reference; clear it to remove it.
208
-
209
- ### AI Placeholder Pattern
210
-
211
- Use `{{description}}` in field values to have AI generate content:
212
-
213
- ```typescript
214
- // Create with AI-generated content
215
- await channel.createObject({
216
- data: {
217
- type: 'article',
218
- headline: '{{catchy headline about coffee}}',
219
- body: '{{informative paragraph}}'
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;
220
116
  }
221
- });
222
117
 
223
- // Update existing content with AI
224
- await channel.updateObject('abc123', {
225
- prompt: 'Make the body shorter and more casual'
226
- });
118
+ // Use the authenticated client here.
119
+ }
227
120
 
228
- // Add new AI-generated field to existing object
229
- await channel.updateObject('abc123', {
230
- data: { summary: '{{one-sentence summary}}' }
231
- });
121
+ void start();
232
122
  ```
233
123
 
234
- When resolving placeholders, the agent has access to the full object data 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.
235
-
236
- 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.
237
-
238
- ### Checkpoints & Undo/Redo
124
+ ### Node.js
239
125
 
240
- 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.
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.
241
127
 
242
128
  ```typescript
243
- // Create a checkpoint before user action
244
- await channel.checkpoint('Delete object');
245
- await channel.deleteObjects([objectId]);
129
+ import { RoolClient } from '@rool-dev/sdk';
130
+ import { NodeAuthProvider } from '@rool-dev/sdk/node';
246
131
 
247
- // User can now undo back to the checkpoint
248
- if (await channel.canUndo()) {
249
- await channel.undo(); // Restores the deleted object
250
- }
132
+ const client = new RoolClient({ authProvider: new NodeAuthProvider() });
133
+ let authenticated = await client.initialize();
251
134
 
252
- // Redo reapplies the undone action
253
- if (await channel.canRedo()) {
254
- await channel.redo(); // Deletes the object again
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();
255
140
  }
256
- ```
257
-
258
- 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.
259
-
260
- ### Hidden Fields
261
141
 
262
- 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:
263
-
264
- ```typescript
265
- await channel.createObject({
266
- data: {
267
- type: 'article',
268
- title: 'My Article',
269
- author: "John Doe",
270
- _ui: { x: 100, y: 200, collapsed: false }
271
- }
272
- });
142
+ if (!authenticated) throw new Error('Login required');
273
143
  ```
274
144
 
275
- ### Real-time Sync
276
-
277
- Events fire for both local and remote changes. The `source` field indicates origin:
278
-
279
- - `local_user` — This client made the change
280
- - `remote_user` — Another user/client made the change
281
- - `remote_agent` — AI agent made the change
282
- - `system` — Resync after error
145
+ ### Auth API
283
146
 
284
- ```typescript
285
- // All UI updates happen in one place, regardless of change source
286
- channel.on('objectUpdated', ({ objectId, object, source }) => {
287
- renderObject(objectId, object);
288
- if (source === 'remote_agent') {
289
- doLayout(); // AI might have added content
290
- }
291
- });
292
-
293
- // Caller just makes the change - event handler does the UI work
294
- channel.updateObject(objectId, { prompt: 'expand this' });
295
- ```
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. |
296
157
 
297
- ### Custom Object IDs
158
+ ## Spaces and Channels
298
159
 
299
- By default, `createObject` generates a 6-character alphanumeric ID. Provide your own via `data.id`:
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.
300
161
 
301
162
  ```typescript
302
- await channel.createObject({ data: { id: 'article-42', type: 'article', title: 'The Meaning of Life' } });
303
- ```
163
+ const space = await client.openSpace('space-id');
304
164
 
305
- **Why use custom IDs?**
306
- - **Fire-and-forget creation** Know the ID immediately without awaiting the response.
307
- - **Meaningful IDs** — Use domain-specific IDs like `user-123` or `doc-abc` for easier debugging and external references.
165
+ space.on('channelCreated', (channel) => console.log(channel.id));
166
+ space.on('filesChanged', () => console.log('files changed'));
308
167
 
309
- ```typescript
310
- // Fire-and-forget: create and reference without waiting
311
- const id = RoolClient.generateId();
312
- channel.createObject({ data: { id, type: 'note', text: '{{expand this idea}}' } });
313
- channel.updateObject(parentId, { data: { notes: [...existingNotes, id] } }); // Add reference immediately
168
+ const channel = await space.openChannel('main');
169
+ await channel.prompt('Summarize this space');
314
170
  ```
315
171
 
316
- **Constraints:**
317
- - Must contain only alphanumeric characters, hyphens (`-`), and underscores (`_`)
318
- - Must be unique within the space (throws if ID exists)
319
- - Cannot be changed after creation (immutable)
172
+ Channel IDs must be 1–32 characters and contain only letters, numbers, `_`, and `-`.
320
173
 
321
- Use `RoolClient.generateId()` when you need an ID before calling `createObject` but don't need it to be meaningful — it gives you a valid random ID without writing your own generator.
322
-
323
- ## Authentication
174
+ ## Object Operations
324
175
 
325
- ### Browser (Default)
326
-
327
- 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.
328
177
 
329
178
  ```typescript
330
- const client = new RoolClient();
331
- const authenticated = await client.initialize();
332
-
333
- if (!authenticated) {
334
- client.login('My App'); // Redirect to the auth page
335
- }
336
- ```
179
+ await channel.createCollection('article', [
180
+ { name: 'title', type: { kind: 'string' } },
181
+ { name: 'status', type: { kind: 'string' } },
182
+ ]);
337
183
 
338
- ### 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
+ });
339
189
 
340
- 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
+ });
341
194
 
342
- ```typescript
343
- 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
+ ]);
344
201
 
345
- const client = new RoolClient({ authProvider: new NodeAuthProvider() });
346
- 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
+ );
347
207
 
348
- if (!authenticated) {
349
- await client.login('My CLI Tool'); // Opens browser, waits for callback
350
- }
208
+ // Delete objects
209
+ await channel.deleteObjects(['/space/article/hello-world.json']);
351
210
  ```
352
211
 
353
- ### Auth Methods
354
-
355
212
  | Method | Description |
356
- |--------|-------------|
357
- | `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). |
358
- | `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. |
359
- | `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. |
360
- | `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. |
361
- | `logout(): void` | Clear tokens and state |
362
- | `isAuthenticated(): Promise<boolean>` | Check auth status (validates token) |
363
- | `getAuthUser(): AuthUser` | Get auth identity from JWT (`{ email, name }`) |
364
- | `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. |
365
221
 
366
222
  ## AI Agent
367
223
 
368
- The `prompt()` method is the primary way to invoke the AI agent. The agent has editor-level capabilities it can create, modify, 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.
369
225
 
370
226
  ```typescript
371
227
  const { message, objects } = await channel.prompt(
372
- "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.'
373
229
  );
374
- console.log(`AI: ${message}`);
375
- console.log(`Modified ${objects.length} objects:`, objects);
376
- ```
377
-
378
- Use `checkpoint()` before prompting to make operations undoable.
379
230
 
380
- ### Method Signature
381
-
382
- ```typescript
383
- prompt(text: string, options?: PromptOptions): Promise<{ message: string; objects: RoolObject[] }>
231
+ console.log(message);
232
+ console.log(objects.map((object) => object.path));
384
233
  ```
385
234
 
386
- Returns a message (the AI's response) and the list of objects that were created or modified.
387
-
388
- ### Options
235
+ ### Prompt Options
389
236
 
390
237
  | Option | Description |
391
- |--------|-------------|
392
- | `objectIds` | Focus the AI on specific objects (given primary attention in context) |
393
- | `responseSchema` | Request structured JSON instead of text summary |
394
- | `effort` | Effort level: `'QUICK'`, `'STANDARD'` (default), `'REASONING'`, or `'RESEARCH'` |
395
- | `ephemeral` | If true, don't record in interaction history (useful for tab completion) |
396
- | `readOnly` | If true, disable mutation tools (create, update, delete). Use for questions. |
397
- | `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). |
398
- | `attachments` | Files to attach (`File`, `Blob`, or `{ data, contentType }`). Uploaded to the media store via `uploadMedia()`. Resulting URLs are stored on the interaction's `attachments` field 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 uploaded and stored but the AI cannot read their contents. |
399
-
400
- ### Effort Levels
401
-
402
- | Level | Description |
403
- |-------|-------------|
404
- | `QUICK` | Fast, lightweight model. Best for simple questions. |
405
- | `STANDARD` | Default behavior with balanced capabilities. |
406
- | `REASONING` | Extended reasoning for complex tasks. |
407
- | `RESEARCH` | Most thorough mode with deep analysis. Slowest and most credit-intensive. |
408
-
409
- ### Examples
410
-
411
- ```typescript
412
- // Reorganize existing objects
413
- const { objects } = await channel.prompt(
414
- "Group these notes by topic and create a parent node for each group."
415
- );
416
-
417
- // Work with specific objects
418
- const result = await channel.prompt(
419
- "Summarize these articles",
420
- { objectIds: ['article-1', 'article-2'] }
421
- );
422
-
423
- // Quick question without mutations (fast model + read-only)
424
- const { message } = await channel.prompt(
425
- "What topics are covered?",
426
- { effort: 'QUICK', readOnly: true }
427
- );
428
-
429
- // Complex analysis with extended reasoning
430
- await channel.prompt(
431
- "Analyze relationships and reorganize",
432
- { effort: 'REASONING' }
433
- );
434
-
435
- // Attach files for the AI to see (File from <input>, Blob, or base64)
436
- const file = fileInput.files[0]; // from <input type="file">
437
- await channel.prompt(
438
- "Describe what's in this photo and create an object for it",
439
- { attachments: [file] }
440
- );
441
- ```
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
252
+ });
442
253
 
443
- ### Structured Responses
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
+ });
444
261
 
445
- Use `responseSchema` to get structured JSON instead of a text message:
262
+ // Upload a local file as an attachment
263
+ await channel.prompt('Describe this image', {
264
+ attachments: [fileInput.files![0]],
265
+ });
446
266
 
447
- ```typescript
448
- const { message } = await channel.prompt("Categorize these items", {
449
- objectIds: ['item-1', 'item-2', 'item-3'],
267
+ // Structured response
268
+ const { message } = await channel.prompt('Categorize these items', {
450
269
  responseSchema: {
451
270
  type: 'object',
452
271
  properties: {
453
- categories: {
454
- type: 'array',
455
- items: { type: 'string' }
456
- },
457
- summary: { type: 'string' }
458
- }
459
- }
272
+ categories: { type: 'array', items: { type: 'string' } },
273
+ summary: { type: 'string' },
274
+ },
275
+ },
460
276
  });
461
-
462
277
  const result = JSON.parse(message);
463
- console.log(result.categories, result.summary);
464
- ```
465
-
466
- ### Context Flow
467
278
 
468
- AI operations automatically receive context:
469
- - **Interaction history** Previous interactions and their results from this channel
470
- - **Recently modified objects** Objects created or changed recently
471
- - **Selected objects** — Objects passed via `objectIds` are given primary focus
472
-
473
- This context flows automatically — no configuration needed. The AI sees enough history to maintain coherent interactions while respecting the `_`-prefixed field hiding rules.
474
-
475
- ## Collaboration
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,
284
+ });
285
+ ac.abort(); // asks the server to stop the in-flight interaction
286
+ await promptPromise;
287
+ ```
476
288
 
477
- ### Adding Users to a Space
289
+ ### Stopping interactions
478
290
 
479
- To add a user to a space, you need their user ID. Use `searchUser()` to find them by email:
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.
480
297
 
481
298
  ```typescript
482
- // Find the user by email
483
- const user = await client.searchUser('colleague@example.com');
484
- if (!user) {
485
- throw new Error('User not found');
486
- }
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();
487
302
 
488
- // Add them to the space
489
- const space = await client.openSpace('space-id');
490
- await space.addUser(user.id, 'editor');
491
- ```
492
-
493
- ### Roles
494
-
495
- | Role | Capabilities |
496
- |------|--------------|
497
- | `owner` | Full control, can delete space and manage all users |
498
- | `admin` | All editor capabilities, plus can manage users (except other admins/owners) |
499
- | `editor` | Can create, modify, and delete objects |
500
- | `viewer` | Read-only access (can query with `prompt` and `findObjects`) |
501
-
502
- ### Space Collaboration Methods
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!);
503
306
 
504
- These methods are available on `RoolSpace`:
307
+ // Conversation handles stop their own in-flight interaction.
308
+ const thread = channel.conversation('thread-42');
309
+ await thread.stop();
310
+ ```
505
311
 
506
312
  | Method | Description |
507
- |--------|-------------|
508
- | `listUsers(): Promise<SpaceMember[]>` | List users with access |
509
- | `addUser(userId, role): Promise<void>` | Add user to space (requires owner or admin role) |
510
- | `removeUser(userId): Promise<void>` | Remove user from space (requires owner or admin role) |
511
- | `setLinkAccess(linkAccess): Promise<void>` | Set URL sharing level (requires owner or admin role) |
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. |
512
317
 
513
- ### URL Sharing
318
+ ## Conversations
514
319
 
515
- Enable public URL access to allow anyone with the space URL to access it:
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.
516
321
 
517
322
  ```typescript
518
- const space = await client.openSpace('space-id');
323
+ await channel.prompt('Hello'); // default conversation
519
324
 
520
- // Allow anyone with the URL to view
521
- await space.setLinkAccess('viewer');
522
-
523
- // Allow anyone with the URL to edit
524
- await space.setLinkAccess('editor');
325
+ const thread = channel.conversation('thread-42');
326
+ await thread.prompt('Hello from another thread');
327
+ await thread.setSystemInstruction('Answer in haiku');
525
328
 
526
- // Disable URL access (default)
527
- await space.setLinkAccess('none');
329
+ const branch = thread.getInteractions(); // active branch, root → leaf
330
+ const tree = thread.getTree(); // full interaction tree
528
331
 
529
- // Check current setting
530
- console.log(space.linkAccess); // 'none' | 'viewer' | 'editor'
332
+ if (thread.activeLeafId) {
333
+ thread.setActiveLeaf(thread.activeLeafId);
334
+ }
531
335
  ```
532
336
 
533
- When a user accesses a space via URL, they're granted the corresponding role (`viewer` or `editor`) based on the space's `linkAccess` setting.
534
-
535
- ### Client User Methods
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. |
536
349
 
537
- | Method | Description |
538
- |--------|-------------|
539
- | `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. |
540
- | `getCurrentUser(): Promise<CurrentUser>` | Fetch fresh user profile from server (id, email, name, photoUrl, slug, plan, creditsBalance, totalCreditsUsed, createdAt, lastActivity, processedAt, storage) |
541
- | `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. |
542
- | `searchUser(email): Promise<UserResult \| null>` | Find user by exact email address (no partial matching) |
350
+ `ConversationHandle` also supports conversation-scoped `putObject`, `patchObject`, `moveObject`, `deleteObjects`, `prompt`, `stop`, collection-schema methods, and `setMetadata`.
543
351
 
544
- ### Real-time Collaboration
352
+ ## Schema and Metadata
545
353
 
546
- When multiple users have a space open, changes sync in real-time. The `source` field in events tells you who made the change:
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.
547
355
 
548
356
  ```typescript
549
- channel.on('objectUpdated', ({ objectId, object, source }) => {
550
- if (source === 'remote_user') {
551
- // Another user made this change
552
- showCollaboratorActivity(object);
553
- }
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
+ ],
554
365
  });
555
- ```
556
-
557
- See [Real-time Sync](#real-time-sync) for more on event sources.
558
366
 
559
- ## RoolClient API
560
-
561
- ### Logging
562
-
563
- By default the SDK logs errors to the console. Pass a `logger` to see more or customize output:
564
-
565
- ```typescript
566
- // Default — errors only
567
- const client = new RoolClient();
367
+ const schema = channel.getSchema();
568
368
 
569
- // Log everything to console
570
- const client = new RoolClient({ logger: console });
369
+ await channel.alterCollection('article', [
370
+ { name: 'title', type: { kind: 'string' } },
371
+ { name: 'status', type: { kind: 'string' } },
372
+ ]);
571
373
 
572
- // Bring your own logger (pino, winston, etc.)
573
- const client = new RoolClient({
574
- logger: myLogger // any object with { debug, info, warn, error }
575
- });
374
+ channel.setMetadata('viewport', { x: 0, y: 0, zoom: 1 });
375
+ const viewport = channel.getMetadata('viewport');
576
376
  ```
577
377
 
578
- ### Space & Channel Lifecycle
579
-
580
378
  | Method | Description |
581
- |--------|-------------|
582
- | `listSpaces(): Promise<RoolSpaceInfo[]>` | List available spaces |
583
- | `openSpace(spaceId): Promise<RoolSpace>` | Open a space with live SSE subscription. Caches and reuses open spaces. Call `space.openChannel(channelId)` to get a channel. |
584
- | `createSpace(name): Promise<RoolSpace>` | Create a new space, returns live handle with SSE subscription |
585
- | `deleteSpace(id): Promise<void>` | Permanently delete a space (cannot be undone) |
586
- | `importArchive(name, archive): Promise<RoolSpace>` | Import from a zip archive, creating a new space |
587
-
588
- ### Channel Management
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. |
589
387
 
590
- Manage channels on the `RoolSpace` handle:
591
-
592
- | Method | Description |
593
- |--------|-------------|
594
- | `space.channels: ChannelInfo[]` | Live channel list (auto-updates via SSE) |
595
- | `space.getChannels(): ChannelInfo[]` | List channels (deprecated — use `space.channels` instead) |
596
- | `space.renameChannel(channelId, name): Promise<void>` | Rename a channel |
597
- | `space.deleteChannel(channelId): Promise<void>` | Delete a channel and its interaction history |
598
- | `channel.rename(name): Promise<void>` | Rename the current open channel |
388
+ Field kinds: `string`, `number`, `boolean`, `ref`, `enum`, `literal`, `array`, and `maybe`.
599
389
 
600
- ### User Storage
390
+ ## Undo/Redo
601
391
 
602
- Server-side key-value storage for user preferences, UI state, and other persistent data. Replaces browser localStorage with cross-device, server-synced storage.
603
-
604
- **Features:**
605
- - Fresh data fetched from server on `initialize()` — cache is authoritative after init
606
- - Sync reads from local cache (fast, no network round-trip)
607
- - Automatic sync to server and across tabs/devices via SSE
608
- - `userStorageChanged` event fires on all changes (local or remote)
609
- - Total storage limited to 10MB per user
610
-
611
- | Method | Description |
612
- |--------|-------------|
613
- | `getUserStorage<T>(key): T \| undefined` | Get a value (sync, from cache) |
614
- | `setUserStorage(key, value): void` | Set a value (updates cache, syncs to server) |
615
- | `getAllUserStorage(): Record<string, unknown>` | Get all stored data (sync, from cache) |
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.
616
393
 
617
394
  ```typescript
618
- // After initialize(), storage is fresh from server
619
- const authenticated = await client.initialize();
620
-
621
- // Sync reads are now trustworthy
622
- const theme = client.getUserStorage<string>('theme');
623
- applyTheme(theme ?? 'light');
624
-
625
- // Write - updates immediately, syncs to server in background
626
- client.setUserStorage('theme', 'dark');
627
- client.setUserStorage('sidebar', { collapsed: true, width: 280 });
628
-
629
- // Delete a key
630
- client.setUserStorage('theme', null);
395
+ await channel.checkpoint('Delete article');
396
+ await channel.deleteObjects(['/space/article/welcome.json']);
631
397
 
632
- // Listen for changes from other tabs/devices
633
- client.on('userStorageChanged', ({ key, value, source }) => {
634
- // source: 'local' (this client) or 'remote' (server/other client)
635
- if (key === 'theme') applyTheme(value as string);
636
- });
398
+ if (await channel.canUndo()) {
399
+ await channel.undo();
400
+ }
637
401
  ```
638
402
 
639
- ### Extensions
640
-
641
- Manage and publish extensions. See [`@rool-dev/extension`](/extension/) for building extensions.
642
-
643
- There are two distinct domains: your **personal library** (extensions you've created or installed) and the **published extensions** (extensions discoverable by all users). Each has its own return type.
644
-
645
- #### Your Library (`ExtensionInfo`)
646
-
647
- Manage extensions you own. Each `ExtensionInfo` includes `published` (whether it's listed in the marketplace) and `marketplaceExtensionId` (non-null if you installed it from someone else's listing, null if you authored it).
648
-
649
- | Method | Description |
650
- |--------|-------------|
651
- | `uploadExtension(extensionId, options): Promise<ExtensionInfo>` | Upload or update an extension (`options.bundle`: zip with `index.html` and `manifest.json`) |
652
- | `listExtensions(): Promise<ExtensionInfo[]>` | List your extensions |
653
- | `getExtensionInfo(extensionId): Promise<ExtensionInfo \| null>` | Get info for a specific extension |
654
- | `deleteExtension(extensionId): Promise<void>` | Delete an extension permanently (removes files and DB row) |
655
-
656
- #### Marketplace (`PublishedExtensionInfo`)
657
-
658
- Discover and install extensions published by other users.
659
-
660
- | Method | Description |
661
- |--------|-------------|
662
- | `findExtensions(options?): Promise<PublishedExtensionInfo[]>` | Search the marketplace. Options: `query` (semantic search string), `limit` (default 20, max 100). Omit `query` to browse all. |
663
- | `installExtension(spaceId, extensionId, channelId): Promise<string>` | Install an extension into a space. If you own it, wires it directly. If it's a marketplace extension, copies and builds a new extension in your library. Returns the channel ID. |
664
- | `publishToPublic(extensionId): Promise<void>` | Publish one of your extensions to the marketplace |
665
- | `unpublishFromPublic(extensionId): Promise<void>` | Remove from the marketplace (keeps the extension in your library) |
666
-
667
- ### Utilities
668
-
669
403
  | Method | Description |
670
- |--------|-------------|
671
- | `RoolClient.generateId(): string` | Generate 6-char alphanumeric ID (static) |
672
- | `destroy(): void` | Clean up resources |
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. |
673
411
 
674
- ### Client Events
412
+ Undo/redo availability and history are scoped to the channel handle (`channel.channelId`).
675
413
 
676
- ```typescript
677
- client.on('authStateChanged', (authenticated: boolean) => void)
678
- client.on('spaceAdded', (space: RoolSpaceInfo) => void) // Space created or access granted
679
- client.on('spaceRemoved', (spaceId: string) => void) // Space deleted or access revoked
680
- client.on('spaceRenamed', (spaceId: string, newName: string) => void)
681
- client.on('channelCreated', (spaceId: string, channel: ChannelInfo) => void)
682
- client.on('channelUpdated', (spaceId: string, channel: ChannelInfo) => void)
683
- client.on('channelDeleted', (spaceId: string, channelId: string) => void)
684
- client.on('userStorageChanged', ({ key, value, source }: UserStorageChangedEvent) => void)
685
- client.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)
686
- client.on('error', (error: Error, context?: string) => void)
687
- ```
414
+ ## File Storage and WebDAV
688
415
 
689
- 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.
416
+ Every space has authenticated WebDAV storage. WebDAV methods take SDK machine paths such as `/space/...`, `/rool-drive/...`, or `/` for the root collection.
690
417
 
691
- **Space list management pattern:**
692
418
  ```typescript
693
- const spaces = new Map<string, RoolSpaceInfo>();
419
+ const webdav = space.webdav;
694
420
 
695
- client.on('spaceAdded', (space) => spaces.set(space.id, space));
696
- client.on('spaceRemoved', (id) => spaces.delete(id));
697
- client.on('spaceRenamed', (id, name) => {
698
- const space = spaces.get(id);
699
- if (space) spaces.set(id, { ...space, name });
421
+ await webdav.mkcol('/rool-drive/docs');
422
+ await webdav.put('/rool-drive/docs/readme.md', '# Hello', {
423
+ contentType: 'text/markdown',
424
+ ifNoneMatch: '*',
700
425
  });
701
- ```
702
-
703
- ## RoolSpace API
704
-
705
- A space handle with a live SSE subscription. Extends `EventEmitter`. Manages user access, link sharing, channels, and export. The `channels` property auto-updates via SSE, and channel lifecycle events fire in real-time.
706
426
 
707
- `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.
708
-
709
- ### Properties
427
+ const listing = await webdav.propfind('/rool-drive/docs', {
428
+ depth: '1',
429
+ props: ['displayname', 'getcontentlength', 'getcontenttype', 'getetag'],
430
+ });
710
431
 
711
- | Property | Description |
712
- |----------|-------------|
713
- | `id: string` | Space ID |
714
- | `name: string` | Space name |
715
- | `role: RoolUserRole` | User's role |
716
- | `linkAccess: LinkAccess` | URL sharing level |
717
- | `memberCount: number` | Number of users with access to the space |
718
- | `channels: ChannelInfo[]` | Live channel list (auto-updates via SSE) |
432
+ const response = await webdav.get('/rool-drive/docs/readme.md');
433
+ console.log(await response.text());
719
434
 
720
- ### Methods
435
+ const file = await space.fetchPath('/rool-drive/docs/readme.md');
436
+ console.log(file.headers.get('Content-Type'));
721
437
 
722
- | Method | Description |
723
- |--------|-------------|
724
- | `openChannel(channelId): Promise<RoolChannel>` | Open a channel on this space |
725
- | `close(): void` | Stop SSE subscription and close all open channels |
726
- | `rename(newName): Promise<void>` | Rename this space |
727
- | `delete(): Promise<void>` | Permanently delete this space |
728
- | `listUsers(): Promise<SpaceMember[]>` | List users with access |
729
- | `addUser(userId, role): Promise<void>` | Add user to space |
730
- | `removeUser(userId): Promise<void>` | Remove user from space |
731
- | `setLinkAccess(linkAccess): Promise<void>` | Set URL sharing level |
732
- | `getChannels(): ChannelInfo[]` | List channels (deprecated — use `channels` property instead) |
733
- | `renameChannel(channelId, name): Promise<void>` | Rename a channel |
734
- | `deleteChannel(channelId): Promise<void>` | Delete a channel |
735
- | `exportArchive(): Promise<Blob>` | Export space as zip archive |
736
- | `refresh(): Promise<void>` | Refresh space data from server |
737
-
738
- ### Space Events
739
-
740
- ```typescript
741
- space.on('channelCreated', (channel: ChannelInfo) => void) // New channel added
742
- space.on('channelUpdated', (channel: ChannelInfo) => void) // Channel metadata changed (name, extension, manifest)
743
- space.on('channelDeleted', (channelId: string) => void) // Channel removed
744
- space.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)
438
+ const usage = await space.getStorageUsage();
439
+ console.log(usage.usedBytes, usage.availableBytes, usage.limitBytes);
745
440
  ```
746
441
 
747
- ## RoolChannel API
748
-
749
- 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.
750
-
751
- ### Properties
752
-
753
- | Property | Description |
754
- |----------|-------------|
755
- | `id: string` | Space ID |
756
- | `name: string` | Space name |
757
- | `role: RoolUserRole` | User's role (`'owner' \| 'admin' \| 'editor' \| 'viewer'`) |
758
- | `linkAccess: LinkAccess` | URL sharing level (`'none' \| 'viewer' \| 'editor'`) |
759
- | `userId: string` | Current user's ID |
760
- | `channelId: string` | Channel ID (read-only, fixed at open time) |
761
- | `isReadOnly: boolean` | True if viewer role |
762
- | `extensionUrl: string \| null` | URL of the installed extension, or null if this is a plain channel |
763
- | `extensionId: string \| null` | ID of the installed extension, or null if this is a plain channel |
764
- | `manifest: ExtensionManifest \| null` | Extension manifest snapshot (name, icon, collections, etc.), or null |
765
-
766
- ### Lifecycle
767
-
768
- | Method | Description |
769
- |--------|-------------|
770
- | `close(): void` | Clean up resources and stop receiving updates |
771
- | `rename(name): Promise<void>` | Rename this channel |
772
- | `conversation(conversationId): ConversationHandle` | Get a handle scoped to a specific conversation (see [Conversations](#conversations)) |
773
-
774
- ### Object Operations
775
-
776
- Objects are plain key/value records. `id` and `type` are reserved; everything else is application-defined. References between objects are data fields whose values are object IDs. Every object must include a `type` field whose value names a collection in the schema (see [Collection Schema](#collection-schema)) — that binds the object to that collection. Before introducing a new kind of object, create the matching collection.
777
-
778
- | Method | Description |
779
- |--------|-------------|
780
- | `getObject(objectId): Promise<RoolObject \| undefined>` | Get object data, or undefined if not found. |
781
- | `stat(objectId): RoolObjectStat \| undefined` | Get object stat (audit info: modifiedAt, modifiedBy, modifiedByName, and the channel/conversation/interaction where the last write happened), or undefined if not found. Sync read from local cache. |
782
- | `findObjects(options): Promise<{ objects, message }>` | Find objects using structured filters and natural language. Results sorted by modifiedAt (desc by default). |
783
- | `getObjectIds(options?): string[]` | Get all object IDs. Sorted by modifiedAt (desc by default). Options: `{ limit?, order? }`. |
784
- | `createObject(options): Promise<{ object, message }>` | Create a new object. Returns the object (with AI-filled content) and message. |
785
- | `updateObject(objectId, options): Promise<{ object, message }>` | Update an existing object. Returns the updated object and message. |
786
- | `deleteObjects(objectIds): Promise<void>` | Delete objects. Other objects referencing deleted objects retain stale ref values. |
787
-
788
- #### createObject Options
789
-
790
- | Option | Description |
791
- |--------|-------------|
792
- | `data` | Object data fields (required). Must include `type` naming an existing collection. Include `id` to use a custom ID. Use `{{placeholder}}` for AI-generated content. Fields prefixed with `_` are hidden from AI. |
793
- | `ephemeral` | If true, the operation won't be recorded in interaction history. Useful for transient operations. |
794
-
795
- #### updateObject Options
442
+ ### Real-time file sync
796
443
 
797
- | Option | Description |
798
- |--------|-------------|
799
- | `data` | Fields to add or update. Pass `null`/`undefined` to delete a field. Use `{{placeholder}}` for AI-generated content. Setting a new `type` retypes the object — the merged result must conform to the new collection. Fields prefixed with `_` are hidden from AI. |
800
- | `prompt` | Natural language instruction for AI to modify content. |
801
- | `ephemeral` | If true, the operation won't be recorded in interaction history. Useful for transient operations. |
802
-
803
- #### findObjects Options
804
-
805
- Find objects using structured filters and/or natural language.
806
-
807
- - **`where` only** — exact-match filtering, no AI, no credits.
808
- - **`collection` only** — filter by collection name (matches objects whose `type` field equals the name), no AI, no credits.
809
- - **`prompt` only** — AI-powered semantic query over all objects.
810
- - **`where` + `prompt`** — `where` (and `objectIds`) narrow the data set first, then the AI queries within the constrained set.
811
-
812
- | Option | Description |
813
- |--------|-------------|
814
- | `where` | Exact-match field filter (e.g. `{ status: 'published' }`). Values must match literally — no operators or `{{placeholders}}`. When combined with `prompt`, constrains which objects the AI can see. |
815
- | `collection` | Filter by collection name. Returns objects whose `type` field equals the given name. |
816
- | `prompt` | Natural language query. Triggers AI evaluation (uses credits). |
817
- | `limit` | Maximum number of results. |
818
- | `objectIds` | Scope to specific object IDs. Constrains the candidate set in both structured and AI queries. |
819
- | `order` | Sort order by modifiedAt: `'asc'` or `'desc'` (default: `'desc'`). |
820
- | `ephemeral` | If true, the query won't be recorded in interaction history. Useful for responsive search. |
821
-
822
- **Examples:**
444
+ Object and file changes are announced at the space level. Use WebDAV `syncCollection()` to reconcile changes.
823
445
 
824
446
  ```typescript
825
- // Filter by collection (no AI, no credits)
826
- const { objects } = await channel.findObjects({
827
- collection: 'article'
828
- });
447
+ let token: string | null = null;
829
448
 
830
- // Exact field matching (no AI, no credits)
831
- const { objects } = await channel.findObjects({
832
- where: { status: 'published' }
833
- });
834
-
835
- // Combine collection and field filters
836
- const { objects } = await channel.findObjects({
837
- collection: 'article',
838
- where: { status: 'published' }
839
- });
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
+ }
840
458
 
841
- // Pure natural language query (AI interprets)
842
- const { objects, message } = await channel.findObjects({
843
- prompt: 'articles about space exploration published this year'
459
+ space.on('filesChanged', syncFiles);
460
+ space.on('filesReset', () => {
461
+ token = null;
462
+ void syncFiles();
844
463
  });
845
464
 
846
- // Combined: collection + where narrow the data, prompt queries within it
847
- const { objects } = await channel.findObjects({
848
- collection: 'article',
849
- prompt: 'that discuss climate solutions positively',
850
- limit: 10
851
- });
465
+ await syncFiles();
852
466
  ```
853
467
 
854
- When `where` or `objectIds` are provided with a `prompt`, the AI only sees the filtered subset — not the full space. The returned `message` explains the query result.
855
-
856
- ### Undo/Redo
857
-
858
- | Method | Description |
859
- |--------|-------------|
860
- | `checkpoint(label?): Promise<string>` | Call before mutations. Saves current state for undo. |
861
- | `canUndo(): Promise<boolean>` | Check if undo available |
862
- | `canRedo(): Promise<boolean>` | Check if redo available |
863
- | `undo(): Promise<boolean>` | Undo to previous checkpoint |
864
- | `redo(): Promise<boolean>` | Redo undone action |
865
- | `clearHistory(): Promise<void>` | Clear undo/redo stack |
866
-
867
- See [Checkpoints & Undo/Redo](#checkpoints--undoredo) for semantics.
868
-
869
- ### Space Metadata
870
-
871
- Store arbitrary data alongside the Space without it being part of the object data (e.g., viewport state, user preferences).
872
-
873
468
  | Method | Description |
874
- |--------|-------------|
875
- | `setMetadata(key, value): void` | Set space-level metadata |
876
- | `getMetadata(key): unknown` | Get metadata value, or undefined if key not set |
877
- | `getAllMetadata(): Record<string, unknown>` | Get all metadata |
878
-
879
- ### Media
880
-
881
- Media URLs in object fields are visible to AI. Both uploaded and AI-generated media work the same way — use `fetchMedia` to retrieve them for display.
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`.
882
486
 
883
- | Method | Description |
884
- |--------|-------------|
885
- | `uploadMedia(file): Promise<string>` | Upload file, returns URL |
886
- | `fetchMedia(url, options?): Promise<MediaResponse>` | Fetch any URL, returns headers and blob() method (adds auth for backend URLs, works for external URLs too). Pass `{ forceProxy: true }` to skip the direct fetch and route through the server proxy immediately. |
887
- | `deleteMedia(url): Promise<void>` | Delete media file by URL |
888
- | `listMedia(): Promise<MediaInfo[]>` | List all media with metadata |
487
+ ## Collaboration
889
488
 
890
489
  ```typescript
891
- // Upload an image
892
- const url = await channel.uploadMedia(file);
893
- await channel.createObject({ data: { type: 'photo', title: 'Photo', image: url } });
894
-
895
- // Or let AI generate one using a placeholder
896
- await channel.createObject({
897
- data: { type: 'photo', title: 'Mascot', image: '{{generate an image of a flying tortoise}}' }
898
- });
899
-
900
- // Display media (handles auth automatically)
901
- const response = await channel.fetchMedia(object.image);
902
- if (response.contentType.startsWith('image/')) {
903
- const blob = await response.blob();
904
- img.src = URL.createObjectURL(blob);
490
+ const user = await client.searchUser('colleague@example.com');
491
+ if (user) {
492
+ await space.addUser(user.id, 'editor');
905
493
  }
906
- ```
907
-
908
- ### Proxied Fetch
909
-
910
- Fetch external URLs via the server, bypassing CORS restrictions. Requires editor role or above. Private/internal IP ranges are blocked (SSRF protection).
911
-
912
- | Method | Description |
913
- |--------|-------------|
914
- | `fetch(url, init?): Promise<Response>` | Fetch a URL via the server proxy. `init` accepts `method`, `headers`, and `body`. |
915
494
 
916
- ```typescript
917
- // GET request
918
- const response = await channel.fetch('https://api.example.com/data');
919
- const data = await response.json();
920
-
921
- // POST with headers and body
922
- const response = await channel.fetch('https://api.example.com/submit', {
923
- method: 'POST',
924
- headers: { 'Content-Type': 'application/json' },
925
- body: { key: 'value' },
926
- });
495
+ await space.setLinkAccess('viewer'); // 'none' | 'viewer' | 'editor'
927
496
  ```
928
497
 
929
- ### Collection Schema
498
+ Roles:
930
499
 
931
- Collections are the types you use to group objects in a space. Every object must belong to a collection: the object's `data.type` field names the collection it belongs to, and the server validates the object's fields against that collection's definition. Renaming a collection cascades to the `type` field of every object bound to it; dropping a collection is blocked while any object is still bound to it.
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. |
932
506
 
933
- 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.
507
+ ## RoolClient API
934
508
 
509
+ ### Constructor config
935
510
 
936
511
  ```typescript
937
- // Define a collection with typed fields
938
- await channel.createCollection('article', [
939
- { name: 'title', type: { kind: 'string' } },
940
- { name: 'status', type: { kind: 'enum', values: ['draft', 'published', 'archived'] } },
941
- { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } },
942
- { name: 'author', type: { kind: 'ref' } },
943
- ]);
944
-
945
- // Read the current schema
946
- const schema = channel.getSchema();
947
- console.log(schema.article.fields); // FieldDef[]
948
-
949
- // Modify an existing collection's fields
950
- await channel.alterCollection('article', [
951
- { name: 'title', type: { kind: 'string' } },
952
- { name: 'status', type: { kind: 'enum', values: ['draft', 'review', 'published', 'archived'] } },
953
- { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } },
954
- { name: 'author', type: { kind: 'ref' } },
955
- { name: 'wordCount', type: { kind: 'number' } },
956
- ]);
957
-
958
- // Remove a collection
959
- await channel.dropCollection('article');
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
+ });
960
518
  ```
961
519
 
962
- | Method | Description |
963
- |--------|-------------|
964
- | `getSchema(): SpaceSchema` | Get all collection definitions |
965
- | `createCollection(name, fields): Promise<CollectionDef>` | Add a new collection to the schema |
966
- | `alterCollection(name, fields): Promise<CollectionDef>` | Replace a collection's field definitions |
967
- | `dropCollection(name): Promise<void>` | Remove a collection from the schema |
968
-
969
- #### Field Types
970
-
971
- | Kind | Description | Example |
972
- |------|-------------|---------|
973
- | `string` | Text value | `{ kind: 'string' }` |
974
- | `number` | Numeric value | `{ kind: 'number' }` |
975
- | `boolean` | True/false | `{ kind: 'boolean' }` |
976
- | `ref` | Reference to another object | `{ kind: 'ref' }` |
977
- | `enum` | One of a set of values | `{ kind: 'enum', values: ['a', 'b'] }` |
978
- | `literal` | Exact value | `{ kind: 'literal', value: 'fixed' }` |
979
- | `array` | List of values | `{ kind: 'array', inner: { kind: 'string' } }` |
980
- | `maybe` | Optional (nullable) | `{ kind: 'maybe', inner: { kind: 'number' } }` |
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);
555
+ ```
981
556
 
982
- ### Import/Export
557
+ ## RoolSpace API
983
558
 
984
- Export and import space data as zip archives for backup, portability, or migration:
559
+ Properties: `id`, `name`, `role`, `linkAccess`, `memberCount`, `channels`, `route`, `webdav`.
985
560
 
986
561
  | Method | Description |
987
- |--------|-------------|
988
- | `space.exportArchive(): Promise<Blob>` | Export objects, metadata, channels, and media as a zip archive |
989
- | `client.importArchive(name, archive): Promise<RoolSpace>` | Import from a zip archive, creating a new space |
990
-
991
- **Export:**
992
- ```typescript
993
- const space = await client.openSpace('space-id');
994
- const archive = await space.exportArchive();
995
- // Save as .zip file
996
- const url = URL.createObjectURL(archive);
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);
997
587
  ```
998
588
 
999
- **Import:**
1000
- ```typescript
1001
- const space = await client.importArchive('Imported Data', archiveBlob);
1002
- const channel = await space.openChannel('main');
1003
- ```
589
+ ## RoolChannel API
1004
590
 
1005
- The archive format bundles `data.json` (with objects, metadata, and channels) and a `media/` folder containing all media files. Media URLs are rewritten to relative paths within the archive and restored on import.
591
+ Properties: `id` (space ID), `name` (space name), `role`, `linkAccess`, `userId`, `channelId`, `channelName`, `conversationId`, `isReadOnly`, `activeLeafId`.
1006
592
 
1007
- ### Channel Events
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 |
1008
603
 
1009
- Semantic events describe what changed. Events fire for both local changes and remote changes.
604
+ Channel events:
1010
605
 
1011
606
  ```typescript
1012
- // source indicates origin:
1013
- // - 'local_user': This client made the change
1014
- // - 'remote_user': Another user/client made the change
1015
- // - 'remote_agent': AI agent made the change
1016
- // - 'system': Resync after error
1017
-
1018
- // Object events
1019
- channel.on('objectCreated', ({ objectId, object, source }) => void)
1020
- channel.on('objectUpdated', ({ objectId, object, source }) => void)
1021
- channel.on('objectDeleted', ({ objectId, source }) => void)
1022
-
1023
- // Space metadata
1024
- channel.on('metadataUpdated', ({ metadata, source }) => void)
1025
-
1026
- // Collection schema changed
1027
- channel.on('schemaUpdated', ({ schema, source }) => void)
1028
-
1029
- // Channel metadata updated (name, extensionUrl)
1030
- channel.on('channelUpdated', ({ channelId, source }) => void)
1031
-
1032
- // Conversation interaction history updated
1033
- channel.on('conversationUpdated', ({ conversationId, channelId, source }) => void)
1034
-
1035
- // Full state replacement (undo/redo, resync after error)
1036
- channel.on('reset', ({ source }) => void)
1037
-
1038
- // Sync error occurred, channel resynced from server
1039
- channel.on('syncError', (error: Error) => void)
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);
1040
613
  ```
1041
614
 
1042
- ### Error Handling
615
+ `channel.fetch(url, init?)` proxies external HTTP requests through the server to bypass browser CORS.
1043
616
 
1044
- AI operations may fail due to rate limiting or other transient errors. Check `error.message` for user-friendly error text:
617
+ ## Import/Export
1045
618
 
1046
619
  ```typescript
1047
- try {
1048
- await channel.updateObject(objectId, { prompt: 'expand this' });
1049
- } catch (error) {
1050
- if (error.message.includes('temporarily unavailable')) {
1051
- showToast('Service busy, please try again in a moment');
1052
- } else {
1053
- showToast(error.message);
1054
- }
1055
- }
620
+ const archive = await space.exportArchive();
621
+ const imported = await client.importArchive('Imported Data', archive);
1056
622
  ```
1057
623
 
1058
- ## Interaction History
1059
-
1060
- 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.
1061
-
1062
- ### Conversation History Methods
1063
-
1064
- | Method | Description |
1065
- |--------|-------------|
1066
- | `getInteractions(): Interaction[]` | Get the active branch as a flat array (root → leaf) |
1067
- | `getTree(): Record<string, Interaction>` | Get the full interaction tree for branch navigation |
1068
- | `activeLeafId: string \| undefined` | The tip of the currently active branch |
1069
- | `setActiveLeaf(id: string): void` | Switch to a different branch (emits `conversationUpdated`) |
1070
- | `getSystemInstruction(): string \| undefined` | Get system instruction for the default conversation |
1071
- | `setSystemInstruction(instruction): Promise<void>` | Set system instruction for the default conversation. Pass `null` to clear. |
1072
- | `getConversations(): ConversationInfo[]` | List all conversations in this channel |
1073
- | `deleteConversation(conversationId): Promise<void>` | Delete a conversation (cannot delete `'default'`) |
1074
- | `renameConversation(name): Promise<void>` | Rename the default conversation |
1075
-
1076
- Channel management (listing, renaming, deleting channels) is done via the client — see [Channel Management](#channel-management).
1077
-
1078
- ### The ai Field
1079
-
1080
- The `ai` field in interactions distinguishes AI-generated responses from synthetic confirmations:
1081
- - `ai: true` — AI processed this operation (prompt, or createObject/updateObject with placeholders)
1082
- - `ai: false` — System confirmation only (e.g., "Created object abc123")
1083
-
1084
- ### Tool Calls
1085
-
1086
- The `toolCalls` array captures what the AI agent did during execution. The `conversationUpdated` event fires when each tool starts and completes. A tool call without a `result` is still running; once `result` is present, the tool has finished.
624
+ Archives include objects, metadata, channels/conversations, and file storage.
1087
625
 
1088
626
  ## Data Types
1089
627
 
1090
- ### Schema Types
1091
-
1092
628
  ```typescript
1093
- // Allowed field types
1094
629
  type FieldType =
1095
630
  | { kind: 'string' }
1096
631
  | { kind: 'number' }
@@ -1108,141 +643,66 @@ interface FieldDef {
1108
643
 
1109
644
  interface CollectionDef {
1110
645
  fields: FieldDef[];
646
+ schemaOrgType?: string;
1111
647
  }
1112
648
 
1113
- // Full schema — collection names to definitions
1114
649
  type SpaceSchema = Record<string, CollectionDef>;
1115
- ```
1116
-
1117
- ### Object Data
1118
650
 
1119
- ```typescript
1120
- // RoolObject represents the object data you work with
1121
- // Always contains `id`, plus any additional fields
1122
- // Fields prefixed with _ are hidden from AI
1123
- // References between objects are fields whose values are object IDs
1124
651
  interface RoolObject {
1125
- id: string;
1126
- [key: string]: unknown;
652
+ path: string;
653
+ body: Record<string, unknown>;
654
+ }
655
+
656
+ interface GetObjectsResult {
657
+ objects: RoolObject[];
658
+ missing: string[];
1127
659
  }
1128
660
 
1129
- // Object stat - audit information returned by channel.stat()
1130
661
  interface RoolObjectStat {
662
+ path: string;
1131
663
  modifiedAt: number;
1132
664
  modifiedBy: string;
1133
665
  modifiedByName: string | null;
1134
- modifiedInChannel: string; // Channel ID where the last modification happened
1135
- modifiedInConversation: string | null; // Conversation ID, or null if not conversation-scoped
1136
- modifiedInInteraction: string | null; // Interaction ID, or null for ephemeral or non-AI writes
1137
- }
1138
- ```
1139
-
1140
- ### Channels and Conversations
1141
-
1142
- ```typescript
1143
- // Conversation — holds interaction tree and optional system instruction
1144
- interface Conversation {
1145
- name?: string; // Conversation name (optional)
1146
- systemInstruction?: string; // Custom system instruction for AI
1147
- createdAt: number; // Timestamp when conversation was created
1148
- createdBy: string; // User ID who created the conversation
1149
- interactions: Record<string, Interaction>; // Interaction tree (keyed by ID, linked by parentId)
1150
- }
1151
-
1152
- // Conversation summary info (returned by channel.getConversations())
1153
- interface ConversationInfo {
1154
- id: string;
1155
- name: string | null;
1156
- systemInstruction: string | null;
1157
- createdAt: number;
1158
- createdBy: string;
1159
- interactionCount: number;
1160
- }
1161
-
1162
- // Channel container with metadata and conversations
1163
- interface Channel {
1164
- name?: string; // Channel name (optional)
1165
- createdAt: number; // Timestamp when channel was created
1166
- createdBy: string; // User ID who created the channel
1167
- createdByName?: string; // Display name at time of creation
1168
- extensionUrl?: string; // URL of installed extension (set by installExtension)
1169
- extensionId?: string; // ID of installed extension (user_extensions.extension_id)
1170
- manifest?: ExtensionManifest; // Extension manifest snapshot (set when extension is wired)
1171
- conversations: Record<string, Conversation>; // Keyed by conversation ID
1172
- }
1173
-
1174
- // Channel summary info (returned by client.getChannels)
1175
- interface ChannelInfo {
1176
- id: string;
1177
- name: string | null;
1178
- createdAt: number;
1179
- createdBy: string;
1180
- createdByName: string | null;
1181
- interactionCount: number;
1182
- extensionUrl: string | null; // URL of installed extension, or null
1183
- extensionId: string | null; // ID of installed extension, or null
1184
- manifest: ExtensionManifest | null; // Extension manifest snapshot, or null
666
+ modifiedInChannel: string;
667
+ modifiedInConversation: string | null;
668
+ modifiedInInteraction: string | null;
1185
669
  }
1186
- ```
1187
670
 
1188
- 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.
671
+ type PromptAttachment =
672
+ | File
673
+ | Blob
674
+ | { data: string; contentType: string; filename?: string }
675
+ | string;
1189
676
 
1190
- ### Interaction Types
677
+ type PromptEffort = 'QUICK' | 'STANDARD' | 'REASONING' | 'RESEARCH';
1191
678
 
1192
- ```typescript
1193
- interface ToolCall {
1194
- name: string; // Tool name (e.g., "create_object", "update_object", "search_web")
1195
- input: unknown; // Arguments passed to the tool
1196
- result?: string; // Truncated result (absent while tool is running)
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;
1197
688
  }
1198
689
 
1199
690
  type InteractionStatus = 'pending' | 'streaming' | 'done' | 'error';
1200
691
 
1201
692
  interface Interaction {
1202
- id: string; // Unique ID for this interaction
1203
- parentId: string | null; // Parent in conversation tree (null = root)
693
+ id: string;
694
+ parentId: string | null;
1204
695
  timestamp: number;
1205
- userId: string; // Who performed this interaction
1206
- userName?: string | null; // Display name at time of interaction
1207
- operation: 'prompt' | 'createObject' | 'updateObject' | 'deleteObjects';
1208
- input: string; // What the user did: prompt text or action description
1209
- output: string | null; // AI response or confirmation message (may be partial when streaming)
1210
- status: InteractionStatus; // Lifecycle status (pending → streaming → done/error)
1211
- ai: boolean; // Whether AI was invoked (vs synthetic confirmation)
1212
- modifiedObjectIds: string[]; // Objects affected by this interaction
1213
- toolCalls: ToolCall[]; // Tools called during this interaction (for AI prompts)
1214
- attachments?: string[]; // Media URLs attached by the user (images, documents, etc.)
1215
- }
1216
- ```
1217
-
1218
- ### Info Types
1219
-
1220
- ```typescript
1221
- type RoolUserRole = 'owner' | 'admin' | 'editor' | 'viewer';
1222
- type LinkAccess = 'none' | 'viewer' | 'editor';
1223
-
1224
- interface RoolSpaceInfo { id: string; name: string; role: RoolUserRole; ownerId: string; size: number; createdAt: string; updatedAt: string; linkAccess: LinkAccess; memberCount: number; }
1225
- interface SpaceMember { id: string; email: string; role: RoolUserRole; photoUrl: string | null; }
1226
- interface UserResult { id: string; email: string; name: string | null; photoUrl: string | null; }
1227
- 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>; }
1228
- interface MediaInfo { url: string; contentType: string; size: number; createdAt: string; }
1229
- interface MediaResponse { contentType: string; size: number | null; blob(): Promise<Blob>; }
1230
- type ChangeSource = 'local_user' | 'remote_user' | 'remote_agent' | 'system';
1231
- ```
1232
-
1233
- ### Prompt Options
1234
-
1235
- ```typescript
1236
- type PromptEffort = 'QUICK' | 'STANDARD' | 'REASONING' | 'RESEARCH';
1237
-
1238
- interface PromptOptions {
1239
- objectIds?: string[]; // Scope to specific objects
1240
- responseSchema?: Record<string, unknown>;
1241
- effort?: PromptEffort; // Effort level (default: 'STANDARD')
1242
- ephemeral?: boolean; // Don't record in interaction history
1243
- readOnly?: boolean; // Disable mutation tools (default: false)
1244
- parentInteractionId?: string | null; // Branch from a specific interaction (omit to auto-continue)
1245
- attachments?: Array<File | Blob | { data: string; contentType: string }>; // Files to attach (uploaded to media store)
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[];
1246
706
  }
1247
707
  ```
1248
708