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

@chatlychat/core

v0.8.0

Published

Headless Chatly conversation core: identity, conversations, realtime, sending, postbacks, steps, reactions and parsed blocks as typed data. No DOM, no React, zero dependencies.

Readme

@chatlychat/core

The headless Chatly conversation core: identity, conversations, realtime, sending, postbacks, interactive steps, reactions and parsed rich blocks, as typed data.

No DOM. No React. No socket library. Zero dependencies. Nothing in this package names window, document, localStorage, navigator, fetch or process — that is enforced by the compiler (lib: ["ES2022"], types: []) and by a runtime spec that replaces each of those globals with a getter that throws. So it runs in a browser, in Node, in a worker, in a Unity WebGL or Godot HTML5 export, and in a 3D world host that has a scene graph instead of a page.

What it is not: a UI. There is no panel, no bubble, no launcher and no stylesheet here. If you want the drop-in web widget, that is @chatlychat/widget-element / @chatlychat/sdk-js. If you want to draw the conversation yourself, this is the package.

pnpm add @chatlychat/core

Everything the core needs from its host is injected

The core imports nothing, so every capability arrives as an option. All of them are optional except publicId, and the defaults are the ones a browser can satisfy on its own.

SessionOptions — identity and transport:

| option | type | default | | ------------- | ------------------------------------------ | ------------------------------------- | | publicId | string (required) | — | | apiUrl | string | https://api.chatlychat.com | | fetch | FetchLike | globalThis.fetch | | storage | KeyValueStorage or a thunk returning one | in-memory, nothing persisted | | randomId | () => string | crypto.randomUUID, then Math.random | | locale | string | not sent | | timezone | string | not sent | | realtimeUrl | string | whatever init reports |

ChatlyCoreOptions extends it — realtime, parsing and the clock:

| option | type | default | | ---------------- | ----------------------------------------- | ----------------------------------- | | socket | SocketFactory | omitted → the core polls | | pollIntervalMs | number | 6000, floored at 2000 | | readBlocks | (raw: unknown) => BlockEnvelope \| null | the envelope reader in this package | | now | () => number | Date.now |

Three of those deserve more than a table row.

socket is a factory, and omitting it is a supported mode, not a degradation to apologise for. The core never imports a socket library — a headless package that pulled in socket.io-client would cost every host the transport whether or not it uses it, and a world engine may have its own. Pass socket.io-client's io (or anything shaped like SocketFactory) and the conversation goes live over push; pass nothing and it polls. Polling is also the automatic fallback when push cannot be set up at all, because a slow thread is better than a dead one.

import { io } from 'socket.io-client';
import { createChatlyCore } from '@chatlychat/core';

const chatly = createChatlyCore({
  publicId: 'ch_pub_your_channel_id',
  fetch: globalThis.fetch.bind(globalThis),
  // A THUNK, not `localStorage` itself: reading that getter THROWS on an
  // opaque origin or with site data blocked, and a thunk moves the throw
  // inside the core's try/catch.
  storage: () => localStorage,
  socket: io,
});

storage defaults to memory, and that is deliberate. The core persists nothing unless you hand it a store, because writing a visitor token on load — before any consent decision — is a defect on a site that must not store anything. With no store the session lives for the lifetime of the object: a second call does not mint a second visitor, it is simply not remembered across restarts. createSafeStorage is exported if you want the same never-throws-falls-back-to-memory wrapper around something of your own.

fetch is structural, not the DOM fetch type:

type FetchLike = (
  url: string,
  init: { method: string; headers: Record<string, string>; body?: unknown },
) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>;

body is unknown because the core hands your fetch either a JSON string (every API call) or your opaque upload bytes (the presigned PUT) untouched. Typing it as BodyInit would import a DOM type, which is the thing this package does not do.

A conversation

const conv = await chatly.startConversation('I need a hand with my order');

const stop = conv.subscribe(() => {
  const snapshot = conv.getSnapshot();
  // snapshot.messages, .typing, .connection, .loaded, .error, .agentReadUpTo
  // ...hand it to your renderer. Reference equality is a valid "changed?".
  void snapshot;
});

await conv.send('order #1182');
await conv.postback({ body: 'Track it', payload: { intent: 'track' } });
conv.setTyping(true);
await conv.markRead();
stop();

Conversation is a plain external store — subscribe() / getSnapshot() — which is what a React hook, a game loop and an engine binding each want and none of them has to adapt. The snapshot is replaced, never mutated, so reference equality is a valid "did anything change".

The state machine you do not have to write: history is read first (loaded), then the live channel opens; a reconnect re-reads the thread, because events published while the socket was down are not replayed to a visitor; a sent message appears at once as a pending row and is replaced, not duplicated, by the server's copy; a failed send removes the row and throws, so you never keep a bubble the server does not have.

Other verbs on a conversation: react(), submitStep(), requestStep(), reportAction(), upload(), refresh(), dismiss(), close().

On the core itself: init(), identify(), listConversations(), unreadCount(), track(), openConversation(), releaseConversation(), startConversation(), close().

openConversation() is reference-counted and returns the same object for one id, so two components cannot open two sockets for one thread. Match each call with releaseConversation().

Identity

await chatly.identify({
  externalId: 'player-9381',
  name: 'Ada',
  // HMAC-SHA256(<channel IDV secret>, externalId), computed by YOUR server.
  // Never ship the secret in a client, a game build least of all.
  userHash: hashFromYourServer,
});

Two credentials, never confused

POST /v1/widget/init returns a long-lived visitor token that rides every REST body. The realtime gateway does not accept it. The realtime credential is minted per conversation by POST /v1/widget/realtime/token, is conversation-scoped with a 1h TTL, and is re-read per connect attempt — a frozen auth object is replayed on reconnect and the socket dies silently for the rest of the session. The core handles all of that; it is written down here because four hand-written mobile SDKs got it wrong and each one shipped green.

React

import { useConversation } from '@chatlychat/core/react';

react is an optional peer dependency. useConversation, useConversationSnapshot, useMessages and useTyping live behind that entry point so the core's main entry stays React-free.

The 3D-world surface

A world host embeds this package rather than the 2D panel, and these three pure functions are the whole of what it needs that a web page does not. All three are synchronous, dependency-free and safe to call in a render.

import { embodiedAgent, resolveWorldPlacement, resolveWorldTheme } from '@chatlychat/core';

// Who is standing there and what are they doing — derived from the snapshot,
// never sent by the server. Four states: idle, listening, thinking, speaking.
const agent = embodiedAgent(conv.getSnapshot(), {
  presences: { '*': { name: 'Mia', description: 'a guide in a red jacket' } },
  visitorComposing: true,
});
// agent.state, agent.clip (your own animation name), agent.described

// Where the surface goes, resolved against what your host can actually do.
// An ABSENT ability means unknown, not unsupported, and there is no result
// that shows nothing.
const placement = resolveWorldPlacement(
  { anchor: 'agent', follow: 'lazy', distanceMetres: 1.2 },
  { anchors: ['viewer', 'agent'], hud: true },
);
// placement.mode, .anchor, .follow, .billboard, .distanceMetres, .changed[]

// One complete token set. A world has no cascade, so every token comes back
// filled.
const theme = resolveWorldTheme(
  { panelMaterial: 'glass', colorScheme: 'auto', darkTheme: { textColor: '#f8fafc' } },
  { scene: 'dark' },
);

Shared spaces (several visitors and an agent in one thread) are createSpaceClient(); spatial voice is VoiceSession plus the signal codec in voice-signals.

Telling the bot what your world can do, and hearing it ask

await core.setSurface({
  kind: 'world',
  capabilities: [
    {
      name: 'world.walk_to',
      label: 'Take me there',
      unprompted: true, // the bot may fire it without a press
      equivalent: { kind: 'text', text: 'The shop is on level 2, by the lifts.' },
    },
  ],
  state: { room: 'lobby', near: ['red-jacket'] },
});

conv.onCapability(async (inv) => {
  // Your world acts on inv.args, or declines; `ok` is its answer.
  const ok = inv.capability === 'world.walk_to';
  await conv.reportAction({
    messageId: inv.messageId,
    actionId: inv.invocationId,
    status: ok ? 'done' : 'failed',
    ...(ok ? {} : { data: { refused: true } }),
  });
});

The channel's look, a room's look, and the viewer's motion setting

await core.init();
const look = resolveWorldTheme(core.worldTheme ?? {}, {
  scene: 'dark',
  layers: [roomTheme], // a room's own look; Chatly stores no rooms
  reducedMotion: viewer.prefersReducedMotion, // always wins
});

A reply's gesture

const cue = messageEmbodiment(message); // { expression: 'point', lookAt: { target: 'item', ref } }
const clip = expressionClip(presence, cue); // your rig's clip name, or null: play nothing

WorldRenderers<N> types the props a replaceable renderer receives (renderCard, renderPick, renderBubble, renderPanel, renderComposer, renderAgent). It is a contract only: this package draws nothing.

Game engines

A Unity WebGL or Godot HTML5 export is a browser, so this package runs there unchanged — the browser supplies fetch, WebSocket and localStorage as the injected adapters. What it does not give you is the bridge between engine code and JavaScript; that is @chatlychat/game-bridge, which is built on this package and ships the .jslib plugin, the C# externs and the GDScript side.

A WebGL export is not a desktop or console build. For a native title you need a visitor client in the engine's own language, which this package is not.

License

MIT. © Chatly.


About this repository

This is a mirror. @chatlychat/core is built from a private monorepo where the rest of the platform, its migrations and its contract checks live, so a pull request here cannot be merged directly — but a patch is still the fastest way to get something fixed and we will port it with attribution.

What you can do here: read the source, run the tests, open an issue, or open a PR and we will treat the diff as the proposal.

Not mirrored: src/package-shape.spec.ts — specs that assert a type restated here still matches the monorepo's original, which is only a meaningful claim inside it. They keep running there.

Issues: https://github.com/chatlychat/chatly-core/issues · Docs: https://chatlychat.com/docs