Files
2026-07-13 12:52:40 +08:00

315 lines
9.3 KiB
Plaintext

---
title: "Writing Plugins"
sidebarTitle: "Writing Plugins"
description: "Build plugins that add tools, observe execution, and modify agent behavior."
---
Plugins are the primary way to package reusable agent capabilities. This guide walks through building a production-quality plugin from scratch.
## What You'll Build
A GitHub integration plugin that:
- Registers tools for interacting with GitHub (list issues, create PRs, post comments)
- Logs all tool calls for auditing
- Tracks token usage per session
## Step 1: Define the Plugin Structure
```typescript
// github-plugin.ts
import { type AgentPlugin } from "@cline/sdk"
import { createTool } from "@cline/sdk"
interface GitHubConfig {
token: string
owner: string
repo: string
}
export function createGitHubPlugin(config: GitHubConfig): AgentPlugin {
let totalTokens = 0
return {
name: "github-integration",
manifest: {
capabilities: ["tools", "hooks"],
},
setup(api, ctx) {
// Register tools in the setup phase
api.registerTool(createListIssuesTool(config))
api.registerTool(createCreateIssueTool(config))
api.registerTool(createPostCommentTool(config))
},
hooks: {
beforeRun() {
console.log(`[github] Run started`)
},
beforeTool(context) {
console.log(`[github] Tool: ${context.toolCall.name}(${JSON.stringify(context.input).slice(0, 100)})`)
},
afterRun(context) {
const usage = context.result.usage
totalTokens += (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0)
console.log(`[github] Run complete. Session tokens so far: ${totalTokens}`)
},
},
}
}
```
## Step 2: Create the Tools
```typescript
function createListIssuesTool(config: GitHubConfig) {
return createTool({
name: "list_github_issues",
description: `List open issues in ${config.owner}/${config.repo}. Returns issue numbers, titles, labels, and assignees.`,
inputSchema: {
type: "object",
properties: {
state: {
type: "string",
enum: ["open", "closed", "all"],
description: "Issue state filter. Default: open.",
},
labels: {
type: "string",
description: "Comma-separated label names to filter by (e.g., 'bug,priority:high').",
},
limit: {
type: "number",
description: "Maximum issues to return. Default: 10, max: 100.",
},
},
},
execute: async (input) => {
const params = new URLSearchParams({
state: input.state ?? "open",
per_page: String(Math.min(input.limit ?? 10, 100)),
})
if (input.labels) params.set("labels", input.labels)
const response = await fetch(
`https://api.github.com/repos/${config.owner}/${config.repo}/issues?${params}`,
{ headers: { Authorization: `token ${config.token}` } }
)
const issues = await response.json()
return {
issues: issues.map((i: Record<string, unknown>) => ({
number: i.number,
title: i.title,
state: i.state,
labels: (i.labels as Array<{ name: string }>).map((l) => l.name),
assignee: (i.assignee as { login: string } | null)?.login,
createdAt: i.created_at,
})),
total: issues.length,
}
},
})
}
function createCreateIssueTool(config: GitHubConfig) {
return createTool({
name: "create_github_issue",
description: `Create a new issue in ${config.owner}/${config.repo}.`,
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Issue title" },
body: { type: "string", description: "Issue body (Markdown supported)" },
labels: {
type: "array",
items: { type: "string" },
description: "Labels to apply",
},
},
required: ["title"],
},
execute: async (input) => {
const response = await fetch(
`https://api.github.com/repos/${config.owner}/${config.repo}/issues`,
{
method: "POST",
headers: {
Authorization: `token ${config.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: input.title,
body: input.body,
labels: input.labels,
}),
}
)
const issue = await response.json()
return { number: issue.number, url: issue.html_url }
},
})
}
function createPostCommentTool(config: GitHubConfig) {
return createTool({
name: "post_github_comment",
description: `Post a comment on an issue or PR in ${config.owner}/${config.repo}.`,
inputSchema: {
type: "object",
properties: {
issueNumber: { type: "number", description: "Issue or PR number" },
body: { type: "string", description: "Comment body (Markdown supported)" },
},
required: ["issueNumber", "body"],
},
execute: async (input) => {
const response = await fetch(
`https://api.github.com/repos/${config.owner}/${config.repo}/issues/${input.issueNumber}/comments`,
{
method: "POST",
headers: {
Authorization: `token ${config.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ body: input.body }),
}
)
const comment = await response.json()
return { id: comment.id, url: comment.html_url }
},
})
}
```
## Step 3: Use the Plugin
```typescript
import { Agent } from "@cline/sdk"
import { createGitHubPlugin } from "./github-plugin"
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a project manager assistant with access to GitHub.",
plugins: [
createGitHubPlugin({
token: process.env.GITHUB_TOKEN,
owner: "my-org",
repo: "my-project",
}),
],
})
await agent.run("List all open bugs and create a summary issue with the count")
```
## Distributing as a File Plugin
To load this plugin from a file in ClineCore, pass its path in `pluginPaths`:
```typescript
// /absolute/path/to/github.ts
import { type AgentPlugin, createTool } from "@cline/sdk"
const plugin: AgentPlugin = {
name: "github",
manifest: { capabilities: ["tools"] },
setup(api, ctx) {
api.registerTool(
createTool({
name: "list_github_issues",
// ... tool definition
})
)
},
}
export default plugin
```
Then include it in session config:
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
config: {
systemPrompt: "Use the GitHub plugin",
// ...model/runtime config
pluginPaths: ["/absolute/path/to/github.ts"],
},
})
```
## Distributing via CLI Install
Single-file plugins can be installed directly from a file URL:
```bash
cline plugin install https://github.com/your-org/your-repo/blob/main/plugins/github-plugin.ts
```
Single-file plugins can only import Node builtins and `@cline/*`. As soon as you need an npm dependency (`zod`, an HTTP client, etc.) you must ship as a package: a directory with a `package.json` that declares a `cline` field for entry points and your runtime `dependencies`. Dependencies under the `@cline/` scope are provided by the host runtime -- the installer strips these and runs `npm install` for the rest, so declare any `@cline/*` package you import as an optional peer dependency:
```json
{
"cline": {
"plugins": [{ "paths": ["./github-plugin.ts"], "capabilities": ["tools", "hooks"] }]
},
"dependencies": {
"@octokit/rest": "^21.0.0"
},
"peerDependencies": {
"@cline/sdk": "*"
},
"peerDependenciesMeta": {
"@cline/sdk": {
"optional": true
}
}
}
```
Users can also install package plugins from git, npm, or a local path:
```bash
cline plugin install https://github.com/your-org/cline-github-plugin.git
```
## Bundling Skills
Package plugins can include skills by adding a top-level `skills/` directory next to `package.json`:
```txt
cline-github-plugin/
package.json
github-plugin.ts
skills/
triage/
SKILL.md
```
Each bundled skill follows the same directory format as any other [Cline skill](/customization/skills). When the plugin is installed or loaded through `pluginPaths`, Cline discovers those skills automatically.
See [Plugins](/customization/plugins) for the full manifest format, directory layout, and the [typescript-lsp-plugin](https://github.com/cline/typescript-lsp-plugin) for a complete working example.
## Plugin Design Guidelines
1. Use factory functions (like `createGitHubPlugin`) when the plugin needs configuration. Export the plugin object directly when it doesn't.
2. Keep `setup()` synchronous and fast. It runs before the first LLM call, so any async initialization delays the agent.
3. Register all tools in `setup()`, not in lifecycle hooks. Tools must be available before the first iteration.
4. Use lifecycle hooks for observation (logging, metrics, auditing), not for modifying agent behavior. If you need to modify behavior, consider using the `beforeRun` or `beforeModel` hooks to adjust the system prompt or context.
5. Handle errors gracefully in hooks. A thrown error in `beforeTool` will count as a tool failure. If your hook is purely observational, catch errors internally.