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

bunnyquery

v1.6.0

Published

Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine

Readme

BunnyQuery

An embeddable, dependency-free AI chat widget for Skapi-powered projects. Drop it into any web page and your users get a full chat experience — account login/signup, conversation history, file & folder uploads, and a settings panel — all talking to your project's BunnyQuery AI agent.

BunnyQuery is a standalone vanilla-JS port of the BunnyQuery (www.bunnyquery.com) agent chatbox. The widget ships as a single IIFE that exposes window.BunnyQuery plus one stylesheet — drop it in via <script>, no build step or framework required.

The package also exports the framework-agnostic chat engine that powers it (bunnyquery/engine) — the same DOM-free core the Skapi admin chatbox consumes — so you can build your own chat UI on top of it. See Importing the chat engine.

Features

  • AI chat against your project's configured agent (Claude or OpenAI under the hood), with streaming-style "Thinking…" indicators and a background indexing queue.
  • Authentication — email/password login, optional signup, password change, email verification, account recovery, and "Sign in with Google".
  • Conversation history — paginated, with "Fetching history…" indicators on first load and on scroll-up.
  • Attachments — drag-and-drop files and folders, per-file upload status (uploading / failed / indexed), and overflow collapsing for large batches. Images are read with vision/OCR, Office/text/code files are extracted server-side, and PDFs are fetched by the model — see Supported file types.
  • Attachment parser plugins — register a client-side parser so the widget extracts text in the browser from formats the model can't otherwise read, and indexes it directly. See Attachment parser plugins.
  • Settings panel — in-place inside the chat: light/dark theme, account details, newsletter subscription, clear history, and remove account.
  • Theming — light and dark modes via CSS custom properties; the choice is remembered in localStorage and falls back to the OS preference.

Requirements

  • A BunnyQuery project (you need its project ID).
  • The skapi-js SDK loaded on the page.
  • A mount element with an explicit height (the widget fills its container).

Quick start

Add the two BunnyQuery files and the Skapi SDK, give it a sized container, then call BunnyQuery.init():

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <!-- Skapi SDK + BunnyQuery -->
  <script src="https://cdn.jsdelivr.net/npm/skapi-js@latest/dist/skapi.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bunnyquery@latest/bunnyquery.css" />
  <script src="https://cdn.jsdelivr.net/npm/bunnyquery@latest/bunnyquery.js"></script>
</head>
<body style="margin: 0">
  <!-- The widget fills this element, so give it a height -->
  <div id="chatbox" style="width: 100%; height: 100dvh"></div>

  <script>
    // 1. Create your Skapi instance
    const skapi = new Skapi("<your-project-id>", { autoLogin: true });

    // 2. Mount BunnyQuery into the container
    BunnyQuery.init(skapi, "chatbox", {
      theme: "light",
      signup: true,
    });
  </script>
</body>
</html>

That's it — BunnyQuery takes over the #chatbox element and renders the login or chat view depending on the user's session.

What's in the package

| Path | Purpose | | ----------------------------- | -------------------------------------------------------------------------------- | | bunnyquery.js | The widget IIFE. Exposes the global window.BunnyQuery. CDN / <script> drop-in. | | bunnyquery.css | The widget's full stylesheet, scoped under .bq-agent / [data-bq-theme]. | | bunnyquery/engine | The framework-agnostic chat engine — ships as ESM + CJS with TypeScript types. | | bunnyquery/styles/chat.css | The shared chat-surface styles (bubbles, markdown, links) for an engine-built UI. |

The two widget files can be hosted yourself (same origin recommended) or loaded from a CDN — no npm needed. The engine / styles subpaths are for bundler consumers (npm install bunnyquery); see Importing the chat engine.

API

BunnyQuery.init(skapi, target, opts?)

Mounts the widget. Returns the BunnyQuery object.

| Argument | Type | Description | | -------- | --------------------- | ----------------------------------------------------------------- | | skapi | Skapi | A constructed Skapi instance. Required. | | target | string \| Element | The mount element, or the id of one. Required. | | opts | object | Options (see below). Optional. |

Options

| Option | Type | Default | Description | | ------------------------ | --------- | -------- | -------------------------------------------------------------------------------------------- | | theme | string | "light"| Initial theme, "light" or "dark". Overridden by a remembered choice or OS preference. | | signup | boolean | false | Enable signup flows (and account remove/recover). When false, only existing users can log in. | | googleClientId | string | null | Google OAuth client ID. Set this to show "Sign in with Google". | | googleClientSecretName | string | "ggl" | The Skapi client-secret name holding your Google OAuth secret. | | signupConfirmationUrl | string | null | Link target used in the signup confirmation email. Defaults to the current page URL. | | dev | boolean | false | Use the development MCP host and skapi.app db-CDN host instead of production. | | mcpBaseUrl | string | null | Override the MCP OAuth server base URL entirely (advanced). | | hostDomain | string | null | db-CDN host for temporary file URLs. Defaults to skapi.app (dev) / skapi.com (prod). | | attachmentParsers | array | null | Client-side attachment parsers. See Attachment parser plugins. |

Methods

The BunnyQuery global also exposes:

| Method | Description | | ---------------------------------- | ----------------------------------------------------------------------------------- | | setTheme(theme) | Apply "light" or "dark" and persist it. | | toggleTheme() | Switch between light and dark. | | logout() | Sign the current user out and return to the login view. | | registerAttachmentParser(parser) | Register a client-side attachment parser. May be called before or after init(). See Attachment parser plugins. | | version | The widget's package version string. Also logged to the console on init(). |

BunnyQuery.setTheme("dark");
BunnyQuery.toggleTheme();
BunnyQuery.logout();

init() is idempotent — calling it twice logs a warning and returns the existing instance rather than re-mounting. On a successful mount it logs its version, e.g. [bunnyquery] v1.3.5.

Supported file types

When a user attaches a file, BunnyQuery makes its contents available to the AI automatically — detected by extension (with a MIME-type fallback), nothing to configure. There are three paths, plus a couple of caveats.

1. Images — read directly by the model (vision + OCR)

.jpg · .jpeg · .png · .gif · .webp

The image is attached to the request inline, so the model both describes the picture and reads any text in it (OCR). Works on both Claude and OpenAI. Only images referenced in the most recent message are inlined (older links may have expired).

2. Documents, data & code — extracted on the server (inlined as text)

The skapi proxy downloads the file, extracts its text server-side, and inlines that text into the request — the model reads it directly, with no fetching. This keeps indexing consistent across model providers.

Office & e-book (binary/zip, parsed): .docx · .xlsx · .pptx · .hwp · .hwpx · .ods · .odt · .odp · .epub

Text, data, markup & source code (decoded as text; .html/.htm have their tags stripped):

.csv .tsv .tab .txt .text .log .md .markdown .rst .json .ndjson .jsonl .geojson
.xml .yaml .yml .toml .ini .conf .cfg .properties .env .rtf .html .htm
.js .mjs .cjs .ts .tsx .jsx .py .rb .go .rs .java .kt .c .h .cpp .cc .hpp .cs
.php .swift .sh .bash .zsh .sql .css .scss .less .vue .svelte .tex .srt .vtt

Plus a MIME fallback: any file whose content type is text-like (text/*, application/json, application/xml, *+json, *+xml, *+yaml, …) is decoded even when its extension isn't in the list above.

Encoding is auto-detected — UTF-8 (BOM-aware) → CP949/EUC-KR (Korean) → Latin-1. Extracted text is capped at 200,000 characters; longer files are truncated with a ...[truncated for length; original N characters] marker.

3. PDFs & other links — fetched by the model

.pdf (and any file that is neither an image nor server-extractable) is handed to the model as a temporary link, which it opens with its built-in web tool: Claude via web_fetch, OpenAI via web_search (external web access is enabled). Both can open and read PDFs, so PDFs work on either provider.

A provider's web tool opens document/page-style URLs such as PDFs, but not necessarily a bare data-file download (e.g. a raw .csv/.tsv link). That's why those data formats are extracted server-side (path 2 above) instead of being left to the model to fetch.

Caveats

  • Legacy / macro Office.doc .xls .ppt (legacy binary) and .docm .xlsm .pptm (macro-enabled) have no reliable server-side reader. They upload fine but are indexed from metadata only; re-save as .docx / .xlsx / .pptx (or PDF) to capture their contents.
  • Anything else — a format covered by none of the above is indexed from its metadata. To support it, register your own Attachment parser plugin — it runs in the browser and feeds parsed text straight into indexing.

Attachment parser plugins

By default the chat agent reads images with vision/OCR, extracts Office/OpenDocument/EPUB and text/data/code files on the server, and lets the model fetch PDFs with its built-in web tool (web_fetch on Claude, web_search on OpenAI) — see Supported file types. For any format read by none of these (e.g. a proprietary binary format), register a parser plugin: it runs in the browser, turns the uploaded file into text (or an HTML string), and the widget sends that content inline for indexing — no web_fetch, no server extraction for that file.

BunnyQuery ships only the mechanism. You bring the parsing library (so the widget stays lean and you choose which formats and which library).

A parser is a plain object:

interface AttachmentParser {
  name?: string;                                   // label, used in logs
  match: (file: { name: string; mime?: string }) => boolean;   // handle this file?
  parse: (file: File) => string | null | undefined | Promise<string | null | undefined>; // text or HTML; falsy = skip
}

The first parser whose match returns true wins. A parser that throws or returns nothing is ignored — the file falls back to its normal path. Output is capped (~200k chars) before it is inlined.

Example

Load whatever parsing library reads your format, then register a parser that turns a File into text:

<!-- bring your own parsing library, e.g. from a CDN -->
<script src="https://cdn.example.com/my-format-parser.js"></script>
<script>
  BunnyQuery.registerAttachmentParser({
    name: "my-format",
    match: (file) => /\.myext$/i.test(file.name),
    parse: async (file) => {
      const bytes = new Uint8Array(await file.arrayBuffer());
      return window.myFormatParser.toText(bytes); // return plain text OR an HTML string
    },
  });

  BunnyQuery.init(skapi, "chatbox", { theme: "light" });
</script>

Equivalent one-shot form via init options:

BunnyQuery.init(skapi, "chatbox", {
  attachmentParsers: [ myParser ],
});

Bundler consumers can import the same registry from the engine:

import { registerAttachmentParser } from "bunnyquery/engine";
registerAttachmentParser(myParser);

Theming

BunnyQuery is themed with CSS custom properties (--bq-*) under a [data-bq-theme="light"|"dark"] attribute that the widget sets on its own root. To customize colors, override the variables in your own stylesheet after bunnyquery.css, scoped to .bq-agent:

.bq-agent {
  --bq-main: #ff4fa3;
  --bq-ink: #111;
}

The active theme is saved to localStorage, so a returning user keeps their choice.

Importing the chat engine

bunnyquery.js is the ready-made widget. Under it sits a framework-agnostic, DOM-free chat engine — the same core that powers both this widget and the Skapi admin chatbox. Import it from bunnyquery/engine when you want to build your own chat UI (React, Vue, Svelte, vanilla…) while reusing the engine's message/queue/typewriter/ cache state machine, request builders, markdown-message composition, and prompts.

Install the package, plus the skapi-js SDK (for the transport) and — if you don't already have one — a markdown renderer such as marked:

npm install bunnyquery skapi-js marked
import {
  configureChatEngine,
  ChatSession,
  composeUserMessage,
  type ChatHost,
} from 'bunnyquery/engine';

// Shared chat-surface styles (message bubbles, rendered markdown, links).
// Pair it with your own container/layout CSS and the --bq-* design tokens.
import 'bunnyquery/styles/chat.css';

// 1. Inject the skapi transport + MCP endpoint ONCE at startup.
configureChatEngine({
  clientSecretRequest: (opts) => skapi.clientSecretRequest(opts),
  clientSecretRequestHistory: (params, fetchOptions) =>
    skapi.clientSecretRequestHistory(params, fetchOptions),
  mcpBaseUrl: 'https://mcp.broadwayinc.computer',
  poll: 0, // see the note below
});

// 2. Implement a ChatHost (identity, render/scroll hooks, the skapi
//    cancel/refresh surface) for your view, then drive a ChatSession.
const session = new ChatSession(host); // host: ChatHost
await session.loadHistory();
session.dispatchComposedMessage('Hello!'); // send a message

The engine owns chat state and logic and calls back into your view through the ChatHost interface (render, scroll, identity, cancel/refresh). It has no bundled runtime dependencies — you inject the skapi transport via configureChatEngine() and render markdown yourself (e.g. with marked). Everything is fully typed: ChatSession, ChatHost, ChatMessage, ChatIdentity, ChatState, composeUserMessage, the request builders (callClaudeWithPublicMcp / callOpenAIWithPublicMcp, getChatHistory, notifyAgentSaveAttachment), the prompt builders, and the token-budget / link / history helpers — see the .d.ts shipped with bunnyquery/engine.

configureChatEngine options:

| Option | Type | Description | | ----------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------ | | clientSecretRequest | function | skapi.clientSecretRequest, bound to your Skapi instance. Required. | | clientSecretRequestHistory | function | skapi.clientSecretRequestHistory, bound to your Skapi instance. Required. | | mcpBaseUrl | string | MCP server base URL (you resolve prod vs dev). Required. | | poll | number? | Value attached as poll on every request. Omit it if your clientSecretRequest already resolves with the final body; pass 0 for the deployed skapi-js@latest (needed for the early ack + a manual .poll() handle that powers queued-send cancel — the widget's case). |

OAuth & redirects

BunnyQuery connects to your AI agent through an MCP OAuth server (mcp.broadwayinc.computer in production, mcp-dev.broadwayinc.computer when dev: true). After authorization, the OAuth server redirects back to the current host page — BunnyQuery reads the ?code=…&state=… parameters, completes the exchange, and cleans them from the URL automatically. No dedicated callback page is needed; just make sure the page that hosts the widget is a stable, reachable URL.

Notes

  • The widget fills its mount element. Give that element a real height (e.g. height: 100dvh) or it will collapse.
  • File and folder uploads are stored in your Skapi project's database storage and served from a temporary db-CDN URL (hostDomain); links in chat refresh on expiry.
  • The agent shown in the header (BunnyQuery · <project name>) reflects the project configured for your Skapi service.