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

@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/agent

Quick 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

  1. Your app signs the user in (e.g. Supabase Auth, Auth0, or your own login). The user gets a JWT/session.
  2. You pass a callback to the SDK: getAuthToken: async () => .... That callback returns the current user’s token (or null if not logged in).
  3. The SDK calls your callback whenever it needs to talk to the Oakmore API, then sends Authorization: Bearer <token>.
  4. 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 localStorage only 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

License

UNLICENSED. Copyright © Oakmore Labs LLC.