一键导入
pi-coding-agent-extensions
Build and use custom extensions for Pi Coding Agent, the open-source alternative to Claude Code
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Build and use custom extensions for Pi Coding Agent, the open-source alternative to Claude Code
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Create AI marketing videos and images using Arcads API — Seedance, Sora, Veo, Kling, Nano Banana, ChatGPT Image, and multi-step ad pipelines
Generate AI marketing videos and static image ads using the Arcads API with skills for Seedance 2.0, Sora 2, Veo 3.1, Kling 3.0, Nano Banana, and 37 Meta ad templates
Create AI marketing videos and images using Arcads API with Seedance 2.0, Sora 2, Veo 3.1, Kling 3.0, Nano Banana, and 37 static Meta ad templates
Generate AI marketing videos and images using Arcads creative stack (Seedance 2.0, Sora 2, Veo 3.1, Kling, Nano Banana, ChatGPT Image) from Claude Code or Cursor
Generate AI marketing videos and static image ads using Arcads external API with Seedance 2.0, Sora 2, Veo 3.1, Kling, Nano Banana, and 37-template Meta image library
Create AI marketing videos and static Meta image ads using the Arcads API with Seedance 2.0, Sora 2, Veo 3.1, Kling, Nano Banana, ChatGPT Image, and 37 static ad templates
| 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"] |
Skill by ara.so — Claude Code Skills collection.
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.
All three are required:
# 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
# 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=...
Pi does NOT auto-load .env files. You must source keys before launching:
Option 1 - Manual source:
source .env && pi
Option 2 - Shell alias (add to ~/.zshrc):
alias pi='source $(pwd)/.env && pi'
Option 3 - Use just (automatic):
just pi # .env is loaded automatically
pi -e extensions/minimal.ts
pi -e extensions/minimal.ts -e extensions/cross-agent.ts
# 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
Extensions export a PiExtension object with lifecycle hooks:
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;
Compact UI showing model name and context usage:
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;
Add a new tool the agent can call:
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;
}
Create a persistent widget that updates in real-time:
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;
Intercept and block dangerous commands before execution:
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:
blocked_patterns:
- "rm -rf /"
- "dd if="
- ":(){ :|:& };:"
- "curl | bash"
- "wget | sh"
allowed_paths:
- "/tmp"
- "./src"
- "./tests"
- "./extensions"
/sub)Offload tasks to background Pi agents:
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
Dispatcher pattern with specialist agents:
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
const agentPath = `/tmp/pi-agent-${args.agent_name}.json`;
writeFileSync(agentPath, JSON.stringify({
systemPrompt: agent.system_prompt,
model: agent.model || app.session.model.name
}));
// Execute Pi with agent config
const result = execSync(
`pi -c ${agentPath} -p "${args.task}"`,
{ encoding: "utf-8", env: process.env }
);
return {
agent: args.agent_name,
output: result
};
}
});
}
} satisfies PiExtension;
Example .pi/agents/teams.yaml:
agents:
backend:
description: Node.js/TypeScript backend specialist
system_prompt: |
You are an expert backend engineer specializing in Node.js, TypeScript,
Express, and PostgreSQL. Focus on API design, database schemas, and server architecture.
model: gpt-4o
frontend:
description: React/Next.js frontend specialist
system_prompt: |
You are an expert frontend engineer specializing in React, Next.js, and Tailwind.
Focus on component architecture, state management, and responsive design.
model: claude-opus-4
security:
description: Security auditing specialist
system_prompt: |
You are a security expert. Audit code for vulnerabilities, suggest fixes,
and ensure best practices for authentication, authorization, and data protection.
model: gpt-4o
Unix socket-based peer-to-peer messaging:
import type { PiExtension, PiApp } from "pi";
import { createServer, connect } from "net";
import { existsSync, mkdirSync, unlinkSync } from "fs";
const SOCKET_DIR = "/tmp/pi-coms";
const messages = new Map<string, string[]>();
export default {
name: "coms",
version: "1.0.0",
async init(app: PiApp) {
if (!existsSync(SOCKET_DIR)) mkdirSync(SOCKET_DIR);
const socketPath = `${SOCKET_DIR}/${app.session.id}.sock`;
// Create local server
const server = createServer((socket) => {
socket.on("data", (data) => {
const msg = JSON.parse(data.toString());
if (!messages.has(msg.from)) messages.set(msg.from, []);
messages.get(msg.from)!.push(msg.content);
});
});
server.listen(socketPath);
// Register communication tools
app.registerTool({
name: "coms_list",
description: "List all available Pi agents on this machine",
parameters: { type: "object", properties: {} },
handler: async () => {
const agents = readdirSync(SOCKET_DIR)
.filter(f => f.endsWith(".sock"))
.map(f => f.replace(".sock", ""));
return { agents };
}
});
app.registerTool({
name: "coms_send",
description: "Send a message to another Pi agent",
parameters: {
type: "object",
properties: {
to: { type: "string", description: "Target agent session ID" },
message: { type: "string", description: "Message content" }
},
required: ["to", "message"]
},
handler: async (args: { to: string; message: string }) => {
const targetSocket = `${SOCKET_DIR}/${args.to}.sock`;
const client = connect(targetSocket);
client.write(JSON.stringify({
from: app.session.id,
content: args.message
}));
client.end();
return { sent: true };
}
});
app.registerTool({
name: "coms_get",
description: "Retrieve messages from other agents",
parameters: { type: "object", properties: {} },
handler: async () => {
const all = Array.from(messages.entries()).map(([from, msgs]) => ({
from,
messages: msgs
}));
messages.clear();
return { messages: all };
}
});
},
async destroy(app: PiApp) {
const socketPath = `${SOCKET_DIR}/${app.session.id}.sock`;
if (existsSync(socketPath)) unlinkSync(socketPath);
}
} satisfies PiExtension;
HTTP/SSE hub for LAN or remote agent coordination:
Start the hub server:
# Local development (127.0.0.1)
just coms-net-server
# LAN-accessible (requires auth token)
export PI_COMS_NET_AUTH_TOKEN=your-secret-token
just coms-net-server-lan
Connect agents:
# Terminal 1
just coms1 # Uses gpt-4o
# Terminal 2 (different machine on same network)
just coms2 # Uses claude-opus-4
The extension (extensions/coms-net.ts) provides tools:
coms_net_list - List connected agentscoms_net_send - Send message to specific agentcoms_net_broadcast - Broadcast to all agentscoms_net_get - Retrieve pending messagesLoad commands and skills from Claude Code, Gemini, Codex directories:
import type { PiExtension, PiApp } from "pi";
import { readdirSync, readFileSync, existsSync } from "fs";
export default {
name: "cross-agent",
version: "1.0.0",
async init(app: PiApp) {
const agentDirs = [".claude", ".gemini", ".codex"];
for (const dir of agentDirs) {
if (!existsSync(dir)) continue;
// Load skills
const skillsDir = `${dir}/skills`;
if (existsSync(skillsDir)) {
for (const file of readdirSync(skillsDir)) {
if (!file.endsWith(".md")) continue;
const content = readFileSync(`${skillsDir}/${file}`, "utf-8");
app.addContextDocument({
name: `${dir}-skill-${file}`,
content
});
}
}
// Load agents
const agentsDir = `${dir}/agents`;
if (existsSync(agentsDir)) {
for (const file of readdirSync(agentsDir)) {
if (!file.endsWith(".md")) continue;
const agentName = file.replace(".md", "");
const systemPrompt = readFileSync(`${agentsDir}/${file}`, "utf-8");
app.registerCommand({
name: `${dir.slice(1)}-${agentName}`,
description: `Switch to ${agentName} agent from ${dir}`,
handler: async () => {
app.session.systemPrompt = systemPrompt;
return `Switched to ${agentName}`;
}
});
}
}
}
}
} satisfies PiExtension;
touch extensions/my-feature.ts
import type { PiExtension, PiApp } from "pi";
export default {
name: "my-feature",
version: "1.0.0",
description: "What this extension does",
async init(app: PiApp) {
// Setup: register tools, widgets, commands
console.log("Extension initialized");
},
async beforeTurn(app: PiApp, turn: Turn) {
// Pre-process agent turns
},
async afterTurn(app: PiApp, turn: Turn) {
// Post-process agent turns
},
async destroy(app: PiApp) {
// Cleanup
}
} satisfies PiExtension;
Edit justfile:
ext-my-feature:
#!/usr/bin/env bash
set -euo pipefail
source .env
pi -e extensions/my-feature.ts
just ext-my-feature
.pi/settings.jsonPi workspace settings:
{
"model": "gpt-4o",
"temperature": 0.7,
"maxTokens": 4096,
"systemPrompt": "You are a helpful coding assistant.",
"theme": "default"
}
.pi/damage-control-rules.yamlSafety rules for damage-control extension:
blocked_patterns:
- "rm -rf /"
- "> /dev/sda"
- "dd if="
- "mkfs"
- "curl | bash"
allowed_paths:
- "./src"
- "./tests"
- "./extensions"
- "/tmp"
max_file_size_mb: 10
.pi/agents/teams.yamlAgent team definitions:
agents:
researcher:
description: Research and documentation specialist
model: gpt-4o
system_prompt: |
You are a research specialist. Your job is to find accurate, up-to-date
information from documentation, APIs, and code examples.
implementer:
description: Code implementation specialist
model: claude-opus-4
system_prompt: |
You are an implementation specialist. Given requirements and research,
you write clean, tested, production-ready code.
const state = {
taskList: [] as string[],
completedCount: 0
};
export default {
async init(app: PiApp) {
app.registerTool({
name: "add_task",
parameters: {
type: "object",
properties: {
task: { type: "string" }
},
required: ["task"]
},
handler: async (args: { task: string }) => {
state.taskList.push(args.task);
return { total: state.taskList.length };
}
});
},
async afterTurn(app: PiApp, turn: Turn) {
// State persists across turns
console.log(`Tasks: ${state.taskList.length}`);
}
} satisfies PiExtension;
const widget = new Widget({
position: "footer",
render: () => dynamicContent
});
let dynamicContent = "Initial";
// Update from anywhere
setInterval(() => {
dynamicContent = new Date().toISOString();
app.ui.refresh();
}, 1000);
app.registerCommand({
name: "mycommand",
description: "Does something useful",
handler: async (args: string[]) => {
// args is array of space-separated arguments
const result = await doSomething(args);
return result; // Displayed to user
}
});
Usage in Pi:
/mycommand arg1 arg2 arg3
Check TypeScript syntax:
bun run tsc --noEmit extensions/my-extension.ts
Verify extension export:
// Must have default export satisfying PiExtension
export default { ... } satisfies PiExtension;
Verify environment:
echo $OPENAI_API_KEY # Should print your key
Check .env file:
cat .env # Verify keys are set
source .env # Load into current shell
Check position:
// Valid positions: "footer", "above-editor", "sidebar"
const widget = new Widget({ position: "footer", ... });
Call refresh after updates:
widget.content = "New content";
app.ui.refresh();
Verify description clarity:
app.registerTool({
name: "my_tool",
description: "Clear, specific description of WHEN to use this tool", // Important!
// ...
});
Check parameter schema:
parameters: {
type: "object",
properties: {
required_param: {
type: "string",
description: "What this parameter does" // Must have description
}
},
required: ["required_param"] // Don't forget this
}
Check socket directory:
ls -la /tmp/pi-coms/ # Should show .sock files
Verify session IDs:
// In extension
console.log("My session ID:", app.session.id);
For coms-net, check server is running:
curl http://localhost:3141/health
# Should return: {"status":"ok","agents":0}
Combine extensions for complex workflows:
# Safety-audited team orchestration with live monitoring
pi -e extensions/damage-control.ts \
-e extensions/agent-team.ts \
-e extensions/tool-counter-widget.ts
Create a custom workflow recipe in justfile:
workflow-full-stack:
#!/usr/bin/env bash
set -euo pipefail
source .env
pi \
-e extensions/agent-team.ts \
-e extensions/damage-control.ts \
-e extensions/tool-counter.ts \
-e extensions/cross-agent.ts \
-p "Build a full-stack app with backend and frontend teams"
Extensions in this repository are examples. To contribute:
extensions/justfilespecs/