Create a pi extension for this project. Use when the user wants to add custom tools, commands, event handlers, or UI interactions to pi.
Install
npx skillscat add joshuajbrunner/pi-config/create-extension Install via the SkillsCat registry.
SKILL.md
Create Extension
Create pi extensions that add custom tools, commands, event handlers, and UI interactions.
Extension Locations
| Location | Scope |
|---|---|
.pi/extensions/*.ts |
Project-local |
.pi/extensions/*/index.ts |
Project-local (subdirectory with dependencies) |
~/.pi/agent/extensions/*.ts |
Global (all projects) |
Available Imports
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
import { Type } from 'typebox'; // Schema definitions for tool parameters
import { StringEnum } from '@earendil-works/pi-ai'; // Google-compatible enumsNode.js built-ins (node:fs, node:path, etc.) are also available.
Basic Extension Template
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
import { Type } from 'typebox';
export default function (pi: ExtensionAPI) {
// Subscribe to events
pi.on('session_start', async (_event, ctx) => {
ctx.ui.notify('Extension loaded!', 'info');
});
// Register a custom tool (callable by the LLM)
pi.registerTool({
name: 'my_tool',
label: 'My Tool',
description: 'What this tool does',
parameters: Type.Object({
input: Type.String({ description: 'The input to process' }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return {
content: [{ type: 'text', text: `Result: ${params.input}` }],
details: {},
};
},
});
// Register a command (user-invoked via /command)
pi.registerCommand('mycommand', {
description: 'Description shown in command list',
handler: async (args, ctx) => {
ctx.ui.notify(`Command executed with: ${args}`, 'info');
},
});
}Key Capabilities
Event Handlers
// Block dangerous operations
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' };
}
});
// Inject context before agent starts
pi.on('before_agent_start', async (event, ctx) => {
return {
systemPrompt: event.systemPrompt + '\n\nExtra instructions...',
};
});
// Cleanup on shutdown
pi.on('session_shutdown', async (_event, ctx) => {
// Save state, close connections, etc.
});UI Methods (via ctx.ui)
// Notifications
ctx.ui.notify('Message', 'info'); // "info" | "success" | "warning" | "error"
// Prompts (blocking, returns user input)
const ok = await ctx.ui.confirm('Title', 'Are you sure?');
const choice = await ctx.ui.select('Pick one:', ['a', 'b', 'c']);
const text = await ctx.ui.input('Enter value:', 'default');
// Status indicators
ctx.ui.setStatus('my-ext', 'Processing...'); // Footer status
ctx.ui.setWidget('my-ext', ['Line 1', 'Line 2']); // Widget above editorTool Parameters with TypeBox
import { Type } from 'typebox';
import { StringEnum } from '@earendil-works/pi-ai';
parameters: Type.Object({
// Required string
name: Type.String({ description: 'The name' }),
// Optional string
description: Type.Optional(Type.String()),
// Enum (use StringEnum for Google compatibility)
action: StringEnum(['create', 'update', 'delete'] as const),
// Number with constraints
count: Type.Number({ minimum: 1, maximum: 100 }),
// Boolean
force: Type.Boolean({ default: false }),
// Array
tags: Type.Array(Type.String()),
});Keyboard Shortcuts
pi.registerShortcut('ctrl+shift+p', {
description: 'Toggle my feature',
handler: async (ctx) => {
ctx.ui.notify('Shortcut triggered!');
},
});Common Events
| Event | Description |
|---|---|
session_start |
Session started/loaded/reloaded |
session_shutdown |
Pi exiting |
tool_call |
Before tool executes (can block) |
tool_result |
After tool executes (can modify) |
before_agent_start |
Before LLM call (can inject context) |
agent_start / agent_end |
Agent turn lifecycle |
input |
User input received (can transform) |
Extension with Dependencies
For extensions needing npm packages, use a subdirectory:
.pi/extensions/my-extension/
├── package.json
├── node_modules/
└── index.ts// package.json
{
"name": "my-extension",
"dependencies": {
"lodash": "^4.17.0"
},
"pi": {
"extensions": ["./index.ts"]
}
}Run npm install in the extension directory.
Testing
Test with the -e flag before placing in .pi/extensions/:
pi -e ./my-extension.tsUse /reload to hot-reload after changes.