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.tsdirectly via tsx — no build step. Shipsrc/*.ts, list them inpackage.jsonpi.extensions; skills go inpi.skills. - Events:
pi.on("session_start"|"before_agent_start"|"tool_call"|"session_shutdown", …).before_agent_startexposesevent.prompt(user text) andevent.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
<server>_<tool>with hyphens→underscores (default prefix "server"), e.g. serverdemarkus-memory→demarkus_memory_mark_publish. NOT Claude'smcp__server__tool. The TSparseMcpToolhandles all three forms (mcp__s__mark_x,s_mark_x, baremark_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.inputis already a JS object, sopublish_metadata_checkcollapsed to direct property reads. - Strictness mapping:
block→block,warn→allow +sendMessagereminder,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_shutdowncannot, so the journal nudge degrades to actx.ui.notify(tracked file-mutation vs soul-write during the session). - Shared
~/.demarkusstate 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~/.demarkusfiles 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) andmcp-config.mjs(Node helper = pi'sclaude mcp add/remove/list, incl.add-httpfor OAuth brokers). - Commands are kept as
commands/*.mdprompt bodies injected byregisterCommand; 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 inpi.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:
{ "toolName": "mcp", "input": { "tool": "demarkus_memory_mark_publish", "args": "{\"url\":\"/x.md\", ...}" } }
event.toolNameis the literal string"mcp"— NOTdemarkus_memory_mark_publish.- the real tool name is
event.input.tool. - the real arguments are
event.input.argsas a JSON string (mustJSON.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: source plugins these pi ports mirror
- Ecosystem: agent integrations catalog the pi plugins join
- Soul join plan: dest-gate and catalog ported to pi here