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

@charivo/core

v0.23.0

Published

Core functionality for Charivo character system

Downloads

1,911

Readme

@charivo/core

@charivo/core is the contract layer for the Charivo workspace.

It exports:

  • Charivo: the top-level orchestrator
  • shared domain types such as Character, Message, and realtime session types
  • interface contracts for LLM, render, TTS, STT, and realtime managers
  • the event contracts CharivoEventBus and CharivoEventEmitter: the render manager receives a bus via setEventBus(...), while the TTS/STT/LLM/Realtime managers receive an emitter via setEventEmitter(...); the concrete bus implementation is internal, and consumers subscribe via charivo.on/off(...)
  • createLipSyncAnalyzer: the shared audio-analysis utility the TTS manager and realtime clients use to compute mouth-open RMS and emit tts:lipsync:update
  • modality-neutral tool contracts (ToolDefinition, ToolRegistration, ...) plus the validation and execution helpers (validateToolArguments, assertToolResultObject, createToolRegistry, withToolTimeout, snapshotToolResult, createToolFailureOutput) shared by @charivo/llm and @charivo/realtime

The public API is factory-first: pluggable managers/clients/players/ transcribers/renderers are created via create* factories and consumed through their interfaces; concrete implementation classes are not exported. Charivo, the CharivoError taxonomy, and the OpenAI/OpenClaw provider classes (OpenAILLMProvider, OpenClawLLMProvider, OpenAITTSProvider, OpenAISTTProvider, OpenAIRealtimeProvider) are the three exceptions, exported directly as concrete classes: Charivo owns the instance lifecycle (wiring managers, the event bus, and dispose()) so it isn't behind a factory; CharivoError is a taxonomy checked via isCharivoError/error.code, not constructed; the OpenAI/OpenClaw providers are exported because consumers rely on instanceof checks and provider methods outside the narrow core interface — a contract packages/server/__tests__/barrel.test.ts pins; their factories are also browser-callable via dangerouslyAllowBrowser for dev/testing, not a "Node-only" restriction. Subclassing Charivo is not supported — extend via composition.

Install

pnpm add @charivo/core

Usage

import { Charivo } from "@charivo/core";

const charivo = new Charivo();

charivo.setCharacter({
  id: "hiyori",
  name: "Hiyori",
  personality: "Cheerful and helpful assistant",
});

charivo.on("message:received", ({ message }) => {
  console.log(message.content);
});

Charivo

The Charivo instance wires managers together:

  • attachRenderer(renderManager)
  • attachLLM(llmManager)
  • attachTTS(ttsManager)
  • attachSTT(sttManager)
  • attachRealtime(realtimeManager)
  • detachLLM()
  • detachRenderer()
  • setCharacter(character)
  • userSay(text)
  • dispose()
  • clearHistory()
  • getHistory()
  • on(event, listener)
  • off(event, listener)

detachRenderer() disconnects the render manager's event-bus listeners and releases any held expression, without destroying the manager, so it remains reusable. Calling attachRenderer(newManager) automatically disconnects the previously-attached manager before wiring the new one.

userSay(text) stops any TTS still playing from a previous turn before generating a response, so — for sequential turns — the new turn's own audio/expression events can't be undone by the previous turn's speech ending; overlapping/concurrent userSay(text) calls are not serialized and can still interleave. A failure during that stop doesn't abort the turn — it's surfaced via tts:error, like other non-fatal TTS failures during userSay(text).

The current render-manager contract is explicit: a RenderManager must expose setEventBus(eventBus) and disconnect() so the core can connect and cleanly tear down typed character, TTS, and realtime events without duck typing.

RenderManager public methods

Beyond the event-bus contract, RenderManager exposes optional public methods for driving avatar state from the app layer:

  • setLocalGaze(coords: GazeCoordinates): boolean — drives local-presence gaze (e.g. webcam face tracking). Returns false (no-op) while AI gaze owns the avatar (the avatar:gaze suspend window is active) or when the renderer has no lookAt.

Errors

Public methods throw typed errors exported from @charivo/core:

  • CharivoStateError
  • CharivoTimeoutError
  • CharivoTransportError
  • CharivoProviderError
  • CharivoDisposeError

Prefer isCharivoError(error) or error.code checks over error.message.includes(...). Raw instanceof CharivoError is only guaranteed to work within a single installed copy of @charivo/core; isCharivoError also recognizes branded errors thrown by a duplicated copy. Symbol brands never survive serialization. error.code survives JSON.stringify (it is a plain enumerable property), but structuredClone and worker postMessage drop custom Error properties entirely — send an explicit { code, message } envelope across those boundaries instead of relying on the error object itself.

Tool Contracts

@charivo/core owns a modality-neutral tool contract used by both @charivo/llm (LLMManager) and @charivo/realtime (RealtimeManager), so a tool built once (e.g. by @charivo/avatar) registers with either manager:

  • ToolDefinition: { type: "function", name, description, parameters }, a JSON Schema-shaped function definition
  • ToolContext: { character?, callId?, state? } passed to a handler; state (a RealtimeState) is present only for realtime sessions
  • ToolHandler: (args, context) => Promise<Record<string, unknown>> — must resolve to a plain object; arrays and primitives are rejected by the runners
  • ToolRegistration: { definition, handler, timeoutMs? }
  • ToolResultProjector / ToolResultProjectorContext: ({ name, output, callId?, emit }) => void, run by a manager after a successful tool call so app code can turn tool output into Charivo events (e.g. avatar:expression)

Validation helpers, used by both managers before/after a handler runs:

  • validateToolArguments(definition, args, toolLabel?): throws a plain Error on the first schema violation. Enforces only required-key presence, enum membership, and each property's top-level type — nested schemas, additionalProperties, and numeric-length constraints are not validated.
  • assertToolResultObject(result, toolName, toolLabel?): throws a plain Error unless result is a plain object.

Execution helpers, shared so both managers run tools with the same guarantees:

  • createToolRegistry(): ToolRegistry: name-keyed registry (register / unregister / get / size / getDefinitions). getDefinitions() deep-copies, so a provider cannot mutate a registered schema.
  • withToolTimeout(promise, timeoutMs, toolName, toolLabel?): rejects with ${toolLabel} "${toolName}" timed out after ${timeoutMs}ms and always clears its timer.
  • snapshotToolResult(result, toolName, toolLabel?): ToolResultSnapshot: serializes once and returns { serialized, snapshot } — the string for the model's tool turn or the transport, and its parsed form for the tool:result event and the result projectors. Call it inside a runner's failure boundary so an unrepresentable result degrades to a failure output: JSON.stringify returns the value undefined (without throwing) for a result whose toJSON() yields undefined, and a toJSON() can also yield null, an array, or a primitive, so the parsed value is re-checked against the tool-result contract. Both tool runners use this, which is what makes a tool result mean the same thing across modalities.
  • createToolFailureOutput(error): the always-serializable { success: false, error } output handed back to the model when a call fails.

LLM Tool-Calling Contracts

  • LLMMessage: role-discriminated union (system/user, assistant with optional toolCalls, or tool with a required toolCallId) so protocol-invalid combinations are unrepresentable for typed callers
  • LLMToolCall: { id, name, arguments }
  • LLMToolResponse: { content, toolCalls? }
  • LLMProvider.generateResponseWithTools?(messages, tools): optional tool-calling variant a provider implements alongside generateResponse
  • LLMClient.callWithTools?(messages, tools): optional tool-calling variant a client implements alongside call
  • LLMManager.registerTool?, unregisterTool?, getRegisteredTools?, setToolInstructions?: optional manager methods for managing the tool registry and the tool-only system-prompt addendum

Events

Important event names include:

  • message:sent
  • message:received
  • character:speak
  • tts:start
  • tts:end
  • tts:error
  • tts:audio:start
  • tts:audio:end
  • tts:lipsync:update
  • stt:start
  • stt:partial
  • stt:stop
  • stt:error
  • llm:error
  • tool:call
  • tool:result
  • tool:error
  • realtime:session:start
  • realtime:session:end
  • realtime:state
  • realtime:user:transcript
  • realtime:assistant:start
  • realtime:assistant:delta
  • realtime:assistant:done
  • realtime:usage
  • avatar:expression
  • avatar:motion
  • avatar:gaze
  • realtime:error

avatar:gaze (a bare GazeCoordinates) and realtime:usage (a flat RealtimeUsageEvent) intentionally use flat payloads rather than wrapper objects.

The event bus isolates each listener: one that throws is reported via console.error and does not stop the listeners queued behind it, so emit never throws into its caller.