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

@silo-software/chat

v0.4.2

Published

Silo reader chat component for external CMS sites

Downloads

1,798

Readme

@silo-software/chat

The Silo reader chat for external publication sites — the same thread, activity timeline, inline citations, sources list and composer readers see on Silo's hosted CMS site, packaged as a self-contained React component.

Silo runs the assistant turn and stores the conversation. The site only forwards messages and renders the stream it receives, so the publication app needs no AI runtime and no model keys. The reader token stays in an HttpOnly cookie of the site; the browser never calls Silo directly.

  • React 19 peer dependency, ESM, no @repo/*, no assistant-ui.
  • Tailwind utilities are compiled into @silo-software/chat/styles.css at build time and prefixed with silo: (silo:flex, silo:hover:bg-muted), so they cannot collide with the host's own classes.
  • No preflight and no global resets: the host page keeps its own base styles.

Install

pnpm add @silo-software/chat @silo-software/sdk

@silo-software/sdk is already a dependency of the package; it is listed here because the site's route handlers use it.

Styles

Import the stylesheet once, from the layout or the page that renders the chat:

import '@silo-software/chat/styles.css';

Colors, borders and radii come from the shadcn CSS variables the host already defines (--background, --foreground, --card, --popover, --muted, --accent, --primary, --border, --input, --ring, --radius), so the host theme and its .dark class apply. Light fallbacks live in a zero-specificity :where(:root) block and are only used when the host defines nothing.

The chat fills its container, so give the wrapper a height:

<main className="mx-auto flex h-[calc(100dvh-4rem)] w-full max-w-3xl flex-col">
  <SiloChat chatId={chatId} initialMessages={messages} />
</main>

Route handlers

SiloChat talks to the site's own routes. The default transport (createFetchChatTransport({ basePath: '/api/chat' })) calls:

| Call | Request | Response | | --- | --- | --- | | Start a chat | POST /api/chat { articleSlug? } | { chatId } | | Send a message | POST /api/chat/{chatId}/messages { message, articleSlug? } | text/event-stream | | Stop the turn | POST /api/chat/{chatId}/stop | any 2xx |

Every SSE data: line is one record of cmsChatStreamEventSchema (@silo-software/sdk/types): { type: 'message', message } carries the complete current state of the turn's answer and replaces the previous one, { type: 'done' } ends the turn, { type: 'error', message } reports a reader-safe failure.

A Next.js route handler set that forwards to Silo with the reader token:

// app/api/chat/route.ts
import { createCmsClient } from '@silo-software/sdk';
import { readReaderAccessToken } from '@/lib/reader-session';

export async function POST(request: Request) {
  const body = (await request.json().catch(() => ({}))) as {
    articleSlug?: string;
  };
  const client = createCmsClient({
    baseUrl: process.env.SILO_API_URL!,
    clientId: process.env.SILO_CLIENT_ID!,
    serverSecret: process.env.SILO_SERVER_SECRET,
  });
  const session = await client.startChatSession(
    { articleSlug: body.articleSlug },
    { accessToken: await readReaderAccessToken() },
  );
  return Response.json(session);
}
// app/api/chat/[chatId]/messages/route.ts
import { createCmsClient } from '@silo-software/sdk';
import { readReaderAccessToken } from '@/lib/reader-session';

export async function POST(
  request: Request,
  { params }: { params: Promise<{ chatId: string }> },
) {
  const { chatId } = await params;
  const body = (await request.json()) as {
    articleSlug?: string;
    message: string;
  };
  const client = createCmsClient({
    baseUrl: process.env.SILO_API_URL!,
    clientId: process.env.SILO_CLIENT_ID!,
    serverSecret: process.env.SILO_SERVER_SECRET,
  });
  const upstream = await client.sendChatMessage(
    {
      accessToken: await readReaderAccessToken(),
      chatId,
      message: body.message,
      articleSlug: body.articleSlug,
    },
    { signal: request.signal },
  );
  return new Response(upstream.body, {
    headers: {
      'cache-control': 'no-store',
      'content-type': 'text/event-stream',
    },
  });
}
// app/api/chat/[chatId]/stop/route.ts
import { createCmsClient } from '@silo-software/sdk';
import { readReaderAccessToken } from '@/lib/reader-session';

export async function POST(
  _request: Request,
  { params }: { params: Promise<{ chatId: string }> },
) {
  const { chatId } = await params;
  const client = createCmsClient({
    baseUrl: process.env.SILO_API_URL!,
    clientId: process.env.SILO_CLIENT_ID!,
    serverSecret: process.env.SILO_SERVER_SECRET,
  });
  await client.stopChatTurn({
    accessToken: await readReaderAccessToken(),
    chatId,
  });
  return Response.json({ stopped: true });
}

Answer a missing or expired reader session with 401; SiloChat turns that into a "please sign in again" notice instead of an error loop.

Component

'use client';

import { SiloChat } from '@silo-software/chat';
import '@silo-software/chat/styles.css';

export function ArticleChat({
  chatId,
  messages,
  articleSlug,
}: {
  chatId: string | null;
  messages: CmsChatMessageDto[];
  articleSlug?: string;
}) {
  return (
    <SiloChat
      articleSlug={articleSlug}
      chatId={chatId}
      getArticleHref={(slug) => `/artikel/${slug}`}
      initialMessages={messages}
      onChatStarted={(startedChatId) => {
        window.history.replaceState(null, '', `/chat/${startedChatId}`);
      }}
      title="Chat"
    />
  );
}

| Prop | Type | Meaning | | --- | --- | --- | | chatId | string \| null | Silo chat session, null before the first message | | initialMessages | CmsChatMessageDto[] | Messages the site's server already loaded | | title | string \| null | Title shown above the thread | | articleSlug | string | Article the reader opened the chat from | | transport | SiloChatTransport | Defaults to the fetch transport on /api/chat | | labels | Partial<SiloChatLabels> | Overrides parts of the German default copy | | getArticleHref | (slug) => string | Link for article sources; defaults to the cited URL | | onChatStarted | (chatId) => void | Called once Silo created the chat, so the URL can follow | | className | string | Extra classes on the outer element |

German copy (defaultSiloChatLabels) is exported from @silo-software/chat for reuse. Any subset can be overridden; templates use {index}, {title}, {count}, {query} and {seconds} placeholders.

Hook and transport

Sites that want their own layout can drive the state machine directly:

const { messages, status, error, send, stop, retry } = useSiloChat({
  chatId,
  initialMessages,
  articleSlug,
  transport: createFetchChatTransport({ basePath: '/api/chat' }),
  onChatStarted: (id) => router.replace(`/chat/${id}`),
});

status is idle | sending | streaming | error. A user message is added optimistically; every message event replaces the assistant answer of that turn; stop aborts the request and asks Silo to stop the turn. A custom transport only has to implement startChat, sendMessage and stop (SiloChatTransport).

What the component renders

  • The reader's question in the muted bubble.
  • The assistant's work steps behind a collapsed disclosure: web search rows with queries, result counts and result cards, plus one row per other step with its detail. While the turn runs the panel stays open and shows the live duration; afterwards it shows the finished work duration.
  • The markdown answer (GFM tables, lists, code, images) with the numbered inline citations [n](url) as hover cards.
  • The collapsible sources list with the site's own article links.
  • Copy, timestamps, the typing dots, the error state and the stopped-run state.
  • The composer with send and stop only.

Not included: model or performance-tier selection, attachments, voice mode and dictation, generative UI, subagents, approvals, feedback, thread lists and project chats. These are admin or hosted-site features, and the reader chat API has no route for them.