@rool-dev/sdk 0.10.2 → 0.11.0

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