OpenCodeRAG integrates with OpenCode as a plugin, providing semantic code search, image description, experiential memory, and a local decision-model tool (tev1) directly within agent conversations.
The plugin (src/plugin.ts) registers several integration points with OpenCode:
The primary retrieval tool that any OpenCode agent can invoke to search the indexed codebase:
Parameters:
| Param | Required | Description |
|---|---|---|
query |
Yes | Narrow, specific search query |
pathHints |
No | Up to 10 path filters (e.g., ["src/auth/"]) |
languageHints |
No | Up to 10 language filters (e.g., ["typescript"]) |
fileExtensions |
No | Up to 10 dot-prefixed file extensions (e.g., [".ts", ".py"]) — hard filter on file suffix, case-insensitive |
topK |
No | Result count (1–25, default 10) |
Returns: Formatted markdown with file paths, line ranges, score, language, content preview, and descriptions for each relevant chunk.
For autonomous agent workflows, the plugin also registers smaller, focused tools that are more efficient than the general-purpose tool for specific tasks:
| Tool | Purpose | Args |
|---|---|---|
get_file_skeleton |
Structural file overview via tree-sitter AST | filePath (req) |
find_usages |
Find all references to a symbol | symbolName (req), pathHint?, topK? |
describe_image |
Describe an image file with a vision model (live call) | filePath (req), systemPrompt? |
make_decision |
Classify/route/score short text with the local tev1 decision model (when decision.enabled) |
state (req), questions (req) |
Conceptual code search — answers questions like "How does authentication work?" or "Where is the chunking logic?". Uses vector + hybrid keyword search and returns the most relevant code snippets with file paths, line numbers, and relevance scores.
Provides a quick structural overview of a source file without reading its full contents. Uses tree-sitter to parse the file's AST and extract top-level declarations:
- Functions, methods, arrow functions
- Classes, interfaces, types, enums
- Struct, trait, impl blocks (Rust), protocol declarations (Swift)
- CSS rule sets, Markdown headings
Supported languages: TypeScript, JavaScript, Python, Java, Go, Rust, C, C++, C#, Ruby, Swift, Kotlin, CSS, Markdown. Falls back to regex-based extraction or simple line count for unrecognized formats.
The extension → grammar mapping honors chunking.parsers overrides, so a file extension remapped in config (e.g. .c → cpp, or a new .cu → cpp) is parsed with the target parser's recipe here as well. The MCP get_file_skeleton shares the same implementation (src/chunker/skeleton.ts) and behaves identically. See Chunking.
Example output:
Skeleton — src/plugin.ts
24 structural elements (9 function, 4 class, 3 interface, ...)
ƒ createRagHooks L398-L605
ƒ buildRetrievalQuery L193-L207
□ TreeSitterChunker L22-L58
...
Essential before editing a function, type, or variable. Searches the indexed codebase for references to a given symbol and returns line-level matches with 2 lines of surrounding context:
- Keyword index search (fast, precise) — matches the symbol token in the inverted index
- Vector store search (broader) — finds semantically related references
- Line extraction — within each matching chunk, identifies the specific lines containing the symbol (excluding its own definition) and returns them with surrounding context
Output format: Table grouped by file, showing line numbers and code excerpts.
Usages of "createRagHooks" — 5 references across 2 files
### src/plugin.ts (typescript)
| Line | Code |
|------|------|
| 521 | const findUsagesTool = createFindUsagesTool({ ... |
| 615 | return createRagHooks({ cfg, storePath, ... |
### src/__tests__/plugin.test.ts (typescript)
| Line | Code |
|------|------|
| 178 | const hooks = createRagHooks({ ... |
Reads an image file from disk, resizes it, and sends it to the configured vision provider — the description is generated live, not retrieved from the index. The call uses the indexing settings (imageDescription.*) unless imageDescription.onDemand is configured, in which case those overrides (provider, model, prompt, …) apply — see Configuration. An optional systemPrompt steers the description toward specific features.
Parameters:
| Param | Required | Description |
|---|---|---|
filePath |
Yes | Path to the image file (relative or absolute) |
systemPrompt |
No | Optional system prompt steering the description (e.g. "focus on colors and layout") |
Returns: Markdown block with file path, the generated description, and the provider/model that produced it.
Answers classification/decision questions with a local decision model (tev1 via Ollama's /v1/systemone) — a live call, no indexing involved. Registered only when decision.enabled is true; requires Ollama ≥ 0.35 and a pulled model (ollama pull tev1:0.8b or ollama pull tev1:4b). The decision model is not a chat model: it scores a state against named questions and returns probabilities.
Parameters:
| Param | Required | Description |
|---|---|---|
state |
Yes | Text (or JSON string) to judge. Keep it short — the effective context is ~2k tokens. |
questions |
Yes | 1–64 questions, each { id, type, instructions, criteria? }. Types: choice (criteria maps option → description, 2–24 options; add a none option when no listed option may fit), noul (true/false probability; optional { "true": "...", "false": "..." } criteria), score (criteria is an ordered array of 2–24 level descriptions, lowest first). |
Returns: one answer per question — choice with probabilities/confidence, noul (probability true), or score/legend. confidence measures probability concentration, not correctness.
{
"state": "Customer says they were charged twice for the October subscription.",
"questions": [
{
"id": "intent",
"type": "choice",
"instructions": "Which support intent best matches the message?",
"criteria": {
"duplicate_charge": "Charged more than once.",
"cancel": "Wants to end the subscription.",
"none": "None of the listed intents matches."
}
}
]
}Configuration: decision. Inputs are validated before the request (question ids, 2–24 options/levels, state length) so malformed calls fail with an actionable message instead of a garbage answer. CLI equivalent: opencode-rag decide (CLI reference). MCP clients see the tool only when decision.enabled is true.
Optional pre-ask routing (decision.routeBeforeAsking): off by default. When enabled, the runtime system prompt, the AGENTS.md directive, and the make-decision skill instruct the agent to resolve option choices through make_decision before interrupting the user — proceeding when the answer is confident, and asking only when the model is undecided (low confidence) or the choice is preference-based.
OpenCode v2 (2.0.x) offers plugin tools to the agent directly only when they are
registered with options.codemode: false; tools without an explicit
codemode: false are folded into the Code Mode catalog and become reachable
exclusively through the execute runtime's tools.* (every direct call fails
with "No tool named … is currently available"). The plugin therefore registers
all of its tools with codemode: false by default, so search_semantic,
get_file_skeleton, find_usages, describe_image, read, and the quirk
tools are available directly in the session.
To force the code-mode-only behavior (e.g. for restricted agents), opt out via
the plugin options in opencode.json(c):
Note: the setting is read at plugin load — an already running session keeps its
tool snapshot until the plugin is reloaded or OpenCode is restarted. MCP servers
use the same flag (codemode on the server config) if you want their tools
direct as well.
By default the plugin grants its read-only RAG tools (search_semantic,
get_file_skeleton, find_usages, describe_image, the quirk tools, and —
when decision.enabled — make_decision) to the built-in explore agent,
so delegated codebase exploration can use semantic search instead of
grep/glob only. OpenCode ships explore with a deny-all policy (everything
except read, glob, grep, and the web tools); the plugin appends allow
rules for its tool actions via the V2 agent transform, after the shipped rules
(last match wins). An explicit user rule for a tool action — allow, ask, or
deny — is respected and never overridden.
Disable with openCode.exploreAgentTools: false. The grant is read at plugin
load and applies after an OpenCode restart; when disabled, runtime guidance is
also skipped for explore (see section 4).
The plugin captures message.part.updated events to accumulate the last assistant message's text. When you trigger injection via hotkey, the search query combines:
[Last assistant response]
[Current user prompt]
This 2-message query provides the embedding model with conversation context for more relevant code retrieval. On the first turn (no prior assistant message), only the user prompt is used.
RAG context is appended directly to the user's message text (not pushed as a separate part), matching the /doc handler pattern that avoids duplication.
Use the hotkeys:
- Ctrl+Enter — Injects a file suggestion list
- Ctrl+Alt+Enter — Injects full code chunks
opencode-rag init creates .opencode/skills/opencode-rag/SKILL.md — an OpenCode skill that teaches agents the recommended tool workflow. This is the primary discovery mechanism:
- Agents see the skill listed in the
skilltool description - Loading it injects the workflow guidance into the conversation
- Zero ongoing token cost — only loaded when the agent chooses to load it
The skill teaches the workflow: skeleton → find_usages → search → read → edit.
opencode-rag init also creates .opencode/skills/make-decision/SKILL.md — a dedicated skill for the decision-model tool covering when to use make_decision, the three question types, prerequisites, and limits. It is generated regardless of decision.enabled and is loaded on demand like any other skill.
The experimental.chat.system.transform hook prepends a tool list to the system prompt on every message, ensuring agents know the tools are available — even before the index is built. OpenCode V2 passes the session's tool catalog and agent ID to the context hook, so the plugin injects the guidance only when at least one RAG tool is present in that catalog. Restricted built-in agents such as explore (which deny everything except read, glob, grep, and the web tools) and custom permission overrides that hide the plugin tools no longer receive an inert mandate referencing tools they cannot call. When the host sends no catalog, injection stays unconditional except for known tool-less built-in agents. openCode.injectSystemPrompt: false disables the guidance entirely.
The guidance text (mandatory tools, decision tree, proactive triggers, anti-patterns, and conditional quirk-capture rules) is defined in a single source at src/opencode/system-guidance.ts. The same text is used at init time to populate the AGENTS.md directive, ensuring consistency between the runtime system prompt and the always-loaded workspace directive.
When documentationMode.enabled is true, the plugin provides a /doc slash command for documenting the codebase. No agent tools are registered — documentation is driven entirely through the slash command.
Configuration:
| Field | Default | Description |
|---|---|---|
enabled |
false |
Enable documentation mode |
systemPrompt |
(built-in) | System prompt for the documentation agent |
How it works:
-
User types
/doc— Thechat.messagehook intercepts the slash command and returns a list of all undocumented files, grouped by subdirectory. -
Agent picks a subdirectory — The agent reads the file list, chooses a subdirectory, and documents all files within it:
- Calls
get_file_skeleton(filePath)to understand file structure - Calls
readto get full file contents - Adds/updates JSDoc/TSDoc comments on public symbols
- Preserves existing comments
- Calls
-
Agent marks subdirectory complete — When done, the agent types
/doc src/auth/(or similar) to mark that subdirectory as documented. The plugin updates the progress tracker and shows remaining files. -
Progress tracking — Documented files are recorded in
.opencode/rag_db/doc-mode-progress.json. On subsequent sessions, the/doccommand resumes where it left off.
Workflow:
User: /doc
Plugin: ## Documentation — lists all files grouped by subdirectory
Agent: picks a subdirectory, documents files
Agent: /doc src/auth/
Plugin: marks src/auth/* as documented, shows remaining files
Agent: picks next subdirectory, repeats
Config example:
{
"documentationMode": {
"enabled": true,
"batchSize": 5
}
}When wikiMode.enabled is true (default), the plugin injects a system prompt that instructs the host agent to build and maintain a persistent knowledge wiki at .opencode/wiki/. No agent tools are registered — wiki maintenance is driven entirely through the injected system prompt and the /wiki slash command.
Configuration:
| Field | Default | Description |
|---|---|---|
enabled |
true |
Enable wiki mode |
systemPrompt |
(built-in) | System prompt for the wiki maintainer agent |
How it works:
-
System prompt injection — When wiki mode is enabled, the plugin injects the wiki maintenance protocol into the system prompt at session start. The protocol defines the wiki layout (
.opencode/wiki/index.md,log.md,entities/,concepts/), page frontmatter conventions, and the three operations: Ingest (extract and synthesize new knowledge), Query (route through the index first, fall back tosearch_semantic), and Lint (health-check for orphans, stale pages, contradictions). -
Agent writes the wiki — The host agent creates and maintains all wiki pages during coding sessions, following the injected protocol. The user never writes wiki pages directly.
-
/wikistatus — Shows the current state of.opencode/wiki/: page count, subdirectories, whetherindex.mdandlog.mdexist, and the latest log entry. -
/wiki lint— Instructs the agent to health-check the wiki for orphan pages, stale entries (source drift), contradictions, and missing cross-references. Findings are appended tolog.md. -
/wiki seed— Instructs the agent to generate the initial wiki from existing sources: README, file structure viaget_file_skeleton, common conventions from code patterns. Createsindex.md,log.md, and initial entity/concept pages.
Workflow:
User: /wiki seed
Agent: scans README, file tree, code patterns → creates initial wiki pages
User: /wiki
Plugin: shows wiki status (pages, index, log, last entry)
Agent: maintains wiki during normal coding sessions (ingest on learning)
User: /wiki lint
Agent: health-checks wiki → appends findings to log.md
Config example:
{
"wikiMode": {
"enabled": true
}
}When openCode.readOverride is true:
- The plugin registers a
readtool that shadows OpenCode's built-in read - Always returns full file contents from disk
- When RAG chunks are available for the file (score ≥ threshold), they are appended as "Related code chunks" after the file content
- If retrieval fails, the file is still returned without RAG context
- If no relevant chunks are found but the file has indexed chunks, related files are suggested
The plugin automatically captures session events for token usage analysis:
- Event hook (
event): Logs every OpenCode event — messages, tool calls, steps, session status - RAG context hook (
chat.message): After each retrieval, logs chunk count, context tokens (tiktoken BPE), top score, and retrieval time
Events are stored as JSONL at ${storePath}/eval-sessions/${sessionID}.jsonl.
Token counting: RAG context tokens are counted per-chunk using tiktoken (cl100k_base BPE encoding) at injection time. This provides accurate counts for code, which tokenizes differently than prose (~4 chars/token is a rough heuristic, but BPE is 20-40% more accurate).
See Evaluation documentation for CLI commands, analysis interpretation, and benchmarking.
When memory.enabled is true, the plugin provides persistent, cross-session memory of non-obvious facts — gotchas, preferences, decisions, and environment constraints — that the agent can recall and extend over time. Quirks live in the same vector store as code chunks and are recalled via semantic search.
Tools exposed to the agent:
| Tool | Use when | Example |
|---|---|---|
recall_quirks |
You hit an error or need to remember a gotcha, preference, or decision from past sessions | recall_quirks("lancedb type casting") |
add_quirk |
You just discovered a non-obvious fact, workaround, or convention worth remembering | add_quirk("npm needs --legacy-peer-deps", { type: "gotcha", tags: ["installation"] }) |
update_quirk |
A recalled quirk is outdated or wrong — fix its content, type, tags, confidence, or source ref | update_quirk("quirk:abc…", { content: "corrected fact", type: "decision" }) |
delete_quirk |
A quirk is fixed, obsolete, or no longer applies — remove it so it stops being recalled/injected | delete_quirk("quirk:abc…") |
How it works:
-
Add a quirk —
add_quirk(content)embeds the text, stores it as aquirk-kind chunk in the vector store + keyword index, and appends an audit entry toquirks.jsonl. Before writing, an immutable trust monitor (src/quirks/monitor.ts) rejects content matching blocked destructive patterns (e.g.rm -rf,force push,bypass security,disable lint). Quirk types:gotcha,preference,decision,environment-constraint. -
Recall a quirk —
recall_quirks(query)runs a hybrid (vector + keyword) search filtered toquirkchunks, then re-weights results by confidence and returns the top matches. Results respectmemory.minConfidenceandmemory.recallMinScore(default0.72). Whenmemory.autoInjectistrue, relevant quirks are automatically injected into the prompt on every user message (via both thechat.messagehook and the system prompt transform), both gated by the strictermemory.autoInjectMinScore(default0.75) - injected quirks occupy context every turn, so only strongly relevant ones qualify. Scores are raw vector similarities ((cosine + 1) / 2, 0-1), so the threshold acts as a semantic gate - e.g.0.85admits only strongly related notes. The recall query combines the agent's previous response with the current user message for maximum relevance. A latency budget (memory.autoInjectLatencyBudgetMs, default 2000ms) prevents slow embedders from blocking message processing. To avoid polluting the context window, each quirk is injected at most once per session — once recalled, it is filtered out from all subsequent auto-injections across both hooks, and at mostmemory.autoInjectTopK(default1) quirks are injected per turn. Each result shows its quirk ID — the ID is the argument forupdate_quirkanddelete_quirk. -
Update a quirk —
update_quirk(id, patch)replaces the stored content/type/tags/confidence/source ref. Whencontentchanges, the new text must pass the trust monitor, the quirk is re-embedded, and the vector-store chunk + keyword index entry are replaced under the same ID; thequirks.jsonlaudit entry is rewritten in place. Metadata-only patches (type/tags/confidence/source ref) skip re-embedding. -
Delete a quirk —
delete_quirk(id)removes the quirk chunk from the vector store + keyword index and its audit entry fromquirks.jsonl, so it is no longer recalled or auto-injected. Deleting (rather than updating) is the right call when a quirk is simply wrong or superseded — it keeps memory free of contradictions. -
Decay & lint — Optional confidence decay ages quirks over time (
memory.decay.halfLifeDays).opencode-rag quirk lintflags low-confidence, stale, and near-duplicate quirks;opencode-rag quirk test "<text>"reports whether a similar quirk has already been appended. -
Auto-capture (three modes) — Reduce reliance on agent discipline by enabling automatic quirk extraction:
- Passive capture (
memory.passiveCapture): After each completed agent turn, the description LLM scans the exchange (user request + assistant response + tool results) for error signals and extracts concise quirks. UsesdetectErrorSignalregex to gate extraction andrecallQuirks+lexicalSimilarityto skip near-duplicates. - Prompt enforcement (
memory.promptEnforcement): Replaces the soft "add_quirk when you discover a non-obvious fact" nudge with mandatory rules listing concrete triggers (build/test/type errors fixed, undocumented constraints, workarounds, etc.) — including mandatoryupdate_quirk/delete_quirkhygiene for outdated stored quirks. - Session-end extraction (
memory.sessionEndExtraction): On non-message lifecycle events (session close), runs a full-transcript extraction pass. Both passive capture and session-end extraction requiredescription.enabled: trueand reuse the same LLM model with a quirk-specific system prompt. - Dedup: Candidate quirks are deduped against existing ones via lexical similarity (
memory.autoCaptureDedupThreshold, default 0.85). All auto-captured quirks pass the immutable trust monitor before storage.
- Passive capture (
-
Relevance gate for auto-injection — After vector recall, each candidate quirk must share at least
memory.autoInjectMinTokenOverlap(default2) word tokens (≥3 chars) with the user's current message alone — not merely the combined assistant+user recall query. This stops meta-quirks (quirks about quirks themselves, e.g. "Quirk content length is guided by…") from being injected into unrelated tasks where they only matched the prior assistant text in the recall query. SetautoInjectMinTokenOverlapto0to disable the gate (restores the old behavior of pure semantic matching). -
Tool-error memory (recall + auto-create) — When a tool call fails, the plugin turns the failure into memory:
- Recall & inject (
memory.injectOnToolError, defaulttrue): The failure is turned into a recall query (Tool "<name>" failed: <error>) and matched against stored quirks. Matches that clearmemory.toolErrorMinScore(default0.7— the "confident match" bar) are injected into the next model request. The failure is also appended to the auto-capture pipeline, so recurring errors can be picked up by the LLM-based extraction modes. - Auto-create (
memory.autoCreateOnToolError, defaulttrue): When no stored quirk covers the failure, the plugin writes one. The content is a deterministic template —Tool \write` failed:plus a short classified cause for well-known error classes (unknown tool, missing path, permissions, schema mismatch, rate limit, timeout) — so no LLM is involved and the path works even withdescription.enabled: false. The new quirk is taggedtool-error/auto-created/tool:, typedgotcha`, and stored with a lower confidence (0.6). - Two bars, one recall: The recall runs once at the floor (
memory.toolErrorCreateMinScore, default0.7). A hit that clears the injection bar (memory.toolErrorMinScore, default0.7) is injected. Only when the recall yields no candidate at all is a new quirk created. - The duplicate check is exact, not fuzzy: A new quirk is written only when nothing in memory covers the failure. The check is (a) a semantic match at or above the floor, or (b) a metadata match — a tool-error quirk with the same
tool:<name>andcause:<class>tags. A loose similarity floor cannot do this job: measured on a real workspace, an error query scores 0.55–0.63 against arbitrary stored quirks, so any floor low enough to catch a genuine duplicate also catches noise — which suppressed creation on every real failure. - Cause classes: The error is classified into one of six deterministic classes (unknown tool · schema mismatch · permissions · path · rate limit · timeout), recorded as the
cause:<class>tag and appended to the quirk as a one-line cause. Unclassified errors get the raw text only. - Persisted, not injected: The created quirk is recorded for future sessions. It is not injected into the turn that produced the failure — the agent already saw the raw error. Set
memory.toolErrorInjectCreatedtotrueto change that. - Bounded: A per-session budget (
memory.toolErrorCreateMaxPerSession, default2) and a per-signature cooldown (memory.toolErrorCreateCooldownMs, default 10 min) cap how much a failing session can write. The create step runs under a latency budget (memory.toolErrorCreateLatencyBudgetMs, default2000). - Trust monitor: Creation goes through the same immutable trust monitor as
add_quirk. An error text containing a blocked pattern (e.g. a shell error echoingrm -rf …) is not stored — the rejection is logged and the session continues. - No context pollution: Quirks tagged
tool-errorare excluded from the generic auto-injection paths, so a raw error dump never leaks into an unrelated turn. They surface only when a matching tool failure occurs.
- Recall & inject (
-
Proactive tool gotchas (prevention) — The recall above only helps after a failure. To stop the failure from happening at all, the plugin injects the known tool gotchas into the system prompt, before the model picks a tool:
- Where:
experimental.chat.system.transform(OpenCode V2:ctx.session.hook("context")). This is the only pre-decision injection surface —tool.execute.beforecan only mutate the tool arguments and is not even forwarded by the V2 adapter, andtool.transformcan only add tools, not modify a built-in tool's description. - What: The gotchas whose tool is in the session's tool catalog — the exact set of tools the model may call (measured live: 27 tools incl.
write,edit,shell,read). A gotcha about a tool that is not callable is not actionable, so it is left out. - Only durable causes:
unknown-tool,schema,permissionsandpathare surfaced proactively. A transient failure (rate limit, timeout) is never warned about in advance — the next call would not fail the same way, and the warning would be misleading. - Bounded: At most
memory.toolErrorProactiveMaxItems(default5) gotchas, injected on every model request. Toggle withmemory.toolErrorProactiveInject(defaulttrue). - Net effect: the first occurrence of a failure is unavoidable (no knowledge exists yet) — but it is recorded, and every later session is warned before the call.
- Where:
Config example:
{
"memory": {
"enabled": true,
"autoInject": true,
"minConfidence": 0.5,
"recallMinScore": 0.72,
"autoInjectMinScore": 0.75,
"autoInjectTopK": 1,
"autoInjectMinTokenOverlap": 2,
"injectOnToolError": true,
"toolErrorMinScore": 0.7,
"autoCreateOnToolError": true,
"toolErrorCreateMaxPerSession": 2,
"toolErrorCreateCooldownMs": 600000,
"toolErrorCreateMinScore": 0.7,
"toolErrorProactiveInject": true,
"toolErrorProactiveMaxItems": 5,
"decay": { "enabled": true, "halfLifeDays": 30 }
}
}See Configuration: memory and CLI Reference: quirk.
OpenCode Runtime
│
┌──────────────┴──────────────┐
│ │
ragPlugin() BackgroundIndexer
│ │
createRagHooks() file watcher
│ (chokidar/git)
┌───────┼───────────┐ debounced scheduler
│ │ │ │
Tool chat.message read periodic timer
hook hook override (git only)
OpenCode V2 (≥2.0) validates every plugin module's default export against the schema
{ id, effect } or { id, setup }. A V1-only shape ({ id, server } or { id, tui })
fails the load with:
Plugin must export a default definition with an id and an effect or setup function
(SchemaError: Missing key at ["default"]["effect"], Missing key at ["default"]["setup"])
The plugin therefore default-exports the V2 shape — id + setup (OpenCode 1.x
support was dropped in v2.0.0):
// src/plugin-entry.ts (server plugin)
import { Plugin } from "@opencode/plugin";
export const id = "opencode-rag-plugin";
async function setup(ctx: Plugin.Context) {
return registerRagPluginV2(ctx); // registers V1-factory hooks via the context adapter
}
export default Plugin.define({ id, setup });// src/tui.ts (TUI plugin)
const plugin = {
id: "opencode-rag-plugin:tui",
setup, // registers sidebar/keybindings when the TUI role loads it; no-op for the server role
};
export default plugin;Key requirements:
- The
defaultexport MUST be an object withidandsetup(oreffect) — V2 rejects bare functions and V1-era{ id, server }/{ id, tui }shapes setupmust be safe under the server role: the server loads the entrypoints it discovers under.opencode/plugins/and callssetupwith a server context that has noui/keymap; the TUI-only module detects this and returns without registering anything
Migration note: OpenCode 1.x (
{ id, server }/{ id, tui }entrypoints) is no longer supported as of v2.0.0. On V2, an old shape produces the schema error above.
Do NOT register the plugin via "plugin": ["opencode-rag-plugin"] in OpenCode config. Instead, rely on plugin-directory auto-discovery:
- Run
opencode-rag initto create the plugin directory.opencode/plugins/opencode-rag/and.opencode/skills/opencode-rag/SKILL.md - The directory holds two entrypoints, each a bare re-export from the workspace
node_modules/:
// .opencode/plugins/opencode-rag/index.js — server (business) entrypoint
import plugin from "../../node_modules/opencode-rag-plugin/dist/plugin-entry.js";
export default plugin;// .opencode/plugins/opencode-rag/tui.js — CLI (TUI) entrypoint
import plugin from "../../node_modules/opencode-rag-plugin/dist/tui.js";
export default plugin;OpenCode 2 resolves a plugin directory as index.* (server entrypoint) + tui.* (CLI
entrypoint beside it), and the terminal UI only discovers directories — a flat
.opencode/plugins/rag-tui.js would only ever load in the server role, which is why the
sidebar disappeared on V2. init therefore also removes the V1-era flat files
(rag-plugin.js, rag-tui.js, .opencode/tui.json) when their content is exactly what an
earlier init generated; hand-written files and custom configs are left untouched.
A bare default re-export satisfies both loaders because the shape ({ id, setup }) lives in
the built module. dist/tui.js imports @opentui/solid lazily, only when the TUI role
calls setup() — a plain registry install without the (optional) OpenTUI peers still loads
the module fine in the server role.
The plugin spawns one BackgroundIndexer per workspace directory (via src/watcher.ts):
- File watcher: Monitors file changes in the workspace (backend configurable via
autoIndex.watcher)- chokidar (default): Real-time filesystem events, minimal overhead
- git: Poll-based diff against last indexed commit
- Debounced scheduler: Waits
autoIndex.debounceMs(2000ms) after changes before re-indexing (disabled by default; enable viaautoIndex.enabled) - Periodic timer: Only for
gitbackend — runs a full pass everyautoIndex.intervalMs(default 5 min). Not used withchokidar(real FS events are sufficient) - Error recovery: Detects LanceDB corruption and triggers auto-rebuild
- Status file: Writes
watcher-status.jsonto the store path for observability — running state, last successful/failed pass, consecutive failures, pending-change timestamp, and the last error.statusand the TUI show a stale warning when passes fail or changes arrived after the last successful pass.
One watcher per workspace: the plugin claims a cross-process lock at
{storePath}/watcher.lock before starting a watcher. If another process
already runs the watcher (a second OpenCode session, or opencode-rag index --watch), the new instance stays dormant — no chokidar watcher, no initial
pass — and takes over ~60s after the owning process exits. This prevents
multiple watchers from racing on the same store.
The TUI plugin (src/tui.ts) registers a settings panel in the OpenCode sidebar:
| Category | Settings |
|---|---|
| Retrieval | topK, minScore, maxChunks, hybrid search toggle |
| Auto-Indexing | Watcher enable/disable, debounce delay, backend selection |
| Chunking | chunkOverlap, maxSvgSizeBytes, nodeTypes JSON editor |
| Embedding | Model picker dropdown (populated from OpenCode's registered providers) |
| LLM Descriptions | Enable/disable toggle, model picker dropdown |
- Model picker: Groups models by provider name, sorted alphabetically, with "Custom…" option for manual entry
- Auto-sets provider and base URL when a model is selected
- Runtime overrides: Settings are persisted to
${storePath}/runtime-overrides.jsonand take precedence overopencode-rag.json - Status sidebar: Shows chunk count, provider/model info, last indexed time, watcher state
- Keyboard shortcut:
Ctrl+Shift+Ropens the settings dialog - Auto-refresh: Status refreshes every 30 seconds
| Shortcut | Action |
|---|---|
| Ctrl+Enter | Retrieve and append a relevant file list to the prompt |
| Ctrl+Alt+Enter | Retrieve and append full code chunks to the prompt |
| Ctrl+Shift+R | Open the settings dialog |
Both shortcuts read the current prompt text combined with the previous assistant response (if any) as the search query. If the prompt is empty, a toast reminds you to type first — no dialogs are opened. Keybindings are configurable in the settings menu under "Keybindings".
This occurs when OpenCode's Bun runtime tries to load the plugin via the "plugin" key in OpenCode config, causing module resolution issues.
Fix:
- Ensure the plugin is loaded via
.opencode/plugins/opencode-rag/auto-discovery, NOT via"plugin"config key - Run
opencode-rag initto regenerate the workspace-local plugin files - Remove stale
"plugin"entries from all OpenCode config files
node --input-type=module -e \
"const m = await import('opencode-rag-plugin'); console.log(typeof m.default?.setup)"function. For the TUI module substitute
.opencode/plugins/opencode-rag/tui.js; its default must expose id and setup.
Plugin load failures (with a ref=err_… reference) are logged to
~/.local/share/opencode/log/opencode.log — grep for failed to load plugin and read the
cause= field; import-time errors surface before schema validation, so the logged cause is
authoritative (a schema error can hide behind an earlier import error).
When using OpenAI as the embedding or description provider, the plugin auto-resolves the API key from OpenCode's own provider configuration if not specified in opencode-rag.json:
- Searches
.opencode/opencode.json,opencode.json,~/.config/opencode/opencode.jsonc - Strips JSONC comments before parsing
- Finds the
apiKeyfor theopenaiprovider
{ "plugins": [ { "package": "opencode-rag-plugin", "options": { "codemode": true } } ] }