本次发布将 hooks(钩子)和 custom tools(自定义工具)统一为单一的“extensions(扩展)”系统,并将“slash commands(斜杠命令)”重命名为“prompt templates(提示词模板)”。([#454](https://github.com/badlogic/pi-mono/issues/454))
**迁移前必读:**
- [docs/extensions.md](docs/extensions.md) - 完整 API 参考文档
- [README.md](README.md) - 包含示例的扩展部分
- [examples/extensions/](examples/extensions/) - 可运行的示例
### 扩展迁移
钩子(Hooks)和自定义工具(Custom Tools)现已统一为**扩展(Extensions)**。两者原本都是导出一个接收 API 对象的工厂函数的 TypeScript 模块。现在统一为一个概念、一个发现位置、一个 CLI 标志和一个 settings.json 配置项。
**自动迁移:**
- 启动时 `commands/` 目录会自动重命名为 `prompts/`(包括 `~/.pi/agent/commands/` 和 `.pi/commands/`)
**需手动迁移:**
1. 将文件从 `hooks/` 和 `tools/` 目录移至 `extensions/`(启动时会显示弃用警告)
2. 更新扩展代码中的导入语句和类型名称
3. 如果配置了明确的钩子和自定义工具路径,请更新 `settings.json`
**目录变更:**
```
# 之前
~/.pi/agent/hooks/*.ts → ~/.pi/agent/extensions/*.ts
~/.pi/agent/tools/*.ts → ~/.pi/agent/extensions/*.ts
.pi/hooks/*.ts → .pi/extensions/*.ts
.pi/tools/*.ts → .pi/extensions/*.ts
```
**扩展发现规则**(位于 `extensions/` 目录中):
1. **直接文件:** `extensions/*.ts` 或 `*.js` → 直接加载
2. **带 index 的子目录:** `extensions/myext/index.ts` → 作为单个扩展加载
3. **带 package.json 的子目录:** `extensions/myext/package.json` 中包含 `"pi"` 字段 → 加载声明的路径
```json
// extensions/my-package/package.json
{
"name": "my-extension-package",
"dependencies": { "zod": "^3.0.0" },
"pi": {
"extensions": ["./src/main.ts", "./src/tools.ts"]
}
}
```
递归深度不超过一层。复杂的包必须使用 `package.json` 清单。依赖项通过 jiti 解析,扩展可以发布到 npm 并从 npm 安装。
**类型重命名:**
- `HookAPI` → `ExtensionAPI`
- `HookContext` → `ExtensionContext`
- `HookCommandContext` → `ExtensionCommandContext`
- `HookUIContext` → `ExtensionUIContext`
- `CustomToolAPI` → `ExtensionAPI` (已合并)
- `CustomToolContext` → `ExtensionContext` (已合并)
- `CustomToolUIContext` → `ExtensionUIContext`
- `CustomTool` → `ToolDefinition`
- `CustomToolFactory` → `ExtensionFactory`
- `HookMessage` → `CustomMessage`
**导入变更:**
```typescript
// 之前(钩子)
import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent";
export default function (pi: HookAPI) { ... }
// 之前(自定义工具)
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
const factory: CustomToolFactory = (pi) => ({ name: "my_tool", ... });
export default factory;
// 之后(现在均为扩展)
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => { ... });
pi.registerTool({ name: "my_tool", ... });
}
```
**自定义工具现在拥有完整的上下文访问权限。** 通过 `pi.registerTool()` 注册的工具现在会接收到与事件处理器相同的 `ctx` 对象。以前,自定义工具的上下文有限。现在所有扩展代码共享相同的能力:
- `pi.registerTool()` - 注册 LLM 可调用的工具
- `pi.registerCommand()` - 注册类似 `/mycommand` 的命令
- `pi.registerShortcut()` - 注册键盘快捷键(在 `/hotkeys` 中显示)
- `pi.registerFlag()` - 注册 CLI 参数(在 `--help` 中显示)
- `pi.registerMessageRenderer()` - 为消息类型自定义 TUI 渲染
- `pi.on()` - 订阅生命周期事件(如 tool_call、session_start 等)
- `pi.sendMessage()` - 向对话中注入消息
- `pi.appendEntry()` - 在会话中持久化自定义数据(重启/分支后保留)
- `pi.exec()` - 运行 Shell 命令
- `pi.getActiveTools()` / `pi.setActiveTools()` - 动态启用/禁用工具
- `pi.getAllTools()` - 列出所有可用工具
- `pi.events` - 用于跨扩展通信的事件总线
- `ctx.ui.confirm()` / `select()` / `input()` - 用户提示框
- `ctx.ui.notify()` - 弹出通知(Toast)
- `ctx.ui.setStatus()` - 在页脚设置持久化状态(多个扩展可各自设置)
- `ctx.ui.setWidget()` - 在编辑器上方显示组件
- `ctx.ui.setTitle()` - 设置终端窗口标题
- `ctx.ui.custom()` - 带键盘处理的完整 TUI 组件
- `ctx.ui.editor()` - 支持外部编辑器的多行文本编辑器
- `ctx.sessionManager` - 读取会话条目,获取分支历史
**配置变更:**
```json
// 之前
{
"hooks": ["./my-hook.ts"],
"customTools": ["./my-tool.ts"]
}
// 之后
{
"extensions": ["./my-extension.ts"]
}
```
**CLI 变更:**
```bash
# 之前
pi --hook ./safety.ts --tool ./todo.ts
# 之后
pi --extension ./safety.ts -e ./todo.ts
```
### 提示词模板迁移
“斜杠命令”(通过 `/名称` 调用的定义可复用提示词的 Markdown 文件)现重命名为“提示词模板”,以避免与扩展注册的命令混淆。
**自动迁移:** 启动时 `commands/` 目录会自动重命名为 `prompts/`(如果 `prompts/` 不存在)。同时适用于普通目录和符号链接。
**目录变更:**
```
~/.pi/agent/commands/*.md → ~/.pi/agent/prompts/*.md
.pi/commands/*.md → .pi/prompts/*.md
```
**SDK 类型重命名:**
- `FileSlashCommand` → `PromptTemplate`
- `LoadSlashCommandsOptions` → `LoadPromptTemplatesOptions`
**SDK 函数重命名:**
- `discoverSlashCommands()` → `discoverPromptTemplates()`
- `loadSlashCommands()` → `loadPromptTemplates()`
- `expandSlashCommand()` → `expandPromptTemplate()`
- `getCommandsDir()` → `getPromptsDir()`
**SDK 选项重命名:**
- `CreateAgentSessionOptions.slashCommands` → `.promptTemplates`
- `AgentSession.fileCommands` → `.promptTemplates`
- `PromptOptions.expandSlashCommands` → `.expandPromptTemplates`
### SDK 迁移
**发现函数:**
- `discoverAndLoadHooks()` → `discoverAndLoadExtensions()`
- `discoverAndLoadCustomTools()` → 已合并至 `discoverAndLoadExtensions()`
- `loadHooks()` → `loadExtensions()`
- `loadCustomTools()` → 已合并至 `loadExtensions()`
**运行器与包装器:**
- `HookRunner` → `ExtensionRunner`
- `wrapToolsWithHooks()` → `wrapToolsWithExtensions()`
- `wrapToolWithHooks()` → `wrapToolWithExtensions()`
**CreateAgentSessionOptions:**
- `.hooks` → 已移除(路径请使用 `.additionalExtensionPaths`)
- `.additionalHookPaths` → `.additionalExtensionPaths`
- `.preloadedHooks` → `.preloadedExtensions`
- `.customTools` 类型变更:`Array<{ path?; tool: CustomTool }>` → `ToolDefinition[]`
- `.additionalCustomToolPaths` → 已合并至 `.additionalExtensionPaths`
- `.slashCommands` → `.promptTemplates`
**AgentSession:**
- `.hookRunner` → `.extensionRunner`
- `.fileCommands` → `.promptTemplates`
- `.sendHookMessage()` → `.sendCustomMessage()`
### 会话迁移
**自动。** 会话版本从 2 升级至 3。现有会话将在首次加载时自动迁移:
- 消息角色 `"hookMessage"` → `"custom"`
### 破坏性变更
- **设置:** `hooks` 和 `customTools` 数组已替换为单个 `extensions` 数组
- **CLI:** `--hook` 和 `--tool` 参数已替换为 `--extension` / `-e`
- **目录:** `hooks/`, `tools/` → `extensions/`;`commands/` → `prompts/`
- **类型:** 参见上方的类型重命名
- **SDK:** 参见上方的 SDK 迁移
### 其他变更
- 扩展可拥有独立的 `package.json` 及依赖(通过 jiti 解析)
- 文档:`docs/hooks.md` 和 `docs/custom-tools.md` 已合并至 `docs/extensions.md`
- 示例:`examples/hooks/` 和 `examples/custom-tools/` 已合并至 `examples/extensions/`
- README:扩展部分已扩充,新增自定义工具、命令、事件、状态持久化、快捷键、参数及 UI 示例
- SDK:`customTools` 选项现在直接接受 `ToolDefinition[]`(从 `Array<{ path?, tool }>` 简化而来)
- SDK:`extensions` 选项现在接受 `ExtensionFactory[]` 用于内联扩展
- SDK:`additionalExtensionPaths` 同时替代了 `additionalHookPaths` 和 `additionalCustomToolPaths`
This release unifies hooks and custom tools into a single "extensions" system and renames "slash commands" to "prompt templates". ([#454](https://github.com/badlogic/pi-mono/issues/454))
**Before migrating, read:**
- [docs/extensions.md](docs/extensions.md) - Full API reference
- [README.md](README.md) - Extensions section with examples
- [examples/extensions/](examples/extensions/) - Working examples
### Extensions Migration
Hooks and custom tools are now unified as **extensions**. Both were TypeScript modules exporting a factory function that receives an API object. Now there's one concept, one discovery location, one CLI flag, one settings.json entry.
**Automatic migration:**
- `commands/` directories are automatically renamed to `prompts/` on startup (both `~/.pi/agent/commands/` and `.pi/commands/`)
**Manual migration required:**
1. Move files from `hooks/` and `tools/` directories to `extensions/` (deprecation warnings shown on startup)
2. Update imports and type names in your extension code
3. Update `settings.json` if you have explicit hook and custom tool paths configured
**Directory changes:**
```
# Before
~/.pi/agent/hooks/*.ts → ~/.pi/agent/extensions/*.ts
~/.pi/agent/tools/*.ts → ~/.pi/agent/extensions/*.ts
.pi/hooks/*.ts → .pi/extensions/*.ts
.pi/tools/*.ts → .pi/extensions/*.ts
```
**Extension discovery rules** (in `extensions/` directories):
1. **Direct files:** `extensions/*.ts` or `*.js` → loaded directly
2. **Subdirectory with index:** `extensions/myext/index.ts` → loaded as single extension
3. **Subdirectory with package.json:** `extensions/myext/package.json` with `"pi"` field → loads declared paths
```json
// extensions/my-package/package.json
{
"name": "my-extension-package",
"dependencies": { "zod": "^3.0.0" },
"pi": {
"extensions": ["./src/main.ts", "./src/tools.ts"]
}
}
```
No recursion beyond one level. Complex packages must use the `package.json` manifest. Dependencies are resolved via jiti, and extensions can be published to and installed from npm.
**Type renames:**
- `HookAPI` → `ExtensionAPI`
- `HookContext` → `ExtensionContext`
- `HookCommandContext` → `ExtensionCommandContext`
- `HookUIContext` → `ExtensionUIContext`
- `CustomToolAPI` → `ExtensionAPI` (merged)
- `CustomToolContext` → `ExtensionContext` (merged)
- `CustomToolUIContext` → `ExtensionUIContext`
- `CustomTool` → `ToolDefinition`
- `CustomToolFactory` → `ExtensionFactory`
- `HookMessage` → `CustomMessage`
**Import changes:**
```typescript
// Before (hook)
import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent";
export default function (pi: HookAPI) { ... }
// Before (custom tool)
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
const factory: CustomToolFactory = (pi) => ({ name: "my_tool", ... });
export default factory;
// After (both are now extensions)
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => { ... });
pi.registerTool({ name: "my_tool", ... });
}
```
**Custom tools now have full context access.** Tools registered via `pi.registerTool()` now receive the same `ctx` object that event handlers receive. Previously, custom tools had limited context. Now all extension code shares the same capabilities:
- `pi.registerTool()` - Register tools the LLM can call
- `pi.registerCommand()` - Register commands like `/mycommand`
- `pi.registerShortcut()` - Register keyboard shortcuts (shown in `/hotkeys`)
- `pi.registerFlag()` - Register CLI flags (shown in `--help`)
- `pi.registerMessageRenderer()` - Custom TUI rendering for message types
- `pi.on()` - Subscribe to lifecycle events (tool_call, session_start, etc.)
- `pi.sendMessage()` - Inject messages into the conversation
- `pi.appendEntry()` - Persist custom data in session (survives restart/branch)
- `pi.exec()` - Run shell commands
- `pi.getActiveTools()` / `pi.setActiveTools()` - Dynamic tool enable/disable
- `pi.getAllTools()` - List all available tools
- `pi.events` - Event bus for cross-extension communication
- `ctx.ui.confirm()` / `select()` / `input()` - User prompts
- `ctx.ui.notify()` - Toast notifications
- `ctx.ui.setStatus()` - Persistent status in footer (multiple extensions can set their own)
- `ctx.ui.setWidget()` - Widget display above editor
- `ctx.ui.setTitle()` - Set terminal window title
- `ctx.ui.custom()` - Full TUI component with keyboard handling
- `ctx.ui.editor()` - Multi-line text editor with external editor support
- `ctx.sessionManager` - Read session entries, get branch history
**Settings changes:**
```json
// Before
{
"hooks": ["./my-hook.ts"],
"customTools": ["./my-tool.ts"]
}
// After
{
"extensions": ["./my-extension.ts"]
}
```
**CLI changes:**
```bash
# Before
pi --hook ./safety.ts --tool ./todo.ts
# After
pi --extension ./safety.ts -e ./todo.ts
```
### Prompt Templates Migration
"Slash commands" (markdown files defining reusable prompts invoked via `/name`) are renamed to "prompt templates" to avoid confusion with extension-registered commands.
**Automatic migration:** The `commands/` directory is automatically renamed to `prompts/` on startup (if `prompts/` doesn't exist). Works for both regular directories and symlinks.
**Directory changes:**
```
~/.pi/agent/commands/*.md → ~/.pi/agent/prompts/*.md
.pi/commands/*.md → .pi/prompts/*.md
```
**SDK type renames:**
- `FileSlashCommand` → `PromptTemplate`
- `LoadSlashCommandsOptions` → `LoadPromptTemplatesOptions`
**SDK function renames:**
- `discoverSlashCommands()` → `discoverPromptTemplates()`
- `loadSlashCommands()` → `loadPromptTemplates()`
- `expandSlashCommand()` → `expandPromptTemplate()`
- `getCommandsDir()` → `getPromptsDir()`
**SDK option renames:**
- `CreateAgentSessionOptions.slashCommands` → `.promptTemplates`
- `AgentSession.fileCommands` → `.promptTemplates`
- `PromptOptions.expandSlashCommands` → `.expandPromptTemplates`
### SDK Migration
**Discovery functions:**
- `discoverAndLoadHooks()` → `discoverAndLoadExtensions()`
- `discoverAndLoadCustomTools()` → merged into `discoverAndLoadExtensions()`
- `loadHooks()` → `loadExtensions()`
- `loadCustomTools()` → merged into `loadExtensions()`
**Runner and wrapper:**
- `HookRunner` → `ExtensionRunner`
- `wrapToolsWithHooks()` → `wrapToolsWithExtensions()`
- `wrapToolWithHooks()` → `wrapToolWithExtensions()`
**CreateAgentSessionOptions:**
- `.hooks` → removed (use `.additionalExtensionPaths` for paths)
- `.additionalHookPaths` → `.additionalExtensionPaths`
- `.preloadedHooks` → `.preloadedExtensions`
- `.customTools` type changed: `Array<{ path?; tool: CustomTool }>` → `ToolDefinition[]`
- `.additionalCustomToolPaths` → merged into `.additionalExtensionPaths`
- `.slashCommands` → `.promptTemplates`
**AgentSession:**
- `.hookRunner` → `.extensionRunner`
- `.fileCommands` → `.promptTemplates`
- `.sendHookMessage()` → `.sendCustomMessage()`
### Session Migration
**Automatic.** Session version bumped from 2 to 3. Existing sessions are migrated on first load:
- Message role `"hookMessage"` → `"custom"`
### Breaking Changes
- **Settings:** `hooks` and `customTools` arrays replaced with single `extensions` array
- **CLI:** `--hook` and `--tool` flags replaced with `--extension` / `-e`
- **Directories:** `hooks/`, `tools/` → `extensions/`; `commands/` → `prompts/`
- **Types:** See type renames above
- **SDK:** See SDK migration above
### Changed
- Extensions can have their own `package.json` with dependencies (resolved via jiti)
- Documentation: `docs/hooks.md` and `docs/custom-tools.md` merged into `docs/extensions.md`
- Examples: `examples/hooks/` and `examples/custom-tools/` merged into `examples/extensions/`
- README: Extensions section expanded with custom tools, commands, events, state persistence, shortcuts, flags, and UI examples
- SDK: `customTools` option now accepts `ToolDefinition[]` directly (simplified from `Array<{ path?, tool }>`)
- SDK: `extensions` option accepts `ExtensionFactory[]` for inline extensions
- SDK: `additionalExtensionPaths` replaces both `additionalHookPaths` and `additionalCustomToolPaths`