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

@sahan-sac/chat-kit

v0.3.1

Published

Headless chatbot core for the portfolio site: guarded prompt building, prompt-injection and output-link guards, an anonymous-visitor rate-limit cookie, a knowledge-base builder that tolerates a failing source, and runChat, the pure request/response path b

Readme

@sahan-sac/chat-kit

Headless portfolio chatbot core, built on @sahan-sac/ai-core. It provides:

  • guarded prompt building;
  • prompt-injection and output-link guards;
  • a signed anonymous-visitor cookie for rate limiting;
  • a knowledge-base builder;
  • runChat, one chat turn from visitor message to filtered reply.

It is headless and framework-agnostic: no Prisma, no Next.js APIs, no React. The host app keeps:

  • the HTTP route, origin checks and rate limits;
  • cookie I/O and settings;
  • storage (through ChatStore) and caching;
  • the widget UI.

Subpaths

| Import | What | | --- | --- | | @sahan-sac/chat-kit/types | ChatbotConfig, ChatSite (the owner identity; optional kind, scope and linkExample set the wording, default "site"), ChatTurn, CHAT_TONES | | @sahan-sac/chat-kit/handler | runChat(input, deps), CHAT_BUDGETS (12 s per attempt, 20 s chain, 5 s hedge, 500 tokens) | | @sahan-sac/chat-kit/knowledge | buildKnowledge(sources), profileSection(site), trainingSection(entries), knowledgeHosts(text) | | @sahan-sac/chat-kit/prompts | buildChatPrompt (fixed system prompt, knowledge as fenced reference data) | | @sahan-sac/chat-kit/guard | guardUserMessage, filterModelOutput (strips HTML, secrets, /admin paths and links to hosts not allowed) | | @sahan-sac/chat-kit/visitor-cookie | newChatVisitorId, signChatVisitorId, verifyChatVisitorCookie, chatVisitorCookieOptions, CHAT_VISITOR_COOKIE (default name "chat_vid"; any name works) | | @sahan-sac/chat-kit/adapter | ChatStore, ChatSessionInput, ChatMessageInput | | @sahan-sac/chat-kit/session-summaries | ChatSessionSummary, parseChatSessionListParams, page-size limits for the admin conversations list |

Only handler imports server-only. The other subpaths are pure, and session-summaries and types are safe in client components.

Environment

Uses @sahan-sac/ai-core's variables; see its README for the full table. The chatbot runs only when chatbotEnabled(env) from @sahan-sac/ai-core/availability is true:

  • ENABLE_CHATBOT is on. It defaults to true; false, 0, no or off turns it off.
  • At least one provider can answer chat: its key is set, it is in AI_PROVIDER_ORDER_CHAT (or AI_PROVIDER_ORDER) when one is set, and a paid one also has AI_ALLOW_PAID=true.

The chat chain falls through providers on any failure (quota, timeout, bad key, bad output), so one provider's outage does not stop the chatbot. Set AI_PROVIDER_ORDER_CHAT to choose which providers answer and in what order; see @sahan-sac/ai-core's README.

When the chatbot is off, return 503 from the route and do not render the widget.

Wiring

import { realProviders, sharedAiHealth } from "@sahan-sac/ai-core/providers";
import { runChat } from "@sahan-sac/chat-kit/handler";

const result = await runChat(
  {
    message,
    history: await store.getRecentMessages(sessionId, 10),
    knowledge: await getKnowledge(), // cached by the app
    config: { tone: "professional" },
    site: { ...chatSite, guidance: ownerGuidance }, // optional: added after the fixed rules
    siteHostname: new URL(env.SITE_URL).hostname,
    extraHosts: ["wa.me", "t.me"],
  },
  { providers: realProviders(env, "chat"), health: sharedAiHealth, logger }
);
if (!result.ok) return new Response(null, { status: 503 });
// result.text is already filtered; store it with result.tokens and result.latencyMs.

Knowledge sources run in parallel and join in order. A source that throws is skipped and reported, so one failing source never empties the knowledge base:

import { buildKnowledge, profileSection, trainingSection } from "@sahan-sac/chat-kit/knowledge";

const knowledge = await buildKnowledge(
  [
    { name: "profile", load: () => profileSection(chatSite) },
    { name: "posts", load: async () => (await getPosts()).map((post) => `- ${post.title} (/blog/${post.slug})\n`).join("") },
    { name: "training", load: async () => trainingSection(await db.chatTrainingEntry.findMany({ where: { isActive: true }, take: 50 })) },
  ],
  { onError: (source, error) => logger.warn("knowledge source failed", { source, error: String(error) }) }
);

A Prisma ChatStore:

import type { ChatStore } from "@sahan-sac/chat-kit/adapter";

export const prismaChatStore: ChatStore = {
  upsertSession: (input) => db.chatSession.upsert({ where: { sessionId: input.sessionId }, update: {}, create: input }),
  addMessage: (input) =>
    Promise.all([
      db.chatSession.update({ where: { sessionId: input.sessionId }, data: { messagesCount: { increment: 1 } } }),
      db.chatMessage.create({ data: input }),
    ]),
  getRecentMessages: async (sessionId, limit) =>
    (await db.chatMessage.findMany({ where: { sessionId }, orderBy: { createdAt: "desc" }, take: limit })).reverse(),
  linkInquiry: (sessionId, inquiryId) => db.chatSession.update({ where: { sessionId }, data: { inquiryId, capturedLead: true } }),
  listSessionSummaries: async ({ limit, offset }) => /* select without message bodies */ [],
};

The visitor cookie is a rate-limit fallback for callers whose IP is unknown. It holds a random id plus an HMAC. Pass the secret in (for example INTERNAL_SIGNING_SECRET): nothing in this package reads process.env.