murmur guide
Everything to add murmur to a site, choose how the AI is called, index your pages and use the npm package.
Quick start
Paste this line before </body> on every page of your site:
<script type="module" src="https://maxgfr.github.io/murmur/cdn/murmur.js"></script>
A bubble appears in the bottom right corner. Without any option, it answers from the text of the page the visitor is reading, with a small AI model that runs in the visitor's browser (after they agree to a one-time 1.1 GB download); visitors can connect their own AI key instead from the AI settings. To answer from the whole site, build an index (see Index your site); for a hosted model with nothing to set up for visitors, use a proxy (see AI models and keys).
The configurator on the home page writes the line for you.
Options
Every option is a data- attribute on the script tag. All are optional.
| Attribute | What it does |
|---|---|
data-index | URL of the index file written by murmur index, for example /murmur-index.json. Without it, the widget reads the current page. |
data-endpoint | URL of your OpenAI-compatible proxy (https). Visitors then chat without a key. |
data-mode | proxy, local, auto (default) or key: who answers (see AI models and keys). |
data-preload | When the in-browser model downloads: question (default), open, idle or consent. |
data-local | false turns off the model that runs in the visitor's browser, which is otherwise the default when there is no proxy: visitors then need their own key. |
data-provider | Provider preselected for visitors who bring their own key: openai, anthropic, google, mistral, openrouter, groq… |
data-model | Model id: the one preselected for visitors, or the one asked of your proxy. |
data-instructions | Your instructions to the assistant: who it is, its tone, what it should not discuss. |
data-title, data-greeting, data-placeholder, data-button | The panel title, the first message, the question field's placeholder and the bubble's label. |
data-lang | Language of the interface and the answers: en or fr. By default, the page's lang, then the browser's. |
data-position | bottom-right (default) or bottom-left. |
data-accent | Any CSS colour for the bubble, the header and the visitor's messages. |
data-theme | auto (default, follows the system), light or dark. |
data-open | true opens the panel on load. |
data-storage-key | Prefix of what the widget stores, to keep two widgets on one domain apart. |
Index your site
The index is a JSON file of your pages cut into passages under their headings. Build it from your site's output folder, after your static site generator:
npx murmur-rag index ./dist --out ./dist/murmur-index.json
It reads every .html, .md and .txt file. In HTML it keeps the content of main (or article, or body) and skips navigation, headers, footers, forms and scripts. Add data-murmur-ignore to any element you want left out.
| Option | What it does |
|---|---|
--out | Where to write the index (default murmur-index.json). |
--base | URL prefix of the pages, for a site served under a sub-path such as /docs/. |
--root | CSS selector of the content in each page. |
--exclude | CSS selectors to skip, on top of the defaults. |
--max-chunk | Longest passage in characters (default 2000). |
The command also takes a live site: npx murmur-rag index https://example.com/sitemap.xml fetches every page of the sitemap.
In a GitHub Action
Add one step after your build, before uploading the Pages artifact:
- run: npx murmur-rag index ./_site --base /my-repo/ --out ./_site/murmur-index.json
AI models and keys
Pick how the bubble answers with data-mode. A key written in a web page can be read by anyone, so murmur never takes one: your key stays in your Worker, and a visitor's key stays in their own browser.
data-mode | Who answers |
|---|---|
proxy | Your Worker, holding your key. Implied by data-endpoint. What most sites use. |
local | A model in the visitor's browser, nothing else: no key settings. |
auto (default) | Your proxy if set; else the browser model, with visitors free to connect their own key. |
key | Each visitor's own key, nothing else. |
Your Worker holds the key
Deploy the example Cloudflare Worker. It keeps your key as a secret, forwards chat requests to any OpenAI-compatible API (OpenRouter, OpenAI, Groq, Mistral…), accepts only your site's origin, fixes the model and caps the length of each answer. Then:
<script type="module" src="https://maxgfr.github.io/murmur/cdn/murmur.js"
data-endpoint="https://murmur-proxy.you.workers.dev/v1" data-model="openai/gpt-5-mini"></script>
Set a spending limit at your provider: anyone who can open your site can use your proxy.
A model in the visitor's browser
With data-mode="local" (or by default when there is no proxy), the browser runs LFM3 1.7B by Liquid AI (October 2026), in its 4-bit WebGPU weights, through Transformers.js. Questions never leave the device.
data-preload says when the weights (about 1.1 GB) download:
data-preload | Download starts |
|---|---|
question (default) | On the first question, with a discreet progress bar above the question field. |
open | When the visitor opens the bubble, so it is often ready by the time they finish typing. |
idle | As soon as the page is idle, even if the visitor never opens the bubble. |
consent | After the visitor agrees on a prompt that names the model and its size. |
The weights stay in the browser's cache, and murmur asks the browser to keep them (persistent storage) so a 1 GB download is not evicted. On the next visit, a model already in the cache is prepared in the background, without network, whatever data-preload says: the first answer then comes in a few seconds.
It needs WebGPU (recent Chrome, Edge or Safari) and a GPU with about 1.5 GB of memory to spare; without it, the bubble says so. A 1.7B model is less precise than a hosted one.
Each visitor brings their key
With data-mode="key", or from the AI settings in the default mode, the visitor picks a provider and a model, pastes a key, tests it and saves. llm-connect encrypts the key with AES-GCM under a key the page cannot export, keeps it in the browser (for the session, or on the device if the visitor asks) and sends it only to the chosen provider. The visitor can also point the widget at Ollama or LM Studio on their own machine.
The browser calls the provider directly, so the provider must accept calls from web pages (CORS): OpenAI, Anthropic, Google, Mistral, OpenRouter, Groq, Together, DeepInfra, xAI and local servers with CORS enabled do.
npm package
npm install murmur-rag
The package is split so you can take only what you need:
| Import | Contents |
|---|---|
murmur-rag | createMurmur, the headless engine, plus everything in /rag. |
murmur-rag/rag | bm25Retriever, buildIndex, splitText, formatSources, citedSources, the Retriever interface. |
murmur-rag/indexer | indexPages, indexDocument, extractSections. |
murmur-rag/llm | proxySettings, visitorStore, resolveSettings, on top of llm-connect. |
murmur-rag/local | localModel, an AI SDK model running in the page on WebGPU; hasWebGPU, DEFAULT_LOCAL_MODEL. Needs @huggingface/transformers. |
murmur-rag/widget | mountMurmur, defineMurmurChat, MurmurChat, renderMarkdown. |
murmur-rag/embed | The script-tag entry: reads data- attributes and mounts the bubble. |
Headless
import { createMurmur } from "murmur-rag";
import { anthropic } from "@ai-sdk/anthropic";
const murmur = createMurmur({ index, model: anthropic("claude-sonnet-5-5"), language: "en" });
const answer = await murmur.ask("What does the Pro plan include?");
for await (const part of answer.textStream) process.stdout.write(part);
const { cited } = await answer.done; // numbers of the passages it cites
Your own retriever
Any object with search(query, { limit, budgetTokens }) returning passages replaces BM25: an embeddings index, a search API, a database.
const retriever = {
async search(query) {
const hits = await mySearch(query);
return hits.map((h) => ({ doc: 0, url: h.url, title: h.title, heading: "", anchor: "", text: h.text }));
},
};
mountMurmur({ retriever, llm: { endpoint: "https://proxy.example/v1" } });
Styling and events
The widget lives in a Shadow DOM, so your CSS cannot break it and its CSS cannot leak into your page. Restyle it through custom properties on murmur-chat:
murmur-chat {
--murmur-accent: #0b5cad;
--murmur-accent-ink: #ffffff;
--murmur-font: "Inter", sans-serif;
--murmur-radius: 12px;
--murmur-z: 1000;
}
It sends a murmur:state event at each step (searching, streaming, done, error) with the passages found and the ones cited: the switchboard on the home page is drawn from it.
document.querySelector("murmur-chat").addEventListener("murmur:state", (e) => {
console.log(e.detail.state, e.detail.passages, e.detail.cited);
});
Privacy and security
- murmur has no server and collects nothing. The question and the matching passages go to the AI provider you or the visitor chose, and nowhere else.
- The conversation is kept in the tab (
sessionStorage) so it survives a click on a source; it is gone when the tab closes. - Answers are rendered as text: model output never becomes HTML in your page, and links are limited to http, https and relative URLs.
- Passages are sent to the model as quoted data with an instruction never to follow orders found inside them.
FAQ
Does it work on GitHub Pages?
Yes: murmur is static files. This site is on GitHub Pages and its bubble answers from its own index, built in its deployment workflow.
Which model should I use?
A fast, inexpensive model is enough: the work is reading a few passages and summarising them. The settings suggest the recommended model of each provider.
What if the answer is not on my site?
The assistant is told to say so and point to where to look, rather than invent. If no passage matches the question at all, it is told that nothing was found.
Can I use the search without AI?
Yes: bm25Retriever(index).search(query) from murmur-rag/rag returns the matching passages with their links, to build a plain site search.