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

@knowl/opencode

v0.1.0

Published

Project memory for opencode, injected rather than requested. Retrieved knowledge reaches the system prompt, files carry their own recorded rules, and memory survives compaction.

Downloads

83

Readme

@knowl/opencode

Project memory for opencode, injected rather than requested.

Knowl already speaks MCP, and opencode can load it as a server. That surface only answers once the model decides to call a tool — which is exactly when a model that has forgotten the project is least likely to ask. This plugin uses opencode's hooks to reach three places a tool call cannot:

| Hook | What it does | | --- | --- | | experimental.chat.system.transform | Puts memory matching the current request into the system prompt, with no tool call by the agent at all. | | tool.execute.after | Turns Knowl's affectedPaths index into a rule that fires when a file is touched, appended to the tool result. | | experimental.session.compacting | Keeps memory and stated intent from being summarised away at compaction. |

Install

opencode plugin install @knowl/opencode

The plugin reads memory through the knowl CLI. It uses one on PATH when present and falls back to npx -y @dat999zx/knowl, so it works before anything is installed globally. Memory is a SQLite database under the project root — nothing is sent anywhere, and there is no API key because there is no service.

If the project has no Knowl database yet:

knowl init

Configuration

Every option is optional. Pass them as the second element of the plugin entry in opencode.json:

{
  "plugin": [
    ["@knowl/opencode", { "limit": 8, "pathTools": ["edit", "write"] }]
  ]
}

| Option | Default | Meaning | | --- | --- | --- | | command | ["knowl"], else npx | argv used to invoke the CLI, command first. Point it at a local build to develop against an unreleased engine. | | directory | opencode's project directory | Whose memory is read. Knowl scopes every atom to a project, so this decides which project answers. | | limit | 3 | Results requested per retrieval. Matches Knowl's own MCP default; see the cost note below before raising it. | | maxChars | 6000 | Character ceiling for one injected block. | | maxAtomChars | 1200 | Ceiling for one item inside that block. | | timeoutMs | 5000 | How long a CLI call may take before the turn proceeds without memory. | | minQueryChars | 12 | Messages shorter than this skip retrieval — "ok" and "continue" retrieve noise. | | pathTools | ["edit","write","patch"] | Tools whose file argument triggers path memory. Add "read" to fire on any file touch — see the cost note below. | | system / paths / compaction | true | Disable any of the three injection points individually. |

Behaviour worth knowing

Retrieval happens on your message, not on the system hook. experimental.chat.system.transform receives a session id and a model — never the prompt. So the query runs in chat.message, where your words exist, and the result is waiting when the system prompt is assembled. That holds the turn for at most timeoutMs.

Path memory fires once per item per session, and each path is looked up once. A rule that repeated on every edit would be scrolled past by the third repetition and pay tokens each time; a path re-queried on every touch would pay a process spawn to rediscover items that already fired.

A turn where memory has nothing injects nothing. Knowl answers every query with its best-ranked rows — returning nothing cannot be told apart from an empty store or a missing index — and flags them abstained when they all fell below its relevance floor. Those rows are dropped, so an off-subject question does not quietly buy the nearest atom in the store. Note the floor is the engine's judgement and it under-triggers on small stores (knowl#146); score cannot be used as a second gate, because on a small store it reads 0.98 for a perfect match and for gibberish alike.

Injection costs about 1,200 tokens on a turn where memory answers, measured against a real store at the default limit of 3 — roughly 47k across a forty-turn session. Raising limit to 5 took it to ~1,500 per turn and ~60k per session for items the engine's own eval suggests are mostly restating the first three (Recall@3 is 0.99). Lower maxAtomChars before raising limit if the budget is tight.

Each lookup costs a process spawn — about 800ms. Measured against a two-atom store, so it is interpreter startup rather than query work; the size of your memory barely moves it. That is why read is not in pathTools by default: it is the most frequent tool in a session, and twenty file reads would spend sixteen seconds. Edits and writes are rarer and already follow a slow model turn, so they carry it comfortably. Within a session the cost is paid once per file — the second touch of the same path is free.

If you want memory on every read, set pathTools to include "read" and accept the latency, or raise minQueryChars and lower limit to trim elsewhere.

It only reads. Writing memory stays with the MCP tools and the CLI, where a human or an explicit tool call decides what is worth recording. A plugin that wrote on its own would fill the store with whatever happened to be on screen.

It fails open. A missing CLI, an uninitialised project, a slow disk or a malformed row degrades to "no memory this turn", never to a broken turn.

Injected memory is fenced and labelled as data. Stored items can carry text a past session captured from files, tool output or third parties, and this plugin puts them where the model is most inclined to obey them. Each item is wrapped in a backtick run longer than any it contains, under a preamble stating the contents are recorded claims to evaluate rather than instructions to follow.

In a linked workspace, items are labelled by the repo that owns them. A fact owned by another repo describes that repo; the block says so, because acting on a neighbour's convention in this repo is the failure mode that matters.

system.transform also fires for the compaction request, measured in a live session. So retrieved memory reaches the summariser whether or not the compaction hook is enabled — disabling compaction and re-running produced a summary still carrying facts that appeared nowhere in the visible transcript. What the compaction hook adds on top is the full set of items surfaced across the session rather than only the last turn's retrieval, plus the instruction to keep stated intent verbatim. It is a widening, not the carrier.

Verifying a change

verify/ drives all three hooks against a real Knowl database with the real Bun shell. It is not published; run it from a checkout:

npm run build
bun run verify/drive.ts    # all three hooks, plus the gates and fail-open paths
bun run verify/paths.ts    # path matching across absolute, relative and Windows shapes

# point this at a SMALL store, so "off subject" is unambiguous
KNOWL_VERIFY_PROJECT=/path/to/tiny/project bun run verify/relevance.ts

Both print what was injected, so a regression is visible rather than merely non-failing.

They cover the plugin's own logic. Confirming that opencode still calls the hooks needs a live session: install the packed tarball into a Knowl-initialised project, then compare opencode run --pure against a normal run on a question only project memory can answer. All three hooks were confirmed that way against opencode 1.18.18.

License

Apache-2.0