Documentation Index Fetch the complete documentation index at: https://mintlify.com/earendil-works/pi/llms.txt
Use this file to discover all available pages before exploring further.
Extensions are TypeScript modules with a default export factory function that receives the ExtensionAPI object. They can register custom tools the LLM can call, intercept and block tool calls, add slash commands, bind keyboard shortcuts, and render custom TUI components — all without modifying Pi internals.
Extensions run with your full system permissions and can execute arbitrary code. Only load extensions from sources you trust.
Quick start
Create ~/.pi/agent/extensions/my-extension.ts:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent" ;
import { Type } from "typebox" ;
export default function ( pi : ExtensionAPI ) {
// React to lifecycle events
pi . on ( "session_start" , async ( _event , ctx ) => {
ctx . ui . notify ( "Extension loaded!" , "info" );
});
// Intercept tool calls
pi . on ( "tool_call" , async ( event , ctx ) => {
if ( event . toolName === "bash" && event . input . command ?. includes ( "rm -rf" )) {
const ok = await ctx . ui . confirm ( "Dangerous!" , "Allow rm -rf?" );
if ( ! ok ) return { block: true , reason: "Blocked by user" };
}
});
// Register a custom tool the LLM can call
pi . registerTool ({
name: "greet" ,
label: "Greet" ,
description: "Greet someone by name" ,
parameters: Type . Object ({
name: Type . String ({ description: "Name to greet" }),
}),
async execute ( toolCallId , params , signal , onUpdate , ctx ) {
return {
content: [{ type: "text" , text: `Hello, ${ params . name } !` }],
details: {},
};
},
});
// Register a slash command
pi . registerCommand ( "hello" , {
description: "Say hello" ,
handler : async ( args , ctx ) => {
ctx . ui . notify ( `Hello ${ args || "world" } !` , "info" );
},
});
}
Test it with the --extension (or -e) flag without installing:
Extension locations
Pi auto-discovers extensions from these directories:
Location Scope ~/.pi/agent/extensions/*.tsGlobal (all projects) ~/.pi/agent/extensions/*/index.tsGlobal (subdirectory) .pi/extensions/*.tsProject-local .pi/extensions/*/index.tsProject-local (subdirectory)
Extensions in auto-discovered locations support hot-reload via /reload. The -e flag loads extensions for the current run only — useful for quick tests.
You can also add paths directly in settings.json:
{
"extensions" : [
"/path/to/local/extension.ts" ,
"/path/to/local/extension/dir"
]
}
To share extensions with others, bundle them as a Pi package .
Extension styles
The simplest style. Place one .ts file in the extensions directory: ~/.pi/agent/extensions/
└── my-extension.ts
For multi-file extensions, create a subdirectory with an index.ts entry point: ~/.pi/agent/extensions/
└── my-extension/
├── index.ts
├── tools.ts
└── utils.ts
Package with npm dependencies
For extensions that need npm packages, add a package.json and run npm install: ~/.pi/agent/extensions/
└── my-extension/
├── package.json
├── node_modules/
└── src/
└── index.ts
{
"name" : "my-extension" ,
"dependencies" : {
"zod" : "^3.0.0"
},
"pi" : {
"extensions" : [ "./src/index.ts" ]
}
}
Imports from node_modules/ resolve automatically after npm install.
Key capabilities
Custom tools Register tools the LLM can call via pi.registerTool(). Tools appear in the system prompt and support custom TUI rendering.
Event interception Block or modify tool calls, inject context into the system prompt, customize compaction, and intercept user input before it reaches the agent.
Custom UI Prompt users via ctx.ui dialogs, add footer status lines, widgets above the editor, custom footers, and full TUI components via ctx.ui.custom().
Custom commands Register slash commands like /mycommand via pi.registerCommand(). Commands support argument autocomplete.
Keyboard shortcuts Bind any key combination via pi.registerShortcut().
Session persistence Persist extension state across restarts via pi.appendEntry(). State survives /reload and session resume.
Events
Lifecycle overview
Pi emits events throughout the session lifecycle. Extensions subscribe with pi.on(eventName, handler).
pi starts
│
├─► session_start { reason: "startup" }
└─► resources_discover { reason: "startup" }
user sends prompt
├─► input (can intercept, transform, or handle)
├─► before_agent_start (inject message, modify system prompt)
├─► agent_start
│
│ ┌─── turn (repeats while LLM calls tools) ───┐
│ ├─► turn_start
│ ├─► context (can modify messages)
│ ├─► before_provider_request
│ ├─► after_provider_response
│ │ ├─► tool_execution_start
│ │ ├─► tool_call (can block)
│ │ ├─► tool_execution_update
│ │ ├─► tool_result (can modify)
│ │ └─► tool_execution_end
│ └─► turn_end
│
└─► agent_end
/new or /resume
├─► session_before_switch (can cancel)
├─► session_shutdown
└─► session_start { reason: "new" | "resume" }
/compact or auto-compaction
├─► session_before_compact (can cancel or customize)
└─► session_compact
exit
└─► session_shutdown
Session events
session_start — fired when a session starts, loads, or reloads.
pi . on ( "session_start" , async ( event , ctx ) => {
// event.reason — "startup" | "reload" | "new" | "resume" | "fork"
ctx . ui . notify ( `Session started: ${ event . reason } ` , "info" );
});
session_before_switch — fired before /new or /resume. Return { cancel: true } to abort.
pi . on ( "session_before_switch" , async ( event , ctx ) => {
if ( event . reason === "new" ) {
const ok = await ctx . ui . confirm ( "Clear?" , "Start a new session?" );
if ( ! ok ) return { cancel: true };
}
});
session_shutdown — fired before the extension runtime tears down. Use for cleanup.
pi . on ( "session_shutdown" , async ( event , ctx ) => {
// event.reason — "quit" | "reload" | "new" | "resume" | "fork"
});
session_before_compact / session_compact — fired on compaction. Return a custom summary or cancel.
pi . on ( "session_before_compact" , async ( event , ctx ) => {
return {
compaction: {
summary: "My custom summary..." ,
firstKeptEntryId: event . preparation . firstKeptEntryId ,
tokensBefore: event . preparation . tokensBefore ,
}
};
});
Agent events
before_agent_start — fires after the user submits a prompt, before the agent loop. Can inject a message or modify the system prompt.
pi . on ( "before_agent_start" , async ( event , ctx ) => {
return {
message: {
customType: "my-extension" ,
content: "Additional context for the LLM" ,
display: true ,
},
systemPrompt: event . systemPrompt + " \n\n Extra instructions..." ,
};
});
agent_start / agent_end — fire once per user prompt.
turn_start / turn_end — fire for each turn (one LLM response plus its tool calls).
context — fires before each LLM call. Modify the message list non-destructively.
pi . on ( "context" , async ( event , ctx ) => {
const filtered = event . messages . filter ( m => ! shouldPrune ( m ));
return { messages: filtered };
});
tool_call — fires before a tool executes. Can block. Mutate event.input in place to patch arguments.
import { isToolCallEventType } from "@earendil-works/pi-coding-agent" ;
pi . on ( "tool_call" , async ( event , ctx ) => {
if ( isToolCallEventType ( "bash" , event )) {
// event.input is typed as { command: string; timeout?: number }
if ( event . input . command . includes ( "rm -rf" )) {
return { block: true , reason: "Dangerous command" };
}
}
});
tool_result — fires after tool execution, before the result is sent to the LLM. Can modify the result.
pi . on ( "tool_result" , async ( event , ctx ) => {
// Return partial patches — omitted fields keep their current values
return { content: [ ... event . content , { type: "text" , text: " [audited]" }] };
});
tool_execution_start / tool_execution_update / tool_execution_end — fire for tool execution lifecycle updates.
input — fires when user input is received, before skill and template expansion. Can transform or handle the input.
pi . on ( "input" , async ( event , ctx ) => {
// Transform: rewrite input text
if ( event . text . startsWith ( "?quick " ))
return { action: "transform" , text: `Respond briefly: ${ event . text . slice ( 7 ) } ` };
// Handle: skip the agent entirely
if ( event . text === "ping" ) {
ctx . ui . notify ( "pong" , "info" );
return { action: "handled" };
}
return { action: "continue" };
});
Model events
model_select — fires when the model changes via /model, Ctrl+P, or session restore.
pi . on ( "model_select" , async ( event , ctx ) => {
// event.model, event.previousModel, event.source
ctx . ui . setStatus ( "model" , ` ${ event . model . provider } / ${ event . model . id } ` );
});
thinking_level_select — fires when the thinking level changes (notification only).
Resource events
resources_discover — fires after session_start. Return additional skill, prompt, and theme paths.
pi . on ( "resources_discover" , async ( event , _ctx ) => {
return {
skillPaths: [ "/path/to/skills" ],
promptPaths: [ "/path/to/prompts" ],
themePaths: [ "/path/to/themes" ],
};
});
ExtensionAPI methods
pi.on(event, handler)
Subscribe to lifecycle events. See Events for all event names and return values.
Register a custom tool callable by the LLM. Tools appear in the system prompt when promptSnippet is set.
import { Type } from "typebox" ;
import { StringEnum } from "@earendil-works/pi-ai" ;
pi . registerTool ({
name: "my_tool" ,
label: "My Tool" ,
description: "What this tool does" ,
promptSnippet: "List or add items in the project todo list" ,
promptGuidelines: [
"Use my_tool for todo planning instead of direct file edits."
],
parameters: Type . Object ({
action: StringEnum ([ "list" , "add" ] as const ),
text: Type . Optional ( Type . String ()),
}),
async execute ( toolCallId , params , signal , onUpdate , ctx ) {
onUpdate ?.({ content: [{ type: "text" , text: "Working..." }] });
return {
content: [{ type: "text" , text: "Done" }],
details: { result: "..." },
};
},
});
Use StringEnum from @earendil-works/pi-ai for string enums instead of Type.Union/Type.Literal. Type.Union does not work with Google’s API.
pi.registerCommand(name, options)
Register a slash command. Commands can include argument autocomplete.
pi . registerCommand ( "stats" , {
description: "Show session statistics" ,
handler : async ( args , ctx ) => {
const count = ctx . sessionManager . getEntries (). length ;
ctx . ui . notify ( ` ${ count } entries` , "info" );
},
});
pi.registerShortcut(shortcut, options)
Register a keyboard shortcut.
pi . registerShortcut ( "ctrl+shift+p" , {
description: "Toggle plan mode" ,
handler : async ( ctx ) => {
ctx . ui . notify ( "Toggled!" , "info" );
},
});
pi.registerFlag(name, options)
Register a CLI flag that can be passed when starting Pi.
pi . registerFlag ( "plan" , {
description: "Start in plan mode" ,
type: "boolean" ,
default: false ,
});
if ( pi . getFlag ( "plan" )) {
// Plan mode enabled
}
pi.sendUserMessage(content, options?)
Send a user message as if typed by the user. Always triggers a turn.
pi . sendUserMessage ( "What is 2+2?" );
// During streaming, specify delivery mode
pi . sendUserMessage ( "Focus on error handling" , { deliverAs: "steer" });
pi . sendUserMessage ( "Then summarize" , { deliverAs: "followUp" });
pi.appendEntry(customType, data?)
Persist extension state to the session file. Does not participate in LLM context. Survives restarts.
pi . appendEntry ( "my-state" , { count: 42 });
// Restore on reload
pi . on ( "session_start" , async ( _event , ctx ) => {
for ( const entry of ctx . sessionManager . getEntries ()) {
if ( entry . type === "custom" && entry . customType === "my-state" ) {
// Reconstruct state from entry.data
}
}
});
pi.registerProvider(name, config)
Register or override a model provider. Useful for proxies, custom endpoints, or dynamically discovered models.
export default async function ( pi : ExtensionAPI ) {
const response = await fetch ( "http://localhost:1234/v1/models" );
const payload = await response . json ();
pi . registerProvider ( "local-openai" , {
baseUrl: "http://localhost:1234/v1" ,
apiKey: "LOCAL_OPENAI_API_KEY" ,
api: "openai-completions" ,
models: payload . data . map (( model ) => ({
id: model . id ,
name: model . name ?? model . id ,
reasoning: false ,
input: [ "text" ],
cost: { input: 0 , output: 0 , cacheRead: 0 , cacheWrite: 0 },
contextWindow: model . context_window ?? 128000 ,
maxTokens: model . max_tokens ?? 4096 ,
})),
});
}
An async factory function is awaited before Pi startup continues, so registered models are available immediately including to pi --list-models.
ExtensionContext
All event handlers receive ctx: ExtensionContext.
ctx.ui
UI methods for user interaction.
// Dialogs
const choice = await ctx . ui . select ( "Pick one:" , [ "A" , "B" , "C" ]);
const ok = await ctx . ui . confirm ( "Delete?" , "This cannot be undone" );
const name = await ctx . ui . input ( "Name:" , "placeholder" );
const text = await ctx . ui . editor ( "Edit:" , "prefilled text" );
// Non-blocking notification
ctx . ui . notify ( "Done!" , "info" ); // "info" | "warning" | "error"
// Footer status (persistent until cleared)
ctx . ui . setStatus ( "my-ext" , "Processing..." );
ctx . ui . setStatus ( "my-ext" , undefined ); // Clear
// Widget above or below the editor
ctx . ui . setWidget ( "my-widget" , [ "Line 1" , "Line 2" ]);
ctx . ui . setWidget ( "my-widget" , [ "Line 1" ], { placement: "belowEditor" });
ctx . ui . setWidget ( "my-widget" , undefined ); // Clear
// Terminal title
ctx . ui . setTitle ( "pi - my-project" );
Dialogs support timeout for auto-dismissal with a live countdown:
const confirmed = await ctx . ui . confirm (
"Timed Confirmation" ,
"This dialog auto-cancels in 5 seconds." ,
{ timeout: 5000 }
);
ctx.sessionManager
Read-only access to session state.
ctx . sessionManager . getEntries () // All entries
ctx . sessionManager . getBranch () // Current branch
ctx . sessionManager . getLeafId () // Current leaf entry ID
ctx.signal
The current agent abort signal. Pass to fetch(), model calls, or other abort-aware operations.
pi . on ( "tool_result" , async ( event , ctx ) => {
const response = await fetch ( "https://example.com/api" , {
method: "POST" ,
body: JSON . stringify ( event ),
signal: ctx . signal ,
});
});
ctx.cwd
Current working directory.
ctx.hasUI
false in print mode (-p) and JSON mode. Check before calling UI methods in non-interactive contexts.
Tools are registered with a name, description, TypeBox schema for parameters, and an execute function. The LLM uses the description to decide when to call the tool.
import { Type } from "typebox" ;
import { StringEnum } from "@earendil-works/pi-ai" ;
pi . registerTool ({
name: "my_tool" ,
label: "My Tool" ,
description: "Manage a list of items" ,
parameters: Type . Object ({
action: StringEnum ([ "list" , "add" , "remove" ] as const ),
item: Type . Optional ( Type . String ({ description: "Item text for add/remove" })),
}),
async execute ( toolCallId , params , signal , onUpdate , ctx ) {
if ( signal ?. aborted ) {
return { content: [{ type: "text" , text: "Cancelled" }] };
}
// Stream progress
onUpdate ?.({ content: [{ type: "text" , text: "Processing..." }] });
return {
content: [{ type: "text" , text: `Action: ${ params . action } ` }],
details: { items: [] },
};
},
});
To signal a tool error, throw from execute. The thrown error is caught, reported to the LLM with isError: true, and execution continues.
Extensions can also override built-in tools (read, bash, edit, write, grep, find, ls) by registering a tool with the same name.
Custom UI
For complex interactions, use ctx.ui.custom(). This temporarily replaces the editor with your component until done() is called.
import { Text } from "@earendil-works/pi-tui" ;
const result = await ctx . ui . custom < boolean >(( tui , theme , keybindings , done ) => {
const text = new Text ( "Press Enter to confirm, Escape to cancel" , 1 , 1 );
text . onKey = ( key ) => {
if ( key === "return" ) done ( true );
if ( key === "escape" ) done ( false );
return true ;
};
return text ;
});
Available imports
Package Purpose @earendil-works/pi-coding-agentExtension types (ExtensionAPI, ExtensionContext, events) typeboxSchema definitions for tool parameters @earendil-works/pi-aiAI utilities (StringEnum for Google-compatible enums) @earendil-works/pi-tuiTUI components for custom rendering
Node.js built-ins (node:fs, node:path, etc.) are also available. Extensions load via jiti , so TypeScript works without compilation.
Skills Package instruction sets the agent loads on demand.
Pi packages Bundle and share extensions via npm or git.
Themes Customize Pi’s terminal appearance.
Prompt templates Reusable prompts that expand with a slash command.