@black942026/pi-debug-tool
Observer-only debug extension for Pi 0.99.x: slash commands to inspect state, trace runs, summarize metrics, and debug native MCP without ever touching model context.
Package details
Install @black942026/pi-debug-tool from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@black942026/pi-debug-tool- Package
@black942026/pi-debug-tool- Version
0.1.1- Published
- Oct 2, 2026
- Downloads
- 287/mo · 287/wk
- Author
- black942026
- License
- MIT
- Types
- extension
- Size
- 190.5 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-debug-tool
An observer-only debug extension for Pi 0.99.x.
It adds one user-invoked slash command, /debug, that inspects session state,
traces runs, summarizes metrics, and debugs native MCP without touching
model context.
The model can never see anything this extension collects. It registers no tools, no skills, no prompt templates, no messages, and never modifies the context, system prompt, tool loadout, or provider payload.
Safety model (highest priority)
The extension is built so that "zero context pollution" is a structural property, not a promise:
- The only registration calls are
pi.on(...)(observer handlers) andpi.registerCommand("debug", ...). - It does not subscribe to
mcp_servers_change. Handling that event marks an extension as the one that connects registered MCP servers; this extension only readspi.getMcpServers()on demand from/debug mcp. - Every event handler is wrapped so it always returns
undefinedand swallows its own errors. It therefore cannot replace a payload, block a tool, append an entry, or request a continuation. - It never calls
registerTool,setActiveTools,sendMessage,sendUserMessage,appendEntry,registerMessageRenderer,registerEntryRenderer,registerMarkdownTransformer,registerProvider,registerMcpServer,unregisterMcpServer,registerShortcut,registerFlag,setSessionName,setLabel,setModel,setThinkingLevel,exec, orevents.emit. - It never connects, calls, reconnects, starts, stops, or toggles an MCP server,
and never runs
pi mcpas a subprocess. - Commands only render to the UI (a TUI overlay, or a notification in other
modes). The only command that writes a file is
/debug export, and only when the user explicitly invokes it. - No state is persisted as session entries or custom messages.
This is enforced by tests:
tests/observer-only.test.tsspies on every forbidden API and fails if any is called, asserts all handlers returnundefined, asserts events are never mutated, asserts nomcp_servers_changehandler is registered, and statically scansindex.tsandsrc/**/*.tsfor forbidden call patterns.tests/zero-context-pollution.test.tscaptures the system prompt, system prompt options, active tools, session projection, branch, and entries before and after running every command (including all/debug mcpforms) and asserts they are byte-for-byte identical.
Install
From npm (after publishing):
pi install npm:@black942026/pi-debug-tool
The npm package includes the pi-package and pi-extension keywords for discovery.
Local directory (recommended while developing):
pi install ./pi-debug-tool
# or load for a single invocation without installing:
pi --extension ./pi-debug-tool/index.ts
From the package root, use pi --extension ./index.ts (or pi --extension .).
The pi.extensions manifest in package.json points to ./index.ts.
Pi loads the TypeScript entry directly via jiti; no build step is required to use
the extension.
Commands
One root command, dispatched by the first argument:
| Command | Description |
|---|---|
/debug or /debug status |
session/cwd/mode/trust/idle/pending/model/thinking/context-usage/branch + a read-only metrics summary |
/debug mcp [overview|tools|calls|doctor] [server] [--server NAME] [--limit N] |
native MCP inventory, exposure, and call metadata (observer-only) |
/debug resources [tools|commands|skills|mcp] |
public-API resource inventory; tools distinguishes active vs all with sourceInfo |
/debug context [summary|sections|messages] [--detail] [--limit N] |
read-only canonical projection + system-prompt/options summary |
/debug trace on|off|status|tail [--limit N] |
in-memory ring buffer of safe metadata events |
/debug timeline [--limit N] |
run/turn/toolCall-correlated text timeline (parallel tools supported) |
/debug stats [session|tools|cache|context|errors] |
statistics with explicit denominators |
/debug doctor |
conservative, fact-based hints only |
/debug export jsonl|markdown [path] [--force] |
write a redacted export (the only file write) |
/debug clear |
clear this extension's own trace/metrics only |
/debug help |
usage and safety notes |
/debug defaults to status. Unknown subcommands print the available list
instead of failing.
Architecture
pi-debug-tool/
├── package.json package metadata + pi.extensions: ["./index.ts"]
├── index.ts Pi entry (re-exports the factory)
├── src/ implementation
├── tests/ vitest suite
├── README.md
└── LICENSE
Source layout:
src/
extension.ts factory: observers + /debug command
runtime.ts all mutable in-memory state
observers.ts observer-only event wiring, fail-closed
core/
clock.ts wall clock + monotonic clock (durations)
ring-buffer.ts fixed capacity + dropped counter
trace-store.ts correlation (run/turn/request/message/tool) + ring
metrics.ts counters, tool durations, usage totals
session-metrics.ts pure derivation over public session entries
inspectors.ts read-only projection/resource summaries
mcp-native.ts MCP tool identity/inventory + bounded call metadata
redaction.ts secret redaction, label/error sanitizing, bounding
format.ts formatting helpers
types.ts shared types
commands/ one module per subcommand + dispatcher
ui/present.ts TUI overlay with notify fallback
tests/ vitest suite (see "Testing")
Correlation uses local counters where Pi exposes no native id, and says so in the UI:
run— incremented onagent_start.turn— Pi'sturnIndexfromturn_start/turn_end.request— incremented onbefore_provider_request.msg— incremented onmessage_start.tool— Pi's nativetoolCallId(the only native id).
Durations are always computed from the monotonic clock; wall-clock time is only used for display.
Public API boundary
Only documented public exports are used, and only for reading:
ExtensionAPI:on,events,registerCommand,getAllTools,getActiveTools,getCommands,getMcpServers.ExtensionContext/ExtensionCommandContext:ui,mode,hasUI,cwd,sessionManager(read-only methods),model,thinkingLevel,isIdle,isProjectTrusted,hasPendingMessages,getContextUsage,getSystemPrompt,getSystemPromptOptions.ReadonlySessionManager:getSessionId,getSessionFile,getCwd,getSessionName,getLeafId,getBranch,buildSessionProjection,getEntries,getTree,getHeader.
No private/deep imports, no internal runner access, no monkey patching, no
reflection on private fields. Anything not available through these APIs is
reported as unavailable rather than guessed.
Native MCP debugging (/debug mcp)
Pi 0.99 connects MCP servers itself and registers their tools as
mcp__<server>__<tool> with exposure direct, codemode, codemode-deferred,
deferred, or hidden (see Pi's docs/mcp.md). /debug mcp debugs this native
integration through public APIs only. There is no dependency on
pi-mcp-adapter and no adapter protocol, channel, cache, or compatibility
layer.
What it can tell you (evidence-based):
- Tool inventory from
getAllTools(): per server/tool exposure, plus the distinction between registered, declared (getActiveTools()), callable (reachable fromctx.executeTool()/codemode, derived from exposure), and hidden.codemode/deferredtools are callable even though they are not declared — that is expected and never reported as "unavailable". - Extension registrations from
getMcpServers(): names and the registering extension path only. These are session registrations, notmcp.jsonentries, and not connection state. - Which source provides
/mcpfromgetCommands(): Pi's built-in MCP integration registers/mcpwith a syntheticbuiltin:source, so a non-builtin/mcpcommand is the documented signal that built-in MCP support is replaced. A built-in/mcpis never reported as a replacement. - Call metadata from native
tool_execution_start/tool_execution_end: per server/tool call counts, error counts, mean/p95/max durations, nested (codemode/tool) calls viaparentToolCallId, and max concurrency. Counts are kept even when tracing is off. With/debug trace on,/debug mcp callsalso prints a bounded per-call detail table (toolCallId,parentToolCallId, server, tool, status, duration) read from the trace ring.
Identity is conservative: the namespace (mcp__<server>) resolves the server
exactly even when the server name contains __; when only the tool name is
available and the split would be ambiguous, or when the namespace and name
disagree, the server is reported as unknown, never guessed. Unknown exposure
is shown as ? (unknown callable state), not no.
What it cannot know (Pi exposes no public API) and therefore reports as
unavailable, pointing you at /mcp and pi mcp list:
| Fact | Why unavailable |
|---|---|
| connection status (connected/failed/needs-auth), reconnect state | not exposed to extensions |
enabled/disabled, the configured server list from mcp.json |
not exposed to extensions |
| server definitions, transport, url/command, env, headers, credentials, OAuth | not exposed to extensions |
| resources, prompts, logging, per-server errors | not exposed to extensions |
/debug doctor never warns that an adapter is "absent" and never claims a server
is connected: a tool being registered or a successful call being observed is
evidence of a call, not proof of the current connection state.
Privacy defaults
- Events carry metadata only: sizes, counts, ids, levels, statuses. Tool arguments/results, prompt text, thinking text, and image bytes are reduced to byte sizes and never stored.
- Message bodies are omitted from
/debug context messagesunless--detailis passed, and previews are then truncated and redacted. after_provider_responserecords only the status and the header count, never header values.- High-frequency
message_update/tool_execution_updateevents update counters only, so the ring buffer cannot be flooded. - Tracing is off by default. Counters and tool durations are cheap metadata
and are always maintained, so
/debug statsworks without enabling tracing. MCP call counts/errors/durations and parent correlation are likewise always maintained; only the per-call detail table needs tracing. - MCP metadata is metadata only: server/tool names, call ids, status flags,
counts, and durations. Tool arguments, results, and error text are never
stored, and
getMcpServers()config (url/command/env/headers/credentials) is never copied. Metadata labels (server, tool, path, id) are control-character neutralized and length-bounded; API exception text is bounded and hasBearer/token=secrets masked. - Exports run every record through
sanitizeValue, which removes values for keys matching authorization/cookie/api key/secret/token/bearer/credential, fully redacts base64 blobs, bounds depth/width/string length, and never includes thinking content. Export files are written with mode0600, refuse to overwrite without--force, refuse directory targets and symlinks, and refuse paths outsidecwd, the home directory, and the temp directory.
Statistics definitions (explicit denominators)
- Tool error rate =
tool_execution_endwithisError÷ alltool_execution_end. Started-but-unfinished calls are excluded. - Session toolResult error rate = errored
toolResultmessages ÷ alltoolResultmessages. - cacheReadShare / cacheWriteShare =
cacheRead(orcacheWrite) ÷ (input + cacheRead + cacheWrite), whereinputcounts non-cached prompt tokens as reported by the provider. - Cache hit ratio is not computed. Providers expose cache read/write token counts, not hit counts, and Pi exposes no such counter.
- Context usage percent is Pi's
ContextUsage.percent, which is already a 0–100 percentage of the context window (30means 30%, not 0.30). It is formatted directly and clamped to[0, 100];null(tokens unknown, e.g. right after compaction) is reported as unavailable. Health thresholds are applied on the normalized fraction:< 80%ok,≥ 80%info,≥ 90%warn. Internal ratios such as tool error rate andcacheReadShareremain 0–1 and are scaled by 100 for display. - Percentiles are nearest-rank over the last 200 durations per tool.
Testing
npm install
npm run typecheck # tsc --noEmit
npm run build # emits type declarations to dist/
npm test # vitest --run
npm run check # typecheck + tests
Coverage highlights: zero-context-pollution and observer-only guarantees,
static forbidden-API scan (including registerMcpServer/unregisterMcpServer
and mcp_servers_change), registration surface, native MCP identity parsing
(ambiguity/conflict), exposure classification (registered/declared/callable/
hidden), unknown-state degradation, MCP call aggregation (parallel calls, nested
codemode correlation, duplicate/unpaired ends, bounded maps), built-in vs
third-party /mcp detection, label/error sanitizing, ring-buffer capacity and
drops, parallel-tool correlation, metric denominators and cache accounting,
redaction, command dispatch, and export path safety.
Known limitations / not implemented in v0.1
- No per-extension handler timings/results, no complete extension registry,
and no retry/queue internal scheduling details: Pi exposes no public API for
these, so
/debug doctorlists them as deliberately unavailable. - No cache hit counts (see above).
- No MCP connection state, server definitions, transports, or credentials are
ever shown: Pi exposes no public API for them.
/debug mcpreports them asunavailableand points at/mcpandpi mcp list. A registered tool or an observed successful call is never presented as proof of a live connection. - MCP per-call detail requires
/debug trace onand is bounded by the trace ring capacity; the per-tool aggregate table needs no tracing. - Ambiguous/conflicting MCP tool names report the server as unknown, never a guessed server.
- Trace data is in-memory only and is cleared on
session_shutdown; it is not persisted to the session. Use/debug exportif you want a durable copy. - Tracing is off by default;
/debug timelineand/debug trace tailrequire/debug trace on. /debug context messages --detailpreviews are truncated and redacted by design; the extension never prints full sensitive bodies.- The default export path is
<cwd>/debug-exports/; pass an explicit path to write elsewhere (within cwd/home/temp).
License
MIT — see LICENSE.