* fix(codex): read the MCP proxy's connection from the hooks' loadConfig
The proxy resolved url, api_key, account and user through the bare
credential chain while taking everything else from loadConfig(). Since
the loader became buildPluginConfig() it adds layers that chain never
sees: ovcli.conf's plugin.codex apiKey/accountId/userId, and ov.conf's
codex.apiKey when ovcli.conf names only the server. With either, hooks
authenticated and every MCP tool call went out without a key.
Codex also hands a stdio MCP server only the env vars .mcp.json lists,
and OPENVIKING_AUTH_MODE was not one of them, so an env-set auth mode
decided the identity headers for hooks but not for MCP calls.
* fix(dsh): forward the resolved auth mode and timeout to the MCP proxy
The proxy runs as a child whose env DSH scrubs, so the parent forwards
what it resolved. It forwarded the endpoint, key, account, user and
peer but not the auth mode or request timeout, so a Cordis patch that
set either configured the in-process runtime and not the MCP calls.
The proxy now also takes its credential source and watched paths from
the resolved config instead of a second credential-chain call, and a
shared test keeps every proxy that ships beside hooks off that chain.
* refactor(shared): one connection resolver for hooks and the MCP proxy
The credential chain lived in two layers. `resolveOpenVikingCredentials()`
could not read ovcli.conf's `plugin.<harness>` keys, the ov.conf harness
fallback or the root-key tail; `buildPluginConfig()` patched those in, and
any caller that used the lower layer alone resolved a different key and
identity than the hooks did.
`resolveConnection(harness, { env, files, hostInput, rootKeyFallback })`
now answers server, key, identity and auth mode in one place, reading only
host input, the environment and the two ~/.openviking files. The hook
loader and `buildProxyConnection()` both consume it, and the old
two-layer entry points (`resolveOpenVikingCredentials`, `resolveAuthMode`,
the credentials.mjs CLI) are gone so a half-resolved chain cannot be
written again.
Behaviour is unchanged for every hook harness (checked field by field
against the previous implementation over thousands of generated file/env
combinations). Two deliberate additions: the portable agent-plugins proxy
now honours ovcli.conf's `plugin` connection keys and ov.conf's harness
section like every other harness, and dsh hands the host's `authMode`
(or `auth_mode`) over as host input, ranking it with the host's endpoint,
key and identity.
* fix(shared): a forced env credential source reads only the environment
`OPENVIKING_CREDENTIAL_SOURCE=env` is documented as "env vars only", but
only the url honoured it: the key, account, user, ovcli.conf's actor peer
and the auth mode still fell through to ovcli.conf, its plugin keys,
ov.conf and the root key when the variable was unset. A process that
exported an empty key to mean "no key" was silently handed whatever the
files held.
Forced to `env`, the connection now reads no file and an unset variable
stays empty; the url defaults to http://127.0.0.1:1933. The `peerId`
setting keeps its own layers. The doctor labels that mode instead of
pointing at files the chain skipped.
* refactor(shared): one proxy-config mapper and one forwarded-env list
Each proxy entrypoint copied a dozen fields out of its loader by hand,
under two sets of names, and the copies had drifted. What the proxy
process must be handed was a second hand-kept list, in Codex's
`.mcp.json` and in its test.
`toMcpProxyConfig(cfg, options)` maps a resolved loader or proxy
connection to the proxy config once. `MCP_PROXY_ENV_VARS` names every
variable that changes what a proxy sends; Codex's `env_vars` is now
checked against it, which adds the missing `OPENVIKING_STATE_DIR`.
* refactor(plugins): every loader takes an env, every proxy exports readProxyConfig(env)
The six MCP proxy entrypoints now reduce to one line: resolve through the
harness's own loader (or `buildProxyConnection` for the hook-less package)
and hand the result to `toMcpProxyConfig`. Every one exports
`readProxyConfig(env)`, and the codex, claude-code, opencode and agent-hook
loaders accept an injected env, so a test can drive a hook and its proxy
from the same inputs without touching process.env.
Mapping through one function fixes what the hand copies had lost: the
Claude Code and DSH proxies never passed `mcpUrl`, so
`OPENVIKING_MCP_URL` moved the hooks and left the tools behind.
The source guard now requires the shared mapper and the exported reader
in every proxy, and `buildProxyConnection` reports its two config paths
instead of a watch list of its own.
* fix(dsh): forward the resolved connection to the MCP proxy
DSH starts its MCP subprocess with the parent's environment minus
credential-shaped names (`/KEY|PASSWORD|SECRET|TOKEN/i`), so the bundle
forwards what it resolved. It forwarded the values but not the mode, and
only the non-empty ones:
- A child that receives `OPENVIKING_URL` runs the chain unpinned. Where
the parent's chain was pinned to an ovcli.conf that names only a url,
the parent sent no key while the proxy fell through to ov.conf's
`server.root_api_key`, so the tools reached the server as root while
the hooks were anonymous.
- `OPENVIKING_ACCOUNT`, `OPENVIKING_USER` and `OPENVIKING_PEER_ID` survive
DSH's scrub, so a value the parent's chain ignored filled the gap in
the child and went out as an identity header.
- With no peer to forward, the proxy derived one from its own launch
directory and sent an actor peer the runtime did not.
`forwardConnectionEnv(connection)` now writes every credential variable,
the empty ones too, with the forced `env` source, so the child reads no
file and resolves exactly the parent's url, MCP url, key, identity, auth
mode and peer. The proxy takes its peer from that environment only.
`buildMcpConfig` moves to `mcp-env.mjs`, which carries no host
dependency, so shared tests can build the child environment without the
DSH bridge.
* test(shared): prove the proxy and the hooks resolve one connection
The existing guards checked shape — that a proxy called the shared
builder — never that it reached the server as the same caller its hooks
did, which is how two harnesses shipped proxies that disagreed with them.
`mcp-hook-parity.test.mjs` runs every harness that ships a proxy beside
hooks through a dozen configurations: ovcli.conf's own fields, its
`plugin.<harness>` and shared plugin keys, ov.conf-only installs, the
pinned fallbacks to a harness key and to the root key, credential and
auth-mode variables, a forced source over stale variables, an explicit
MCP URL, a host's own input, and a workspace file that tries to move the
connection. The hook loader sees the full environment; the proxy sees
only what its host lets through — Codex's `env_vars`, DSH's scrubbed
inheritance plus the forwarded connection, everyone else's full
environment — and the url, key, identity and identity-header switch they
put on the wire must match. Scenarios with a known answer pin it too, and
a coverage check fails when a new proxy or hook client has no row.
The two codex-only proxy tests the matrix now covers are removed.
* docs(plugins): one connection for hooks and MCP, and version bumps
The capability reference, plugin development guide, Agent Plugins and
Codex pages (en/zh), both doctor references and the plugin READMEs now
describe the chain `resolveConnection()` runs: host input first, the
pinned ovcli.conf branch and what still falls through it, the auth mode
reading `OPENVIKING_AUTH_MODE` and the `plugin` keys in every mode, a
forced `env` source reading no file, and the two ways a connection crosses
into an MCP process (Codex's forwarded-variable list, dsh's forwarded
connection). The parity test is registered with the credential tests.
Versions move past both this branch's base and main: claude-code 0.5.2,
codex 0.9.2, agent-hook 0.3.2, opencode 0.3.2, dsh 0.4.3, pi 0.3.2,
agent-plugins 0.1.2.
OpenViking OpenCode Plugin
A unified OpenCode plugin for OpenViking repository retrieval and long-term memory.
Requires an OpenViking server with
viking://~home-alias support. Recall targets the caller's own context space throughviking://~/memoriesandviking://~/skills; the uid-lessviking://user/memoriesshorthand is rejected by newer servers.
This is the only OpenCode plugin example maintained in this repository. It supersedes the former split examples for indexed repository prompt injection and long-term memory.
The plugin uses OpenCode hooks for lifecycle behavior and registers OpenViking's standard stdio MCP proxy for model tools. It does not install or require an OpenCode skill, and agents do not need to run ov shell commands.
What It Does
- Injects indexed
viking://resources/repositories into the system prompt. - Exposes the same OpenViking MCP tools used by the Claude Code and Codex memory plugins.
- Maps each OpenCode session to an OpenViking session.
- Captures user and assistant text messages into OpenViking.
- Commits sessions at lifecycle boundaries for memory extraction.
- Automatically recalls relevant memories and injects them as hidden synthetic context for the current user message.
- Blocks accidental local filesystem reads of
viking://URIs and points the agent back toopenviking_read,openviking_glob, oropenviking_search. Shell commands that carry aviking://URI still run, with a notice appended to their output.
Files
examples/opencode-plugin/
├── index.mjs
├── package.json
├── README.md
├── INSTALL-ZH.md
├── lib/
│ ├── config.mjs
│ ├── mcp-config.mjs
│ ├── runtime.mjs
│ ├── repo-context.mjs
│ ├── memory-session.mjs
│ ├── memory-recall.mjs
│ ├── session-inject.mjs
│ ├── viking-uri-guard.mjs
│ └── utils.mjs
├── servers/
│ └── mcp-proxy.mjs
├── tests/
└── wrappers/
└── openviking.js
There is intentionally no skills/openviking/SKILL.md. The tool surface comes from OpenViking's MCP endpoint.
Requirements
- OpenCode
- OpenViking HTTP server
- Node.js 18+
- An OpenViking API key if your server requires authentication
Start OpenViking first:
openviking-server --config ~/.openviking/ov.conf
Installation
Published Package
Normal users should enable it through OpenCode's package plugin mechanism:
The published npm package is @openviking/opencode-plugin; verify availability with:
npm view @openviking/opencode-plugin version
{
"plugin": ["@openviking/opencode-plugin"]
}
Source Install
For development or PR testing, copy the package into OpenCode's plugin directory with a top-level wrapper:
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.
This creates a stable OpenCode plugin layout:
~/.config/opencode/plugins/
├── openviking.js
└── openviking/
├── index.mjs
├── package.json
├── lib/
└── servers/
The top-level openviking.js is only a wrapper:
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.
Configuration
Behaviour knobs live in ~/.openviking/ovcli.conf beside the connection fields, in the shared plugin section or in the plugin.opencode override:
{
"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,
"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. Use Context quotas directly
when exact category ceilings are required.
API keys are resolved from environment variables or ~/.openviking/ovcli.conf and sent as Authorization: Bearer ... by both hooks and the MCP proxy. Recall goes through the server-side context face (POST /api/v1/search/search with mode="context"), falling back to the deprecated /api/v1/search/recall on older deployments. 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.
By default the plugin derives a peer from the git identity of the project directory: the normalized origin URL, else the repository root path. Outside a git repository no peer is sent at all, and what is remembered there goes to the user-level space viking://user/<you>/memories. git@github.com:volcengine/OpenViking.git becomes github.com-volcengine-openviking; the path fallback keeps the older naming rule where every non-letter-or-digit character becomes -, so /Users/x/Dev/OpenViking becomes -Users-x-Dev-OpenViking. One repository therefore keeps one peer across subdirectories, worktrees, clones and machines, while a fork's different origin keeps it separate. Derivation reads .git directly, so no git binary is needed. The plugin reads the workspace's .openviking/config.json, so a peer.id or peer.source written there applies. Data-plane memory/resource requests send the effective peer as X-OpenViking-Actor-Peer; captured session messages store it as body peer_id. Configure peerId in the plugin section or OPENVIKING_PEER_ID to override the derived peer, or set workspacePeer to false / OPENVIKING_WORKSPACE_PEER=0 to send no peer at all. Memories written under the older directory-derived peer stay reachable: the default broad recall sweeps every peer under the user, and recallPeerScope="actor" asks that previous peer separately.
Recall defaults to the broad mode: global memory, the current workspace, and
other workspace memories can all be recalled, with other workspaces penalized
and rendered later. Set recallPeerScope="actor" or
OPENVIKING_RECALL_PEER_SCOPE=actor for the isolation mode, which only sees
global memory plus the current workspace. In deployments where one bot serves
multiple real people, such as zouk, vikingbot, or AstrBot, use the isolation mode
with an explicit actor peer so one person's memories are not recalled into
another person's session.
OPENVIKING_API_KEY, OPENVIKING_ACCOUNT, OPENVIKING_USER,
and OPENVIKING_PEER_ID take precedence over ovcli.conf. Below the
environment, the plugin section's peerId takes precedence over
ovcli.conf's actor_peer_id: a peer written for this harness is the more
specific answer, and this is the order every memory plugin follows.
OPENVIKING_CLI_CONFIG_FILE points the plugin at an ovcli.conf somewhere other than ~/.openviking/ovcli.conf.
Hook-only mode
When OpenViking is already exposed through another MCP server, retain the lifecycle hooks while skipping this plugin's bundled MCP registration:
{
"plugin": {
"opencode": { "mcpEnabled": false }
}
}
This leaves repository context, automatic recall, message capture, and lifecycle commits enabled.
It does not add or overwrite OpenCode's mcp.openviking entry.
OpenCode's local read, glob, and grep tools cannot read viking:// URIs.
When the agent accidentally tries that, the plugin blocks the filesystem tool
call and points it to the OpenViking MCP tools. A bash command that contains a
viking:// URI is not blocked, because the URI is often data (an ov argument,
an HTTP payload); the command runs and the plugin appends a notice naming the
MCP tools to its output.
MCP Tools
OpenCode sees the OpenViking MCP server as openviking, so tool names are namespaced with openviking_.
openviking_search: deep semantic retrieval across memories, resources, and skills; usemode="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 moreviking://files.openviking_list: list aviking://directory.openviking_tree: show aviking://directory tree.openviking_grep: exact text or regex search.openviking_glob: glob file matching.openviking_write: create, overwrite, or append to aviking://file.openviking_edit: exact string replacement in aviking://file.openviking_add_resource: add a URL, local file, sitemap, or feed.openviking_forget: delete aviking://URI after explicit user confirmation.openviking_list_watches/openviking_cancel_watch: inspect or cancel resource watches.openviking_health: check OpenViking server health.
The proxy forwards the server's real tools/list response; the plugin does not maintain a separate native tool list.
Runtime Files
The plugin writes runtime files to ~/.config/opencode/openviking/ by default:
openviking-memory.logopenviking-session-state.json
Set dataDir in plugin.opencode to override this directory.
Automated npm releases
Changes merged into upstream main under examples/opencode-plugin/ trigger
.github/workflows/plugin-npm-release.yml. Releases use the same calendar
version convention as the OpenClaw plugin: YYYY.M.D, then YYYY.M.D-N for
subsequent releases on that date (Asia/Shanghai). No manual source version bump
is required. The workflow changes the version only in the package being published;
it does not commit generated version changes back to the repository.
Releases are serialized. A queued run checks out current main, so several quick
merges may be included in one package. The npm manifest records
openvikingSourceCommit; rerunning a successfully published commit skips publication.
Registry failures stop the workflow instead of treating an unavailable registry as
an unused version. Only upstream main can publish the official npm latest tag.
Manual dispatch on main retries a failed release using the existing npm credentials
or Trusted Publishing configuration.