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

@skillful-ai/agent-widget

v0.2.0

Published

Embeddable chat + live voice-call widget for Skillful agents (WordPress / any site)

Readme

@skillful-ai/agent-widget

Embeddable chat + live voice-call widget for Skillful agents. Drop one <script> tag onto any website — WordPress, plain HTML, React — and get a floating bubble that does text chat and a real-time browser voice call, both backed by the same Skillful Agents API.

  • 🪶 One script tag — no build step required on the host site.
  • 🎨 Fully themeable — colors, position, sizing, copy, light/dark.
  • 🔌 Shadow-DOM isolated — never clashes with the host page's CSS.
  • 💬 Chat over SSE · 📞 Voice over WebRTC/Socket.IO with live transcript.
  • 🔐 Key-safe — the API key stays on your server; the browser only gets a short-lived token.
  • 🧪 Customization Studio — a live playground that generates the exact embed snippet.

Table of contents


Install

Two ways to use it — pick whichever fits the host site:

A. Script tag (any website — WordPress, Webflow, plain HTML). No install, no build. Load the bundle from the CDN:

<script src="https://cdn.jsdelivr.net/npm/@skillful-ai/[email protected]/dist/widget.js"></script>

B. npm (React / Vue / any bundler). Install and import:

npm install @skillful-ai/agent-widget

Both options expose the exact same configuration; only the way you load it differs. The CDN bundle is fully self-contained (styles + voice worklet); the npm build ships ESM + TypeScript types.


Quick start

Once the script tag is on the page, one call mounts a floating chat bubble:

<script src="https://cdn.jsdelivr.net/npm/@skillful-ai/[email protected]/dist/widget.js"></script>
<script>
  SkillfulChat.init({
    apiUrl:  'https://api.agents.skillfulai.io',
    agentId: '<AGENT_UUID>',
    token:   '<EMBED_JWT>',          // minted server-side — see Authentication
    title:   'Support',
    greeting:'Hi! How can I help?',
    theme:   { mode: 'dark', position: 'bottom-right' },
  });
</script>

That's the entire integration for a static site. The only value you have to produce on your side is token (the Authentication section explains exactly how, with copy-paste code).

SkillfulChat.init(config) returns an instance:

const agent = SkillfulChat.init({ ... });
agent.open();             // open the panel
agent.close();            // close it
agent.updateToken(jwt);   // swap in a refreshed token
agent.destroy();          // remove the widget entirely

To add voice, set voice.enabled (a 📞 button appears in the header):

SkillfulChat.init({
  apiUrl: '…', agentId: '…', token: '<EMBED_JWT>',
  voice: { enabled: true, apiKey: '<RAW_API_KEY>', disableBargeIn: true },
});

Use with a bundler (React, Vue, etc.)

The npm package exports an ESM build with TypeScript types:

import { mountWidget } from '@skillful-ai/agent-widget'
import type { WidgetConfig } from '@skillful-ai/agent-widget'

// Mounts into its own Shadow-DOM host (same as the script tag).
const widget = mountWidget({ apiUrl: '…', agentId: '…', token: '…' })
// later: widget.destroy()

In React, call mountWidget from an effect and destroy() on cleanup, or render the SkillfulChatWidget component directly. Either way, react / react-dom are optional peer deps — if your app already has React, the widget reuses it.


Configuration reference

SkillfulChat.init({
  // ── required ──
  apiUrl: string,                 // Skillful Agents API base URL
  agentId: string,                // agent UUID
  token: string,                  // chat auth token (embed JWT — see Authentication)

  // ── optional ──
  organizationId?: string,
  title?: string,                 // header title
  greeting?: string,              // first assistant message
  placeholder?: string,           // input placeholder
  avatar?: string,                // avatar image URL
  showBranding?: boolean,         // "Powered by Skillful" footer (default true)
  session?: 'ephemeral' | 'persistent',   // history model (default 'ephemeral')

  theme?: {
    mode?: 'light' | 'dark' | 'auto',
    position?: 'bottom-right' | 'bottom-left',
    primaryColor?: string,        // brand color (hex)
    backgroundColor?: string,
    textColor?: string,
    borderColor?: string,
    fontFamily?: string,
    borderRadius?: number,        // px
    bubbleSize?: number,          // px
    panelWidth?: number,          // px
    panelHeight?: number,         // px
    zIndex?: number,
  },

  voice?: {
    enabled: boolean,             // adds the "Talk" button + call view
    apiKey?: string,              // RAW API key / wallet JWT for the voice socket
    agentId?: string,             // defaults to top-level agentId
    organizationId?: string,
    agentName?: string,           // name shown in the call UI
    disableBargeIn?: boolean,     // default true (open-speaker safe)
    workletUrl?: string,          // default '/audio/worklet-processor.js'
  },

  leadForm?: {                    // intro mode: form instead of a greeting
    enabled: boolean,
    title?: string,
    description?: string,
    submitLabel?: string,
    fields?: Array<{ key, label, type?: 'text'|'email'|'tel', required?, placeholder? }>,
    messageTemplate?: string,     // {key} placeholders → submitted values
  },

  // callbacks
  onMessage?: (m) => void,
  onOpen?: () => void,
  onClose?: () => void,
  onError?: (err) => void,
});

Authentication: test vs production

The widget is agnostic about how you get the token — it just takes one. That lets you start with the easy path and harden later with zero widget changes.

🧪 Test / quick (insecure — local only)

Mint the token in the browser, or pass the raw API key. Fast for trying things; never ship this — the API key would be visible in page source. The Customization Studio does this for you.

🔒 Production (server-side minting — like Zeno)

Your backend holds the API key and signs a short-lived embed JWT; the browser only ever sees that token. This is exactly how the Zeno product does it (zenobet-main-api skillful.service.generateEmbedToken). The token is a standard HS256 JWT:

jwt.sign(
  { sub: '<unique-per-visitor>', agentId: '<AGENT_UUID>', iss: '<client-user-id>' },
  API_KEY,                       // the HMAC secret — stays on the server
  { algorithm: 'HS256', expiresIn: '1h' },
)
  • iss must be the client user id that owns the API key (the backend looks up that user's keys to verify the signature).
  • sub should be unique per visitor/session so conversations don't co-mingle.

For WordPress, the included mu-plugin does this minting in PHP for you.

⚠️ Chat and voice use different credentials

Verified against the backend — this is the #1 gotcha:

| Channel | Endpoint | Accepts | |---------|----------|---------| | Chat | POST /api/embed/chat/{agentId} (REST/SSE) | the embed JWT (HS256, signed with the API key). Not a raw API key. | | Voice | /socket.io connect auth | the raw API key (UUID) or a wallet JWT. Not the embed JWT. |

So put the embed JWT in token (chat) and the raw API key in voice.apiKey. On a public site this exposes the key for voice — see the voice note.


Intro modes — greeting vs lead form

By default the widget opens with a greeting bubble. Set leadForm to switch to lead-capture mode instead: on open it shows a short form (e.g. name / phone / email), and on submit it fills messageTemplate with the values and sends that as the user's first message — so there's no greeting, the first bubble is the visitor's, and the agent replies to it (and now has their details). Each field's required flag is set per deployment; email fields are format-validated.

leadForm: {
  enabled: true,
  title: 'Mielőtt kezdenénk…',
  submitLabel: 'Indítás',
  fields: [
    { key: 'name',  label: 'Név',         type: 'text',  required: true },
    { key: 'phone', label: 'Telefonszám', type: 'tel',   required: true },
    { key: 'email', label: 'E-mail cím',  type: 'email', required: false },
  ],
  messageTemplate:
    'Üdvözlöm! A nevem {name}, a telefonszámom pedig {phone}, az e-mail címem pedig {email}.',
}

The form reappears on a fresh conversation (new tab / "New conversation" button).

Conversation history & privacy

History lives in the visitor's own browser, namespaced per agent. The session option chooses the store and therefore the isolation model:

| session | Store | Behavior | |-----------|-------|----------| | 'ephemeral' (default) | sessionStorage | One conversation per browser tab; survives reload in that tab but auto-clears when the tab closes and is never shared across tabs. Best for support / anonymous / shared devices — the next visitor never inherits the last one's chat. | | 'persistent' | localStorage | Conversation resumes across reloads and future visits. Use only when the token sub is a real per-user account id. |

A "New conversation" button in the header clears and restarts at any time (either mode). Server-side isolation also relies on a unique sub per visitor in the token.


WordPress

A ready-to-use PHP mu-plugin lives in wordpress/. It mints the chat token server-side (API key never reaches the browser) and loads the widget from jsDelivr — drop the file in wp-content/mu-plugins/, set four constants in wp-config.php, done. Full guide: wordpress/README.md.

Voice on WordPress: the voice socket currently needs the raw API key in the browser, so the plugin keeps voice off by default. Enable it only on internal/trusted pages until the backend accepts the embed token on the socket too.


Customization Studio (playground)

playground.html is a live studio for tuning a deployment: toggle chat-only vs chat + voice, pick brand colors / position / sizing, edit the copy, and copy the generated embed snippet. Run it with npm run dev → http://localhost:5173/playground.html.


Browser support & requirements

  • HTTPS is required for microphone access (voice). Any real site is fine; localhost counts as secure.
  • The Agents API must allow CORS from the embedding origin (chat REST and Socket.IO).
  • Voice uses the AudioWorklet + getUserMedia APIs (all modern browsers; test iOS Safari, which is the fussiest).
  • The AudioWorklet file is loaded at runtime from voice.workletUrl — it can't be inlined into widget.js, so make sure dist/audio/worklet-processor.js is reachable (the CDN URL handles this automatically).
  • Shadow DOM isolates the widget from the host page's CSS.

Local development

npm install
npm run dev        # dev server + Customization Studio at http://localhost:5173
npm run build      # → dist/widget.js (IIFE), dist/chat-widget.js (ESM), dist/audio/, *.d.ts
npm run typecheck  # tsc --noEmit

Publishing (npm → jsDelivr CDN)

npm version patch          # bump
npm publish --access public
# → served at https://cdn.jsdelivr.net/npm/@skillful-ai/agent-widget@<version>/dist/widget.js

prepublishOnly rebuilds automatically. Only dist/ + this README are published — no source, no playground, no secrets.


Project structure

| Path | What | |------|------| | src/init.ts | IIFE entry — exposes window.SkillfulChat.init() | | src/index.ts | ESM entry — mountWidget, SkillfulChatWidget, types | | src/core/use-chat.ts | Chat over SSE (src/core/api-client.ts, sse-parser.ts) | | src/core/storage.ts | Session/history storage (ephemeral vs persistent) | | src/core/voice/use-voice-call.ts | Realtime voice call over Socket.IO | | src/core/voice/{audio-recorder,audio-player,speech-detector}.ts | Mic capture · playback · VAD | | public/audio/worklet-processor.js | 16 kHz PCM AudioWorklet → dist/audio/ | | src/components/CallPanel.tsx | Voice-call UI (reactive orb + transcript) | | src/components/* | Chat bubble, panel, header, messages | | playground.html | Customization Studio | | wordpress/ | PHP mu-plugin + install guide |

Built on the proven chat widget (SSE, Shadow-DOM) with the voice engine ported from huntline-app-fe (Socket.IO realtime + AudioWorklet). Chat and voice hit the same apiUrl: chat via REST/SSE, voice via io(apiUrl, { path: '/socket.io' }).