npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rool-dev/sdk

v0.12.0

Published

TypeScript SDK for working with Rool Spaces

Readme

Rool SDK

TypeScript SDK for Rool, a persistent collaborative workspace for objects, AI-assisted editing, and per-space files.

Core primitives:

  • Spaces — containers for objects, schema, metadata, conversations, collaborators, and files.
  • Conversations — independent interaction histories in a space.
  • Objects — JSON records addressed by object paths such as /space/article/welcome.json.
  • Files — user-visible files stored under /rool-drive/... through WebDAV.

Installation

npm install @rool-dev/sdk

Quick Start

import { RoolClient } from '@rool-dev/sdk';

async function main() {
  const client = new RoolClient();

  if (!(await client.initialize())) {
    await client.login('My App');
    // Browser auth redirects away. Run startup again after the auth callback.
    return;
  }

  const space = await client.createSpace('Solar System');
  const conversation = space.conversation('main');

  await space.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' } } },
  ]);

  const { object: sun } = await space.putObject('/space/body/sun.json', {
    name: 'Sun',
    mass: '1 solar mass',
    radius: '696,340 km',
  });

  const { object: earth } = await space.putObject('/space/body/earth.json', {
    name: 'Earth',
    mass: '1 Earth mass',
    radius: '6,371 km',
    orbits: sun.path,
  });

  const { message, objects } = await conversation.prompt(
    'Add the other planets in our solar system, each referencing the Sun.'
  );

  console.log(message);
  console.log(`Modified ${objects.length} objects`);

  const loadedEarth = await space.getObject(earth.path);
  console.log(loadedEarth?.body.name);

  space.close();
}

void main();

Paths and Resource URIs

Most SDK methods take plain path strings:

  • Object paths: /space/<collection>/<name>.json (exactly three segments; no dotfile collection or object names)
  • File paths: /rool-drive/<path/to/file>

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.

import { machinePath, machineUri, isObjectPath } from '@rool-dev/sdk';

machinePath('rool-machine:/rool-drive/docs/read%20me.md');
// '/rool-drive/docs/read me.md'

machineUri('/space/article/welcome.json');
// 'rool-machine:/space/article/welcome.json'

isObjectPath('/space/article/welcome.json'); // true

Object APIs require full object paths. References between objects are ordinary body fields containing object paths:

{
  path: '/space/body/earth.json',
  body: { name: 'Earth', orbits: '/space/body/sun.json' },
}

Authentication

Browser

The default auth provider stores tokens in browser storage and redirects to the Rool auth page.

async function start() {
  const client = new RoolClient();

  if (!(await client.initialize())) {
    await client.login('My App');
    // Browser auth redirects; stop startup until the callback reloads the app.
    return;
  }

  // Use the authenticated client here.
}

void start();

Node.js

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.

import { RoolClient } from '@rool-dev/sdk';
import { NodeAuthProvider } from '@rool-dev/sdk/node';

const client = new RoolClient({ authProvider: new NodeAuthProvider() });
let authenticated = await client.initialize();

if (!authenticated) {
  await client.login('My CLI Tool');
  // Re-run initialize after the non-redirect login to hydrate currentUser,
  // user storage, and client-level event subscriptions.
  authenticated = await client.initialize();
}

if (!authenticated) throw new Error('Login required');

Native (Capacitor, Cordova, Tauri, ...)

Use the native PKCE provider for JS app shells that sign in through an external system browser. login()/signup() open the auth server's /authorize page via the openExternal callback you supply; when the deep link returns, feed it to client.handleAuthRedirect(url) from your platform's deep-link handler. The code is exchanged for a session at /token and tokens are stored like the browser provider.

import { RoolClient, NativePkceAuthProvider } from '@rool-dev/sdk';
import { Browser } from '@capacitor/browser';
import { App } from '@capacitor/app';

const client = new RoolClient({
  authProvider: new NativePkceAuthProvider({
    redirectUri: 'roolandroidauth://auth/callback', // must match the server allowlist
    defaultProvider: 'google',                       // 'google' | 'apple'
    openExternal: (url) => Browser.open({ url }),
  }),
});

// Complete sign-in when the OS hands the app its deep link.
App.addListener('appUrlOpen', async ({ url }) => {
  if (await client.handleAuthRedirect(url)) {
    await Browser.close();
    // Now authenticated — refresh your UI.
  }
});

if (!(await client.initialize())) {
  // Opens the system browser; completion arrives via the listener above.
  await client.login('My App'); // pass { provider: 'apple' } to override the default
}

Email + password and magic links (native)

The native provider also supports email/password sign-in and magic links — no system browser, the server returns the token set as JSON directly.

// Password sign-in
const result = await client.signInWithPassword(email, password);
if (result.status === 'signed_in') {
  // authenticated — refresh your UI
} else {
  // status === 'verify_required': the email isn't verified yet and the server
  // has emailed a magic link. Tell the user to check their inbox.
}

// Or request a magic link explicitly
await client.requestMagicLink(email);

The magic link carries a ?verify=<token> param; complete sign-in by passing that token to client.verify(token) once the link lands back in the app.

⚠️ Magic links open the website, not the app, until Universal Links / App Links are configured. The emailed link is an https:// URL for the Rool web app. On native, an https link only re-opens your app if you've set up iOS Universal Links / Android App Links for that domain (custom-scheme deep links don't apply to email links). Without that setup the magic link completes sign-in in the browser/website, not in the native app — so for now treat magic links on native as a website hand-off, and prefer password or social sign-in for an in-app experience.

Auth API

| Method | Description | | --- | --- | | initialize(): Promise<boolean> | Call on startup. Initializes auth, refreshes user/storage state, and starts client events when authenticated. | | login(appName, params?): Promise<void> | Start login flow. | | signup(appName, params?): Promise<void> | Start signup flow. | | verify(token): Promise<boolean> | Complete email verification token flow; returns false when the active auth provider does not implement verification. | | handleAuthRedirect(url): Promise<boolean> | Complete a native PKCE sign-in from a deep-link callback URL. Returns false when the active auth provider does not implement it. | | signInWithPassword(email, password): Promise<PasswordSignInResult> | Email + password sign-in (native provider). Resolves { status: 'signed_in' }, or { status: 'verify_required' } when the email is unverified (a magic link was emailed). Rejects on bad credentials. | | requestMagicLink(email): Promise<void> | Email the user a magic sign-in link (native provider). See the caveat below — on native the link opens the website, not the app, until Universal Links / App Links are configured. | | logout(): void | Clear auth state and close open spaces. | | isAuthenticated(): Promise<boolean> | Whether credentials are held locally. No network call — a server outage does not read as logged out. | | getAuthUser(): AuthUser | Return auth identity decoded from the token. | | setPassword(password): Promise<void> | Set/change password for the current user. |

Offline behavior

A temporarily unreachable server never reads as "logged out". initialize() reports authentication from stored credentials, so on an offline start it can return true while currentUser is still null and user storage is empty — the SDK keeps reconnecting in the background and hydrates both automatically once the server is reachable, emitting currentUserChanged. Only an invalid or expired refresh token ends the session, via authStateChanged(false).

Spaces and Conversations

Open a space to receive live events and manage objects, schema, metadata, collaborators, file storage, and conversations. Use a conversation handle for conversation metadata and AI.

const space = await client.openSpace('space-id');

const conversation = space.conversation('main');
space.on('filesChanged', () => console.log('files changed'));

await conversation.prompt('Summarize this space');

Conversation IDs must be 1–32 characters and contain only letters, numbers, _, and -.

Object Operations

Objects are JSON files under /space. Create the collection before writing objects in it.

await space.createCollection('article', [
  { name: 'title', type: { kind: 'string' } },
  { name: 'status', type: { kind: 'string' } },
]);

// Create or replace an exact object path
const { object } = await space.putObject('/space/article/welcome.json', {
  title: 'Welcome',
  status: 'draft',
});

// Patch fields; null or undefined deletes a field
await space.patchObject(object.path, {
  data: { status: 'published', obsoleteField: null },
});

// Read one or many objects
await space.getObject('/space/article/welcome.json');
await space.getObjects([
  '/space/article/welcome.json',
  '/space/article/intro.json',
]);

// Rename or move an object
await space.moveObject(
  '/space/article/welcome.json',
  '/space/article/hello-world.json'
);

// Delete objects
await space.deleteObjects(['/space/article/hello-world.json']);

| Method | Description | | --- | --- | | getObject(path): Promise<RoolObject | undefined> | Fetch one object by object path. | | getObjects(paths): Promise<GetObjectsResult> | Fetch objects in bulk; returns objects and missing. | | space.putObject(path, body): Promise<{ object, message }> | Create or replace an object at an exact path. | | space.patchObject(path, { data }): Promise<{ object, message }> | Patch an object's body; null/undefined deletes fields. | | space.moveObject(from, to, options?): Promise<{ object, message }> | Rename or relocate an object; options.body can replace the body after moving. | | space.deleteObjects(paths): Promise<void> | Delete object files. |

AI Agent

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.

const { message, objects } = await conversation.prompt(
  'Create a topic node for the solar system, then child nodes for each planet.'
);

console.log(message);
console.log(objects.map((object) => object.path));

Prompt Options

| Option | Description | | --- | --- | | responseSchema | Request structured JSON text matching a JSON-schema-like shape. | | effort | 'QUICK' (fast/read-only), 'STANDARD' (default), 'REASONING', or 'RESEARCH'. | | ephemeral | Do not record the prompt in interaction history. | | readOnly | Disable mutation tools. | | parentInteractionId | Conversation-tree parent. Omit to fetch and continue from the current default leaf; pass null for a new root branch. | | attachments | Existing object/file paths or rool-machine:/... URIs, plus local files (File, Blob, or { data, contentType, filename? }). | | signal | AbortSignal used to request that the server stop an in-flight prompt. | | eventName | Optional telemetry event name. Defaults to 'prompt_user'. |

// Read-only quick question
await conversation.prompt('What topics are covered?', {
  effort: 'QUICK', // fast/read-only
});

// Focus on existing objects and files
await conversation.prompt('Compare these resources', {
  attachments: [
    '/space/article/intro.json',
    'rool-machine:/rool-drive/docs/report.pdf',
  ],
});

// Upload a local file as an attachment
await conversation.prompt('Describe this image', {
  attachments: [fileInput.files![0]],
});

// Structured response
const { message } = await conversation.prompt('Categorize these items', {
  responseSchema: {
    type: 'object',
    properties: {
      categories: { type: 'array', items: { type: 'string' } },
      summary: { type: 'string' },
    },
  },
});
const result = JSON.parse(message);

// Stop a long prompt with a signal (when the caller holds the controller)
const ac = new AbortController();
const promptPromise = conversation.prompt('Do a deep analysis', {
  effort: 'RESEARCH',
  signal: ac.signal,
});
ac.abort(); // asks the server to stop the in-flight interaction
await promptPromise;

Stopping a conversation

Use signal when the same call site cancels the prompt. When the Stop button lives elsewhere — a different component, after a reload, or a prompt another client started — stop the conversation itself. A conversation processes one run at a time, so no interaction ID is needed. Stopping is best-effort: the server halts the agent loop and closes the stream, but an LLM turn already in flight keeps generating server-side and is billed.

// Stop whatever is running in a conversation. Returns whether anything was
// actually running.
await space.stopConversation('thread-42');

// Conversation handles stop their own running work.
const thread = space.conversation('thread-42');
await thread.stop();

| Method | Description | | --- | --- | | stopConversation(conversationId): Promise<boolean> | Stop whatever is running in a conversation. | | conversation.stop(): Promise<boolean> | Stop this conversation's running work. |

Conversations

Use space.conversation(id) for an imperative API scoped to one conversation. The handle retains no conversation contents or branch cursor; fetch contents when needed. With no explicit parent, prompt() fetches the current conversation and continues from its default leaf.

import { conversationBranch, defaultConversationLeaf } from '@rool-dev/sdk';

const thread = space.conversation('thread-42');
await thread.prompt('Hello from another thread');
await thread.setSystemInstruction('Answer in haiku');

const conversation = await thread.get();
const leaf = defaultConversationLeaf(conversation);
const branch = conversationBranch(conversation, leaf);

| Method/property | Description | | --- | --- | | space.conversation(id): ConversationHandle | Get a stateless conversation-scoped API handle. | | conversation.get(): Promise<Conversation | null> | Fetch current conversation contents. | | conversation.prompt(text, options?) | Prompt the conversation. Pass parentInteractionId to choose a branch explicitly. | | conversation.setSystemInstruction(value) | Set or clear the system instruction. | | conversation.rename(name) / conversation.delete() / conversation.stop() | Manage the scoped conversation. | | listConversations(): Promise<ConversationMeta[]> | Fetch the conversation roster. | | createConversation(agent, visibility): Promise<string> | Create a conversation under an agent; returns the server-minted conversation ID. | | deleteConversation(id): Promise<void> | Delete a conversation by ID. |

Agents

Conversations belong to an agent. Every space has the stock agent rool; spaces can also host custom agents. createConversation creates a conversation under any agent — including rool — with a server-minted ID and the visibility you choose; prompt the returned ID through a normal handle. Stock (rool) conversations can also spring into existence from a client-minted ID on first prompt (as in the examples above); custom-agent conversations are only created explicitly.

Each conversation has a visibility: 'shared' (every space member), 'private' (only you), or 'temporary' (private, and auto-deleted after sitting idle). A conversation's agent and visibility are reported on its ConversationMeta.

const agents = await space.listAgents(); // always includes 'rool'

const id = await space.createConversation('research-bot', 'private');
await space.conversation(id).prompt('Hello');

await space.deleteAgent('research-bot'); // removes the agent and all its conversations

| Method | Description | | --- | --- | | listAgents(): Promise<string[]> | The space's agents. Always includes the stock agent rool. | | createConversation(agent, visibility): Promise<string> | Create a conversation under an agent with the given visibility. | | deleteAgent(agent): Promise<void> | Delete a custom agent and all its conversations. The stock agent rool cannot be deleted. |

Schema and Metadata

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.

await space.createCollection('article', {
  schemaOrgType: 'Article',
  fields: [
    { name: 'title', type: { kind: 'string' } },
    { name: 'status', type: { kind: 'enum', values: ['draft', 'published'] } },
    { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } },
    { name: 'author', type: { kind: 'ref' } },
  ],
});

const schema = await space.readSchema();

await space.alterCollection('article', [
  { name: 'title', type: { kind: 'string' } },
  { name: 'status', type: { kind: 'string' } },
]);

await space.writeMeta({ viewport: { x: 0, y: 0, zoom: 1 } });
const meta = await space.readMeta();

| Method | Description | | --- | --- | | readSchema(): Promise<SpaceSchema> | Collection definitions, read from /space/<name>/.schema.json. | | createCollection(name, fieldsOrDef, options?): Promise<CollectionDef> | Create a collection. | | alterCollection(name, fieldsOrDef, options?): Promise<CollectionDef> | Replace a collection definition. | | dropCollection(name): Promise<void> | Remove a collection and its object directory. | | readMeta(): Promise<Record<string, unknown>> | Read metadata from /space/.meta.json. | | writeMeta(meta): Promise<void> | Write the full metadata blob to /space/.meta.json. |

Field kinds: string, number, boolean, ref, enum, literal, array, and maybe.

Undo/Redo

Undo/redo works over the whole space. Checkpoints are managed automatically by the server, so you don't need to create them yourself — just call undo()/redo().

await space.deleteObjects(['/space/article/welcome.json']);

if (await space.canUndo()) {
  await space.undo();
}

| Method | Description | | --- | --- | | canUndo(): Promise<boolean> | Check whether undo is available. | | canRedo(): Promise<boolean> | Check whether redo is available. | | undo(): Promise<boolean> | Restore the latest checkpoint. | | redo(): Promise<boolean> | Reapply undone work. |

Undo/redo availability is scoped to the space.

File Storage and WebDAV

Every space has authenticated WebDAV storage. WebDAV methods take SDK machine paths such as /space/..., /rool-drive/..., or / for the root collection.

const webdav = space.webdav;

await webdav.mkcol('/rool-drive/docs');
await webdav.put('/rool-drive/docs/readme.md', '# Hello', {
  contentType: 'text/markdown',
  ifNoneMatch: '*',
});

const listing = await webdav.propfind('/rool-drive/docs', {
  depth: '1',
  props: ['displayname', 'getcontentlength', 'getcontenttype', 'getetag'],
});

const response = await webdav.get('/rool-drive/docs/readme.md');
console.log(await response.text());

const file = await space.fetchPath('/rool-drive/docs/readme.md');
console.log(file.headers.get('Content-Type'));

const usage = await space.getStorageUsage();
console.log(usage.usedBytes, usage.availableBytes, usage.limitBytes);

Real-time file sync

Object and file changes are announced at the space level. Use WebDAV syncCollection() to reconcile changes.

let token: string | null = null;

async function syncFiles() {
  const result = await space.webdav.syncCollection('/', {
    token,
    level: 'infinite',
    props: ['displayname', 'getetag', 'getlastmodified', 'resourcetype'],
  });
  token = result.token;
  updateFileTree(result.responses);
}

space.on('filesChanged', syncFiles);
space.on('filesReset', () => {
  token = null;
  void syncFiles();
});

await syncFiles();

| Method | Description | | --- | --- | | webdav.href(path) / webdav.url(path) | Return WebDAV href/URL for an absolute SDK path. | | webdav.options(path) | Send OPTIONS. | | webdav.propfind(path, options) | Read properties/list collections. depth is required. | | webdav.syncCollection(path, options) | WebDAV REPORT sync-collection; returns changed responses and next token. | | webdav.get(path, options?) / webdav.head(path) | Read a file; get supports byte ranges. | | webdav.put(path, body, options?) | Write a file/object at an exact path. Parent collection must exist unless createParents: true is passed, which creates missing parent collections atomically (intended for /rool-drive paths). | | webdav.mkcol(path) | Create one collection. | | webdav.copy(source, destination, options?) | Copy a file or collection. | | webdav.move(source, destination, options?) | Move a file or collection. | | webdav.delete(path, options?) | Delete a file or collection. | | webdav.lock(path, options) / refreshLock(path, token) / unlock(token) | WebDAV write locks. | | webdav.request(method, path, init?) | Raw authenticated WebDAV request. | | space.fetchPath(path, options?) | Fetch a /rool-drive/... file path or rool-machine: file URI. | | space.getStorageUsage() / webdav.getStorageUsage() | Storage quota usage. |

High-level WebDAV methods that validate response status throw WebDAVError with status, statusText, and body; raw request() and options() return Response.

Collaboration

New members join a space by redeeming an invite. Owners and admins mint invites; the returned url contains the secret token and is only available at mint time.

// Shareable invite link
const invite = await space.createInvite('editor', { expiresInDays: 7 });
console.log(invite.url);

// Email-guarded invite: single-use, locked to that address, sent by mail
const emailed = await space.createInvite('viewer', { email: '[email protected]' });
if (emailed.emailStatus !== 'sent') {
  // Mail did not go out (e.g. no mail provider configured) — share emailed.url yourself
}

// Manage outstanding invites
const invites = await space.listInvites();
await space.revokeInvite(invites[0].inviteId);

// Change an existing member's role, or remove them
await space.setUserRole(userId, 'admin');
await space.removeUser(userId);

On the join page, look up the invite before sign-in and redeem it once authenticated:

const preview = await client.previewInvite(token); // no auth required
console.log(preview.spaceName, preview.role, preview.inviterName);

const result = await client.redeemInvite(token);
console.log(result.spaceId, result.status); // 'joined' | 'upgraded' | 'already_member'

Invalid, expired, revoked, exhausted, or email-mismatched invites throw InviteError with a code of 'INVITE_INVALID' | 'INVITE_EXPIRED' | 'INVITE_REVOKED' | 'INVITE_EXHAUSTED' | 'INVITE_EMAIL_MISMATCH'.

import { InviteError } from '@rool-dev/sdk';

try {
  await client.redeemInvite(token);
} catch (error) {
  if (error instanceof InviteError && error.code === 'INVITE_EXPIRED') {
    // Ask for a fresh invite
  }
}

Roles (invites grant admin, editor, or viewer — never owner):

| Role | Capabilities | | --- | --- | | owner | Full control. | | admin | Editor capabilities plus user/link management. | | editor | Create, modify, move, and delete objects/files. | | viewer | Read-only access. |

RoolClient API

Constructor config

const client = new RoolClient({
  apiUrl: 'https://api.rool.dev',
  authUrl: 'https://rool.dev/auth',
  graphqlUrl: 'https://api.rool.dev/graphql',
  client: {
    appName: 'com.example.app',
    appVersion: '1.4.2',
    osVersion: 'iOS 17.5',
  },
  logger: console,
});

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.

client is optional application identity sent on requests alongside the SDK package name/version. Compatibility is based only on the SDK version.

| Method/property | Description | | --- | --- | | currentUser: CurrentUser | null | Cached user profile from initialization/fetch. | | getCurrentUser(): Promise<CurrentUser> | Fetch current user. | | updateCurrentUser(input): Promise<CurrentUser> | Update name, slug, or marketingOptIn. | | deleteCurrentUser(): Promise<void> | Mark account for deletion and log out. | | previewInvite(token): Promise<InvitePreview> | Look up an invite link without redeeming it. No auth required. | | redeemInvite(token): Promise<InviteRedeemResult> | Redeem an invite, joining (or upgrading in) its space. | | listSpaces(): Promise<RoolSpaceInfo[]> | List accessible spaces. | | openSpace(id): Promise<RoolSpace> | Open/cached live space handle. | | createSpace(name): Promise<RoolSpace> | Create and open a space. | | duplicateSpace(sourceId, name): Promise<RoolSpace> | Duplicate a space. | | deleteSpace(id): Promise<void> | Permanently delete a space. | | importArchive(name, archive): Promise<RoolSpace> | Import a zip archive as a new space. | | getUserStorage<T>(key): T | undefined | Sync read from user-storage cache. | | setUserStorage(key, value): void | Update user storage; null/undefined deletes. | | getAllUserStorage(): Record<string, unknown> | Copy all cached user storage. | | reportEvent(event, url?): void | Fire-and-forget telemetry event. | | destroy(): void | Close subscriptions, spaces, auth resources, and listeners. | | generateId(): string | Generate a unique ID suitable for conversation IDs. |

Client events

client.on('authStateChanged', (authenticated) => void 0);
client.on('currentUserChanged', (user) => void 0); // CurrentUser | null; null on sign-out
client.on('spaceAdded', (space) => void 0);
client.on('spaceRemoved', (spaceId) => void 0);
client.on('spaceRenamed', (spaceId, newName) => void 0);
client.on('userStorageChanged', ({ key, value, source }) => void 0);
client.on('connectionStateChanged', (state) => void 0);
client.on('error', (error, context) => void 0);
client.on('serverInfoChanged', (info) => void 0);
client.on('unsupported', (info) => void 0); // SDK older than server minimum

RoolSpace API

Properties: id, name, role, memberCount, openSpaceResult, route, webdav.

| Method | Description | | --- | --- | | conversation(conversationId): ConversationHandle | Get a stateless conversation-scoped handle. | | getObject, getObjects | Read object data. | | listConversations, createConversation, deleteConversation | Fetch, create, and delete conversations. | | listAgents, deleteAgent | List the space's agents and delete a custom agent. | | readMeta, writeMeta, readSchema | Read and write WebDAV-backed metadata and schema. | | canUndo, canRedo, undo, redo | Space history controls. | | stopConversation(conversationId): Promise<boolean> | Stop whatever is running in a conversation. | | fetch(url, init?): Promise<Response> | Proxy an external HTTP request through the server to bypass browser CORS. | | close(): void | Stop the space subscription. | | rename(newName): Promise<void> | Rename the space. | | delete(): Promise<void> | Permanently delete the space. | | listUsers(): Promise<SpaceMember[]> | List collaborators. | | setUserRole(userId, role): Promise<void> | Change an existing member's role. | | removeUser(userId): Promise<void> | Remove collaborator. | | createInvite(role, options?): Promise<SpaceInviteCreated> | Mint an invite link; options takes email, expiresInDays, maxUses. | | listInvites(): Promise<SpaceInvite[]> | List currently redeemable invites. | | revokeInvite(inviteId): Promise<boolean> | Revoke an invite so its link stops working. | | exportArchive(): Promise<Blob> | Export a space archive. | | refresh(): Promise<OpenSpaceResult> | Refresh identity, access, member count, and conversation metadata. | | fetchPath(path, options?): Promise<Response> | Fetch a /rool-drive/... file. | | getStorageUsage(): Promise<SpaceFileStorageUsage> | File-storage quota usage. |

Events:

space.on('conversationUpdated', ({ conversationId, conversation, source, timestamp }) => void 0);
space.on('syncError', (error) => void 0);
space.on('filesChanged', ({ spaceId, source, timestamp }) => void 0);
space.on('filesReset', ({ spaceId, source, timestamp }) => void 0);
space.on('connectionStateChanged', (state) => void 0);

Import/Export

const archive = await space.exportArchive();
const imported = await client.importArchive('Imported Data', archive);

Archives include objects, metadata, conversations, and file storage.

Data Types

// Outcome of signInWithPassword. 'verify_required' means the account's email
// isn't verified yet and the server has emailed a magic link.
type PasswordSignInResult = { status: 'signed_in' | 'verify_required' };

type 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[];
  schemaOrgType?: string;
}

type SpaceSchema = Record<string, CollectionDef>;

interface RoolObject {
  path: string;
  body: Record<string, unknown>;
}

interface GetObjectsResult {
  objects: RoolObject[];
  missing: string[];
}

// Who may see a conversation: 'shared' is every space member; 'private' is
// owner-only; 'temporary' is private plus auto-delete once it sits idle.
type ConversationVisibility = 'shared' | 'private' | 'temporary';

// Lightweight conversation roster entry (no interaction bodies), returned by
// openSpace/listConversations. `updatedAt` drives last-activity display.
interface ConversationMeta {
  id: string;
  agent: string; // owning agent ('rool' is the stock agent)
  visibility: ConversationVisibility;
  name: string | null;
  systemInstruction: string | null;
  createdAt: number;
  createdBy: string;
  interactionCount: number;
  updatedAt: number;
}

type InviteRole = 'admin' | 'editor' | 'viewer';

interface SpaceInvite {
  inviteId: string;
  spaceId: string;
  role: InviteRole;
  email: string | null;
  createdBy: string;
  createdAt: string;
  expiresAt: string;
  maxUses: number | null;
  useCount: number;
}

// Outcome of the invite email send. Null when no email was involved (open link).
// The invite is always minted and its `url` is usable regardless of this value;
// only the email delivery is reflected here.
// - 'sent': email dispatched
// - 'not_configured': server has no mail provider (local dev)
// - 'failed': provider rejected the send
// - 'cooldown': a recent invite to this same address was already emailed
// - 'rate_limited': the inviter hit their daily email-invite cap
// Treat unknown values as not sent.
type InviteEmailStatus =
  | 'sent'
  | 'not_configured'
  | 'failed'
  | 'cooldown'
  | 'rate_limited'
  | (string & {});

interface SpaceInviteCreated {
  inviteId: string;
  spaceId: string;
  role: InviteRole;
  email: string | null;
  expiresAt: string;
  maxUses: number | null;
  url: string; // contains the secret token; only available at mint time
  emailStatus: InviteEmailStatus | null;
}

interface InvitePreview {
  spaceId: string;
  spaceName: string;
  role: InviteRole;
  email: string | null;
  inviterName: string | null;
}

interface InviteRedeemResult {
  spaceId: string;
  role: RoolUserRole;
  status: 'joined' | 'upgraded' | 'already_member';
}

type PromptAttachment =
  | File
  | Blob
  | { data: string; contentType: string; filename?: string }
  | string;

type PromptEffort = 'QUICK' | 'STANDARD' | 'REASONING' | 'RESEARCH';

interface PromptOptions {
  responseSchema?: Record<string, unknown>;
  effort?: PromptEffort;
  parentInteractionId?: string | null;
  ephemeral?: boolean;
  readOnly?: boolean;
  attachments?: PromptAttachment[];
  signal?: AbortSignal;
  eventName?: string;
}

type InteractionStatus = 'pending' | 'streaming' | 'done' | 'error';

interface Interaction {
  id: string;
  parentId: string | null;
  timestamp: number;
  userId: string;
  userName?: string | null;
  operation: 'prompt' | 'putObject' | 'patchObject' | 'moveObject' | 'deleteObjects' | 'deletePaths' | string;
  input: string;
  output: string | null;
  status: InteractionStatus;
  ai: boolean;
  modifiedObjectPaths: string[];
  toolCalls: ToolCall[];
  attachments?: string[];
}

License

MIT - see LICENSE for details.