Cline Agent 插件开发实战:从单文件插件到完整插件包的完整指南
本文基于 Cline 官方插件撰写指南(仓库中的 sdk/.cline/skills/plugin.md)编写,系统讲解如何为一个构建在 Cline Core SDK 之上的 Agent 编写、加载和分发插件。读完本文,你将掌握插件的注册生命周期(resolve / validate / setup / activate)、manifest 能力声明机制、七种运行时钩子的用法与拦截语义、消息构建器与自动化事件,以及单文件插件和插件包两种形态的完整模板、安装方式与测试策略,并能直接对照仓库源码验证每一处 API 行为。
1. 什么是 Cline 插件,以及它的两种形态
Cline 插件是一个 TypeScript 模块,用于扩展任何基于 Cline Core SDK 构建的 Agent。同一个插件可以在 Cline CLI、VS Code / JetBrains 扩展、Kanban 宿主,以及任何基于 @cline/core 构建的自定义应用中运行——一次编写,所有宿主获得相同的新行为。
一个插件可以做到:
- 注册工具(Tools)——模型可以调用的新能力(最常见的用途);
- 接入 Agent 循环的钩子——在运行前后、模型调用前后、工具调用前后介入;
- 改写发往 Provider 的消息——在消息触达模型之前做自定义压缩、脱敏、上下文整形;
- 注册斜杠命令、提示词规则、模型 Provider、自动化事件类型。
插件以两种形态之一发布:
- 单文件插件——一个导出默认插件对象的
.ts文件,放进发现目录即被加载; - 插件包(Plugin package)——带
package.json、npm 依赖和(可选)打包资源(如 markdown 模板)的目录,可通过cline plugin install安装。
两种形态使用完全相同的插件 API,包形态只是额外提供了依赖管理与资源打包能力。
1.1 心智模型:注册表的四阶段生命周期
宿主启动会话时,会构建一个插件注册表并执行四个阶段——这一点在源码中可以直接印证。ContributionRegistry 类内部维护了一个 phase 状态字段,取值恰为 "resolve" | "validate" | "setup" | "activate" | "run"(见 contribution-registry.ts 第 456–458 行):
- resolve — 收集插件对象;
- validate — 校验每个插件的
manifest:capabilities必须非空;声明的钩子阶段必须有匹配的处理器;若存在hooks对象,"hooks"必须出现在capabilities中; - setup — 依次调用每个插件的
setup(api, ctx),在这里执行registerTool、registerCommand等注册动作; - activate — 注册表冻结,Agent 循环开始,你的钩子和工具正式生效。
注册表强制执行两条不变量,源码中的校验逻辑(normalizeManifest,contribution-registry.ts 第 345–407 行)给出了精确的报错文案:
- 每一项贡献都需要对应的能力声明:未声明
"rules"就调用api.registerRule(...)会抛出registerRule requires the "rules" capability; - 能力与处理器必须一致:声明了
"hooks"却没有hooks对象(或反之,有hooks却没声明能力)会校验失败,后者报runtime hooks require the "hooks" capability。
校验之后,注册是一次性的——会话期间没有动态注册/注销。
2. 最小可运行插件
import type { AgentPlugin } from "@cline/core";
import { createTool } from "@cline/core";
const plugin: AgentPlugin = {
name: "hello-plugin", // 必填,会话内唯一
manifest: {
capabilities: ["tools"], // 声明 setup() 将注册什么
},
setup(api, ctx) {
api.registerTool(
createTool({
name: "say_hello",
description: "Greet a person by name.",
inputSchema: {
type: "object",
properties: { name: { type: string } },
required: ["name"],
},
async execute({ name }: { name: string }) {
return { greeting: `Hello, ${name}!` };
},
}),
);
},
};
export default plugin;
这就是一个完整的插件,Agent 会把 say_hello 当作可调用的工具看到。
3. manifest:能力声明与字段说明
manifest: {
capabilities: ["tools", "hooks"], // 必填——非空数组
paths?: string[], // 可选——多入口包
providerIds?: string[], // 可选——provider 插件
modelIds?: string[], // 可选——model 插件
}
| 字段 | 使用场景 |
|---|---|
capabilities |
始终需要。列出插件的贡献内容;对应 api.register* 方法被它门控。 |
paths |
仅用于 package.json 的 cline.plugins 条目——当一个包暴露多个插件入口时。 |
providerIds |
当 capabilities 包含 "providers" 时——声明你注册哪些 provider ID。 |
modelIds |
当你要贡献与特定 ID 绑定的模型时。 |
3.1 完整能力列表
| 能力 | 在 api 上解锁的方法 |
|---|---|
tools |
api.registerTool() |
commands |
api.registerCommand()(聊天界面的斜杠命令) |
rules |
api.registerRule()(注入系统提示词的字符串) |
messageBuilders |
api.registerMessageBuilder()(改写发往 provider 的消息) |
providers |
api.registerProvider()(如自定义模型 provider) |
automationEvents |
api.registerAutomationEventType() 与 ctx.automation?.ingestEvent() |
hooks |
插件上的运行时 hooks 对象(生命周期回调) |
你可以声明任意组合——大多数实际插件只需要 1–3 个能力。
源码补充:从源码结构看,注册表内部的能力枚举
ExtensionCapabilityOptions比文档表格还多出skills与mcp两项(contribution-registry.ts 第 204–214 行),并且AgentExtensionApi上还有一个registerMcpServer方法,用于把 MCP 服务器暴露为运行时工具,同样需要"mcp"能力门控。也就是说文档表列出的是常用能力,源码才是能力全集。
4. setup(api, ctx):注册阶段
setup() 每个会话只运行一次,且在 Agent 循环开始前。你在这里注册的一切在该会话生命周期内都被冻结。
4.1 api 对象
每个 register* 方法都需要 manifest 中有对应能力:
api.registerTool(tool); // 需要 "tools"
api.registerCommand({ name, description, handler }); // 需要 "commands"
api.registerRule({ id, content, source }); // 需要 "rules"
api.registerMessageBuilder({ name, build }); // 需要 "messageBuilders"
api.registerProvider({ name, description }); // 需要 "providers"
api.registerAutomationEventType({ eventType, source, /* ... */ }); // 需要 "automationEvents"
api 的接口定义见 contribution-registry.ts 第 125–144 行的 AgentExtensionApi,其 JSDoc 明确说明所有注册项会累积进 ContributionRegistry,在 setup() 完成后供宿主使用。
4.2 ctx 对象:宿主提供的会话上下文
setup() 的第二个参数携带宿主对当前会话所知的全部信息。所有字段都是可选的,使用前必须做特性探测(feature-detect)——同一个插件必须在提供较少上下文的宿主(单元测试、沙箱化插件进程)中也能工作。这一点与源码中 PluginSetupContext 的定义一致(contribution-registry.ts 第 159–202 行),其注释同样强调:这些值始终来源于宿主会话配置,绝不来自 process.cwd()。
ctx.session?.sessionId // string —— 稳定的 core 会话 ID
ctx.client?.name // 宿主名:"cline-cli"、"cline-vscode" 等
ctx.user // 认证后的用户/组织信息(如可用)
ctx.workspaceInfo // { rootPath, hint, latestGitBranchName,
// latestGitCommitHash, associatedRemoteUrls }
ctx.automation?.ingestEvent // 发出规范化自动化事件
ctx.logger?.log // 作用域限定到本插件的结构化日志
ctx.telemetry // ITelemetryService —— 进程内为直连服务;
// 沙箱化插件中是一个 IPC 桥(见下)
关于 ctx.workspaceInfo 的两条重要规则:
- 始终优先使用
ctx.workspaceInfo?.rootPath而非process.cwd()。 CLI 可能通过--cwd启动而没有调用chdir,VS Code 工作区也不共享单一 CWD。workspaceInfo来源于会话配置,永远正确。 - 不要用
import.meta.url技巧去定位"工作区"。 那拿到的是插件自身的位置,不是用户项目的位置。
4.3 在钩子之间持久化状态
setup() 先运行,钩子后触发。共享状态最简单的方式是在插件文件中使用模块级变量:
let sessionWorkspaceRoot: string | undefined;
let sessionBranch: string | undefined;
const plugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
setup(api, ctx) {
sessionWorkspaceRoot = ctx.workspaceInfo?.rootPath;
sessionBranch = ctx.workspaceInfo?.latestGitBranchName;
},
hooks: {
beforeTool({ toolCall, input }) {
if (sessionBranch === "main" && toolCall.toolName === "run_commands") {
// 检查 input,必要时拦截。
}
return undefined;
},
},
};
单个 Node 进程可能并发承载多个会话。如果插件会运行在多会话宿主中,请以 ctx.session?.sessionId 为键存储状态,而不是使用模块级单例:
const stateBySession = new Map<string, MyState>();
setup(api, ctx) {
const id = ctx.session?.sessionId;
if (id) stateBySession.set(id, /* ... */);
}
5. 工具:api.registerTool
工具是插件为 Agent 赋予新能力的方式。使用 @cline/core 的 createTool() 辅助函数:
import { createTool } from "@cline/core";
api.registerTool(
createTool({
name: "get_weather", // 对模型可见
description: "Get current weather for a city.",
inputSchema: {
type: "object",
properties: {
city: { type: "string", description: "The city name" },
},
required: ["city"],
},
async execute(input, context) {
const { city } = input as { city: string };
// context.sessionId、context.conversationId、context.cwd 可用
return { city, temperature: "72°F", condition: "sunny" };
},
}),
);
源码补充:
createTool的实现见 sdk/packages/shared/src/tools/create.ts。除了文档示例中的四个字段,它还接受timeoutMs(默认 30000ms)、retryable(默认true)、maxRetries(默认 3)与lifecycle回调,并且支持直接传入 Zod schema——内部会通过zodToJsonSchema转成 JSON Schema 并校验顶层必须是 object 类型,在注册期就快速失败而不是等到推理时。
编写好工具的准则:
- 名字是 snake_case 动词——如
goto_definition、start_background_command; - 描述是写给模型看的,不是写给人类看的。写清楚何时使用该工具、各输入参数的含义、输出形态;
- 输入是 JSON Schema。显式标注
required字段,尽量用枚举约束取值; - 返回 JSON 可序列化的值——字符串、数字、纯对象、数组。宿主会先把结果序列化再回传给模型;
- 输入非法或硬失败时抛异常。运行时会把抛出的错误转成模型可恢复的工具错误结果;
- 保持工具聚焦。
start / get / delete三个小工具,胜过一个带mode枚举的巨型工具。
6. 运行时钩子:hooks: { ... }
运行时钩子是同一套钩子层上的带类型的进程内回调——运行时内部用的就是这一层。它们在 Agent 循环内部执行,拥有完整类型信息——无 IPC、无 JSON 序列化开销。
在 manifest.capabilities 中声明 "hooks",然后添加 hooks 属性:
const plugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
hooks: {
beforeRun(ctx) { /* ... */ },
beforeTool({ toolCall, input }) { /* ... */ },
afterTool({ toolCall, result }) { /* ... */ },
afterRun({ result }) { /* ... */ },
onEvent(event) { /* ... */ },
},
};
6.1 七个钩子
| 钩子 | 触发时机 | 能终止循环吗 | 常见用途 |
|---|---|---|---|
beforeRun |
运行时循环开始前(一个用户回合) | 能 | 问候、日志、附加会话元数据 |
afterRun |
运行时循环结束后(成功、中止或失败均触发) | 不能 | 通知、指标、持久化日志 |
beforeModel |
每次模型请求前 | 能(可修改请求) | 注入上下文、最后一公里的提示词修改 |
afterModel |
每次模型响应后、工具执行前 | 能 | 基于模型输出做拦截 |
beforeTool |
每次工具执行前 | 能(返回 { stop }) |
审计、脱敏、拦截危险工具 |
afterTool |
每次工具执行后 | 可替换结果 | 后处理、擦除工具输出中的密钥 |
onEvent |
运行时发出每个 AgentRuntimeEvent 时 |
不能 | 流式 UI、遥测管道 |
源码中这七个回调定义在 AgentRuntimeHooks 接口(sdk/packages/shared/src/agent.ts 第 420–449 行),注释直接写着"7-callback hook bag consumed by AgentRuntime",与上表一一对应;插件上的 hooks 字段类型正是 Partial<AgentRuntimeHooks>,所以任意一个钩子都可以省略。
6.2 从钩子中断循环
多个钩子返回一个可选的控制对象。最常见的模式是用 beforeTool 拦截破坏性工具调用:
beforeTool({ toolCall, input }) {
if (toolCall.toolName === "run_commands") {
const { commands } = input as { commands?: string[] };
if (sessionBranch === "main" && commands?.some((c) => c.startsWith("git push"))) {
return { stop: true, reason: "Blocked git push on protected branch" };
}
}
return undefined; // 显式表示"继续"
}
返回 undefined(或省略 return)即让执行正常继续。
6.3 afterRun 的语义
afterRun 对所有终态都会触发——completed、aborted、failed。如果只关心成功:
afterRun({ result }) {
if (result.status !== "completed") return;
// 发通知、记录成功指标等
}
6.4 插件钩子 vs 文件钩子
运行时支持两套钩子系统:
- 文件钩子——
.cline/hooks/下的外部脚本,以序列化 JSON 调用。适合不随代码分发的用户/工作区级脚本; - 插件运行时钩子——带类型的进程内回调。适合行为属于可复用扩展、且需要类型化访问运行时的场景。
Core 会把文件钩子适配到运行时钩子层,所以你不需要两套都写。如果你在发布一个插件,就把它写成运行时钩子。
7. 消息构建器:api.registerMessageBuilder
消息构建器在模型调用之前改写发往 provider 的消息列表。它们在运行时消息被转换成 SDK 消息块之后运行,但在 Core 内置的安全构建器之前——后者对 provider 安全的截断始终拥有最终决定权。
适用场景:
- 自定义压缩策略(用摘要替换中间历史);
- 在消息到达 provider 前擦除 PII 或密钥;
- 针对特定模型的能力特点重塑上下文。
api.registerMessageBuilder({
name: "summarize-middle-history",
build(messages) {
if (estimateTokens(messages) < THRESHOLD) return messages;
return [...prefix, summary, ...recent];
},
});
多个构建器按注册顺序执行;前一个的输出是后一个的输入。
什么时候该改用 beforeModel:只有当你需要运行时快照、或想直接修改请求对象本身时,才用 beforeModel 钩子。纯粹的消息改写属于构建器。
8. 自动化事件:api.registerAutomationEventType + ctx.automation
插件可以声明规范化事件类型,并把事件发送到 Cline 自动化中。没有启用自动化的宿主会直接忽略两者——你的插件应当对 ctx.automation 做特性探测。
manifest: { capabilities: ["automationEvents"] },
setup(api, ctx) {
api.registerAutomationEventType({
eventType: "github.pull_request.opened",
source: "github",
description: "A new GitHub PR was opened",
attributesSchema: { /* envelope.attributes 的 JSON Schema */ },
});
if (!ctx.automation) return; // 宿主没有自动化
ctx.automation.ingestEvent({
eventId: "pr-1234",
eventType: "github.pull_request.opened",
source: "github",
subject: "owner/repo#1234",
occurredAt: new Date().toISOString(),
attributes: { /* ... */ },
});
}
源码补充:一个有意思的细节——
ContributionRegistry.setup()中,只有声明了"automationEvents"能力的插件才会收到完整的setupContext,其他插件收到的ctx.automation会被显式置为undefined(contribution-registry.ts 第 555–560 行)。这从机制上保证了"没声明能力就拿不到自动化入口"。
9. 加载插件的三种方式
9.1 自动发现(CLI)
CLI 启动时扫描这些目录:
<workspace>/.cline/plugins/—— 项目级插件(可提交或 gitignore);~/.cline/plugins/—— 用户级插件;- 系统"Plugins"目录 —— 宿主托管的安装。
丢一个 .ts 或 .js 文件进去,运行 cline 即可:
mkdir -p .cline/plugins
cp my-plugin.ts .cline/plugins/
cline -i "do the thing my plugin enables"
9.2 SDK 配置中的显式 extensions: [...]
当你用 ClineCore 构建自己的宿主时,直接传入插件对象:
import plugin from "./my-plugin";
import { ClineCore } from "@cline/core";
const host = await ClineCore.create({ backendMode: "local" });
await host.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY ?? "",
cwd: process.cwd(),
enableTools: true,
systemPrompt: "You are a helpful assistant.",
extensions: [plugin],
// 要让 ctx.workspaceInfo 被填充,这是必需的:
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
prompt: "...",
interactive: false,
});
9.3 目录型插件用 pluginPaths: [...]
当插件是带 package.json 的目录时,把 pluginPaths 指向该目录。加载器读取 package.json,从 cline.plugins 字段找到入口:
config: {
// ...
pluginPaths: ["./path/to/my-plugin-package"],
}
或者用 CLI 安装:
cline plugin install ./path/to/my-plugin-package
cline plugin install @scope/my-cline-plugin # 从 npm
cline plugin install --git github.com/owner/repo # 从 git
源码佐证:包发现的解析逻辑在 plugin-config-loader.ts 中——
readDeclaredPluginEntryPaths读取包根的package.json,解析cline.plugins数组,每个条目可以是字符串路径,也可以是带paths数组的对象,条目可混用。
10. 单文件插件完整模板
这是单文件插件的完整形态。保存为 my-plugin.ts,放入 .cline/plugins/:
/**
* My Cline Plugin
*
* What it does: <一段面向用户的说明>.
*
* CLI 用法:
* mkdir -p .cline/plugins
* cp my-plugin.ts .cline/plugins/
* cline -i "trigger something the plugin enables"
*
* 直接演示:
* ANTHROPIC_API_KEY=sk-... bun run my-plugin.ts
*/
import { type AgentPlugin, ClineCore, createTool } from "@cline/core";
let sessionRoot: string | undefined;
const plugin: AgentPlugin = {
name: "my-plugin",
manifest: {
capabilities: ["tools", "hooks"],
},
setup(api, ctx) {
sessionRoot = ctx.workspaceInfo?.rootPath;
api.registerTool(
createTool({
name: "do_thing",
description: "Do the thing this plugin exists for.",
inputSchema: {
type: "object",
properties: { target: { type: "string" } },
required: ["target"],
},
async execute(input) {
const { target } = input as { target: string };
return { ok: true, target, root: sessionRoot };
},
}),
);
},
hooks: {
beforeRun() {
console.log("[my-plugin] run started");
},
afterRun({ result }) {
if (result.status !== "completed") return;
console.log(`[my-plugin] done in ${result.iterations} iteration(s)`);
},
},
};
// 可选:一个可运行的演示入口,让用户可以 `bun run` 直接跑这个文件。
async function runDemo(): Promise<void> {
const host = await ClineCore.create({ backendMode: "local" });
try {
const result = await host.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY ?? "",
cwd: process.cwd(),
enableTools: true,
systemPrompt: "You are a helpful assistant. Use tools when needed.",
extensions: [plugin],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
prompt: "Use do_thing on the target 'world'.",
interactive: false,
});
console.log(result.result?.text ?? "");
} finally {
await host.dispose();
}
}
if (import.meta.main) {
await runDemo();
}
export { plugin, runDemo };
export default plugin;
这就是全部形态。复制它、改工具名、替换你的逻辑即可。仓库的 sdk/examples/plugins/ 目录下提供了 web-search.ts、weather-metrics.ts、background-terminal.ts、typescript-lsp/、agents-squad/ 等可直接参考的官方示例插件,与本文模板结构一致。
11. 插件包完整走查
插件包是一个带 package.json 的目录。当你需要以下任何一项时用它:
- npm 依赖(
zod、yaml、typescript等); - 一个包内的多个插件入口;
- 打包资源(markdown 模板、agent 定义、schema、fixtures);
- 通过 npm 或 git 发布并版本化插件。
包本身仍然是普通 npm 包——让它成为插件的是 package.json 里的 cline.plugins 字段。
11.1 目录布局
典型包结构:
my-cline-plugin/
├── package.json
├── tsconfig.json (可选——仅本地类型检查)
├── index.ts (插件入口)
├── README.md (面向用户的文档)
└── assets/ (可选——打包内容)
├── templates/
│ └── greeting.md
└── schemas/
└── input.json
较大的插件也可以按功能组织:
my-cline-plugin/
├── package.json
├── index.ts
├── tools/
│ ├── do-thing.ts
│ └── read-thing.ts
├── hooks/
│ └── audit.ts
├── lib/
│ └── helpers.ts
└── assets/
└── ...
11.2 package.json:发现契约
{
"name": "my-cline-plugin",
"version": "0.1.0",
"private": true,
"description": "What this plugin does, in one sentence.",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"clean": "rm -rf node_modules dist"
},
"exports": {
".": "./index.ts"
},
"cline": {
"plugins": [
{
"paths": ["./index.ts"],
"capabilities": ["tools", "hooks"]
}
]
},
"peerDependencies": {
"@cline/core": "*"
},
"peerDependenciesMeta": {
"@cline/core": { "optional": true }
},
"dependencies": {
"zod": "^4.1.5"
}
}
逐字段说明:
type: "module"—— 必填。Cline 插件是 ES module;exports—— 指向 npm 消费者的入口。对于 Cline 运行时加载的 TypeScript 源码插件,可以直接导出./index.ts;加载器会处理 TS;cline.plugins—— 发现契约。一个条目数组,每个条目包含:paths—— 相对包根的入口文件。一个包暴露多个插件对象时,把所有入口都列出;capabilities—— 预声明的能力,加载器在导入入口前就校验它;
@cline/core的peerDependencies—— 宿主已经提供@cline/core。标为 peer 依赖可避免版本漂移;标为 optional 则允许用户在单独类型检查插件时不被强制安装@cline/core;dependencies—— 你自己的依赖(解析器、schema 库、你封装的 SDK)。
11.3 tsconfig.json(可选)
仅用于本地类型检查:
{
"extends": "../../tsconfig.json",
"include": ["index.ts"]
}
如果插件不在 monorepo 内,一个独立的最小 tsconfig.json 也行:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["index.ts"]
}
11.4 index.ts:包入口
与单文件版本相同的插件形态,只是放在包里:
import { type AgentPlugin, createTool } from "@cline/core";
import { z } from "zod";
const InputSchema = z.object({
target: z.string().min(1),
});
const plugin: AgentPlugin = {
name: "my-cline-plugin",
manifest: {
capabilities: ["tools"],
},
setup(api, ctx) {
api.registerTool(
createTool({
name: "do_thing",
description: "Do the thing.",
inputSchema: {
type: "object",
properties: { target: { type: "string" } },
required: ["target"],
},
async execute(input) {
const { target } = InputSchema.parse(input);
return { ok: true, target };
},
}),
);
},
};
export default plugin;
11.5 打包资源
index.ts 旁边的任何内容都会随包一起分发。解析资源路径时用 import.meta.url,不要用 process.cwd():
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { readFileSync, existsSync, readdirSync } from "node:fs";
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
const TEMPLATES_DIR = join(MODULE_DIR, "assets", "templates");
function loadTemplate(name: string): string | undefined {
const path = join(TEMPLATES_DIR, `${name}.md`);
return existsSync(path) ? readFileSync(path, "utf8") : undefined;
}
这是插件中唯一适合用 import.meta.url 的地方——定位插件包内部的文件。工作区路径则永远用 ctx.workspaceInfo?.rootPath。
11.6 覆盖模式(bundled / global / project)
一个包可以自带默认资源,同时允许用户用自己的文件覆盖。Cline 插件生态的通用约定是三级查找,按 name 后者覆盖前者:
- bundled —— 插件包内的文件(随插件分发的默认值);
- global ——
~/.cline/data/settings/<kind>/下的文件(用户级覆盖); - project ——
<workspace>/.cline/<kind>/下的文件(项目级覆盖)。
示例:一个支持用户以带 YAML frontmatter 的 markdown 文件定义"预设"的插件:
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import YAML from "yaml";
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
const BUNDLED_DIR = join(MODULE_DIR, "presets");
function resolveDataDir(): string {
return process.env.CLINE_DATA_DIR ??
join(process.env.HOME ?? "~", ".cline", "data");
}
function readPresets(workspaceRoot: string) {
const sources = [
{ dir: BUNDLED_DIR, source: "bundled" as const },
{ dir: join(resolveDataDir(), "settings", "presets"), source: "global" as const },
{ dir: join(workspaceRoot, ".cline", "presets"), source: "project" as const },
];
const presets = new Map<string, { name: string; body: string; source: string }>();
for (const { dir, source } of sources) {
if (!existsSync(dir)) continue;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
const raw = readFileSync(join(dir, entry.name), "utf8");
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
const data = match ? YAML.parse(match[1] ?? "") ?? {} : {};
const body = (match ? match[2] : raw).trim();
const name = data?.name ?? entry.name.replace(/\.md$/, "");
// 项目覆盖全局,全局覆盖内置——后写者胜。
presets.set(name, { name, body, source });
}
}
return [...presets.values()];
}
这个模式让用户可以:开箱即用(bundled 默认值);为所有项目做全局定制(在 ~/.cline/data/settings/<kind>/ 放文件);按项目覆盖(在 <workspace>/.cline/<kind>/ 放文件)。
11.7 一个包中的多个插件入口
如果一个包暴露多个插件,在 cline.plugins 中逐一列出:
"cline": {
"plugins": [
{ "paths": ["./tools-plugin.ts"], "capabilities": ["tools"] },
{ "paths": ["./hooks-plugin.ts"], "capabilities": ["hooks"] }
]
}
每个入口文件都应 export default 各自的插件对象。
11.8 安装插件包
包在磁盘上、npm 上或 git 仓库里后,用户用如下方式安装:
cline plugin install ./my-cline-plugin # 本地路径
cline plugin install @scope/my-cline-plugin # npm
cline plugin install --git github.com/owner/repo # git
CLI 会把包安装到 <workspace>/.cline/plugins/.installs/(或 ~/.cline/plugins/.installs/),并在下一次会话自动发现它。SDK 消费者则直接把 pluginPaths 指向包目录(见 9.3 节)。
12. 测试你的插件
12.1 单元测试
插件对象是纯数据。你可以用最小上下文驱动 setup(),直接调用工具:
import plugin from "../my-plugin";
const tools: unknown[] = [];
const api = {
registerTool: (t: unknown) => tools.push(t),
registerCommand: () => {},
registerRule: () => {},
registerMessageBuilder: () => {},
registerProvider: () => {},
registerAutomationEventType: () => {},
};
await plugin.setup?.(api as never, {
workspaceInfo: { rootPath: "/tmp/fake-workspace" },
});
// 现在 tools 里就是已注册的工具——调用 tool.execute(input, ctx)。
为了更高的保真度,可以构建一个真实注册表(new ContributionRegistry({ extensions: [plugin] }))并调用 initialize()——这会同时走一遍校验逻辑。对应实现与测试见 contribution-registry.test.ts。
12.2 用 runDemo() 做端到端验证
在插件文件中加一个 runDemo()(见第 10 节),针对 ANTHROPIC_API_KEY 启动一个真实的 ClineCore 会话:
ANTHROPIC_API_KEY=sk-... bun run my-plugin.ts
这是验证插件端到端可用性的最快方式。
12.3 CLI 冒烟测试
mkdir -p .cline/plugins
cp my-plugin.ts .cline/plugins/
cline -i "trigger something that exercises the plugin"
对于包:
cline plugin install ./my-cline-plugin
cline -i "..."
如果插件校验或 setup 失败,CLI 会打印清晰的错误并继续运行(不带该插件)。
13. 常见坑位清单
- "capabilities must be a non-empty array" —— 忘了写
manifest.capabilities,或者它是[]。 - "registerRule requires the 'rules' capability" —— 能力/处理器漂移。要么在 capabilities 里加
"rules",要么别再调用registerRule。 - 模型看不到工具 —— 检查会话配置的
enableTools: true,以及 capabilities 里是否声明了"tools"。 - SDK 测试中
ctx.workspaceInfo为 undefined —— 宿主没传extensionContext.workspace。在 SDK 代码中显式设置(见 9.2 节)。 - 状态在会话之间泄漏 —— 同进程内的模块级变量会在多个会话间共享。宿主并发跑多会话时,按
ctx.session?.sessionId做键。 afterRun在中止时也触发 —— 用if (result.status !== "completed") return;守卫。setup()里做重活 ——setup()会阻塞会话启动。把昂贵工作推迟到首次工具调用或beforeRun。- 导入宿主内部实现 —— 只从
@cline/core导入。伸手进宿主特定包(如 CLI 内部实现)会在非 CLI 宿主里坏掉。 - 发遥测 —— 对
ctx.telemetry做特性探测(ctx.telemetry?.capture(...));宿主没有遥测服务时它是 undefined。进程内插件直接拿到宿主服务;沙箱化插件拿到一个桥:capture/captureRequired/recordCounter|Histogram|Gauge通过 JSON IPC 转发到宿主,因此属性必须是纯 JSON 数据(字符串、数字、布尔)——绝不能是活对象。宿主会把所有插件事件与指标命名到plugin.之下、打上plugin_name标记,并在用户退出遥测时丢弃这些事件。 - 沙箱中
ctx.telemetry.isEnabled()永远返回true—— 桥是无状态的,宿主才是仲裁者:退出遥测的事件在宿主侧被丢弃。不要用isEnabled()去门控昂贵的属性计算;让遥测属性的构造保持廉价。身份/公共属性设置器(setDistinctId、setCommonProperties等)是宿主的职责,在沙箱中是空操作。 - 解析打包资源 —— 用
import.meta.url+fileURLToPath定位包内文件;永远不要用process.cwd()。工作区路径则相反:用ctx.workspaceInfo?.rootPath,永远不要用import.meta.url。 - 插件名冲突 ——
name在会话内必须唯一。两个插件同名时校验失败。用包名做命名空间(my-org-redactor而不是redactor)。
14. 决策指南:该用哪个扩展点?
| 你想…… | 使用 |
|---|---|
| 给模型一个新能力 | registerTool |
| 在聊天界面加一个斜杠命令 | registerCommand |
| 向系统提示词注入文本 | registerRule |
| 在消息触达 provider 前改写它 | registerMessageBuilder |
| 添加自定义模型 provider | registerProvider |
| 发出规范化的 cron/webhook 事件 | registerAutomationEventType + ctx.automation |
| 观察或引导 Agent 循环 | hooks.* |
| 拦截危险的工具调用 | hooks.beforeTool 返回 { stop: true } |
| 完成时发通知 | hooks.afterRun(用 status === "completed" 做门控) |
| 微调每次模型请求 | hooks.beforeModel |
| 把事件流式推给 UI | hooks.onEvent |
| 随插件分发可复用模板 | 把资源放到 index.ts 旁边,用 import.meta.url 解析 |
| 允许用户全局或按项目覆盖默认值 | 三级查找:bundled / global / project |
15. 发布前快速检查清单
- [ ]
manifest.capabilities是非空数组。 - [ ] 每次
api.register*调用都有对应的能力声明。 - [ ] 若存在
hooks,capabilities中有"hooks"。 - [ ] 工作区路径使用
ctx.workspaceInfo?.rootPath(而不是process.cwd())。 - [ ] 可选的
ctx字段都做了特性探测。 - [ ] 工具名是 snake_case 动词;描述是写给模型看的。
- [ ] 工具输入有带
required的 JSON Schema。 - [ ] 只关心成功时,
afterRun处理器用result.status === "completed"做门控。 - [ ] 不能在并发会话间泄漏的状态都按
ctx.session?.sessionId做键。 - [ ] (包形态)
package.json有type: "module"、cline.plugins,且@cline/core是可选 peer 依赖。 - [ ] (包形态)打包资源用
import.meta.url解析,而不是process.cwd()。 - [ ] 冒烟测试:把插件放进
.cline/plugins/(或cline plugin install),运行cline -i "...",确认它正常工作。
拿不准的时候,先写一个小工具,把它端到端跑通,再逐步扩展它。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00