Files
t0sakiandTrent Telfer 12076389da feat(opencode-plugin): support OpenCode v2 (#5341)
* feat(opencode-plugin): support the OpenCode v2 plugin API

OpenCode 2 does not run v1 hook plugins. Keep the existing server()
entrypoint and add setup() so one package serves both, adapting MCP,
recall, capture, and lifecycle events to the v2 shapes.

Refs #5226

* fix(opencode-plugin): align v2 lifecycle handling

* docs(opencode-plugin): document v2 support

* fix(opencode-plugin): preserve capture across v2 compaction

* fix(opencode-plugin): drop reasoning and keep state on failed v2 executions

v2 capture turned reasoning blocks into assistant text, so chain-of-thought
reached memory extraction and inflated pending tokens. v1 parts and the shared
capture filter drop reasoning; v2 now does the same.

A failed v2 execution ends one turn, not the session. Mapping it to v1's
session.error committed and deleted the session state, so a later capture
without a cursor resent every earlier turn. Failed executions now take the
same idle path as succeeded and interrupted ones.

* fix(opencode-plugin): scope v2 capture to the plugin location and persist its cursor

One OpenCode v2 service runs a plugin instance per location, and the plugin
event stream carries every location's events. Each instance therefore
captured and committed every session, storing each message once per open
project. Handle only lifecycle events whose session belongs to this
location, resolved from session.created or ctx.session.get because execution
events carry no envelope location.

The capture cursor lived only in memory, and the shared session state file
is rewritten by every instance, so a reloaded or evicted instance could
resend the transcript since the last compaction. Keep the cursor in plugin
storage and remove it when the session is deleted.

* docs(opencode-plugin): describe v2 commit points and location scoping

---------

Co-authored-by: Trent Telfer <4094016+ttelfer@users.noreply.github.com>
2026-09-24 15:35:07 +08:00

12 KiB

Install the Unified OpenViking OpenCode Plugin

This plugin adds one unified OpenViking plugin for OpenCode:

  • OpenViking MCP tools for memory, resources, and code context
  • Long-term memory, session synchronization, lifecycle commit, and automatic recall

This is the only OpenCode plugin example maintained in this repository. It does not install skills/openviking/SKILL.md, and it does not require the agent to use the ov command. Model tools are provided by the same stdio MCP proxy used by the Claude Code and Codex memory plugins.

Prerequisites

Prepare the following first:

  • OpenCode 1.15.7+ or OpenCode 2.0.15+
  • OpenViking HTTP Server
  • Node.js 18+
  • A valid OpenViking API key if authentication is enabled on the server

Start OpenViking first:

openviking-server --config ~/.openviking/ov.conf

Check the service:

curl http://localhost:1933/health

Installation Method 1: Published Package

Normal users are recommended to enable it through OpenCode's package plugin mechanism:

{
  "plugin": ["@openviking/opencode-plugin"]
}

This plugin entry works on both v1 and v2. OpenCode v2 normalizes it to the native plugins form at load time, so no manual config migration is required.

Installation Method 2: Source Install

Use this method for development, debugging, or PR testing. OpenCode's recommended plugin directory is:

~/.config/opencode/plugins

Run the following commands from the repository root:

node examples/memory-plugin-shared/sync.mjs
mkdir -p ~/.config/opencode/plugins/openviking
cp examples/opencode-plugin/wrappers/openviking.js ~/.config/opencode/plugins/openviking.js
cp examples/opencode-plugin/index.mjs examples/opencode-plugin/package.json ~/.config/opencode/plugins/openviking/
cp -r examples/opencode-plugin/lib ~/.config/opencode/plugins/openviking/
cp -r examples/opencode-plugin/servers ~/.config/opencode/plugins/openviking/

sync.mjs generates lib/shared/, the shared modules the plugin and its MCP proxy import. That directory is not in git, so run it before copying, and again after every git pull.

After installation, the layout should look like this:

~/.config/opencode/plugins/
├── openviking.js
└── openviking/
    ├── index.mjs
    ├── package.json
    ├── lib/
    └── servers/

The top-level openviking.js forwards the first-level .js entry that OpenCode can discover to the actual plugin directory:

export { OpenVikingPlugin, default } from "./openviking/index.mjs"

This wrapper is only for source installs with the directory layout shown above. npm package installs load index.mjs directly through package.json. Use the .js wrapper for source installs; OpenCode's local plugin scanner discovers JavaScript/TypeScript plugin files.

If you install through an npm package, you can also use examples/opencode-plugin as a normal OpenCode plugin package.

Configuration

Behaviour knobs live in the shared client configuration file:

~/.openviking/ovcli.conf

Example configuration:

{
  "url": "http://127.0.0.1:1933",
  "api_key": "your-api-key-here",
  "plugin": {
    "recallLimit": 6,
    "opencode": {
      "enabled": true,
      "mcpEnabled": true,
      "timeoutMs": 30000,
      "repoContext": true,
      "repoContextCacheTtlMs": 60000,
      "autoRecall": true,
      "scoreThreshold": 0.35,
      "recallMaxContentChars": 500,
      "recallPreferAbstract": true,
      "recallTokenBudget": 2000,
      "minQueryLength": 3,
      "commitTokenThreshold": 20000,
      "commitKeepRecentCount": 10,
      "profileTokenBudget": 10000,
      "skillCatalog": true,
      "skillCatalogTokenBudget": 1200,
      "resumeContextBudget": 32000
    }
  }
}

Keys in plugin apply to every harness; keys in plugin.opencode apply to this one and override them. Resolution is OPENVIKING_* environment variables → the workspace's .openviking/config.json, .openviking/config.local.json and machine registry entry → plugin.opencode → plugin → built-in defaults. Every knob, with its type, default, range, environment variable and accepted older spellings, is declared in examples/memory-plugin-shared/lib/config-schema.mjs.

recallLimit is a legacy quota-scaling input, not a final result cap. Explicit values from 1 through 5 produce an effective total quota of 6 because each coding category keeps one retrieval slot.

The first message of each session carries a hidden <openviking-context source="session-start"> block with your profile.md, the preferences/ and entities/ memory indexes, and an <available-skills> catalog: your own skills first, then the account-shared ones under viking://agent/skills, leaving out a shared skill that has the same name as one of yours. profileTokenBudget covers the profile and memory indexes; the catalog has its own budget, skillCatalogTokenBudget (default 1200, OPENVIKING_SKILL_CATALOG_TOKEN_BUDGET). When the descriptions do not fit, the catalog lists names only (with a ... +N more tail if even the names do not all fit), and when not even one name fits, a one-line count. skillCatalog: false (OPENVIKING_SKILL_CATALOG=0) or a budget of 0 turns the catalog off; with no skills, or on a server without GET /api/v1/skills, it is left out.

It is recommended to provide the API key through an environment variable instead of writing it into the configuration file:

export OPENVIKING_API_KEY="your-api-key-here"

API keys are resolved from environment variables or ~/.openviking/ovcli.conf and sent as Authorization: Bearer ... by both hooks and the MCP proxy. account and user are trusted-mode identity headers sent as X-OpenViking-Account and X-OpenViking-User; an api_key server reads both out of the key, so the plugin withholds them there. peerId is sent as X-OpenViking-Actor-Peer on data-plane memory/resource requests; captured session messages store it as body peer_id.

OPENVIKING_API_KEY, OPENVIKING_ACCOUNT, OPENVIKING_USER, and OPENVIKING_PEER_ID take precedence over the corresponding values in ovcli.conf.

For advanced setups, use OPENVIKING_CLI_CONFIG_FILE to point to an ovcli.conf at another path.

Hook-only mode

If another MCP server already exposes OpenViking, set the bundled MCP registration to false while keeping this plugin's lifecycle hooks active:

{
  "plugin": {
    "opencode": { "mcpEnabled": false }
  }
}

Repository context, automatic recall, message capture, and lifecycle commits remain enabled. This does not add or overwrite OpenCode's mcp.openviking entry.

Verify

Restart OpenCode after changing plugin or OpenViking configuration.

In a new OpenCode session, ask the agent to browse OpenViking memory or search for a known indexed resource. The plugin should expose the OpenViking MCP server, with tools namespaced by OpenCode as openviking_*:

  • openviking_search, openviking_find
  • openviking_read, openviking_list, openviking_tree, openviking_grep, openviking_glob
  • openviking_remember, openviking_write, openviking_edit, openviking_add_resource, openviking_add_skill
  • openviking_list_watches, openviking_cancel_watch, openviking_forget, openviking_health

If anything looks wrong, check the runtime files:

ls ~/.config/opencode/openviking/
tail -n 100 ~/.config/opencode/openviking/openviking-memory.log

For a local server, also confirm OpenViking is reachable:

curl http://localhost:1933/health

Available MCP Tools

The plugin registers OpenViking's stdio MCP proxy through OpenCode config. The server's real tools/list response is the source of truth; current OpenViking servers expose:

  • openviking_search: deep semantic retrieval across memories, resources, and skills; use mode="context" for balanced, injection-ready context.
  • openviking_find: fast semantic retrieval.
  • openviking_remember: store important facts or decisions for memory extraction.
  • openviking_read: read one or more viking:// files.
  • openviking_list: list a viking:// directory.
  • openviking_tree: show a viking:// directory tree.
  • openviking_grep: exact text or regex search.
  • openviking_glob: glob file matching.
  • openviking_write: create, overwrite, or append to a viking:// file.
  • openviking_edit: exact string replacement in a viking:// file.
  • openviking_add_resource: add a URL, local file, sitemap, or feed.
  • openviking_add_skill: create or replace a skill from its full SKILL.md text (data), or install one from a Git URL or a local SKILL.md, skill directory, or .zip (path); target_uri="viking://agent/skills" shares it with the account.
  • openviking_forget: delete a viking:// URI after explicit user confirmation.
  • openviking_list_watches / openviking_cancel_watch: inspect or cancel resource watches.
  • openviking_health: check OpenViking server health.

Usage guidance:

  • Use openviking_search for conceptual questions.
  • Use openviking_grep for exact symbols, function names, class names, or error strings.
  • Use openviking_glob to enumerate files.
  • Use openviking_read to read content.
  • Use openviking_list to explore directory structure.
  • Before following a skill from <available-skills>, read its SKILL.md with openviking_read; create, install, or share a skill with openviking_add_skill.
  • Before deleting anything, obtain explicit user confirmation first; then call openviking_forget.
  • If an agent tries to use OpenCode's local read, glob, or grep tools on a viking:// URI, the plugin blocks that call and points it to the MCP tools.
  • A bash command that contains a viking:// URI still runs; the plugin appends a notice pointing to the MCP tools to its output, which the agent can ignore when the URI is intentional.

Local Files with openviking_add_resource

openviking_add_resource supports three input types:

  • Remote http(s) URL: directly calls /api/v1/resources
  • Local file path: first calls /api/v1/resources/temp_upload, then adds the resource using the returned temp_file_id
  • file:// URL: handled as a local file

Relative paths are resolved against the current OpenCode project directory. Examples:

openviking_add_resource(path="https://example.com/spec.md", to="viking://resources/spec")
openviking_add_resource(path="./docs/notes.md", to="viking://resources/notes.md")
openviking_add_resource(path="file:///home/alice/project/notes.md", description="project notes")

Automatic zip upload for local directories is not supported yet. Passing a directory will return a clear error.

Runtime Files

By default, the plugin writes runtime files to:

~/.config/opencode/openviking/

Possible files include:

  • openviking-memory.log
  • openviking-session-state.json

You can change this directory with dataDir in plugin.opencode.

These are local runtime files and should not be committed to the repository.

Troubleshooting

Issue What to check
Plugin does not load For package installs, confirm ~/.config/opencode/opencode.json contains @openviking/opencode-plugin; for source installs, confirm ~/.config/opencode/plugins/openviking.js exists
Load fails with a missing lib/shared/*.mjs module The source copy was made without running sync.mjs first. Run node examples/memory-plugin-shared/sync.mjs from the repository root and copy lib/ again
MCP tools call the wrong server Check ~/.openviking/ovcli.conf, or set OPENVIKING_* env vars / OPENVIKING_CLI_CONFIG_FILE to the intended config path
401 / 403 from OpenViking Verify OPENVIKING_API_KEY; for trusted-mode deployments, also verify OPENVIKING_ACCOUNT and OPENVIKING_USER
Recall is empty Confirm OpenViking has indexed memories/resources and autoRecall is true
Local openviking_add_resource fails Pass a file path, not a directory; local directories are not uploaded automatically yet