315 lines
9.3 KiB
Plaintext
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.
|