コンテンツにスキップ

拡張機能

pi は拡張機能を作成できます。あなたのユースケースに合わせて構築するよう依頼してください。

拡張機能は、pi の動作を拡張する TypeScript モジュールです。ライフサイクル イベントのサブスクライブ、LLM によって呼び出し可能なカスタム ツールの登録、コマンドの追加などを行うことができます。

/reload の配置: 自動検出のために、拡張機能を ~/.pi/agent/extensions/ (グローバル) または .pi/extensions/ (プロジェクトローカル) に配置します。 pi -e ./path.ts は簡単なテストの場合にのみ使用してください。自動検出された場所にある拡張機能は、/reload を使用してホットリロードできます。

主な機能:

  • カスタム ツール - LLM が pi.registerTool() 経由で呼び出すことができるツールを登録します。
  • イベントインターセプト - ツール呼び出しのブロックまたは変更、コンテキストの挿入、圧縮のカスタマイズ
  • ユーザー操作 - ctx.ui 経由でユーザーにプロンプトを表示します (選択、確認、入力、通知)
  • カスタム UI コンポーネント - 複雑な対話のための ctx.ui.custom() 経由のキーボード入力を備えた完全な TUI コンポーネント
  • カスタム コマンド - pi.registerCommand() 経由で /mycommand のようなコマンドを登録します
  • セッションの永続性 - pi.appendEntry() による再起動後も保存されるストア状態
  • カスタム レンダリング - ツールの呼び出し/結果およびメッセージが TUI にどのように表示されるかを制御します

ユースケース例:

  • 許可ゲート(rm -rfsudoなどの前で確認)
  • Git チェックポイント設定 (各ターンにスタッシュ、ブランチ時に復元)
  • パス保護 (.envnode_modules/ への書き込みをブロック)
  • カスタム圧縮 (会話を自分の方法で要約)
  • 会話の要約 (summarize.ts の例を参照)
  • インタラクティブなツール (質問、ウィザード、カスタム ダイアログ)
  • ステートフル ツール (ToDo リスト、接続プール)
  • 外部統合 (ファイル ウォッチャー、Webhook、CI トリガー)
  • 待っている間のゲーム (snake.ts の例を参照)

動作する実装については、examples/extensions/ を参照してください。

~/.pi/agent/extensions/my-extension.ts を作成します。

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// React to events
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Extension loaded!", "info");
});
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
if (!ok) return { block: true, reason: "Blocked by user" };
}
});
// Register a custom tool
pi.registerTool({
name: "greet",
label: "Greet",
description: "Greet someone by name",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: {},
};
},
});
// Register a command
pi.registerCommand("hello", {
description: "Say hello",
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
},
});
}

--extension (または -e) フラグを使用してテストします。

Terminal window
pi -e ./my-extension.ts

セキュリティ: 拡張機能は完全なシステム権限で実行され、任意のコードを実行できます。信頼できるソースからのみインストールしてください。

拡張機能は信頼できる場所から自動的に検出されます。プロジェクト ローカルの .pi/extensions エントリは、プロジェクトが信頼された後にのみロードされます。

場所スコープ
~/.pi/agent/extensions/*.tsグローバル(すべてのプロジェクト)
~/.pi/agent/extensions/*/index.tsグローバル(サブディレクトリ)
.pi/extensions/*.tsプロジェクトローカル
.pi/extensions/*/index.tsプロジェクトローカル(サブディレクトリ)

settings.json 経由の追加パス:

{
"packages": [
"npm:@foo/[email protected]",
"git:github.com/user/repo@v1"
],
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension/dir"
]
}

npm または git を介して拡張機能を pi パッケージとして共有するには、packages.md を参照してください。

パッケージ目的
@earendil-works/pi-coding-agent拡張タイプ(ExtensionAPIExtensionContext、イベント)
typeboxツールパラメータのスキーマ定義
@earendil-works/pi-aiAI ユーティリティ(Google 互換列挙型の StringEnum
@earendil-works/pi-tuiカスタムレンダリング用 TUI コンポーネント

npm の依存関係も機能します。拡張機能の隣 (または親ディレクトリ) に package.json を追加し、npm install を実行すると、node_modules/ からのインポートが自動的に解決されます。

pi install (npm または git) でインストールされた分散 pi パッケージの場合、ランタイム deps は dependencies にある必要があります。パッケージのインストールでは、デフォルトで実稼働インストール (npm install --omit=dev) が使用されるため、実行時には devDependencies を使用できません。 npmCommand が設定されている場合、git パッケージはラッパーとの互換性のためにプレーン install を使用します。

Node.js ビルトイン (node:fsnode:path など) も利用できます。

拡張機能は、ExtensionAPI を受け取るデフォルトのファクトリ関数をエクスポートします。ファクトリは同期または非同期にすることができます。

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// Subscribe to events
pi.on("event_name", async (event, ctx) => {
// ctx.ui for user interaction
const ok = await ctx.ui.confirm("Title", "Are you sure?");
ctx.ui.notify("Done!", "info");
ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)
});
// Register tools, commands, shortcuts, flags
pi.registerTool({ ... });
pi.registerCommand("name", { ... });
pi.registerShortcut("ctrl+x", { ... });
pi.registerFlag("my-flag", { ... });
}

拡張機能は jiti 経由でロードされるため、TypeScript はコンパイルなしで動作します。

ファクトリが Promise を返した場合、pi は起動を続行する前にそれを待ちます。つまり、非同期初期化は、session_start より前、resources_discover より前、そして pi.registerProvider() 経由でキューに入れられたプロバイダー登録がフラッシュされる前に完了します。

リモート構成の取得や利用可能なモデルの動的検出などの 1 回限りの起動作業には、非同期ファクトリーを使用します。

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default async function (pi: ExtensionAPI) {
const response = await fetch("http://localhost:1234/v1/models");
const payload = (await response.json()) as {
data: Array<{
id: string;
name?: string;
context_window?: number;
max_tokens?: number;
}>;
};
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "$LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((model) => ({
id: model.id,
name: model.name ?? model.id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: model.context_window ?? 128000,
maxTokens: model.max_tokens ?? 4096,
})),
});
}

このパターンにより、フェッチされたモデルが通常の起動中および pi --list-models で使用可能になります。

有効期間の長いリソースとシャットダウン

Section titled “有効期間の長いリソースとシャットダウン”

拡張機能ファクトリーは、セッションを開始しない呼び出しで実行される場合があります。プロセス、ソケット、ファイル ウォッチャー、タイマーなどのバックグラウンド リソースを工場出荷時に開始しないでください。

session_start またはリソースを必要とするコマンド/ツール/イベントが実行されるまで、バックグラウンド リソースの起動を延期します。べき等 session_shutdown ハンドラーを登録して、開始したセッション スコープのリソースを閉じます。

単一ファイル - 最も単純で、小さな拡張子の場合:

~/.pi/agent/extensions/
└── my-extension.ts

index.ts を持つディレクトリ - 複数のファイル拡張子の場合:

~/.pi/agent/extensions/
└── my-extension/
├── index.ts # Entry point (exports default function)
├── tools.ts # Helper module
└── utils.ts # Helper module

依存関係のあるパッケージ - npm パッケージを必要とする拡張機能の場合:

~/.pi/agent/extensions/
└── my-extension/
├── package.json # Declares dependencies and entry points
├── package-lock.json
├── node_modules/ # After npm install
└── src/
└── index.ts
package.json
{
"name": "my-extension",
"dependencies": {
"zod": "^3.0.0",
"chalk": "^5.0.0"
},
"pi": {
"extensions": ["./src/index.ts"]
}
}

拡張機能ディレクトリで npm install を実行すると、node_modules/ からのインポートが自動的に機能します。

pi starts
├─► project_trust (user/global and CLI extensions only, before project resources load)
├─► session_start { reason: "startup" }
└─► resources_discover { reason: "startup" }
user sends prompt ─────────────────────────────────────────┐
│ │
├─► (extension commands checked first, bypass if found) │
├─► input (can intercept, transform, or handle) │
├─► (skill/template expansion if not handled) │
├─► before_agent_start (can inject message, modify system prompt)
├─► agent_start │
├─► message_start / message_update / message_end │
│ │
│ ┌─── turn (repeats while LLM calls tools) ───┐ │
│ │ │ │
│ ├─► turn_start │ │
│ ├─► context (can modify messages) │ │
│ ├─► before_provider_request (can inspect or replace payload)
│ ├─► after_provider_response (status + headers, before stream consume)
│ │ │ │
│ │ LLM responds, may call tools: │ │
│ │ ├─► tool_execution_start │ │
│ │ ├─► tool_call (can block) │ │
│ │ ├─► tool_execution_update │ │
│ │ ├─► tool_result (can modify) │ │
│ │ └─► tool_execution_end │ │
│ │ │ │
│ └─► turn_end │ │
│ │
└─► agent_end │
user sends another prompt ◄────────────────────────────────┘
/new (new session) or /resume (switch session)
├─► session_before_switch (can cancel)
├─► session_shutdown
├─► session_start { reason: "new" | "resume", previousSessionFile? }
└─► resources_discover { reason: "startup" }
/fork or /clone
├─► session_before_fork (can cancel)
├─► session_shutdown
├─► session_start { reason: "fork", previousSessionFile }
└─► resources_discover { reason: "startup" }
/compact or auto-compaction
├─► session_before_compact (can cancel or customize)
└─► session_compact
/tree navigation
├─► session_before_tree (can cancel or customize)
└─► session_tree
/model or Ctrl+P (model selection/cycling)
├─► thinking_level_select (if model change changes/clamps thinking level)
└─► model_select
thinking level changes (settings, keybinding, pi.setThinkingLevel())
└─► thinking_level_select
exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
└─► session_shutdown

pi が動的構成 (.pi または .agents/skills) を持つプロジェクトを信頼するかどうかを決定する前に起動されます。これは起動時と、現在のプロセスで信頼が解決されていない cwd にセッション置換 (/resume など) が入ったときに実行されます。ユーザー/グローバル拡張機能と CLI -e 拡張機能のみが参加します。プロジェクトローカル拡張機能は、信頼が解決されるまでロードされません。

pi.on("project_trust", async (event, ctx) => {
// event.cwd - current working directory
// ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers
if (await ctx.ui.confirm("Trust project?", event.cwd)) {
return { trusted: "yes", remember: true };
}
return { trusted: "undecided" };
});

project_trust ハンドラーは { trusted: "yes" | "no" | "undecided" } を返す必要があります。 "yes" または "no" を返すユーザー/グローバルまたは CLI 拡張機能が決定を所有します。最初の「はい/いいえ」の決定が優先され、組み込みの信頼プロンプトが抑制されます。 remember: true を使用して、はい/いいえの決定を永続化します。それ以外の場合は、現在のプロセスにのみ適用されます。 "undecided" を返して、後のハンドラーまたは組み込みの信頼フローに決定させます。プロンプトを表示する前に、ctx.hasUI を確認してください。どのハンドラーも Yes/No を返さない場合、通常の信頼解決が続行されます。保存された trust.json 決定が最初に適用され、次に defaultProjectTrust がデフォルトで pi が質問するか、信頼するか、拒否するかを制御します。

session_start の後に起動されるため、拡張機能は追加のスキル、プロンプト、テーマのパスを提供できます。 起動パスは reason: "startup" を使用します。リロードでは reason: "reload" を使用します。

pi.on("resources_discover", async (event, _ctx) => {
// event.cwd - current working directory
// event.reason - "startup" | "reload"
return {
skillPaths: ["/path/to/skills"],
promptPaths: ["/path/to/prompts"],
themePaths: ["/path/to/themes"],
};
});

セッションストレージの内部と SessionManager API については、セッション形式 を参照してください。

セッションの開始時、ロード時、またはリロード時に発生します。

pi.on("session_start", async (event, ctx) => {
// event.reason - "startup" | "reload" | "new" | "resume" | "fork"
// event.previousSessionFile - present for "new", "resume", and "fork"
ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
});

新しいセッションを開始する前 (/new)、またはセッションを切り替える前 (/resume) に発生します。

pi.on("session_before_switch", async (event, ctx) => {
// event.reason - "new" or "resume"
// event.targetSessionFile - session we're switching to (only for "resume")
if (event.reason === "new") {
const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
if (!ok) return { cancel: true };
}
});

切り替えまたは新しいセッションのアクションが成功した後、pi は古い拡張機能インスタンスに対して session_shutdown を発行し、新しいセッションに対して拡張機能をリロードおよび再バインドしてから、reason: "new" | "resume" および previousSessionFile を含む session_start を発行します。 session_shutdown でクリーンアップ作業を実行し、その後 session_start でメモリ内の状態を再確立します。

/fork 経由でフォークするとき、または /clone 経由でクローン作成するときに発生します。

pi.on("session_before_fork", async (event, ctx) => {
// event.entryId - ID of the selected entry
// event.position - "before" for /fork, "at" for /clone
return { cancel: true }; // Cancel fork/clone
// OR
return { skipConversationRestore: true }; // Reserved for future conversation restore control
});

フォークまたはクローンが成功した後、pi は古い拡張機能インスタンスに対して session_shutdown を発行し、新しいセッションに対して拡張機能をリロードおよび再バインドしてから、reason: "fork" および previousSessionFile を含む session_start を発行します。 session_shutdown でクリーンアップ作業を実行し、その後 session_start でメモリ内の状態を再確立します。

圧縮時に発火します。詳細については、compaction.md を参照してください。

pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, signal } = event;
// Cancel:
return { cancel: true };
// Custom summary:
return {
compaction: {
summary: "...",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
}
};
});
pi.on("session_compact", async (event, ctx) => {
// event.compactionEntry - the saved compaction
// event.fromExtension - whether extension provided it
});

セッション前ツリー / セッションツリー

Section titled “セッション前ツリー / セッションツリー”

/tree ナビゲーションで発生します。ツリー ナビゲーションの概念については、セッション を参照してください。

pi.on("session_before_tree", async (event, ctx) => {
const { preparation, signal } = event;
return { cancel: true };
// OR provide custom summary:
return { summary: { summary: "...", details: {} } };
});
pi.on("session_tree", async (event, ctx) => {
// event.newLeafId, oldLeafId, summaryEntry, fromExtension
});

開始されたセッション ランタイムが破棄される前に発生します。これを使用して、session_start または他のセッション スコープのフックから開かれたリソースをクリーンアップします。

pi.on("session_shutdown", async (event, ctx) => {
// event.reason - "quit" | "reload" | "new" | "resume" | "fork"
// event.targetSessionFile - destination session for session replacement flows
// Cleanup, save state, etc.
});

ユーザーがプロンプトを送信した後、エージェント ループの前に発生します。メッセージを挿入したり、システム プロンプトを変更したりできます。

pi.on("before_agent_start", async (event, ctx) => {
// event.prompt - user's prompt text
// event.images - attached images (if any)
// event.systemPrompt - current chained system prompt for this handler
// (includes changes from earlier before_agent_start handlers)
// event.systemPromptOptions - structured options used to build the system prompt
// .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)
// .selectedTools - tools currently active in the prompt
// .toolSnippets - one-line descriptions for each tool
// .promptGuidelines - custom guideline bullets
// .appendSystemPrompt - text from --append-system-prompt flags
// .cwd - working directory
// .contextFiles - AGENTS.md files and other loaded context files
// .skills - loaded skills
return {
// Inject a persistent message (stored in session, sent to LLM)
message: {
customType: "my-extension",
content: "Additional context for the LLM",
display: true,
},
// Replace the system prompt for this turn (chained across extensions)
systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
};
});

systemPromptOptions フィールドにより、拡張機能は Pi がシステム プロンプトを構築するために使用するのと同じ構造化データにアクセスできるようになります。これにより、リソースを再検出したりフラグを再解析したりすることなく、Pi がロードしたもの (カスタム プロンプト、ガイドライン、ツール スニペット、コンテキスト ファイル、スキル) を検査できます。これは、拡張機能がユーザー指定の設定を尊重しながら、情報に基づいてシステム プロンプトに詳細な変更を加える必要がある場合に使用します。

before_agent_start 内では、event.systemPrompt および ctx.getSystemPrompt() は両方とも、現在のハンドラーの時点でのチェーンされたシステム プロンプトを反映しています。その後の before_agent_start ハンドラーは、引き続きこれを再度変更できます。

ユーザープロンプトごとに 1 回起動されます。

pi.on("agent_start", async (_event, ctx) => {});
pi.on("agent_end", async (event, ctx) => {
// event.messages - messages from this prompt
});

各ターン(LLM 応答 1 回+ツール呼び出し)ごとに起動されます。

pi.on("turn_start", async (event, ctx) => {
// event.turnIndex, event.timestamp
});
pi.on("turn_end", async (event, ctx) => {
// event.turnIndex, event.message, event.toolResults
});

message_start / message_update / message_end

Section titled “message_start / message_update / message_end”

メッセージのライフサイクル更新のために発生します。

  • message_start および message_end は、ユーザー、アシスタント、toolResult メッセージに対して起動されます。
  • message_update は、アシスタントのストリーミング更新のために起動します。
  • message_end ハンドラーは、確定されたメッセージを置き換えるために { message } を返すことができます。置き換えるメッセージは同じ role を維持する必要があります。
pi.on("message_start", async (event, ctx) => {
// event.message
});
pi.on("message_update", async (event, ctx) => {
// event.message
// event.assistantMessageEvent (token-by-token stream event)
});
pi.on("message_end", async (event, ctx) => {
if (event.message.role !== "assistant") return;
return {
message: {
...event.message,
usage: {
...event.message.usage,
cost: {
...event.message.usage.cost,
total: 0.123,
},
},
},
};
});

tool_execution_start / tool_execution_update / tool_execution_end

Section titled “tool_execution_start / tool_execution_update / tool_execution_end”

ツール実行ライフサイクルの更新のために発生します。

パラレルツールモード:

  • tool_execution_start は、プリフライト フェーズ中にアシスタント ソースの順序で発行されます。
  • tool_execution_update イベントはツール間でインターリーブされる可能性があります
  • tool_execution_end は、各ツールが完了した後、ツールの完了順に出力されます。
  • 最終 toolResult メッセージ イベントは、引き続きアシスタント ソースの順序で後から出力されます。
pi.on("tool_execution_start", async (event, ctx) => {
// event.toolCallId, event.toolName, event.args
});
pi.on("tool_execution_update", async (event, ctx) => {
// event.toolCallId, event.toolName, event.args, event.partialResult
});
pi.on("tool_execution_end", async (event, ctx) => {
// event.toolCallId, event.toolName, event.result, event.isError
});

各 LLM 呼び出しの前に発生します。メッセージを非破壊的に変更します。メッセージの種類については、セッション形式 を参照してください。

pi.on("context", async (event, ctx) => {
// event.messages - deep copy, safe to modify
const filtered = event.messages.filter(m => !shouldPrune(m));
return { messages: filtered };
});

プロバイダー固有のペイロードが構築された後、リクエストが送信される直前に起動されます。ハンドラーは拡張機能のロード順序で実行されます。 undefined を返すと、ペイロードは変更されません。他の値を返すと、後のハンドラーと実際のリクエストのペイロードが置き換えられます。

このフックは、プロバイダーレベルのシステム命令を書き換えたり、完全に削除したりできます。これらのペイロード レベルの変更は、最終的なシリアル化されたプロバイダー ペイロードではなく Pi のシステム プロンプト文字列を報告する ctx.getSystemPrompt() には反映されません。

pi.on("before_provider_request", (event, ctx) => {
console.log(JSON.stringify(event.payload, null, 2));
// Optional: replace payload
// return { ...event.payload, temperature: 0 };
});

これは主に、プロバイダーのシリアル化とキャッシュの動作をデバッグするのに役立ちます。

HTTP 応答を受信した後、そのストリーム本体が消費される前に発生します。ハンドラーは拡張機能のロード順序で実行されます。

pi.on("after_provider_response", (event, ctx) => {
// event.status - HTTP status code
// event.headers - normalized response headers
if (event.status === 429) {
console.log("rate limited", event.headers["retry-after"]);
}
});

ヘッダーが利用できるかどうかは、プロバイダーとトランスポートによって異なります。 HTTP 応答を抽象化するプロバイダーはヘッダーを公開できない場合があります。

/model コマンド、モデルのサイクリング (Ctrl+P)、またはセッションの復元によってモデルが変更されたときに発生します。

pi.on("model_select", async (event, ctx) => {
// event.model - newly selected model
// event.previousModel - previous model (undefined if first selection)
// event.source - "set" | "cycle" | "restore"
const prev = event.previousModel
? `${event.previousModel.provider}/${event.previousModel.id}`
: "none";
const next = `${event.model.provider}/${event.model.id}`;
ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");
});

これを使用して、UI 要素 (ステータス バー、フッター) を更新したり、アクティブなモデルが変更されたときにモデル固有の初期化を実行したりできます。

思考レベルが変化したときに発生します。これは通知のみです。ハンドラーの戻り値は無視されます。

pi.on("thinking_level_select", async (event, ctx) => {
// event.level - newly selected thinking level
// event.previousLevel - previous thinking level
ctx.ui.setStatus("thinking", `thinking: ${event.level}`);
});

pi.setThinkingLevel()、モデルの変更、または組み込みの思考レベル コントロールによってアクティブな思考レベルが変更されたときに、これを使用して拡張機能 UI を更新します。

tool_execution_start の後、ツールが実行される前に発生します。ブロックできます。 isToolCallEventType を使用して型を絞り込み、型付きの入力を取得します。

tool_call を実行する前に、pi は、以前に発行されたエージェント イベントが AgentSession を介して排出し終わるのを待ちます。これは、現在のアシスタント ツール呼び出しメッセージを通じて ctx.sessionManager が最新であることを意味します。

デフォルトの並列ツール実行モードでは、同じアシスタント メッセージからの兄弟ツール呼び出しが順番にプリフライトされ、同時に実行されます。 tool_call では、ctx.sessionManager の同じアシスタント メッセージからの兄弟ツールの結果が表示されることは保証されていません。

event.input は変更可能です。実行前にツール引数にパッチを適用するために、適切な場所で変更します。

動作の保証:

  • event.input への変更は実際のツールの実行に影響します
  • 後の tool_call ハンドラーは、前のハンドラーによる変更を認識します
  • 変更後に再検証は実行されません
  • tool_call の戻り値は { block: true, reason?: string } によるブロックのみを制御します
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
pi.on("tool_call", async (event, ctx) => {
// event.toolName - "bash", "read", "write", "edit", etc.
// event.toolCallId
// event.input - tool parameters (mutable)
// Built-in tools: no type params needed
if (isToolCallEventType("bash", event)) {
// event.input is { command: string; timeout?: number }
event.input.command = `source ~/.profile\n${event.input.command}`;
if (event.input.command.includes("rm -rf")) {
return { block: true, reason: "Dangerous command" };
}
}
if (isToolCallEventType("read", event)) {
// event.input is { path: string; offset?: number; limit?: number }
console.log(`Reading: ${event.input.path}`);
}
});

カスタム ツールは入力タイプをエクスポートする必要があります。

my-extension.ts
export type MyToolInput = Static<typeof myToolSchema>;

isToolCallEventType を明示的な型パラメータとともに使用します。

import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
import type { MyToolInput } from "my-extension";
pi.on("tool_call", (event) => {
if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
event.input.action; // typed
}
});

ツールの実行終了後、tool_execution_end および最終的なツール結果メッセージイベントが発行される前に発生します。結果を変更できます。

パラレル ツール モードでは、tool_resulttool_execution_end がツールの完了順序でインターリーブする可能性がありますが、最終 toolResult メッセージ イベントは引き続きアシスタント ソース順序で後で発行されます。

tool_result ハンドラーはミドルウェアのように連鎖します。

  • ハンドラーは拡張機能のロード順序で実行されます
  • 各ハンドラーは、以前のハンドラー変更後の最新の結果を参照します。
  • ハンドラーは部分的なパッチ (contentdetails、または isError) を返すことができます。省略されたフィールドは現在の値を保持します

ハンドラー内でネストされた非同期作業には ctx.signal を使用します。これにより、Esc でモデル呼び出し、fetch()、および拡張機能によって開始されたその他の中止対応操作をキャンセルできるようになります。

import { isBashToolResult } from "@earendil-works/pi-coding-agent";
pi.on("tool_result", async (event, ctx) => {
// event.toolName, event.toolCallId, event.input
// event.content, event.details, event.isError
if (isBashToolResult(event)) {
// event.details is typed as BashToolDetails
}
const response = await fetch("https://example.com/summarize", {
method: "POST",
body: JSON.stringify({ content: event.content }),
signal: ctx.signal,
});
// Modify result:
return { content: [...], details: {...}, isError: false };
});

ユーザーが ! または !! コマンドを実行すると発生します。インターセプト可能。

import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
pi.on("user_bash", (event, ctx) => {
// event.command - the bash command
// event.excludeFromContext - true if !! prefix
// event.cwd - working directory
// Option 1: Provide custom operations (e.g., SSH)
return { operations: remoteBashOps };
// Option 2: Wrap pi's built-in local bash backend
const local = createLocalBashOperations();
return {
operations: {
exec(command, cwd, options) {
return local.exec(`source ~/.profile\n${command}`, cwd, options);
}
}
};
// Option 3: Full replacement - return result directly
return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
});

拡張コマンドのチェック後、スキルとテンプレートの展開前に、ユーザー入力を受信したときに発生します。イベントは生の入力テキストを参照するため、/skill:foo/template はまだ展開されていません。

処理順序:

  1. 拡張コマンド (/cmd) が最初にチェックされます。見つかった場合、ハンドラーが実行され、入力イベントはスキップされます
  2. input イベントが発生します。インターセプト、変換、または処理が可能
  3. 処理されない場合: スキルコマンド (/skill:name) がスキルコンテンツに展開されます
  4. 処理されない場合: プロンプトテンプレート (/template) がテンプレートコンテンツに展開されます
  5. エージェント処理が開始されます (before_agent_start など)
pi.on("input", async (event, ctx) => {
// event.text - raw input (before skill/template expansion)
// event.images - attached images, if any
// event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
// event.streamingBehavior - "steer" | "followUp" | undefined
// undefined when idle, "steer" for mid-stream interrupts,
// "followUp" for messages queued until the agent finishes
// Transform: rewrite input before expansion
if (event.text.startsWith("?quick "))
return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };
// Handle: respond without LLM (extension shows its own feedback)
if (event.text === "ping") {
ctx.ui.notify("pong", "info");
return { action: "handled" };
}
// Route by source: skip processing for extension-injected messages
if (event.source === "extension") return { action: "continue" };
// Intercept skill commands before expansion
if (event.text.startsWith("/skill:")) {
// Could transform, block, or let pass through
}
return { action: "continue" }; // Default: pass through to expansion
});

結果:

  • continue - 変更せずに通過します (ハンドラーが何も返さない場合のデフォルト)
  • transform - テキスト/画像を変更し、拡張を続行します
  • handled - エージェントを完全にスキップします (これを返す最初のハンドラーが勝ちます)

ハンドラー間でチェーンを変換します。 streamingBehavior 対応ルーティングについては、input-transform.ts および input-transform-streaming.ts を参照してください。

すべてのハンドラーは ctx: ExtensionContext を受け取ります。

ユーザー対話のための UI メソッド。詳細については、カスタム UI を参照してください。

現在の実行モード: "tui""rpc""json"、または "print"ctx.mode === "tui" を使用して、custom()、コンポーネントファクトリ、ターミナル入力、ダイレクト TUI レンダリングなどのターミナル専用機能を保護します。

TUI および RPC モードの true。印刷モード (-p) および JSON モードの false。これを使用して、TUI モードと RPC モードの両方で動作するダイアログ メソッド (selectconfirminputeditor) およびファイア アンド フォーゲット メソッド (notifysetStatussetWidgetsetTitlesetEditorText) を保護します。 RPC モードでは、一部の TUI 固有のメソッドは操作なし、またはデフォルトを返します (rpc.md を参照)。

現在の作業ディレクトリ。

プロジェクトローカル構成パスを構築するときは、.pi をハードコーディングする代わりに CONFIG_DIR_NAME を使用してください。ブランド変更されたディストリビューションでは、別の構成ディレクトリ名を使用できます。

import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { join } from "node:path";
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json");
// ...
});
}

現在のセッション コンテキストに対してプロジェクト ローカルの信頼がアクティブかどうかを返します。これには、グローバル トラスト ストアに保存された決定だけでなく、一時的な信頼の決定と CLI の信頼のオーバーライドも含まれます。

これは、信頼できるプロジェクトに対してのみ適用されるプロジェクト ローカル拡張機能の設定を読み取る前に使用してください。

セッション状態への読み取り専用アクセス。完全な SessionManager API とエントリ タイプについては、セッション形式 を参照してください。

tool_call の場合、この状態はハンドラーが実行される前に、現在のアシスタント メッセージを通じて同期されます。並列ツール実行モードでは、同じアシスタント メッセージからの兄弟ツールの結果が含まれることはまだ保証されていません。

ctx.sessionManager.getEntries() // All entries
ctx.sessionManager.getBranch() // Current branch
ctx.sessionManager.getLeafId() // Current leaf entry ID

モデルと API キーへのアクセス。

現在のエージェントの中止シグナル。アクティブなエージェントターンがない場合は undefined

拡張ハンドラーから起動された中止対応のネスト処理で使用します。例:

  • fetch(..., { signal: ctx.signal })
  • signal を受け入れるモデル呼び出し
  • AbortSignal を受け入れるファイルまたはプロセスヘルパー

ctx.signal は通常、tool_calltool_resultmessage_updateturn_end などのアクティブなターンイベント中に定義されます。 セッションイベント、拡張コマンド、pi がアイドル状態のときに起動されるショートカットなど、アイドルまたは非ターンコンテキストでは通常 undefined です。

pi.on("tool_result", async (event, ctx) => {
const response = await fetch("https://example.com/api", {
method: "POST",
body: JSON.stringify(event),
signal: ctx.signal,
});
const data = await response.json();
return { details: data };
});

ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()

Section titled “ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()”

制御フローヘルパー。

pi の正常なシャットダウンを要求します。

  • 対話型モード: エージェントがアイドル状態になるまで延期されます (キューに入れられたステアリングおよびフォローアップ メッセージをすべて処理した後)。
  • RPC モード: 次のアイドル状態まで延期されます (現在のコマンド応答が完了した後、次のコマンドを待機しているとき)。
  • 印刷モード: 何もしません。すべてのプロンプトが処理されると、プロセスは自動的に終了します。

終了する前に、すべての拡張機能に session_shutdown イベントを送信します。すべてのコンテキスト (イベント ハンドラー、ツール、コマンド、ショートカット) で使用できます。

pi.on("tool_call", (event, ctx) => {
if (isFatal(event.input)) {
ctx.shutdown();
}
});

アクティブなモデルの現在のコンテキストの使用状況を返します。利用可能な場合は最後のアシスタントの使用状況を使用し、後続メッセージのトークンを推定します。

const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
// ...
}

完了を待たずに圧縮をトリガーします。フォローアップアクションには onComplete および onError を使用します。

ctx.compact({
customInstructions: "Focus on recent changes",
onComplete: (result) => {
ctx.ui.notify("Compaction completed", "info");
},
onError: (error) => {
ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
},
});

Pi の現在のシステム プロンプト文字列を返します。

  • before_agent_start の間、これは現在のターンでこれまでに行われた連鎖的なシステム プロンプトの変更を反映します。
  • これには、その後の context メッセージの変異は含まれません。
  • before_provider_request ペイロードの書き換えは含まれません。
  • 後からロードされた拡張機能が拡張機能の後に実行された場合でも、最終的に送信される内容が変更される可能性があります。
pi.on("before_agent_start", (event, ctx) => {
const prompt = ctx.getSystemPrompt();
console.log(`System prompt length: ${prompt.length}`);
});

コマンド ハンドラーは ExtensionCommandContext を受け取ります。これは、セッション制御メソッドで ExtensionContext を拡張します。これらはイベント ハンドラーから呼び出された場合にデッドロックが発生する可能性があるため、コマンドでのみ使用できます。

Pi がシステム プロンプトを構築するために現在使用している基本入力を返します。

const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];

これは、before_agent_start event.systemPromptOptions と同じ形状および変更可能性を持ちます: カスタム プロンプト、アクティブ ツール、ツール スニペット、プロンプト ガイドライン、追加されたシステム プロンプト テキスト、CWD、ロードされたコンテキスト ファイル、およびロードされたスキル。これにはコンテキスト ファイルの完全な内容が含まれる可能性があるため、機密性の高い拡張機能ローカル データとして扱い、コマンド リスト、ログ、またはオートコンプリート メタデータを通じて公開することは避けてください。

これは、現在の基本プロンプト入力を報告します。これには、ターンごとの before_agent_start チェーンされたシステム プロンプトの変更、その後の context イベント メッセージの変更、または before_provider_request ペイロードの書き換えは含まれません。

エージェントがストリーミングを終了するまで待ちます。

pi.registerCommand("my-cmd", {
handler: async (args, ctx) => {
await ctx.waitForIdle();
// Agent is now idle, safe to modify session
},
});

新しいセッションを作成します。

const parentSession = ctx.sessionManager.getSessionFile();
const kickoff = "Continue in the replacement session";
const result = await ctx.newSession({
parentSession,
setup: async (sm) => {
sm.appendMessage({
role: "user",
content: [{ type: "text", text: "Context from previous session..." }],
timestamp: Date.now(),
});
},
withSession: async (ctx) => {
// Use only the replacement-session ctx here.
await ctx.sendUserMessage(kickoff);
},
});
if (result.cancelled) {
// An extension cancelled the new session
}

オプション:

  • parentSession: 新しいセッション ヘッダーに記録する親セッション ファイル
  • setup: withSession が実行される前に、新しいセッションの SessionManager を変更します。
  • withSession: 新しい交換セッション コンテキストに対して切り替え後の作業を実行します。キャプチャされた古い pi / コマンド ctx は使用しないでください。 セッション交換のライフサイクルとフットガンを参照してください。

特定のエントリからフォークして、新しいセッション ファイルを作成します。

const result = await ctx.fork("entry-id-123", {
withSession: async (ctx) => {
// Use only the replacement-session ctx here.
ctx.ui.notify("Now in the forked session", "info");
},
});
if (result.cancelled) {
// An extension cancelled the fork
}
const cloneResult = await ctx.fork("entry-id-456", { position: "at" });
if (cloneResult.cancelled) {
// An extension cancelled the clone
}

オプション:

  • position: "before" (デフォルト) は、選択したユーザー メッセージの前でフォークし、そのプロンプトをエディターに復元します。
  • position: "at" は、エディター テキストを復元せずに、選択したエントリを介してアクティブ パスを複製します。
  • withSession: 新しい交換セッション コンテキストに対して切り替え後の作業を実行します。キャプチャされた古い pi / コマンド ctx は使用しないでください。 セッション交換のライフサイクルとフットガンを参照してください。

ctx.navigateTree(targetId、オプション?)

Section titled “ctx.navigateTree(targetId、オプション?)”

セッション ツリー内の別のポイントに移動します。

const result = await ctx.navigateTree("entry-id-456", {
summarize: true,
customInstructions: "Focus on error handling changes",
replaceInstructions: false, // true = replace default prompt entirely
label: "review-checkpoint",
});

オプション:

  • summarize: 放棄されたブランチの概要を生成するかどうか
  • customInstructions: サマライザーのカスタム命令
  • replaceInstructions: true の場合、customInstructions は追加されるのではなく、デフォルトのプロンプトを置き換えます。
  • label: ブランチ概要エントリ (または概要でない場合はターゲット エントリ) に付けるラベル

ctx.switchSession(セッションパス、オプション?)

Section titled “ctx.switchSession(セッションパス、オプション?)”

別のセッション ファイルに切り替えます。

const result = await ctx.switchSession("/path/to/session.jsonl", {
withSession: async (ctx) => {
await ctx.sendUserMessage("Resume work in the replacement session");
},
});
if (result.cancelled) {
// An extension cancelled the switch via session_before_switch
}

オプション:

使用可能なセッションを検出するには、静的な SessionManager.list() メソッドまたは SessionManager.listAll() メソッドを使用します。

import { SessionManager } from "@earendil-works/pi-coding-agent";
pi.registerCommand("switch", {
description: "Switch to another session",
handler: async (args, ctx) => {
const sessions = await SessionManager.list(ctx.cwd);
if (sessions.length === 0) return;
const choice = await ctx.ui.select(
"Pick session:",
sessions.map(s => s.file),
);
if (choice) {
await ctx.switchSession(choice, {
withSession: async (ctx) => {
ctx.ui.notify("Switched session", "info");
},
});
}
},
});

セッション置換のライフサイクルとフットガン

Section titled “セッション置換のライフサイクルとフットガン”

withSession は新しい ReplacedSessionContext を受け取り、置換セッションにバインドされた非同期 sendMessage() および sendUserMessage() ヘルパーで ExtensionCommandContext を拡張します。

ライフサイクルとフットガン:

  • withSession は、古いセッションが session_shutdown を発行し、古いランタイムが破棄され、代替セッションがバインドされ、新しい拡張機能インスタンスがすでに session_start を受信した後にのみ実行されます。
  • コールバックは、新しい拡張機能インスタンス内ではなく、元のクロージャ内で引き続き実行されます。つまり、withSession が開始される前に、古い拡張機能インスタンスがすでにシャットダウン クリーンアップを実行している可能性があります。
  • キャプチャされた古い pi / 古いコマンド ctx セッション バインド オブジェクトは、置換後は古くなり、使用するとスローされます。セッションにバインドされた作業には、withSession に渡された ctx のみを使用してください。
  • 以前に抽出した生のオブジェクトは引き続きお客様の責任となります。たとえば、置換前に const sm = ctx.sessionManager をキャプチャした場合、sm は古い SessionManager オブジェクトのままです。交換後の再使用はしないでください。
  • withSession のコードは、session_shutdown ハンドラーによって無効化された状態はすでになくなっていると想定する必要があります。文字列、ID、シリアル化された構成など、シャットダウンしても問題なく残るプレーン データのみをキャプチャします。

安全なパターン:

pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const kickoff = "Continue from the replacement session";
await ctx.newSession({
withSession: async (ctx) => {
await ctx.sendUserMessage(kickoff);
},
});
},
});

安全でないパターン:

pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const oldSessionManager = ctx.sessionManager;
await ctx.newSession({
withSession: async (_ctx) => {
// stale old objects: do not do this
oldSessionManager.getSessionFile();
pi.sendUserMessage("wrong");
},
});
},
});

/reload と同じリロード フローを実行します。

pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, and themes",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});

重要な行動:

  • await ctx.reload() は、現在の拡張機能ランタイムに対して session_shutdown を発行します
  • その後、リソースをリロードし、reason: "reload" を含む session_start"reload" を理由とする resources_discover を発行します。
  • 現在実行中のコマンド ハンドラーは引き続き古い呼び出しフレームを継続します。
  • await ctx.reload() 以降のコードはリロード前のバージョンから引き続き実行されます
  • await ctx.reload() 以降のコードは、古いメモリ内拡張機能の状態がまだ有効であると想定してはなりません
  • ハンドラーが戻った後、以降のコマンド/イベント/ツール呼び出しでは新しい拡張バージョンが使用されます。

予測可能な動作を実現するには、リロードをそのハンドラー (await ctx.reload(); return;) の端末として扱います。

ツールは ExtensionContext で実行されるため、ctx.reload() を直接呼び出すことはできません。コマンドをリロード エントリポイントとして使用し、そのコマンドをフォローアップ ユーザー メッセージとしてキューに入れるツールを公開します。

LLM がリロードをトリガーするために呼び出すことができるツールの例:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, and themes",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});
pi.registerTool({
name: "reload_runtime",
label: "Reload Runtime",
description: "Reload extensions, skills, prompts, and themes",
parameters: Type.Object({}),
async execute() {
pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
return {
content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
};
},
});
}

イベントを購読します。イベントの種類と戻り値については、イベント を参照してください。

LLM によって呼び出し可能なカスタム ツールを登録します。詳細については、カスタム ツール を参照してください。

pi.registerTool() は、拡張機能のロード中と起動後の両方で機能します。これは、session_start、コマンド ハンドラー、またはその他のイベント ハンドラー内で呼び出すことができます。新しいツールは同じセッション内ですぐに更新されるため、pi.getAllTools() に表示され、/reload がなくても LLM から呼び出すことができます。

pi.setActiveTools() を使用して、実行時にツール (動的に追加されたツールを含む) を有効または無効にします。

カスタム ツールを Available tools の 1 行エントリに選択するには promptSnippet を使用し、ツールがアクティブなときにデフォルトの Guidelines セクションにツール固有の箇条書きを追加するには promptGuidelines を使用します。

重要: promptGuidelines の箇条書きは、ツール名のプレフィックスなしで Guidelines セクションにフラットに追加されます。各ガイドラインでは、参照するツールに名前を付ける必要があります。LLM は「これ」がどのツールを意味するかを判断できないため、「次の場合にこのツールを使用する」は避けてください。代わりに「次の場合に my_tool を使用する」と書きます。

完全な例については、dynamic-tools.ts を参照してください。

import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does",
promptSnippet: "Summarize or transform text according to action",
promptGuidelines: ["Use my_tool when the user asks to summarize previously generated text."],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const),
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
// Optional compatibility shim. Runs before schema validation.
// Return the current schema shape, for example to fold legacy fields
// into the modern parameter object.
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Stream progress
onUpdate?.({ content: [{ type: "text", text: "Working..." }] });
return {
content: [{ type: "text", text: "Done" }],
details: { result: "..." },
};
},
// Optional: Custom rendering
renderCall(args, theme, context) { ... },
renderResult(result, options, theme, context) { ... },
});

pi.sendMessage(メッセージ、オプション?)

Section titled “pi.sendMessage(メッセージ、オプション?)”

カスタム メッセージをセッションに挿入します。

pi.sendMessage({
customType: "my-extension",
content: "Message text",
display: true,
details: { ... },
}, {
triggerTurn: true,
deliverAs: "steer",
});

オプション:

  • deliverAs - 配信モード:
    • "steer" (デフォルト) - ストリーミング中にメッセージをキューに入れます。現在のアシスタント ターンがツール呼び出しの実行を終了した後、次の LLM 呼び出しの前に配信されます。
    • "followUp" - エージェントが終了するまで待機します。エージェントがツールを呼び出さなくなった場合にのみ配信されます。
    • "nextTurn" - 次のユーザー プロンプトのキューに入れられました。何も中断したりトリガーしたりしません。
  • triggerTurn: true - エージェントがアイドル状態の場合、LLM 応答を直ちにトリガーします。 "steer" および "followUp" モードにのみ適用されます ("nextTurn" の場合は無視されます)。

pi.sendUserMessage(コンテンツ、オプション?)

Section titled “pi.sendUserMessage(コンテンツ、オプション?)”

ユーザーメッセージをエージェントに送信します。カスタム メッセージを送信する sendMessage() とは異なり、ユーザーが入力したかのように表示される実際のユーザー メッセージが送信されます。常にターンを誘発します。

// Simple text message
pi.sendUserMessage("What is 2+2?");
// With content array (text + images)
pi.sendUserMessage([
{ type: "text", text: "Describe this image:" },
{ type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },
]);
// During streaming - must specify delivery mode
pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });

オプション:

  • deliverAs - エージェントがストリーミングしている場合に必要です:
    • "steer" - 現在のアシスタント ターンがツール呼び出しの実行を終了した後、配信するメッセージをキューに入れます。
    • "followUp" - エージェントがすべてのツールを完了するまで待機します

ストリーミングしていない場合、メッセージはすぐに送信され、新しいターンがトリガーされます。 deliverAs を使用せずにストリーミングすると、エラーがスローされます。

完全な例については、send-user-message.ts を参照してください。

拡張機能の状態を保持します (LLM コンテキストには参加しません)。

pi.appendEntry("my-state", { count: 42 });
// Restore on reload
pi.on("session_start", async (_event, ctx) => {
for (const entry of ctx.sessionManager.getEntries()) {
if (entry.type === "custom" && entry.customType === "my-state") {
// Reconstruct from entry.data
}
}
});

セッションの表示名を設定します (最初のメッセージの代わりにセッション セレクターに表示されます)。

pi.setSessionName("Refactor auth module");

現在のセッション名が設定されている場合は、それを取得します。

const name = pi.getSessionName();
if (name) {
console.log(`Session: ${name}`);
}

エントリのラベルを設定またはクリアします。ラベルは、ブックマークとナビゲーション用のユーザー定義のマーカーです (/tree セレクターに表示されます)。

// Set a label
pi.setLabel(entryId, "checkpoint-before-refactor");
// Clear a label
pi.setLabel(entryId, undefined);
// Read labels via sessionManager
const label = ctx.sessionManager.getLabel(entryId);

ラベルはセッション内に保持され、再起動しても存続します。これらを使用して、会話ツリー内の重要なポイント (ターン、チェックポイント) をマークします。

pi.registerCommand(名前, オプション)

Section titled “pi.registerCommand(名前, オプション)”

コマンドを登録します。

複数の拡張機能が同じコマンド名を登録している場合、pi はそれらすべてを保持し、ロード順に数値呼び出しサフィックス (/review:1/review:2 など) を割り当てます。

pi.registerCommand("stats", {
description: "Show session statistics",
handler: async (args, ctx) => {
const count = ctx.sessionManager.getEntries().length;
ctx.ui.notify(`${count} entries`, "info");
}
});

オプション: /command ... の引数自動補完を追加します。

import type { AutocompleteItem } from "@earendil-works/pi-tui";
pi.registerCommand("deploy", {
description: "Deploy to an environment",
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
const envs = ["dev", "staging", "prod"];
const items = envs.map((e) => ({ value: e, label: e }));
const filtered = items.filter((i) => i.value.startsWith(prefix));
return filtered.length > 0 ? filtered : null;
},
handler: async (args, ctx) => {
ctx.ui.notify(`Deploying: ${args}`, "info");
},
});

現在のセッションで prompt 経由で呼び出し可能なスラッシュ コマンドを取得します。拡張コマンド、プロンプト テンプレート、スキル コマンドが含まれます。 このリストは RPC get_commands の順序と一致します。最初に拡張機能、次にテンプレート、次にスキルです。

const commands = pi.getCommands();
const bySource = commands.filter((command) => command.source === "extension");
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");

各エントリは次のような形状になっています。

{
name: string; // Invokable command name without the leading slash. May be suffixed like "review:1"
description?: string;
source: "extension" | "prompt" | "skill";
sourceInfo: {
path: string;
source: string;
scope: "user" | "project" | "temporary";
origin: "package" | "top-level";
baseDir?: string;
};
}

sourceInfo を正規の出所フィールドとして使用します。コマンド名やアドホック パス解析から所有権を推測しないでください。

組み込みの対話型コマンド (/model/settings など) はここには含まれません。インタラクティブでのみ処理されます モードであり、prompt 経由で送信された場合は実行されません。

pi.registerMessageRenderer(customType, レンダラー)

Section titled “pi.registerMessageRenderer(customType, レンダラー)”

メッセージ用のカスタム TUI レンダラーを customType に登録します。 カスタム UI を参照してください。

pi.registerShortcut(ショートカット、オプション)

Section titled “pi.registerShortcut(ショートカット、オプション)”

キーボードショートカットを登録します。ショートカットの形式と組み込みのキーバインディングについては、keybindings.md を参照してください。

pi.registerShortcut("ctrl+shift+p", {
description: "Toggle plan mode",
handler: async (ctx) => {
ctx.ui.notify("Toggled!");
},
});

CLIフラグを登録します。

pi.registerFlag("plan", {
description: "Start in plan mode",
type: "boolean",
default: false,
});
// Check value
if (pi.getFlag("plan")) {
// Plan mode enabled
}

pi.exec(コマンド、引数、オプション?)

Section titled “pi.exec(コマンド、引数、オプション?)”

シェルコマンドを実行します。

const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killed

pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)

Section titled “pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)”

アクティブなツールを管理します。 This works for both built-in tools and dynamically registered tools. pi.getActiveTools() returns the active tool names as string[]; pi.getAllTools() returns metadata for all configured tools.

const active = pi.getActiveTools(); // ["read", "bash", ...]
const all = pi.getAllTools();
// all = [{
// name: "read",
// description: "Read file contents...",
// parameters: ...,
// promptGuidelines: ["Use read to examine files instead of cat or sed."],
// sourceInfo: { path: "<builtin:read>", source: "builtin", scope: "temporary", origin: "top-level" }
// }, ...]
const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin");
const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk");
pi.setActiveTools([...new Set([...active, "my_custom_tool"])]); // Keep current tools and enable my_custom_tool
pi.setActiveTools(["read", "bash"]); // Switch to read-only

pi.getAllTools() は、namedescriptionparameterspromptGuidelines、および sourceInfo を返します。

Typical sourceInfo.source values:

  • builtin for built-in tools
  • sdk for tools passed via createAgentSession({ customTools })
  • 拡張機能によって登録されたツールの拡張機能ソース メタデータ

現在のモデルを設定します。モデルで使用できる API キーがない場合は、false を返します。カスタム モデルの構成については、models.md を参照してください。

const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
if (model) {
const success = await pi.setModel(model);
if (!success) {
ctx.ui.notify("No API key for this model", "error");
}
}

pi.getThinkingLevel() / pi.setThinkingLevel(レベル)

Section titled “pi.getThinkingLevel() / pi.setThinkingLevel(レベル)”

思考レベルを取得または設定します。レベルはモデルの能力に固定されます (非推論モデルは常に「オフ」を使用します)。変更により thinking_level_select が発行されます。

const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
pi.setThinkingLevel("high");

拡張機能間の通信用の共有イベント バス:

pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });

モデルプロバイダーを動的に登録またはオーバーライドします。プロキシ、カスタム エンドポイント、またはチーム全体のモデル構成に役立ちます。

拡張ファクトリー関数中に行われた呼び出しはキューに入れられ、ランナーが初期化されると適用されます。それ以降に行われた呼び出し (たとえば、ユーザー セットアップ フローに続くコマンド ハンドラーからの呼び出し) は、/reload を必要とせずにすぐに有効になります。

リモート エンドポイントからモデルを検出する必要がある場合は、フェッチを session_start に遅延させるよりも、非同期拡張ファクトリーを優先してください。 pi は起動を続行する前にファクトリーを待機するため、pi --list-models を含め、登録されたモデルはすぐに使用可能になります。

// Register a new provider with custom models
pi.registerProvider("my-proxy", {
name: "My Proxy",
baseUrl: "https://proxy.example.com",
apiKey: "$PROXY_API_KEY", // env var reference
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet (proxy)",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 16384
}
]
});
// Override baseUrl for an existing provider (keeps all models)
pi.registerProvider("anthropic", {
baseUrl: "https://proxy.example.com"
});
// Register provider with OAuth support for /login
pi.registerProvider("corporate-ai", {
baseUrl: "https://ai.corp.com",
api: "openai-responses",
models: [...],
oauth: {
name: "Corporate AI (SSO)",
async login(callbacks) {
// Custom OAuth flow
callbacks.onAuth({ url: "https://sso.corp.com/..." });
const code = await callbacks.onPrompt({ message: "Enter code:" });
return { refresh: code, access: code, expires: Date.now() + 3600000 };
},
async refreshToken(credentials) {
// Refresh logic
return credentials;
},
getApiKey(credentials) {
return credentials.access;
}
}
});

設定オプション:

  • name - UI でのプロバイダーの表示名 (/login など)。
  • baseUrl - API エンドポイント URL。モデルを定義するときに必要です。
  • apiKey - API キー リテラル、環境補間 ($ENV_VAR または ${ENV_VAR})、または先頭の !command。モデルを定義するときに必要です (oauth が提供されていない場合)。 $$$ をエスケープし、$! はコマンドの実行をトリガーせずにリテラル ! をエスケープします。
  • api - API タイプ: "anthropic-messages""openai-completions""openai-responses" など。
  • headers - リクエストに含めるカスタム ヘッダー。
  • authHeader - true の場合、Authorization: Bearer ヘッダーが自動的に追加されます。
  • models - モデル定義の配列。指定すると、このプロバイダーの既存のモデルがすべて置き換えられます。モデル定義では、baseUrl を設定して、そのモデルのプロバイダー エンドポイントをオーバーライドできます。
  • oauth - /login サポートのための OAuth プロバイダー構成。プロバイダーを指定すると、ログイン メニューに表示されます。
  • streamSimple - 非標準 API のカスタム ストリーミング実装。

カスタム ストリーミング API、OAuth の詳細、モデル定義リファレンスなどの高度なトピックについては、custom-provider.md を参照してください。

以前に登録したプロバイダーとそのモデルを削除します。プロバイダーによってオーバーライドされた組み込みモデルが復元されます。プロバイダーが登録されていない場合は効果がありません。

registerProvider と同様、これは初期ロード フェーズの後に呼び出すとすぐに有効になるため、/reload は必要ありません。

pi.registerCommand("my-setup-teardown", {
description: "Remove the custom proxy provider",
handler: async (_args, _ctx) => {
pi.unregisterProvider("my-proxy");
},
});

状態を含む拡張機能は、適切な分岐サポートのためにツールの結果 details にそれを保存する必要があります。

export default function (pi: ExtensionAPI) {
let items: string[] = [];
// Reconstruct state from session
pi.on("session_start", async (_event, ctx) => {
items = [];
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type === "message" && entry.message.role === "toolResult") {
if (entry.message.toolName === "my_tool") {
items = entry.message.details?.items ?? [];
}
}
}
});
pi.registerTool({
name: "my_tool",
// ...
async execute(toolCallId, params, signal, onUpdate, ctx) {
items.push("new item");
return {
content: [{ type: "text", text: "Added" }],
details: { items: [...items] }, // Store for reconstruction
};
},
});
}

LLM が pi.registerTool() 経由で呼び出すことができるツールを登録します。ツールはシステム プロンプトに表示され、カスタム レンダリングを行うことができます。

デフォルトのシステム プロンプトの Available tools セクションの短い 1 行エントリには、promptSnippet を使用します。省略した場合、カスタム ツールはそのセクションから除外されます。

promptGuidelines を使用して、ツール固有の箇条書きをデフォルトのシステム プロンプト Guidelines セクションに追加します。これらの箇条書きは、ツールがアクティブな間 (たとえば、pi.setActiveTools([...]) の後) にのみ含まれます。

重要: promptGuidelines の箇条書きは、ツール名のプレフィックスやグループ化なしで、フラットに Guidelines セクションに追加されます。各ガイドラインでは、参照するツールに名前を付ける必要があります。LLM は「これ」がどのツールを意味するかを判断できないため、「次の場合にこのツールを使用する」は避けてください。代わりに「次の場合に my_tool を使用する」と書きます。

注: 一部のモデルはツールパス引数に @ プレフィックスを含めることがあります。組み込みツールはパスを解決する前に先頭の @ を除去します。カスタムツールがパスを受け入れる場合は、先頭の @ も同様に正規化してください。

カスタム ツールがファイルを変更する場合は、withFileMutationQueue() を使用して、組み込みの edit および write と同じファイルごとのキューに参加させます。デフォルトではツール呼び出しが並行して実行されるため、これは重要です。キューがなければ、2 つのツールが同じ古いファイルの内容を読み取り、異なる更新を計算し、最後に書き込まれた方が他方を上書きする可能性があります。

失敗例: カスタム ツールは foo.ts を編集しますが、組み込みの edit も同じアシスタント ターンで foo.ts を変更します。ツールがキューに参加していない場合、両方が元の foo.ts を読み取り、別々の変更を適用することができ、それらの変更の 1 つが失われます。

raw ユーザー引数ではなく、実際のターゲット ファイル パスを withFileMutationQueue() に渡します。最初に、ctx.cwd またはツールの作業ディレクトリを基準とした絶対パスに解決します。既存のファイルの場合、ヘルパーは realpath() を通じて正規化するため、同じファイルのシンボリックリンク エイリアスは 1 つのキューを共有します。新しいファイルの場合は、realpath() にはまだ何もないため、解決された絶対パスに戻ります。

変更ウィンドウ全体をそのターゲット パス上でキューに入れます。これには、最終的な書き込みだけでなく、読み取り、変更、書き込みロジックも含まれます。

import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const absolutePath = resolve(ctx.cwd, params.path);
return withFileMutationQueue(absolutePath, async () => {
await mkdir(dirname(absolutePath), { recursive: true });
const current = await readFile(absolutePath, "utf8");
const next = current.replace(params.oldText, params.newText);
await writeFile(absolutePath, next, "utf8");
return {
content: [{ type: "text", text: `Updated ${params.path}` }],
details: {},
};
});
}
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
import { Text } from "@earendil-works/pi-tui";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does (shown to LLM)",
promptSnippet: "List or add items in the project todo list",
promptGuidelines: [
"Use my_tool for todo planning instead of direct file edits when the user asks for a task list."
],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return args;
const input = args as { action?: string; oldAction?: string };
if (typeof input.oldAction === "string" && input.action === undefined) {
return { ...input, action: input.oldAction };
}
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Check for cancellation
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
}
// Stream progress updates
onUpdate?.({
content: [{ type: "text", text: "Working..." }],
details: { progress: 50 },
});
// Run commands via pi.exec (captured from extension closure)
const result = await pi.exec("some-command", [], { signal });
// Return result
return {
content: [{ type: "text", text: "Done" }], // Sent to LLM
details: { data: result }, // For rendering & state
// Optional: stop after this tool batch when every finalized tool result
// in the batch also returns terminate: true.
terminate: true,
};
},
// Optional: Custom rendering
renderCall(args, theme, context) { ... },
renderResult(result, options, theme, context) { ... },
});

エラーの通知: ツールの実行を失敗としてマークするには (結果に isError: true を設定し、LLM に報告します)、execute からエラーをスローします。戻りオブジェクトにどのようなプロパティを含めるかに関係なく、値を返すとエラー フラグが設定されることはありません。

早期終了: execute() から terminate: true を返し、現在のツール バッチの後に自動フォローアップ LLM 呼び出しをスキップする必要があることを示します。これは、すべての完成したツールの結果としてそのバッチが終了する場合にのみ有効になります。エージェントが最後の構造化出力ツール呼び出しで終了する最小限の例については、examples/extensions/structurd-output.ts を参照してください。

// Correct: throw to signal an error
async execute(toolCallId, params) {
if (!isValid(params.input)) {
throw new Error(`Invalid input: ${params.input}`);
}
return { content: [{ type: "text", text: "OK" }], details: {} };
}

重要: 文字列列挙には @earendil-works/pi-ai から StringEnum を使用します。 Type.Union/Type.Literal は Google の API では動作しません。

引数の準備: prepareArguments(args) はオプションです。定義されている場合、スキーマ検証の前および execute() の前に実行されます。これを使用して、保存されているツール呼び出し引数が現在のスキーマと一致しなくなった古いセッションを pi が再開するときに、受け入れられた古い入力形状を模倣します。 parameters に対して検証するオブジェクトを返します。パブリックスキーマを厳密に保ちます。再開された古いセッションを動作し続けるためだけに、非推奨の互換性フィールドを parameters に追加しないでください。

例: 古いセッションには、トップレベル oldText および newText を使用した edit ツール呼び出しが含まれている可能性がありますが、現在のスキーマは edits: [{ oldText, newText }] のみを受け入れます。

pi.registerTool({
name: "edit",
label: "Edit",
description: "Edit a single file using exact text replacement",
parameters: Type.Object({
path: Type.String(),
edits: Type.Array(
Type.Object({
oldText: Type.String(),
newText: Type.String(),
}),
),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return args;
const input = args as {
path?: string;
edits?: Array<{ oldText: string; newText: string }>;
oldText?: unknown;
newText?: unknown;
};
if (typeof input.oldText !== "string" || typeof input.newText !== "string") {
return args;
}
return {
...input,
edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],
};
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// params now matches the current schema
return {
content: [{ type: "text", text: `Applying ${params.edits.length} edit block(s)` }],
details: {},
};
},
});

組み込みツールのオーバーライド

Section titled “組み込みツールのオーバーライド”

拡張機能は、同じ名前のツールを登録することで、組み込みツール (readbasheditwritegrepfindls) をオーバーライドできます。対話型モードでは、これが発生した場合に警告が表示されます。

Terminal window
# Extension's read tool replaces built-in read
pi -e ./tool-override.ts

または、--no-builtin-tools を使用して、拡張ツールを有効にしたまま組み込みツールなしで開始します。

Terminal window
# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.ts

ロギングとアクセス制御で read をオーバーライドする完全な例については、examples/extensions/tool-override.ts を参照してください。

レンダリング: 組み込みレンダラーの継承はスロットごとに解決されます。実行オーバーライドとレンダリング オーバーライドは独立しています。オーバーライドで renderCall を省略した場合は、組み込みの renderCall が使用されます。オーバーライドで renderResult を省略した場合は、組み込みの renderResult が使用されます。オーバーライドで両方を省略した場合、組み込みレンダラが自動的に使用されます (構文の強調表示、差分など)。これにより、UI を再実装することなく、ログ記録やアクセス制御用の組み込みツールをラップできます。

プロンプト メタデータ: promptSnippet および promptGuidelines は組み込みツールから継承されません。オーバーライドでこれらのプロンプト指示を維持する必要がある場合は、それらをオーバーライドで明示的に定義します。

実装は、details タイプを含む結果の形状と正確に一致する必要があります。 UI とセッション ロジックは、レンダリングと状態追跡のためにこれらの形状に依存します。

組み込みツールの実装:

組み込みツールは、リモート システム (SSH、コンテナなど) に委任するためのプラグ可能な操作をサポートします。

import { createReadTool, createBashTool, type ReadOperations } from "@earendil-works/pi-coding-agent";
// Create tool with custom operations
const remoteRead = createReadTool(cwd, {
operations: {
readFile: (path) => sshExec(remote, `cat ${path}`),
access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
}
});
// Register, checking flag at execution time
pi.registerTool({
...remoteRead,
async execute(id, params, signal, onUpdate, _ctx) {
const ssh = getSshConfig();
if (ssh) {
const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });
return tool.execute(id, params, signal, onUpdate);
}
return localRead.execute(id, params, signal, onUpdate);
},
});

操作インターフェイス: ReadOperationsWriteOperationsEditOperationsBashOperationsLsOperationsGrepOperationsFindOperations

user_bash の場合、拡張機能はローカル プロセスの生成、シェル解決、プロセス ツリーの終了を再実装する代わりに、createLocalBashOperations() 経由で pi のローカル シェル バックエンドを再利用できます。

bash ツールは、実行前にコマンド、cwd、または env を調整するための spawn フックもサポートしています。

import { createBashTool } from "@earendil-works/pi-coding-agent";
const bashTool = createBashTool(cwd, {
spawnHook: ({ command, cwd, env }) => ({
command: `source ~/.profile\n${command}`,
cwd: `/mnt/sandbox${cwd}`,
env: { ...env, CI: "1" },
}),
});

--ssh フラグを使用した完全な SSH の例については、examples/extensions/ssh.ts を参照してください。

ツールは、LLM コンテキストの負荷を避けるために、出力を切り詰めなければなりません。出力が大きいと、次のような問題が発生する可能性があります。

  • コンテキスト オーバーフロー エラー (プロンプトが長すぎます)
  • 圧縮の失敗
  • モデルのパフォーマンスの低下

組み込みの制限は 50KB (約 10,000 トークン) および 2000 行 のいずれか最初に到達した方です。エクスポートされた切り捨てユーティリティを使用します。

import {
truncateHead, // Keep first N lines/bytes (good for file reads, search results)
truncateTail, // Keep last N lines/bytes (good for logs, command output)
truncateLine, // Truncate a single line to maxBytes with ellipsis
formatSize, // Human-readable size (e.g., "50KB", "1.5MB")
DEFAULT_MAX_BYTES, // 50KB
DEFAULT_MAX_LINES, // 2000
} from "@earendil-works/pi-coding-agent";
async execute(toolCallId, params, signal, onUpdate, ctx) {
const output = await runCommand();
// Apply truncation
const truncation = truncateHead(output, {
maxLines: DEFAULT_MAX_LINES,
maxBytes: DEFAULT_MAX_BYTES,
});
let result = truncation.content;
if (truncation.truncated) {
// Write full output to temp file
const tempFile = writeTempFile(output);
// Inform the LLM where to find complete output
result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
result += ` Full output saved to: ${tempFile}]`;
}
return { content: [{ type: "text", text: result }] };
}

重要なポイント:

  • 始まりが重要なコンテンツ (検索結果、ファイルの読み取り) には truncateHead を使用します。
  • 終わりが重要なコンテンツ (ログ、コマンド出力) には truncateTail を使用します。
  • 出力が切り詰められた場合と、完全なバージョンの場所を常に LLM に通知します。
  • ツールの説明に切り捨て制限を文書化します。

rg (ripgrep) を適切な切り捨てでラップする完全な例については、examples/extensions/truncated-tool.ts を参照してください。

1 つの拡張機能で複数のツールを共有状態に登録できます。

export default function (pi: ExtensionAPI) {
let connection = null;
pi.registerTool({ name: "db_connect", ... });
pi.registerTool({ name: "db_query", ... });
pi.registerTool({ name: "db_close", ... });
pi.on("session_shutdown", async () => {
connection?.close();
});
}

ツールは、カスタム TUI 表示用の renderCall および renderResult を提供できます。完全なコンポーネント API については tui.md を、ツール行の構成方法については tool-execution.ts を参照してください。

デフォルトでは、ツールの出力はパディングと背景を処理する Box でラップされます。定義された renderCall または renderResultComponent を返す必要があります。スロット レンダラーが定義されていない場合、tool-execution.ts はそのスロットに対してフォールバック レンダリングを使用します。

ツールがデフォルトの Box を使用する代わりに独自のシェルをレンダリングする必要がある場合は、renderShell: "self" を設定します。これは、フレームや背景の動作を完全に制御する必要があるツール、たとえば、ツールが安定した後も視覚的に安定した状態を維持する必要がある大きなプレビューなどに便利です。

pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "Custom shell example",
parameters: Type.Object({}),
renderShell: "self",
async execute() {
return { content: [{ type: "text", text: "ok" }], details: undefined };
},
renderCall(args, theme, context) {
return new Text(theme.fg("accent", "my custom shell"), 0, 0);
},
});

renderCallrenderResult はそれぞれ、以下の context オブジェクトを受け取ります。

  • args - 現在のツール呼び出し引数
  • state - renderCallrenderResult 間で行ローカル状態を共有
  • lastComponent - そのスロットに対して以前に返されたコンポーネント (存在する場合)
  • invalidate() - このツール行の再レンダリングを要求します
  • toolCallIdcwdexecutionStartedargsCompleteisPartialexpandedshowImagesisError

クロススロット共有状態には context.state を使用します。レンダリング間で同じコンポーネントを再利用および変更する場合は、返されたコンポーネント インスタンスにスロット ローカル キャッシュを保持します。

ツール呼び出しまたはヘッダーをレンダリングします。

import { Text } from "@earendil-works/pi-tui";
renderCall(args, theme, context) {
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
let content = theme.fg("toolTitle", theme.bold("my_tool "));
content += theme.fg("muted", args.action);
if (args.text) {
content += " " + theme.fg("dim", `"${args.text}"`);
}
text.setText(content);
return text;
}

ツールの結果または出力をレンダリングします。

renderResult(result, { expanded, isPartial }, theme, context) {
if (isPartial) {
return new Text(theme.fg("warning", "Processing..."), 0, 0);
}
if (result.details?.error) {
return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0);
}
let text = theme.fg("success", "✓ Done");
if (expanded && result.details?.items) {
for (const item of result.details.items) {
text += "\n " + theme.fg("dim", item);
}
}
return new Text(text, 0, 0);
}

スロットに意図的に表示可能なコンテンツがない場合は、空の Container などの空の Component を返します。

keyHint() を使用して、アクティブなキーバインド設定を考慮したキーバインドのヒントを表示します。

import { keyHint } from "@earendil-works/pi-coding-agent";
renderResult(result, { expanded }, theme, context) {
let text = theme.fg("success", "✓ Done");
if (!expanded) {
text += ` (${keyHint("app.tools.expand", "to expand")})`;
}
return new Text(text, 0, 0);
}

利用可能な機能:

  • keyHint(keybinding, description) - "app.tools.expand""tui.select.confirm" などの構成されたキーバインド ID をフォーマットします
  • keyText(keybinding) - キーバインド ID に設定された生のキー テキストを返します。
  • rawKeyHint(key, description) - 生のキー文字列をフォーマットする

名前空間付きキーバインド ID を使用します。

  • コーディング エージェント ID は app.* 名前空間を使用します (例: app.tools.expandapp.editor.externalapp.session.rename)
  • 共有 TUI ID は tui.* 名前空間を使用します (例: tui.select.confirmtui.select.canceltui.input.tab)

キーバインド ID とデフォルトの完全なリストについては、keybindings.md を参照してください。 keybindings.json は、それらと同じ名前空間 ID を使用します。

カスタム エディターと ctx.ui.custom() コンポーネントは、挿入された引数として keybindings: KeybindingsManager を受け取ります。 getKeybindings() または setKeybindings() を呼び出す代わりに、挿入されたマネージャーを直接使用する必要があります。

  • Text とパディング (0, 0) を使用します。デフォルトのボックスはパディングを処理します。
  • 複数行のコンテンツには \n を使用します。
  • ストリーミングの進行状況について isPartial を処理します。
  • オンデマンドで詳細を確認するには expanded をサポートします。
  • デフォルトのビューをコンパクトに保ちます。
  • 引数を context.state にコピーする代わりに、renderResultcontext.args を読み取ります。
  • context.state は、呼び出しスロットと結果スロット間で共有する必要があるデータにのみ使用します。
  • 同じコンポーネント インスタンスを適切な場所で更新できる場合は、context.lastComponent を再利用します。
  • renderShell: "self" は、デフォルトのボックス化されたシェルが邪魔になる場合にのみ使用してください。セルフシェル モードでは、ツールは独自のフレーム、パディング、および背景を担当します。

スロット レンダラーが定義されていない場合、またはスローされる場合:

  • renderCall: ツール名を表示します。
  • renderResult: content からの生のテキストを表示します

拡張機能は、ctx.ui メソッドを介してユーザーと対話し、メッセージ/ツールのレンダリング方法をカスタマイズできます。

カスタム コンポーネントについては、tui.md を参照してください。これには次のコピー&ペースト パターンがあります。

  • 選択ダイアログ (SelectList)
  • キャンセル付きの非同期操作 (BorderLoader)
  • 設定切り替え (SettingsList)
  • ステータスインジケーター (setStatus)
  • ストリーミング中の動作メッセージ、可視性、インジケーター (setWorkingMessagesetWorkingVisiblesetWorkingIndicator)
  • エディターの上/下のウィジェット (setWidget)
  • 組み込みのスラッシュ/パス補完の上に重ねられたオートコンプリート プロバイダー (addAutocompleteProvider)
  • カスタムフッター (setFooter)
// Select from options
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
// Confirm dialog
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
// Text input
const name = await ctx.ui.input("Name:", "placeholder");
// Multi-line editor
const text = await ctx.ui.editor("Edit:", "prefilled text");
// Notification (non-blocking)
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"

カウントダウン付きの時間指定ダイアログ

Section titled “カウントダウン付きの時間指定ダイアログ”

ダイアログは、ライブ カウントダウン表示で自動終了する timeout オプションをサポートしています。

// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
const confirmed = await ctx.ui.confirm(
"Timed Confirmation",
"This dialog will auto-cancel in 5 seconds. Confirm?",
{ timeout: 5000 }
);
if (confirmed) {
// User confirmed
} else {
// User cancelled or timed out
}

タイムアウト時の戻り値:

  • select()undefined を返します
  • confirm()false を返します
  • input()undefined を返します

さらに制御するには (タイムアウトとユーザーのキャンセルを区別するなど)、AbortSignal を使用します。

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const confirmed = await ctx.ui.confirm(
"Timed Confirmation",
"This dialog will auto-cancel in 5 seconds. Confirm?",
{ signal: controller.signal }
);
clearTimeout(timeoutId);
if (confirmed) {
// User confirmed
} else if (controller.signal.aborted) {
// Dialog timed out
} else {
// User cancelled (pressed Escape or selected "No")
}

完全な例については、examples/extensions/timed-confirm.ts を参照してください。

ウィジェット、ステータス、フッター

Section titled “ウィジェット、ステータス、フッター”
// Status in footer (persistent until cleared)
ctx.ui.setStatus("my-ext", "Processing...");
ctx.ui.setStatus("my-ext", undefined); // Clear
// Working loader (shown during streaming)
ctx.ui.setWorkingMessage("Thinking deeply...");
ctx.ui.setWorkingMessage(); // Restore default
ctx.ui.setWorkingVisible(false); // Hide the built-in working loader row entirely
ctx.ui.setWorkingVisible(true); // Show the built-in working loader row
// Working indicator (shown during streaming)
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "")] }); // Static dot
ctx.ui.setWorkingIndicator({
frames: [
ctx.ui.theme.fg("dim", "·"),
ctx.ui.theme.fg("muted", ""),
ctx.ui.theme.fg("accent", ""),
ctx.ui.theme.fg("muted", ""),
],
intervalMs: 120,
});
ctx.ui.setWorkingIndicator({ frames: [] }); // Hide indicator
ctx.ui.setWorkingIndicator(); // Restore default spinner
// Widget above editor (default)
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
// Widget below editor
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));
ctx.ui.setWidget("my-widget", undefined); // Clear
// Custom footer (replaces built-in footer entirely)
ctx.ui.setFooter((tui, theme) => ({
render(width) { return [theme.fg("dim", "Custom footer")]; },
invalidate() {},
}));
ctx.ui.setFooter(undefined); // Restore built-in footer
// Terminal title
ctx.ui.setTitle("pi - my-project");
// Editor text
ctx.ui.setEditorText("Prefill text");
const current = ctx.ui.getEditorText();
// Paste into editor (triggers paste handling, including collapse for large content)
ctx.ui.pasteToEditor("pasted content");
// Stack custom autocomplete behavior on top of the built-in provider
ctx.ui.addAutocompleteProvider((current) => ({
triggerCharacters: ["#"],
async getSuggestions(lines, line, col, options) {
const beforeCursor = (lines[line] ?? "").slice(0, col);
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
if (!match) {
return current.getSuggestions(lines, line, col, options);
}
return {
prefix: `#${match[1] ?? ""}`,
items: [{ value: "#2983", label: "#2983", description: "Extension API for autocomplete" }],
};
},
applyCompletion(lines, line, col, item, prefix) {
return current.applyCompletion(lines, line, col, item, prefix);
},
shouldTriggerFileCompletion(lines, line, col) {
return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;
},
}));
// Tool output expansion
const wasExpanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(true);
ctx.ui.setToolsExpanded(wasExpanded);
// Custom editor (vim mode, emacs mode, etc.)
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
const currentEditor = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))
);
ctx.ui.setEditorComponent(undefined); // Restore default editor
// Theme management (see themes.md for creating themes)
const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...]
const lightTheme = ctx.ui.getTheme("light"); // Load without switching
const result = ctx.ui.setTheme("light"); // Switch by name
if (!result.success) {
ctx.ui.notify(`Failed: ${result.error}`, "error");
}
ctx.ui.setTheme(lightTheme!); // Or switch by Theme object
ctx.ui.theme.fg("accent", "styled text"); // Access current theme

カスタムの作業インジケーター フレームはそのままレンダリングされます。色が必要な場合は、ctx.ui.theme.fg(...) などを使用して、自分でフレーム文字列に色を追加します。

オートコンプリートプロバイダー

Section titled “オートコンプリートプロバイダー”

ctx.ui.addAutocompleteProvider() を使用して、組み込みのスラッシュ コマンドとパス プロバイダーの上にカスタム オートコンプリート ロジックをスタックします。 $ などのカスタム ナチュラル トリガーには triggerCharacters を設定します。

典型的なパターン:

  • カーソルの前のテキストを検査します
  • 拡張機能固有の構文が一致する場合に独自の提案を返します
  • それ以外の場合は current.getSuggestions(...) に委任します
  • カスタムの挿入動作が必要な場合を除き、applyCompletion(...) をデリゲートします。
pi.on("session_start", (_event, ctx) => {
ctx.ui.addAutocompleteProvider((current) => ({
triggerCharacters: ["#"],
async getSuggestions(lines, cursorLine, cursorCol, options) {
const line = lines[cursorLine] ?? "";
const beforeCursor = line.slice(0, cursorCol);
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
if (!match) {
return current.getSuggestions(lines, cursorLine, cursorCol, options);
}
return {
prefix: `#${match[1] ?? ""}`,
items: [
{ value: "#2983", label: "#2983", description: "Extension API for registering custom @ autocomplete providers" },
{ value: "#2753", label: "#2753", description: "Reload stale resource settings" },
],
};
},
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
},
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
},
}));
});

gh issue list を使用して最新のオープンな GitHub 問題をプリロードし、高速に #... を完了できるようにローカルでフィルターする完全な例については、github-issue-autocomplete.ts を参照してください。 GitHub CLI (gh) と GitHub リポジトリのチェックアウトが必要です。

複雑な UI の場合は、ctx.ui.custom() を使用します。これにより、done() が呼び出されるまで、エディターがコンポーネントに一時的に置き換えられます。

import { Text, Component } from "@earendil-works/pi-tui";
const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);
text.onKey = (key) => {
if (key === "return") done(true);
if (key === "escape") done(false);
return true;
};
return text;
});
if (result) {
// User pressed Enter
}

コールバックは以下を受け取ります:

  • tui - TUI インスタンス (画面サイズ、フォーカス管理用)
  • theme - スタイリングの現在のテーマ
  • keybindings - アプリのキーバインドマネージャー (ショートカットの確認用)
  • done(value) - コンポーネントを閉じて値を返す呼び出し

完全なコンポーネント API については、tui.md を参照してください。

{ overlay: true } を渡すと、画面をクリアせずにコンポーネントを既存のコンテンツの上にフローティング モーダルとしてレンダリングします。

const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{ overlay: true }
);

高度な配置 (アンカー、マージン、パーセンテージ、応答性の可視性) の場合は、overlayOptions を渡します。 onHandle を使用して、プログラムでフォーカスまたは可視性を制御します。

const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{
overlay: true,
overlayOptions: { anchor: "top-right", width: "50%", margin: 2 },
onHandle: (handle) => {
handle.focus(); // focus this overlay and bring it to the visual front
// handle.unfocus({ target: editorComponent }); // release input to a specific component
// handle.setHidden(true/false); // toggle visibility
// handle.hide(); // permanently remove
}
}
);

フォーカスされた表示オーバーレイは、一時的な非オーバーレイ カスタム UI が閉じた後に入力を再利用できます。オーバーレイが表示されている間、別のコンポーネントが入力を維持するように意図的にしたい場合は、handle.unfocus({ target }) を呼び出します。 { target: null } を渡すと、別のコンポーネントにフォーカスすることなくオーバーレイが解放されます。

完全な OverlayOptions および OverlayHandle API については tui.md を、例については overlay-qa-tests.ts を参照してください。

メインの入力エディタをカスタム実装 (vim モード、emacs モードなど) に置き換えます。

import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { matchesKey } from "@earendil-works/pi-tui";
class VimEditor extends CustomEditor {
private mode: "normal" | "insert" = "insert";
handleInput(data: string): void {
if (matchesKey(data, "escape") && this.mode === "insert") {
this.mode = "normal";
return;
}
if (this.mode === "normal" && data === "i") {
this.mode = "insert";
return;
}
super.handleInput(data); // App keybindings + text editing
}
}
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
ctx.ui.setEditorComponent((_tui, theme, keybindings) =>
new VimEditor(theme, keybindings)
);
});
}

重要なポイント:

  • CustomEditor (ベース Editor ではない) を拡張して、アプリのキーバインドを取得します (中止するエスケープ、ctrl+d、モデルの切り替え)
  • 扱っていないキーについては super.handleInput(data) に電話してください
  • 工場はアプリから themekeybindings を受け取ります
  • 以前に構成したカスタム エディターをラップするには、setEditorComponent() の前に ctx.ui.getEditorComponent() を使用します。
  • undefined を渡してデフォルトに戻します: ctx.ui.setEditorComponent(undefined)

すでにエディターを置き換えている別の拡張機能を使用して作成するには、自分のファクトリーを設定する前に、以前のファクトリーをキャプチャーします。

const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);

モードインジケーターを使用した完全な例については、tui.md パターン 7 を参照してください。

customType を使用してメッセージのカスタム レンダラーを登録します。

import { Text } from "@earendil-works/pi-tui";
pi.registerMessageRenderer("my-extension", (message, options, theme) => {
const { expanded } = options;
let text = theme.fg("accent", `[${message.customType}] `);
text += message.content;
if (expanded && message.details) {
text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
}
return new Text(text, 0, 0);
});

メッセージは pi.sendMessage() 経由で送信されます。

pi.sendMessage({
customType: "my-extension", // Matches registerMessageRenderer
content: "Status update",
display: true, // Show in TUI
details: { ... }, // Available in renderer
});

すべてのレンダリング関数は theme オブジェクトを受け取ります。カスタム テーマとフルカラー パレットの作成については、themes.md を参照してください。

// Foreground colors
theme.fg("toolTitle", text) // Tool names
theme.fg("accent", text) // Highlights
theme.fg("success", text) // Success (green)
theme.fg("error", text) // Errors (red)
theme.fg("warning", text) // Warnings (yellow)
theme.fg("muted", text) // Secondary text
theme.fg("dim", text) // Tertiary text
// Text styles
theme.bold(text)
theme.italic(text)
theme.strikethrough(text)

カスタム ツール レンダラでの構文ハイライトの場合:

import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";
// Highlight code with explicit language
const highlighted = highlightCode("const x = 1;", "typescript", theme);
// Auto-detect language from file path
const lang = getLanguageFromPath("/path/to/file.rs"); // "rust"
const highlighted = highlightCode(code, lang, theme);
  • 内線エラーがログに記録され、エージェントは続行します
  • tool_call エラーがツールをブロックする (フェールセーフ)
  • ツール execute エラーは、スローによって通知する必要があります。スローされたエラーが捕捉され、isError: true を使用して LLM に報告され、実行が続行されます。
モードctx.modectx.hasUIメモ
インタラクティブ"tui"trueターミナルレンダリングを備えた完全な TUI
RPC (--mode rpc)"rpc"trueJSON プロトコルを介したダイアログと通知。custom()undefined を返す。rpc.md を参照
JSON (--mode json)"json"falseイベントストリームを標準出力に送信。UI メソッドはノーオペレーション
Print (-p)"print"false拡張機能は実行されるがプロンプト不可

TUI 固有の機能 (custom()、コンポーネント ファクトリ、端末入力) の前に ctx.mode === "tui" を使用します。 TUI モードと RPC モードの両方で機能するダイアログ メソッドと通知メソッドの前に ctx.hasUI を使用します。

すべての例は examples/extensions/ にあります。

説明主要な API
ツール
hello.ts最小限のツール登録registerTool
question.tsユーザーインタラクションを備えたツールregisterToolui.select
questionnaire.tsマルチステップウィザードツールregisterToolui.custom
todo.ts永続性を備えたステートフルツールregisterToolappendEntryrenderResult、セッションイベント
dynamic-tools.ts起動後およびコマンド中にツールを登録registerToolsession_startregisterCommand
structured-output.tsterminate: true を使用した最終的な構造化出力ツールregisterTool、終了するツール結果
truncated-tool.ts出力の切り捨ての例registerTooltruncateHead
tool-override.ts組み込み read ツールをオーバーライドregisterTool(組み込みと同じ名前)
コマンド
pirate.tsターンごとにシステムプロンプトを変更registerCommandbefore_agent_start
summarize.ts会話要約コマンドregisterCommandui.custom
handoff.tsプロバイダー間モデルのハンドオフregisterCommandui.editorui.custom
qna.tsカスタム UI に関する Q&AregisterCommandui.customsetEditorText
send-user-message.tsユーザーメッセージを挿入registerCommandsendUserMessage
reload-runtime.tsリロードコマンドと LLM ツールのハンドオフregisterCommandctx.reload()sendUserMessage
shutdown-command.tsグレースフルシャットダウンコマンドregisterCommandshutdown()
イベントとゲート
permission-gate.ts危険なコマンドをブロックon("tool_call")ui.confirm
project-trust.tsユーザー/グローバルまたは CLI 拡張からのプロジェクト信頼を決定または延期on("project_trust")、信頼 UI、必要な信頼結果
protected-paths.ts特定のパスへの書き込みをブロックon("tool_call")
confirm-destructive.tsセッションの変更を確認on("session_before_switch")on("session_before_fork")
dirty-repo-guard.tsダーティな git リポジトリについて警告on("session_before_*")exec
input-transform.tsユーザー入力を変換on("input")
input-transform-streaming.tsストリーミング対応の入力変換on("input")streamingBehavior
model-status.tsモデル変更への対応on("model_select")setStatus
provider-payload.tsペイロードとプロバイダー応答ヘッダーを検査on("before_provider_request")on("after_provider_response")
system-prompt-header.tsシステムプロンプト情報を表示on("agent_start")getSystemPrompt
claude-rules.tsファイルからルールをロードon("session_start")on("before_agent_start")
prompt-customizer.tssystemPromptOptions を使用してコンテキスト認識ツールガイダンスを追加on("before_agent_start")BuildSystemPromptOptions
file-trigger.tsファイルウォッチャーがメッセージをトリガーsendMessage
圧縮とセッション
custom-compaction.tsカスタム圧縮の要約on("session_before_compact")
trigger-compact.ts圧縮を手動でトリガーcompact()
git-checkpoint.tsターン中の Git スタッシュon("turn_start")on("session_before_fork")exec
git-merge-and-resolve.ts競合をフェッチ、マージ、解決on("agent_end")execsendUserMessage
auto-commit-on-exit.tsシャットダウン時にコミットon("session_shutdown")exec
UI コンポーネント
status-line.tsフッターステータスインジケーターsetStatus、セッションイベント
working-indicator.tsストリーミング動作インジケーターをカスタマイズsetWorkingIndicatorregisterCommand
github-issue-autocomplete.tsgh issue list から最近の未解決 issue をプリロードして組み込みオートコンプリートの上に #1234 issue 補完を追加addAutocompleteProvideron("session_start")exec
custom-footer.tsフッターを完全に置き換えregisterCommandsetFooter
custom-header.ts起動ヘッダーを置き換えon("session_start")setHeader
modal-editor.tsVim スタイルのモーダルエディターsetEditorComponentCustomEditor
rainbow-editor.tsカスタムエディタースタイルsetEditorComponent
widget-placement.tsエディターの上/下のウィジェットsetWidget
overlay-test.tsオーバーレイコンポーネントui.custom(オーバーレイオプションあり)
overlay-qa-tests.ts包括的なオーバーレイテストui.custom、すべてのオーバーレイオプション
notify.tsシンプルな通知ui.notify
timed-confirm.tsタイムアウトのあるダイアログui.confirm(タイムアウト/シグナル付き)
mac-system-theme.tsテーマの自動切り替えsetThemeexec
複雑な拡張機能
plan-mode/フルプランモードの実装すべてのイベントタイプ、registerCommandregisterShortcutregisterFlagsetStatussetWidgetsendMessagesetActiveTools
preset.ts保存可能なプリセット(モデル、ツール、思考)registerCommandregisterShortcutregisterFlagsetModelsetActiveToolssetThinkingLevelappendEntry
tools.tsツールのオン/オフを切り替える UIregisterCommandsetActiveToolsSettingsList、セッションイベント
リモートとサンドボックス
ssh.tsSSH リモート実行registerFlagon("user_bash")on("before_agent_start")、ツール操作
interactive-shell.ts永続的なシェルセッションon("user_bash")
sandbox/サンドボックスツールの実行ツール操作
gondolin/組み込みツールと ! コマンドを Gondolin マイクロ VM にルーティングツール操作、組み込みツールオーバーライド、on("user_bash")
subagent/サブエージェントを起動registerToolexec
ゲーム
snake.tsスネークゲームregisterCommandui.custom、キーボード処理
space-invaders.tsスペースインベーダーゲームregisterCommandui.custom
doom-overlay/オーバーレイでの Doomui.custom(オーバーレイ付き)
プロバイダー
custom-provider-anthropic/カスタム Anthropic プロキシregisterProvider
custom-provider-gitlab-duo/GitLab Duo 統合OAuth を使用した registerProvider
メッセージと通信
message-renderer.tsカスタムメッセージのレンダリングregisterMessageRenderersendMessage
event-bus.ts拡張間イベントpi.events
セッションメタデータ
session-name.tsセレクター用にセッションに名前を付けるsetSessionNamegetSessionName
bookmark.ts/tree のブックマークエントリsetLabel
その他
inline-bash.tsツール呼び出しのインライン bashon("tool_call")
bash-spawn-hook.ts実行前に bash コマンド、cwd、env を調整createBashToolspawnHook
with-deps/npm 依存関係を伴う拡張package.json を使ったパッケージ構造