A search engine for your codebase, built into opencode.
When you're working on a large project, the AI wastes thousands of tokens reading entire files just to find one function. This plugin fixes that by giving the AI a local code index — a searchable database of every function, class, and interface in your project. Instead of reading blindly, the AI searches first, then reads only the 20 lines it actually needs.
The result: 90% fewer tokens spent on code lookups, longer productive sessions, and the AI finds things grep can't.
The plugin runs silently in the background. When opencode starts, it automatically indexes your project — walking every code file, extracting function names, class names, interfaces, types, and enums, and storing them in a local SQLite database with full-text search.
Then, every time the AI needs to find code, it uses context_search instead of grep + reading whole files. The search returns exact file paths and line numbers in under a millisecond, so the AI reads only what it needs.
When you edit a file, the index updates itself automatically — no manual re-indexing, ever.
Here's what happens when the AI needs to find authenticate() in a 16-file project:
Without this plugin:
- The AI runs
grep "authenticate" src/— gets back matches across multiple files (hundreds of tokens) - The AI reads one full file hoping it's the right one (1,000+ tokens)
- If the name doesn't match literally, the AI lists files with
glob, then reads them one by one — 2,000+ tokens wasted - Total: 2,052 tokens per query
With this plugin:
- The AI calls
context_search "authenticate"— gets backfunction authenticate @ src/auth/auth.ts:4in 0.16ms - The AI reads auth.ts (1,117 chars)
- Total: 320 tokens per query, always finds the right answer
The difference is even bigger for searches where the keyword doesn't match literally. Searching for "invoice" finds createInvoice. Searching for "websocket" finds WebSocketHandler. The trigram tokenizer matches substrings, not just whole words — something grep can't do.
Measured with Bun's built-in token estimation (~4 chars/token) on a synthetic 16-file project (74 indexed chunks, 8,207 source chars).
| Query | Without plugin | With plugin | Saved |
|---|---|---|---|
authenticate login |
2,052 | 320 | 84.4% |
| Average | 2,052 | 320 | 84.4% |
The index compresses the full codebase to 34% of its original size (2,052 → 699 tokens). For a targeted lookup, the AI reads 1 file instead of 16 — saving 1,732 tokens per query.
| Previous (JSON) | Current (SQLite FTS5) | Speedup | |
|---|---|---|---|
| Average query time | 2.16ms | 0.16ms | 14x |
The JSON approach re-tokenized all 842 chunks on every search. SQLite FTS5 uses an inverted index with constant-time lookup — it stays fast as your project grows.
Add the package name to your opencode config file. That's it — opencode installs the plugin automatically via Bun on startup.
Global (~/.config/opencode/opencode.json):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@madtech/opencode-context-manager-plugin"]
}Project-level (opencode.json in your project root):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@madtech/opencode-context-manager-plugin"]
}The npm install postinstall hook automatically copies a small context-manager-loading-shim.ts to ~/.config/opencode/plugins/. That shim loads instantly on every opencode startup and shows an "Installing plugin…" toast while the main npm plugin is downloading. After the first install, both the shim and the npm cache are warm, and startups are fast.
Restart opencode. The plugin auto-indexes your project in a background worker (does not block the TUI) and copies the bundled SKILL.md to ~/.config/opencode/skills/context-manager/ so opencode discovers it without extra config. No manual setup needed.
Note: The first opencode startup after adding the plugin will appear frozen for ~30 seconds while opencode downloads the package via Bun. This is normal — subsequent startups use the cache and are instant.
If you open opencode in a very large directory (e.g. your home), the auto-index is capped at 10,000 files. Pass a narrower path to context_analyze for full coverage, or raise the cap with CONTEXT_MANAGER_MAX_FILES=50000.
If you prefer to run from a local clone (useful for development or if you don't have npm):
git clone https://github.com/madkoding/opencode-context-manager.git
cd opencode-context-manager
./install.shThe installer copies the plugin and the shim to ~/.config/opencode/plugins/, installs @opencode-ai/plugin via Bun, and adds the plugin to your opencode.jsonc. The skill is copied on first plugin load.
Install output is newline-delimited JSON on stderr (parseable with jq):
./install.sh 2>&1 | jq -r .message # human-readable one-liners
./install.sh 2> install.log # capture to fileJust run npm install -g @madtech/opencode-context-manager-plugin@latest (or npm update -g). The postinstall hook compares the bundled shim with the one in ~/.config/opencode/plugins/ and overwrites it only if the version changed. Opencode picks up the new shim on the next startup.
npm: Remove "@madtech/opencode-context-manager-plugin" from your plugin array and restart. The plugin and the shim are both self-cleaning:
- The shim checks the config on every opencode startup. If the plugin is no longer in the config, the shim self-deletes from
~/.config/opencode/plugins/. - The main plugin also checks the config in
ensureShimInstalledand removes the shim if the plugin was uninstalled.
For a thorough manual cleanup:
rm ~/.config/opencode/plugins/context-manager-loading-shim.ts
rm -rf ~/.cache/opencode/packages/@madtech
rm -rf ~/.cache/opencode/context-manager.sqlite*
rm -rf ~/.config/opencode/skills/context-managerSource: Run ./uninstall.sh from the cloned repo.
When opencode starts and no index exists, the plugin automatically walks your project directory — skipping node_modules/, .git/, dist/, build/, and other common ignore targets. For every code file it finds, it uses language-specific regex patterns to extract:
- Functions and methods (including arrow functions in JS/TS)
- Classes and structs
- Interfaces
- Type aliases
- Enums
- Traits (Rust), modules (Ruby), namespaces (C++)
Each symbol becomes a "chunk" stored in SQLite: { name, file, type, line, content }. Auto-indexing runs in a background worker on first load and never blocks the TUI. The walk is capped at 10,000 files (override with CONTEXT_MANAGER_MAX_FILES). You can also trigger a manual re-index anytime with context_analyze, or index a specific path.
The index lives at ~/.cache/opencode/context-manager.sqlite. Three tables:
chunks_fts— an FTS5 virtual table with atrigramtokenizer that enables substring matching. The columnsnameandcontentare full-text indexed;id,file,type, andlineare stored but unindexed (metadata only).file_hashes— MD5 hashes per file, used to skip re-parsing when a file's content hasn't changed.meta— key-value store forprojectRootandindexedAt.
WAL mode is enabled for concurrent reads during writes. The database connection is opened once at plugin startup and reused for all queries.
When the AI calls context_search "auth handler":
- The query is tokenized:
"auth" OR "handler" - SQLite FTS5 runs a MATCH query against the inverted index
- Results are ranked by BM25 (the standard full-text ranking algorithm)
- Top N results are returned as
type name @ file:line
The trigram tokenizer is the key to why this works better than grep: it breaks text into 3-character sequences, so "invoice" matches inside "createInvoice", and "websocket" matches inside "WebSocketHandler". grep can only match literal strings.
When you edit, create, or delete a file, the event hook fires:
- 500ms debounce — coalesces rapid save bursts
- MD5 hash check — if the file content hasn't changed, skip entirely
- Incremental update — delete old chunks for that file (
DELETE FROM chunks_fts WHERE file = ?), parse the new content, insert the new chunks
Deleted files have their chunks and hash removed. The index is always current — you never need to re-run context_analyze after the initial auto-index.
Each chat turn, the experimental.chat.system.transform hook checks if an index exists. If so, it injects a block into the system prompt:
<context-manager>
Code index available: 74 chunks across 16 files.
Indexed: 2026-07-09T03:39:54Z
IMPORTANT: Use the context_search tool to find code locations BEFORE reading files.
This saves tokens — search returns function/class names with line numbers,
so you can read only the specific file and section you need.
</context-manager>
This nudges the AI toward search-first behavior without forcing it.
The bundled skill teaches opencode to classify files into three tiers when context is limited:
| Tier | What the AI keeps | When it applies |
|---|---|---|
| Active files | Full content | Files the user is editing or discussing |
| Dependencies | Signatures + types only | Imported/required files |
| Rest of repo | One-line summary | Everything else |
Token budget: 50% active files / 30% dependency signatures / 20% search results + summaries.
The plugin adds 4 tools that the AI calls directly:
| Tool | Arguments | What it does |
|---|---|---|
context_analyze |
path (optional) |
Re-indexes the project. Runs automatically on first load. Use manually to re-index or target a specific path. |
context_search |
query (required), n (optional, default 10) |
Searches the index via FTS5 + BM25. Returns type name @ file:line ranked by relevance, with substring matching. |
context_stats |
— | Shows what's indexed: project root, timestamp, chunk counts by language, file count. |
context_clear |
— | Deletes the entire index. Use when switching projects or starting fresh. |
| Language | Extensions | What it extracts |
|---|---|---|
| Python | .py |
functions, classes |
| JavaScript / TypeScript | .js .jsx .ts .tsx |
functions, arrow functions, classes, interfaces, types, enums |
| Go | .go |
functions, structs, interfaces |
| Rust | .rs |
functions, structs, enums, traits |
| Java | .java |
classes, methods, interfaces, enums |
| Ruby | .rb |
functions, classes, modules |
| PHP | .php |
functions, classes, interfaces, traits, enums |
| C / C++ | .c .h .cpp .hpp |
functions, classes, structs, namespaces, enums |
| C# | .cs |
methods, classes, interfaces, structs, enums |
plugins/
@madtech-opencode-context-manager-plugin.ts # Plugin entry point — tool + hook wiring
context-manager-loading-shim.ts # Local shim — instant-load toast during npm install
worker-indexer.ts # Background worker for the auto-index
src/
types.ts # Chunk interface, IGNORE/CODE_EXTS constants
store.ts # SQLite layer (FTS5, CRUD, search, BM25)
indexer.ts # Filesystem walker, project indexer, incremental update
prompt.ts # System prompt builder
parsers/
python.ts # pyParse
javascript.ts # jsParse, tsParse
go.ts # goParse
rust.ts # rsParse
java.ts # javaParse
ruby.ts # rbParse
php.ts # phpParse
cpp.ts # cppParse (C + C++)
csharp.ts # csParse
formats.ts # cssParse, htmlParse, dataParse, sqlParse, mdParse
index.ts # PARSERS map
skills/
context-manager/SKILL.md # Bundled skill (auto-copied to ~/.config/opencode/skills/)
scripts/
postinstall.sh # Runs on npm install — drops the shim into the global plugins dir
test/
parsers.test.ts # Parser correctness per language
store.test.ts # SQLite CRUD, FTS5, BM25
indexer.test.ts # walk, indexProject, updateFile
integration.test.ts # End-to-end index → search → verify
contracts.test.ts # Interface/output format stability
143 tests, 0 failures. Run them with bun test.
The plugin is fully decoupled: parsers are pure functions, the SQLite layer accepts a Database instance as a parameter (no globals), and the indexer accepts a database + filesystem path. The entry point wires everything together with opencode's plugin API.
The plugin emits structured logs via client.app.log() with service: "opencode-context-manager-plugin" (and service: "context-manager-shim" for the shim):
| Level | When it fires |
|---|---|
info |
Plugin initialized, project indexed, auto-analyze complete |
debug |
File re-indexed, file removed from index |
warn |
File read failed (permissions, missing) |
Install/uninstall scripts emit newline-delimited JSON to stderr for programmatic consumption.
- opencode >=1.0.0 — declared in
package.jsonunderengines.opencode. Enforced on npm install; serves as documentation for local installs. - Bun — only needed for local-file installs. npm installs are handled by opencode's built-in Bun integration. SQLite is built into Bun (
bun:sqlite) — zero additional dependencies.
MIT