Rool SDK
v0.10.1
The TypeScript SDK for Rool, a persistent and collaborative environment for organizing objects.
Building a new Rool extension? Start with
@rool-dev/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.
The SDK manages authentication, real-time synchronization, and per-space file storage. Core primitives:
- Spaces — Containers for objects, schema, metadata, channels, and files
- Channels — Named contexts within a space. All object and AI operations go through a channel.
- Conversations — Independent interaction histories within a channel.
- 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. - AI operations — Create, update, or query objects using natural language and
{{placeholders}} - File storage — Every space has WebDAV file storage
Installation
npm install @rool-dev/sdkQuick Start
import { RoolClient } from '@rool-dev/sdk';
const client = new RoolClient();const authenticated = await client.initialize();
if (!authenticated) { client.login('My App'); // Redirects to auth page, shows "Sign in to My App"}
// Create a new space, then open a channel on itconst space = await client.createSpace('Solar System');const channel = await space.openChannel('main');
// Define the schema — what types of objects exist and their fieldsawait channel.createCollection('body', [ { name: 'name', type: { kind: 'string' } }, { name: 'mass', type: { kind: 'string' } }, { name: 'radius', type: { kind: 'string' } }, { name: 'orbits', type: { kind: 'maybe', inner: { kind: 'ref' } } },]);
// Create objects with AI-generated content using {{placeholders}}.// First arg is the collection, second is the body.const { object: sun } = await channel.createObject('body', { name: 'Sun', mass: '{{mass in solar masses}}', radius: '{{radius in km}}',}, { basename: 'sun' });
const { object: earth } = await channel.createObject('body', { name: 'Earth', mass: '{{mass in Earth masses}}', radius: '{{radius in km}}', orbits: sun.location, // Reference to the sun via its location});
// Use the AI agent to work with your dataconst { message, objects } = await channel.prompt( 'Add the other planets in our solar system, each referencing the Sun');console.log(message); // AI explains what it didconsole.log(`Modified ${objects.length} objects`);
// Query with natural languageconst { objects: innerPlanets } = await channel.findObjects({ prompt: 'planets closer to the sun than Earth'});
// Clean upchannel.close();Core Concepts
Spaces and Channels
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.
There are two main handles:
RoolSpace— Live handle with SSE subscription for user management, link access, channel management, file storage, export, and channel lifecycle events. ExtendsEventEmitter.RoolChannel— Full real-time handle for objects, AI prompts, schema, and undo/redo.
// Open a space — live handle with SSE subscriptionconst space = await client.openSpace('space-id');await space.addUser(userId, 'editor');await space.setLinkAccess('viewer');
// React to channel changes in real-timespace.on('channelCreated', (channel) => console.log('New channel:', channel.id));space.on('channelUpdated', (channel) => console.log('Updated:', channel.id));space.on('channelDeleted', (channelId) => console.log('Deleted:', channelId));
// Open a channel for object and AI operationsconst channel = await space.openChannel('my-channel');await channel.prompt('Create some planets');
// Open another channel on the same spaceconst channel2 = await space.openChannel('research');await channel2.prompt('Analyze the data'); // Independent channel
// Clean up — stops subscription and closes all open channelsspace.close();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.
Channel ID constraints:
- 1–32 characters
- Only alphanumeric characters, hyphens (
-), and underscores (_)
Conversations
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.
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:
// Default conversation — most apps use thisconst space = await client.openSpace('space-id');const channel = await space.openChannel('main');await channel.prompt('Hello'); // Uses 'default' conversation
// Conversation handle — for multi-thread UIsconst thread = channel.conversation('thread-42');await thread.prompt('Hello'); // Uses 'thread-42' conversationthread.getInteractions(); // Interactions for thread-42 onlyEach 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.
// System instructions are per-conversationconst thread = channel.conversation('research');await thread.setSystemInstruction('Respond in haiku');
// List all conversations in this channelconst conversations = channel.getConversations();
// Delete a conversation (cannot delete 'default')await channel.deleteConversation('old-thread');
// Rename a conversationawait thread.rename('Research Thread');Branching Conversations
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:
const thread = channel.conversation('chat');
// Normal conversation — each prompt auto-continues from the lastawait thread.prompt('My favorite color is blue. Say OK.');await thread.prompt('What is my favorite color?'); // Sees "blue"
// Branch: go back to the first message and say something differentconst firstLeaf = thread.activeLeafId; // ID of the "blue" interactionconst tree = thread.getTree();const firstInteractionId = tree[firstLeaf!].parentId!; // The root
await thread.prompt('My favorite color is red. Say OK.', { parentInteractionId: firstInteractionId, // Sibling of "blue"});await thread.prompt('What is my favorite color?'); // Sees "red", not "blue"
// Switch back to the blue branchthread.setActiveLeaf(firstLeaf!);thread.getInteractions(); // Returns the blue branch (root → leaf)Key concepts:
getInteractions()returns the active branch as a flatInteraction[](root → leaf)getTree()returns the fullRecord<string, Interaction>for branch navigation UIactiveLeafIdis the tip of the branch the user is currently viewingsetActiveLeaf(id)switches branches (emitsconversationUpdatedso reactive UIs refresh)prompt()with noparentInteractionIdauto-continues fromactiveLeafIdprompt()withparentInteractionId: nullstarts a new root-level branch
Objects, Locations, and References
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.
{ location: '/space/article/welcome.json', collection: 'article', basename: 'welcome', body: { title: 'Hello World', status: 'draft' },}The body holds the user-defined data.
References between objects are body fields whose values are location strings:
// A planet references a star{ location: '/space/body/earth.json', collection: 'body', basename: 'earth', body: { name: 'Earth', orbits: '/space/body/sun.json' },}
// An array of references{ location: '/space/team/alpha.json', collection: 'team', basename: 'alpha', body: { name: 'Alpha', members: [ '/space/user/alice.json', '/space/user/bob.json', '/space/user/carol.json', ], },}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.
Location helpers
import { loc, parseLocation, normalizeLocation, generateBasename } from '@rool-dev/sdk';
loc('article', 'welcome'); // '/space/article/welcome.json'parseLocation('/space/article/welcome.json'); // { collection: 'article', basename: 'welcome' }
// normalizeLocation accepts canonical or short form and returns canonicalnormalizeLocation('article/welcome'); // '/space/article/welcome.json'normalizeLocation('/space/article/welcome.json'); // unchanged
// 6-char random basename — same generator the SDK uses by defaultgenerateBasename(); // e.g., 'X7kQ9p'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.
Machine resource links
import { resolveMachineResource } from '@rool-dev/sdk';
const objectResource = resolveMachineResource('/space/article/welcome.json');// { kind: 'object', path: '/space/article/welcome.json' }
const fileResource = resolveMachineResource('/rool-drive/docs/readme.md');// { kind: 'file', path: '/rool-drive/docs/readme.md' }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).
AI Placeholder Pattern
Use {{description}} in body field values to have AI generate content:
// Create with AI-generated contentawait channel.createObject('article', { headline: '{{catchy headline about coffee}}', body: '{{informative paragraph}}',});
// Update existing content with AIawait channel.updateObject('/space/article/welcome.json', { prompt: 'Make the body shorter and more casual'});
// Add new AI-generated field to existing objectawait channel.updateObject('/space/article/welcome.json', { data: { summary: '{{one-sentence summary}}' }});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.
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.
Checkpoints & Undo/Redo
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.
// Create a checkpoint before user actionawait channel.checkpoint('Delete object');await channel.deleteObjects([location]);
// User can now undo back to the checkpointif (await channel.canUndo()) { await channel.undo(); // Restores the deleted object}
// Redo reapplies the undone actionif (await channel.canRedo()) { await channel.redo(); // Deletes the object again}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.
Hidden Fields
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:
await channel.createObject('article', { title: 'My Article', author: 'John Doe', _ui: { x: 100, y: 200, collapsed: false }});Real-time Sync
Events fire for both local and remote changes. The source field indicates origin:
local_user— This client made the changeremote_user— Another user/client made the changeremote_agent— AI agent made the changesystem— Resync after error
// All UI updates happen in one place, regardless of change sourcechannel.on('objectUpdated', ({ location, object, source }) => { renderObject(location, object); if (source === 'remote_agent') { doLayout(); // AI might have added content }});
// Caller just makes the change - event handler does the UI workchannel.updateObject(location, { prompt: 'expand this' });Locations & Basenames
By default, createObject mints a 6-character alphanumeric basename. Provide your own via options.basename for meaningful identifiers:
await channel.createObject('article', { title: 'The Meaning of Life' }, { basename: 'meaning-of-life' },);// → location: /space/article/meaning-of-life.jsonWhy pin a basename?
- Fire-and-forget creation — Know the location immediately without awaiting the response.
- Meaningful identifiers — Use domain-specific names like
welcomeor2026-budgetfor easier debugging and external references.
// Fire-and-forget: create and reference without waitingconst basename = RoolClient.generateBasename();const location = loc('note', basename);
channel.createObject('note', { text: '{{expand this idea}}' }, { basename });channel.updateObject(parentLocation, { data: { notes: [...existingNotes, location] },}); // Add reference immediatelyBasename constraints:
- Must start with an alphanumeric character.
- Other characters may be alphanumeric, hyphens (
-), or underscores (_). - Must be unique within its collection (throws if the location already exists).
Use moveObject to rename an object or move it to a different collection — see Moving and Renaming.
Authentication
Browser (Default)
No configuration needed. Uses localStorage for tokens, redirects to login page.
const client = new RoolClient();const authenticated = await client.initialize();
if (!authenticated) { client.login('My App'); // Redirect to the auth page}Node.js
For CLI tools and scripts. Stores credentials in ~/.config/rool/, opens browser for login.
import { NodeAuthProvider } from '@rool-dev/sdk/node';
const client = new RoolClient({ authProvider: new NodeAuthProvider() });const authenticated = await client.initialize();
if (!authenticated) { await client.login('My CLI Tool'); // Opens browser, waits for callback}Auth Methods
| Method | Description |
|---|---|
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). |
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. |
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. |
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. |
logout(): void | Clear tokens and state |
isAuthenticated(): Promise<boolean> | Check auth status (validates token) |
getAuthUser(): AuthUser | Get auth identity from JWT ({ email, name }) |
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. |
AI Agent
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.
const { message, objects } = await channel.prompt( "Create a topic node for the solar system, then child nodes for each planet.");console.log(`AI: ${message}`);console.log(`Modified ${objects.length} objects:`, objects);Use checkpoint() before prompting to make operations undoable.
Method Signature
prompt(text: string, options?: PromptOptions): Promise<{ message: string; objects: RoolObject[] }>Returns a message (the AI’s response) and the list of objects that were created or modified.
Options
| Option | Description |
|---|---|
locations | Focus the AI on specific objects, identified by location (given primary attention in context). |
responseSchema | Request structured JSON instead of text summary |
effort | Effort level: 'QUICK', 'STANDARD' (default), 'REASONING', or 'RESEARCH' |
ephemeral | If true, don’t record in interaction history (useful for tab completion) |
readOnly | If true, disable mutation tools (create, update, move, delete). Use for questions. |
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). |
attachments | Files to attach (File, Blob, or { data, contentType }). Stored as authenticated space files; resulting rool-machine:/rool-drive/... references are stored on the interaction’s attachments field for UI rendering. The AI can interpret images (JPEG, PNG, GIF, WebP, SVG), PDFs, text-based files (plain text, Markdown, CSV, HTML, XML, JSON), and DOCX documents. Other file types are uploaded and stored but the AI cannot natively consume their contents, only use shell tools on them. |
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. |
Effort Levels
| Level | Description |
|---|---|
QUICK | Fast, lightweight model. Best for simple questions. |
STANDARD | Default behavior with balanced capabilities. |
REASONING | Extended reasoning for complex tasks. |
RESEARCH | Most thorough mode with deep analysis. Slowest and most credit-intensive. |
Examples
// Reorganize existing objectsconst { objects } = await channel.prompt( "Group these notes by topic and create a parent node for each group.");
// Work with specific objectsconst result = await channel.prompt( "Summarize these articles", { locations: ['/space/article/intro.json', '/space/article/conclusion.json'] });
// Quick question without mutations (fast model + read-only)const { message } = await channel.prompt( "What topics are covered?", { effort: 'QUICK', readOnly: true });
// Complex analysis with extended reasoningawait channel.prompt( "Analyze relationships and reorganize", { effort: 'REASONING' });
// Attach files for the AI to see (File from <input>, Blob, or base64)const file = fileInput.files[0]; // from <input type="file">await channel.prompt( "Describe what's in this photo and create an object for it", { attachments: [file] });
// Cancel a long-running promptconst ac = new AbortController();cancelButton.onclick = () => ac.abort();await channel.prompt("Do a deep analysis...", { effort: 'RESEARCH', signal: ac.signal,});Structured Responses
Use responseSchema to get structured JSON instead of a text message:
const { message } = await channel.prompt("Categorize these items", { locations: [ '/space/item/widget.json', '/space/item/gadget.json', '/space/item/gizmo.json', ], responseSchema: { type: 'object', properties: { categories: { type: 'array', items: { type: 'string' } }, summary: { type: 'string' } } }});
const result = JSON.parse(message);console.log(result.categories, result.summary);Context Flow
AI operations automatically receive context:
- Interaction history — Previous interactions and their results from this channel
- Recently modified objects — Objects created or changed recently
- Selected objects — Objects passed via
locationsare given primary focus
This context flows automatically — no configuration needed. The AI sees enough history to maintain coherent interactions while respecting the _-prefixed field hiding rules.
Collaboration
Adding Users to a Space
To add a user to a space, you need their user ID. Use searchUser() to find them by email:
// Find the user by emailconst user = await client.searchUser('colleague@example.com');if (!user) { throw new Error('User not found');}
// Add them to the spaceconst space = await client.openSpace('space-id');await space.addUser(user.id, 'editor');Roles
| Role | Capabilities |
|---|---|
owner | Full control, can delete space and manage all users |
admin | All editor capabilities, plus can manage users (except other admins/owners) |
editor | Can create, modify, move, and delete objects |
viewer | Read-only access (can query with prompt and findObjects) |
Space Collaboration Methods
These methods are available on RoolSpace:
| Method | Description |
|---|---|
listUsers(): Promise<SpaceMember[]> | List users with access |
addUser(userId, role): Promise<void> | Add user to space (requires owner or admin role) |
removeUser(userId): Promise<void> | Remove user from space (requires owner or admin role) |
setLinkAccess(linkAccess): Promise<void> | Set URL sharing level (requires owner or admin role) |
URL Sharing
Enable public URL access to allow anyone with the space URL to access it:
const space = await client.openSpace('space-id');
// Allow anyone with the URL to viewawait space.setLinkAccess('viewer');
// Allow anyone with the URL to editawait space.setLinkAccess('editor');
// Disable URL access (default)await space.setLinkAccess('none');
// Check current settingconsole.log(space.linkAccess); // 'none' | 'viewer' | 'editor'When a user accesses a space via URL, they’re granted the corresponding role (viewer or editor) based on the space’s linkAccess setting.
Client User Methods
| Method | Description |
|---|---|
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. |
getCurrentUser(): Promise<CurrentUser> | Fetch fresh user profile from server (id, email, name, photoUrl, slug, plan, creditsBalance, totalCreditsUsed, createdAt, lastActivity, processedAt, storage) |
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. |
deleteCurrentUser(): Promise<void> | Mark the current user’s account for deletion (10-minute grace period before irreversible). Logs out the client. |
searchUser(email): Promise<UserResult | null> | Find user by exact email address (no partial matching) |
Real-time Collaboration
When multiple users have a space open, changes sync in real-time. The source field in events tells you who made the change:
channel.on('objectUpdated', ({ location, object, source }) => { if (source === 'remote_user') { // Another user made this change showCollaboratorActivity(object); }});See Real-time Sync for more on event sources.
RoolClient API
Logging
By default the SDK logs errors to the console. Pass a logger to see more or customize output:
// Default — errors onlyconst client = new RoolClient();
// Log everything to consoleconst client = new RoolClient({ logger: console });
// Bring your own logger (pino, winston, etc.)const client = new RoolClient({ logger: myLogger // any object with { debug, info, warn, error }});Space & Channel Lifecycle
| Method | Description |
|---|---|
listSpaces(): Promise<RoolSpaceInfo[]> | List available spaces |
openSpace(spaceId): Promise<RoolSpace> | Open a space with live SSE subscription. Caches and reuses open spaces. Call space.openChannel(channelId) to get a channel. |
createSpace(name): Promise<RoolSpace> | Create a new space, returns live handle with SSE subscription |
duplicateSpace(sourceSpaceId, name): Promise<RoolSpace> | Duplicate an existing space. Returns a handle to the new space. |
deleteSpace(id): Promise<void> | Permanently delete a space (cannot be undone) |
importArchive(name, archive): Promise<RoolSpace> | Import from a zip archive, creating a new space |
webdav(spaceId): RoolWebDAV | Open a WebDAV client for a space’s file storage |
getSpaceStorageUsage(spaceId): Promise<SpaceFileStorageUsage> | Get WebDAV quota usage for a space |
Channel Management
Manage channels on the RoolSpace handle:
| Method | Description |
|---|---|
space.channels: ChannelInfo[] | Live channel list (auto-updates via SSE) |
space.getChannels(): ChannelInfo[] | List channels (deprecated — use space.channels instead) |
space.renameChannel(channelId, name): Promise<void> | Rename a channel |
space.deleteChannel(channelId): Promise<void> | Delete a channel and its interaction history |
channel.rename(name): Promise<void> | Rename the current open channel |
User Storage
Server-side key-value storage for user preferences, UI state, and other persistent data. Replaces browser localStorage with cross-device, server-synced storage.
Features:
- Fresh data fetched from server on
initialize()— cache is authoritative after init - Sync reads from local cache (fast, no network round-trip)
- Automatic sync to server and across tabs/devices via SSE
userStorageChangedevent fires on all changes (local or remote)- Total storage limited to 10MB per user
| Method | Description |
|---|---|
getUserStorage<T>(key): T | undefined | Get a value (sync, from cache) |
setUserStorage(key, value): void | Set a value (updates cache, syncs to server) |
getAllUserStorage(): Record<string, unknown> | Get all stored data (sync, from cache) |
// After initialize(), storage is fresh from serverconst authenticated = await client.initialize();
// Sync reads are now trustworthyconst theme = client.getUserStorage<string>('theme');applyTheme(theme ?? 'light');
// Write - updates immediately, syncs to server in backgroundclient.setUserStorage('theme', 'dark');client.setUserStorage('sidebar', { collapsed: true, width: 280 });
// Delete a keyclient.setUserStorage('theme', null);
// Listen for changes from other tabs/devicesclient.on('userStorageChanged', ({ key, value, source }) => { // source: 'local' (this client) or 'remote' (server/other client) if (key === 'theme') applyTheme(value as string);});Extensions
Manage and publish extensions. See @rool-dev/extension for building extensions.
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.
Your Library (ExtensionInfo)
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).
| Method | Description |
|---|---|
uploadExtension(extensionId, options): Promise<ExtensionInfo> | Upload or update an extension (options.bundle: zip with index.html and manifest.json) |
listExtensions(): Promise<ExtensionInfo[]> | List your extensions |
getExtensionInfo(extensionId): Promise<ExtensionInfo | null> | Get info for a specific extension |
deleteExtension(extensionId): Promise<void> | Delete an extension permanently (removes files and DB row) |
Marketplace (PublishedExtensionInfo)
Discover and install extensions published by other users.
| Method | Description |
|---|---|
findExtensions(options?): Promise<PublishedExtensionInfo[]> | Search the marketplace. Options: query (semantic search string), limit (default 20, max 100). Omit query to browse all. |
publishToPublic(extensionId): Promise<void> | Publish one of your extensions to the marketplace |
unpublishFromPublic(extensionId): Promise<void> | Remove from the marketplace (keeps the extension in your library) |
Utilities
| Method | Description |
|---|---|
RoolClient.generateBasename(): string | Generate a 6-char alphanumeric basename for new object identities. |
RoolClient.generateId(): string | Same as generateBasename(); retained for callers minting non-object IDs (interactions, conversations, channels). |
destroy(): void | Clean up resources |
Client Events
client.on('authStateChanged', (authenticated: boolean) => void)client.on('spaceAdded', (space: RoolSpaceInfo) => void) // Space created or access grantedclient.on('spaceRemoved', (spaceId: string) => void) // Space deleted or access revokedclient.on('spaceRenamed', (spaceId: string, newName: string) => void)client.on('channelCreated', (spaceId: string, channel: ChannelInfo) => void)client.on('channelUpdated', (spaceId: string, channel: ChannelInfo) => void)client.on('channelDeleted', (spaceId: string, channelId: string) => void)client.on('userStorageChanged', ({ key, value, source }: UserStorageChangedEvent) => void)client.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)client.on('error', (error: Error, context?: string) => void)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.
Space list management pattern:
const spaces = new Map<string, RoolSpaceInfo>();
client.on('spaceAdded', (space) => spaces.set(space.id, space));client.on('spaceRemoved', (id) => spaces.delete(id));client.on('spaceRenamed', (id, name) => { const space = spaces.get(id); if (space) spaces.set(id, { ...space, name });});RoolSpace API
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.
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.
Properties
| Property | Description |
|---|---|
id: string | Space ID |
name: string | Space name |
role: RoolUserRole | User’s role |
linkAccess: LinkAccess | URL sharing level |
memberCount: number | Number of users with access to the space |
channels: ChannelInfo[] | Live channel list (auto-updates via SSE) |
webdav: RoolWebDAV | WebDAV client for this space’s file storage |
Methods
| Method | Description |
|---|---|
openChannel(channelId): Promise<RoolChannel> | Open a channel on this space |
close(): void | Stop SSE subscription and close all open channels |
rename(newName): Promise<void> | Rename this space |
delete(): Promise<void> | Permanently delete this space |
listUsers(): Promise<SpaceMember[]> | List users with access |
addUser(userId, role): Promise<void> | Add user to space |
removeUser(userId): Promise<void> | Remove user from space |
setLinkAccess(linkAccess): Promise<void> | Set URL sharing level |
getChannels(): ChannelInfo[] | List channels (deprecated — use channels property instead) |
renameChannel(channelId, name): Promise<void> | Rename a channel |
deleteChannel(channelId): Promise<void> | Delete a channel |
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. |
exportArchive(): Promise<Blob> | Export space as zip archive |
getStorageUsage(): Promise<SpaceFileStorageUsage> | Get WebDAV quota usage for this space |
fetchMachineResource(resource): Promise<Response> | Fetch a resolved file MachineResource through this space |
refresh(): Promise<void> | Refresh space data from server |
Space Events
space.on('channelCreated', (channel: ChannelInfo) => void) // New channel addedspace.on('channelUpdated', (channel: ChannelInfo) => void) // Channel metadata changed (name, extension, manifest)space.on('channelDeleted', (channelId: string) => void) // Channel removedspace.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)RoolChannel API
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.
Properties
| Property | Description |
|---|---|
id: string | Space ID |
name: string | Space name |
role: RoolUserRole | User’s role ('owner' | 'admin' | 'editor' | 'viewer') |
linkAccess: LinkAccess | URL sharing level ('none' | 'viewer' | 'editor') |
userId: string | Current user’s ID |
channelId: string | Channel ID (read-only, fixed at open time) |
isReadOnly: boolean | True if viewer role |
extensionUrl: string | null | URL of the installed extension, or null if this is a plain channel |
extensionId: string | null | ID of the installed extension, or null if this is a plain channel |
manifest: ExtensionManifest | null | Extension manifest snapshot (name, icon, collections, etc.), or null |
Lifecycle
| Method | Description |
|---|---|
close(): void | Clean up resources and stop receiving updates |
rename(name): Promise<void> | Rename this channel |
conversation(conversationId): ConversationHandle | Get a handle scoped to a specific conversation (see Conversations) |
Object Operations
Objects are records addressed by location (/space/<collection>/<basename>.json). Every object must belong to a collection — create the collection first (see Collection Schema). The body holds the user-defined fields.
All methods that accept a location accept either the canonical form or the short form (collection/basename).
| Method | Description |
|---|---|
getObject(location): Promise<RoolObject | undefined> | Get an object, or undefined if not found. |
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. |
findObjects(options): Promise<{ objects, message }> | Find objects using structured filters and/or natural language. Results sorted by modifiedAt (desc by default). |
getObjectLocations(options?): string[] | Get all object locations. Sorted by modifiedAt (desc by default). Options: { limit?, order? }. |
createObject(collection, body, options?): Promise<{ object, message }> | Create a new object in collection. The SDK mints a random basename unless you pass options.basename. |
updateObject(location, options): Promise<{ object, message }> | Update an existing object’s body. |
moveObject(from, to, options?): Promise<{ object, message }> | Rename or relocate an object. See Moving and Renaming. |
deleteObjects(locations): Promise<void> | Delete objects by location. Other objects’ refs become stale. |
createObject
// Auto-generated basenameconst { object } = await channel.createObject('article', { title: 'Hello', body: 'World',});// → object.location: '/space/article/X7kQ9p.json'
// Pinned basenameawait channel.createObject('article', { title: 'Welcome' }, { basename: 'welcome' },);// → location: '/space/article/welcome.json'
// AI placeholdersawait channel.createObject('article', { headline: '{{catchy headline}}', body: '{{long-form intro}}',});| Option | Description |
|---|---|
basename | Specific basename to use. If omitted, the SDK generates a random 6-char one. |
ephemeral | If true, the operation won’t be recorded in interaction history. |
parentInteractionId | Conversation tree parent. Omit to auto-continue; pass null for a new root. |
updateObject
// Add/update fieldsawait channel.updateObject('/space/article/welcome.json', { data: { status: 'published' },});
// Delete a field (pass null)await channel.updateObject('/space/article/welcome.json', { data: { draft: null },});
// AI-driven rewriteawait channel.updateObject('/space/article/welcome.json', { prompt: 'Tighten the intro by 30%.',});| Option | Description |
|---|---|
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. |
prompt | Natural language instruction for AI to modify content. |
ephemeral | If true, the operation won’t be recorded in interaction history. |
parentInteractionId | Conversation tree parent. Omit to auto-continue; pass null for a new root. |
Use moveObject to change an object’s location (collection or basename).
Moving and Renaming
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.
// Rename within the same collectionawait channel.moveObject( '/space/article/welcome.json', '/space/article/hello-world.json',);
// Move into a different collectionawait channel.moveObject( '/space/draft/post-42.json', '/space/article/post-42.json',);
// Move and replace body in one goawait channel.moveObject(from, to, { body: { title: 'Hello, world', status: 'published' },});| Option | Description |
|---|---|
body | Replace the body atomically as part of the move. If omitted, the body is preserved. |
ephemeral | If true, the operation won’t be recorded in interaction history. |
parentInteractionId | Conversation tree parent. Omit to auto-continue; pass null for a new root. |
findObjects
Find objects using structured filters and/or natural language.
whereonly — exact-match filtering, no AI, no credits.collectiononly — filter by collection name, no AI, no credits.promptonly — AI-powered semantic query over all objects.where+prompt—where(andlocations) narrow the data set first, then the AI queries within the constrained set.
| Option | Description |
|---|---|
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. |
collection | Filter by collection name. |
prompt | Natural language query. Triggers AI evaluation (uses credits). |
limit | Maximum number of results. |
locations | Scope to specific object locations. Constrains the candidate set in both structured and AI queries. |
order | Sort order by modifiedAt: 'asc' or 'desc' (default: 'desc'). |
ephemeral | If true, the query won’t be recorded in interaction history. Useful for responsive search. |
Examples:
// Filter by collection (no AI, no credits)const { objects } = await channel.findObjects({ collection: 'article'});
// Exact field matching (no AI, no credits)const { objects } = await channel.findObjects({ where: { status: 'published' }});
// Combine collection and field filtersconst { objects } = await channel.findObjects({ collection: 'article', where: { status: 'published' }});
// Pure natural language query (AI interprets)const { objects, message } = await channel.findObjects({ prompt: 'articles about space exploration published this year'});
// Combined: collection + where narrow the data, prompt queries within itconst { objects } = await channel.findObjects({ collection: 'article', prompt: 'that discuss climate solutions positively', limit: 10});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.
Undo/Redo
| Method | Description |
|---|---|
checkpoint(label?): Promise<string> | Call before mutations. Saves current state for undo. |
canUndo(): Promise<boolean> | Check if undo available |
canRedo(): Promise<boolean> | Check if redo available |
undo(): Promise<boolean> | Undo to previous checkpoint |
redo(): Promise<boolean> | Redo undone action |
clearHistory(): Promise<void> | Clear undo/redo stack |
See Checkpoints & Undo/Redo for semantics.
Space Metadata
Store arbitrary data alongside the space without it being part of an object’s body (e.g., viewport state, user preferences).
| Method | Description |
|---|---|
setMetadata(key, value): void | Set space-level metadata |
getMetadata(key): unknown | Get metadata value, or undefined if key not set |
getAllMetadata(): Record<string, unknown> | Get all metadata |
Space File Storage
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).
Use client.webdav(spaceId) when you only have an ID, or space.webdav when you already have an open space.
import { resolveMachineResource } from '@rool-dev/sdk';
const webdav = client.webdav('space-id');
await webdav.mkcol('docs');await webdav.put('docs/readme.md', '# Hello', { contentType: 'text/markdown', ifNoneMatch: '*',});
const listing = await webdav.propfind('docs/', { depth: '1', props: ['displayname', 'getcontentlength', 'getcontenttype', 'getetag'],});
const file = await webdav.get('docs/readme.md');console.log(await file.text());
const resource = resolveMachineResource('/rool-drive/docs/read me.md');if (!resource || resource.kind !== 'file') throw new Error('not a file');const sameFile = await space.fetchMachineResource(resource);
const usage = await space.getStorageUsage();console.log(usage.usedBytes);console.log(usage.availableBytes); // null means unlimitedconsole.log(usage.limitBytes); // null means unlimitedPaths 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.
| Method | Description |
|---|---|
client.webdav(spaceId) | Create a WebDAV client for a space |
client.getSpaceStorageUsage(spaceId) | Get WebDAV quota usage for a space |
space.webdav | WebDAV client for an open space |
space.getStorageUsage() | Get WebDAV quota usage for an open space |
webdav.getStorageUsage() | Get WebDAV quota usage through the WebDAV client |
webdav.path(path) | Normalize a WebDAV path |
webdav.propfind(path, options) | Read properties/list collections; explicit depth required |
webdav.get(path, options?) / webdav.head(path) | Read a file, including optional byte ranges for get |
webdav.put(path, body, options?) | Write an exact file path; parents must already exist |
webdav.mkcol(path) | Create one collection |
webdav.copy(source, destination, options?) | Copy a file or collection within the same space |
webdav.move(source, destination, options?) | Move a file or collection within the same space |
webdav.delete(path, options?) | Delete a file or collection |
webdav.lock(path, options) / webdav.refreshLock(path, token) / webdav.unlock(token) | WebDAV Class 2 write locks |
webdav.request(method, path, init?) | Raw authenticated WebDAV request escape hatch |
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 withspace.fetchMachineResource(resource). Object resources identify records inside the space. They’re not interchangeable.
File references from AI responses
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).
const resource = resolveMachineResource('rool-machine:/rool-drive/docs/readme.md');if (!resource || resource.kind !== 'file') throw new Error('not a file');const response = await space.fetchMachineResource(resource);const blob = await response.blob();img.src = URL.createObjectURL(blob);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.
Proxied Fetch
Fetch external URLs via the server, bypassing CORS restrictions. Requires editor role or above. Private/internal IP ranges are blocked (SSRF protection).
| Method | Description |
|---|---|
fetch(url, init?): Promise<Response> | Fetch a URL via the server proxy. init accepts method, headers, and body. |
// GET requestconst response = await channel.fetch('https://api.example.com/data');const data = await response.json();
// POST with headers and bodyconst response = await channel.fetch('https://api.example.com/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: { key: 'value' },});Collection Schema
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.
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.
// Define a collection with typed fieldsawait channel.createCollection('article', [ { name: 'title', type: { kind: 'string' } }, { name: 'status', type: { kind: 'enum', values: ['draft', 'published', 'archived'] } }, { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } }, { name: 'author', type: { kind: 'ref' } },]);
// Read the current schemaconst schema = channel.getSchema();console.log(schema.article.fields); // FieldDef[]
// Modify an existing collection's fieldsawait channel.alterCollection('article', [ { name: 'title', type: { kind: 'string' } }, { name: 'status', type: { kind: 'enum', values: ['draft', 'review', 'published', 'archived'] } }, { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } }, { name: 'author', type: { kind: 'ref' } }, { name: 'wordCount', type: { kind: 'number' } },]);
// Remove a collectionawait channel.dropCollection('article');| Method | Description |
|---|---|
getSchema(): SpaceSchema | Get all collection definitions |
createCollection(name, fields): Promise<CollectionDef> | Add a new collection to the schema |
alterCollection(name, fields): Promise<CollectionDef> | Replace a collection’s field definitions |
dropCollection(name): Promise<void> | Remove a collection from the schema |
Field Types
| Kind | Description | Example |
|---|---|---|
string | Text value | { kind: 'string' } |
number | Numeric value | { kind: 'number' } |
boolean | True/false | { kind: 'boolean' } |
ref | Reference to another object (location string) | { kind: 'ref' } |
enum | One of a set of values | { kind: 'enum', values: ['a', 'b'] } |
literal | Exact value | { kind: 'literal', value: 'fixed' } |
array | List of values | { kind: 'array', inner: { kind: 'string' } } |
maybe | Optional (nullable) | { kind: 'maybe', inner: { kind: 'number' } } |
Import/Export
Export and import space data as zip archives for backup, portability, or migration:
| Method | Description |
|---|---|
space.exportArchive(): Promise<Blob> | Export objects, metadata, channels, and files as a zip archive |
client.importArchive(name, archive): Promise<RoolSpace> | Import from a zip archive, creating a new space |
Export:
const space = await client.openSpace('space-id');const archive = await space.exportArchive();// Save as .zip fileconst url = URL.createObjectURL(archive);Import:
const space = await client.importArchive('Imported Data', archiveBlob);const channel = await space.openChannel('main');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.
Channel Events
Semantic events describe what changed. Events fire for both local changes and remote changes.
// source indicates origin:// - 'local_user': This client made the change// - 'remote_user': Another user/client made the change// - 'remote_agent': AI agent made the change// - 'system': Resync after error
// Object events — payload includes the full RoolObjectchannel.on('objectCreated', ({ location, object, source }) => void)channel.on('objectUpdated', ({ location, object, source }) => void)channel.on('objectDeleted', ({ location, source }) => void)channel.on('objectMoved', ({ from, to, object, source }) => void)
// Space metadatachannel.on('metadataUpdated', ({ metadata, source }) => void)
// Collection schema changedchannel.on('schemaUpdated', ({ schema, source }) => void)
// Channel metadata updated (name, extensionUrl)channel.on('channelUpdated', ({ channelId, source }) => void)
// Conversation interaction history updatedchannel.on('conversationUpdated', ({ conversationId, channelId, source }) => void)
// Full state replacement (undo/redo, resync after error)channel.on('reset', ({ source }) => void)
// Sync error occurred, channel resynced from serverchannel.on('syncError', (error: Error) => void)Error Handling
AI operations may fail due to rate limiting or other transient errors. Check error.message for user-friendly error text:
try { await channel.updateObject(location, { prompt: 'expand this' });} catch (error) { if (error.message.includes('temporarily unavailable')) { showToast('Service busy, please try again in a moment'); } else { showToast(error.message); }}Interaction History
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.
Conversation History Methods
| Method | Description |
|---|---|
getInteractions(): Interaction[] | Get the active branch as a flat array (root → leaf) |
getTree(): Record<string, Interaction> | Get the full interaction tree for branch navigation |
activeLeafId: string | undefined | The tip of the currently active branch |
setActiveLeaf(id: string): void | Switch to a different branch (emits conversationUpdated) |
getSystemInstruction(): string | undefined | Get system instruction for the default conversation |
setSystemInstruction(instruction): Promise<void> | Set system instruction for the default conversation. Pass null to clear. |
getConversations(): ConversationInfo[] | List all conversations in this channel |
deleteConversation(conversationId): Promise<void> | Delete a conversation (cannot delete 'default') |
renameConversation(name): Promise<void> | Rename the default conversation |
Channel management (listing, renaming, deleting channels) is done via the client — see Channel Management.
The ai Field
The ai field in interactions distinguishes AI-generated responses from synthetic confirmations:
ai: true— AI processed this operation (prompt, or createObject/updateObject with placeholders)ai: false— System confirmation only (e.g., “Created object /space/note/welcome.json”)
Tool Calls
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.
Data Types
Schema Types
// Allowed field typestype FieldType = | { kind: 'string' } | { kind: 'number' } | { kind: 'boolean' } | { kind: 'array'; inner?: FieldType } | { kind: 'maybe'; inner: FieldType } | { kind: 'enum'; values: string[] } | { kind: 'literal'; value: string | number | boolean } | { kind: 'ref' };
interface FieldDef { name: string; type: FieldType;}
interface CollectionDef { fields: FieldDef[];}
// Full schema — collection names to definitionstype SpaceSchema = Record<string, CollectionDef>;Object Data
// An object addressed by location. References between objects are body// fields whose values are location strings.interface RoolObject { location: string; // "/space/<collection>/<basename>.json" collection: string; basename: string; body: Record<string, unknown>;}
// Object stat — audit information returned by channel.stat()interface RoolObjectStat { location: string; modifiedAt: number; modifiedBy: string; modifiedByName: string | null; modifiedInChannel: string; // Channel ID where the last modification happened modifiedInConversation: string | null; // Conversation ID, or null if not conversation-scoped modifiedInInteraction: string | null; // Interaction ID, or null for ephemeral or non-AI writes}Channels and Conversations
// Conversation — holds interaction tree and optional system instructioninterface Conversation { name?: string; // Conversation name (optional) systemInstruction?: string; // Custom system instruction for AI createdAt: number; // Timestamp when conversation was created createdBy: string; // User ID who created the conversation interactions: Record<string, Interaction>; // Interaction tree (keyed by ID, linked by parentId)}
// Conversation summary info (returned by channel.getConversations())interface ConversationInfo { id: string; name: string | null; systemInstruction: string | null; createdAt: number; createdBy: string; interactionCount: number;}
// Channel container with metadata and conversationsinterface Channel { name?: string; // Channel name (optional) createdAt: number; // Timestamp when channel was created createdBy: string; // User ID who created the channel createdByName?: string; // Display name at time of creation extensionUrl?: string; // URL of installed extension (set by installExtension) extensionId?: string; // ID of installed extension (user_extensions.extension_id) manifest?: ExtensionManifest; // Extension manifest snapshot (set when extension is wired) conversations: Record<string, Conversation>; // Keyed by conversation ID}
// Channel summary info (returned by client.getChannels)interface ChannelInfo { id: string; name: string | null; createdAt: number; createdBy: string; createdByName: string | null; interactionCount: number; extensionUrl: string | null; // URL of installed extension, or null extensionId: string | null; // ID of installed extension, or null manifest: ExtensionManifest | null; // Extension manifest snapshot, or null}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.
Interaction Types
type ToolCall = | { id: string; name: string; // Tool name (e.g., "create_object", "update_object", "search_web") input: unknown; // Arguments passed to the tool status: 'running'; } | { id: string; name: string; input: unknown; status: 'done'; result: string; // Truncated result };
type InteractionStatus = 'pending' | 'streaming' | 'done' | 'error';
interface Interaction { id: string; // Unique ID for this interaction parentId: string | null; // Parent in conversation tree (null = root) timestamp: number; userId: string; // Who performed this interaction userName?: string | null; // Display name at time of interaction operation: 'prompt' | 'createObject' | 'updateObject' | 'moveObject' | 'deleteObjects'; input: string; // What the user did: prompt text or action description output: string | null; // AI response or confirmation message (may be partial when streaming) status: InteractionStatus; // Lifecycle status (pending → streaming → done/error) ai: boolean; // Whether AI was invoked (vs synthetic confirmation) modifiedObjectLocations: string[]; // Locations of objects affected by this interaction toolCalls: ToolCall[]; // Tools called during this interaction (for AI prompts) attachments?: string[]; // rool-machine:/rool-drive/... file references attached by the user}Info Types
type RoolUserRole = 'owner' | 'admin' | 'editor' | 'viewer';type LinkAccess = 'none' | 'viewer' | 'editor';
interface RoolSpaceInfo { id: string; name: string; inboundEmailAddress: string; role: RoolUserRole; ownerId: string; size: number; createdAt: string; updatedAt: string; linkAccess: LinkAccess; memberCount: number; }interface SpaceMember { id: string; email: string; role: RoolUserRole; photoUrl: string | null; }interface UserResult { id: string; email: string; name: string | null; photoUrl: string | null; }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>; }type ChangeSource = 'local_user' | 'remote_user' | 'remote_agent' | 'system';Prompt Options
type PromptEffort = 'QUICK' | 'STANDARD' | 'REASONING' | 'RESEARCH';
interface PromptOptions { locations?: string[]; // Scope to specific objects responseSchema?: Record<string, unknown>; effort?: PromptEffort; // Effort level (default: 'STANDARD') ephemeral?: boolean; // Don't record in interaction history readOnly?: boolean; // Disable mutation tools (default: false) parentInteractionId?: string | null; // Branch from a specific interaction (omit to auto-continue) attachments?: Array<File | Blob | { data: string; contentType: string }>; // Files to attach signal?: AbortSignal; // Cancel an in-flight prompt}License
MIT - see LICENSE for details.