# demarkus journal — 2026-06-23 ## Built pi-agent ports of the memory + knowledge plugins New packages in the demarkus repo: `plugins/pi-memory/` (port of `claude-code`/demarkus-memory) and `plugins/pi-knowledge/` (port of `claude-code-knowledge`/demarkus-knowledge). Full parity; hook/gate logic ported to TypeScript; bash server-lifecycle reused verbatim. ### Key facts about pi (the coding agent, pi.dev) extension API - An extension is a TS module: `export default function(pi: ExtensionAPI)`. pi loads `.ts` **directly via tsx — no build step**. Ship `src/*.ts`, list them in `package.json` `pi.extensions`; skills go in `pi.skills`. - Events: `pi.on("session_start"|"before_agent_start"|"tool_call"|"session_shutdown", …)`. - `before_agent_start` exposes `event.prompt` (user text) **and** `event.systemPrompt`; fires before every turn. Return `{message:{customType,content,display}}` or `{systemPrompt}` to inject context. This is the one channel for both SessionStart-style guidance and UserPromptSubmit-style nudges. - `tool_call` (pre-only) returns `{block:true, reason}`. There is **no post-call context channel and no native "ask"**. - `pi.registerCommand(name,{description,handler})`; `pi.sendMessage({...},{triggerTurn})`; `ctx.cwd`, `ctx.ui.notify`, `ctx.sessionManager`. - MCP comes via the **pi-mcp-adapter** extension (must be installed separately). It reads server config from `~/.config/mcp/mcp.json` (generic global) / `~/.pi/mcp.json` / project `.mcp.json` — same `{mcpServers:{name:{command,args,env}}}` shape as Claude. HTTP/OAuth servers use `{url, auth:"oauth"}`. ### Gotchas / non-obvious decisions - **MCP tool naming differs.** pi-mcp-adapter names tools `_` with hyphens→underscores (default prefix "server"), e.g. server `demarkus-memory` → `demarkus_memory_mark_publish`. NOT Claude's `mcp__server__tool`. The TS `parseMcpTool` handles all three forms (`mcp__s__mark_x`, `s_mark_x`, bare `mark_x`) and compares server names separator-insensitively. - **No JSON-parsing awk needed.** Claude hooks parsed the tool payload with a pure-awk JSON scanner (no jq); in pi `event.input` is already a JS object, so `publish_metadata_check` collapsed to direct property reads. - **Strictness mapping:** `block`→block, `warn`→allow + `sendMessage` reminder, `ask`→block with a reason telling the agent to confirm with the user (pi has no native ask). - **Stop→session_shutdown:** Claude's Stop hook can force one more turn to make the agent journal; `session_shutdown` cannot, so the journal nudge degrades to a `ctx.ui.notify` (tracked file-mutation vs soul-write during the session). - **Shared `~/.demarkus` state is deliberate.** The pi packages bundle copies of the bash (`lib.sh`, `setup.sh`, `mcp-wrapper.sh`, soul/promote scripts) and read/write the same `~/.demarkus` files as the Claude Code plugins → one soul, one token, one registry; pi and Claude Code coexist. New bash: `provision.sh` (per-session entrypoint mirroring session-start.sh's setup block) and `mcp-config.mjs` (Node helper = pi's `claude mcp add/remove/list`, incl. `add-http` for OAuth brokers). - Commands are kept as `commands/*.md` prompt bodies injected by `registerCommand`; bodies use a `${DEMARKUS_SCRIPTS}` token substituted to the bundled scripts dir at runtime. Only the genuinely on-demand skill (`soul-memory`, `knowledge-promote`) is listed in `pi.skills`. Both packages type-check clean under `tsc --strict`; bash + the Node MCP helper smoke-tested. Not committed (user commits). ## Gotcha: pi-mcp-adapter proxies MCP tool calls through a single `mcp` tool Live-testing the pi-memory publish gate exposed a non-obvious bug. A pi extension's `tool_call` handler does NOT see MCP tools by their direct name. pi-mcp-adapter routes every MCP call through one proxy tool, so the event the agent fires is: ```json { "toolName": "mcp", "input": { "tool": "demarkus_memory_mark_publish", "args": "{\"url\":\"/x.md\", ...}" } } ``` - `event.toolName` is the literal string `"mcp"` — NOT `demarkus_memory_mark_publish`. - the real tool name is `event.input.tool`. - the real arguments are `event.input.args` as a **JSON string** (must `JSON.parse`), not an object. Our gates read `event.toolName`/`event.input` directly, so they never matched `mark_publish` and every tagless/misrouted write sailed through silently (the gate logic itself was correct — verified in isolation; only the event shape was wrong). Strictness env vars propagated fine; the bug was purely the proxy indirection. **Fix:** a `normalizeToolCall(event)` helper in both extensions — when `toolName === "mcp"`, unwrap `input.tool` as the name and `JSON.parse(input.args)` as the args; direct (non-proxied) tool calls pass through unchanged. After the fix, both the destination gate (bound-soul misroute) and the publish tag-gate (missing `metadata.tags`) block correctly through the real MCP path. **General lesson for pi extensions:** to gate/observe MCP tools via `tool_call`, you must handle the `mcp` proxy shape — direct tool-name matching is insufficient. (pi-mcp-adapter also only surfaces a newly-registered server's tools after a one-time interactive `/mcp` connect that caches them; headless `-p` can prime `~/.pi/agent/mcp-cache.json` by computing the server `configHash` over the identity fields and writing the tool list.) ## Related documents - [Claude Code plugin plan](/plans/claude-code-plugin.md): source plugins these pi ports mirror - [Ecosystem](/ecosystem.md): agent integrations catalog the pi plugins join - [Soul join plan](/plans/soul-join.md): dest-gate and catalog ported to pi here