# @skippr/live-agent-sdk

[![Website](https://img.shields.io/badge/Website-skippr.ai-blue)](https://skippr.ai) [![NPM Version](https://img.shields.io/npm/v/%40skippr%2Flive-agent-sdk?color=red)](https://www.npmjs.com/package/@skippr/live-agent-sdk)

Embed product specialists that see, speak, and act in real time. Configure **modules** — for onboarding, demos, training, support, and anything else — and let your users pick which one to talk to from a single embed.

## Key Features

- **Multi-module picker** — the widget fetches your active modules and lets the user pick which one to start (onboarding, demo, support, etc.), so a single embed covers every use case
- **Real-time voice** — two-way audio with live transcription
- **Capture modes** — `screenshare` (user shares screen) or `auto` (DOM capture)
- **Agent controls** — opt-in capabilities like element highlighting and on-screen actions, configured per module (auto mode only)
- **Chat + transcript** — text messaging with voice transcripts merged into one thread
- **Session agenda** — structured phases with progress tracking
- **Flexible auth** — email OTP (direct auth) or backend-signed JWT (secret mode)
- **Drop-in integration** — one React component or script tag, no WebRTC code needed
- **Host-safe styles** — prefixed CSS that won't conflict with your app

## Prerequisites

You need an `appKey` from the [Skippr Platform](https://specialist.skippr.ai/).

1. **Sign up** at [specialist.skippr.ai](https://specialist.skippr.ai/)
2. **Create your modules** in the dashboard — for example an `onboarding` agent to walk new users through setup and a `support` module for help. Every active module shows up in the widget's picker automatically.
3. **Create an App Key** in Settings and copy the `appKey` — one App Key works for all your modules

Optionally, you can pin the widget to a single module by passing its id as `agentId` (see [Pinning](#pinning-to-a-specific-module) below). Without it, the user picks from all active modules at runtime.

## Installation

### React

```bash
npm install @skippr/live-agent-sdk
```

### Script Tag

No install needed. Add this to any webpage:

```html
<script src="https://unpkg.com/@skippr/live-agent-sdk/dist/skippr-sdk.js"></script>
<script>
  Skippr.initialize({
    appKey: 'pk_live_your_key',
  });
</script>
```

## Quick Start

The widget opens with a picker of your active modules — the user chooses which one to start a session with.

### React

```tsx
import { LiveAgent } from '@skippr/live-agent-sdk';
import '@skippr/live-agent-sdk/styles';

function App() {
  return <LiveAgent appKey="pk_live_your_key" />;
}
```

### Script Tag

```html
<script src="https://unpkg.com/@skippr/live-agent-sdk/dist/skippr-sdk.js"></script>
<script>
  Skippr.initialize({
    appKey: 'pk_live_your_key',
  });
</script>
```

### Pinning to a specific module

If you want to skip the picker and always open straight into one module, pass its id as `agentId`:

```tsx
<LiveAgent appKey="pk_live_your_key" agentId="your_module_id" />
```

## Authentication

The SDK supports two identity modes, configured per App Key in the Skippr dashboard.

### Direct Auth (default)

Users log in via email OTP inside the widget. No backend integration needed. The SDK handles the full OTP flow: email input, code verification, and token persistence automatically.

> **Best suited for development and testing.** Direct Auth persists tokens in the browser and has no automatic token refresh, so it works in production but isn't what we recommend there. For production, we suggest Secret Mode — your backend vouches for the user and controls token lifetime.

**React:**

```tsx
<LiveAgent appKey="pk_live_your_key" />
```

**Script tag:**

```js
Skippr.initialize({
  appKey: 'pk_live_your_key',
});
```

### Secret Mode

Your backend signs a short-lived JWT with the App Key's identity secret. You give the SDK a `getUserToken` callback that returns a freshly signed JWT each time it's called; the SDK exchanges it for a bearer token server-side and skips the login form entirely.

Because it's a callback (not a static token), the SDK can keep the session alive without a page reload: it invokes `getUserToken` to bootstrap, again proactively shortly before the current bearer expires, and once more if a request comes back `401`. Your callback should always return a *current* JWT from your source of truth — typically by re-minting it on your backend per call.

The bearer's lifetime is the **session token lifetime** you set on the App Key in the dashboard (15 minutes to 24 hours, default 1 hour).

**Step 1: Generate a signed JWT on your backend**

The JWT must be signed with **HS256** using the identity secret you received when creating the App Key.

| Claim | Type | Required | Description |
|-------|------|----------|-------------|
| `sub` | `string` | Yes | Unique user identifier in your system |
| `name` | `string` | No | User's display name |
| `email` | `string` | No | User's email address |
| `exp` | `number` | Recommended | Expiration timestamp (Unix seconds) |

**Node.js example:**

```js
import jwt from 'jsonwebtoken';

const userToken = jwt.sign(
  {
    sub: user.id,
    name: user.name,
    email: user.email,
  },
  process.env.SKIPPR_IDENTITY_SECRET,
  { algorithm: 'HS256', expiresIn: '1h' },
);
```

**Step 2: Give the SDK a callback that fetches a fresh token**

**React:**

```tsx
<LiveAgent
  appKey="pk_live_your_key"
  getUserToken={() => fetch('/api/skippr-token').then((r) => r.text())}
/>
```

**Script tag:**

```js
Skippr.initialize({
  appKey: 'pk_live_your_key',
  getUserToken: () => fetch('/api/skippr-token').then((r) => r.text()),
});
```

The callback runs whenever the SDK needs a token, so each call should return a newly minted JWT (e.g. from the `/api/skippr-token` endpoint on your backend that signs the JWT shown in Step 1).

#### Static token (testing)

If you already hold a signed JWT and just want to drop it in, pass it as a static `userToken` string instead of a callback. The SDK exchanges it for a bearer token the same way.

```tsx
<LiveAgent appKey="pk_live_your_key" userToken={signedJwt} />
```

> **Best suited for quick tests.** A static token can't be re-minted, so once it expires the session cannot refresh — for production use the `getUserToken` callback above. When both are provided, `getUserToken` wins.

## Custom Components

Any component rendered inside `<LiveAgent>` can use the `useLiveAgent` hook to access session state and controls:

```tsx
import { LiveAgent, useLiveAgent } from '@skippr/live-agent-sdk';

function ConnectionStatus() {
  const { isConnected } = useLiveAgent();

  if (isConnected) return <p>Agent connected</p>;
  return <p>Agent disconnected</p>;
}

function App() {
  return (
    <LiveAgent appKey="pk_live_your_key">
      <ConnectionStatus />
    </LiveAgent>
  );
}
```

## API Reference

### `<LiveAgent>`

Self-contained widget component. Renders a floating button that opens a sidebar panel for real-time agent interaction.

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `appKey` | `string` | *required* | Publishable App Key from the Skippr dashboard |
| `agentId` | `string` | — | Pin the widget to a specific module (pass that module's id). Omit to let the user pick from all your active modules at runtime. |
| `getUserToken` | `() => Promise<string>` | — | Async callback returning a freshly signed JWT for secret mode. The SDK calls it to bootstrap, again proactively before the bearer expires, and once more on a `401` to re-bootstrap. |
| `userToken` | `string` | — | Static signed JWT for secret mode (testing). Exchanged for a bearer token but can't be refreshed once expired. `getUserToken` takes precedence when both are set. |
| `position` | `'left' \| 'right'` | `'right'` | Side of the screen for the widget |
| `variant` | `'floating' \| 'sidebar'` | `'floating'` | Widget display mode |
| `minimizable` | `boolean` | `true` | Whether the widget can be minimized |
| `defaultOpen` | `boolean` | `false` | Whether the panel starts open |
| `welcomeMessage` | `string` | — | Message shown on the minimized bubble |
| `startSessionLabel` | `string` | `'Talk to Skippr'` | Label for the start-session button (only shown when `agentId` is pinned) |
| `autoFocusChat` | `boolean` | `true` | Whether the chat input auto-focuses when opened |
| `captureMode` | `'screenshare' \| 'auto'` | `'screenshare'` | How the agent sees the page — `'screenshare'` prompts the user to share, `'auto'` uses DOM capture (no permission prompt) |
| `animateAgentCursor` | `boolean` | `false` | Animate the agent's cursor as it points to elements on the page. Primarily configured per agent in the dashboard; this prop is a fallback when the agent has no preference set. |

### `useLiveAgent()`

Hook for accessing session state and panel controls. Must be called within `<LiveAgent>`.

#### State

| Field | Type | Description |
|-------|------|-------------|
| `isConnected` | `boolean` | Whether the agent is connected |
| `isStarting` | `boolean` | Whether a session is being created or resumed |
| `isDisconnecting` | `boolean` | Whether the session is being torn down |
| `isPausing` | `boolean` | Whether a pause request is in flight (before `isPaused` commits) |
| `isPaused` | `boolean` | Whether the current session is paused |
| `resumableSession` | `{ id: string } \| null` | The pinned agent's most recent paused session, or `null` |
| `resumableSessions` | `{ id: string; agentId: string }[]` | All of the user's paused sessions (one per agent), for module pickers |
| `isPanelOpen` | `boolean` | Whether the panel is currently open |
| `isMinimized` | `boolean` | Whether the widget is minimized |
| `isAuthenticated` | `boolean` | Whether the user is authenticated |
| `variant` | `'floating' \| 'sidebar'` | Current widget display mode |
| `position` | `'left' \| 'right'` | Current widget position |
| `error` | `string` | Error message, if any |
| `hasModuleSelector` | `boolean` | Whether the widget is in picker mode |
| `availableModules` | `Module[]` | Modules fetched for the picker |
| `activeModule` | `Module \| null` | The module the current session was started with |
| `isLoadingModules` | `boolean` | Whether the picker list is being fetched |
| `modulesError` | `string \| null` | Error from fetching the picker list, if any |

```ts
interface Module {
  id: string;
  name: string;
  description: string | null;
  type: string;
  priority: number;
  controls: { highlight?: boolean; actions?: boolean };
}
```

#### Methods

| Method | Type | Description |
|--------|------|-------------|
| `startSession` | `(opts: { agentId: string; agentControls?: { highlight?: boolean; actions?: boolean } }) => Promise<void>` | Start a new agent session |
| `pauseSession` | `() => Promise<void>` | Pause the active session and disconnect from LiveKit; resumable later |
| `resumeSession` | `() => Promise<void>` | Resume the pinned agent's paused session, reconnecting to continue |
| `disconnect` | `() => Promise<void>` | End the current session |
| `openPanel` | `() => void` | Open the panel |
| `closePanel` | `() => void` | Close the panel |
| `togglePanel` | `() => void` | Toggle the panel open/closed |
| `expandPanel` | `() => void` | Expand from minimized state |
| `minimizePanel` | `() => void` | Minimize the widget |
| `setPosition` | `(position: 'left' \| 'right') => void` | Change widget position |
| `selectModule` | `(id: string) => void` | Start a session with the module of the given id |
| `refetchModules` | `() => Promise<void>` | Refetch the picker list |

### Additional Hooks

All hooks must be called within `<LiveAgent>`.

| Hook | Returns | Description |
|------|---------|-------------|
| `useMediaControls()` | `{ isMuted, isScreenSharing, toggleMute, toggleScreenShare }` | Mic and screen share state and toggles |
| `useAgentVoiceState()` | `{ state, isSpeaking, isListening }` | Agent voice activity state — `state` is the full agent state (`'listening' \| 'thinking' \| 'speaking' \| ...`) |
| `useIsLocalSpeaking()` | `boolean` | Whether the local user is currently speaking |
| `useElapsedSeconds(isRunning)` | `number` | Drift-safe elapsed seconds since the flag flipped to `true` |

### Utilities

| Export | Signature | Description |
|--------|-----------|-------------|
| `formatTime` | `(seconds: number) => string` | Format seconds as `mm:ss` |

### Global API (Script Tag)

Available on `window.Skippr` when using the script tag bundle.

| Method | Description |
|--------|-------------|
| `Skippr.initialize(config)` | Mount the widget. Accepts `appKey` (required), `agentId` (optional — omit for picker), `getUserToken`, `userToken`, `position`, `variant`, `minimizable`, `captureMode`. |
| `Skippr.logout()` | Revoke the current session server-side, clear stored auth tokens, and show the login form (direct auth mode) |
| `Skippr.destroy()` | Remove the widget from the page and clear auth tokens |


## On-screen actions

When an agent has the `actions` control enabled (auto mode only), it can act on the page for the user, not just guide them. A small on-screen indicator shows each action as it happens.

Actions are guardrailed: the SDK refuses to act on masked fields (passwords, secrets), sensitive iframes (Stripe, Plaid, captchas), or disabled controls. Use `data-skippr-private` (below) to put anything else off limits.

## Hiding elements from the agent

To hide an element (and everything inside it) from the agent in `captureMode: 'auto'`, add the `data-skippr-private` attribute:

```jsx
<div data-skippr-private>
  <SensitiveContent />
</div>
```

```html
<section data-skippr-private>
  <input type="password" />
</section>
```

Use this for sensitive content or anything you don't want the agent to see or reference.

## Support

For questions, technical support, or feedback:

* **Email**: contact@skippr.ai
* **Website**: [https://skippr.ai](https://skippr.ai)

---

© 2026 Skippr