- name
- pi-coding-agent-extensions
- description
- Build and use custom extensions for Pi Coding Agent, the open-source alternative to Claude Code
- triggers
- ["how do I extend pi coding agent","create a custom pi extension","add a widget to pi agent","build a pi coding agent extension","customize pi agent ui","make a pi agent tool","orchestrate multiple pi agents","pi agent communication between agents"]
# Pi Coding Agent Extensions
> Skill by [ara.so](https://ara.so) — Claude Code Skills collection.
## Overview
Pi Coding Agent is an open-source terminal-based AI coding assistant similar to Claude Code. This project (`pi-vs-claude-code`) provides a collection of production-ready extensions that showcase Pi's customization capabilities: custom UI widgets, multi-agent orchestration, safety auditing, cross-agent integrations, and inter-agent communication.
Extensions are TypeScript files that hook into Pi's lifecycle events to add tools, modify UI, intercept commands, and coordinate with other agents.
## Prerequisites
All three are required:
```bash
# Install Bun (runtime & package manager)
curl -fsSL https://bun.sh/install | bash
# Install just (task runner)
brew install just # macOS
# or cargo install just # cross-platform
# Install Pi Coding Agent CLI
# See https://github.com/mariozechner/pi-coding-agent
```
## Installation
```bash
# Clone the extensions repository
git clone https://github.com/disler/pi-vs-claude-code.git
cd pi-vs-claude-code
# Install dependencies
bun install
# Copy environment template
cp .env.sample .env
# Edit .env and add your API keys:
# OPENAI_API_KEY=sk-...
# ANTHROPIC_API_KEY=sk-ant-...
# GEMINI_API_KEY=...
# OPENROUTER_API_KEY=...
```
## API Key Setup
Pi does NOT auto-load `.env` files. You must source keys before launching:
**Option 1 - Manual source:**
```bash
source .env && pi
```
**Option 2 - Shell alias (add to `~/.zshrc`):**
```bash
alias pi='source $(pwd)/.env && pi'
```
**Option 3 - Use just (automatic):**
```bash
just pi # .env is loaded automatically
```
## Running Extensions
### Single Extension
```bash
pi -e extensions/minimal.ts
```
### Stack Multiple Extensions
```bash
pi -e extensions/minimal.ts -e extensions/cross-agent.ts
```
### Using Just Recipes
```bash
# List all available recipes
just
# Common recipes
just ext-minimal # Compact footer with context meter
just ext-tool-counter # Rich footer with tool usage stats
just ext-subagent-widget # Spawn background agents with live widgets
just ext-agent-team # Multi-agent orchestration dashboard
just ext-damage-control # Safety auditing with path controls
just ext-pi-pi # Meta-agent that builds Pi agents
just coms-net-server # Start agent-to-agent communication hub
just coms # Connect to coms hub
just all # Open every extension in separate terminals
```
## Extension Architecture
Extensions export a `PiExtension` object with lifecycle hooks:
```typescript
import type { PiExtension, PiApp } from "pi";
export default {
name: "my-extension",
version: "1.0.0",
// Called when extension loads
async init(app: PiApp) {
// Register tools, widgets, commands
},
// Called before agent processes a turn
async beforeTurn(app: PiApp, turn: Turn) {
// Intercept or modify the turn
},
// Called after agent completes a turn
async afterTurn(app: PiApp, turn: Turn) {
// Log, analyze, or trigger follow-up actions
},
// Called on shutdown
async destroy(app: PiApp) {
// Cleanup resources
}
} satisfies PiExtension;
```
## Key Extension Examples
### Minimal Footer
Compact UI showing model name and context usage:
```typescript
import type { PiExtension, PiApp } from "pi";
import { Widget } from "pi/ui";
export default {
name: "minimal",
version: "1.0.0",
async init(app: PiApp) {
const footer = new Widget({
position: "footer",
render: () => {
const model = app.session.model.name;
const usage = app.session.contextUsage;
const pct = Math.round(usage * 100);
const filled = Math.round(usage * 10);
const bar = "█".repeat(filled) + "░".repeat(10 - filled);
return `${model} [${bar}] ${pct}%`;
}
});
app.ui.addWidget(footer);
}
} satisfies PiExtension;
```
### Custom Tool Registration
Add a new tool the agent can call:
```typescript
import type { PiExtension, PiApp } from "pi";
export default {
name: "custom-tool",
version: "1.0.0",
async init(app: PiApp) {
app.registerTool({
name: "analyze_code_quality",
description: "Analyze code quality metrics for a file",
parameters: {
type: "object",
properties: {
file_path: {
type: "string",
description: "Path to the file to analyze"
}
},
required: ["file_path"]
},
handler: async (args: { file_path: string }) => {
const content = await app.fs.readFile(args.file_path);
// Run analysis logic
return {
lines: content.split("\n").length,
complexity: calculateComplexity(content),
issues: findIssues(content)
};
}
});
}
} satisfies PiExtension;
function calculateComplexity(code: string): number {
// Simplified cyclomatic complexity
const branches = (code.match(/if|while|for|case/g) || []).length;
return branches + 1;
}
function findIssues(code: string): string[] {
const issues: string[] = [];
if (code.includes("eval(")) issues.push("Dangerous eval() usage");
if (code.includes("TODO")) issues.push("Unfinished TODO items");
return issues;
}
```
### Live Widget Above Editor
Create a persistent widget that updates in real-time:
```typescript
import type { PiExtension, PiApp, Turn } from "pi";
import { Widget } from "pi/ui";
const toolCounts = new Map<string, number>();
export default {
name: "tool-counter-widget",
version: "1.0.0",
async init(app: PiApp) {
const widget = new Widget({
position: "above-editor",
height: 3,
render: () => {
const entries = Array.from(toolCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
return entries
.map(([tool, count]) => `${tool}: ${count}`)
.join(" | ") || "No tools used yet";
}
});
app.ui.addWidget(widget);
},
async afterTurn(app: PiApp, turn: Turn) {
// Count tool calls from this turn
for (const call of turn.toolCalls || []) {
toolCounts.set(call.name, (toolCounts.get(call.name) || 0) + 1);
}
app.ui.refresh();
}
} satisfies PiExtension;
```
### Safety Auditing (Damage Control)
Intercept and block dangerous commands before execution:
```typescript
import type { PiExtension, PiApp, Turn, ToolCall } from "pi";
import { readFileSync } from "fs";
import { parse } from "yaml";
interface DamageControlRules {
blocked_patterns: string[];
allowed_paths: string[];
}
export default {
name: "damage-control",
version: "1.0.0",
async beforeTurn(app: PiApp, turn: Turn) {
const rules: DamageControlRules = parse(
readFileSync(".pi/damage-control-rules.yaml", "utf-8")
);
for (const call of turn.toolCalls || []) {
if (call.name === "bash") {
const cmd = call.arguments.command;
// Check blocked patterns
for (const pattern of rules.blocked_patterns) {
if (cmd.includes(pattern)) {
throw new Error(
`🚨 BLOCKED: Command contains dangerous pattern "${pattern}"`
);
}
}
// Check path restrictions for file operations
if (cmd.match(/rm|mv|cp|>|>>/) && call.arguments.path) {
const isAllowed = rules.allowed_paths.some(
allowed => call.arguments.path.startsWith(allowed)
);
if (!isAllowed) {
throw new Error(
`🚨 BLOCKED: Path "${call.arguments.path}" is outside allowed directories`
);
}
}
}
}
}
} satisfies PiExtension;
```
Example `.pi/damage-control-rules.yaml`:
```yaml
blocked_patterns:
- "rm -rf /"
- "dd if="
- ":(){ :|:& };:"
- "curl | bash"
- "wget | sh"
allowed_paths:
- "/tmp"
- "./src"
- "./tests"
- "./extensions"
```
## Multi-Agent Orchestration
### Subagent Spawning (`/sub`)
Offload tasks to background Pi agents:
```typescript
import type { PiExtension, PiApp } from "pi";
import { Widget } from "pi/ui";
import { spawn } from "child_process";
const subagents = new Map<string, any>();
export default {
name: "subagent-widget",
version: "1.0.0",
async init(app: PiApp) {
// Register /sub command
app.registerCommand({
name: "sub",
description: "Spawn a background Pi agent for a task",
handler: async (args: string[]) => {
const task = args.join(" ");
const id = `sub-${Date.now()}`;
const widget = new Widget({
position: "above-editor",
height: 4,
render: () => `🤖 ${id}\n${subagents.get(id)?.status || "Starting..."}`
});
app.ui.addWidget(widget);
// Spawn Pi subprocess
const proc = spawn("pi", ["-p", task], {
env: process.env,
stdio: ["ignore", "pipe", "pipe"]
});
subagents.set(id, { proc, status: "Running...", widget });
proc.stdout.on("data", (data) => {
subagents.get(id).status = data.toString().slice(-200);
app.ui.refresh();
});
proc.on("close", (code) => {
subagents.get(id).status = `✓ Complete (exit ${code})`;
app.ui.refresh();
setTimeout(() => {
app.ui.removeWidget(widget);
subagents.delete(id);
}, 3000);
});
}
});
}
} satisfies PiExtension;
```
Usage:
```
/sub implement user authentication with bcrypt
```
### Agent Team Orchestration
Dispatcher pattern with specialist agents:
```typescript
import type { PiExtension, PiApp } from "pi";
import { readFileSync } from "fs";
import { parse } from "yaml";
import { execSync } from "child_process";
interface TeamConfig {
agents: {
[key: string]: {
description: string;
system_prompt: string;
model?: string;
}
};
}
export default {
name: "agent-team",
version: "1.0.0",
async init(app: PiApp) {
const config: TeamConfig = parse(
readFileSync(".pi/agents/teams.yaml", "utf-8")
);
app.registerTool({
name: "dispatch_agent",
description: "Delegate a task to a specialist agent",
parameters: {
type: "object",
properties: {
agent_name: {
type: "string",
enum: Object.keys(config.agents),
description: "Which specialist agent to use"
},
task: {
type: "string",
description: "The task to delegate"
}
},
required: ["agent_name", "task"]
},
handler: async (args: { agent_name: string; task: string }) => {
const agent = config.agents[args.agent_name];
if (!agent) throw new Error(`Unknown agent: ${args.agent_name}`);
// Write temporary agent config
View on GitHub