---
name: musepi-extension-dev
description: 为 MusePi（TUI/CLI + 桌面 GUI）开发扩展——custom tools、extensions（生命周期/事件）、hooks、plugins、slash commands、skills。触发：用户要求"加个工具/扩展/命令/hook/插件"、"给 musepi 加 X 支持"、修改扩展加载机制、写 MCP 相关集成。桌面 GUI 会话（daemon）与 TUI 共享同一扩展系统，扩展天然两形态可用。
---

# MusePi 扩展开发

扩展系统在 `packages/coding-agent/src/extensibility/`。六种形态，各有用途：

| 形态 | 是什么 | 关键文件 |
|---|---|---|
| **Custom tool** | 模型可调用的函数（execute + Zod schema） | `extensibility/custom-tools/loader.ts` |
| **Extension** | 生命周期/事件框架——可注册工具、拦截/修改事件 | `extensibility/extensions/`（loader/runner） |
| **Hook** | 外部 pre/post 命令脚本（如 tool_call 拦截） | `extensibility/hooks` |
| **Plugin** | 可安装的扩展包（`musepi.extensions` 清单，`omp`/`pi` 遗留兼容） | `extensibility/plugins` |
| **Slash command** | 斜杠命令（`/xxx`） | `extensibility/slash-commands.ts` |
| **Skill** | 静态知识包（SKILL.md） | `extensibility/skills.ts` + `discovery/builtin.ts` |

## 快速路径：最小扩展（够用 90% 场景）

写一个 TS 文件（默认导出 factory），放到 `~/.musepi/agent/extensions/<name>/index.ts`（用户级）或 `<cwd>/.musepi/extensions/<name>/index.ts`（项目级）：

```ts
import type { ExtensionAPI } from "@musepi/pi-coding-agent";

export default function (pi: ExtensionAPI) {
	const z = pi.zod;
	pi.setLabel("My Extension");

	pi.on("tool_call", async (event) => {
		if (event.toolName === "bash" && /rm -rf/.test(event.input.command ?? "")) {
			return { block: true, reason: "Blocked by extension policy" };
		}
	});

	pi.registerTool({
		name: "my_tool",
		label: "My Tool",
		description: "…",
		parameters: z.object({ query: z.string() }),
		async execute(_id, params, _signal, _onUpdate, _ctx) {
			return { content: [{ type: "text", text: `got ${params.query}` }] };
		},
	});

	pi.registerCommand("my-cmd", {
		description: "…",
		handler: async (_args, ctx) => ctx.ui.notify("hi", "info"),
	});
}
```

扩展在 **Bun 运行时**执行：ESM 模块（无 `require`）、顶层 `export default` factory、可用 `await import()` 按需加载依赖。扩展代码与其他扩展共享进程——**不要**在扩展里做长阻塞/全局状态污染。

## 加载与发现（怎么让 musepi 看到你的扩展）

1. **自动发现**：`~/.musepi/agent/extensions`（用户）+ `<cwd>/.musepi/extensions`（项目）——目录含 `index.ts` 即可
2. **显式配置**：`~/.musepi/agent/config.yml` 的 `extensions:` 数组（路径/包目录）或 `-e/--extension` CLI flag
3. **禁用**：`disabledExtensions: [extension-module:<name>]`（name = 目录/文件名）；`--no-extensions` 全关
4. **验证**：`musepi /extensions` 列出已加载扩展；扩展里的 `console.error` 进 daemon/TUI 日志
5. **改动生效（HMR 热拔插）**：改扩展**入口文件**后 daemon watcher ~500ms 内广播 `extensions.changed`，GUI ~1s 刷新，活跃会话执行 `reloadExtension`（忙会话挂起、agent_end 补做）——**不用重启 daemon**；会话内工具下次调用生效，未重注册的旧工具名被移除。注意：**子模块改动不热生效**（Bun 模块缓存只重键入口 specifier）——多文件扩展改了子模块要 touch 入口文件。扩展内存态不迁移。只有改了扩展的发现/加载机制本身、或 daemon 侧注册代码时才需要重启 daemon（GUI 菜单"重启 daemon"）；TUI 新会话即生效

## 插件配置（manifest config + pi.config，dsh 管理页 parity）

扩展需要用户可配项时，在扩展目录的 package.json 的 `musepi` 块（权威清单字段；`omp`/`pi` 遗留兼容可读）声明 `config` 字段表（`boolean | number | string | select | path`，数字可带 `min/max/step`，下拉必须给 `options`，每个字段可标 `restart: none|session|daemon` 生效域）与 `resources` 资源卡（`disk`/`memory`/`setupMinutes`/`models`）。扩展中心详情页自动渲染管理表单并落盘；运行时读取：

```ts
const threshold = await pi.config.get<number>("threshold"); // 钳制后的值，未写过 = 声明默认
const all = await pi.config.getAll(); // 完整值表（只含声明键）
```

取值与表单、写入共用同一条钳制链路：存储坏值回退声明默认，未声明键读不到。存储落点 `<agentDir>/extensions/plugin-config.json`，键 = `extension-module:<name>`。fail-soft：坏清单字段被逐个丢弃并给警告，不拖垮扩展登记。详见 `docs/extensions-dev.md` §13。

## 自举工具环（agent 在会话内自己管扩展）

daemon 会话给 agent 挂了一组扩展自举工具，让创造类任务不靠 GUI 就能完成"写文件 → 加载 → 自查 → 热修 → 回退"闭环：

- `extensions_list`——已加载扩展清单（**唯一常激活**，只读、便宜）
- `extension_load` / `extension_reload` / `extension_status` / `extension_validate` / `extension_rollback`——生命周期五件套；`extension_rollback` 回退到加载时的版本化快照（快照在 `~/.musepi/extension-backups`）
- `ext_define` / `ext_run` / `ext_stop` / `ext_undefine` / `ext_inspect`——会话级动态插件（vm 沙箱，`extdyn__` 命名空间），不写盘就能试原型

激活方式：普通会话里 `/extensions` 斜杠命令；**creator（创造）预设自动激活整环**（settings `tools.extensionMetaTools`，会话起即顶层可用，不走 xd:// 设备）。默认不激活的原因：普通会话不为这 10 个工具的 schema 付费。

## Cordis 插件运行时（组件 / 子 fiber / 兼容性预检）

- **组件**：插件 package.json `musepi.components` 声明组件数组（`[{ id, label?, deny? }]`）；宿主为组件建独立子 fiber，设置页可对单组件启停（deny 列表落盘）。整插件 reload 时组件一并重建。
- **宿主 fiber 检视**：cordis 组 fiber 下各插件/组件的加载状态可从扩展检视入口查询（`extension_status` / 插件管理页）。
- **兼容性预检**：加载插件前宿主先做 Bun/平台兼容性检查；声明不兼容时插件详情弹窗给归因段，用户可对整个插件**授予精确版本豁免**（写 `<agentDir>/compatibility.json`，RPC `extensions.setVersionExemption`）——比 `bunfig` 豁免窄，只放过单个版本。

## 文档路由表（需要细节时读这些，别猜）

**这张表只在 MusePi 仓库 clone 内有效**：它指向的 `docs/` 目录不随发行包分发。在仓库外开发扩展时改用运行时可观测的事实——`settings.schema` 的字段、`extension_status` 检视面、`~/.musepi/agent/` 下的实际文件、`--help`——不要按这些路径去找文件，也不要声称读过读不到的文件。

| 任务 | 文档 |
|---|---|
| 扩展 API 全貌/事件名/命令上下文 | `docs/extensions.md` |
| 插件配置（manifest config / pi.config / 资源卡） | `docs/extensions-dev.md` §13 |
| 加载机制细节（发现顺序/路径解析/禁用） | `docs/extension-loading.md` |
| 自定义工具（模型直接调用） | `docs/custom-tools.md` |
| Hook（pre/post 脚本、可突变什么） | `docs/hooks.md` |
| 加 LLM provider（如新模型厂） | `docs/adding-a-provider.md` + 参照 `packages/ai/src/registry/` 现有 provider 的写法（一个 def 文件 + 一个 registry 行 + catalog 条目） |
| MCP server/工具 | `docs/mcp-config.md` / `docs/mcp-server-tool-authoring.md` |
| 插件市场/安装器 | `docs/marketplace.md` / `docs/plugin-manager-installer-plumbing.md` |
| 设置项（settings.schema） | `docs/settings.md` + `src/config/settings-schema.ts`（单文件权威） |
| 斜杠命令实现 | `docs/slash-command-internals.md` |
| 系统提示词/技能注入 | `docs/context-files.md` / `docs/skills.md` |
| GUI 侧（daemon RPC/Electron） | `docs/gui-implementation.md`（daemon 契约/IPC 形状） |

## 硬约束

- **类型**：扩展 API 类型从 `@musepi/pi-coding-agent` 导入（`ExtensionAPI`/`CustomToolAPI`）；不要用 `any` 绕过
- **ESM**：无 `require`；`verbatimModuleSyntax` 下类型导入用 `import type`
- **Zod**：工具参数 schema 用 `pi.zod`（扩展工厂参数）——不用 typebox（扩展侧）
- **错误**：工具执行抛错会被包装成工具错误——用有意义的 message
- **权限**：扩展与 agent 同权限（非沙箱）——破坏性操作需用户确认（工具审批链）
- **测试**：扩展逻辑抽纯函数可单测（`bun test`）；端到端用真实会话跑一次工具调用验证
