なぜ Pi Agent に拡張を書くのか?
もしあなたがすでに Pi Coding Agent を使ったことがあるなら、それが「小さくて完成された」ターミナル AI プログラミングアシスタントであることに気づくでしょう——コアがやることはただ一つ、LLM の機能をターミナルワークフローに組み込むことだけです。しかし、それを truly「自分専用」にするのは**拡張システム(Extensions)**です。
Pi の拡張は TypeScript モジュールで、ライフサイクルイベントの購読、LLM が呼び出せるカスタムツールの登録、スラッシュコマンドの追加、TUI コンポーネントのカスタマイズなどができます。公式エコシステムにはすでに pi-hosts(23 pts)、Parallel Pi agents(8 pts)といったコミュニティプロジェクトがありますが、日本語の深い技術コンテンツはまだ稀少です。
まだ Pi をインストールしていない方は、まずこちらの記事をご覧ください:2026年 主要オープンソース AI コーディングエージェント比較ガイド。Pi の位置づけの比較があります。本記事は拡張開発にフォーカスし、ゼロから 4 つの実践拡張を作っていきます。
Pi Agent アーキテクチャ解説
コアコンポーネント
Pi のアーキテクチャは一言で言うと:最小カーネル + 拡張可能な境界。
┌─────────────────────────────────────────────┐
│ Pi コアカーネル │
│ ├─ LLM Provider 層(Anthropic/OpenAI/ローカル)│
│ ├─ Session 管理(セッション、圧縮、ブランチ) │
│ ├─ 組み込みツール(bash、read、write、search…) │
│ └─ TUI レンダリング(Ink/React) │
└─────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
Extensions Skills Packages
(TypeScript) (Markdown) (npm/git)
- Extensions:TypeScript モジュール。ランタイムでロードされ、イベントのインターセプト、ツールの登録、コマンドの追加が可能。
- Skills:Markdown ファイル。必要に応じてコンテキストにロードされ、ワークフローやプロンプトを定義。
- Packages:npm や git 経由で配布される拡張の集合。
プラグインシステムの仕組み
Pi の拡張ローディングは jiti に基づいており、コンパイルなしで TypeScript を直接実行できます。拡張のエントリポイントはデフォルトエクスポートされるファクトリ関数で、ExtensionAPI インスタンスを受け取ります:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// ここでイベントの購読、ツールの登録、コマンドの追加を行う
}
拡張は以下の 2 箇所に置くと自動検出されます:
| 場所 | スコープ |
|---|---|
~/.pi/agent/extensions/*.ts |
グローバル(全プロジェクト) |
.pi/extensions/*.ts |
プロジェクトローカル(プロジェクトの信頼が必要) |
API インターフェース説明
ExtensionAPI は 3 種類の機能を提供します:
- イベント購読:
pi.on("event_name", handler)—session_startからtool_callまでの完全なライフサイクルをカバー。 - ツール登録:
pi.registerTool({ name, description, parameters, execute })— LLM があなたのカスタム関数を呼び出せるようにする。 - コマンド登録:
pi.registerCommand("name", { handler })—/nameスラッシュコマンドを追加。
開発環境のセットアップ
前提条件
- Node.js 18+(20 LTS 推奨)
- npm または pnpm
- Pi Coding Agent インストール済み:
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
拡張プロジェクトの作成
# グローバル拡張ディレクトリを作成
mkdir -p ~/.pi/agent/extensions/my-first-ext
cd ~/.pi/agent/extensions/my-first-ext
# package.json を初期化
npm init -y
# 型定義をインストール
npm install @earendil-works/pi-coding-agent typebox
# エントリファイルを作成
touch index.ts
ディレクトリ構造
my-first-ext/
├── package.json
├── node_modules/
└── src/
└── index.ts # エントリ
package.json で拡張のエントリポイントを宣言する必要があります:
{
"name": "my-first-ext",
"dependencies": {
"@earendil-works/pi-coding-agent": "^1.0.0",
"typebox": "^0.34.0"
},
"pi": {
"extensions": ["./src/index.ts"]
}
}
最初の拡張:Hello World
基本版:通知 + コマンド
// src/index.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// セッション開始時に通知を表示
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Hello World 拡張がロードされました!", "info");
});
// /hello コマンドを登録
pi.registerCommand("hello", {
description: "Pi に挨拶する",
handler: async (args, ctx) => {
const name = args?.trim() || "World";
ctx.ui.notify(`Hello, ${name}! 👋`, "info");
},
});
}
テスト実行
# 方法 1:自動検出(~/.pi/agent/extensions/ に配置)
pi
# 方法 2:明示的に指定(一時的なテスト)
pi -e ./src/index.ts
Pi に入った後 /hello Kevin と入力すると、通知が表示されるはずです:Hello, Kevin! 👋。
発展:危険なコマンドのインターセプト
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("⚠️ 危険な操作", "rm -rf を実行しますか?");
if (!ok) return { block: true, reason: "ユーザーがキャンセルしました" };
}
});
この拡張は tool_call イベントのインターセプト機能を示しています——LLM がツールを実行する前に権限チェックを行えます。
実践プロジェクト 1:カスタムコードテンプレートジェネレーター
要件
チームに決まったコードテンプレート(React コンポーネント、Express ルート、Python スクリプトなど)があり、新しいファイルを作るたびにコピー&ペーストしている。拡張を書いて、LLM が generate_template ツールを呼び出して自動生成できるようにしましょう。
実装
// 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: "テンプレートジェネレーター",
description: "テンプレート名とコンポーネント名に基づいてコードファイルを生成します。利用可能なテンプレート:react-component, express-route, python-script",
parameters: Type.Object({
template: Type.String({ description: "テンプレート名" }),
name: Type.String({ description: "コンポーネント/モジュール名(PascalCase)" }),
outputPath: Type.String({ description: "出力ファイルパス" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const tpl = TEMPLATES[params.template];
if (!tpl) {
return {
content: [{ type: "text", text: `❌ 不明なテンプレート: ${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: `✅ ${outPath} を生成しました\n\n${code}` }],
details: { path: outPath, template: params.template },
};
},
});
}
使い方
Pi の中で直接こう言います:
react-component テンプレートで UserProfile コンポーネントを生成して、src/components/UserProfile.tsx に配置して
Pi が generate_template ツールを呼び出し、変数置換とファイル書き込みを自動で行います。
実践プロジェクト 2:Git ワークフロー自動化
要件
コミット前に自動的に lint を実行し、コミットメッセージを生成し、プッシュ後にチームに通知する。拡張で bash ツールをインターセプトし、git commit を検出したら事前チェックを注入します。
実装
// 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 をインターセプトして事前 lint を実行
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 チェックを実行中...");
try {
execSync("npm run lint --if-present", { stdio: "pipe" });
execSync("npm run test --if-present", { stdio: "pipe" });
ctx.ui.setStatus("git-workflow", "✅ チェック合格、コミットを続行");
} catch (err) {
ctx.ui.notify("❌ Lint/Test に失敗したため、コミットをブロックしました", "error");
return { block: true, reason: "pre-commit checks failed" };
}
}
// git push をインターセプトして通知を送信
if (cmd.startsWith("git push")) {
ctx.ui.setStatus("git-workflow", "📤 プッシュ中...");
// ここで webhook を呼び出してチームに通知可能
}
});
// /git-summary コマンドを登録
pi.registerCommand("git-summary", {
description: "現在のブランチのコミットサマリーを表示",
handler: async (_args, ctx) => {
try {
const log = execSync(
"git log --oneline -10 --format='%h %s (%an, %ar)'",
{ encoding: "utf-8" }
);
ctx.ui.notify(`直近 10 件のコミット:\n${log}`, "info");
} catch {
ctx.ui.notify("❌ git 履歴を読み取れません", "error");
}
},
});
}
効果
- Pi が
git commitを実行しようとしたとき、拡張が自動的に lint とテストを実行し、失敗したらコミットをブロック。 /git-summaryコマンドで直近のコミットを素早く確認。
実践プロジェクト 3:コード品質チェック統合
要件
LLM がコードを書く際に自動的に外部品質チェックツール(eslint、pylint、semgrep など)を呼び出し、結果に修正提案を添える。
実装
// 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: "コード品質チェック",
description: "指定ファイルに対して lint/静的解析を実行し、問題リストと修正提案を返します",
parameters: Type.Object({
filePath: Type.String({ description: "チェック対象のファイルパス" }),
linter: Type.Optional(
Type.String({ description: "リンターを指定(eslint/pylint/semgrep)、空欄なら自動検出" })
),
}),
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: "⚠️ 一致するリンターが見つかりません" }],
details: {},
};
}
ctx.ui.setStatus("quality", `🔍 ${linter.cmd.split(" ")[0]} を実行中...`);
try {
const output = execSync(`${linter.cmd} ${file}`, {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
return {
content: [{ type: "text", text: `✅ 問題なし\n${output}` }],
details: { file, linter: linter.cmd },
};
} catch (err: any) {
// リンターは通常 0 以外の終了コードで問題を返す
const output = err.stdout || err.stderr || "";
return {
content: [{ type: "text", text: `⚠️ 問題を検出:\n${output.slice(0, 3000)}` }],
details: { file, linter: linter.cmd, hasIssues: true },
};
}
},
});
}
使い方
src/auth/login.ts のコード品質をチェックして
Pi が check_code_quality を呼び出し、lint 結果を返して、自動的に修正提案を行います。
実践プロジェクト 4:カスタム検索ツール統合
要件
Pi がチームの内部ナレッジベース(Notion、Confluence、ローカル Markdown ファイルなど)を検索できるようにする。ここではローカル Markdown ナレッジベースを例にします。
実装
// 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: "ナレッジベース検索",
description: "ローカル Markdown ナレッジベースから関連内容を検索します",
parameters: Type.Object({
query: Type.String({ description: "検索キーワード" }),
kbRoot: Type.Optional(Type.String({ description: "ナレッジベースのルートディレクトリ、デフォルト ~/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: `❌ ナレッジベースディレクトリが存在しません: ${resolved}` }],
details: {},
};
}
const results = searchMarkdown(params.query, resolved);
if (results.length === 0) {
return {
content: [{ type: "text", text: "🔍 関連する内容が見つかりませんでした" }],
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} 件の一致を検出:\n\n${formatted}` }],
details: { query: params.query, count: results.length },
};
},
});
}
使い方
環境変数 KB_ROOT=~/my-docs を設定して、Pi の中で:
ナレッジベースから API 認証に関する内容を検索して
Pi が search_knowledge_base を呼び出し、マッチしたファイルのスニペットを返します。
高度なトピック
マルチエージェント連携
Pi は拡張を通じてマルチエージェントモードを実現できます。基本的な考え方は:メイン Agent がタスクを受け取り、分割してサブ Agent に委譲する(bash ツールで新しい pi プロセスを起動し、--resume またはセッションファイルでコンテキストを共有)。
pi.registerTool({
name: "delegate_to_specialist",
label: "スペシャリストに委譲",
description: "サブタスクを専門 Agent に委譲します",
parameters: Type.Object({
task: Type.String({ description: "サブタスクの説明" }),
specialist: Type.String({ description: "スペシャリストの種類: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", `🤖 ${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: `❌ 委譲に失敗: ${err.message}` }],
details: {},
};
}
},
});
パフォーマンス最適化
- 遅延ロード:ファクトリ関数内でバックグラウンドプロセスを起動せず、
session_startや初回ツール呼び出しまで遅延させる。 - 結果のキャッシュ:頻繁に呼び出されるツール(lint など)には、Map でファイルハッシュと結果をキャッシュし、再実行を避ける。
- ストリーミング更新:
onUpdateコールバックでリアルタイムに進捗をフィードバックし、UI のフリーズを防ぐ。
// キャッシュの例
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 } };
// ... チェックを実行 ...
cache.set(key, result);
return { content: [{ type: "text", text: result }], details: {} };
}
拡張の公開と共有
Pi 拡張は npm や git リポジトリ経由で pi packages として配布できます:
// package.json
{
"name": "pi-ext-my-tools",
"version": "1.0.0",
"pi": {
"extensions": ["./src/index.ts"]
}
}
ユーザーのインストール方法:
# npm から
pi install npm:pi-ext-my-tools@1.0.0
# git から
pi install git:github.com/user/repo@v1
pi.dev/packages で公式パッケージディレクトリに公開することもできます。
コミュニティの優れた拡張紹介
| 拡張 | 機能 | リンク |
|---|---|---|
| pi-hosts | /etc/hosts を管理し、環境を素早く切り替え | GitHub |
| Parallel Pi agents | マルチエージェントでタスクを並列実行 | GitHub |
| pi-extensions (narumiruna) | 自動化、プランニング、ブラウザ制御、Git ワークフローのコレクション | GitHub |
| pi-extensions-skill (Dwsy) | 段階的な拡張開発学習ガイド | GitHub |
| pi-extension-builder (LobeHub) | 拡張スキャフォールドジェネレーター | LobeHub |
よくある質問 FAQ
Pi Agent の拡張と Skills の違いは?
Extensions は TypeScript モジュールで、ランタイムにロードされ、イベントのインターセプト、ツールの登録、コマンドの追加が可能で、完全なシステム権限を持ちます。Skills は Markdown ファイルで、必要に応じて LLM コンテキストにロードされ、ワークフローやプロンプトを定義しますが、直接コードを実行することはできません。簡単に言うと:Extension は「コードレベルの拡張」、Skill は「プロンプトレベルの拡張」です。
拡張開発には TypeScript が必要?
基本的な TypeScript の知識が必要です。Pi は jiti を使って .ts ファイルを直接ロードするため、コンパイルステップは不要です。JavaScript しか書けない場合でも大丈夫——ファイルの拡張子を .js に変更すれば使えます。型定義は任意です。
拡張のデバッグ方法は?
pi -e ./my-ext.ts で起動し、コードに console.log を入れると、出力が Pi の TUI 下部に表示されます。ctx.ui.notify() で通知をポップアップすることもできます。複雑なロジックはユニットテストを書いて Node.js で直接実行することをお勧めします。
拡張は Pi の起動速度に影響する?
同期ロードされる拡張は起動をブロックします。時間がかかる操作(ネットワークリクエスト、ファイルスキャン)は session_start イベントや初回ツール呼び出し時に遅延実行することをお勧めします。ファクトリ関数内では pi.on() や pi.registerTool() といった軽量な登録のみ行ってください。
拡張内で外部 API を呼び出せる?
はい。拡張は Node.js 環境で動作するため、fetch、http、あらゆる npm パッケージを使用できます。package.json の dependencies に依存関係を宣言するのを忘れないでください。
まとめ
Pi Agent の拡張システムは、開発者に「AI アシスタントを改造する」ための完全な機能を提供します。危険なコマンドのインターセプトからカスタムツールの登録、Git 自動化からナレッジベース検索まで、TypeScript + イベント駆動の組み合わせで、拡張は柔軟かつ制御可能です。
本記事の 4 つの実践プロジェクトは、最も一般的なシナリオをカバーしています:テンプレート生成、Git ワークフロー、コード品質、ナレッジ検索。そのまま使えるほか、テンプレートとしてさらに多くの拡張を作ることもできます。
次のステップ:
- 公式 Extensions ドキュメントを読む
- examples/extensions/ のサンプルを見る
- pi.dev/packages でインスピレーションを得る、または最初の拡張を公開する