# 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`; `block` → `throw 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 ```text 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).