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

opencode-episodic-memory

v0.3.2

Published

Semantic search over past OpenCode conversations — local embeddings (Transformers.js), SQLite index, native plugin tools. Inspired by obra/episodic-memory, rebuilt natively for OpenCode.

Readme

opencode-episodic-memory

skills.sh

Semantic search over your past OpenCode conversations. Remember past discussions, decisions, and patterns across sessions.

Inspired by obra/episodic-memory, rebuilt for OpenCode primitives — native plugin tools instead of an MCP server, plugin events instead of hooks, and OpenCode's own session database as the source.

Wondering how this compares to opencode-mem, codemem, memsearch, and the rest of the OpenCode memory-plugin landscape? See docs/alternatives.md.

How it works

  1. Read — sessions/messages/parts from OpenCode's ~/.local/share/opencode/opencode.db (read-only)
  2. Parse — condensed exchanges (user text, assistant text, tool names; no reasoning blobs or tool output)
  3. Embed — local, offline embeddings via Transformers.js in a lazily started, idle-evictable system-Node sidecar (Snowflake/snowflake-arctic-embed-m-v1.5 q8, 768 dims; retrieval prefix on search queries). Chosen by empirical eval on a real corpus — see docs/embedding-model-eval.md
  4. Index — plain SQLite at ~/.local/share/opencode-episodic-memory/index.db by default; optional libSQL/Turso remote storage can combine source-scoped indexes across devices
  5. Recall — native plugin tools episodic_search / episodic_read_window / episodic_read_session, plus a remembering-conversations skill that teaches the agent when to search
  6. Stay fresh — the plugin re-indexes each session on the session.idle event

Design note: bun:sqlite cannot load dynamic extensions, so sqlite-vec is not usable inside OpenCode plugins. Brute-force cosine is single-digit milliseconds at this scale (thousands of chunks) and has zero native-dependency risk. The store layer is the single swap point if a real ANN index is ever needed. FTS5 is compiled into bun:sqlite (not a loadable extension), so lexical BM25 ranking is available; search is vector-only by default, with lexical and hybrid (reciprocal-rank-fusion) modes opt-in — hybrid is off by default because BM25 tends to match injected boilerplate on this corpus.

Install

opencode plugin [email protected] -g

This adds the plugin to your OpenCode config (-g = global config; omit it to install for the current project only). Pin the version — OpenCode caches npm plugins and never re-resolves a bare name / @latest (anomalyco/opencode#25293). To update later, re-run with the new version and --force.

Or edit ~/.config/opencode/opencode.json manually:

{
  "plugin": ["[email protected]"]
}

Default sidecar-mode semantic indexing and vector/hybrid search require a system Node 20+ binary (node by default). The first embedding run downloads the model (~100 MB, cached afterward). The model and its native runtime live in that Node sidecar, not inside OpenCode's Bun/TUI process. The warm sidecar is stopped after its configured idle timeout to release model memory: the host sends SIGTERM, then SIGKILL if the child remains alive one second later. Output from retired children is ignored. The next embedding request waits for the old child to exit before starting a fresh one and reloading the cached model, so the first request after idle eviction has model-loading latency. Explicit EPISODIC_EMBED_MODE=inline works without Node but is unsafe in affected OpenCode/Bun versions. episodic_read_window, episodic_read_session, and lexical text search also remain available without Node.

Install the skill so the agent knows when to search, via the skills CLI:

npx skills add robertn702/opencode-episodic-memory -g

(-g installs to ~/.config/opencode/skills/; omit it to install into the current project. npx skills update picks up future skill changes.)

Alternatively, copy it manually — it's included in the npm package; once OpenCode has downloaded the plugin (i.e. after first launch), copy it out of the package cache (the path contains your pinned version):

cp -r ~/.cache/opencode/packages/[email protected]/node_modules/opencode-episodic-memory/skills/remembering-conversations ~/.config/opencode/skills/

Then backfill existing history and restart OpenCode:

bunx [email protected] sync

CLI

The package ships an opencode-episodic binary (requires bun on PATH). Invoke it through the package spec — pin it to match your plugin version:

bunx [email protected] sync [--force]          # index new/changed sessions
bunx [email protected] search "query"          # semantic (vector) search
bunx [email protected] search q --text "terms"  # lexical BM25 (all terms AND-matched, token-based)
bunx [email protected] search q --hybrid       # fuse vector + BM25 (RRF; opt-in)
bunx [email protected] search q --after 2026-07-01 --limit 5
bunx [email protected] read <session-id>       # full transcript (live store)
bunx [email protected] read <id> --indexed     # local indexed excerpts
bun run src/cli.ts read <id> --indexed --source laptop     # remote indexed excerpts (development/source checkout)
bunx [email protected] stats                   # index statistics
bunx [email protected] doctor                  # diagnose setup

--after/--before take YYYY-MM-DD (midnight UTC). --after D is inclusive of day D; --before D is exclusive of day D (i.e. up to the start of that day).

Agent tools

  • episodic_search — query (+ optional text, mode: vector|text|hybrid, after, before, limit). vector (default) is semantic; text is lexical BM25; hybrid fuses both via RRF (opt-in — can surface lexical noise). Returns dated excerpts with session IDs, scores, and message anchors. Remote indexes are vector-only: the plugin exposes only vector there, and explicit remote text/hybrid requests fail before embedding with actionable guidance rather than silently falling back. Local text/hybrid behavior is unchanged.
  • episodic_read_window — session_id, anchor_message_id (+ optional source_id, required for remote indexes; before, after, each 0-20, default 3). Current-source hits use privacy-gated live messages. Foreign-source hits use labeled indexed exchanges around the anchor, with before/after counting chunks instead of messages and each chunk rendered at up to 600 UTF-8 bytes. Missing or stale anchors cannot expand; use episodic_read_session with the same session/source and indexed: true for available indexed excerpts.
  • episodic_read_session — session_id (+ optional source_id, required for remote indexes; indexed). Reads the full live transcript, or indexed excerpts when requested, deleted, or on another source. Current-source indexed reads still check the live privacy marker and withhold cached content if validation fails. Prefer episodic_search -> episodic_read_window -> episodic_read_session, stopping once enough context has been recovered.

Excluding conversations

Any conversation containing this marker is archived nowhere and indexed nowhere:

DO NOT INDEX THIS CHAT

Note: the marker is matched as a bare substring anywhere in any message part, so this also excludes conversations that merely quote the phrase (such as discussions about this tool itself). This is broader than upstream's full instruction-tag match — the intent is the same, but our matching is literal.

Configuration (env vars)

| Variable | Default | Purpose | |---|---|---| | EPISODIC_SOURCE_DB | ~/.local/share/opencode/opencode.db | OpenCode session store | | EPISODIC_INDEX_DB | ~/.local/share/opencode-episodic-memory/index.db | Index location | | EPISODIC_INDEX_URL | unset | Opt-in libSQL/Turso index URL; activates remote vector-only mode | | EPISODIC_INDEX_AUTH_TOKEN | unset | Required for remote network URLs; passed to @libsql/client | | EPISODIC_SOURCE_ID | unset | Required in remote mode; stable device/source identity | | EPISODIC_EMBED_MODEL | Snowflake/snowflake-arctic-embed-m-v1.5 | Transformers.js embedding model | | EPISODIC_EMBED_MODE | sidecar | sidecar runs embeddings in Node; inline is an explicit escape hatch | | EPISODIC_NODE_BINARY | node | Node 20+ executable used by sidecar mode | | EPISODIC_EMBED_BATCH_SIZE | 32 | Texts per sidecar request (1-64) | | EPISODIC_EMBED_READY_TIMEOUT_MS | 600000 | Maximum wait for sidecar/model startup | | EPISODIC_EMBED_REQUEST_TIMEOUT_MS | 120000 | Maximum wait for a post-startup embedding request | | EPISODIC_EMBED_IDLE_TIMEOUT_MS | 300000 | Stop an idle sidecar after this many milliseconds; 0 disables idle eviction |

EPISODIC_EMBED_MODE=inline loads Transformers.js native addons directly in OpenCode's embedded Bun process. It exists only as an explicit compatibility escape hatch and is unsafe with affected OpenCode/Bun releases that can crash during native-addon teardown. It is never selected automatically if sidecar startup fails. Run bun run src/cli.ts doctor to diagnose the selected mode, Node version, and a real embedding.

The sidecar idle timer starts only after initialization and all embedding work has completed. Startup, queued requests, in-flight work, and sequential batches keep it alive. Background indexing counts as embedding activity. Invalid timeout settings fail before a sidecar is spawned; the timer is unref'd so it does not keep the CLI running. Idle eviction reduces retained idle memory, but does not cap memory across simultaneously active runtimes.

Optional shared remote index

Set all three variables on each device (use a distinct, stable source ID per device):

export EPISODIC_INDEX_URL='libsql://your-index.turso.io'
export EPISODIC_INDEX_AUTH_TOKEN='...'
export EPISODIC_SOURCE_ID='laptop'

This is an explicit privacy boundary: sync still reads OpenCode locally and generates embeddings locally, but it uploads condensed chunk text, session metadata, anchors, and embedding vectors to the configured database. Do not configure it unless that database is appropriate for this conversation data. Sessions containing DO NOT INDEX THIS CHAT upload neither content nor a metadata tombstone; an existing row for that source/session is removed. Absolute local file: libSQL URLs are supported without a token for hermetic testing; network URLs must use https:, wss:, or TLS-enabled libsql: and require a token supplied only through EPISODIC_INDEX_AUTH_TOKEN (not embedded in the URL). For libsql:, omit tls or use exactly one lowercase tls=1 parameter. The remote schema is freshly created or validates/adopts a compatible existing schema and does not use FTS, so remote mode supports vector search only. --text, --hybrid, and plugin text/hybrid modes fail clearly in remote mode. Remote cosine search reads embedding candidates in bounded pages and hydrates only the final result set.

Remote search includes the source ID in each hit. source_id is required for remote indexed reads. Another device's hit can be read through the bounded indexed foreign-source path in episodic_read_window when its anchor is present and current; otherwise use episodic_read_session with indexed: true. These are condensed indexed excerpts, not a live transcript; indexes remain stale until the source syncs and carry no live privacy or freshness guarantees across devices.

Network failures are surfaced by the CLI and doctor; plugin background reindex logs failures and never silently falls back to a local index (which would split history). To return to local-only mode, unset EPISODIC_INDEX_URL, EPISODIC_INDEX_AUTH_TOKEN, and EPISODIC_SOURCE_ID; the existing local index is selected unchanged. Re-run sync if the local index needs rebuilding.

Not yet implemented (deliberate)

  • LLM-generated per-session summaries embedded instead of raw exchange text (upstream does this; deferred until search quality data says it's needed — would use OpenCode provider auth via client.session.prompt)
  • Multi-concept AND search, MCP server wrapper for non-OpenCode clients
  • ANN index (see design note above)

Development

To hack on the plugin itself, clone the repo and point OpenCode at the local entrypoint instead of the npm package:

git clone https://github.com/robertn702/opencode-episodic-memory.git
cd opencode-episodic-memory
bun install
// ~/.config/opencode/opencode.json
{
  "plugin": ["/path/to/opencode-episodic-memory/plugin/episodic-memory.ts"]
}

Inside the repo, run the CLI as bun run src/cli.ts <command> (same subcommands as above), tests with bun test, and typechecking with bun run typecheck.

License

MIT