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

@squirrelnotes.app/mcp-embeddings

v2.5.0

Published

Opt-in local semantic memory search for the Squirrel Notes MCP (stdio). The embedding model runs on your machine; no memory text is sent anywhere to be embedded.

Readme

Squirrel Notes MCP: local embeddings (opt-in)

@squirrelnotes.app/mcp-embeddings is the Squirrel Notes stdio MCP server with semantic memory search. search_memory finds a memory by its meaning as well as its words, so "food allergies" finds "Is allergic to peanuts", which shares no word with it.

It is the same server as @squirrelnotes.app/mcp: the same tools, the same inputs and the same output shape. It is built from the same source and published from the same commit, at the same version. The only differences are the order search_memory returns, its ranking (hybrid) and each hit's semanticScore, which the base package sends as null. If you do not install it, nothing changes for you.

What stays on your machine

  • The model runs locally. It is gte-small, a small English sentence-embedding model (all-MiniLM-L6-v2 until feature 0059). It runs in this process, on your CPU, through ONNX Runtime. No memory text is sent anywhere to be embedded: not to Squirrel Notes, not to HuggingFace, and not to any embedding API.
  • The model ships inside the package. Nothing is downloaded when the server runs, so embeddings work offline.
  • ONNX Runtime's own telemetry is turned off. The runtime's official macOS and Linux builds report usage to Microsoft by default. This package sets ORT_DISABLE_TELEMETRY=1 before the runtime loads, whatever your environment says. On Windows the runtime reports through Windows' own event tracing (ETW) instead, which that setting does not affect; whether those events leave your machine depends on your Windows diagnostic-data settings.
  • Vectors stay local and encrypted. Embedding vectors can reveal roughly what a text said, so they are treated like the text itself:
    • they are never sent to the server, never logged and never returned by a tool;
    • they are kept in one file per account in your OS cache directory (~/Library/Caches/squirrel-notes/ on macOS, $XDG_CACHE_HOME or ~/.cache on Linux, %LOCALAPPDATA% on Windows);
    • that file is encrypted with AES-256-GCM, under a key derived (HKDF-SHA256) from your data key, and written with mode 0600. Windows ignores Unix modes; there the file has your user profile's permissions.
  • What still leaves your machine is what always did. The server stores your memories as ciphertext only. search_memory's results (the memory text) go to the agent that asked, and so to that agent's model provider, exactly as with the base package.

What the encryption does not protect against: anyone who can read your MCP client config. That file holds your passphrase, which unlocks the data key, which unlocks the vector file. The encryption protects the vector file when it leaks on its own: in a backup, a synced folder or a support bundle.

Install (opt-in)

Change args in your MCP client config from the base package to this one. The env block stays the same; add ONNXRUNTIME_NODE_INSTALL as shown.

{
  "mcpServers": {
    "squirrel-notes": {
      "command": "npx",
      "args": ["-y", "@squirrelnotes.app/mcp-embeddings"],
      "env": {
        "SQUIRREL_API_BASE_URL": "https://api.squirrelnotes.app",
        "SQUIRREL_API_KEY": "sqn_<your_api_key>",
        "SQUIRREL_PASSPHRASE": "<your_passphrase>",
        "ONNXRUNTIME_NODE_INSTALL": "skip"
      }
    }
  }
}

Why ONNXRUNTIME_NODE_INSTALL=skip. The runtime, onnxruntime-node, has an install script. On Linux x64 only, that script downloads the NVIDIA CUDA add-ons from nuget.org by default, checking no hash. This package does not use the GPU, so the download is never needed. The setting turns it off. On macOS, Windows and Linux arm64, the script downloads nothing.

Pinned dependencies. The package ships an npm-shrinkwrap.json, so npm installs exactly the dependency versions that were audited and licence-checked for the release.

Size. The package is about 77 MB to download (134 MB unpacked), most of it the full-precision model (about 17 MB with all-MiniLM-L6-v2 until feature 0059). onnxruntime-node adds about 301 MB (287 MiB) unpacked, because it ships binaries for every platform it supports. That is why embeddings are a separate package.

Intel Macs. onnxruntime-node 1.30.0 ships no binary for Intel Macs (darwin/x64): its native bindings cover macOS on Apple silicon, Linux x64 and arm64, and Windows x64 and arm64. On an Intel Mac the install should still succeed (the runtime's install script has nothing to fetch there), but the runtime cannot load, so the server logs memory: local embeddings were not loaded (runtime); memory search is lexical only once on stderr and search_memory stays lexical, exactly as with the base package. This is read from the runtime's published files and its loader; it has not been tested on an Intel Mac.

To go back to lexical search, change args back to @squirrelnotes.app/mcp. The vector file can be deleted at any time; it is rebuilt when needed.

Recommended: the agent skill

Recommended: give your agent the Squirrel Notes skill from github.com/squirrel-notes/skills. It teaches the agent to start each session with get_context, search before answering (with a near-miss retry), save memories that will be found again, and correct them without losing history. In Claude Code, copy skills/squirrel-notes into ~/.claude/skills/; the repository's README covers other agents.

How search ranks

  • Without this package, search_memory is lexical: BM25 over the words of each memory's text, entity, tags and keywords, times a recency decay per category.
  • Each memory is embedded as its entity, text, tags and keywords (entity: text (tags, keywords); feature 0061). A memory with no keywords is embedded from the same text as before, but the vector file now records how the text is built, so the first searches after updating re-embed every memory once (an older companion may have embedded a memory without the keywords the app gave it, at the same revision). After that, a memory whose keywords change is re-embedded because the change is a new revision.
  • With it, the two scores each hit returns are fused (feature 0050, in place of reciprocal rank fusion; feature 0054, "words win, age on meaning"). The fused value is 0.3 × lexicalScore + 0.7 × the semantic part × the recency decay, where the semantic part is 0 at or below the model's floor (minSimilarity, 0.837), 1 at the floor plus its span (semanticSpan, 0.086, so a cosine of 0.923) or above, and linear between. Both values are the model's, in model-manifest.json. The decay is applied once, to the semantic part only, so age does not fade a memory's word match. A hit that holds every query word (lexicalScore 1) scores 0.5 + half the fused value, any other hit half of it: so those hits come first, and score never rises down the list. Of two hits on the same side of 0.5 with the same recency weight (the same age and category), one at least as high on both lexicalScore and semanticScore, and higher on one, ranks first; ties go to the higher cosine. The weight 0.3 was chosen after measuring on the recall gate's corpus (any weight between 0 and 1 keeps that order). The span belongs to the model's cosine range, so it is in model-manifest.json with the floor: 0.086 for gte-small, whose cosines sit between about 0.75 and 0.95, calibrated to keep the same share of relevant pairs as all-MiniLM-L6-v2's 0.35 and 0.4 did (feature 0059).
  • The members are the memories whose words match and those at or above the floor. Below that, a memory is returned only if its words match.
  • An old memory matched by words keeps its word part whatever its age: only the meaning part is discounted. One that holds every query word ranks above any memory matched by meaning alone, and one that holds some of them can still rank above a fresh memory matched by meaning alone (feature 0054, settling proposal 0074's open question 2). Lexical search is unchanged: BM25 times the decay.
  • score is on a 0–1 scale here, and unbounded BM25 in the base package. It orders one response only. The keys are the same in both.
  • To judge a hit, use the absolute scores: semanticScore (the cosine similarity, reported for every hit, also those below the floor found by words) and lexicalScore (the share of the query's content words, stopwords aside). Hits come in rank order, and the tool suggests no cut-off: a low score means lower confidence, not a wrong answer. With all-MiniLM-L6-v2 the recall gate's synthetic data put a cut at semanticScore 0.45 (ADR-0444), but correct hits on real memories scored 0.416 and 0.426, so agents are told to check low-scoring hits against the question before dismissing them (ADR-0444, amended 2026-10-13). semanticScore is a cosine, −1 to 1; with gte-small most memories score between about 0.75 and 0.95, related or not, so it is compared between the hits of one response. ranking is hybrid when the semantic order ran, lexical while the model loads or after it fails.
  • Accents on Latin letters are ignored by word matching in both packages (creme finds "crème", strasse finds "Straße"). Word matching works in any script in both packages: Greek, Cyrillic or Korean by words, and Chinese, Japanese, Thai, Lao, Khmer and Myanmar, written without spaces, by pairs of characters (a one-character query counts half for each pair holding it). Other forms of a word are not matched (тёмная does not find "тёмную", and a two-syllable Korean query such as 서울 does not find "서울에", where a particle is attached), and accents on letters of other scripts still count. The model reads English: embeddings add little for other languages.
  • A hyphenated word matches whole (pihole or Pi-hole), and each of its parts counts half; a query word of one or two letters matches only that whole word, or a hyphenated word's part of the same spelling (half).
  • A query with no letter or digit in any script (most emoji, or punctuation, alone) always comes back with a note. It says the query cannot be searched, unless the semantic ranking actually ran, in which case it says the results were found only by meaning.

Near misses (opt in)

search_memory with nearMisses: true also returns up to 3 of the closest memories that did not match (no word in common, meaning under the floor), after the matches, each with nearMiss: true, score 0 and lexicalScore 0; total does not count them (feature 0060, ADR-0478). Default search is unchanged, so a question with nothing stored still comes back empty. Agents are told to use it once, on a retry, and to treat a near miss as a candidate to check, never an answer. Without this package none are returned.

When it is lexical anyway

Search falls back to the base package's lexical order, and says so once on stderr, never per call, when:

  • SQUIRREL_MEMORY_EMBEDDINGS=0 is set;
  • a model file's size or SHA-256 differs from model-manifest.json, or a model file is missing (the model is refused, never loaded);
  • ONNX Runtime cannot load on your platform (an Intel Mac, for one: see Intel Macs above);
  • the runtime fails during a search (lexical for the rest of the session).

While the model is still loading at start-up (a fraction of a second), search is lexical too.

First searches. Each memory is embedded once, then only again when it changes (its revision moves). A search spends at most 8 seconds embedding memories that have no vector yet. Until a later search embeds them, the rest have no semanticScore: they are returned only if their words match, and their fused value is at most 0.3, so during this warm-up a partial word match not yet embedded can rank below a memory found by meaning alone (a match on every query word still ranks first). Measured with all-MiniLM-L6-v2 on an Apple M1 Pro: 500 memories in 4.4 s, 5,000 in 26.7 s. gte-small at full precision takes about 3.5 times as long per memory (7.5 ms against about 2.1 on an AMD Ryzen 7 5800X; feature 0059), so expect about 3.5 times these; not measured on Apple silicon.

Model integrity

model-manifest.json pins the model to a commit of thenlper/gte-small (17e1f347…, the full-precision onnx/model.onnx; MIT, see LICENSE-MODEL.txt). Full precision, not the repository's int8 build: int8 kernels differ by CPU family, and with gte-small's narrow cosine band the int8 build ranked differently on an Intel runner than on AMD (feature 0059 round 2); float arithmetic gives the same vectors everywhere to within 1e-5. It records each file's size and SHA-256, and the model's floor and span. Until feature 0059 it pinned sentence-transformers/all-MiniLM-L6-v2; the vector file records the model, so the first searches after an upgrade embed every memory again (ADR-0443).

  • The release workflow fetches the files from that commit, never from a branch. It refuses any file that does not match, and checks them again before packing.
  • Every start reads each file, checks it, and gives the runtime the same bytes it checked, so a file replaced after the check is never loaded.
  • SQUIRREL_MEMORY_MODEL_DIR can point at a copy you placed yourself. It goes through the same check.

The model is Apache-2.0; see LICENSE-MODEL.txt. This package is MIT.

Recall

The release is gated on a recall test (npm run test:recall). It compares recall@5 of lexical and hybrid search on 500 memories and 160 queries.

  • The corpus is synthetic and written by an AI agent, not user data, which zero-knowledge encryption makes unreadable.
  • Hybrid search must gain at least 15 points on paraphrased queries, and must not lose on exact-word queries.
  • Since feature 0050 it also gates, by words alone, a small ranking set of stand-ins for reported misses (hyphenated words, short fragments) and a ten-query set in other scripts (lexical recall@5 at least 0.8), and reports recall@1 and MRR beside recall@5.
  • The latest results are in ADR-0444 and the feature 0050 notes in the Squirrel Notes repository.

Checking recall on your own memories

A local diagnostic shows, for questions you choose, where the memory you expected ranks and how close it came to the floor (feature 0060). It runs the same unlock, model and ranking as this package, on your machine, from a checkout of the repository (it is not in the published package):

npm --prefix mcp-embeddings ci && npm --prefix mcp-embeddings run fetch-model
# queries.json: [{ "query": "DNS ad blocking", "expect": "Pi-hole" }, ...]
export SQUIRREL_API_BASE_URL=https://…   # the same value as in your MCP config
npm --prefix mcp-embeddings run diagnose -- queries.json

It asks for your API key and passphrase on the terminal, without showing them, unless SQUIRREL_API_KEY and SQUIRREL_PASSPHRASE are already set. Do not type them on the command line (SQUIRREL_PASSPHRASE=… npm …): the shell saves that line, with the values, to its history file.

expect is a piece of the memory you mean (its text, entity or tags), matched ignoring case and Latin accents. Each line reports the memory's id, its rank as a match or a near miss, its rank by meaning, its cosine and how far under the floor it is. It prints ids, ranks, scores and your own queries only: never memory text, entities or tags, and it writes no log. Its vectors go to a temporary directory that is deleted when it finishes or is interrupted (Ctrl-C); your companion's own vector file is not touched.

Environment variables

Everything in the base package's README applies, plus:

| Variable | Default | Description | |---|---|---| | SQUIRREL_MEMORY_EMBEDDINGS | on | 0 turns semantic search off (lexical only). | | SQUIRREL_MEMORY_MODEL_DIR | the package's model/ | Directory holding the model files. They are checked against the manifest. | | SQUIRREL_MEMORY_CACHE_DIR | the OS cache directory | Where squirrel-notes/memory-vectors-<id>.bin is kept. | | ONNXRUNTIME_NODE_INSTALL | — | Set to skip so the runtime's install script fetches no GPU add-ons (Linux x64). |

What is logged

Everything in the base package's list, plus one line at start-up: either local embeddings ready, or were not loaded, followed by a reason (manifest, missing, integrity or runtime) and, for the first three, a file name. After that, a line appears only if the runtime fails or the vector file cannot be written, and then only once. No line contains memory text, a query or a vector.