@oakmore/agent
v0.1.1
Published
Oakmore Runsmart chat widget SDK - embeddable health bot with full-screen and floating panel modes
Readme
@oakmore/agent
Embeddable chat widget for the Oakmore Runsmart health bot. Use full-screen or floating panel (slide-over) on your site with minimal setup.
Production API: https://agent.oakmore.io (Swagger docs)
Test build: Uses your existing auth only (Bearer token). No tenant API key required.
Install
npm install @oakmore/agentQuick start
Full-screen layout
Renders the full chat UI (sidebar + messages) inside a container.
import { init } from '@oakmore/agent';
init('#chat-root', {
baseUrl: 'https://agent.oakmore.io',
getAuthToken: async () => {
// Return your app's user JWT, or null if not logged in
return localStorage.getItem('authToken');
},
layout: 'fullpage',
});<div id="chat-root" style="height: 100vh;"></div>Floating button (panel) layout
Shows a chat icon button (e.g. bottom-right). Click to open a slide-over panel with the same chat UI.
import { init } from '@oakmore/agent';
init('#chat-root', {
baseUrl: 'https://agent.oakmore.io',
getAuthToken: async () => localStorage.getItem('authToken'),
layout: 'panel',
position: 'right',
width: 420,
});Use an empty container; the button is fixed to the viewport. Example:
<div id="chat-root"></div>With React
Use the React component instead of init():
import { OakmoreChat } from '@oakmore/agent';
const config = {
baseUrl: 'https://agent.oakmore.io',
getAuthToken: async () => localStorage.getItem('authToken'),
layout: 'fullpage',
theme: { title: 'Health Assistant', primaryColor: '#1976d2' },
};
function App() {
return (
<div style={{ height: '100vh' }}>
<OakmoreChat config={config} />
</div>
);
}Script tag (UMD)
Load the IIFE bundle, then call the global:
<div id="chat-root" style="height: 100vh;"></div>
<script src="https://unpkg.com/@oakmore/agent/dist/runsmart-chat.umd.global.js"></script>
<script>
OakmoreRunsmartChat.init('#chat-root', {
baseUrl: 'https://agent.oakmore.io',
getAuthToken: async function() {
return localStorage.getItem('authToken');
},
layout: 'fullpage'
});
</script>Note: With script tag, React and React-DOM must be loaded on the page if the bundle expects them as externals, or use the full UMD bundle that includes dependencies.
Authentication
You use the same Bearer token as the user who is already signed in to your app. The SDK does not log anyone in; it only sends the token you provide on each API request.
How it works
- Your app signs the user in (e.g. Supabase Auth, Auth0, or your own login). The user gets a JWT/session.
- You pass a callback to the SDK:
getAuthToken: async () => .... That callback returns the current user’s token (ornullif not logged in). - The SDK calls your callback whenever it needs to talk to the Oakmore API, then sends
Authorization: Bearer <token>. - The Oakmore backend validates the token (e.g. with Supabase). Your backend already does this: it uses
supabase.auth.get_user(token)and then loads/creates the user profile. So any valid Supabase JWT from your app works.
So: the chat runs inside another web UI; the user is already authenticated there. You give the SDK a way to get that same token, and the backend validates it with Supabase (or your configured provider). No separate login inside the widget is required.
Can the SDK “access” the token? Security?
The SDK does not read your app’s storage or memory by itself. It only receives what you return from getAuthToken. You implement the callback; the SDK just calls it when it needs a token. So:
- Safe: Your app gets the token from your auth provider (e.g. Supabase session) inside the callback and returns it. The token is only used for outbound API requests.
- Avoid: Storing the token in a global or
localStorageonly so the SDK can read it. Prefer getting it from your auth client (e.g.getSession()) in the callback so only your code and the SDK’s requests use it.
So it’s not a security issue to “give” the token to the SDK via the callback: the host app already has the token; the callback is just the agreed way to pass it for API calls.
Example: Supabase in the host app
If your host app uses Supabase Auth, pass the Supabase access token in the callback. The Oakmore backend will validate it with Supabase.
import { createClient } from '@supabase/supabase-js';
import { init } from '@oakmore/agent';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
init('#chat-root', {
baseUrl: 'https://agent.oakmore.io',
getAuthToken: async () => {
const { data: { session } } = await supabase.auth.getSession();
return session?.access_token ?? null;
},
layout: 'fullpage',
});When the user is not signed in, return null; the widget can show a “Sign in” state or you can hide it until the user is logged in.
Config (test build)
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| baseUrl | string | Yes | API origin (e.g. https://agent.oakmore.io). |
| getAuthToken | () => Promise<string | null> | Yes | Returns the user's Bearer token, or null if not authenticated. |
| layout | 'fullpage' \| 'panel' | No | Default 'fullpage'. Use 'panel' for floating button + slide-over. |
| position | 'left' \| 'right' | No | Panel only. Side from which the panel slides. Default 'right'. |
| width | number | string | No | Panel only. Panel width in px. Default 420. |
| conversationHistoryLimit | number | 'all' | No | Max conversations in sidebar (e.g. 5 or 'all'). Default behaviour: show up to 50. |
| theme | object | No | { primaryColor?, logoUrl?, title? }. |
| stream | boolean | No | Default true. Use streaming responses. |
Headless (no UI)
Use only the client for custom UIs. Import from @oakmore/agent/core (no React/MUI deps).
import { RunsmartClient } from '@oakmore/agent/core';
const client = new RunsmartClient({
baseUrl: 'https://agent.oakmore.io',
getAuthToken: async () => localStorage.getItem('authToken'),
});
// Stream a query
for await (const chunk of await client.streamQuery('How can I improve my sleep?')) {
if (chunk.type === 'content' && chunk.data?.chunk) {
process.stdout.write(chunk.data.chunk);
}
}
// List conversations
const list = await client.getConversations(10);
// Update a conversation's title
await client.updateConversationTitle(conversationId, 'My new title');Multi-turn (thread continuity)
Persist thread_id from stream chunks and pass it to subsequent queries:
let threadId: string | null = null;
for await (const chunk of await client.streamQuery('I want to improve my health', threadId)) {
if (chunk.thread_id) threadId = chunk.thread_id;
if (chunk.type === 'content' && chunk.data?.chunk) {
console.log(chunk.data.chunk);
}
}
// Follow-up in same thread
for await (const chunk of await client.streamQuery('I have trouble sleeping', threadId)) {
// ...
}Conversation list and switching
const conversations = await client.getConversations(20);
// Load messages when user selects a conversation
const messages = await client.getConversationMessages(conversations[0].id);
// Send follow-up in that thread
for await (const chunk of await client.streamQuery('Tell me more', conversations[0].id)) {
// ...
}Error handling
for await (const chunk of await client.streamQuery(query, threadId)) {
if (chunk.type === 'error') {
console.error(chunk.data?.error);
break;
}
// ...
}For thrown errors (e.g. 401, 422):
try {
const list = await client.getConversations();
} catch (err) {
if (err instanceof Error && err.message.includes('401')) {
// Redirect to login or refresh token
}
}React hooks
For React apps, use useRunsmartClient and useChatStream from @oakmore/agent/react:
import { useRunsmartClient, useChatStream } from '@oakmore/agent/react';
function Chat() {
const client = useRunsmartClient({
baseUrl: 'https://agent.oakmore.io',
getAuthToken: async () => localStorage.getItem('authToken'),
});
const { sendMessage, messages, isLoading, error } = useChatStream(client);
// ...
}Integration guides
- Integration Steps – Step-by-step guide to connect an existing chat UI to the agent
- Bring Your Own UI (BYOI) – Full reference (auth, CORS, context questions, code examples)
License
UNLICENSED. Copyright © Oakmore Labs LLC.
