Files
t0saki b7d0415c24 feat(plugins): skill catalog and openviking-skills for the memory plugins (#5161)
* refactor(skills): install skills through one shared helper

POST /api/v1/skills kept its whole install loop (source resolution, per-skill
install, source metadata, list_only) inline in the route. Move it into
openviking/server/skill_ingest.py:install_skills so the MCP add_skill tool
and signed skill uploads can reuse the exact same code path. The REST
route's behavior is unchanged.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(mcp): add an add_skill tool

MCP clients had no way to create a skill: write refuses the skills/
subtree (_USER_MANAGED_SUBTREES) and add_resource validates its target as
a resource. Agents that should keep skills in OpenViking could read them
but never add one.

add_skill takes either the full SKILL.md text (data) or a path. A Git or
GitHub tree URL installs through the same source resolution as REST, with
skills=[...] to pick from a multi-skill repository and list_only to
preview it. A local SKILL.md, directory, or zip gets the add_resource
treatment: the tool mints a one-time upload token, now tagged kind="skill"
with the target root, selection and list_only, and the signed temp_upload
installs the file as skills instead of ingesting it as a resource.
target_uri="viking://agent/skills" shares the skill with the account.

All three paths (REST, MCP inline/Git, signed upload) go through
skill_ingest.install_skills. The tool count in the server log, app
comment, docs, and the Codex plugin's REAL_MCP_TOOLS moves to 16.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): search shared skills in find(context_type="skill")

Without a target_uri, find resolved the generic default targets, which
stop at the caller's user root, so a skill search never reached the
account-shared viking://agent/skills. REST /skills/find and the context
search already cover both roots. When context_type resolves to skill only
and no target_uri is given, the MCP tool now targets
default_target_directories(ctx, context_type=SKILL): the user's own skills
plus viking://agent/skills. REST find semantics are unchanged.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): print directory abstracts in tree(include_abstract=true)

The tree tool skipped to the next entry right after printing a directory,
and only printed abstracts for files, but the storage layer only fills
abstracts for directories (files always come back empty). The flag
therefore never printed anything. Print the abstract after either kind
of entry and ask for up to 1024 characters, enough for a full skill
description, so tree(uri="viking://~/skills", level_limit=1,
include_abstract=true) lists every skill with its description.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(skills): honor node_limit in GET /api/v1/skills

list_skills declared node_limit but always listed each skill root with a
hardcoded 1000. Pass it through per root; 0 keeps the default so the CLI's
accepted range (-n 0) still lists everything.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): point skill hits in find/search at their SKILL.md

A skill is indexed through its directory's .abstract.md, so find and
list-mode search printed hits like viking://agent/skills/x/.abstract.md.
Following the "use the read tool to expand a URI" advice returned only
the frontmatter, and read_content inlined the same stub. Skill hits now
show <dir>/SKILL.md, and read_content reads that file.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): validate add_skill targets and sources before minting an upload

Review findings on the add_skill tool:

- target_uri passed the content-kind check for any path under a skills
  root (viking://~/skills/pdf) and, for ROOT, for another user's root,
  but the installer only accepts the caller's own skills root or
  viking://agent/skills. On the local-path branch the tool minted a
  one-time upload token anyway, and the upload failed with 400 after the
  token was spent. The target is now resolved with the installer's own
  rule first; shared subpaths map to viking://agent/skills, the rest fail
  at once, and the error names both allowed roots.
- Non-Git remote sources such as tos:// were treated as remote, then
  refused as "direct host filesystem paths". add_skill now decides Git
  with the same prefixes resolve_skill_source uses (shared as
  GIT_SKILL_SOURCE_PREFIXES) and reports other schemes as unsupported.
- With list_only, the upload instructions still said the skill would be
  installed and that no further call was needed; they now say the upload
  only lists the source's skills.
- The zip example packaged hidden files, so .git and .env files went
  into the stored skill. It now excludes VCS data, .env files,
  node_modules and .DS_Store, starting from a fresh archive.
- tree(include_abstract=true) printed the "abstract is not ready"
  placeholder for directories that never get an abstract, such as a
  skill's scripts/. Those placeholders are skipped.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(mcp): say that write only refuses the user's own skills subtree

The capability reference claimed MCP write refuses every skill URI. It
refuses the user's own skills/ subtree, but under viking://agent/skills
it writes a plain file that skips skill installation. State that, and
point shared skills at add_skill as well.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* style(skills): format skill_processor.py

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): inject an <available-skills> catalog at session start

Agents on every harness learned about memories at session start but had
no idea which skills OpenViking held, so a stored skill was only found if
a later recall happened to surface it.

buildProfileBlock() now takes the caller's resolved config as a fourth
argument and, when skillCatalog is on (default), adds <available-skills>
after <available-memories>: one GET /api/v1/skills lists the user's own
skills first, then account-shared ones, dropping a shared skill the user
shadows by name. Descriptions are cut to about 40 tokens and envelope
tags in them are escaped, since the shared root is written by anyone on
the account. The block has its own budget (skillCatalogTokenBudget,
default 1200) and degrades from descriptions to names to a one-line
count; with no skills, or a server without the endpoint, it is omitted.

The shared formatListing now gives entries back so its "+N more" tail
fits: a greedily filled listing never left room for it, so a cut listing
ended silently. This also applies to <available-memories>.

All six callers (claude-code, codex, opencode, dsh, pi, and the thin hook
runtime for cursor/trae/zcode) pass their config through.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): recall account-shared skills and flag skill entries

The server context face already mixes both skill roots into per-prompt
recall, but the plugins' last-resort ranked find only searched
viking://~/memories and viking://~/skills, so on servers without the
context face a shared skill in viking://agent/skills could never be
recalled. Add it as a third source, and name each skill hit by its
directory rather than the .abstract.md it was indexed through, matching
the context face and the session-start catalog.

When an injected recall block carries a skill (a type="skills" entry, or
a [skill] line from the fallback), its header gains one line telling the
agent to read the skill's SKILL.md before following it. Turns without a
skill are unchanged.

The openclaw plugin's vendored recall-core copy is regenerated with it.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): point skill writes at add_skill in the URI guard

A local Write or Edit aimed at viking://.../skills/... was denied with a
hint to use MCP write or edit instead, but the server refuses both under
the skills subtree, so the hint led straight into a second error. Hints
may now depend on the URI: for a skill URI (viking://~/skills,
viking://user/<id>/skills, viking://agent/skills) the default table and
dsh's bridged table name add_skill with an add_skill(data="<the full
SKILL.md text>") example. Other URIs keep their hints.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): bundle an openviking-skills skill

Nothing told an agent how to work with skills stored in OpenViking: how
to load one from the catalog, run its helper files, create one through
add_skill, install from Git or a local folder, share it with the account,
or move the user's existing local skills over.

examples/skills/openviking-skills covers all of that, including a
user-triggered, one-time migration of ~/.claude/skills, ~/.agents/skills
and ~/.cursor/skills that keeps environment-bound skills local (shipped
by a plugin or marketplace, symlinked in by a CLI installer such as
lark-cli, or needing a local binary) and uploads only what the user
approves skill by skill. It passes strict server validation.

sync.mjs ships it wherever add_skill is a real tool and a bundled skill
loads: the codex, claude-code, cursor and dsh plugins. openviking-memory
now points to it for skill work. A new sync test keeps synced skills
flat, since copySkill copies a flat file list and a subdirectory would
crash it; the marketplace tests pin the packaged copies, and dsh's
provider test expects both bundled skills.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* chore(plugins): bump versions for the skill integration

claude-code 0.6.0, codex 0.10.0, agent-hook (cursor/trae/zcode) 0.4.0 and
dsh 0.5.0 gain the skill catalog and, except trae/zcode, the bundled
openviking-skills skill; opencode 0.3.3 and pi 0.3.3 gain the catalog.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(codex): recall shared skills and flag skill entries in Codex too

Codex builds its recall block itself instead of through recall-core's
wrappers, so the previous commit's changes never reached it: its raw
search still skipped viking://agent/skills, its digest carried no skill
hint, and its post-processing kept only level-2 leaves, which silently
dropped every skill hit (skills are found through their directory's
level-0 abstract), including the viking://~/skills search it already ran.

recall-core now exports skillEntryHint() and skillHitUri(). The hint is
added when a block carries a type="skills" entry or cites any skill URI,
which also covers digests that only keep URIs. Codex searches the shared
skill root as a third bucket, labels skill hits "skills" under their
directory URI, lets them through post-processing, and adds the same hint
line to its envelope.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(cursor): install openviking-skills next to openviking-memory

sync.mjs puts openviking-skills into hosts/cursor/skills, but the
installer copied only openviking-memory into ~/.cursor/skills, so Cursor
never saw the new skill. Install, uninstall, the post-install check and
the doctor's file list now cover both skills. The install test also moves
to the agent-hook plugin's new 0.4.0 version string.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(dsh): keep PLUGIN_VERSION in step with package.json

The version bump moved package.json to 0.5.0 but left the PLUGIN_VERSION
constant at 0.4.3, which bundle.test.mjs and npm run check:version
compare against the manifest.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(skills): match ov-skills commands to the real ov CLI flags

The ov-skills skill documented flags the CLI never had (--json, and a
--limit that is only a hidden alias), a raw-content "ov skills add -"
form that sends a literal "-", and ov resources subcommands that do not
exist. Every command line now follows the clap definitions: -o json for
JSON, -n/--node-limit, -p/--uri on read commands and
-p/--parent-auto-create on add, -s/--skill as a comma list, show
--format, and validate's --strict-only body-length warning. ov add-skill
is documented as the same command as ov skills add.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(plugins): document the skill catalog, openviking-skills, and skill-aware recall

The integration pages (Claude Code, Codex, Cursor, TRAE, opencode, pi,
dsh), the capability reference, the plugin development guide, and the
plugin READMEs now describe the <available-skills> session-start block,
its skillCatalog / skillCatalogTokenBudget knobs, the bundled
openviking-skills skill where it ships, recall reaching
viking://agent/skills with the skill-entry hint, and the URI guard
sending skill writes to add_skill. en and zh pages carry the same facts.

Stale statements fixed on the lines touched: thin hook hosts use the
same 10000-token profile budget as the rest, the claude-code and codex
plugins ship four skills, dsh mounts the server MCP surface, and the
opencode install guide lists openviking_add_skill once.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(plugins): let the user's own skills use the catalog budget the shared group leaves

Review findings on <available-skills>:

- Each group got at most its even share of the listing budget, with
  unused tokens passing only forward, so the user's own skills (always
  first) never got more than half. Twenty own skills and one shared skill
  fell back to names only while most of the 1200 tokens went unused. A
  group now takes its even share or everything the later groups leave
  when listed in full, whichever is larger.
- When a group's share could not hold its header plus the "+N more"
  tail, formatListing gave back every entry and printed a bare header,
  which reads as an empty directory. It now prints the one-line
  "N entries, budget too tight" stub instead (memory listings too).
- A budget too small for even the one-line count now injects nothing
  rather than overrunning it.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(plugins): rank skill hits like memory leaves in the recall fallbacks

Naming a skill hit by its directory instead of its .abstract.md cost it
the 0.12 leaf boost, since the boost keyed on a ".md" URI, so a skill
that main would recall lost to ten slightly weaker memory leaves. In
Codex, skills were also never picked while enough memory leaves passed
the threshold (leaves are picked first), and a hit the server labeled
with another category lost its "skills" label. Skill hits now count as
leaves in ranking and picking in both recall-core and Codex, and Codex
always labels them "skills".

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(plugins): send shared-skill edits back to the shared root in the URI guard

The guard's add_skill example carried no target_uri, and add_skill
without one installs into the caller's own root. Fixing a shared skill
that way left the team copy unchanged and created a private copy that
the catalog then shows instead. For viking://agent/skills URIs the
example now passes target_uri="viking://agent/skills", and a helper file
(anything below a skill's SKILL.md) points to a folder upload through
add_skill(path=...) rather than SKILL.md text. addSkillExample() builds
the example for both the default table and dsh's.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(skills): tighten openviking-skills where review found unsafe steps

- The catalog is a snapshot that drops descriptions or entries with many
  skills, so a name missing from it does not prove the name is free.
  Check <root>/<name>/SKILL.md, and confirm with the user before
  replacing an existing skill, since add_skill replaces silently.
- Updating a shared skill must pass target_uri="viking://agent/skills"
  after the user confirms; otherwise add_skill creates a private copy
  that shadows it.
- Every file in an uploaded folder is stored with the skill: zip without
  .git, .env files, node_modules and .DS_Store, and delete the archive
  afterwards. The migration now inspects the whole folder, hidden files
  included, for secrets.
- Migration flags frontmatter keys OpenViking drops (for example
  disable-model-invocation or context), and fixes a missing name or
  description in a temporary copy, never in the user's file.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): keep the session-start block under the host's inline limit

Claude Code saves hook context over 10,000 characters to a file and
shows the model a 2 KB preview; Codex and trae-cli spill past about
10,000 bytes; ZCode drops stdout over 32 KB. The session-start block
(profile at a 10,000-token budget, memory index, skill catalog, and the
archive on resume) routinely ran 25-40 KB, so on these hosts the model
saw only the start of the profile, and the catalog appended at the end
never reached it.

sessionStartMaxBytes caps the whole block in UTF-8 bytes: 9500 for
claude-code and codex, 20000 for zcode, no cap elsewhere. Under the cap
buildProfileBlock shrinks its token budgets (about 4 bytes per estimated
token); if the block still does not fit it drops the memory index, then
the catalog. On resume/compact the archive takes up to half, truncated
on a line with a pointer to viking://~/sessions/<id>/history/.

On resume, claude-code and codex skip the profile block when it matches
the one this session already received, since the restored history holds
it; a changed block is injected again.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): return one hit per skill package in find

#5045 made a skill index as a whole package, so an item-level find now
returns one hit per file inside it. Route skill-only find through
SearchService.find_skills, which keeps the best hit per package, and
resolve every skill hit to its package's SKILL.md instead of only
rewriting the two index sidecars.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): point skill changes at add_skill in the tool descriptions

The server keeps accepting write/edit under viking://agent/skills, and
forget still removes a skill directory, so the constraint lives in the
tool descriptions: add_skill is the one entry point for creating and
updating a skill, and removal goes through ov skills remove or Studio.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(mcp): describe a skill hit by its own abstract

A package hit can be any file inside the skill, whose abstract describes
that file and not the skill, so find would list a skill under a helper
script's summary. Read the package's abstract for those hits, the way
GET /skills/find already does. Keep a filter-only skill query on the
generic find, which find_skills does not serve.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): document package-level skill retrieval in find

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* feat(agent-plugins): ship the openviking-skills skill

Agent Plugins has no hooks, so no session-start catalog: without this skill
the model never learns that the account's skills exist. The skill's own text
now reaches for find(context_type="skill") first and treats
<available-skills> as something only some harnesses inject, so one copy
reads correctly in both kinds of harness.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(plugins): name the skill package a recall hit came from

Since a skill is indexed as a whole package, a hit can be any file inside
it, not just the two index sidecars the old rewrite stripped. Derive the
package root the way the server's skill_root_uri does, drop the internal
update backups, and keep one entry per package at its best score.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(plugins): let the skill catalog use both roots' full listings

The server already caps each skill root at node_limit, so a second cap over
the merged list only bites once the private root alone fills it — and then
it drops the shared root whole while reporting nothing dropped. The token
budget is what should decide, and it already does.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(plugins): say node_limit caps each skill root, not the merged list

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(mcp): do not paste an unready abstract over a skill hit's own summary

fs.abstract returns a placeholder string rather than raising when a package
has no usable .abstract.md, so the substitution replaced a useful file
summary with a diagnostic line. Reject the same placeholders tree already
rejects, and bound the per-package reads the way read_content is bounded.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): correct how search reports skill hits, and refresh the tool table

find and search both render one line per skill package, so the earlier
wording — that search returns several hits per package — contradicted the
code. Say what actually differs: search still spends a limit slot per
matching file and keeps that file's summary. Also point forget and
add_resource at add_skill where an agent would look for them, name the REST
delete alongside the CLI, and bring the capability table's line citations
back in step with mcp_endpoint.py.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(agent-plugins): list add_skill among the tools the package exposes

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* refactor(mcp): drop guards and prose no caller can reach

install_skills only ever returns a dict, add_skill always fills root_uri,
and a source with no SKILL.md raises before it gets here, so the
isinstance, empty-list and missing-uri branches were unreachable.
fs.abstract only returns the directory placeholder. One skill package
resolves to one rendered item, so the pending map holds one each. In the
docstrings, drop what Args already says and the one removal path an MCP
caller cannot take.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(plugins): drop a comment about a branch formatListing cannot take

A listing left with only its header returns the stub above, so it never
reaches the silent close the comment described. Also name
sessionStartMaxBytes in buildProfileBlock's options type, where the .d.mts
already has it.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* revert(plugins): drop the skill changes in the recall fallback

Reverts the recall half of the skill integration: 992215bfb, 553416200,
5e838a0a0 and 62a456c08. The seven files they touched go back to their
state at aa77061c1, the merge base with main.

Those four commits taught the local recall assembly about skills: a third
source for viking://agent/skills, skillHitUri naming a hit's package,
dedupeSkillHits keeping one entry per package, rankItem scoring a skill
like a memory leaf, and a header line telling the model to read SKILL.md.
Four of the five only ever ran in the raw-find fallback. recallForPeer
calls buildServerAssembledBlock first and returns as soon as it answers,
so searchAllSources, rankItem and the fallback block builder are reached
only on a deployment whose server has no context face. The fifth, the
header line in wrapContext, did run on the main path, but the server
already reports each entry's type and the URI it wants read, so the line
restates what the block carries.

Skills still reach the model on the path that runs: the context face
searches both skill roots, returns them as entries with type="skills",
and the session-start <available-skills> catalog lists every skill with
its description. Both stay.

The documentation that described the fallback behaviour goes with it. The
statements that survive are the ones the context face makes true on its
own, such as recall covering the skills shared with the account.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* chore(plugins): bump opencode and pi past main's releases

Both were 0.3.3 on this branch and main has since shipped 0.3.3 of its
own, so the version a host installs by no longer moves for the skill
catalog they now carry through the shared library.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): say what list-mode search actually reports for a skill hit

The search row claimed a skill package's summary is the matching file's.
It is not: _format_search_result rewrites every skill hit onto the
package's SKILL.md, keeps the best-scored one per package, and
_describe_skills_by_package replaces the summary with the package's own
abstract. What is true is that limit applies during retrieval, before
that merge, so a package matching several files still spends several
slots and fewer than limit results come back.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW
2026-09-22 15:09:44 +08:00

7.4 KiB

OpenViking Agent Plugins (Agent Plugins 1.0)

Portable Agent Plugins 1.0 package for OpenViking: long-term semantic memory and context for coding agents.

Agent Plugins 1.0 is a vendor-neutral packaging format for extending AI coding agents, backed by Amazon, Cursor, Microsoft, OpenAI, and Vercel among others. A plugin is a plain directory with a plugin.json manifest, auto-discovered Agent Skills under skills/, and optional MCP server declarations in mcp.json — one package that any conforming client (Cursor, VS Code, Amazon- and OpenAI-side clients, ...) can load the same way. This directory is that package for OpenViking.

What's inside

plugin.json                          # Agent Plugins 1.0 manifest
mcp.json                             # stdio MCP server: "openviking"
servers/mcp-proxy.mjs                # stdio -> streamable-HTTP proxy to the OV server's /mcp
servers/shared/                      # generated from examples/memory-plugin-shared/lib (do not edit)
skills/openviking-memory/SKILL.md    # teaches the model the recall + persist loop
skills/ov-memory-troubleshoot/       # read-only extraction troubleshooting
skills/ov-experience-memory/         # retrieve and apply prior task Experience
skills/openviking-skills/            # find, use, create, and share OpenViking skills
plugin.test.mjs                      # node --test conformance checks

Use ov-memory-troubleshoot to trace backward from a memory file to its archive diff and, when needed, session messages. Diagnosis is read-only.

ov-experience-memory has the model search viking://~/memories/experiences with find or search before executable work and read the one to three Experience files that apply. In this package it is retrieval-only: without hooks nothing commits the conversation as a session, so its reads are not linked back to the Experience they used and produce no new trajectories. The Experience it retrieves comes from harnesses that do capture sessions, such as Claude Code and Codex.

openviking-skills covers the skills stored in OpenViking itself: how to find one with find(context_type="skill"), read and follow its SKILL.md, create or replace one with the add_skill MCP tool, install one from Git or a local folder, share one with the account, and move local skill folders into OpenViking when you ask. This package has no session-start hook, so there is no <available-skills> catalog here and the skill has the model search for a skill instead.

Zero npm dependencies; the proxy and tests run on the Node.js standard library (Node 18+ for global fetch).

Install

  1. Have an OpenViking server reachable (see the quickstart); default local endpoint is http://127.0.0.1:1933.
  2. Point your Agent-Plugins-conforming client at this directory (each client has its own install command or plugin directory; consult its docs). The client will:
    • register the openviking MCP server from mcp.json — it runs node <plugin>/servers/mcp-proxy.mjs over stdio;
    • discover the openviking-memory, ov-memory-troubleshoot, ov-experience-memory, and openviking-skills skills from skills/.
  3. Configure credentials (next section) and start a session. The model gains find / search / read / remember / write / add_skill and the other OpenViking MCP tools. Use search with mode="context" for server-assembled context.

Why a stdio proxy instead of a streamable-http entry

OpenViking already speaks streamable HTTP at /mcp, but a streamable-http entry in mcp.json cannot work portably: the server URL is per-deployment (localhost for one user, a remote endpoint for another), and the Agent Plugins spec forbids credentials in the static headers map. The stdio proxy solves both — it resolves the URL and API key at runtime from the same local sources as the ov CLI, injects them per request, and forwards JSON-RPC over streamable HTTP unchanged.

Credential resolution

Highest to lowest priority (same chain as the ov CLI and the other OpenViking plugins):

  1. Environment variables: OPENVIKING_URL (or OPENVIKING_BASE_URL), OPENVIKING_API_KEY (or OPENVIKING_BEARER_TOKEN), OPENVIKING_ACCOUNT, OPENVIKING_USER, OPENVIKING_PEER_ID
  2. ~/.openviking/ovcli.conf (url, api_key, account, user, then the plugin.agent_plugins and plugin keys) — override the path with OPENVIKING_CLI_CONFIG_FILE
  3. ~/.openviking/ov.conf agent_plugins section, then its server section (url or host/port, root_api_key) — override the path with OPENVIKING_CONFIG_FILE
  4. Defaults: http://127.0.0.1:1933, no auth (local mode)

Config file changes are picked up by the running proxy without a restart. Debugging: set OPENVIKING_DEBUG=1 to log JSON lines to ~/.openviking/logs/agent-plugins.log (path override: OPENVIKING_DEBUG_LOG).

Scope: what this package does and doesn't do

This package is the portable recall + write surface: skills plus MCP tools, driven by the model. Agent Plugins 1.0 deliberately excludes hooks, commands, and agents, so automatic conversation capture and automatic pre-prompt recall are out of scope here — the skills/openviking-memory skill instead teaches the model to recall at task start and persist durable facts via remember/write itself.

If your harness has a hook system, prefer the dedicated plugin — hook-driven recall and capture cost no tool calls and don't depend on the model choosing to remember. One installer covers Claude Code, Codex, Cursor, TRAE / TRAE CN, ZCode, OpenCode, and pi; it prompts for language, harnesses, download source, and credentials, and is idempotent:

bash <(curl -fsSL https://raw.githubusercontent.com/volcengine/OpenViking/main/examples/memory-plugin-shared/install.sh)
# GitHub hard to reach? Same installer from the Volcengine TOS mirror:
bash <(curl -fsSL https://ovrelease.tos-cn-beijing.volces.com/memory-plugin-shared/install.sh)

Use this Agent Plugins package for harnesses with no hooks, or when you want one package that loads across many clients.

Per the spec, client-specific integrations may later be embedded in this package under reverse-domain namespaced directories (e.g. com.example.client/) or the manifest's extensions object without breaking other clients.

Development

node --test agent-plugins/plugin.test.mjs

servers/shared/*.mjs are generated, verbatim copies from examples/memory-plugin-shared/lib — do not edit them here. This directory is a target of the shared-lib sync script, so refresh them with:

node examples/memory-plugin-shared/sync.mjs

examples/memory-plugin-shared/sync.test.mjs fails if they drift, and pins this package to the connection half of the shared runtime: servers/mcp-proxy.mjs resolves everything through buildProxyConnection() in shared/credentials.mjs — the same resolveConnection() every other plugin's hooks and proxy use — so the hook-tuning knobs — which this spec has no hooks to run — never enter the bundle.

Both test files run in CI via .github/workflows/pr.yml.