
Cline Sdk
- 468 installs
- 10 repo stars
- Updated June 17, 2026
- cline/sdk-skill
Integrate or extend Cline SDK patterns when building agent clients, editor extensions, or automation that calls Cline APIs from your codebase.
About
The cline-sdk skill documents how to adopt the Cline SDK for embedding coding-agent behavior in apps, CLIs, and extensions, covering client setup, API calls, and integration patterns for production agent tooling.
- Official Cline SDK usage patterns
- Agent client and extension integration
- API-oriented coding assistant workflows
- Reduces bespoke agent wiring mistakes
- Pairs with editor and CLI agent builds
Cline Sdk by the numbers
- 468 all-time installs (skills.sh)
- Ranked #1,843 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cline/sdk-skill --skill cline-sdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 468 |
|---|---|
| repo stars | ★ 10 |
| Last updated | June 17, 2026 |
| Repository | cline/sdk-skill ↗ |
What it does
Integrate or extend Cline SDK patterns when building agent clients, editor extensions, or automation that calls Cline APIs from your codebase.
Files
Cline SDK Skill
Consolidated skill for building AI agents with the Cline SDK. Use the decision trees below to find the right entry point and API surface, then load detailed references.
Critical Rules
Follow these rules in all Cline SDK code:
1. Install with npm install @cline/sdk. The @cline/sdk package re-exports @cline/core, not every sub-package directly. Core re-exports the public SDK surface such as ClineCore, Agent, createAgentRuntime, createTool, built-in tool helpers, provider helpers, and the Llms namespace. Import from @cline/agents, @cline/llms, or @cline/shared only when you need APIs that core does not re-export, such as AgentRuntime, createAgent, or some low-level types. 2. Requires Node.js 22 or later. 3. Use createTool() from @cline/sdk (or @cline/shared) to define tools. Tool names must be snake_case. 4. Prefer returning structured error data from tool execute functions when the agent can recover. Direct Agent converts thrown tool errors into error tool results; ClineCore can also count repeated failed tool turns toward its mistake-limit handling. 5. Use lifecycle: { completesRun: true } on tools that should end the agent loop (e.g. a "submit answer" tool). 6. When using ClineCore, always call dispose() when done to clean up resources. 7. The direct Agent and ClineCore have different event systems. For Agent: use agent.subscribe() to get AgentRuntimeEvent types, text streaming is "assistant-text-delta", and result text is result.outputText. For ClineCore: use cline.subscribe() to get CoreSessionEvent types. Render user-facing text, reasoning, and tool activity from "agent_event" payloads (content_start, content_update, content_end, done). Treat "chunk" events as raw transport chunks with { stream, chunk, ts }, not as typed text deltas. ClineCore result text is result.text. There is no top-level onEvent field on AgentRuntimeConfig; use agent.subscribe() or hooks.onEvent instead. Do not use "content_update" or "content_start" with agent.subscribe(); those are host-facing AgentEvent types carried inside ClineCore agent_event events. 8. For direct Agent, plugins are simple runtime plugins with setup(context) returning { tools, hooks }. For ClineCore, extensions are AgentPlugin objects with manifest, setup(api, ctx), and optional hooks. Do not use ClineCore plugin examples inside direct Agent.plugins. 9. Plugin skills are file-based, not registered. There is no registerSkill() and no api.registerSkill. A plugin ships skills as SKILL.md files under <package>/skills/<name>/SKILL.md (package shape required); the host discovers them and surfaces them as /slash-commands automatically -- do not call registerCommand for skills. Plugin MCP servers use api.registerMcpServer() with the "mcp" capability. Configured agents (agent profiles) are YAML files in .cline/agents/ loaded as subagent_<name> tools when enableSpawnAgent is true.
How to Use This Skill
Reference File Structure
The two main API surfaces (Agent and ClineCore) follow a 4-file pattern. Cross-cutting concepts are single-file guides.
Each main API surface in ./references/<api>/ contains:
| File | Purpose | When to Read |
|---|---|---|
REFERENCE.md | Overview, when to use, quick start | Always read first |
api.md | Full API: classes, methods, config, types | Writing code |
patterns.md | Common patterns, best practices | Implementation guidance |
gotchas.md | Pitfalls, limitations, debugging | Troubleshooting |
Cross-cutting concepts in ./references/<concept>/ have REFERENCE.md as the entry point.
Reading Order
1. Start with REFERENCE.md for your chosen API surface 2. Then read additional files relevant to your task:
- Writing agent code ->
api.md - Common patterns ->
patterns.md - Creating tools ->
tools/REFERENCE.md - Adding plugins/hooks ->
plugins/REFERENCE.md - Configuring LLM providers ->
providers/REFERENCE.md - Streaming events ->
events/REFERENCE.md - Deploying to production ->
production/REFERENCE.md - Scheduling agents ->
scheduling/REFERENCE.md - Multi-agent orchestration ->
multi-agent/REFERENCE.md - Debugging ->
gotchas.md
Example Paths
./references/agent/REFERENCE.md # Start here for lightweight agents
./references/clinecore/REFERENCE.md # Start here for full runtime
./references/agent/api.md # Agent class, config, methods
./references/tools/REFERENCE.md # Creating and using tools
./references/plugins/REFERENCE.md # Plugin system
./references/providers/REFERENCE.md # LLM provider configurationQuick Decision Trees
"Which API surface should I use?"
Which API?
+-- I want a simple, in-memory agent with custom tools
| +-- agent/ (Agent class from @cline/agents, re-exported by @cline/sdk)
+-- I need session persistence, built-in tools, config discovery
| +-- clinecore/ (ClineCore from @cline/core)
+-- I want built-in file/shell/search/web tools
| +-- clinecore/ (has built-in tools; Agent does not)
+-- I want scheduled or recurring agents
| +-- clinecore/ (automation API)
+-- I need multi-process or multi-client session sharing
| +-- clinecore/ (hub-backed runtime)
+-- I'm building a browser-compatible agent
| +-- agent/ (no Node.js dependencies)"I need to create tools"
Tools?
+-- Define a custom tool with schema -> tools/REFERENCE.md
+-- Use built-in tools (read_files, search_codebase, run_commands, etc.) -> tools/REFERENCE.md (built-in section)
+-- Control tool approval/policies -> tools/REFERENCE.md (policies section)
+-- Tool that ends the agent loop -> tools/REFERENCE.md (completion tools)
+-- Package tools as a reusable plugin -> plugins/REFERENCE.md"I need to handle events"
Events?
+-- Stream text/reasoning in real time -> events/REFERENCE.md
+-- Track token usage and costs -> events/REFERENCE.md
+-- Watch tool calls -> events/REFERENCE.md
+-- Detect completion/errors -> events/REFERENCE.md
+-- Hook into lifecycle stages -> plugins/REFERENCE.md"I need to configure a model provider"
Providers?
+-- Anthropic (Claude) -> providers/REFERENCE.md
+-- OpenAI (GPT) -> providers/REFERENCE.md
+-- Google (Gemini/Vertex) -> providers/REFERENCE.md
+-- AWS Bedrock -> providers/REFERENCE.md
+-- Mistral -> providers/REFERENCE.md
+-- OpenAI-compatible (vLLM, Together, etc.) -> providers/REFERENCE.md
+-- Custom/self-hosted provider -> providers/REFERENCE.md"I need plugins or hooks"
Plugins?
+-- Package tools + hooks together -> plugins/REFERENCE.md
+-- Observe tool calls (logging, metrics) -> plugins/REFERENCE.md
+-- Intercept lifecycle events -> plugins/REFERENCE.md
+-- Add system prompt rules -> plugins/REFERENCE.md
+-- Expose an MCP server's tools -> plugins/REFERENCE.md (MCP servers)
+-- Bundle reusable skills (SKILL.md, auto slash commands) -> plugins/REFERENCE.md (bundled skills)
+-- Distribute via npm/git -> plugins/REFERENCE.md"I need multi-agent coordination"
Multi-agent?
+-- Run one-off delegated sub-agents -> multi-agent/REFERENCE.md (sub-agents)
+-- Predefined named sub-agents from files -> multi-agent/REFERENCE.md (configured agents)
+-- Persistent cross-session teams -> multi-agent/REFERENCE.md (teams)
+-- Parent-child delegation -> multi-agent/REFERENCE.md (sub-agents)
+-- Peer-to-peer task board -> multi-agent/REFERENCE.md (teams)"I need scheduling or automation"
Scheduling?
+-- Recurring cron jobs -> scheduling/REFERENCE.md
+-- One-off scheduled tasks -> scheduling/REFERENCE.md
+-- Event-driven triggers -> scheduling/REFERENCE.md
+-- CLI schedule management -> scheduling/REFERENCE.md"I need to go to production"
Production?
+-- Error handling and status checks -> production/REFERENCE.md
+-- Cost control and token limits -> production/REFERENCE.md
+-- Observability (OpenTelemetry) -> production/REFERENCE.md
+-- Security and sandboxing -> production/REFERENCE.md
+-- Deployment patterns -> production/REFERENCE.mdTroubleshooting Index
- Agent loop not stopping ->
tools/REFERENCE.md(completion tools) - Tool errors crashing the agent ->
agent/gotchas.mdorclinecore/gotchas.md - Provider auth failures ->
providers/REFERENCE.md - Session not persisting ->
clinecore/gotchas.md - Token usage too high ->
production/REFERENCE.md(cost control) - Hub connection issues ->
clinecore/gotchas.md - Plugin not loading ->
plugins/REFERENCE.md - Events not firing ->
events/REFERENCE.md
Product Index
API Surfaces
| API | Entry File | Description |
|---|---|---|
| Agent | ./references/agent/REFERENCE.md | Lightweight in-memory agent loop |
| ClineCore | ./references/clinecore/REFERENCE.md | Full runtime with sessions, persistence, built-in tools |
Cross-Cutting Concepts
| Concept | Entry File | Description |
|---|---|---|
| Tools | ./references/tools/REFERENCE.md | Built-in and custom tool creation |
| Plugins | ./references/plugins/REFERENCE.md | Extension system with hooks, MCP servers, and bundled skills |
| Events | ./references/events/REFERENCE.md | Real-time streaming events |
| Providers | ./references/providers/REFERENCE.md | LLM provider configuration |
| Production | ./references/production/REFERENCE.md | Deployment, security, observability |
| Scheduling | ./references/scheduling/REFERENCE.md | Cron jobs and automation |
| Multi-Agent | ./references/multi-agent/REFERENCE.md | Teams, sub-agents, and configured agent profiles |
Package Map
| Package | Purpose |
|---|---|
@cline/sdk | User-facing alias for @cline/core; install this first |
@cline/core | Sessions, persistence, built-in tools, config, hub, and selected re-exports |
@cline/agents | Browser-compatible AgentRuntime class and lower-level factories |
@cline/llms | LLM provider gateway |
@cline/shared | Types, tool helpers, hook engine |
Resources
Repository: https://github.com/cline/cline SDK Source: https://github.com/cline/cline/tree/main/sdk Documentation: https://docs.cline.bot/sdk/overview Discord: https://discord.gg/cline
Agent API Reference
Constructor
import { Agent } from "@cline/sdk"
const agent = new Agent(config)Also available through the lower-level factory re-export:
import { createAgentRuntime } from "@cline/sdk"
const agent = createAgentRuntime(config)AgentRuntime and createAgent are exported by @cline/agents directly, not by @cline/sdk. Runtime event and snapshot types are exported by @cline/agents; some plugin helper types live in @cline/shared.
AgentRuntimeConfig
Two config forms exist as a discriminated union:
With Provider ID (recommended)
interface AgentRuntimeConfigWithProvider {
providerId: string // e.g. "anthropic", "openai-native", "gemini"
modelId: string // provider model id
apiKey?: string // provider API key
baseUrl?: string // custom endpoint
headers?: Record<string, string>
systemPrompt?: string
modelOptions?: Record<string, unknown>
tools?: readonly AgentTool[]
initialMessages?: readonly AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
hooks?: Partial<AgentRuntimeHooks>
plugins?: readonly AgentRuntimePlugin[] // use structural typing; not re-exported by @cline/sdk
logger?: BasicLogger
telemetry?: ITelemetryService
maxIterations?: number
toolExecution?: "sequential" | "parallel"
requestToolApproval?: (request: ToolApprovalRequest) => Promise<ToolApprovalResult> | ToolApprovalResult
}With Pre-built Model
interface AgentRuntimeConfigWithModel {
model: AgentModel // pre-built model from gateway
systemPrompt?: string
modelOptions?: Record<string, unknown>
tools?: readonly AgentTool[]
initialMessages?: readonly AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
hooks?: Partial<AgentRuntimeHooks>
plugins?: readonly AgentRuntimePlugin[] // use structural typing; not re-exported by @cline/sdk
logger?: BasicLogger
telemetry?: ITelemetryService
maxIterations?: number
toolExecution?: "sequential" | "parallel"
requestToolApproval?: (request: ToolApprovalRequest) => Promise<ToolApprovalResult> | ToolApprovalResult
}Note: there is no top-level onEvent field on AgentRuntimeConfig. For event streaming, use agent.subscribe() or hooks.onEvent (see AgentRuntimeHooks below).
Direct Agent runtime plugins are not the same as ClineCore AgentPlugin extensions. A runtime plugin has name and optional setup(context) returning { tools, hooks }. Use ClineCore AgentPlugin objects only with config.extensions.
Advanced AgentRuntimeConfig Fields
interface AgentRuntimeConfig {
sessionId?: string
agentId?: string
conversationId?: string
parentAgentId?: string | null
agentRole?: string
messageModelInfo?: AgentMessage["modelInfo"]
completionPolicy?: {
requireCompletionTool?: boolean
completionGuard?: () => string | undefined
}
toolContextMetadata?: Record<string, unknown>
prepareTurn?: (context: AgentRuntimePrepareTurnContext) =>
AgentRuntimePrepareTurnResult | undefined | Promise<AgentRuntimePrepareTurnResult | undefined>
consumePendingUserMessage?: () => string | undefined | Promise<string | undefined>
}Use the identity fields when embedding Agent inside a larger host that needs stable session, conversation, or parent-agent routing. Use requestToolApproval with toolPolicies entries that set autoApprove: false; without that callback, approval-required tools are blocked and return an error tool result.
Methods
run(input)
Start the agent with user input. Returns when the agent loop completes.
const result: AgentRunResult = await agent.run("Build a REST API")Input can be a string, an AgentMessage, or an array of AgentMessage[].
continue(input?)
Continue an existing conversation with optional new input.
const result = await agent.continue("Now add authentication")abort(reason?)
Cancel the currently active run.
agent.abort("User cancelled")subscribe(listener)
Register a listener for streaming events.
import type { AgentRuntimeEvent } from "@cline/agents"
const unsubscribe = agent.subscribe((event: AgentRuntimeEvent) => {
// handle event
})
// Later: stop listening
unsubscribe()snapshot()
Get the current runtime state including message history.
import type { AgentRuntimeStateSnapshot } from "@cline/agents"
const state: AgentRuntimeStateSnapshot = agent.snapshot()restore(messages)
Replace the agent's message history.
agent.restore(previousMessages)There is no hasRun property. run() and continue() both execute against the current in-memory message history. Track first-run state in your app when that distinction matters.
AgentRunResult
Returned by run() and continue().
interface AgentRunResult {
agentId: string
agentRole?: string
runId: string
status: "completed" | "aborted" | "failed"
iterations: number
outputText: string
messages: readonly AgentMessage[]
usage: AgentUsage
error?: Error
}Status Values
"completed"- Agent finished normally"aborted"- Cancelled viaabort()"failed"- Unrecoverable error
AgentMessage
interface AgentMessage {
id: string
role: "user" | "assistant" | "tool"
content: AgentMessagePart[]
createdAt: number
metadata?: Record<string, unknown>
modelInfo?: { id: string; provider: string; family?: string }
metrics?: {
inputTokens: number
outputTokens: number
cacheReadTokens: number
cacheWriteTokens: number
cost?: number
}
}AgentUsage
interface AgentUsage {
inputTokens: number
outputTokens: number
cacheReadTokens: number
cacheWriteTokens: number
totalCost?: number
}AgentRuntimeHooks
interface AgentRuntimeHooks {
beforeRun?(context): AgentStopControl | undefined
afterRun?(context): void
beforeModel?(context): AgentBeforeModelResult | undefined
afterModel?(context): AgentStopControl | undefined
beforeTool?(context): AgentBeforeToolResult | undefined
afterTool?(context): AgentAfterToolResult | undefined
onEvent?(event: AgentRuntimeEvent): void | Promise<void>
}Hooks can intercept and modify behavior at each stage. Return a stop control from beforeRun, afterModel, or beforeTool to halt the agent loop.
hooks.onEvent receives the same AgentRuntimeEvent types as agent.subscribe(), but hook callbacks are awaited (can be async), while subscribe() listeners are called synchronously. Use subscribe() for UI streaming and hooks.onEvent for async side effects like logging to an external service.
AgentRuntimeStateSnapshot
interface AgentRuntimeStateSnapshot {
agentId: string
agentRole?: string
parentAgentId?: string | null
conversationId?: string
runId?: string
status: "idle" | "running" | "completed" | "aborted" | "failed"
iteration: number
messages: readonly AgentMessage[]
pendingToolCalls: readonly string[]
usage: AgentUsage
lastError?: string
}Factory: createAgentRuntime
Lower-level factory that returns the same Agent class:
import { createAgentRuntime } from "@cline/sdk"
const runtime = createAgentRuntime(config)See Also
REFERENCE.md- Overview and quick startpatterns.md- Common patterns../tools/REFERENCE.md- Tool creation../events/REFERENCE.md- Event types../providers/REFERENCE.md- Provider setup
Agent Gotchas
Agent Loop Never Stops
If the agent keeps iterating without completing:
- Make sure at least one tool has
lifecycle: { completesRun: true }if you want the agent to explicitly finish. - Without any tools, the agent will complete after the model returns text without tool calls.
- If using tools, ensure the system prompt guides the model toward calling the completion tool when done.
- Check that
completesRuntools return successfully (not throwing errors).
Tool Errors Become Error Results
When a direct Agent tool's execute function throws an exception, the runtime catches it and returns a tool-result part with isError: true. ClineCore wraps the direct runtime and can treat repeated failed tool turns as recoverable mistakes.
Instead, return errors as structured data:
// Bad: throwing
execute: async (input) => {
throw new Error("File not found")
}
// Good: returning error data
execute: async (input) => {
return { error: "File not found", path: input.path }
}run() vs continue()
run(input)andcontinue(input?)both execute against the current in-memory message history.- Neither method resets history. Use
restore(messages)to replace history, or create a newAgentfor a fresh conversation. - There is no
agent.hasRunproperty. Track first-run state in your app if you need separate UI behavior.
Browser Compatibility
@cline/agents is browser-safe. @cline/sdk re-exports @cline/core, which is Node-oriented. For browser usage, import directly from @cline/agents:
import { Agent } from "@cline/agents"No Top-Level onEvent on Agent Config
AgentRuntimeConfig does not have a top-level onEvent field. Passing onEvent to new Agent({ onEvent: ... }) has no effect. There are two ways to receive events:
// Option 1: subscribe() - synchronous, best for UI streaming
const agent = new Agent({ ...config })
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
// Option 2: hooks.onEvent - awaited, best for async side effects
const agent = new Agent({
...config,
hooks: {
onEvent: async (event) => {
if (event.type === "assistant-text-delta") {
await logToService(event.text)
}
},
},
})Both receive the same AgentRuntimeEvent types. Prefer subscribe() for streaming UI.
Event Listener Timing
Register event listeners via subscribe() before calling run():
// Good: subscribe before run
agent.subscribe(handler)
const result = await agent.run(input)
// Bad: subscribing after run starts loses early events
const promise = agent.run(input)
agent.subscribe(handler) // may miss eventsTool Input Schema Matters
The model uses the tool's inputSchema to decide what arguments to pass. A vague or missing schema leads to incorrect tool calls.
- Use
z.enum()for fixed value sets, not free-form strings - Describe every property with
.describe()in Zod ordescriptionin JSON Schema - Include constraints (rate limits, max values) in the tool description
Memory and Long Conversations
The Agent holds all messages in memory. For long-running conversations, memory usage grows with each turn. Consider:
- Using
ClineCorewith compaction for long sessions - Periodically creating a new agent with a summary of the conversation
- Monitoring
result.usage.inputTokensandresult.messages.lengthto track context growth
Abort Signal Handling in Tools
Long-running tools should respect the abort signal:
execute: async (input, context) => {
for (const item of items) {
if (context.signal?.aborted) {
return { partial: results, aborted: true }
}
results.push(await process(item))
}
return { results }
}Provider API Key
If you get authentication errors, check:
apiKeyis set in the config or via environment variables- The key matches the
providerId(e.g., Anthropic key forproviderId: "anthropic") - For OpenAI-compatible providers, both
apiKeyandbaseUrlare set
See ../providers/REFERENCE.md for provider-specific setup.
See Also
api.md- Full API referencepatterns.md- Common patterns../tools/REFERENCE.md- Tool creation../clinecore/REFERENCE.md- Use ClineCore for persistence
Agent Patterns
Interactive CLI Agent
A multi-turn conversational agent in the terminal with streaming output:
import { Agent } from "@cline/sdk"
import * as readline from "node:readline"
let hasRun = false
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant. Keep responses concise.",
tools: [],
})
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
function prompt(): void {
rl.question("\nYou: ", async (input) => {
const trimmed = input.trim()
if (!trimmed || trimmed === "exit") {
rl.close()
return
}
process.stdout.write("\nAssistant: ")
if (hasRun) {
await agent.continue(trimmed)
} else {
await agent.run(trimmed)
hasRun = true
}
process.stdout.write("\n")
prompt()
})
}
prompt()Conversational Agent (Slack Bot, Chat App)
Maintain per-thread agents with conversation memory:
import { Agent } from "@cline/sdk"
const agents = new Map<string, Agent>()
const hasRunByThread = new Set<string>()
async function handleMessage(threadId: string, message: string) {
let agent = agents.get(threadId)
if (!agent) {
agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a concise assistant.",
tools: [],
})
agents.set(threadId, agent)
}
const result = hasRunByThread.has(threadId)
? await agent.continue(message)
: await agent.run(message)
hasRunByThread.add(threadId)
return result.outputText
}Streaming UI
Build a real-time UI by handling events via subscribe():
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [myTool],
})
agent.subscribe((event) => {
switch (event.type) {
case "assistant-text-delta":
ui.appendText(event.text)
break
case "assistant-message":
ui.endText()
break
case "turn-started":
ui.startTurn(event.iteration)
break
case "turn-finished":
if (event.toolCallCount > 0) ui.showToolCount(event.toolCallCount)
break
case "usage-updated":
ui.updateUsage(event.usage.inputTokens, event.usage.outputTokens)
break
}
})
const result = await agent.run("Hello!")Structured Output via Completion Tool
Use a tool with completesRun: true to extract structured data:
import { Agent, createTool } from "@cline/sdk"
import { z } from "zod"
const submitReview = createTool({
name: "submit_review",
description: "Submit the final code review with structured feedback.",
inputSchema: z.object({
summary: z.string(),
issues: z.array(z.object({
file: z.string(),
line: z.number(),
severity: z.enum(["error", "warning", "info"]),
message: z.string(),
})),
approved: z.boolean(),
}),
lifecycle: { completesRun: true },
execute: async (input) => input,
})
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "Review the code diff and submit structured feedback.",
tools: [submitReview],
})
const result = await agent.run(diffContent)
const toolResults = result.messages
.flatMap(message => message.content)
.filter(part => part.type === "tool-result" && part.toolName === "submit_review")
const review = toolResults.at(-1)?.output
console.log(review)Agent with Abort/Timeout
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "Analyze this data.",
tools: [],
})
const timeout = setTimeout(() => agent.abort("Timeout"), 30_000)
try {
const result = await agent.run(data)
if (result.status === "aborted") {
console.log("Agent was aborted")
} else {
console.log(result.outputText)
}
} finally {
clearTimeout(timeout)
}Agent with Plugins
import { Agent } from "@cline/sdk"
const loggingPlugin = {
name: "logging",
setup() {
return {
hooks: {
beforeTool({ toolCall }) {
console.log(`Calling tool: ${toolCall.toolName}`)
},
afterRun({ result }) {
console.log(`Completed in ${result.iterations} iterations`)
},
},
},
},
}
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [myTool],
plugins: [loggingPlugin],
})Restoring State Across Sessions
Save and restore agent state manually:
// Save state
const snapshot = agent.snapshot()
const serialized = JSON.stringify(snapshot.messages)
// Later: restore
const agent2 = new Agent({ ...config })
const messages = JSON.parse(serialized)
agent2.restore(messages)
const result = await agent2.continue("Continue where we left off")For automatic persistence, use ClineCore instead.
Pre-Built Model via Gateway
For advanced provider configuration:
import { Agent } from "@cline/sdk"
import { Llms } from "@cline/sdk"
const gateway = Llms.createGateway({
providerConfigs: [
{ providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
{ providerId: "openai-native", apiKey: process.env.OPENAI_API_KEY },
],
})
const model = gateway.createAgentModel({
providerId: "anthropic",
modelId: "claude-opus-4-7",
})
const agent = new Agent({
model,
systemPrompt: "You are a helpful assistant.",
tools: [],
})See Also
api.md- Full API referencegotchas.md- Common pitfalls../tools/REFERENCE.md- Creating tools../plugins/REFERENCE.md- Plugin system
Agent Runtime
The Agent class is the lightweight in-memory agent loop. It is implemented in @cline/agents and re-exported by @cline/sdk through @cline/core. It sends messages to an LLM, executes tool calls, collects results, and repeats until the task is done.
When to Use Agent
| Use Agent when... | Use ClineCore instead when... |
|---|---|
| You want a simple agent with custom tools | You need built-in tools (run_commands, editor, etc.) |
| You want minimal dependencies | You need session persistence |
You need browser compatibility through @cline/agents | You need config discovery from .cline/ |
| You manage persistence yourself | You need multi-process session sharing |
| You want full control over the runtime | You want batteries-included setup |
Quick Start
import { Agent } from "@cline/sdk"
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
const result = await agent.run("What is the capital of France?")
console.log(result.outputText)Core Concepts
The Agent operates in a loop: 1. Accept user input (string, message, or array of messages) 2. Build turn context (system prompt, messages, tools) 3. Call the LLM provider 4. If the model returns tool calls, execute them and loop back to step 3 5. If the model returns text without tool calls, the run completes 6. Emit events throughout for streaming
The agent does not persist anything to disk. Conversation history is held in memory and can be accessed via snapshot().
Key APIs
new Agent(config)- Create an agent from@cline/sdkcreateAgentRuntime(config)- Factory re-exported by@cline/sdkAgentRuntimeandcreateAgent(config)- Lower-level exports from@cline/agentsagent.run(input)- Start a run with user inputagent.continue(input?)- Continue an existing conversationagent.abort(reason?)- Cancel an active runagent.subscribe(listener)- Listen to streaming eventsagent.snapshot()- Get current runtime stateagent.restore(messages)- Replace message history
See api.md for full API details.
Multi-Turn Conversations
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [],
})
const first = await agent.run("What is 2 + 2?")
console.log(first.outputText)
const second = await agent.continue("Now multiply that by 3")
console.log(second.outputText)There is no agent.hasRun property. Keep your own conversation state, or inspect agent.snapshot().messages.length if you need to decide whether to pass new user input.
Event Streaming
Use agent.subscribe() to stream events in real time. Register the listener before calling run() to avoid missing early events.
There is no top-level onEvent field on the Agent config. For an async alternative, use hooks.onEvent (see api.md and gotchas.md).
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [],
})
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
const result = await agent.run("What is the capital of France?")See events/REFERENCE.md for the full event type catalog.
Next Steps
api.md- Full Agent API referencepatterns.md- Common patterns and best practicesgotchas.md- Pitfalls and debugging../tools/REFERENCE.md- Creating custom tools../events/REFERENCE.md- Event system details../providers/REFERENCE.md- Provider configuration
ClineCore API Reference
Creating ClineCore
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create(options: ClineCoreOptions)ClineCoreOptions
interface ClineCoreOptions {
clientName?: string // identifies your app
distinctId?: string // user/instance identifier
backendMode?: "auto" | "local" | "hub" | "remote"
hub?: HubOptions
remote?: RemoteOptions
capabilities?: RuntimeCapabilities
telemetry?: ITelemetryService
logger?: BasicLogger
toolPolicies?: Record<string, ToolPolicy>
messagesArtifactUploader?: SessionMessagesArtifactUploader
automation?: boolean | ClineCoreAutomationOptions
fetch?: typeof fetch
sessionService?: SessionBackend
prepare?: (input: ClineCoreStartInput) => {
applyToStartSessionInput(input: ClineCoreStartInput): ClineCoreStartInput | Promise<ClineCoreStartInput>
dispose?(): void | Promise<void>
} | undefined | Promise<{
applyToStartSessionInput(input: ClineCoreStartInput): ClineCoreStartInput | Promise<ClineCoreStartInput>
dispose?(): void | Promise<void>
} | undefined>
}RuntimeCapabilities
interface RuntimeCapabilities {
toolExecutors?: Partial<ToolExecutors>
requestToolApproval?: (request: ToolApprovalRequest) => ToolApprovalResult | Promise<ToolApprovalResult>
}Starting Sessions
start(input)
const session = await cline.start(input: ClineCoreStartInput)Returns a StartSessionResult:
interface StartSessionResult {
sessionId: string
manifest: SessionManifest
manifestPath: string
messagesPath: string
result?: AgentResult
}ClineCoreStartInput
interface ClineCoreStartInput {
prompt?: string
config: CoreSessionConfig
source?: string
interactive?: boolean
sessionMetadata?: Record<string, unknown>
initialMessages?: Message[]
userImages?: string[]
userFiles?: string[]
toolPolicies?: Record<string, ToolPolicy>
capabilities?: RuntimeCapabilities
localRuntime?: object
}CoreSessionConfig
interface CoreSessionConfig {
cwd: string // working directory
workspaceRoot?: string // workspace root when different from cwd
providerId: string // LLM provider
modelId: string // model identifier
apiKey?: string // provider API key
baseUrl?: string
headers?: Record<string, string>
knownModels?: Record<string, ModelInfo>
providerConfig?: ProviderConfig
thinking?: boolean
reasoningEffort?: ProviderConfig["reasoningEffort"]
systemPrompt: string // custom system prompt
mode?: "act" | "plan" | "yolo" | "zen"
rules?: string
maxIterations?: number
toolPolicies?: Record<string, ToolPolicy>
extraTools?: readonly AgentTool[] // additional custom tools
enableTools: boolean // enable built-in tools
disableMcpSettingsTools?: boolean
yolo?: boolean
hooks?: AgentHooks // runtime hooks
hookErrorMode?: HookErrorMode
extensions?: AgentPlugin[] // plugins loaded inline
pluginPaths?: string[] // paths to plugin packages
extensionContext?: ExtensionContext // from @cline/shared
checkpoint?: CoreCheckpointConfig
compaction?: CoreCompactionConfig
execution?: AgentConfig["execution"]
telemetry?: ITelemetryService
logger?: BasicLogger
enableSpawnAgent: boolean // enable sub-agent spawning
enableAgentTeams: boolean // enable team coordination
teamName?: string // team identifier
missionLogIntervalSteps?: number
missionLogIntervalMs?: number
onTeamEvent?: (event: TeamEvent) => void
onConsecutiveMistakeLimitReached?: (context: ConsecutiveMistakeLimitContext) =>
ConsecutiveMistakeLimitDecision | Promise<ConsecutiveMistakeLimitDecision>
toolRoutingRules?: ToolRoutingRule[]
skills?: string[]
workspaceMetadata?: string
}extensions passes plugin objects directly. pluginPaths can point at plugin files or directories. Directory plugins can declare entries in package.json under cline.plugins; otherwise index.ts or index.js is used when present. ClineCore builds ctx.workspaceInfo from cwd or workspaceRoot, so pass those fields for predictable workspace-aware plugins.
Follow-Up Messages
send({ sessionId, prompt })
Send a follow-up message to an existing session:
const result = await cline.send({
sessionId: session.sessionId,
prompt: "Now add authentication",
})Returns AgentResult | undefined.
Event Subscription
subscribe(listener, options?)
const unsubscribe = cline.subscribe(
(event: CoreSessionEvent) => {
// handle events
},
{ sessionId: "optional-filter" }
)CoreSessionEvent
type CoreSessionEvent =
| { type: "chunk"; payload: SessionChunkEvent }
| { type: "agent_event"; payload: { sessionId: string, event: AgentEvent, teamAgentId?: string, teamRole?: "lead" | "teammate" } }
| { type: "ended"; payload: SessionEndedEvent }
| { type: "team_progress"; payload: SessionTeamProgressEvent }
| { type: "pending_prompts"; payload: SessionPendingPromptsEvent }
| { type: "pending_prompt_submitted"; payload: SessionPendingPromptSubmittedEvent }
| { type: "session_snapshot"; payload: SessionSnapshotEvent }
| { type: "status"; payload: { sessionId: string, status: string } }
| { type: "hook"; payload: SessionToolEvent }For direct ClineCore subscribers, render text, reasoning, and tool activity from the agent_event branch. chunk payloads are raw transport chunks:
interface SessionChunkEvent {
sessionId: string
stream: "stdout" | "stderr" | "agent"
chunk: string
ts: number
}
interface SessionEndedEvent {
sessionId: string
reason: string
ts: number
}Session Management
list(limit?, options?)
const sessions: SessionHistoryRecord[] = await cline.list(50)listHistory(options?)
const sessions: SessionHistoryRecord[] = await cline.listHistory({ limit: 50 })get(sessionId)
const session: SessionRecord | undefined = await cline.get(sessionId)readMessages(sessionId)
const messages: Message[] = await cline.readMessages(sessionId)getAccumulatedUsage(sessionId)
const usage = await cline.getAccumulatedUsage(sessionId)
// usage.usage - root agent only
// usage.aggregateUsage - root + subagents/teammatesupdate(sessionId, updates)
await cline.update(sessionId, { title: "New title" })abort(sessionId, reason?)
await cline.abort(sessionId, "User cancelled")stop(sessionId)
await cline.stop(sessionId)updateSessionModel(sessionId, modelId)
Switch the model for an active session:
await cline.updateSessionModel(sessionId, "claude-opus-4-7")delete(sessionId)
await cline.delete(sessionId)restore(input)
Restore a session from a checkpoint:
await cline.restore({ sessionId, checkpointRunCount: 3 })dispose(reason?)
Clean up all resources. Always call this when done:
await cline.dispose("Shutting down")Pending Prompts
Interactive sessions can queue or steer follow-up prompts while a run is active:
const prompts = await cline.pendingPrompts.list({ sessionId })
await cline.pendingPrompts.update({
sessionId,
promptId: prompts[0].id,
prompt: "Prioritize the test failures first.",
delivery: "steer",
})
await cline.pendingPrompts.delete({ sessionId, promptId: prompts[0].id })AgentResult
Returned by session operations:
interface AgentResult {
text: string
usage: LegacyAgentUsage
messages: MessageWithMetadata[]
toolCalls: ToolCallRecord[]
iterations: number
finishReason: "completed" | "max_iterations" | "aborted" | "mistake_limit" | "error"
model: { id: string; provider: string; info?: ModelInfo }
startedAt: Date
endedAt: Date
durationMs: number
}Tool Policies
Control tool access at the session level:
const session = await cline.start({
prompt: "Review the code",
config: {
...config,
toolPolicies: {
editor: { enabled: false },
},
},
toolPolicies: {
read_files: { autoApprove: true },
run_commands: { autoApprove: false },
},
})Use config.toolPolicies when disabled tools should be removed before the model sees them. Use top-level toolPolicies for per-session execution approval and blocking.
ToolPolicy
interface ToolPolicy {
enabled?: boolean // false = blocked; in ClineCore config, also removed before model requests
autoApprove?: boolean // false = requires approval callback
}Interactive Approval
const cline = await ClineCore.create({
clientName: "my-app",
capabilities: {
requestToolApproval: async (request) => {
console.log(`Tool: ${request.toolName}, Input: ${JSON.stringify(request.input)}`)
const approved = await askUser(`Allow ${request.toolName}?`)
return { approved }
},
},
})Automation API
When automation is enabled in ClineCore.create():
const cline = await ClineCore.create({
clientName: "my-app",
automation: true,
})
// Access automation methods
await cline.automation.start()
await cline.automation.reconcileNow()
cline.automation.ingestEvent(event)
cline.automation.listEvents()
cline.automation.listSpecs()
cline.automation.listRuns()
await cline.automation.stop()Settings API
// Read settings
const settings = await cline.settings.list()
// Toggle tools, plugins, MCP servers
await cline.settings.toggle({ type: "tools", name: "run_commands", enabled: true })See Also
REFERENCE.md- Overview and quick startpatterns.md- Common patternsgotchas.md- Pitfalls../tools/REFERENCE.md- Tool creation../plugins/REFERENCE.md- Plugin system
ClineCore Gotchas
Always Call dispose()
ClineCore holds resources (file watchers, database connections, hub connections). Failing to call dispose() can leave orphan processes and file locks.
const cline = await ClineCore.create({ clientName: "my-app" })
try {
// ... use cline
} finally {
await cline.dispose()
}Node.js 22 Required
ClineCore and @cline/core require Node.js 22 or later. If you're on an older version, you'll get runtime errors. Check with node --version.
Session Config vs Global Config
Tool policies can be set at two levels:
- Global execution policy: in
ClineCore.create({ toolPolicies })applies to all sessions - Per-session execution policy: in
cline.start({ toolPolicies })overrides global approval and blocking for that session - Session tool-list policy: in
cline.start({ config: { toolPolicies } })filters disabled built-in and extension tools before the model sees them
For hiding a tool from model requests, use config.toolPolicies. For approval or blocking at execution time, use global or top-level start policies.
enableTools Must Be Explicit
Built-in tools (run_commands, editor, read_files, etc.) are not available unless you set enableTools: true in the session config:
await cline.start({
prompt: "Read package.json",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
systemPrompt: "You are a helpful coding agent.",
enableTools: true, // required for built-in tools
enableSpawnAgent: false,
enableAgentTeams: false,
},
})Without this, the agent only has access to custom tools you provide via config.extraTools and tools contributed by enabled plugins.
cwd Matters for Built-in Tools
Built-in tools like run_commands, editor, and read_files operate relative to config.cwd. If not set, they use the process working directory. Always set it explicitly for predictable behavior:
config: {
cwd: "/absolute/path/to/project",
// ...
}Hub Startup Latency
With backendMode: "auto", the first session may be slow if a hub daemon needs to be spawned. For immediate responsiveness:
- Use
backendMode: "local"for in-process execution (fastest startup) - Pre-warm the hub with
cline hub ensureCLI command - Accept the one-time startup cost and let subsequent sessions reuse the hub
Session Storage Location
Sessions are stored at ~/.cline/data/sessions/. This includes:
sessions.db- SQLite database with session metadata- per-session manifest and message artifacts. Use
manifestPathandmessagesPathreturned fromcline.start()instead of assuming file names.
If you're running in a container or ephemeral environment, these paths may not persist across restarts.
requestToolApproval Blocks Execution
When a tool policy has autoApprove: false and you provide a requestToolApproval callback, the agent loop blocks until your callback resolves. If your callback never resolves (e.g., waiting for user input that never comes), the session hangs.
For automated pipelines, either:
- Set all tools to
autoApprove: true - Implement a timeout in your approval callback
Plugin Discovery Paths
ClineCore discovers plugins from:
- Global:
~/.cline/plugins/ - Workspace:
.cline/plugins/
For SDK consumers, pass plugins via extensions: [plugin] or pluginPaths: ["./path"] in the session config.
If a plugin isn't loading, verify:
- The file is in one of the discovery directories, or passed via
extensions/pluginPaths - The file exports a default plugin object with a non-empty
manifest.capabilitiesarray - Every
api.register*call insetup()has a matching capability declared - If
hooksis present on the plugin,"hooks"is incapabilities
Workspace Context for Plugins
If your plugins use ctx.workspaceInfo, pass cwd or workspaceRoot in the session config. ClineCore derives structured workspace metadata from those fields and forwards it to plugin setup:
await cline.start({
config: {
...baseConfig,
cwd: process.cwd(),
extensions: [myPlugin],
},
})The CLI sets workspace fields automatically, but SDK consumers should pass them explicitly.
send() Requires an Active Session
cline.send() only works on sessions that are still active. If a session has already completed, send() may return undefined or fail. Check session status with cline.get(sessionId) first.
Result May Be Undefined
session.result can be undefined if the session was started but hasn't completed yet (e.g., in a non-blocking hub mode). Check for this:
const session = await cline.start({ ... })
if (session.result) {
console.log(session.result.text)
} else {
console.log("Session started but not yet complete")
}Compaction and Long Sessions
For long-running sessions, message history grows and eventually exceeds the model's context window. ClineCore handles this via compaction, which summarizes older messages. Configure it via compaction:
config: {
compaction: {
strategy: "basic",
// ...
},
}The default strategy works for most cases, but extremely long sessions may benefit from tuning.
See Also
api.md- Full API referencepatterns.md- Common patterns../agent/gotchas.md- Agent-level gotchas../tools/REFERENCE.md- Tool troubleshooting../providers/REFERENCE.md- Provider troubleshooting
ClineCore Patterns
Basic Session with Built-in Tools
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
const session = await cline.start({
prompt: "Read package.json and summarize the dependencies",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
cwd: process.cwd(),
systemPrompt: "You are a helpful coding agent.",
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
},
})
console.log(session.result?.text)
await cline.dispose()Streaming Session with UI Updates
const cline = await ClineCore.create({ clientName: "my-app" })
cline.subscribe((event) => {
switch (event.type) {
case "agent_event":
if (
event.payload.event.type === "content_start" &&
event.payload.event.contentType === "text" &&
event.payload.event.text
) {
ui.appendText(event.payload.event.text)
}
break
case "ended":
ui.showComplete(event.payload.reason)
break
}
})
await cline.start({
prompt: "Refactor the auth module",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: "/path/to/project",
systemPrompt: "You are a helpful coding agent.",
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
},
})Multi-Turn Session
const cline = await ClineCore.create({ clientName: "my-app" })
const session = await cline.start({
prompt: "Create a new Express server",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: "/path/to/project",
systemPrompt: "You are a helpful coding agent.",
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
},
})
// Follow-up
const result = await cline.send({
sessionId: session.sessionId,
prompt: "Now add a health check endpoint",
})
console.log(result?.text)
await cline.dispose()Tiered Permission Model
Auto-approve reads, require approval for writes:
const cline = await ClineCore.create({
clientName: "my-app",
toolPolicies: {
read_files: { autoApprove: true },
search_codebase: { autoApprove: true },
fetch_web_content: { autoApprove: true },
run_commands: { autoApprove: false },
editor: { autoApprove: false },
apply_patch: { autoApprove: false },
},
capabilities: {
requestToolApproval: async (request) => {
const approved = await promptUser(
`Allow ${request.toolName}?\n${JSON.stringify(request.input, null, 2)}`
)
return { approved }
},
},
})Custom Tools Alongside Built-ins
import { ClineCore, createTool } from "@cline/sdk"
import { z } from "zod"
const deployTool = createTool({
name: "deploy",
description: "Deploy the application to the specified environment.",
inputSchema: z.object({
environment: z.enum(["staging", "production"]),
}),
execute: async (input) => {
const result = await runDeployment(input.environment)
return { url: result.url, status: "deployed" }
},
})
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
prompt: "Deploy the app to staging",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
systemPrompt: "You are a deployment assistant.",
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
extraTools: [deployTool],
},
})Session with Plugins
Load plugins inline with extensions. Pass cwd or workspaceRoot so plugins receive accurate ctx.workspaceInfo:
import { ClineCore } from "@cline/sdk"
import myPlugin from "./my-plugin"
const cline = await ClineCore.create({
clientName: "my-app",
backendMode: "local",
})
await cline.start({
prompt: "Do the thing my plugin enables",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
systemPrompt: "You are a helpful assistant.",
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
extensions: [myPlugin],
},
})
await cline.dispose()For directory-based plugin packages, use pluginPaths instead:
config: {
pluginPaths: ["./my-cline-plugin"],
cwd: process.cwd(),
}See ../plugins/REFERENCE.md for the full plugin authoring guide.
Session Listing and Replay
const cline = await ClineCore.create({ clientName: "my-app" })
// List recent sessions
const sessions = await cline.list(10)
for (const session of sessions) {
console.log(`${session.sessionId}: ${session.metadata?.title ?? ""}`)
}
// Read messages from a past session
const messages = await cline.readMessages(sessions[0].sessionId)
for (const msg of messages) {
console.log(`[${msg.role}] ${msg.content}`)
}
// Check usage
const usage = await cline.getAccumulatedUsage(sessions[0].sessionId)
const aggregate = usage?.aggregateUsage
console.log(`Total tokens: ${(aggregate?.inputTokens ?? 0) + (aggregate?.outputTokens ?? 0)}`)Graceful Shutdown
const cline = await ClineCore.create({ clientName: "my-app" })
process.on("SIGTERM", async () => {
await cline.dispose("SIGTERM received")
process.exit(0)
})
// Run sessions...Stateless Worker Pattern
For request/response workloads (API endpoints, queue consumers):
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({
clientName: "worker",
backendMode: "local",
})
async function handleRequest(prompt: string, workspace: string) {
const session = await cline.start({
prompt,
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: workspace,
systemPrompt: "You are a focused worker agent.",
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
},
})
return {
text: session.result?.text,
usage: session.result?.usage,
sessionId: session.sessionId,
}
}Hub-Backed Multi-Client
Multiple clients can attach to the same session:
// Process 1: start session
const cline = await ClineCore.create({
clientName: "backend",
backendMode: "hub",
})
const session = await cline.start({
prompt: "Long running refactor task",
config: { ... },
})
// Process 2: attach and stream events
const viewer = await ClineCore.create({
clientName: "dashboard",
backendMode: "hub",
})
viewer.subscribe((event) => {
dashboard.render(event)
}, { sessionId: session.sessionId })See Also
api.md- Full API referencegotchas.md- Common pitfalls../tools/REFERENCE.md- Tool creation../plugins/REFERENCE.md- Plugin system../scheduling/REFERENCE.md- Scheduled agents
ClineCore Runtime
ClineCore is the full-featured runtime from @cline/core, re-exported by @cline/sdk. It wraps the Agent loop with session persistence, built-in tools (run_commands, editor, read_files, search_codebase, fetch_web_content), config discovery, plugin loading, automation, and optional hub-backed multi-process support.
When to Use ClineCore
| Use ClineCore when... | Use Agent instead when... |
|---|---|
You need built-in tools (run_commands, editor, etc.) | You only need custom tools |
| You want session persistence to disk | No disk persistence is fine |
You need config discovery from .cline/ dirs | You handle config yourself |
| You want scheduled/automated agents | You don't need scheduling |
| You need multi-client session sharing | Single-process is fine |
| You're building a full application | You want minimal dependencies |
Quick Start
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
const session = await cline.start({
prompt: "Set up CI with GitHub Actions",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
cwd: "/path/to/project",
systemPrompt: "You are a helpful coding agent.",
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
},
})
console.log(session.result?.text)
await cline.dispose()Core Concepts
Sessions
Every cline.start() call creates a session with a unique ID. Sessions persist their messages and metadata to SQLite. You can list, read, resume, and delete sessions.
Built-in Tools
ClineCore provides the default tool suite when enableTools: true. The exact enabled set depends on mode, tool routing, available configured skills, MCP settings, and policies:
| Tool | Description |
|---|---|
run_commands | Execute shell commands |
editor | Edit files |
read_files | Read file contents |
apply_patch | Apply unified diffs |
search_codebase | Search file contents and structure |
fetch_web_content | HTTP requests and web content |
skills | Invoke configured skills |
ask_question | Ask a follow-up question through the host |
submit_and_exit | Submit final answer and end the session |
Config Discovery
ClineCore watches .cline/ directories for:
- Rules (system prompt additions)
- Skills (domain knowledge)
- Workflows (multi-step procedures)
- Hooks (lifecycle logic)
- Plugins (tool + hook bundles)
- MCP servers (external tool providers)
Backend Modes
| Mode | Description |
|---|---|
"auto" (default) | Tries to connect to a local hub; falls back to in-process if unavailable |
"local" | In-process execution, local SQLite storage, no hub |
"hub" | Requires a compatible local WebSocket hub; fails if unavailable |
"remote" | Connects to an explicit remote hub endpoint |
The default mode is "auto". For simple scripts and CLI tools, "local" avoids hub discovery overhead. Hub mode enables multi-client session sharing (e.g., a dashboard watching a running session from another process).
Key APIs
ClineCore.create(options)- Create and initializecline.start(input)- Start a new sessioncline.send({ sessionId, prompt })- Send follow-up messagecline.subscribe(listener)- Listen to session eventscline.list()- List sessionscline.listHistory(options)- List sessions with history filterscline.get(sessionId)- Get session metadatacline.readMessages(sessionId)- Read persisted messagescline.getAccumulatedUsage(sessionId)- Token/cost totalscline.pendingPrompts.list/update/delete(...)- Inspect or steer queued prompts for active interactive sessionscline.updateSessionModel(sessionId, modelId)- Switch model for an active sessioncline.abort(sessionId)- Abort a sessioncline.stop(sessionId)- Stop a sessioncline.restore(input)- Restore a checkpointcline.delete(sessionId)- Delete a sessioncline.settings.list/toggle(...)- Inspect or toggle tools, plugins, MCP, skills, rules, and workflowscline.automation.*- Manage automation when enabledcline.dispose()- Clean up resources
See api.md for full API details.
Event Streaming
cline.subscribe() emits CoreSessionEvent types. These are different from the AgentRuntimeEvent types emitted by the standalone Agent class -- see ../events/REFERENCE.md for the full comparison.
cline.subscribe((event) => {
switch (event.type) {
case "agent_event":
if (
event.payload.event.type === "content_start" &&
event.payload.event.contentType === "text" &&
event.payload.event.text
) {
process.stdout.write(event.payload.event.text)
}
break
case "ended":
console.log(`Session ended: ${event.payload.reason}`)
break
}
})ClineCore results use AgentResult with .text (not .outputText like the standalone Agent's AgentRunResult).
Session Persistence
Sessions are stored at:
~/.cline/data/sessions/
sessions.db # SQLite databaseThe exact artifact paths are returned from cline.start() as manifestPath and messagesPath. Use those returned paths instead of assuming a filename.
Next Steps
api.md- Full ClineCore API referencepatterns.md- Common patterns and best practicesgotchas.md- Pitfalls and debugging../tools/REFERENCE.md- Custom tool creation../plugins/REFERENCE.md- Plugin system../scheduling/REFERENCE.md- Scheduled agents
Events
The Cline SDK has three event layers. Which one you use depends on whether you're working with the standalone Agent class or ClineCore.
Which Events Do I Get?
| If you use... | You subscribe with... | You receive... | Text streaming event |
|---|---|---|---|
Standalone Agent | agent.subscribe() | AgentRuntimeEvent | assistant-text-delta |
ClineCore | cline.subscribe() | CoreSessionEvent | agent_event with content_start text |
These are different event types with different shapes. Do not mix them up.
Layer 1: AgentRuntimeEvent (Standalone Agent)
Emitted by the Agent class via agent.subscribe(). This is what you get when using new Agent(...) directly. Every event includes a snapshot field with the current AgentRuntimeStateSnapshot.
Run Lifecycle
{ type: "run-started", snapshot }
{ type: "run-finished", snapshot, result: AgentRunResult }
{ type: "run-failed", snapshot, error: Error }Turns
{ type: "turn-started", snapshot, iteration: number }
{ type: "turn-finished", snapshot, iteration: number, toolCallCount: number }Text Streaming
// Streaming text delta (arrives as chunks during generation)
{ type: "assistant-text-delta", snapshot, iteration: number, text: string, accumulatedText: string }
// Streaming reasoning delta (when model uses extended thinking)
{ type: "assistant-reasoning-delta", snapshot, iteration: number, text: string, accumulatedText: string, redacted?: boolean, metadata?: unknown }
// Complete assistant message after model finishes
{ type: "assistant-message", snapshot, iteration: number, message: AgentMessage, finishReason: "stop" | "tool-calls" | "max-tokens" | "aborted" | "error" }Messages
// Fired when any user, assistant, or tool message is added to conversation history
{ type: "message-added", snapshot, message: AgentMessage }Tool Events
{ type: "tool-started", snapshot, iteration: number, toolCall: { toolName: string, toolCallId: string, input: unknown } }
{ type: "tool-updated", snapshot, iteration: number, toolCall: { toolName: string, toolCallId: string }, update: unknown }
{ type: "tool-finished", snapshot, iteration: number, toolCall: { toolName: string, toolCallId: string }, message: AgentMessage }Usage
{
type: "usage-updated",
snapshot,
usage: {
inputTokens: number,
outputTokens: number,
cacheReadTokens: number,
cacheWriteTokens: number,
totalCost?: number,
},
}Notices
{ type: "status-notice", snapshot, message: string, metadata?: Record<string, unknown> }Subscribing
Use agent.subscribe(). Register the listener before calling run() to avoid missing early events.
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
agent.subscribe((event) => {
switch (event.type) {
case "assistant-text-delta":
process.stdout.write(event.text)
break
case "tool-started":
console.log(`\nUsing tool: ${event.toolCall.toolName}`)
break
case "usage-updated":
console.log(`Cost: $${event.usage.totalCost?.toFixed(4)}`)
break
case "run-finished":
console.log(`\nDone: ${event.result.status}`)
break
}
})
const result = await agent.run("Hello!")You can also receive events through hooks (these are awaited, so they can be async):
const agent = new Agent({
...config,
hooks: {
onEvent: async (event) => {
// Same AgentRuntimeEvent types as subscribe()
},
},
})Layer 2: AgentEvent (ClineCore Internal)
When using ClineCore, a RuntimeEventAdapter translates Layer 1 events into a legacy format called AgentEvent. Direct cline.subscribe() consumers receive these events inside CoreSessionEvent records with type: "agent_event". Render assistant text, reasoning, and tool activity from this structured branch. Treat chunk events as raw transport chunks, not as the primary text stream. The key mappings:
| AgentRuntimeEvent (Layer 1) | AgentEvent (Layer 2) |
|---|---|
turn-started | iteration_start |
turn-finished | iteration_end |
assistant-text-delta | content_start (text) |
assistant-message | content_end (text) |
tool-started | content_start (tool) |
tool-updated | content_update (tool) |
tool-finished | content_end (tool) |
usage-updated | usage (with computed deltas) |
run-finished | done |
run-failed | error |
run-started, message-added | (suppressed, not emitted) |
This layer exists for backwards compatibility. If you see event types like content_update or iteration_start in other documentation, they refer to this layer, not to what agent.subscribe() emits.
Layer 3: CoreSessionEvent (ClineCore Subscriber)
Emitted by ClineCore via cline.subscribe(). These are higher-level session events.
type CoreSessionEvent =
| { type: "chunk"; payload: SessionChunkEvent }
| { type: "agent_event"; payload: { sessionId: string, event: AgentEvent, teamAgentId?: string, teamRole?: "lead" | "teammate" } }
| { type: "ended"; payload: SessionEndedEvent }
| { type: "team_progress"; payload: SessionTeamProgressEvent }
| { type: "pending_prompts"; payload: SessionPendingPromptsEvent }
| { type: "pending_prompt_submitted"; payload: SessionPendingPromptSubmittedEvent }
| { type: "session_snapshot"; payload: SessionSnapshotEvent }
| { type: "status"; payload: { sessionId: string, status: string } }
| { type: "hook"; payload: SessionToolEvent }SessionChunkEvent
interface SessionChunkEvent {
sessionId: string
stream: "stdout" | "stderr" | "agent"
chunk: string
ts: number
}Raw agent chunks contain serialized AgentEvent envelopes in the current runtime. Prefer the structured agent_event branch instead of parsing or printing raw chunks.
SessionEndedEvent
interface SessionEndedEvent {
sessionId: string
reason: string
ts: number
}Subscribing
cline.subscribe((event) => {
switch (event.type) {
case "agent_event":
if (
event.payload.event.type === "content_start" &&
event.payload.event.contentType === "text" &&
event.payload.event.text
) {
process.stdout.write(event.payload.event.text)
}
break
case "ended":
console.log(`Finished: ${event.payload.reason}`)
break
}
})Filter by session:
cline.subscribe(handler, { sessionId: "specific-session-id" })Hub Events (Layer 3b)
When ClineCore runs in hub mode (via backendMode: "hub" or "auto" when a hub is available), events are projected over WebSocket using HubEventName types like assistant.delta, iteration.started, tool.started, etc. You do not interact with these directly through cline.subscribe(), which still gives you CoreSessionEvent.
Result Type Differences
The standalone Agent and ClineCore return different result types:
| API | Result type | Text property |
|---|---|---|
agent.run() | AgentRunResult | result.outputText |
cline.start() / cline.send() | AgentResult | result.text |
Common Patterns
Streaming Text (Standalone Agent)
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})Streaming Text (ClineCore)
cline.subscribe((event) => {
if (
event.type === "agent_event" &&
event.payload.event.type === "content_start" &&
event.payload.event.contentType === "text" &&
event.payload.event.text
) {
process.stdout.write(event.payload.event.text)
}
})Tool Call Logging (ClineCore)
cline.subscribe((event) => {
if (
event.type === "agent_event" &&
event.payload.event.type === "content_start" &&
event.payload.event.contentType === "tool"
) {
console.log(`Tool started: ${event.payload.event.toolName}`)
}
if (
event.type === "agent_event" &&
event.payload.event.type === "content_end" &&
event.payload.event.contentType === "tool"
) {
console.log(`Tool finished: ${event.payload.event.toolName}`)
}
})Usage Tracking (Standalone Agent)
agent.subscribe((event) => {
if (event.type === "usage-updated" && event.usage.totalCost) {
console.log(`Running cost: $${event.usage.totalCost.toFixed(4)}`)
}
})Tool Call Logging (Standalone Agent)
agent.subscribe((event) => {
if (event.type === "tool-started") {
console.log(`Tool started: ${event.toolCall.toolName}`)
}
if (event.type === "tool-finished") {
console.log(`Tool finished: ${event.toolCall.toolName}`)
}
})See Also
../agent/REFERENCE.md- Agent runtime overview../clinecore/REFERENCE.md- ClineCore session management../plugins/REFERENCE.md- Plugin hooks for lifecycle events../production/REFERENCE.md- Observability in production
Multi-Agent Coordination
The Cline SDK supports two models for multi-agent work: sub-agents (parent-child) and teams (peer-to-peer).
Sub-Agents vs Teams
| Feature | Sub-Agents | Teams |
|---|---|---|
| Enable with | enableSpawnAgent: true | enableAgentTeams: true |
| Persistence | Result returned to parent only | Across sessions |
| Coordination | Parent-child hierarchy | Peer-to-peer |
| Shared state | None | Task board, mailbox, mission log |
| Best for | One-off delegation | Complex multi-session projects |
Sub-Agents
Sub-agents are spawned by a parent agent during a run. The current spawn_agent tool runs the delegated task synchronously and returns the sub-agent's result to the parent tool call.
Enabling Sub-Agents
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
prompt: "Refactor the auth module and update tests",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
systemPrompt: "You are a helpful coding agent.",
enableSpawnAgent: true,
enableAgentTeams: false,
enableTools: true,
},
})When enableSpawnAgent is true, the agent gets access to sub-agent tools:
| Tool | Description |
|---|---|
spawn_agent | Run a delegated task with a focused sub-agent |
Each defined agent profile also appears as its own subagent_<name> tool (see "Configured Agents" below).
How Sub-Agents Work
1. The parent agent decides a subtask can be delegated 2. It calls spawn_agent with a focused system prompt and task description 3. The sub-agent runs the delegated task with its own focused prompt 4. The tool returns the sub-agent text, iteration count, finish reason, and token usage 5. The parent incorporates that result into its own run
Configured Agents (Agent Profiles)
Configured agents are predefined sub-agents declared as files instead of spawned ad hoc. Each one becomes its own dedicated sub-agent tool, so the model can delegate to a named specialist ("the reviewer", "the migrator") rather than describing a fresh sub-agent every time.
Defining an Agent Profile
Agent profiles are YAML-frontmatter files (.yml / .yaml) placed in an agents/ directory:
<workspace>/.cline/agents/-- project-scoped profiles.~/.cline/agents/-- user-scoped profiles.
---
name: Reviewer
description: Reviews a diff for bugs, missing tests, and migration risk.
tools: read_files, search_codebase # optional, restrict built-in tools
skills: code-review # optional, scope to specific skills
providerId: anthropic # optional, override provider
modelId: claude-sonnet-4-6 # optional, override model
maxIterations: 12 # optional, cap the sub-agent loop
---
You are a meticulous code reviewer. Inspect the diff, then report
findings ranked by severity, followed by any open questions.The markdown body is the agent's system prompt. Only name and description are required; tools/skills accept a comma-separated string or a YAML array.
Loading and Use
Profiles load automatically when enableSpawnAgent: true -- no extra config field. Each profile is exposed as a sub-agent tool named subagent_<name> (sanitized, with a short hash suffix on collisions), invoked with { prompt }:
await cline.start({
prompt: "Have the reviewer look at the staged changes, then summarize.",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
systemPrompt: "You coordinate specialist sub-agents.",
enableSpawnAgent: true,
enableTools: true,
},
})Behavior notes:
toolsrestricts which built-in tools the sub-agent may use; omit it to inherit the default suite.skillsscopes the sub-agent to specific skills.providerId/modelIdoverride the model for that agent only; otherwise it inherits the parent's.- Profiles are deduplicated by
name(case-insensitive); workspace profiles take precedence over user profiles. A malformed file is skipped and reported, not fatal. - Configured agents complement the generic
spawn_agenttool -- both are available whenenableSpawnAgentis true.
Teams
Teams provide persistent, cross-session coordination between agents.
Enabling Teams
await cline.start({
prompt: "Coordinate the auth sprint",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
systemPrompt: "You coordinate a team of agents.",
enableSpawnAgent: true,
enableAgentTeams: true,
teamName: "auth-sprint",
enableTools: true,
},
})Team Tools
When enableAgentTeams is true, the coordinator agent gets:
| Tool | Description |
|---|---|
team_spawn_teammate | Create a new agent with a role and task |
team_shutdown_teammate | Shut down a teammate agent |
team_task | Create, update, or inspect team tasks |
team_run_task | Start a run for a teammate task |
team_cancel_run | Cancel a teammate run |
team_status | Inspect team and teammate status |
team_list_runs | List teammate runs |
team_await_runs | Wait for selected runs |
team_send_message | Send a mailbox message |
team_broadcast | Broadcast a mailbox message |
team_read_mailbox | Read team mailbox messages |
team_mission_log | Append or read mission log entries |
team_cleanup | Clean up team state |
team_create_outcome | Create an outcome record |
team_attach_outcome_fragment | Attach a fragment to an outcome |
team_review_outcome_fragment | Review an outcome fragment |
team_finalize_outcome | Finalize an outcome |
team_list_outcomes | List outcome records |
Team Persistence
Teams store shared state in:
~/.cline/data/Team state is persisted by the ClineCore session and team stores. Treat the storage layout as an implementation detail and use the team tools or session APIs instead of reading files directly.
CLI Team Access
cline --team-name auth-sprint "Continue the auth refactor"Choosing Between Sub-Agents and Teams
Use sub-agents when:
- You need one-off delegation within a single session
- Tasks are independent and don't need to communicate with each other
- Results only matter to the parent agent
Use teams when:
- Work spans multiple sessions over time
- Agents need to coordinate and share progress
- Tasks have dependencies between them
- You want a persistent record of multi-agent collaboration
Patterns
Focused Research with Sub-Agents
A parent agent can call spawn_agent for focused subtasks and then synthesize the returned results. Each spawn_agent tool call waits for that delegated run to finish before the parent receives the result:
await cline.start({
prompt: `Research these three topics:
1. Current best practices for JWT auth
2. OAuth 2.0 provider comparison
3. Session management patterns
Use spawn_agent for each topic, then synthesize the returned results.`,
config: {
enableSpawnAgent: true,
enableAgentTeams: false,
enableTools: true,
// ...
},
})Team Sprint
A coordinator manages a multi-session project:
await cline.start({
prompt: `You are the coordinator for the auth-sprint team.
Review the task board and delegate the next highest-priority task
to a teammate. Check status on any in-progress tasks.`,
config: {
enableAgentTeams: true,
enableSpawnAgent: true,
teamName: "auth-sprint",
enableTools: true,
// ...
},
})See Also
../clinecore/REFERENCE.md- ClineCore runtime../clinecore/api.md- Session config for teams../tools/REFERENCE.md- Tool system../plugins/REFERENCE.md- Plugin system
Plugins
A ClineCore plugin is a TypeScript module that extends hosts built on @cline/core or @cline/sdk. The same plugin shape is used by the Cline CLI, editor extensions, and custom apps that load plugins through ClineCore.
A plugin can:
- Register tools the model can call.
- Register MCP servers whose tools the model can call.
- Bundle skills -- reusable
SKILL.mdinstructions the host discovers from the package and surfaces as slash commands. - Hook into the agent loop before/after runs, model calls, and tool calls.
- Rewrite provider messages before they hit the model (custom compaction, redaction, context shaping).
- Register slash commands, prompt rules, providers, and automation event types.
A plugin ships in one of two shapes:
1. Single-file plugin -- one .ts file that exports a default plugin object. Drop it in a discovery folder and it loads. Can only import Node builtins (node:fs, etc.) and host-provided @cline/* packages. No npm dependencies. 2. Plugin package -- a directory with package.json, npm dependencies, and optionally bundled assets. Installable via cline plugin install.
Both shapes use the same ClineCore plugin API.
Which Shape Do I Need?
The dividing line is npm dependencies. If your plugin only uses Node builtins and @cline/*, ship a single file. The moment you reach for zod, yaml, axios, an internal SDK client, or any other third-party package, you need the package shape. There is no middle ground -- a single .ts file with import { z } from "zod" fails at load with Cannot find module 'zod' because the plugin loader walks up from the plugin file looking for node_modules/<package> and there is nowhere for those deps to live next to a bare file.
Direct Agent has a different, smaller runtime plugin shape: plugins entries can return { tools, hooks } from setup(context), but they do not use manifest or setup(api, ctx). Use the AgentPlugin examples in this file with ClineCore extensions or pluginPaths, not with direct Agent.plugins.
The Mental Model
When the host starts a session, it builds a registry of plugins and runs four phases:
1. resolve -- collect the plugin objects. 2. validate -- check each plugin's manifest. Capabilities must be non-empty, unsupported capability names fail, and if hooks is present, "hooks" must be in capabilities. 3. setup -- call each plugin's setup(api, ctx) once. This is where you registerTool, registerCommand, etc. 4. activate -- registry is frozen, the agent loop starts, and your hooks/tools are live.
Two invariants the registry enforces:
- Declare the matching capability for every contribution. The registry throws for missing
"rules","automationEvents", and"mcp"during those registrations, and it throws when a plugin defineshookswithout the"hooks"capability. Other contribution capability checks may be enforced by host discovery and plugin metadata, so keep the manifest accurate. Bundled skills are the exception -- they are file-based and never go throughapi(see "Bundled Skills" below). - If a plugin defines runtime hooks,
"hooks"must be inmanifest.capabilities. Declaring"hooks"without ahooksobject is allowed but unnecessary.
After validation, registration is one-shot -- no dynamic register/unregister during the session.
The Smallest Working Plugin
import type { AgentPlugin } from "@cline/sdk"
import { createTool } from "@cline/sdk"
const plugin: AgentPlugin = {
name: "hello-plugin",
manifest: {
capabilities: ["tools"],
},
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 pluginThe agent will see say_hello as a callable tool.
The Manifest
manifest: {
capabilities: ["tools", "hooks"], // required, non-empty array
paths?: string[], // optional, multi-entry packages
providerIds?: string[], // optional, provider plugins
modelIds?: string[], // optional, model plugins
}The Complete Capability List
| Capability | What It Unlocks in api |
|---|---|
"tools" | api.registerTool() |
"commands" | api.registerCommand() (slash commands in chat surfaces) |
"rules" | api.registerRule() (string injected into the system prompt) |
"mcp" | api.registerMcpServer() (exposes an MCP server's tools to the agent) |
"messageBuilders" | api.registerMessageBuilder() (rewrites provider-bound messages) |
"providers" | api.registerProvider() (provider contribution metadata) |
"automationEvents" | api.registerAutomationEventType() and ctx.automation?.ingestEvent() |
"hooks" | The runtime hooks object on the plugin (lifecycle callbacks) |
"skills" | No api method. Optional declaration that the plugin ships skills; the host discovers them from the package skills/ directory (see "Bundled Skills"). There is no api.registerSkill(). |
You declare any combination -- most real plugins need 1-3 capabilities.
setup(api, ctx) -- The Registration Phase
setup() runs once per session before the agent loop starts. Everything you register here is frozen for the lifetime of the session.
The api Object
Each register* method should have the matching capability in your manifest:
api.registerTool(tool) // declare "tools"
api.registerCommand({ name, description, handler }) // declare "commands"
api.registerRule({ id, content, source }) // requires "rules"
api.registerMcpServer({ name, transport }) // requires "mcp"
api.registerMessageBuilder({ name, build }) // declare "messageBuilders"
api.registerProvider({ name, description, metadata }) // declare "providers"
api.registerAutomationEventType({ eventType, source }) // requires "automationEvents"There is no api.registerSkill(). Skills are file-based -- you ship them as SKILL.md files in the package and the host discovers them (see "Bundled Skills").
The ctx Object -- Host-Provided Session Context
The second argument carries everything the host knows about the current session. All fields are optional, so feature-detect before using them -- the same plugin must work in hosts that supply less context (unit tests, sandboxed plugin processes).
ctx.session?.sessionId // string, stable core session id
ctx.client?.name // host: "cline-cli", "cline-vscode", etc.
ctx.user // authenticated user/org info, when available
ctx.workspaceInfo // { rootPath, hint, latestGitBranchName,
// latestGitCommitHash, associatedRemoteUrls }
ctx.automation?.ingestEvent // emit normalized automation events
ctx.logger?.log // structured logger scoped to this plugin
ctx.telemetry // ITelemetryService, only present in-processTwo rules about ctx.workspaceInfo:
1. Always prefer ctx.workspaceInfo?.rootPath over process.cwd(). The CLI may have been launched with --cwd without calling chdir, and VS Code workspaces don't share a single CWD. workspaceInfo is sourced from the session config and is always correct. 2. Don't use import.meta.url tricks to find "the workspace". That gives you the plugin's own location, not the user's project.
Persisting State Across Hooks
setup() runs first; hooks fire later. The simplest way to share state is module-level variables:
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") {
// inspect input, optionally block
}
return undefined
},
},
}A single Node process may host multiple sessions concurrently. If your plugin will run in a multi-session host, key your state by ctx.session?.sessionId:
const stateBySession = new Map<string, MyState>()
setup(api, ctx) {
const id = ctx.session?.sessionId
if (id) stateBySession.set(id, /* ... */)
}Runtime Hooks
Runtime hooks are typed in-process callbacks on the same hook layer the runtime uses internally. They run inside the agent loop with full type information -- no IPC, no JSON marshaling.
Declare "hooks" in manifest.capabilities, then add a hooks property:
const plugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
hooks: {
beforeRun(ctx) { /* ... */ },
beforeTool({ toolCall, input }) { /* ... */ },
afterTool({ toolCall, result }) { /* ... */ },
afterRun({ result }) { /* ... */ },
onEvent(event) { /* ... */ },
},
}The Seven Hooks
| Hook | Fires | Can Stop the Loop? | Common Uses |
|---|---|---|---|
beforeRun | Before the runtime loop starts | Yes | Greet, log, attach session metadata |
afterRun | After the runtime loop finishes (success, abort, or fail) | No | Notifications, metrics, persistent logs |
beforeModel | Before each model request | Yes (mutate req) | Inject context, last-mile prompt edits |
afterModel | After each model response, before tool execution | Yes | Block based on model output |
beforeTool | Before each tool execution | Yes ({ stop }) | Audit, redact, block dangerous tools |
afterTool | After each tool execution | Can replace result | Post-process, redact secrets in tool output |
onEvent | On every AgentRuntimeEvent emitted by the runtime | No | Streaming UIs, telemetry pipes |
Stopping the Loop from a Hook
Several hooks return an optional control object. The most common pattern is beforeTool blocking a destructive tool call:
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 // explicit "continue"
}Returning undefined (or omitting return) lets execution continue normally.
afterRun Semantics
afterRun fires for every terminal status -- completed, aborted, failed. If you only want to act on success:
afterRun({ result }) {
if (result.status !== "completed") return
// notify, log success metrics, etc.
}Plugin Hooks vs File Hooks
The runtime supports two hook systems:
- File hooks -- external scripts in
.cline/hooks/invoked with serialized JSON. Right for user/workspace-specific scripts that don't ship with code. - Plugin runtime hooks -- typed in-process callbacks. Right when the behavior belongs to a reusable extension and needs typed access to the runtime.
Core adapts file hooks onto the runtime hook layer, so you don't need both. If you're shipping a plugin, write it as runtime hooks.
Message Builders
Message builders rewrite the provider-bound message list before the model call. They run after runtime messages are converted into SDK message blocks but before core's built-in safety builder.
Use them for:
- Custom compaction policies (replace middle history with a summary).
- Redacting PII or secrets before they reach the provider.
- Reshaping context for a specific model's strengths.
api.registerMessageBuilder({
name: "summarize-middle-history",
build(messages) {
if (estimateTokens(messages) < THRESHOLD) return messages
return [...prefix, summary, ...recent]
},
})Multiple builders run in registration order; the output of one is the input of the next.
When to use beforeModel instead: reach for the beforeModel hook only if you need the runtime snapshot or want to mutate the request object itself. Pure message rewrites belong in a builder.
Automation Events
Plugins can declare normalized event types and emit them into Cline automation. Hosts that don't have automation enabled simply ignore both -- feature-detect 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: { /* JSON Schema for envelope.attributes */ },
})
if (!ctx.automation) return // host has no automation
ctx.automation.ingestEvent({
eventId: "pr-1234",
eventType: "github.pull_request.opened",
source: "github",
subject: "owner/repo#1234",
occurredAt: new Date().toISOString(),
attributes: { /* ... */ },
})
}Slash Commands
Register a slash command with api.registerCommand() (declare "commands"). The handler receives the raw argument string typed after the command and returns a result:
api.registerCommand({
name: "summarize",
description: "Summarize the current diff.",
handler(input) {
// input is the text typed after "/summarize"
return { submitPrompt: `Summarize this diff with focus on: ${input}` }
},
})A command result is either a string or an object:
type AgentExtensionCommandResult =
| string
| {
reply?: string // text shown back to the user
submitPrompt?: string // queue a new prompt for the agent to run
}- Return a string (or
{ reply }) to print a message without invoking the model. - Return
{ submitPrompt }to submit a new prompt to the agent, as if the user had typed it. This is how a command kicks off an actual agent run rather than just replying.
Bundled skills also appear as slash commands automatically -- you do not call registerCommand for them (see "Bundled Skills").
MCP Servers
Declare "mcp" and call api.registerMcpServer() to expose an MCP (Model Context Protocol) server's tools to the agent. The host launches/connects the server and merges its tools into the runtime tool list alongside built-in, custom, and other plugin tools.
const plugin: AgentPlugin = {
name: "github-mcp",
manifest: { capabilities: ["mcp"] },
setup(api) {
api.registerMcpServer({
name: "github",
transport: {
type: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
},
env: {
// Resolve from the host process env; skip the server if unset.
GITHUB_TOKEN: { fromEnv: "GITHUB_TOKEN", required: true },
},
})
},
}Transports
// stdio: spawn a local process
{ type: "stdio", command: string, args?: string[], cwd?: string, env?: Record<string, string> }
// sse: connect to a Server-Sent Events endpoint
{ type: "sse", url: string, headers?: Record<string, string> }
// streamableHttp: connect to a streamable HTTP endpoint
{ type: "streamableHttp", url: string, headers?: Record<string, string> }Env Resolution
Top-level env entries are resolved by the host and merged into the stdio process environment (they are ignored for sse/streamableHttp, which use headers). Each value is either a literal string or a resolver object:
interface AgentExtensionMcpEnvValue {
fromEnv?: string // read this var from the host process env
value?: string // literal fallback when fromEnv is omitted or unset
required?: boolean // skip the whole MCP server if no value resolves
}This keeps secrets out of the plugin source -- ship { fromEnv: "GITHUB_TOKEN", required: true }, not the token itself.
Install-Time OAuth
When a plugin registers an MCP server that supports OAuth, cline plugin install runs an authorization flow during install and stores the resulting tokens in the host MCP settings alongside the registered server (tagged with metadata.source = "plugin"). The end user authorizes once at install time; the plugin source never holds credentials. If authorization fails or is declined, the server is left unauthorized and its tools are unavailable until the user re-authorizes.
Bundled Skills
A plugin package can ship skills -- reusable SKILL.md instruction files that the host discovers from the package and makes available as slash commands. There is no `api.registerSkill()`: skills are purely file-based. You don't register them, and you don't register slash commands for them either -- the runtime discovers them and exposes them automatically.
To bundle skills:
1. Use the package shape (a directory with package.json whose cline.plugins declares the entry). Skill discovery walks from the plugin entry to its owning package root, so a bare single-file plugin dropped in .cline/plugins/ cannot bundle skills this way. 2. Add a skills/ directory at the package root. 3. Put each skill in its own subdirectory containing a SKILL.md:
my-cline-plugin/
+-- package.json
+-- index.ts
+-- skills/
+-- code-review/
| +-- SKILL.md
+-- migrate-db/
+-- SKILL.mdEach SKILL.md is a standard skill file: YAML frontmatter (name, description) plus a markdown body of instructions.
---
name: code-review
description: Review a code change with this project's checklist.
---
# Code Review
Inspect the current diff. Check for missing tests, behavior changes, and
migration risk. Report findings first, then any open questions.Rules and behavior:
- Discovery is file-based and does not require the
"skills"capability -- placing the files is what matters. Declaring"skills"inmanifest.capabilitiesis an optional, recommended signal that the package contributes skills; a plugin can declare"skills"and contribute nothing but skills (nosetup). - Plugin-bundled skills share the same skills executor and slash-command listing as local
.cline/skills/and~/.cline/skills/skills. A skill namedcode-reviewis invokable as/code-review. - A single
SKILL.mdplaced directly inskills/is also discovered; subdirectories are the convention when a package ships more than one skill. - The skill name comes from the
SKILL.mdfrontmatter, not the directory name.
Note: askills/directory holding flat, non-SKILL.mdfiles (as in theagents-squadexample) is plugin-private data the plugin reads itself -- it is not the host skill-discovery mechanism described here. Host-discovered skills must beSKILL.mdfiles.
Loading a Plugin
There are three ways a plugin gets into a session:
Auto-Discovery (CLI)
The CLI scans these directories on startup:
<workspace>/.cline/plugins/-- project-scoped plugins.~/.cline/plugins/-- user-scoped plugins.~/Documents/Cline/Plugins/-- user-scoped plugins in the documents location.
Drop a .ts or .js file in, run cline, done:
mkdir -p .cline/plugins
cp my-plugin.ts .cline/plugins/
cline -i "do the thing my plugin enables"Explicit extensions in SDK Config
When you build your own host with ClineCore, pass the plugin object directly:
import plugin from "./my-plugin"
import { ClineCore } from "@cline/sdk"
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,
enableSpawnAgent: false,
enableAgentTeams: false,
systemPrompt: "You are a helpful assistant.",
extensions: [plugin],
},
prompt: "...",
interactive: false,
})pluginPaths for Directory-Based Plugins
When the plugin is a directory with package.json, point pluginPaths at the directory:
config: {
pluginPaths: ["./path/to/my-plugin-package"],
}Or install with the CLI:
cline plugin install ./path/to/my-plugin-package
cline plugin install @scope/my-cline-plugin # from npm
cline plugin install --git github.com/owner/repo # from gitSingle-File Plugin Template
Save as my-plugin.ts, drop in .cline/plugins/:
import { type AgentPlugin, ClineCore, createTool } from "@cline/sdk"
let sessionRoot: string | undefined
interface DoThingInput {
target: string
}
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: DoThingInput) {
const { target } = input
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)`)
},
},
}
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,
enableSpawnAgent: false,
enableAgentTeams: false,
systemPrompt: "You are a helpful assistant. Use tools when needed.",
extensions: [plugin],
},
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 pluginCopy it, rename the tool, swap in your logic. The runDemo() function lets you test with ANTHROPIC_API_KEY=sk-... bun run my-plugin.ts.
Plugin Package
Use a plugin package when you need npm dependencies, multiple entry points, bundled assets, or npm/git distribution.
Layout
my-cline-plugin/
+-- package.json
+-- tsconfig.json (optional, for local typechecking)
+-- index.ts (the plugin entry point)
+-- README.md
+-- assets/ (optional, bundled content)
+-- templates/
+-- schemas/package.json -- The Discovery Contract
Dependencies under the @cline/ scope are provided by the host runtime. The installer automatically strips these from the plugin's dependency list before running npm install, so declare any @cline/* package your plugin imports as an optional peer dependency.
{
"name": "my-cline-plugin",
"version": "0.1.0",
"private": true,
"description": "What this plugin does, in one sentence.",
"type": "module",
"exports": {
".": "./index.ts"
},
"cline": {
"plugins": [
{
"paths": ["./index.ts"],
"capabilities": ["tools", "hooks"]
}
]
},
"peerDependencies": {
"@cline/sdk": "*"
},
"peerDependenciesMeta": {
"@cline/sdk": { "optional": true }
},
"dependencies": {
"zod": "^4.1.5"
}
}Key fields:
type: "module"-- required. Cline plugins are ES modules.cline.plugins-- the discovery contract. Array of entries withpathspointing at entry files. The exported plugin object's ownmanifest.capabilitiesis still the runtime source of truth.- Bundled skills (optional) -- add a
skills/<name>/SKILL.mdper skill at the package root. There is no manifest field for skills; discovery is file-based (see "Bundled Skills"). Declaring"skills"in the entry'scapabilitiesis a recommended signal but not required for discovery. peerDependenciesfor the@cline/*package your plugin imports -- the host already provides it. Marking it optional lets you typecheck in isolation.dependencies-- any npm package your plugin imports at runtime. These get installed intonode_modulesadjacent to your entry file, and the plugin loader walks up from the entry to resolve them.
Local Dev Loop
You don't have to cline plugin install on every edit. Two iteration patterns:
# 1. Install your deps once
cd my-cline-plugin
npm installThen point the SDK at the directory directly:
await host.start({
config: {
// ...provider/model
pluginPaths: ["./my-cline-plugin"],
},
prompt: "...",
})pluginPaths accepts either an entry file or a package directory (resolveConfiguredPluginModulePaths in @cline/shared/storage). When it gets a directory it reads package.json, follows the cline.plugins paths, and loads each entry. Edit index.ts, restart, repeat -- no install step.
cline plugin install is the right tool for distribution, not for iteration. See "Distributing" below.
Distributing
cline plugin install <source> handles the whole pipeline for recipients: it stages your plugin into ~/.cline/plugins/_installed/<source-type>/, strips host-provided @cline/* deps from package.json, runs npm install --omit=dev --omit=peer inside the install path, and writes a wrapper manifest that points at your entry file. The end user never runs npm install themselves.
Three distribution channels, same install command:
cline plugin install --git github.com/your-org/my-cline-plugin
cline plugin install npm:@your-org/my-cline-plugin
cline plugin install /local/path/to/my-cline-pluginLocal installs copy the directory (skipping .git and node_modules) and then run npm install in the copy, so your local dev node_modules is not what runs in production.
Bundling Assets
Resolve asset paths with import.meta.url, not process.cwd():
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { readFileSync, existsSync } 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
}This is the only place import.meta.url is appropriate in a plugin -- locating files inside the plugin package. For workspace paths, always use ctx.workspaceInfo?.rootPath.
The Override Pattern (Bundled / Global / Project)
A package can ship default assets and let users override them. The convention is a three-tier lookup, last write wins by name:
1. bundled -- files inside the plugin package (defaults shipped with the plugin). 2. global -- files under ~/.cline/data/settings/<kind>/ (user overrides). 3. project -- files under <workspace>/.cline/<kind>/ (project overrides).
Multiple Plugin Entries
If your package exposes more than one plugin, list each in cline.plugins:
"cline": {
"plugins": [
{ "paths": ["./tools-plugin.ts"], "capabilities": ["tools"] },
{ "paths": ["./hooks-plugin.ts"], "capabilities": ["hooks"] }
]
}Each entry file should export default its own plugin object.
Testing Your Plugin
Unit Tests
The plugin object is plain data. Drive setup() against a minimal context and exercise tools directly:
import plugin from "../my-plugin"
const tools: unknown[] = []
type PluginSetup = NonNullable<typeof plugin.setup>
const api: Parameters<PluginSetup>[0] = {
registerTool: (t: unknown) => tools.push(t),
registerCommand: () => {},
registerRule: () => {},
registerMcpServer: () => {},
registerMessageBuilder: () => {},
registerProvider: () => {},
registerAutomationEventType: () => {},
}
await plugin.setup?.(api, {
workspaceInfo: { rootPath: "/tmp/fake-workspace" },
})
// Now `tools` contains the registered tools -- call tool.execute(input, ctx)End-to-End with runDemo()
Add a runDemo() in your plugin file (see the single-file template above) that boots a real ClineCore session:
ANTHROPIC_API_KEY=sk-... bun run my-plugin.tsCLI Smoke Test
mkdir -p .cline/plugins
cp my-plugin.ts .cline/plugins/
cline -i "trigger something that exercises the plugin"For packages:
cline plugin install ./my-cline-plugin
cline -i "..."If the plugin fails validation or setup, the CLI prints a clear error and continues without it.
Common Gotchas
- "capabilities must be a non-empty array" -- you forgot
manifest.capabilities, or it's[]. - "registerRule requires the 'rules' capability" -- capability/handler drift. Add
"rules"to capabilities, or stop callingregisterRule. - "registerAutomationEventType requires the 'automationEvents' capability" -- add
"automationEvents"to capabilities, or stop registering automation event types. - Plugin tool not visible to the model -- check that the plugin declares
"tools"before callingapi.registerTool(), that the plugin loaded successfully, and that global tool settings have not disabled the tool.enableToolscontrols the default built-in suite, not whether plugin tools can be registered. - MCP server tools not appearing -- declare
"mcp"before callingapi.registerMcpServer(), confirm the transport command/url is correct, and verify anyrequiredenv values resolve (an unmetrequiredvalue skips the server). For OAuth servers, confirm authorization completed duringcline plugin install. - Bundled skill not discovered -- bundled skills require the package shape; a single-file plugin in
.cline/plugins/cannot ship them. Confirm files are at<package>/skills/<name>/SKILL.md(namedSKILL.md, not<name>.md), the packagepackage.jsondeclares the entry incline.plugins, and the frontmatter has a non-emptyname. There is noregisterSkillcall to add. ctx.workspaceInfois undefined in unit tests -- your test did not pass setup context. In ClineCore sessions, passcwdorworkspaceRootso core can derive workspace metadata.- State leaking across sessions -- module-level variables are shared across sessions in the same process. Key by
ctx.session?.sessionIdif your host runs multiple sessions concurrently. afterRunfiring on aborts -- guard withif (result.status !== "completed") return.- Heavy work in
setup()--setup()blocks session start. Defer expensive work into the first tool call orbeforeRun. - Importing host internals -- import public SDK APIs from
@cline/sdkor@cline/core. Reaching into host-specific packages, such as CLI internals, will break in non-CLI hosts. - Sandboxed plugins and
telemetry-- telemetry is process-local. Feature-detectctx.telemetryand expect it to be undefined in sandboxed plugin processes. - Resolving bundled assets -- use
import.meta.url+fileURLToPathto find files inside your package; neverprocess.cwd(). For workspace paths, do the opposite: usectx.workspaceInfo?.rootPath, neverimport.meta.url. - Plugin name collisions --
namemust be unique within a session. If two plugins share a name, validation fails. Namespace by package (my-org-redactor, notredactor). Cannot find module 'xxx'from a single-file plugin -- you reached for an npm dep from a.tsdropped in.cline/plugins/. Single-file plugins can only import Node builtins and@cline/*. Convert to a package: make a directory, addpackage.jsonwithdependencies,npm install, then pointpluginPathsat the directory (orcline plugin installit).
Decision Guide -- Which Extension Point?
| You want to... | Use |
|---|---|
| Give the model a new capability | registerTool |
| Expose an MCP server's tools | registerMcpServer (declare "mcp") |
Ship reusable instructions users invoke as /commands | Bundle skills/<name>/SKILL.md (no API call) |
| Add a slash command in chat surfaces | registerCommand |
| Submit a follow-up prompt from a command | registerCommand handler returning { submitPrompt } |
| Inject text into the system prompt | registerRule |
| Rewrite messages before they hit the provider | registerMessageBuilder |
| Add provider contribution metadata | registerProvider |
| Emit normalized cron/webhook events | registerAutomationEventType + ctx.automation |
| Observe or steer the agent loop | hooks.* |
| Block a dangerous tool call | hooks.beforeTool returning { stop: true } |
| Notify on completion | hooks.afterRun (gate on status === "completed") |
| Tweak each model request | hooks.beforeModel |
| Stream events to a UI | hooks.onEvent |
| Ship reusable templates with the plugin | Bundle assets next to index.ts, resolve via import.meta.url |
| Let users override defaults globally or per-project | Three-tier lookup: bundled / global / project |
Pre-Ship Checklist
manifest.capabilitiesis a non-empty array.- Every
api.register*call has a matching capability declared (registerMcpServerneeds"mcp"). - If
hooksis present,"hooks"is incapabilities. - (Bundled skills) Skills live at
<package>/skills/<name>/SKILL.mdwith non-emptyname/descriptionfrontmatter; the package shape is used (single-file plugins cannot bundle skills). Declaring"skills"is recommended but not required. - (MCP) Secrets are resolved via
env{ fromEnv }, not hardcoded;requiredis set on values the server cannot run without. ctx.workspaceInfo?.rootPathis used for workspace paths (notprocess.cwd()).- Optional
ctxfields are feature-detected. - Tool names are snake_case verbs; descriptions are written for the model.
- Tool inputs have JSON Schema with
requiredset. afterRunhandlers gate onresult.status === "completed"if they only want successes.- State that must not leak between concurrent sessions is keyed by
ctx.session?.sessionId. - (Package)
package.jsonhastype: "module",cline.plugins, and whichever@cline/*package you import as an optional peer dependency. - (Package) Bundled assets resolved via
import.meta.url, notprocess.cwd(). - Smoke test: drop the plugin into
.cline/plugins/(orcline plugin install), runcline -i "...", watch it work.
Plugin Examples from SDK
The SDK repo includes these example plugins:
| Plugin | Description |
|---|---|
weather-metrics.ts | Tool registration + lifecycle metrics |
mac-notify.ts | macOS Notification Center alerts |
custom-compaction.ts | Custom message compaction via message builders |
background-terminal.ts | Detached shell job management |
automation-events.ts | Plugin-emitted automation events |
gitignore-read-files-guard.ts | File access policy enforcement via beforeTool |
web-search.ts | Web search via Exa API |
typescript-lsp/ | TypeScript Language Service tools (plugin package) |
agents-squad/ | Multi-agent team orchestration (plugin package) |
See Also
../tools/REFERENCE.md- Tool creation../events/REFERENCE.md- Event system../agent/REFERENCE.md- Using plugins with Agent../clinecore/REFERENCE.md- Using plugins with ClineCore
Going to Production
Guidelines for deploying Cline SDK agents in production environments.
Error Handling
Always check the result status:
const result = await agent.run(input)
switch (result.status) {
case "completed":
console.log("Success:", result.outputText)
break
case "aborted":
console.log("Cancelled:", result.error?.message)
break
case "failed":
console.error("Failed:", result.error)
break
}For ClineCore, check finishReason:
const session = await cline.start({ ... })
switch (session.result?.finishReason) {
case "completed":
// normal completion
break
case "max_iterations":
// agent hit iteration limit
break
case "aborted":
// manually cancelled
break
case "mistake_limit":
// too many tool errors
break
case "error":
// unrecoverable error
break
}Cost Control
Token Limits
Set maximum tokens per turn and iteration limits:
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
modelOptions: { maxTokens: 4096 },
maxIterations: 10,
tools: [...],
})Model Selection
Use cheaper models for simple tasks:
// Simple classification or formatting
{ providerId: "anthropic", modelId: "claude-haiku-4-5" }
// Complex reasoning and code generation
{ providerId: "anthropic", modelId: "claude-sonnet-4-6" }
// Hardest tasks requiring deep reasoning
{ providerId: "anthropic", modelId: "claude-opus-4-7" }Usage Tracking
Monitor spending in real time:
agent.subscribe((event) => {
if (event.type === "usage-updated" && event.usage.totalCost) {
if (event.usage.totalCost > MAX_BUDGET) {
agent.abort("Budget exceeded")
}
}
})Observability
OpenTelemetry Integration
The SDK can emit telemetry through an injected ITelemetryService. ClineCore.create() does not create OpenTelemetry telemetry by itself; construct a telemetry service and pass it in:
import {
ClineCore,
createClineTelemetryServiceConfig,
createConfiguredTelemetryHandle,
} from "@cline/sdk"
const telemetryConfig = createClineTelemetryServiceConfig({
enabled: true,
serviceName: "my-agent-service",
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
logsExporter: "otlp",
metricsExporter: "otlp",
tracesExporter: "otlp",
metadata: {
extension_version: "1.0.0",
cline_type: "sdk-app",
platform: process.platform,
platform_version: process.version,
os_type: process.platform,
os_version: process.version,
},
})
const telemetryHandle = createConfiguredTelemetryHandle(telemetryConfig)
const cline = await ClineCore.create({
clientName: "my-app",
telemetry: telemetryHandle.telemetry,
})
process.on("SIGTERM", async () => {
await cline.dispose("SIGTERM")
await telemetryHandle.dispose()
process.exit(0)
})Structured Logging
Use the BasicLogger interface for injectable logging:
import type { BasicLogger } from "@cline/sdk"
const logger: BasicLogger = {
debug: (msg, meta) => console.debug(msg, meta),
log: (msg, meta) => console.log(msg, meta),
error: (msg, meta) => console.error(msg, meta),
}
await cline.start({
config: {
logger,
// ...
},
})Custom Metrics via Plugins
const metricsPlugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
setup() {},
hooks: {
beforeRun() {
metrics.increment("agent.runs.started")
},
afterRun({ result }) {
metrics.increment("agent.runs.completed")
metrics.histogram("agent.iterations", result.iterations)
metrics.histogram("agent.tokens.output", result.usage.outputTokens)
},
beforeTool({ toolCall }) {
metrics.increment(`agent.tools.${toolCall.toolName}`)
},
},
}Security
Sandbox Tool Execution
Validate tool inputs to prevent path traversal and injection:
execute: async (input) => {
const safePath = path.resolve(WORKSPACE_ROOT, input.path)
if (!safePath.startsWith(WORKSPACE_ROOT)) {
return { error: "Path traversal attempt blocked" }
}
return await readFile(safePath, "utf-8")
}API Key Management
- Use environment variables, never hardcode keys
- Rotate keys regularly
- Use different keys for development and production
{
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY, // never a literal string
}Tool Policy Hardening
Disable tools you don't need before model requests and require approval for dangerous ones:
await cline.start({
prompt: "...",
config: {
...config,
toolPolicies: {
fetch_web_content: { enabled: false }, // removed before model requests
},
},
toolPolicies: {
read_files: { autoApprove: true },
search_codebase: { autoApprove: true },
run_commands: { autoApprove: false }, // require approval
editor: { autoApprove: false },
apply_patch: { autoApprove: false },
},
})Deployment Patterns
Stateless Worker
For request/response workloads (API endpoints, queue consumers):
const cline = await ClineCore.create({
clientName: "worker",
backendMode: "local",
})
app.post("/agent", async (req, res) => {
const session = await cline.start({
prompt: req.body.prompt,
config: { ... },
})
res.json({ text: session.result?.text, usage: session.result?.usage })
})Persistent Service
For long-running services with session management:
const cline = await ClineCore.create({
clientName: "service",
backendMode: "hub",
})
process.on("SIGTERM", async () => {
await cline.dispose("SIGTERM")
process.exit(0)
})Scheduled Automation
See ../scheduling/REFERENCE.md for recurring agent tasks.
Retry and Resilience
createTool()storesretryableandmaxRetriesmetadata, but the direct Agent runtime does not automatically retry failed custom tool executions today- Built-in ClineCore tools and provider handlers have their own timeout and retry behavior where implemented
- Implement retries inside custom tool
executefunctions when the operation is idempotent - Implement provider-level retries or fallback model selection in your host when your reliability target requires it
- Use
context.signaland an in-tool timeout for long-running custom tools - Monitor
mistake_limitfinish reason to detect systematic tool failures
See Also
../agent/REFERENCE.md- Agent overview../clinecore/REFERENCE.md- ClineCore overview../tools/REFERENCE.md- Tool configuration../plugins/REFERENCE.md- Metrics plugins../scheduling/REFERENCE.md- Scheduled agents
Model Providers
The Cline SDK provider layer lives in @cline/llms. The @cline/sdk package re-exports it as the Llms namespace through @cline/core.
Supported Providers
| Provider ID | Models |
|---|---|
"anthropic" | Claude Opus 4.7, Sonnet 4.6, Haiku 4.5 |
"openai-native" | OpenAI API models |
"openai-codex" | OpenAI ChatGPT subscription models through OAuth |
"openai-codex-cli" | Local Codex CLI provider |
"gemini" | Google Gemini API models |
"vertex" | Google Vertex AI models |
"bedrock" | AWS Bedrock models |
"mistral" | Mistral models |
"openai-compatible" | Generic OpenAI-compatible endpoint |
"openrouter", "cline", "deepseek", "xai", "together", "fireworks", "groq", "cerebras", "sambanova", "nebius", "baseten", "requesty", "litellm", "ollama", "lmstudio" | Common built-in compatible provider presets |
"huggingface", "vercel-ai-gateway", "v0", "aihubmix", "hicap", "nousResearch", "huawei-cloud-maas", "qwen", "qwen-code", "doubao", "zai", "zai-coding-plan", "moonshot", "wandb", "xiaomi", "kilo", "asksage", "minimax" | Additional built-in compatible provider presets |
"claude-code", "opencode", "dify", "oca", "sapaicore" | Additional built-in provider families and integrations |
Provider and model catalogs change over time. Prefer Llms.getProviderIds(), Llms.getProvider(id), and Llms.getModelsForProvider(id) over hardcoding lists in generated code.
Basic Configuration
With Agent
import { Agent } from "@cline/sdk"
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant.",
tools: [],
})With ClineCore
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
prompt: "Hello",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
cwd: process.cwd(),
systemPrompt: "You are a helpful assistant.",
enableTools: false,
enableSpawnAgent: false,
enableAgentTeams: false,
},
})Provider-Specific Configuration
Anthropic
{
providerId: "anthropic",
modelId: "claude-opus-4-7", // or "claude-sonnet-4-6", "claude-haiku-4-5"
apiKey: process.env.ANTHROPIC_API_KEY,
}OpenAI
{
providerId: "openai-native",
modelId: "gpt-5.4",
apiKey: process.env.OPENAI_API_KEY,
}Google (Gemini)
{
providerId: "gemini",
modelId: "gemini-3.1-pro-preview",
apiKey: process.env.GOOGLE_API_KEY,
}Google (Vertex AI)
{
providerId: "vertex",
modelId: "gemini-3.1-pro-preview",
// Uses application default credentials or service account
}AWS Bedrock
{
providerId: "bedrock",
modelId: "anthropic.claude-sonnet-4-6",
// Uses AWS credential chain (env vars, config file, IAM role)
// Set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
}Mistral
{
providerId: "mistral",
modelId: "mistral-large-latest",
apiKey: process.env.MISTRAL_API_KEY,
}OpenAI-Compatible
For any provider with an OpenAI-compatible API:
{
providerId: "openai-compatible",
modelId: "my-model",
apiKey: process.env.API_KEY,
baseUrl: "https://api.together.xyz/v1",
}Works with: vLLM, Together AI, Fireworks, Groq, Ollama, LiteLLM, etc.
Custom Base URL
Override the API endpoint for any provider:
{
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.API_KEY,
baseUrl: "https://my-proxy.example.com/v1",
}Custom Headers
Pass additional headers to API requests:
{
providerId: "openai-native",
modelId: "gpt-5.4",
apiKey: process.env.API_KEY,
headers: {
"X-Custom-Header": "value",
},
}Gateway API
For advanced multi-provider setups, use the Gateway directly:
import { Agent, Llms } from "@cline/sdk"
const gateway = Llms.createGateway({
providerConfigs: [
{ providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
{ providerId: "openai-native", apiKey: process.env.OPENAI_API_KEY },
],
})
// Create a model for a specific provider
const model = gateway.createAgentModel({
providerId: "anthropic",
modelId: "claude-opus-4-7",
})
// Use with Agent
const agent = new Agent({ model, systemPrompt: "...", tools: [] })Gateway Methods
gateway.registerProvider(registration) // add a custom provider
gateway.configureProvider(config) // update provider settings
gateway.listProviders() // list available providers
gateway.listModels(providerId?) // list available models
gateway.createAgentModel(selection) // create model for agent
gateway.stream(request) // raw streaming (AsyncIterable)Provider Registry
Query and register providers programmatically:
import { Llms } from "@cline/sdk"
// List all registered providers
const providers = await Llms.getAllProviders()
// Get models for a provider
const models = await Llms.getModelsForProvider("anthropic")
// Register a custom provider in the model catalog
Llms.registerProvider({
provider: {
id: "my-provider",
name: "My Custom Provider",
defaultModelId: "my-model",
client: "custom",
source: "file",
},
models: {
"my-model": {
id: "my-model",
name: "My Model",
contextWindow: 128000,
},
},
})Model Metadata
Access model info (context window, pricing, capabilities):
import { Llms } from "@cline/sdk"
const models = await Llms.getModelsForProvider("anthropic")
for (const [modelId, model] of Object.entries(models)) {
console.log(`${modelId}: context=${model.contextWindow}, input=$${model.pricing?.input}/MTok`)
}Cost Tracking
Track per-request and cumulative costs:
// Via events
agent.subscribe((event) => {
if (event.type === "usage-updated") {
console.log(`Cost: $${event.usage.totalCost?.toFixed(4)}`)
}
})
// Via result
const result = await agent.run("...")
console.log(`Total cost: $${result.usage.totalCost?.toFixed(4)}`)
// Via ClineCore accumulated usage
const usage = await cline.getAccumulatedUsage(sessionId)See Also
../agent/REFERENCE.md- Using providers with Agent../clinecore/REFERENCE.md- Using providers with ClineCore../production/REFERENCE.md- Cost control in production