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

@exotel-npm-dev/exotel-ai-assist

v1.1.5

Published

AI-powered real-time suggestions, transcript, and sentiment for agent calls

Downloads

1,740

Readme

@exotel-npm-dev/exotel-ai-assist

Real-time AI suggestions and live transcript for Exotel calls — delivered over WebSocket.

Features

  • One WebSocket per browser session — uses SharedWorker (with BroadcastChannel + Navigator Locks fallback) so multiple open tabs share a single connection
  • Auto-reconnect with exponential back-off
  • Live call_sid switching — closing the old connection and opening a new one automatically
  • Framework-agnostic — works in Vue, Angular, vanilla JS, or plain HTML
  • React subpath for React apps that want to avoid bundling React twice
  • Headless controller subpath for raw data with no UI

Installation

npm install @exotel-npm-dev/exotel-ai-assist

React is bundled inside the default build. You do not need React installed for the default entry point.


Quick Start — Plain HTML / Vanilla JS

Note: Browsers cannot resolve bare specifiers (@exotel-npm-dev/...) in <script type="module"> without a bundler or an import map. Choose one of the approaches below.

Option A — Recommended: Vite (or any bundler)

npm create vite@latest my-app -- --template vanilla
cd my-app
npm install @exotel-npm-dev/exotel-ai-assist
// main.js
import { mountExotelAIAssist } from "@exotel-npm-dev/exotel-ai-assist";

mountExotelAIAssist(document.getElementById("ai-assist"), {
  authToken: "your-auth-token",
  call_sid: "CALL-SID-001",
  accountId: "your-account-id",
});

Option B — Import map (no bundler, static server)

Add an import map before your module script so the browser knows where to find the package:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script type="importmap">
      {
        "imports": {
          "@exotel-npm-dev/exotel-ai-assist": "./node_modules/@exotel-npm-dev/exotel-ai-assist/dist/index.js"
        }
      }
    </script>
  </head>
  <body>
    <div id="ai-assist" style="height: 500px;"></div>

    <script type="module">
      import { mountExotelAIAssist } from "@exotel-npm-dev/exotel-ai-assist";

      mountExotelAIAssist(document.getElementById("ai-assist"), {
        authToken: "your-auth-token",
        call_sid: "CALL-SID-001",
        accountId: "your-account-id",
      });
    </script>
  </body>
</html>

Requires a local HTTP server (e.g. npx serve .) so that node_modules is accessible. Opening index.html as a file:// URL will not work.

Unmounting

import { unmountExotelAIAssist } from "@exotel-npm-dev/exotel-ai-assist";

unmountExotelAIAssist(container);

React App — /react subpath

Import from the subpath to avoid bundling React twice:

import { ExotelAIAssist } from "@exotel-npm-dev/exotel-ai-assist/react";

function AgentDashboard() {
  return (
    <div style={{ height: 500 }}>
      <ExotelAIAssist authToken="your-auth-token" call_sid="CALL-SID-001" accountId="your-account-id" />
    </div>
  );
}

Using the hook directly

import { useExotelAIAssist } from "@exotel-npm-dev/exotel-ai-assist/react";

function MyCustomUI({ call_sid }: { call_sid: string }) {
  const { isReady, status, suggestions, transcripts, lastError } = useExotelAIAssist({
    authToken: "your-auth-token",
    call_sid,
    accountId: "your-account-id",
  });

  if (!isReady) return <p>Connecting…</p>;

  return (
    <div>
      <p>Status: {status}</p>
      {suggestions.map((s) => (
        <p key={s.id}>{s.text}</p>
      ))}
    </div>
  );
}

Context Provider (share state across components)

import { ExotelAIAssistProvider, useExotelAIAssistContext } from "@exotel-npm-dev/exotel-ai-assist/react";

function App() {
  return (
    <ExotelAIAssistProvider authToken="your-auth-token" call_sid="CALL-SID-001" accountId="your-account-id">
      <SuggestionsPanel />
      <TranscriptPanel />
    </ExotelAIAssistProvider>
  );
}

function SuggestionsPanel() {
  const { suggestions } = useExotelAIAssistContext();
  return (
    <ul>
      {suggestions.map((s) => (
        <li key={s.id}>{s.text}</li>
      ))}
    </ul>
  );
}

Headless Controller — /controller subpath

For Vue, Angular, or any other framework where you want raw data with no React UI:

import { ExotelAIAssistController } from "@exotel-npm-dev/exotel-ai-assist/controller";

const ctrl = new ExotelAIAssistController({
  authToken: "your-auth-token",
  call_sid: "CALL-SID-001",
  accountId: "your-account-id",
});

ctrl.on("onReady", (ready) => {
  if (ready) {
    console.log("Connection established and acknowledged by server");
    // Safe to start consuming data / show the UI
  } else {
    console.log("Connection lost");
  }
});

ctrl.on("suggestion", (s) => console.log("Suggestion:", s));
ctrl.on("transcript", (t) => console.log("Transcript:", t));
ctrl.on("statusChange", (status) => console.log("Status:", status));
ctrl.on("error", (err) => console.error("Error:", err));

ctrl.connect();

// Switch to a new call
ctrl.setParams({ call_sid: "CALL-SID-002" });

// Clean up
ctrl.destroy();

API Reference

mountExotelAIAssist(container, params)

Mounts the widget into a DOM element.

| Parameter | Type | Required | Description | | ----------- | ---------------------- | -------- | --------------------- | | container | HTMLElement | ✓ | Target DOM element | | params | ExotelAIAssistParams | ✓ | Connection parameters |

unmountExotelAIAssist(container)

Unmounts and cleans up the widget.


ExotelAIAssistController

Extends EventEmitter.

Constructor options (ExotelAIAssistParams)

| Field | Type | Required | Default | Description | | ---------------------- | -------- | -------- | ------------------------ | -------------------------------------------------------- | | authToken | string | ✓ | — | Bearer token | | call_sid | string | ✓ | — | Active call SID | | accountId | string | ✓ | — | Exotel account identifier | | wssBaseUrl | string | — | Exotel AI Assist backend | Override only when pointing at a non-production endpoint | | reconnectInterval | number | — | 3000 | Base reconnect delay in ms | | maxReconnectAttempts | number | — | 5 | Max retries before error | | [customParam] | string | — | — | Max 3 params you can send |

Methods

| Method | Description | | ------------------ | ---------------------------------------------- | | connect() | Open the WebSocket | | disconnect() | Close cleanly | | setParams(patch) | Merge params; reconnects if call_sid changes | | destroy() | Dispose controller and remove all listeners | | getStatus() | Returns current ConnectionStatus |

Events

| Event | Payload | Description | | -------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- | | onReady | boolean | true when connected and server sends ack; false on disconnect. Multi-tab safe — works for follower tabs too | | suggestion | Suggestion | New AI suggestion (capped at last 50) | | transcript | TranscriptLine[] | Live transcript update | | sentiment | Sentiment | Sentiment label update | | onCallStart | — | Connection opened | | onCallEnd | — | Connection closed | | statusChange | ConnectionStatus | Status transition | | error | Error | Any error (auth, parse, max-reconnect) | | raw | unknown | Every raw server message |


useExotelAIAssist(params) — Hook return values

| Field | Type | Description | | ------------- | ------------------ | ---------------------------------------------------------------------- | | isReady | boolean | true after connected + server ack. Multi-tab safe. false on disconnect | | status | ConnectionStatus | Current connection status | | suggestions | Suggestion[] | AI suggestions, oldest first, capped at 50 | | transcripts | TranscriptLine[] | Live transcript lines, ordered by start time | | sentiment | Sentiment \| null| Latest sentiment reading | | lastError | Error \| null | Most recent error | | connect() | () => void | Manually open the connection | | disconnect() | () => void | Manually close the connection | | setParams() | (patch) => void | Merge params; reconnects if connection params change |


TypeScript Types

type ConnectionStatus = "idle" | "connecting" | "connected" | "disconnected" | "error";

interface Suggestion {
  id: string;
  text: string;
  timestamp: number;
}

interface TranscriptLine {
  id: string;
  text: string;
  startTime: number;
  endTime: number;
  isFinal: boolean;
}

interface Sentiment {
  label: "positive" | "neutral" | "negative";
  timestamp: number;
}

WebSocket Protocol

URL

When wssBaseUrl is provided it overrides the host + path portion:

wss://<wssBaseUrl>?[customParam1=value1&customParam2=value2&...]

Reconnection

  • Exponential back-off: delay = Math.min(baseInterval × 2^attempt, 30 000)
  • After maxReconnectAttempts: error emitted with code = MAX_RECONNECT_EXCEEDED

License

Apache 2.0