pi-extension-builder
Build, debug, and package pi coding-agent extensions. Use when the user wants to create or modify a pi extension, register custom tools or commands, handle lifecycle events, or package extensions for sharing.
- License 许可
- GPL-3.0
- 所属仓库
- Artawower/kaizen
- 最近更新
- 2026-11-10
- Stars
- ★ 2
- Forks
- 0
01 前言 Front Matter
- name
- pi-extension-builder
- description
- Build, debug, and package pi coding-agent extensions. Use when the user wants to create or modify a pi extension, register custom tools or commands, handle lifecycle events, or package extensions for sharing.
03 SKILL.md 正文
Pi Extension Builder
Freshness: Fetch live docs when implementing event handlers, stateful tools, file-mutating tools, or custom rendering:
fetch_content https://raw.githubusercontent.com/badlogic/pi-mono/refs/heads/main/packages/coding-agent/docs/extensions.md
Pre-conditions
- Check for existing extensions before creating a new one:
- Global:
~/.pi/agent/extensions/ - Project-local:
.pi/extensions/
- Global:
- Confirm with the user: global or project-local scope?
- Are npm dependencies needed? → determines extension structure below.
What are you building?
| Goal | Tool |
|---|---|
| Custom tool for the LLM | pi.registerTool() |
Slash command /mycommand |
pi.registerCommand() |
| Block a tool call | pi.on("tool_call") → return { block: true, reason? } |
| Modify tool call arguments | pi.on("tool_call") → mutate event.input in place (return only blocks) |
| Modify tool output | pi.on("tool_result") → return { content?, details?, isError? } |
| Intercept user input | pi.on("input") → return { action: "transform", text: ... } |
| Inject context before each turn | pi.on("before_agent_start") → return { message?, systemPrompt? } |
| User interaction (confirm, select, input) | ctx.ui.* |
| Custom TUI rendering | renderCall / renderResult |
| Register dynamic skills/prompts/themes | pi.on("resources_discover") → return { skillPaths?, promptPaths?, themePaths? } |
| Override built-in tool (bash, read, write…) | pi.registerTool({ name: "bash", ... }) — same name replaces built-in |
| Persist state across sessions | pi.appendEntry() / details pattern |
| Share via npm/git | package.json with pi key |
Extension structure
Single file (simple extensions):
~/.pi/agent/extensions/my-extension.ts
Directory (multiple modules):
~/.pi/agent/extensions/my-extension/
├── index.ts
└── helpers.ts
Package (needs npm dependencies):
~/.pi/agent/extensions/my-extension/
├── package.json # with "pi" key
├── node_modules/
└── src/index.ts
package.json for a package:
{
"name": "my-extension",
"dependencies": { "zod": "^3.0.0" },
"pi": { "extensions": ["./src/index.ts"] }
}
Extension skeleton
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import {
truncateHead,
DEFAULT_MAX_BYTES,
DEFAULT_MAX_LINES,
} from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { StringEnum } from "@mariozechner/pi-ai"; // required for Google model compatibility
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does (shown to LLM)",
promptSnippet:
"One-liner for Available tools section — omit and LLM won't discover this tool",
parameters: Type.Object({
action: StringEnum(["list", "add"] as const),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
if (signal?.aborted)
return { content: [{ type: "text", text: "Cancelled" }] };
// throw to signal failure — returning any value always succeeds (isError: false)
const { stdout } = await pi.exec("git", ["status"], { signal });
const truncation = truncateHead(stdout, {
maxLines: DEFAULT_MAX_LINES,
maxBytes: DEFAULT_MAX_BYTES,
});
return { content: [{ type: "text", text: truncation.content }] };
},
});
pi.on("session_shutdown", async () => {
// close connections, file watchers, timers
});
}
Workflow
- Define the goal — tool, command, event handler, or a combination
- Choose scope — global vs project-local
- Choose structure — single file / directory / package
- Scaffold —
export default function (pi: ExtensionAPI) { ... } - Register capabilities —
registerTool,registerCommand,pi.on - Handle state — decide if
detailspattern is needed for reconstruction - Test —
pi -e ./extension.tsor/reloadafter placing in auto-discovery path - Package if needed — add
package.jsonwithpikey for sharing
Critical pitfalls
- Google compatibility: use
StringEnum(["a", "b"] as const), neverType.Enum()or union types - Error signaling:
throw new Error(...)fromexecute()to report failure — returning any value always succeeds (isError: false) - Output truncation: use
truncateHead/truncateTailfrom@mariozechner/pi-coding-agentfor output over 50KB/2000 lines — untruncated output breaks context - promptSnippet: without it, the LLM won't see the tool in the
Available toolssection and won't discover it - tool_call mutation: return
{ block: true }to block, mutateevent.inputin place to modify args — return value does NOT modify arguments - Non-interactive modes: always check
ctx.hasUIbefore calling UI methods - State and branches: store state in tool result
details; reconstruct fromctx.sessionManager.getBranch()in BOTHsession_startANDsession_tree(tree navigation firessession_tree, notsession_start) - File-mutating tools: wrap with
withFileMutationQueue()— tool calls run in parallel, without the queue concurrent edits to the same file will clobber each other - sendUserMessage during streaming: must pass
{ deliverAs: "steer" | "followUp" }— calling without it while agent is streaming throws - Cleanup: always register
session_shutdownfor connections, file watchers, timers - Reload is terminal: code after
await ctx.reload()is not guaranteed to run — treat it as the last statement
Placement and scope
| Path | Scope | Hot-reload |
|---|---|---|
~/.pi/agent/extensions/*.ts |
Global | Yes (/reload) |
~/.pi/agent/extensions/*/index.ts |
Global (subdirectory) | Yes |
.pi/extensions/*.ts |
Project-local | Yes |
pi -e ./path.ts |
One-time test | No |
Available imports (no npm install needed)
| Package | Purpose |
|---|---|
@mariozechner/pi-coding-agent |
ExtensionAPI, event types, type guards, truncateHead, truncateTail, withFileMutationQueue |
@sinclair/typebox |
Type.Object, Type.String, etc. for parameters |
@mariozechner/pi-ai |
StringEnum for Google-compatible enums |
@mariozechner/pi-tui |
Text, Box, List, Input, Select |
04 同仓库更多 Skills
yt-analyze
Fetch YouTrack task + comments via MCP, critically analyze relevance and requirements, produce concise summary of what to implement, and build an implementation plan mapped to codebase. Trigger - YouTrack issue ID or request to "analyze task", "plan task", "break down YT issue".
youtrack-task
Analyzes a YouTrack issue, reviews comments and attachments, and proposes or creates actionable subtasks. Use when the user gives a YouTrack issue ID and wants synthesis, decomposition, or follow-up tasks.
youtrack-search
Searches YouTrack from natural language or native query syntax and returns a concise report with the most relevant issues. Use when the user wants to find issues, triage work, or translate a plain-language query into YouTrack search.
writing-for-agents
Writing documents for agents. Use when creating or editing skills, or modifying AGENTS.md or CLAUDE.md.
write-a-prd
Create a PRD through user interview, codebase exploration, and module design, then submit as a GitHub issue. Use when user wants to write a PRD, create a product requirements document, or plan a new feature.
vitest
Plan and write Vitest tests for changed code. Analyses git diff, checks existing tests, produces a concrete test plan using flat test structure (no describe/it blocks). Use when adding tests for new/changed code, improving Vitest coverage, or asking what to test after a change.