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

@sindarin/persona

v0.4.1

Published

Sindarin Persona SDK

Readme

Sindarin Persona SDK

Version

Sindarin Persona enables you to add lifelike conversational AI agents to your applications. In browser environments, it features ultra low latency voice I/O with industry-leading turn-taking and transcription, and high-fidelity speech synthesis. In Node.js environments, it provides text-based conversational capabilities with the same powerful AI backend.

Demo: https://sindarin.tech

Installation

Install the package with:

npm install @sindarin/persona
# or
yarn add @sindarin/persona

or

var script = document.createElement("script");
script.src = "https://api.prod.dal.lat.sindarin.tech/PersonaClientPublicV2?apikey=<YOUR_CLIENT_KEY>";
script.onload = () => {
  var personaClient = new window.PersonaClient.default("<YOUR_CLIENT_KEY>");
  // ... initialize, etc
};
document.head.appendChild(script);

Usage

The package needs to be configured with your account's secret key, which is available in the Persona Web App / Settings.

import PersonaClient from "@sindarin/persona";

const personaClient = new PersonaClient("<YOUR_CLIENT_KEY>");

const handleStartChatButtonClick = async () => {
  // Example config using a persona defined in the Playground; there are many ways to do things!
  const config = {
    userId: "admin",
    personaName: "John",
    options: {
      debugMode: true,
      streamTranscripts: true,
      shouldNotSaveConversation: true,
    },
  };

  try {
    await personaClient.init(config);
    configurePersonaEvents();
  } catch (error) {
    console.log(error);
  }
};

const handlePauseChatButtonClick = async () => {
  try {
    await personaClient.pause();
  } catch (error) {
    console.log(error);
  }
};

const handleResumeChatButtonClick = async () => {
  try {
    await personaClient.resume();
  } catch (error) {
    console.log(error);
  }
};

const handleStopChatButtonClick = async () => {
  try {
    await personaClient.end();
  } catch (error) {
    console.log(error);
  }
};

const handleMuteButtonClick = async () => {
  try {
    // Mute incoming audio (stops and clears any buffered audio)
    await personaClient.mute();
  } catch (error) {
    console.log(error);
  }
};

const handleUnmuteButtonClick = async () => {
  try {
    // Unmute to resume listening to incoming audio
    await personaClient.unmute();
  } catch (error) {
    console.log(error);
  }
};

const handleUpdateState = async (newState) => {
  try {
    // Simple state update
    personaClient.updateState(newState);
    
    // Or with additional context (backwards compatible with old 'thought' string parameter)
    personaClient.updateState(newState, true, {
      text: 'The user clicked a button',
      shouldInterrupt: false,
      canBeInterrupted: false,
      shouldEnqueue: true
    });
  } catch (error) {
    console.log(error);
  }
};

const handleReactTo = async (thought) => {
  try {
    // Simple reaction
    personaClient.reactTo(thought);
    
    // Or with options
    personaClient.reactTo(thought, {
      shouldReact: true,
      shouldInterrupt: false,
      canBeInterrupted: true,
      shouldEnqueue: true
    });
  } catch (error) {
    console.log(error);
  }
};

const handleTextMessage = async (text) => {
  try {
    // textMessage always interrupts current AI speech
    // and the AI's response can be interrupted by default
    personaClient.textMessage(text, { canBeInterrupted: true });
  } catch (error) {
    console.log(error);
  }
};

const configurePersonaEvents = () => {
  personaClient.on("messages_update", (messages) => {
    console.log(messages);
  });

  personaClient.on("state_updated", (newState) => {
    console.log(newState);
  })

  personaClient.on("action", (action) => {
    console.log(action);
  })
};

Configuration

Initialize with config object

You can initialize a conversation with the following config fields:

const config = {
  userId: "admin", // optional
  personaName: "John", // optional
  personaId: "example_id", // optional; this is found in the Persona Playground
  details: { firstName: "John" }, // optional; arbitrary data you can inject into your prompt with {{details.x}}
  metadata: { orderId: "1" }, // optional; arbitrary data used for conversation retrieval / filtering later
  personaConfig: {
    initialMessage: "Hello {{details.firstName}}!",
    prompt: "You are a fun and helpful persona.",
    actions: { properties: {} },
    ttsConfig: {
      voiceId: "example_id", // voice IDs can be found and tested in the Playground
    },
    llmConfig: {
      llm: "example_llm_name", // e.g. llama-3-70b, llama-3-8b, gpt-4o, hermes-2-mixtral-8x7B-dpo
      temperature: 0.8, // 0 to 1
      repetitionPenalty: 1 // -3.5 to 3.5 for non-openai models; -2 to 2 for openai models
    },
    interjections: { // the ability for the persona to speak spontaneously
      enabled: true,
      minWait: 7500,
      maxWait: 10000,
      thought: "Nobody has said anything in a while. I should nudge the conversation along."
    },
    chargeLimitEnabled: true, // optional; should the conversation be cut off after a certain amount of charges incurred?
    chargeLimit: 2, // optional; the charge limit
    lifecycleEvents: { // optional; if the webhook endpoint returns { state } or { details } in the response body, they will be injected
      onInit: {
        webhook: {
          url: "http://example.com",
          method: "GET",
          body: {},
          headers: {}
        }
      },
      onStateChange: { /* ... */ },
      onEnd: { /* ... */ }
    }
  },
  options: {
    debugMode: true, // emit debug events in messages
    streamTranscripts: true, // stream transcripts in realtime
    shouldNotSaveConversation: true, // do not save transcripts
  },
};

try {
  await personaClient.init(config);
} catch (error) {
  console.log(error);
}

| Option | Default | Description | | ----------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ | | userId | null | [Optional] Your unique ID for the current user. | | personaName | null | [Optional; this or personaId or personaConfig required] The persona name in the Playground specifying a particular persona config. | | personaId | null | [Optional; this or personaName or personaConfig required] The persona ID in the Playground specifying a particular persona config. This takes priority over personaName if both are provided. | | details | null | [Optional] An object containing arbitrary information that you can inject into your prompt before the conversation starts using {{details.x}}. | | metadata | null | [Optional] An object containing arbitrary information for the developer to retrieve the conversation by. | | personaConfig | null | [Optional; this or personaName or personaId required] An ad hoc persona configuration specified entirely in your code. | | options | - | [Optional] Additional options (below). | | options.debugMode | false | Messages array will contain additional debug events (info, warn, error) for debugging purposes during a conversation. | | options.streamTranscripts | false | Temporary transcripts will be rendered in the messages array. | options.shouldNotSaveConversation | false | If true, the transcript will not be saved after the conversation ends. |

Node.js Compatibility

This SDK now supports both browser and Node.js environments. The same npm package can be used in both contexts, with automatic feature detection and adaptation.

Environment Detection

The SDK automatically detects the runtime environment and adjusts its behavior accordingly:

import PersonaClient, { isNode, isBrowser, hasAudioSupport } from "@sindarin/persona";

// Check the environment
console.log("Running in Node.js:", isNode);
console.log("Running in Browser:", isBrowser);
console.log("Audio support available:", hasAudioSupport);

Feature Availability by Environment

| Feature | Browser | Node.js | |---------|---------|---------| | Voice Input (Microphone) | ✅ | ❌ (console warning) | | Voice Output (Speaker) | ✅ | ❌ (console warning) | | Audio Controls (mute, unmute) | ✅ | ⚠️ (no-op with warning) | | Text Messaging (sayText, reactTo, textMessage) | ✅ | ✅ | | State Management (updateState) | ✅ | ✅ | | WebSocket Events | ✅ | ✅ | | Message History | ✅ | ✅ | | LLM Completions (getChatCompletion, prompt) | ✅ | ✅ | | All PersonaService Methods | ✅ | ✅ |

Node.js Usage Example

import PersonaClient from "@sindarin/persona";

const personaClient = new PersonaClient("<YOUR_CLIENT_KEY>");

async function startTextChat() {
  const config = {
    userId: "server-user",
    personaName: "Assistant",
    options: {
      debugMode: true,
      streamTranscripts: true,
    },
  };

  try {
    await personaClient.init(config);
    // Voice I/O will be automatically disabled with a warning
    
    // Use text-based interactions
    personaClient.sayText("Hello from Node.js!");
    personaClient.textMessage("This is a user message", { canBeInterrupted: true });
    personaClient.reactTo("User clicked something");
    
    // Handle events
    personaClient.on("messages_update", (messages) => {
      console.log("Messages:", messages);
    });
    
    // Use LLM completions
    const response = await personaClient.prompt("What is the weather today?");
    console.log("AI Response:", response);
    
  } catch (error) {
    console.error("Error:", error);
  }
}

startTextChat();

Audio Controls

The SDK provides methods to control audio playback:

  1. mute() - Mutes incoming AI audio and immediately stops/clears any buffered audio

    await personaClient.mute();
  2. unmute() - Unmutes to resume listening to incoming AI audio

    await personaClient.unmute();

Note: These methods only affect audio playback in browser environments where audio is enabled. In text-only or Node.js environments, they will log a warning but have no effect.

Text Messaging Methods

The SDK provides three methods for sending text to the AI:

  1. sayText(text: string) - Makes the AI speak the provided text

    personaClient.sayText("Hello, how are you?");
  2. reactTo(text: string, options?) - Sends an event/thought for the AI to react to

    personaClient.reactTo("User clicked the help button", {
      shouldReact: true,        // Should AI generate a response (default: true)
      shouldInterrupt: false,   // Should interrupt current AI speech (default: false)
      canBeInterrupted: true,   // Can this response be interrupted (default: true)
      shouldEnqueue: false      // Should enqueue this message (default: false)
    });
  3. textMessage(text: string, options?) - Sends a user message that always interrupts

    personaClient.textMessage("What's the weather like?", {
      canBeInterrupted: true    // Can the AI's response be interrupted (default: true)
    });

    Note: textMessage always interrupts any ongoing AI speech, unlike reactTo which has configurable interruption.

State Management

updateState(state: unknown, shouldReact?: boolean, thoughtOrOptions?: string | options) - Updates the conversation state

The third parameter supports both old (string) and new (object) formats for backwards compatibility:

// Simple state update
personaClient.updateState({ score: 100 });

// With shouldReact flag
personaClient.updateState({ score: 100 }, true);

// Old API (backwards compatible) - string as third parameter
personaClient.updateState({ score: 100 }, true, "User scored a point");

// New API - object with additional options
personaClient.updateState({ score: 100 }, true, {
  text: 'User scored a point',
  shouldInterrupt: false,     // Should interrupt current AI speech (default: false)
  canBeInterrupted: true,     // Can this response be interrupted (default: true)
  shouldEnqueue: false        // Should enqueue the reaction to be spoken *after* any current AI speech finishes(default: false)
});

Backwards Compatibility

This update maintains full backwards compatibility with existing browser implementations. All existing code will continue to work without any changes. The Node.js support is additive and does not affect browser functionality.

Error Handling Options

try {
  await personaClient.init(config);
} catch (error) {
  console.log(error);
}

personaClient.on("error", (error) => {
  console.log(error);
});

personaClient.on("connect_error", (error) => {
  console.log(error);
});