soul.demarkus.io:6309/plans/opencode-memory-plugin.md/v1 draft reader meta

Plan: OpenCode Memory Plugin (1:1 port)

Port the Claude Code demarkus-memory plugin (plugins/claude-code/) 1:1 to OpenCode (opencode.ai, sst/opencode), as plugins/opencode-memory/. Same behavior, same shared state, third harness adapter after Claude Code and pi.

Architecture stance

Identical to pi-memory: a thin TypeScript adapter over the shared demarkus-plugin Go binary, which owns all gate, nudge, guidance, provisioning, and registry logic. The adapter hands the binary normalized JSON (gate, nudge, guidance, provision, registry mcp add) and applies the result. No new binary subcommands and no --format opencode needed; the generic JSON contract pi uses is sufficient. Fails open when the binary is absent (pre-provisioning), matching the bash and pi adapters. Shares ~/.demarkus state, so all three plugins coexist on one machine: one soul, one token, one registry.

OpenCode extension surface (verified 2026-08-09)

  • Plugins: JS/TS in .opencode/plugins/ or ~/.config/opencode/plugins/, or npm packages listed under "plugin" in opencode.json (auto-installed via Bun). A plugin exports async ({ project, client, $, directory, worktree }) => Hooks.
  • Hooks (from @opencode-ai/plugin): config (receives the merged Config, mutable), event (bus: session.created, session.idle, ...), chat.message (mutable output.parts), tool.execute.before (throw to block), tool.execute.after (mutable output.output), permission.ask, custom tool map.
  • MCP: opencode.json "mcp" key, {type: "local", command: [...], environment, enabled}.
  • Commands: markdown in ~/.config/opencode/commands/ or the config "command" key; frontmatter description, template, $ARGUMENTS.
  • Skills: native; SKILL.md discovered from .opencode/skills/, ~/.config/opencode/skills/, and Claude-compatible ~/.claude/skills/ paths, surfaced via a skill tool.

Behavior mapping (Claude Code → OpenCode)

Claude Code hook OpenCode
SessionStart provisioning plugin init function on load: bootstrap.sh then demarkus-plugin provision (async, warn on failure)
SessionStart guidance injection chat.message: append guidance part on first message of a session (per-session flag; retry next turn if binary unavailable, as pi does)
UserPromptSubmit recall-nudge chat.message: demarkus-plugin nudge --event recall on the prompt text, append part
PreToolUse publish/dest gate (deny) tool.execute.before on mark_publish/mark_append: demarkus-plugin gate; blockthrow new Error(reason)
gate ask strictness no native ask in tool.execute.before; block with reason instructing agent to confirm with the user first (same compromise as pi)
PostToolUse gate warn tool.execute.after: append gate warning to output.output
PostToolUse promote-nudge tool.execute.after on mark_publish: nudge --event promote, append
Stop journal-nudge event hook on session.idle: nudge --event session-end with changed-files / soul-write signals tracked in-process (port pi's SessionActivity); surface via toast (client) since no extra turn can be forced
.mcp.json bundled MCP server config hook: set config.mcp["demarkus-memory"] = {type: "local", command: [BIN, "mcp-serve"], enabled: true}; remote souls keep registering via demarkus-plugin registry mcp add
commands/*.md slash commands config hook: populate config.command from the bundled commands/*.md bodies (frontmatter description + body as template), so nothing is copied into the user's config dir
skills/soul-memory/SKILL.md ships in package; provision symlinks/copies it into ~/.config/opencode/skills/soul-memory/ (Claude-path discovery already covers machines with the Claude plugin installed, but don't rely on it)

MCP tool names on OpenCode are servername_toolname style rather than mcp__server__tool; the gate matcher must accept both (regex on mark_(publish|append)$ suffix, as pi's normalizer does).

Layout

plugins/opencode-memory/
  package.json          # npm: opencode-demarkus-memory (opencode-* convention)
  tsconfig.json
  src/index.ts          # adapter: hooks per mapping table
  src/plugin.ts         # runBin/callGate/callNudge/callGuidance — copy from pi-memory
  src/nudges.ts         # SessionActivity — copy from pi-memory
  scripts/bootstrap.sh  # reused verbatim from claude-code plugin
  commands/*.md         # reused; frontmatter converted (allowed-tools dropped, template = body)
  skills/soul-memory/   # reused verbatim
  context/session-guidance.md  # reused verbatim
  README.md, CHANGELOG.md

Steps

  1. Branch (git switch -c opencode-memory-plugin).
  2. Scaffold plugins/opencode-memory/ per layout; copy src/plugin.ts + src/nudges.ts from pi-memory, scripts/bootstrap.sh + commands/ + skills/ + context/ from claude-code.
  3. Write src/index.ts against @opencode-ai/plugin types implementing the mapping table.
  4. Command frontmatter conversion: keep description, drop allowed-tools, body becomes template; resolve ${DEMARKUS_SCRIPTS} to the bundled scripts dir at load (pi's commandBody pattern).
  5. Skill install step in provision path (idempotent copy into ~/.config/opencode/skills/).
  6. Tests where pi-memory has them; bash pre-commit.sh.
  7. README with behavior-mapping table and install instructions; version starts at the current plugin line (0.13.x) to keep the pin-bump convention (SERVER/CLIENT/TOOLS_VERSION move with every change).
  8. Distribution: extend the pi mirror flow (setup-pi-repos.sh + pi-plugin-mirror.yml) or npm-publish; see open questions.
  9. Roadmap + index updates; journal.

Open questions (for Fritz)

  1. Distribution: npm publish (opencode-demarkus-memory, cleanest for OpenCode's "plugin" key + ecosystem registry) vs standalone-repo mirror like pi. npm implies a publish pipeline the repo doesn't have yet.
  2. Commands via config hook vs file install: config-hook injection is clean (no user-dir pollution) but depends on the command config shape accepting full template bodies; verify against a live OpenCode before committing. Fallback: idempotent copy into ~/.config/opencode/commands/.
  3. Scope: memory plugin only; claude-code-knowledge port is a follow-up plan once this lands (same adapter, --surface knowledge).

Risks

  • config hook mutation semantics (MCP + command registration) are the least-documented part of the surface; prototype first, fall back to registry-file writes via demarkus-plugin registry mcp add if config mutation doesn't persist.
  • session.idle fires more often than Claude's Stop; the session-end nudge needs a once-per-session guard (sentinel, as the bash Stop hook has).
  • OpenCode runs plugins under Bun; keep to node: APIs that Bun supports (execFile is fine, pi code carries over unchanged).
trail
  1. soul.demarkus.io:6309 v1