首页
/ Cline Agent 插件开发实战:从单文件插件到完整插件包的完整指南

Cline Agent 插件开发实战:从单文件插件到完整插件包的完整指南

2026-09-06 15:35:11作者:郜逊炳

本文基于 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、自动化事件类型

插件以两种形态之一发布:

  1. 单文件插件——一个导出默认插件对象的 .ts 文件,放进发现目录即被加载;
  2. 插件包(Plugin package)——带 package.json、npm 依赖和(可选)打包资源(如 markdown 模板)的目录,可通过 cline plugin install 安装。

两种形态使用完全相同的插件 API,包形态只是额外提供了依赖管理与资源打包能力。

1.1 心智模型:注册表的四阶段生命周期

宿主启动会话时,会构建一个插件注册表并执行四个阶段——这一点在源码中可以直接印证。ContributionRegistry 类内部维护了一个 phase 状态字段,取值恰为 "resolve" | "validate" | "setup" | "activate" | "run"(见 contribution-registry.ts 第 456–458 行):

  1. resolve — 收集插件对象;
  2. validate — 校验每个插件的 manifestcapabilities 必须非空;声明的钩子阶段必须有匹配的处理器;若存在 hooks 对象,"hooks" 必须出现在 capabilities 中;
  3. setup — 依次调用每个插件的 setup(api, ctx),在这里执行 registerToolregisterCommand 等注册动作;
  4. activate — 注册表冻结,Agent 循环开始,你的钩子和工具正式生效。

注册表强制执行两条不变量,源码中的校验逻辑(normalizeManifestcontribution-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.jsoncline.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 比文档表格还多出 skillsmcp 两项(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 的两条重要规则:

  1. 始终优先使用 ctx.workspaceInfo?.rootPath 而非 process.cwd() CLI 可能通过 --cwd 启动而没有调用 chdir,VS Code 工作区也不共享单一 CWD。workspaceInfo 来源于会话配置,永远正确。
  2. 不要用 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/corecreateTool() 辅助函数:

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_definitionstart_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所有终态都会触发——completedabortedfailed。如果只关心成功:

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 会被显式置为 undefinedcontribution-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.tsweather-metrics.tsbackground-terminal.tstypescript-lsp/agents-squad/ 等可直接参考的官方示例插件,与本文模板结构一致。

11. 插件包完整走查

插件包是一个带 package.json 的目录。当你需要以下任何一项时用它:

  • npm 依赖(zodyamltypescript 等);
  • 一个包内的多个插件入口;
  • 打包资源(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/corepeerDependencies —— 宿主已经提供 @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 后者覆盖前者:

  1. bundled —— 插件包内的文件(随插件分发的默认值);
  2. global —— ~/.cline/data/settings/<kind>/ 下的文件(用户级覆盖);
  3. 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() 去门控昂贵的属性计算;让遥测属性的构造保持廉价。身份/公共属性设置器(setDistinctIdsetCommonProperties 等)是宿主的职责,在沙箱中是空操作。
  • 解析打包资源 —— 用 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* 调用都有对应的能力声明。
  • [ ] 若存在 hookscapabilities 中有 "hooks"
  • [ ] 工作区路径使用 ctx.workspaceInfo?.rootPath(而不是 process.cwd())。
  • [ ] 可选的 ctx 字段都做了特性探测。
  • [ ] 工具名是 snake_case 动词;描述是写给模型看的。
  • [ ] 工具输入有带 required 的 JSON Schema。
  • [ ] 只关心成功时,afterRun 处理器用 result.status === "completed" 做门控。
  • [ ] 不能在并发会话间泄漏的状态都按 ctx.session?.sessionId 做键。
  • [ ] (包形态)package.jsontype: "module"cline.plugins,且 @cline/core 是可选 peer 依赖。
  • [ ] (包形态)打包资源用 import.meta.url 解析,而不是 process.cwd()
  • [ ] 冒烟测试:把插件放进 .cline/plugins/(或 cline plugin install),运行 cline -i "...",确认它正常工作。

拿不准的时候,先写一个小工具,把它端到端跑通,再逐步扩展它。

登录后查看全文
热门项目推荐
相关项目推荐