Warum solltest du Erweiterungen für Pi Agent schreiben?
Wenn du Pi Coding Agent bereits verwendet hast, weißt du, dass er ein "kleiner, aber feiner" KI-Programmierassistent für das Terminal ist – sein Kern tut im Wesentlichen eines: die Fähigkeiten eines LLM in den Terminal-Workflow zu integrieren. Was ihn wirklich "persönlich" macht, ist sein Erweiterungssystem (Extensions).
Pi-Erweiterungen sind TypeScript-Module, die Lebenszyklus-Ereignisse abonnieren, benutzerdefinierte Tools für das LLM registrieren, Slash-Befehle hinzufügen oder sogar TUI-Komponenten anpassen können. Im offiziellen Ökosystem gibt es bereits Community-Projekte wie pi-hosts (23 pts) und Parallel Pi agents (8 pts), aber tiefgehende technische Inhalte auf Deutsch sind noch rar.
Falls du Pi noch nicht installiert hast, lies am besten zuerst diese Einführung: Die 5 besten Open-Source-KI-Programmierassistenten 2026 im Vergleich – dort findest du auch eine Einordnung von Pi. Dieser Artikel konzentriert sich hingegen auf die Erweiterungsentwicklung und führt dich Schritt für Schritt durch 4 Praxis-Erweiterungen.
Architektur von Pi Agent im Detail
Kernkomponenten
Pi's Architektur lässt sich in einem Satz zusammenfassen: Minimaler Kern + erweiterbare Grenzen.
┌─────────────────────────────────────────────┐
│ Pi Core Kernel │
│ ├─ LLM-Provider-Schicht (Anthropic/OpenAI/lokal) │
│ ├─ Session-Management (Sitzungen, Komprimierung, Branches) │
│ ├─ Eingebaute Tools (bash, read, write, search…) │
│ └─ TUI-Rendering (Ink/React) │
└─────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
Extensions Skills Packages
(TypeScript) (Markdown) (npm/git)
- Extensions: TypeScript-Module, die zur Laufzeit geladen werden und Ereignisse abfangen, Tools registrieren und Befehle hinzufügen können.
- Skills: Markdown-Dateien, die bei Bedarf in den Kontext geladen werden und Workflows sowie Prompts definieren.
- Packages: Über npm oder git verteilte Sammlungen von Erweiterungen.
Funktionsweise des Plugin-Systems
Pi lädt Erweiterungen über jiti – TypeScript wird direkt ohne Kompilierung ausgeführt. Der Einstiegspunkt einer Erweiterung ist eine Standard-exportierte Factory-Funktion, die eine ExtensionAPI-Instanz entgegennimmt:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// Hier Ereignisse abonnieren, Tools registrieren, Befehle hinzufügen
}
Erweiterungen können an zwei Speicherorten abgelegt werden, um automatisch erkannt zu werden:
| Speicherort | Geltungsbereich |
|---|---|
~/.pi/agent/extensions/*.ts |
Global (alle Projekte) |
.pi/extensions/*.ts |
Projektlokal (vertrauenswürdiges Projekt erforderlich) |
API-Schnittstellen
ExtensionAPI bietet drei Kategorien von Funktionen:
- Ereignisabonnements:
pi.on("event_name", handler)– deckt den gesamten Lebenszyklus vonsession_startbistool_callab. - Tool-Registrierung:
pi.registerTool({ name, description, parameters, execute })– ermöglicht dem LLM den Aufruf deiner benutzerdefinierten Funktionen. - Befehlsregistrierung:
pi.registerCommand("name", { handler })– fügt/name-Slash-Befehle hinzu.
Entwicklungsumgebung einrichten
Voraussetzungen
- Node.js 18+ (empfohlen: 20 LTS)
- npm oder pnpm
- Pi Coding Agent bereits installiert:
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
Erweiterungsprojekt erstellen
# Globales Erweiterungsverzeichnis erstellen
mkdir -p ~/.pi/agent/extensions/my-first-ext
cd ~/.pi/agent/extensions/my-first-ext
# package.json initialisieren
npm init -y
# Typdefinitionen installieren
npm install @earendil-works/pi-coding-agent typebox
# Einstiegsdatei erstellen
touch index.ts
Verzeichnisstruktur
my-first-ext/
├── package.json
├── node_modules/
└── src/
└── index.ts # Einstiegspunkt
package.json muss den Erweiterungseinstiegspunkt deklarieren:
{
"name": "my-first-ext",
"dependencies": {
"@earendil-works/pi-coding-agent": "^1.0.0",
"typebox": "^0.34.0"
},
"pi": {
"extensions": ["./src/index.ts"]
}
}
Die erste Erweiterung: Hello World
Basisversion: Benachrichtigung + Befehl
// src/index.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// Benachrichtigung beim Sitzungsstart anzeigen
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Hello World Erweiterung geladen!", "info");
});
// /hello Befehl registrieren
pi.registerCommand("hello", {
description: "Pi begrüßen",
handler: async (args, ctx) => {
const name = args?.trim() || "World";
ctx.ui.notify(`Hello, ${name}! 👋`, "info");
},
});
}
Testausführung
# Methode 1: Automatische Erkennung (in ~/.pi/agent/extensions/ abgelegt)
pi
# Methode 2: Explizit angeben (temporäres Testen)
pi -e ./src/index.ts
Nach dem Start von Pi gib /hello Kevin ein – du solltest die Benachrichtigung sehen: Hello, Kevin! 👋.
Fortgeschritten: Gefährliche Befehle abfangen
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("⚠️ Gefährliche Aktion", "rm -rf wirklich ausführen?");
if (!ok) return { block: true, reason: "Vom Benutzer abgebrochen" };
}
});
Diese Erweiterung demonstriert die Abfangfähigkeit des tool_call-Ereignisses – du kannst Berechtigungsprüfungen durchführen, bevor das LLM ein Tool ausführt.
Praxisprojekt 1: Benutzerdefinierter Code-Vorlagengenerator
Anforderung
Das Team verwendet feste Code-Vorlagen (React-Komponenten, Express-Routen, Python-Skripte etc.), und bei jeder neuen Datei muss kopiert und eingefügt werden. Wir schreiben eine Erweiterung, die es dem LLM ermöglicht, das generate_template-Tool zur automatischen Generierung aufzurufen.
Implementierung
// src/template-generator.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import * as fs from "node:fs";
import * as path from "node:path";
const TEMPLATES: Record<string, string> = {
"react-component": `import React from 'react';
interface {{Name}}Props {
title: string;
}
export const {{Name}}: React.FC<{{Name}}Props> = ({ title }) => {
return <div className="{{name}}">{title}</div>;
};
`,
"express-route": `import { Router, Request, Response } from 'express';
const router = Router();
router.get('/', (req: Request, res: Response) => {
res.json({ message: '{{Name}} endpoint' });
});
export default router;
`,
"python-script": `#!/usr/bin/env python3
"""{{Name}} module."""
import argparse
import sys
def main(args: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="{{Name}}")
parser.add_argument("--name", default="World")
parsed = parser.parse_args(args)
print(f"Hello, {parsed.name}!")
return 0
if __name__ == "__main__":
raise SystemExit(main())
`,
};
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "generate_template",
label: "Vorlagengenerator",
description: "Generiert eine Codedatei basierend auf Vorlagenname und Komponentenname. Verfügbare Vorlagen: react-component, express-route, python-script",
parameters: Type.Object({
template: Type.String({ description: "Vorlagenname" }),
name: Type.String({ description: "Komponenten-/Modulname (PascalCase)" }),
outputPath: Type.String({ description: "Ausgabedateipfad" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const tpl = TEMPLATES[params.template];
if (!tpl) {
return {
content: [{ type: "text", text: `❌ Unbekannte Vorlage: ${params.template}` }],
details: {},
};
}
const pascalName = params.name.replace(/(^|[-_])(\w)/g, (_, __, c) => c.toUpperCase());
const kebabName = pascalName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
const code = tpl
.replace(/\{\{Name\}\}/g, pascalName)
.replace(/\{\{name\}\}/g, kebabName);
const outPath = path.resolve(params.outputPath);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, code, "utf-8");
return {
content: [{ type: "text", text: `✅ Generiert: ${outPath}\n\n${code}` }],
details: { path: outPath, template: params.template },
};
},
});
}
Verwendung
Sag einfach in Pi:
Erstelle eine UserProfile-Komponente mit der react-component-Vorlage und speichere sie unter src/components/UserProfile.tsx
Pi ruft das generate_template-Tool auf und erledigt automatisch die Variablenersetzung und das Schreiben der Datei.
Praxisprojekt 2: Git-Workflow-Automatisierung
Anforderung
Vor jedem Commit automatisch Lint ausführen, Commit-Nachricht generieren und das Team nach dem Push benachrichtigen. Wir verwenden eine Erweiterung, die das bash-Tool abfängt und beim Erkennen von git commit Vorabprüfungen injiziert.
Implementierung
// src/git-workflow.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { execSync } from "node:child_process";
export default function (pi: ExtensionAPI) {
// git commit abfangen, vorgelagertes Lint ausführen
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "bash") return;
const cmd: string = event.input.command || "";
if (cmd.startsWith("git commit")) {
ctx.ui.setStatus("git-workflow", "🔍 Pre-Commit-Prüfung läuft...");
try {
execSync("npm run lint --if-present", { stdio: "pipe" });
execSync("npm run test --if-present", { stdio: "pipe" });
ctx.ui.setStatus("git-workflow", "✅ Prüfungen bestanden, Commit wird fortgesetzt");
} catch (err) {
ctx.ui.notify("❌ Lint/Test fehlgeschlagen, Commit wurde blockiert", "error");
return { block: true, reason: "pre-commit checks failed" };
}
}
// git push abfangen, Benachrichtigung senden
if (cmd.startsWith("git push")) {
ctx.ui.setStatus("git-workflow", "📤 Push läuft...");
// Hier könnte ein Webhook aufgerufen werden, um das Team zu benachrichtigen
}
});
// /git-summary Befehl registrieren
pi.registerCommand("git-summary", {
description: "Commit-Zusammenfassung des aktuellen Branches anzeigen",
handler: async (_args, ctx) => {
try {
const log = execSync(
"git log --oneline -10 --format='%h %s (%an, %ar)'",
{ encoding: "utf-8" }
);
ctx.ui.notify(`Letzte 10 Commits:\n${log}`, "info");
} catch {
ctx.ui.notify("❌ Git-Verlauf konnte nicht gelesen werden", "error");
}
},
});
}
Ergebnis
- Wenn Pi versucht,
git commitauszuführen, führt die Erweiterung automatisch Lint und Tests aus und blockiert den Commit bei Fehlern. - Der
/git-summary-Befehl zeigt schnell die letzten Commits an.
Praxisprojekt 3: Code-Qualitätsprüfung-Integration
Anforderung
Das LLM soll beim Schreiben von Code automatisch externe Qualitätswerkzeuge (wie eslint, pylint, semgrep) aufrufen und die Ergebnisse mit Reparaturvorschlägen versehen.
Implementierung
// src/quality-checker.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import { execSync } from "node:child_process";
type Linter = {
cmd: string;
pattern: RegExp;
};
const LINTERS: Linter[] = [
{ cmd: "npx eslint --format json", pattern: /\.(ts|tsx|js|jsx)$/ },
{ cmd: "pylint --output-format=json", pattern: /\.py$/ },
{ cmd: "semgrep --json", pattern: /.*/ },
];
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "check_code_quality",
label: "Code-Qualitätsprüfung",
description: "Führt Lint/Statische Analyse für eine Datei aus und gibt eine Problemliste mit Reparaturvorschlägen zurück",
parameters: Type.Object({
filePath: Type.String({ description: "Zu prüfender Dateipfad" }),
linter: Type.Optional(
Type.String({ description: "Linter angeben (eslint/pylint/semgrep), leer lassen für automatische Erkennung" })
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const file = params.filePath;
let linter = LINTERS.find((l) => l.pattern.test(file));
if (params.linter) {
linter = LINTERS.find((l) => l.cmd.startsWith(params.linter!));
}
if (!linter) {
return {
content: [{ type: "text", text: "⚠️ Kein passender Linter gefunden" }],
details: {},
};
}
ctx.ui.setStatus("quality", `🔍 ${linter.cmd.split(" ")[0]} wird ausgeführt...`);
try {
const output = execSync(`${linter.cmd} ${file}`, {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
return {
content: [{ type: "text", text: `✅ Keine Probleme\n${output}` }],
details: { file, linter: linter.cmd },
};
} catch (err: any) {
// Linter geben Probleme üblicherweise mit einem Exit-Code ungleich null zurück
const output = err.stdout || err.stderr || "";
return {
content: [{ type: "text", text: `⚠️ Probleme gefunden:\n${output.slice(0, 3000)}` }],
details: { file, linter: linter.cmd, hasIssues: true },
};
}
},
});
}
Verwendung
Prüfe die Codequalität von src/auth/login.ts
Pi ruft check_code_quality auf, gibt die Lint-Ergebnisse zurück und liefert automatisch Reparaturvorschläge.
Praxisprojekt 4: Integration eines benutzerdefinierten Suchtools
Anforderung
Pi soll die interne Wissensdatenbank des Teams durchsuchen können (z.B. Notion, Confluence oder lokale Markdown-Dateien). Hier verwenden wir eine lokale Markdown-Wissensdatenbank als Beispiel.
Implementierung
// src/knowledge-search.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import * as fs from "node:fs";
import * as path from "node:path";
const KB_ROOT = process.env.KB_ROOT || "~/knowledge-base";
interface SearchResult {
file: string;
line: number;
context: string;
}
function searchMarkdown(query: string, root: string): SearchResult[] {
const results: SearchResult[] = [];
const terms = query.toLowerCase().split(/\s+/);
function walk(dir: string) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.name.endsWith(".md")) {
const content = fs.readFileSync(full, "utf-8");
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const lower = lines[i].toLowerCase();
if (terms.every((t) => lower.includes(t))) {
results.push({
file: path.relative(root, full),
line: i + 1,
context: lines.slice(Math.max(0, i - 1), i + 2).join("\n"),
});
}
}
}
}
}
walk(path.resolve(root));
return results.slice(0, 10);
}
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "search_knowledge_base",
label: "Wissensdatenbank-Suche",
description: "Durchsucht die lokale Markdown-Wissensdatenbank nach relevanten Inhalten",
parameters: Type.Object({
query: Type.String({ description: "Suchbegriffe" }),
kbRoot: Type.Optional(Type.String({ description: "Stammverzeichnis der Wissensdatenbank, Standard: ~/knowledge-base" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const root = params.kbRoot || KB_ROOT;
const resolved = path.resolve(root.replace("~/", process.env.HOME + "/"));
if (!fs.existsSync(resolved)) {
return {
content: [{ type: "text", text: `❌ Wissensdatenbank-Verzeichnis nicht gefunden: ${resolved}` }],
details: {},
};
}
const results = searchMarkdown(params.query, resolved);
if (results.length === 0) {
return {
content: [{ type: "text", text: "🔍 Keine relevanten Inhalte gefunden" }],
details: { query: params.query },
};
}
const formatted = results
.map((r) => `📄 ${r.file}:${r.line}\n${r.context}`)
.join("\n\n");
return {
content: [{ type: "text", text: `${results.length} Treffer gefunden:\n\n${formatted}` }],
details: { query: params.query, count: results.length },
};
},
});
}
Verwendung
Setze die Umgebungsvariable KB_ROOT=~/my-docs und dann in Pi:
Durchsuche die Wissensdatenbank nach Inhalten zur API-Authentifizierung
Pi ruft search_knowledge_base auf und gibt die passenden Dateiausschnitte zurück.
Fortgeschrittene Themen
Multi-Agent-Zusammenarbeit
Pi unterstützt Multi-Agent-Modi über Erweiterungen. Das Grundprinzip: Der Haupt-Agent empfängt Aufgaben, zerlegt sie und delegiert sie an Unteragenten (indem neue pi-Prozesse über das bash-Tool gestartet werden, wobei --resume oder Session-Dateien zur Kontextfreigabe verwendet werden).
pi.registerTool({
name: "delegate_to_specialist",
label: "Experten delegieren",
description: "Delegiert Teilaufgaben an spezialisierte Agenten",
parameters: Type.Object({
task: Type.String({ description: "Beschreibung der Teilaufgabe" }),
specialist: Type.String({ description: "Expertentyp: frontend/backend/test" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const sessionFile = `/tmp/pi-${params.specialist}-${Date.now()}.json`;
const prompt = encodeURIComponent(params.task);
ctx.ui.setStatus("delegate", `🤖 Delegiere an ${params.specialist}...`);
try {
const output = execSync(
`pi --session ${sessionFile} --print "${params.task}"`,
{ encoding: "utf-8", timeout: 120_000 }
);
return {
content: [{ type: "text", text: output }],
details: { specialist: params.specialist, session: sessionFile },
};
} catch (err: any) {
return {
content: [{ type: "text", text: `❌ Delegation fehlgeschlagen: ${err.message}` }],
details: {},
};
}
},
});
Leistungsoptimierung
- Lazy Loading: Starte keine Hintergrundprozesse in der Factory-Funktion, verschiebe sie auf
session_startoder den ersten Tool-Aufruf. - Ergebnisse zwischenspeichern: Für häufig aufgerufene Tools (z.B. Lint) verwende eine Map, um Dateihashes und Ergebnisse zu cachen und wiederholte Ausführungen zu vermeiden.
- Streaming-Updates: Verwende den
onUpdate-Callback, um den Fortschritt in Echtzeit zu melden und ein Einfrieren der UI zu vermeiden.
// Cache-Beispiel
const cache = new Map<string, string>();
async execute(_toolCallId, params) {
const hash = crypto.createHash("md5").update(fs.readFileSync(params.filePath)).digest("hex");
const key = `${params.filePath}:${hash}`;
if (cache.has(key)) return { content: [{ type: "text", text: cache.get(key)! }], details: { cached: true } };
// ... Prüfung durchführen ...
cache.set(key, result);
return { content: [{ type: "text", text: result }], details: {} };
}
Erweiterungen veröffentlichen und teilen
Pi-Erweiterungen können als Pi Packages über npm oder Git-Repositories verteilt werden:
// package.json
{
"name": "pi-ext-my-tools",
"version": "1.0.0",
"pi": {
"extensions": ["./src/index.ts"]
}
}
Benutzerinstallation:
# Von npm
pi install npm:pi-ext-my-tools@1.0.0
# Von Git
pi install git:github.com/user/repo@v1
Du kannst sie auch im offiziellen Paketverzeichnis unter pi.dev/packages veröffentlichen.
Empfehlenswerte Community-Erweiterungen
| Erweiterung | Funktion | Link |
|---|---|---|
| pi-hosts | Verwaltet /etc/hosts, schnelles Wechseln zwischen Umgebungen | GitHub |
| Parallel Pi agents | Parallele Aufgabenausführung durch mehrere Agenten | GitHub |
| pi-extensions (narumiruna) | Automatisierung, Planung, Browsersteuerung, Git-Workflow-Sammlung | GitHub |
| pi-extensions-skill (Dwsy) | Progressiver Leitfaden zum Erlernen der Erweiterungsentwicklung | GitHub |
| pi-extension-builder (LobeHub) | Gerüstgenerator für Erweiterungen | LobeHub |
Häufig gestellte Fragen (FAQ)
Was ist der Unterschied zwischen Pi Agent Erweiterungen und Skills?
Extensions sind TypeScript-Module, die zur Laufzeit geladen werden, Ereignisse abfangen, Tools registrieren und Befehle hinzufügen können – mit vollständigen Systemrechten. Skills sind Markdown-Dateien, die bei Bedarf in den LLM-Kontext geladen werden, Workflows und Prompts definieren, aber keinen Code direkt ausführen können. Kurz gesagt: Extensions sind "Erweiterungen auf Code-Ebene", Skills sind "Erweiterungen auf Prompt-Ebene".
Muss man TypeScript können, um Erweiterungen zu entwickeln?
Grundlegende TypeScript-Kenntnisse sind erforderlich. Pi lädt .ts-Dateien direkt mit jiti, kein Kompilierungsschritt nötig. Wenn du nur JavaScript kannst, kannst du trotzdem schreiben – ändere einfach die Dateiendung in .js, Typdefinitionen sind optional.
Wie debuggt man Erweiterungen?
Starte mit pi -e ./my-ext.ts, füge console.log im Code ein – die Ausgabe erscheint unten in Pi's TUI. Du kannst auch ctx.ui.notify() verwenden, um Benachrichtigungen anzuzeigen. Für komplexe Logik empfiehlt es sich, Unit-Tests zu schreiben und sie direkt mit Node.js auszuführen.
Beeinflussen Erweiterungen die Startgeschwindigkeit von Pi?
Synchron geladene Erweiterungen blockieren den Start. Es wird empfohlen, zeitaufwändige Operationen (Netzwerkanfragen, Dateiscans) auf das session_start-Ereignis oder den ersten Tool-Aufruf zu verschieben. In der Factory-Funktion sollten nur leichte Registrierungen wie pi.on() und pi.registerTool() erfolgen.
Kann man in Erweiterungen externe APIs aufrufen?
Ja. Erweiterungen laufen in der Node.js-Umgebung, du kannst fetch, http und beliebige npm-Pakete verwenden. Vergiss nicht, die Abhängigkeiten in den dependencies der package.json zu deklarieren.
Zusammenfassung
Das Erweiterungssystem von Pi Agent gibt Entwicklern die vollständige Fähigkeit, "KI-Assistenten anzupassen". Vom Abfangen gefährlicher Befehle bis zur Registrierung benutzerdefinierter Tools, von Git-Automatisierung bis zur Wissensdatenbanksuche – die Kombination aus TypeScript + ereignisgesteuert macht Erweiterungen sowohl flexibel als auch kontrollierbar.
Die 4 Praxisprojekte in diesem Artikel decken die häufigsten Szenarien ab: Vorlagengenerierung, Git-Workflow, Code-Qualität und Wissenssuche. Du kannst sie direkt verwenden oder als Vorlage für weitere Erweiterungen anpassen.
Nächste Schritte:
- Lies die offizielle Extensions-Dokumentation
- Schau dir die Beispiele in examples/extensions/ an
- Finde Inspiration auf pi.dev/packages oder veröffentliche deine erste Erweiterung