chore: rename example folders; fix references (#60)

* rename folders

* fix examples reorg references

* move over files

* having agent fixing and validating all examples are still working

* fix sidecar paths

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
This commit is contained in:
Renee Huang
2026-05-07 20:38:04 -07:00
committed by GitHub
co-authored by abeatrix
parent 2b2ff9667d
commit e215d3d91c
438 changed files with 372 additions and 975 deletions
+269
View File
@@ -0,0 +1,269 @@
# Cline Custom Plugin Example
Shows how to author a reusable plugin module that works in both the SDK and the CLI. A plugin can:
- **Register tools** — give the agent new capabilities it can invoke
- **Hook into the lifecycle** — observe or influence execution at key points
- **Rewrite provider messages** — add custom context compaction before the model call
- **Emit automation events** — normalize plugin-owned events into ClineCore automation
Example plugins:
- [weather-plugin.example.ts](./weather-plugin.example.ts) - weather tool plus lifecycle metrics hooks
- [mac-notify.ts](./mac-notify.ts) - macOS Notification Center alert on successful run completion
- [automation-events.ts](./automation-events.ts) - local plugin-emitted automation event example
- [custom-compaction.ts](./custom-compaction.ts) - custom summary-based message compaction
- [../hooks/custom-compaction-hook.example.ts](../hooks/custom-compaction-hook.example.ts) - equivalent compaction using a runtime `beforeModel` hook
- [background-terminal.ts](./background-terminal.ts) - detached background shell jobs with persisted logs and optional session steering
## Use It With The CLI
The CLI does not have a `--plugin` flag yet. It discovers plugin modules from `.cline/plugins` in the workspace.
```bash
mkdir -p .cline/plugins
cp examples/plugins/weather-plugin.example.ts .cline/plugins/weather-metrics.ts
cline -i "What's the weather like in Tokyo and Paris?"
```
The module exports `default` and `plugin`, so the CLI loader can import it directly.
To send a macOS Notification Center alert when a run completes successfully:
```bash
mkdir -p .cline/plugins
cp examples/plugins/mac-notify.ts .cline/plugins/mac-notify.ts
cline -i "Run the test suite"
```
The notification example uses the `afterRun` hook and `/usr/bin/osascript`. macOS may ask you to allow notifications for the terminal or host process the first time it fires.
To add custom provider-message compaction before each model call:
```bash
mkdir -p .cline/plugins
cp examples/plugins/custom-compaction.ts .cline/plugins/custom-compaction.ts
cline -i "Search the codebase for dispatcher usage, then summarize it"
```
To add background shell jobs that keep running after the tool call returns:
```bash
mkdir -p .cline/plugins
cp examples/plugins/background-terminal.ts .cline/plugins/background-terminal.ts
cline -i "Start the dev server in the background, then continue with the next task"
```
The background terminal plugin registers three tools:
| Tool | Purpose |
| ---- | ------- |
| `start_background_command` | starts a detached shell command, returns a job id immediately, and stores stdout/stderr under Cline's data directory |
| `get_background_command` | reads job status plus recent stdout/stderr tails |
| `delete_background_command` | deletes saved job metadata, and optionally deletes captured logs |
When `notifyParent` is true or omitted, the plugin emits a `steer_message`
through the host bridge after the command exits. That pushes a completion
summary back into the active session, so the agent can react to long-running
commands without blocking the original tool call.
## Run The Demo Directly
```bash
ANTHROPIC_API_KEY=sk-... bun run examples/plugins/weather-plugin.example.ts
```
## How it works
A plugin is a plain object with four parts:
```ts
const myPlugin: AgentPlugin = {
// 1. Identity
name: "my-plugin",
// 2. Manifest — declare what the plugin does
manifest: {
capabilities: ["tools", "hooks"],
},
// 3. Setup — register tools, commands, etc.
setup(api, ctx) {
api.registerTool(createTool({ ... }));
},
// 4. Runtime hooks — observe or influence agent execution
hooks: {
beforeRun({ snapshot }) { ... },
afterRun({ result }) { ... },
},
};
```
Then pass it to the agent:
```ts
import plugin from "./weather-plugin.example";
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(),
mode: "act",
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
systemPrompt: "You are a helpful assistant. Use tools when needed.",
extensions: [plugin],
},
prompt: "What's the weather like in Tokyo and Paris?",
interactive: false,
});
```
## Available capabilities
| Capability | What it unlocks |
| ------------------ | -------------------------------------------- |
| `tools` | `api.registerTool()` |
| `commands` | `api.registerCommand()` |
| `providers` | `api.registerProvider()` |
| `messageBuilders` | `api.registerMessageBuilder()` |
| `automationEvents` | `api.registerAutomationEventType()` and `ctx.automation?.ingestEvent()` |
| `hooks` | runtime lifecycle hook handlers (see below) |
## Automation event plugins
Plugins can contribute normalized automation event types and, when running in a
`ClineCore` host with automation enabled, emit events through setup context:
```ts
const plugin: AgentPlugin = {
name: "local-events",
manifest: { capabilities: ["automationEvents"] },
setup(api, ctx) {
api.registerAutomationEventType({
eventType: "local.plugin_event",
source: "local-plugin",
description: "Local normalized event emitted by a plugin",
});
void ctx.automation?.ingestEvent({
eventId: `local-plugin-${Date.now()}`,
eventType: "local.plugin_event",
source: "local-plugin",
occurredAt: new Date().toISOString(),
});
},
};
```
The setup context can also include `session`, `client`, `user`, `workspaceInfo`,
`logger`, and `telemetry` when provided by the host. See
[`automation-events.ts`](./automation-events.ts) for a local timer-based demo.
## Runtime Hooks
Plugins use the same runtime hook names as `@cline/agents`. These are
in-process callbacks, not file hook event names:
- Runtime hooks: typed in-process plugin/agent lifecycle callbacks such as
`beforeRun`, `beforeModel`, and `afterTool`.
- File hooks: external scripts discovered from hook config directories and run
with serialized JSON payloads.
- Hook events: serialized payload names used by file hooks, such as
`agent_end`, `tool_call`, and `prompt_submit`.
| Hook | When it fires |
| ------------- | -------------------------------------------------- |
| `beforeRun` | before the runtime loop starts |
| `afterRun` | after the runtime loop finishes |
| `beforeModel` | before each model request |
| `afterModel` | after each model response, before tool execution |
| `beforeTool` | before each tool execution |
| `afterTool` | after each tool execution |
| `onEvent` | on every `AgentRuntimeEvent` emitted by the runtime |
`beforeRun` and `afterRun` wrap one `run()` / `continue()` invocation. In an
interactive session, that maps to one submitted user turn. `afterRun` is the
right plugin hook for task completion notifications, but it also fires for
aborted and failed runs, so check `result.status === "completed"` when you only
want successful completion. The equivalent file-hook event is `agent_end`.
## Runtime hooks vs file hooks
File hooks are external scripts discovered from hook config directories such as
`.cline/hooks`. They use serialized event names from `@cline/shared`, while
plugin runtime hooks use the typed in-process runtime lifecycle names above.
Core adapts file hooks onto the runtime hook layer before executing the scripts.
| File hook file name | File hook event | Plugin runtime hook backing it |
| ------------------- | --------------- | ------------------------------ |
| `TaskStart` | `agent_start` | `beforeRun` |
| `TaskResume` | `agent_resume` | `beforeRun` with resume context |
| `UserPromptSubmit` | `prompt_submit` | `beforeRun` plus submitted prompt context |
| `PreToolUse` | `tool_call` | `beforeTool` |
| `PostToolUse` | `tool_result` | `afterTool` |
| `TaskComplete` | `agent_end` | `afterRun` when completed |
| `TaskError` | `agent_error` | `afterRun` when failed |
| `TaskCancel` | `agent_abort` | `afterRun` or session shutdown with abort/cancel reason |
| `SessionShutdown` | `session_shutdown` | session cleanup / runtime shutdown |
| `PreCompact` | not wired for file hooks today | none |
Use file hooks for user/workspace-configured scripts. Use plugin runtime hooks
when the behavior belongs to a reusable extension and needs typed access to the
runtime snapshot, model request, tool context, or emitted runtime events.
For custom message compaction, use a plugin runtime hook such as `beforeModel`
or the `messageBuilders` API. That is separate from the serialized
`pre_compact` hook-event payload type, and `PreCompact` files are not currently
wired into file-hook execution.
### Naming note
The current public plugin field is `hooks` because it is the runtime-native
extension field consumed by the agent. In user-facing docs and examples, call
these **runtime hooks** to avoid confusing them with file hook events. If the
plugin API is renamed before the SDK has external consumers, prefer
`runtimeHooks` for `AgentPlugin` and reserve **file hooks** for the external
script/event system.
## Custom message compaction
Use `messageBuilders` when a plugin needs to transform the provider-bound
message list before the model call. Message builders run after runtime messages
are converted into SDK message blocks and before the built-in API safety pass,
so core still applies final provider-safe truncation afterward.
See [`custom-compaction.ts`](./custom-compaction.ts) for a full
plugin that estimates context size, preserves the first user message and recent
working context, and replaces older middle history with one continuation summary.
There is also a runtime-hook version at
[`../hooks/custom-compaction-hook.example.ts`](../hooks/custom-compaction-hook.example.ts).
Both examples perform similar compaction, but they run at different layers:
| Example | Extension point | Message shape | Best for |
| ------- | --------------- | ------------- | -------- |
| `custom-compaction.ts` | `api.registerMessageBuilder()` | SDK/provider-bound `Message[]` after runtime messages are converted for model delivery | most reusable plugin-owned message rewrites and compaction policies |
| `../hooks/custom-compaction-hook.example.ts` | `hooks.beforeModel` runtime hook | Agent runtime request messages with runtime parts such as `tool-call`, `tool-result`, `reasoning`, `image`, and `file` | cases that need runtime-hook context, the current runtime snapshot, or direct request mutation |
Prefer the message-builder version for normal plugin-owned compaction because
it runs in the core message pipeline before the built-in provider-safety
builder. Use the `beforeModel` runtime-hook version when the logic needs access
to runtime hook context or the exact runtime request object.
Notes:
- message builders receive and return `Message[]`
- builders may be sync or async
- multiple builders run in plugin registration order
- the built-in core message builder runs last to normalize input and enforce provider-safe truncation
- use `beforeModel` hooks for runtime request changes; use message builders for message-list rewrites
+91
View File
@@ -0,0 +1,91 @@
/**
* Automation Event Plugin Example
*
* Shows how a plugin can declare normalized event types and emit events into
* ClineCore automation without importing cron internals.
*
* Local demo:
* mkdir -p .cline/plugins .cline/cron/events
* cp examples/plugins/automation-events.ts .cline/plugins/automation-events.ts
* cp examples/cron/events/local-plugin-event.event.md .cline/cron/events/local-plugin-event.event.md
* perl -0pi -e "s#/absolute/path/to/repo#$PWD#g" .cline/cron/events/local-plugin-event.event.md
* CLINE_LOCAL_EVENT_INTERVAL_MS=2000 cline -i "wait for the plugin event"
*/
import type { AgentPlugin } from "@cline/core";
const stopLocalEmitters = new Map<string, () => void>();
function emitterKey(sessionId: string | undefined): string | undefined {
return sessionId?.trim() || undefined;
}
export const plugin: AgentPlugin = {
name: "local-automation-events",
manifest: {
capabilities: ["automationEvents"],
},
setup(api, ctx) {
api.registerAutomationEventType({
eventType: "local.plugin_event",
source: "local-plugin",
description: "Local normalized event emitted by a plugin",
attributesSchema: {
type: "object",
properties: {
topic: { type: "string" },
},
required: ["topic"],
},
examples: [
{
eventId: "local-plugin-demo-1",
eventType: "local.plugin_event",
source: "local-plugin",
subject: "plugin-demo",
occurredAt: "2026-04-24T10:00:00.000Z",
attributes: { topic: "plugin-demo" },
},
],
});
ctx.logger?.log("local automation event source registered", {
sessionId: ctx.session?.sessionId,
client: ctx.client?.name,
});
const intervalMs = Number(process.env.CLINE_LOCAL_EVENT_INTERVAL_MS ?? 0);
if (!ctx.automation || !Number.isFinite(intervalMs) || intervalMs <= 0) {
return;
}
const key = emitterKey(ctx.session?.sessionId);
if (!key) {
ctx.logger?.log(
"local automation event emitter disabled; setup context has no session id",
{ severity: "warn" },
);
return;
}
stopLocalEmitters.get(key)?.();
const timer = setInterval(() => {
void ctx.automation?.ingestEvent({
eventId: `local-plugin-${Date.now()}`,
eventType: "local.plugin_event",
source: "local-plugin",
subject: "plugin-demo",
occurredAt: new Date().toISOString(),
dedupeKey: "local-plugin:plugin-demo",
attributes: { topic: "plugin-demo" },
payload: {
message: "Hello from a plugin-emitted automation event.",
},
});
}, intervalMs);
stopLocalEmitters.set(key, () => clearInterval(timer));
},
};
export default plugin;
+432
View File
@@ -0,0 +1,432 @@
import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import {
appendFileSync,
existsSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import {
type AgentPlugin,
type AgentToolContext,
createTool,
} from "@cline/core";
/**
* Background Terminal Plugin Example
*
* Starts shell commands in detached background processes, stores stdout/stderr
* under Cline's data directory, and optionally steers a completion summary back
* into the current session when the command exits.
*
* CLI usage:
* mkdir -p .cline/plugins
* cp examples/plugins/background-terminal.ts .cline/plugins/background-terminal.ts
* cline -i "Start the dev server in the background and keep working"
*/
type JobStatus = "running" | "completed" | "failed";
type JobRecord = {
jobId: string;
command: string;
cwd: string;
shell: string;
startedAt: string;
completedAt?: string;
status: JobStatus;
exitCode?: number | null;
signal?: string | null;
notifyParent: boolean;
sessionId?: string;
pid?: number;
stdoutPath: string;
stderrPath: string;
metaPath: string;
};
interface ClinePluginHost {
emitEvent?: (name: string, payload?: unknown) => void;
}
declare global {
var __clinePluginHost: ClinePluginHost | undefined;
}
const DEFAULT_SHELL = process.env.SHELL || "/bin/zsh";
const CLINE_DATA_DIR =
process.env.CLINE_DATA_DIR || join(homedir(), ".cline", "data");
const JOBS_DIR = join(CLINE_DATA_DIR, "plugins", "background-shell", "jobs");
let sessionDefaultCwd = process.cwd();
let setupSessionId: string | undefined;
function ensureJobsDir() {
mkdirSync(JOBS_DIR, { recursive: true });
}
function asObject(value: unknown): Record<string, unknown> {
return value && typeof value === "object"
? (value as Record<string, unknown>)
: {};
}
function requireString(value: unknown, field: string) {
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${field} must be a non-empty string`);
}
return value;
}
function optionalString(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function optionalBoolean(value: unknown) {
return typeof value === "boolean" ? value : undefined;
}
function optionalInt(value: unknown) {
return typeof value === "number" && Number.isInteger(value)
? value
: undefined;
}
function resolveToolSessionId(context: AgentToolContext): string | undefined {
return context.sessionId?.trim() || setupSessionId;
}
function jobDir(jobId: string) {
return join(JOBS_DIR, jobId);
}
function metaPath(jobId: string) {
return join(jobDir(jobId), "job.json");
}
function stdoutPath(jobId: string) {
return join(jobDir(jobId), "stdout.log");
}
function stderrPath(jobId: string) {
return join(jobDir(jobId), "stderr.log");
}
function readTextIfExists(path: string) {
return existsSync(path) ? readFileSync(path, "utf8") : "";
}
function tail(text: string, lineCount: number) {
const lines = text.split(/\r?\n/);
return lines
.slice(Math.max(0, lines.length - lineCount))
.join("\n")
.trim();
}
function writeJob(record: JobRecord) {
ensureJobsDir();
mkdirSync(jobDir(record.jobId), { recursive: true });
writeFileSync(
record.metaPath,
`${JSON.stringify(record, null, 2)}\n`,
"utf8",
);
}
/** @returns {JobRecord} */
function readJob(jobId: string) {
const path = metaPath(jobId);
if (!existsSync(path)) {
throw new Error(`Unknown background command job: ${jobId}`);
}
return /** @type {JobRecord} */ (JSON.parse(readFileSync(path, "utf8")));
}
function formatCompletionMessage(record: JobRecord) {
const stdout = tail(readTextIfExists(record.stdoutPath), 80);
const stderr = tail(readTextIfExists(record.stderrPath), 80);
const statusLine =
record.status === "completed"
? `Background command completed successfully (exit ${record.exitCode ?? 0}).`
: `Background command failed (exit ${record.exitCode ?? "unknown"}${record.signal ? `, signal ${record.signal}` : ""}).`;
return [
"Background shell job finished.",
`Job ID: ${record.jobId}`,
`Command: ${record.command}`,
`CWD: ${record.cwd}`,
statusLine,
stdout ? `STDOUT:\n${stdout}` : "STDOUT: <empty>",
stderr ? `STDERR:\n${stderr}` : "STDERR: <empty>",
].join("\n\n");
}
function emitSteer(sessionId: string | undefined, prompt: string) {
if (!sessionId || !prompt.trim()) {
return;
}
globalThis.__clinePluginHost?.emitEvent?.("steer_message", {
sessionId,
prompt,
});
}
function startCommand(
command: string,
cwd: string,
shell: string,
notifyParent: boolean,
sessionId: string | undefined,
) {
ensureJobsDir();
const jobId = randomUUID();
mkdirSync(jobDir(jobId), { recursive: true });
const outPath = stdoutPath(jobId);
const errPath = stderrPath(jobId);
const child = spawn(shell, ["-lc", command], {
cwd,
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env: process.env,
});
const record: JobRecord = {
jobId,
command,
cwd,
shell,
startedAt: new Date().toISOString(),
status: "running",
notifyParent,
sessionId,
pid: child.pid,
stdoutPath: outPath,
stderrPath: errPath,
metaPath: metaPath(jobId),
};
writeJob(record);
child.stdout?.on("data", (chunk) => {
appendFileSync(outPath, chunk);
});
child.stderr?.on("data", (chunk) => {
appendFileSync(errPath, chunk);
});
child.on("error", (error) => {
const current = readJob(jobId);
const updated = {
...current,
status: /** @type {JobStatus} */ ("failed"),
completedAt: new Date().toISOString(),
exitCode: null,
signal: null,
};
appendFileSync(errPath, `${error.message}\n`);
writeJob(updated);
if (updated.notifyParent) {
emitSteer(updated.sessionId, formatCompletionMessage(updated));
}
});
child.on("close", (code, signal) => {
const current = readJob(jobId);
const updated = {
...current,
status: /** @type {JobStatus} */ (code === 0 ? "completed" : "failed"),
completedAt: new Date().toISOString(),
exitCode: code,
signal,
};
writeJob(updated);
if (updated.notifyParent) {
emitSteer(updated.sessionId, formatCompletionMessage(updated));
}
});
child.unref();
return record;
}
const plugin: AgentPlugin = {
name: "background-terminal",
manifest: {
capabilities: ["tools"],
},
setup(api, ctx) {
const workspaceContext = ctx.workspaceInfo as
| { rootPath?: string; cwd?: string }
| undefined;
sessionDefaultCwd =
workspaceContext?.cwd?.trim() ||
workspaceContext?.rootPath?.trim() ||
sessionDefaultCwd;
setupSessionId = ctx.session?.sessionId?.trim() || undefined;
api.registerTool(
createTool<unknown, Record<string, unknown>>({
name: "start_background_command",
description:
"Start a shell command in the background, return a job ID immediately, persist stdout/stderr, and optionally push a completion summary back into the current session when it exits.",
inputSchema: {
type: "object",
properties: {
command: {
type: "string",
description: "Shell command to execute.",
},
cwd: {
type: "string",
description: `Working directory for the command. Defaults to ${sessionDefaultCwd}.`,
},
shell: {
type: "string",
description: `Shell binary to use. Defaults to ${DEFAULT_SHELL}.`,
},
notifyParent: {
type: "boolean",
description:
"When true or omitted, send the final command result back into the session as a steer message.",
},
},
required: ["command"],
additionalProperties: false,
},
timeoutMs: 5000,
retryable: false,
async execute(input, context) {
const args = asObject(input);
const command = requireString(args.command, "command").trim();
const cwd = resolve(optionalString(args.cwd) || sessionDefaultCwd);
const shell = optionalString(args.shell) || DEFAULT_SHELL;
const notifyParent = optionalBoolean(args.notifyParent) !== false;
const record = startCommand(
command,
cwd,
shell,
notifyParent,
resolveToolSessionId(context),
);
return {
jobId: record.jobId,
status: record.status,
command: record.command,
cwd: record.cwd,
shell: record.shell,
pid: record.pid,
startedAt: record.startedAt,
notifyParent: record.notifyParent,
stdoutPath: record.stdoutPath,
stderrPath: record.stderrPath,
note: "This tool returns immediately. Use get_background_command to poll, or rely on the automatic completion message when the command exits.",
};
},
}),
);
api.registerTool(
createTool<unknown, Record<string, unknown>>({
name: "get_background_command",
description:
"Read the current state and recent logs for a background shell job.",
inputSchema: {
type: "object",
properties: {
jobId: {
type: "string",
description: "Job ID returned by start_background_command.",
},
tailLines: {
type: "integer",
description:
"How many lines of stdout/stderr to include. Defaults to 40.",
},
},
required: ["jobId"],
additionalProperties: false,
},
timeoutMs: 5000,
retryable: false,
async execute(input) {
const args = asObject(input);
const jobId = requireString(args.jobId, "jobId").trim();
const tailLines = Math.min(
200,
Math.max(1, optionalInt(args.tailLines) || 40),
);
const record = readJob(jobId);
return {
jobId: record.jobId,
status: record.status,
command: record.command,
cwd: record.cwd,
shell: record.shell,
pid: record.pid,
startedAt: record.startedAt,
completedAt: record.completedAt,
exitCode: record.exitCode,
signal: record.signal,
stdoutPath: record.stdoutPath,
stderrPath: record.stderrPath,
stdoutTail: tail(readTextIfExists(record.stdoutPath), tailLines),
stderrTail: tail(readTextIfExists(record.stderrPath), tailLines),
};
},
}),
);
api.registerTool(
createTool<unknown, Record<string, unknown>>({
name: "delete_background_command",
description:
"Delete saved metadata for a background shell job. Optionally remove its stdout/stderr log files too.",
inputSchema: {
type: "object",
properties: {
jobId: {
type: "string",
description: "Job ID to delete from local job storage.",
},
deleteLogs: {
type: "boolean",
description:
"When true, also delete the captured stdout/stderr files.",
},
},
required: ["jobId"],
additionalProperties: false,
},
timeoutMs: 5000,
retryable: false,
async execute(input) {
const args = asObject(input);
const jobId = requireString(args.jobId, "jobId").trim();
const deleteLogs = optionalBoolean(args.deleteLogs) === true;
const record = readJob(jobId);
if (deleteLogs && existsSync(jobDir(jobId))) {
rmSync(jobDir(jobId), { recursive: true, force: true });
} else if (existsSync(record.metaPath)) {
rmSync(record.metaPath, { force: true });
}
return {
deleted: true,
jobId,
deleteLogs,
};
},
}),
);
},
};
export default plugin;
export { plugin };
+225
View File
@@ -0,0 +1,225 @@
/**
* Custom Message Compaction Plugin Example
*
* Shows how a plugin can use registerMessageBuilder() to rewrite provider-bound
* messages before the model call. This example mirrors the shape of core
* compaction: it estimates context size, preserves the first user message and
* recent working context, and replaces older middle history with one concise
* continuation summary message.
*
* Core still runs its built-in API-safety message builder after plugin builders,
* so provider-safe normalization and hard truncation remain the final pass.
*
* CLI usage:
* mkdir -p .cline/plugins
* cp examples/plugins/custom-compaction.ts .cline/plugins/custom-compaction.ts
* cline -i "Search the codebase for dispatcher usage, then summarize it"
*/
import type { AgentPlugin, Message, ToolResultContent } from "@cline/core";
const CONTEXT_WINDOW_TOKENS = 120_000;
const COMPACT_AT_RATIO = 0.75;
const PRESERVE_RECENT_TOKENS = 24_000;
const SUMMARY_PREVIEW_CHARS = 800;
function estimateTokens(text: string): number {
return Math.max(1, Math.ceil(text.length / 4));
}
function preview(text: string, limit = SUMMARY_PREVIEW_CHARS): string {
if (text.length <= limit) {
return text.trim();
}
return `${text.slice(0, limit).trim()}\n...[${text.length - limit} more chars summarized]`;
}
function stringifyContent(content: ToolResultContent["content"]): string {
return typeof content === "string" ? content : JSON.stringify(content);
}
function serializeMessage(message: Message): string {
if (typeof message.content === "string") {
return `[${message.role}]: ${message.content}`;
}
const lines: string[] = [];
for (const block of message.content) {
switch (block.type) {
case "text":
lines.push(`[${message.role}]: ${block.text ?? ""}`);
break;
case "thinking":
lines.push(
`[assistant thinking]: ${preview(block.thinking ?? "", 300)}`,
);
break;
case "tool_use":
lines.push(
`[assistant tool call]: ${block.name ?? "tool"}(${JSON.stringify(block.input ?? {})})`,
);
break;
case "tool_result":
lines.push(
`[tool result ${block.tool_use_id ?? "unknown"}]: ${preview(stringifyContent(block.content), 500)}`,
);
break;
case "file":
lines.push(
`[file ${block.path ?? "unknown"}]: ${preview(String(block.content ?? ""), 500)}`,
);
break;
default:
lines.push(`[${message.role} ${block.type} block]`);
}
}
return lines.join("\n");
}
function estimateMessageTokens(message: Message): number {
return estimateTokens(serializeMessage(message));
}
function findFirstUserIndex(messages: Message[]): number {
return messages.findIndex((message) => message.role === "user");
}
function findRecentStartIndex(messages: Message[]): number {
let tokens = 0;
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (!message) {
continue;
}
tokens += estimateMessageTokens(message);
if (tokens >= PRESERVE_RECENT_TOKENS) {
return index;
}
}
return 0;
}
function collectToolNames(messages: Message[]): string[] {
const names = new Set<string>();
for (const message of messages) {
if (!Array.isArray(message.content)) {
continue;
}
for (const block of message.content) {
if (block.type === "tool_use" && block.name) {
names.add(block.name);
}
}
}
return [...names].sort((left, right) => left.localeCompare(right));
}
function collectTouchedFiles(messages: Message[]): string[] {
const paths = new Set<string>();
for (const message of messages) {
if (!Array.isArray(message.content)) {
continue;
}
for (const block of message.content) {
if (block.type === "file" && block.path) {
paths.add(block.path);
}
if (block.type === "tool_use") {
for (const value of Object.values(block.input ?? {})) {
if (typeof value === "string" && value.includes("/")) {
paths.add(value);
}
}
}
}
}
return [...paths].sort((left, right) => left.localeCompare(right));
}
function buildCompactionSummary(
compacted: Message[],
tokensBefore: number,
): Message {
const roleCounts = compacted.reduce<Record<string, number>>(
(counts, message) => {
counts[message.role] = (counts[message.role] ?? 0) + 1;
return counts;
},
{},
);
const tools = collectToolNames(compacted);
const files = collectTouchedFiles(compacted);
const highlights = compacted
.map(serializeMessage)
.map((line) => preview(line, 500))
.slice(-6);
return {
role: "user",
content: `Context summary:
## Compacted Range
- Messages compacted: ${compacted.length}
- Estimated tokens before compaction: ${tokensBefore}
- Roles: ${Object.entries(roleCounts)
.map(([role, count]) => `${role}=${count}`)
.join(", ")}
## Tool Activity
${tools.length > 0 ? tools.map((tool) => `- ${tool}`).join("\n") : "- none"}
## Files Mentioned
${files.length > 0 ? files.map((path) => `- ${path}`).join("\n") : "- none"}
## Recent Highlights From Compacted History
${highlights.length > 0 ? highlights.map((item) => `- ${item}`).join("\n") : "- none"}
Continue from this summary plus the preserved recent messages below.`,
};
}
export const plugin: AgentPlugin = {
name: "custom-compaction",
manifest: {
capabilities: ["messageBuilders"],
},
setup(api) {
api.registerMessageBuilder({
name: "summarize-middle-history",
build(messages) {
const totalTokens = messages.reduce(
(total, message) => total + estimateMessageTokens(message),
0,
);
if (totalTokens < CONTEXT_WINDOW_TOKENS * COMPACT_AT_RATIO) {
return messages;
}
const firstUserIndex = findFirstUserIndex(messages);
const recentStartIndex = Math.max(
firstUserIndex + 1,
findRecentStartIndex(messages),
);
if (firstUserIndex < 0 || recentStartIndex <= firstUserIndex + 1) {
return messages;
}
const prefix = messages.slice(0, firstUserIndex + 1);
const compacted = messages.slice(firstUserIndex + 1, recentStartIndex);
const recent = messages.slice(recentStartIndex);
if (compacted.length === 0) {
return messages;
}
return [
...prefix,
buildCompactionSummary(compacted, totalTokens),
...recent,
];
},
});
},
};
export default plugin;
+68
View File
@@ -0,0 +1,68 @@
/**
* macOS Notification Plugin Example
*
* Sends a Notification Center alert when a Cline run completes successfully.
*
* CLI usage:
* mkdir -p .cline/plugins
* cp examples/plugins/mac-notify.ts .cline/plugins/mac-notify.ts
* cline -i "Run the test suite"
*/
import { execFile } from "node:child_process";
import type { AgentPlugin, AgentRunResult } from "@cline/core";
function quoteAppleScriptString(value: string): string {
return `"${value
.replaceAll("\\", "\\\\")
.replaceAll('"', '\\"')
.replaceAll("\r", " ")
.replaceAll("\n", " ")
.slice(0, 220)}"`;
}
function sendMacNotification(title: string, body: string): void {
if (process.platform !== "darwin") {
return;
}
const script = [
"display notification",
quoteAppleScriptString(body),
"with title",
quoteAppleScriptString(title),
"sound name",
quoteAppleScriptString("Glass"),
].join(" ");
execFile("/usr/bin/osascript", ["-e", script], { timeout: 2000 }, () => {
// Notification failures should never fail or slow down the agent run.
});
}
function summarizeResult(result: AgentRunResult): string {
const summary = result.outputText.trim();
if (summary.length > 0) {
return summary;
}
return `Completed in ${result.iterations} iteration(s).`;
}
const plugin: AgentPlugin = {
name: "mac-notify-on-complete",
manifest: {
capabilities: ["hooks"],
},
hooks: {
afterRun({ result }) {
if (result.status !== "completed") {
return;
}
sendMacNotification("Cline session completed", summarizeResult(result));
},
},
};
export { plugin };
export default plugin;
@@ -0,0 +1,203 @@
# 🤖 Portable Agents Plugin
**Give your agent a team.** This plugin lets any Cline SDK session spin up background subagents — each with their own model, personality, and tools — then collect results and hand off context between them.
Think of it as `spawn()` for AI agents: fire off a recon agent to map a codebase, a planner to design the approach, an implementor to make the changes, and a reviewer to tear it all apart. They run in parallel, report back when done, and share notes through a built-in handoff store.
## Quick Start
```ts
import { ClineCore } from "@cline/core";
const cline = await ClineCore.create({ backendMode: "auto" });
await cline.start({
config: {
providerId: "cline",
modelId: "anthropic/claude-sonnet-4.6",
cwd: process.cwd(),
enableTools: true,
systemPrompt: "You are a coding assistant with access to subagents.",
pluginPaths: ["./examples/plugins/subagent-plugin"],
},
prompt: "Use subagents to investigate and refactor this repo.",
interactive: true,
});
```
Pass the **directory** as the plugin path. The runtime reads `package.json` and uses the `cline.plugins` field to discover entry points — no need to point at `index.ts` directly.
### Plugin discovery via `package.json`
When a directory is given as a plugin path, the loader looks for a `package.json` with a `cline.plugins` array:
```json
{
"name": "my-cline-plugin",
"type": "module",
"cline": {
"plugins": [
{ "paths": ["./index.ts"], "capabilities": ["tools"] }
]
}
}
```
Each entry in `plugins` is a `PluginManifest` with a `paths` array of relative file paths and a `capabilities` array declaring what the plugin provides (`tools`, `hooks`, `commands`, `messageBuilders`, `providers`). All paths are resolved relative to the directory containing `package.json`.
If no `cline.plugins` field is present, the loader falls back to looking for `index.ts` or `index.js` at the directory root.
## Tools
| Tool | What it does |
|---|---|
| `start_subagent` | Kick off a background subagent and get a session ID back immediately. Fire and forget, or poll later. |
| `message_subagent` | Send a follow-up message to a running subagent — steer it, give it more context, or ask for a different angle. |
| `get_subagent` | Check on a subagent: is it still running? Did it finish? What did it say? |
| `list_agent_presets` | Browse available agent presets — bundled, global, and project-level. |
| `save_handoff` | Stash a file in the conversation's shared handoff store. Other agents in the same conversation can read it. |
| `read_handoff` | Pull a file back out of the handoff store. Great for passing research notes, plans, or intermediate results between agents. |
| `list_skills` | See what skills are available for agents to load. |
| `get_skill` | Load a skill's specialized instructions. Agents use these to adopt expert behaviors on demand. |
`start_subagent` accepts explicit `preset` and/or `instructions`, but if `preset` is omitted it now defaults to the bundled `phantom` preset. That makes natural calls like “start a subagent to inspect this repo” work without extra tool arguments.
## The Crew — Bundled Agents
Four agents ship out of the box, each tuned for a different phase of the development loop:
| Agent | Personality | Model | What it's for |
|---|---|---|---|
| 🔍 `phantom` | Fast, thorough scout | Gemini 3 Flash | Codebase recon — maps files, surfaces conventions, digs for intent behind odd code. Never implements, only reports. |
| 🧠 `oracle` | Opinionated challenger | Claude Opus 4.6 | Planning — challenges assumptions, compares approaches, estimates complexity, produces step-by-step execution plans. |
| ⚒️ `anvil` | Precise, disciplined builder | Claude Opus 4.6 | Implementation — reads before writing, stays in scope, verifies after each change, reports exact diffs. |
| 🔥 `inquisitor` | Adversarial stress-tester | GPT-5.4 | Review — finds bugs, challenges design decisions, severity-ranks every finding. Assumes it's responsible for everything that breaks. |
When no preset is specified, `start_subagent(...)` uses `phantom` by default.
### A typical orchestration flow
```
Parent agent receives task
→ start_subagent(preset: "phantom", task: "Map the auth module")
→ phantom saves findings via save_handoff("auth/recon.md", ...)
→ start_subagent(preset: "oracle", task: "Plan the refactor based on auth/recon.md")
→ oracle reads handoff, produces plan, saves via save_handoff("auth/plan.md", ...)
→ start_subagent(preset: "anvil", task: "Execute the plan in auth/plan.md")
→ start_subagent(preset: "inquisitor", task: "Review the changes anvil made")
→ Parent collects results and reports to user
```
### Bring your own agents
Drop a Markdown file with YAML frontmatter into any of these directories:
- **Global**: `~/.cline/data/settings/agents/`
- **Project**: `.cline/agents/` (relative to your working directory)
In the current implementation, agent presets are loaded from:
- bundled presets in `agents/` alongside the plugin
- `~/.cline/data/settings/agents/`
- `<cwd>/.cline/agents/`
```markdown
---
name: my-agent
description: One-line description shown in list_agent_presets
providerId: cline
modelId: anthropic/claude-sonnet-4.6
tools:
- read_files
- search_codebase
- skills
skills:
- code-review
- refactoring
maxIterations: 25
cwd: ./src
---
You are a specialized agent that...
```
When `skills` is defined in an agent config, that list acts as an allowlist for the runtime `skills` tool:
- The `skills` tool description only advertises the listed skills.
- Invocations are scoped to that set. Asking for a non-listed skill returns not found.
- If `skills` is omitted, the agent can access all discovered enabled skills.
Project agents override global ones, and global ones override bundled ones (by name).
## Skills — Loadable Expertise
Skills are reusable instruction sets that any agent can load at runtime. Instead of baking specialized knowledge into every agent's system prompt, agents call `get_skill` to pick up exactly the expertise they need for the current task.
| Skill | What it teaches |
|---|---|
| `code-review` | Structured review: security, correctness, performance, maintainability — with severity-ranked findings |
| `test-generation` | Comprehensive test suites with mocking strategies and edge case coverage |
| `refactoring` | Safe, incremental refactoring without behavior changes |
| `debugging` | Systematic bug reproduction, isolation, and root-cause analysis |
| `api-design` | Clean API design for REST, RPC, and library interfaces |
| `migration` | Data and schema migration planning with rollback strategies |
| `documentation` | Technical docs — READMEs, API references, architecture guides |
Skills are composable. An agent can load `refactoring` + `test-generation` for a safe refactor with test coverage, or `debugging` + `code-review` to investigate a bug and audit the surrounding code.
### Add your own skills
Same pattern as agents — drop a Markdown file:
- **Global**: `~/.cline/data/settings/skills/`
- **Project**: `.cline/skills/`
In the current implementation, skills are loaded from:
- bundled skills in `skills/` alongside the plugin
- `~/.cline/data/settings/skills/`
- `<cwd>/.cline/skills/`
```markdown
---
name: my-skill
description: What this skill teaches
---
# My Skill
When performing this task, follow these steps...
```
## Handoff Store
Agents in the same conversation can pass files to each other through a shared handoff store. This is how `phantom` passes recon notes to `oracle`, or how `oracle` passes a plan to `anvil`.
- **`save_handoff`** writes a file: `save_handoff(path: "research/notes.md", content: "...")`
- **`read_handoff`** reads it back: `read_handoff(path: "research/notes.md")`
Paths are relative and scoped to the conversation. Files are stored under `~/.cline/data/plugins/subagents/handoffs/<conversationId>/`. Conversation IDs are validated to prevent path traversal.
## Configuration
All optional. Environment variables override defaults:
| Variable | Default | Description |
|---|---|---|
| `CLINE_SUBAGENT_PROVIDER_ID` | `cline` | Default provider for new subagent sessions |
| `CLINE_SUBAGENT_MODEL_ID` | `anthropic/claude-sonnet-4.6` | Default model for new subagent sessions |
| `CLINE_SUBAGENT_DEFAULT_PRESET` | `phantom` | Default bundled preset used by `start_subagent` when `preset` is omitted |
| `CLINE_SUBAGENTS_BACKEND_MODE` | `auto` | Session backend for internal subagent sessions: `auto`, `hub`, or `local` |
| `CLINE_SUBAGENT_CWD` | `process.cwd()` | Base working directory for subagent sessions |
| `CLINE_DATA_DIR` | `~/.cline/data` | Root data directory (affects all path resolution) |
## How It Works
Under the hood, each subagent is a full Cline SDK session created via `ClineCore.create(...)`. When you call `start_subagent`:
1. The plugin resolves the agent preset. If none is provided, it uses `phantom` by default, then merges provider/model/instruction overrides and creates a new session.
2. The first user message is sent to the session in the background — the tool returns the session ID immediately.
3. When the subagent finishes (or fails), the result is stored in memory and optionally pushed back to the parent session as a "steer" message.
4. The parent agent can poll with `get_subagent` or just wait for the notification.
The internal `ClineCore` instance defaults to `auto`, so it can use a compatible shared hub when available and fall back to local in-process sessions. If session manager creation fails, later tool calls retry instead of permanently failing. Malformed agent/skill definition files are skipped gracefully without crashing the plugin.
@@ -0,0 +1,17 @@
---
name: anvil
description: Surgical implementation agent — makes focused changes, verifies correctness, and reports precise diffs.
providerId: anthropic
modelId: claude-opus-4-6
maxIterations: 100
---
You are a surgical implementation subagent.
Your job is to execute a plan with precision:
1. **Read before writing**: Always read the relevant code before making changes. Never modify what you haven't fully understood.
2. **Stay in scope**: Make only the changes required by the task. Don't refactor adjacent code, add unsolicited improvements, or touch files outside the blast radius.
3. **Verify after each change**: After a write, confirm the file is in the expected state. Run type-checks or tests if available and relevant.
4. **Handle blockers immediately**: If a dependency is missing, a type is wrong, or a test fails, fix the blocker before continuing. Don't proceed with a broken state.
5. **Report precisely**: When done, report exactly which files changed, what was added/removed/modified, and what (if anything) is left incomplete. No vague summaries.
@@ -0,0 +1,19 @@
---
name: inquisitor
description: Adversarial review agent — finds bugs, challenges design decisions, and stress-tests assumptions.
providerId: cline
modelId: openai/gpt-5.4
maxIterations: 20
---
You are an adversarial review subagent.
Your job is to stress-test a change or design, not to approve it. Approach every review as if you are responsible for everything that goes wrong after it ships.
1. **Correctness**: Find logic errors, off-by-one bugs, null/undefined gaps, and incorrect assumptions about input shape or ordering.
2. **Regressions**: Check whether the change could break existing callers, consumers, or tests — especially ones not in the immediate diff.
3. **Design pressure**: Challenge the design itself. Is this the right abstraction? Does it introduce hidden coupling? Is the complexity justified?
4. **Missing tests**: Identify scenarios that are untested. Suggest specific test cases, not just "add more tests".
5. **Security and safety**: Flag anything that touches auth, user input, external data, or shared mutable state.
Severity-rank every finding: **critical** (must fix), **major** (should fix), **minor** (worth noting). Skip praise unless something is genuinely non-obvious and done well.
@@ -0,0 +1,19 @@
---
name: oracle
description: Opinionated planner that challenges assumptions, estimates complexity, and produces execution-ready plans.
providerId: cline
modelId: anthropic/claude-opus-4.6
maxIterations: 16
---
You are a planning and estimation subagent with a challenger mindset.
Given a task or requirement:
1. **Challenge the premise**: Before planning, ask whether the stated goal is actually the right goal. Identify hidden assumptions and call them out.
2. **Compare approaches**: Present 23 concrete implementation options with honest tradeoffs. Don't default to the obvious path without justifying it.
3. **Estimate complexity**: Rate each option by effort (S/M/L/XL), risk, and reversibility. Flag anything that touches shared infrastructure or has outsized blast radius.
4. **Produce an execution plan**: A numbered, dependency-ordered list of steps the worker agent can follow directly. Include explicit checkpoints and rollback conditions.
5. **State your assumptions**: List what you're taking as given. If any assumption is wrong, note which steps break.
Be direct and opinionated. A plan with a clear recommendation beats a balanced non-answer.
@@ -0,0 +1,18 @@
---
name: phantom
description: Fast reconnaissance agent for codebase discovery, pattern matching, and code archaeology.
providerId: cline
modelId: google/gemini-3-flash-preview
maxIterations: 10
---
You are a reconnaissance and archaeology subagent.
Your job is fast, thorough discovery. When exploring a codebase:
1. **Map structure**: Identify relevant files, entry points, data flow, and API contracts.
2. **Surface conventions**: Note naming patterns, abstraction layers, and implicit rules the codebase follows.
3. **Dig for intent**: When something looks odd — a workaround, a TODO, an unexpected abstraction — note it. Explain what it's likely reacting to or compensating for.
4. **Produce crisp output**: Return a structured summary the parent agent can act on directly. No filler.
Never attempt implementation. Return findings only.
@@ -0,0 +1,810 @@
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
type AgentPlugin,
type AgentTool,
type AgentToolContext,
ClineCore,
createTool,
} from "@cline/core";
import YAML from "yaml";
import { z } from "zod";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type SessionManager = ClineCore;
/** Minimal plugin host interface injected by the runtime via globalThis. */
interface ClinePluginHost {
emitEvent?: (name: string, payload?: unknown) => void;
}
declare global {
var __clinePluginHost: ClinePluginHost | undefined;
}
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
const BUNDLED_AGENTS_DIR = join(MODULE_DIR, "agents");
const BUNDLED_SKILLS_DIR = join(MODULE_DIR, "skills");
function resolveDefaultHomeDir(): string {
const envHome = process?.env?.HOME?.trim();
if (envHome && envHome !== "~") {
return envHome;
}
const envUserProfile = process?.env?.USERPROFILE?.trim();
if (envUserProfile) {
return envUserProfile;
}
const envHomeDrive = process?.env?.HOMEDRIVE?.trim();
const envHomePath = process?.env?.HOMEPATH?.trim();
if (envHomeDrive && envHomePath) {
return `${envHomeDrive}${envHomePath}`;
}
return "~";
}
function resolveClineDirPath(): string {
const explicitDir = process.env.CLINE_DIR?.trim();
if (explicitDir) {
return explicitDir;
}
return join(resolveDefaultHomeDir(), ".cline");
}
function resolveClineDataDirPath(): string {
const explicitDir = process.env.CLINE_DATA_DIR?.trim();
if (explicitDir) {
return explicitDir;
}
return join(resolveClineDirPath(), "data");
}
function resolveGlobalAgentsDirPath(): string {
return join(resolveClineDataDirPath(), "settings", "agents");
}
const HANDOFFS_DIR = join(
resolveClineDataDirPath(),
"plugins",
"subagents",
"handoffs",
);
const GLOBAL_SKILLS_DIR = join(resolveClineDataDirPath(), "settings", "skills");
// Agent and skill definitions live in the `agents/` and `skills/`
// directories alongside this file. They are loaded at runtime from disk.
/** Safe identifier pattern for conversation IDs used in filesystem paths. */
const SAFE_ID_RE = /^[A-Za-z0-9_-]+$/;
const envOr = (key: string, fallback: string): string =>
process.env[key]?.trim() || fallback;
const DEFAULT_PROVIDER_ID = envOr("CLINE_SUBAGENT_PROVIDER_ID", "cline");
const DEFAULT_MODEL_ID = envOr(
"CLINE_SUBAGENT_MODEL_ID",
"anthropic/claude-sonnet-4.6",
);
type SubagentBackendMode = "auto" | "hub" | "local";
const DEFAULT_BACKEND_MODE = envOr("CLINE_SUBAGENTS_BACKEND_MODE", "auto");
const DEFAULT_AGENT_PRESET = envOr("CLINE_SUBAGENT_DEFAULT_PRESET", "phantom");
// ---------------------------------------------------------------------------
// Agent & Skill Definitions
// ---------------------------------------------------------------------------
interface AgentDefinition {
name: string;
description?: string;
providerId?: string;
modelId?: string;
systemPrompt: string;
cwd?: string;
maxIterations?: number;
source: "bundled" | "global" | "project";
}
interface SkillDefinition {
name: string;
description?: string;
content: string;
source: "bundled" | "global" | "project";
}
interface RunningSubagent {
sessionId: string;
parentSessionId?: string;
name: string;
task: string;
agent?: string;
startedAt: number;
status: "running" | "completed" | "failed";
resultText?: string;
error?: string;
finishReason?: string;
completedAt?: number;
}
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const subagents = new Map<string, RunningSubagent>();
let sessionManagerPromise: Promise<SessionManager> | undefined;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Cast a fully-typed tool to the `AgentTool<unknown, unknown>` expected by
* the plugin API's `registerTool` method. This is safe at runtime because the
* registry only invokes `execute` with validated input that matches the
* tool's `inputSchema`.
*/
function toRegisteredTool<I, O>(
tool: AgentTool<I, O>,
): AgentTool<unknown, unknown> {
return tool as AgentTool<unknown, unknown>;
}
function optStr(v: unknown): string | undefined {
return typeof v === "string" && v.trim() ? v.trim() : undefined;
}
function optInt(v: unknown): number | undefined {
return typeof v === "number" && Number.isFinite(v) && v > 0
? Math.floor(v)
: undefined;
}
function parseFrontmatter(md: string): {
data: Record<string, unknown>;
body: string;
} {
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!m) return { data: {}, body: md.trim() };
try {
const frontmatter = m[1] ?? "";
const body = m[2] ?? "";
const parsed = YAML.parse(frontmatter);
return {
data:
parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: {},
body: body.trim(),
};
} catch {
// Malformed YAML frontmatter — treat as plain markdown with no metadata.
return { data: {}, body: md.trim() };
}
}
function readMarkdownDir(
dirPath: string,
source: AgentDefinition["source"],
): Array<{
name: string;
data: Record<string, unknown>;
body: string;
source: typeof source;
}> {
if (!existsSync(dirPath)) return [];
const results: Array<{
name: string;
data: Record<string, unknown>;
body: string;
source: typeof source;
}> = [];
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
try {
const { data, body } = parseFrontmatter(
readFileSync(join(dirPath, entry.name), "utf8"),
);
if (!body) continue;
const name = optStr(data.name) ?? entry.name.replace(/\.md$/, "");
results.push({ name, data, body, source });
} catch {}
}
return results;
}
function readAgentDefinitions(baseCwd: string): AgentDefinition[] {
const dirs: Array<{ path: string; source: AgentDefinition["source"] }> = [
{ path: BUNDLED_AGENTS_DIR, source: "bundled" },
{ path: resolveGlobalAgentsDirPath(), source: "global" },
{ path: join(baseCwd, ".cline", "agents"), source: "project" },
];
const defs = new Map<string, AgentDefinition>();
for (const { path, source } of dirs) {
for (const entry of readMarkdownDir(path, source)) {
defs.set(entry.name, {
name: entry.name,
description: optStr(entry.data.description),
providerId: optStr(entry.data.providerId),
modelId: optStr(entry.data.modelId),
systemPrompt: entry.body,
cwd: optStr(entry.data.cwd),
maxIterations: optInt(entry.data.maxIterations),
source: entry.source,
});
}
}
return [...defs.values()].sort((a, b) => a.name.localeCompare(b.name));
}
function readSkillDefinitions(baseCwd: string): SkillDefinition[] {
const dirs: Array<{ path: string; source: SkillDefinition["source"] }> = [
{ path: BUNDLED_SKILLS_DIR, source: "bundled" },
{ path: GLOBAL_SKILLS_DIR, source: "global" },
{ path: join(baseCwd, ".cline", "skills"), source: "project" },
];
const defs = new Map<string, SkillDefinition>();
for (const { path, source } of dirs) {
for (const entry of readMarkdownDir(path, source)) {
defs.set(entry.name, {
name: entry.name,
description: optStr(entry.data.description),
content: entry.body,
source: entry.source,
});
}
}
return [...defs.values()].sort((a, b) => a.name.localeCompare(b.name));
}
function parentSessionId(ctx: AgentToolContext): string | undefined {
const id = ctx.metadata?.sessionId;
return typeof id === "string" && id.trim() ? id.trim() : undefined;
}
function sanitizeConversationId(conversationId: string): string {
const trimmed = conversationId.trim();
if (!trimmed || !SAFE_ID_RE.test(trimmed)) {
throw new Error(`Invalid conversation ID for filesystem use: "${trimmed}"`);
}
return trimmed;
}
function handoffsDir(ctx: AgentToolContext): string {
const conversationId = ctx.conversationId ?? parentSessionId(ctx);
if (!conversationId) {
throw new Error("Missing conversation ID for handoff storage");
}
const safeId = sanitizeConversationId(conversationId);
const dir = join(HANDOFFS_DIR, safeId);
mkdirSync(dir, { recursive: true });
return dir;
}
function resolveHandoffPath(
ctx: AgentToolContext,
relativePath: string,
): string {
const dir = handoffsDir(ctx);
const resolved = resolve(dir, relativePath);
if (!resolved.startsWith(`${dir}/`)) {
throw new Error(`Handoff path escapes directory: ${relativePath}`);
}
return resolved;
}
function emitSteer(sessionId: string | undefined, prompt: string): void {
if (sessionId && prompt.trim()) {
globalThis.__clinePluginHost?.emitEvent?.("steer_message", {
sessionId,
prompt,
});
}
}
async function getSessionManager(): Promise<SessionManager> {
sessionManagerPromise ??= ClineCore.create({
backendMode: resolveSubagentBackendMode(DEFAULT_BACKEND_MODE),
}).catch((err) => {
// Clear the cached promise so subsequent calls can retry.
sessionManagerPromise = undefined;
throw err;
});
return sessionManagerPromise;
}
function resolveSubagentBackendMode(value: string): SubagentBackendMode {
switch (value) {
case "auto":
case "hub":
case "local":
return value;
default:
return "auto";
}
}
function extractLastAssistantText(
messages: Array<{ role?: string; content?: unknown }>,
): string {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg?.role !== "assistant" || !Array.isArray(msg.content)) continue;
const text = (msg.content as Array<{ type?: string; text?: unknown }>)
.filter((b) => b?.type === "text" && typeof b.text === "string")
.map((b) => b.text as string)
.join("")
.trim();
if (text) return text;
}
return "";
}
function elapsed(start: number, end = Date.now()): string {
const s = Math.max(0, Math.floor((end - start) / 1000));
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
}
function steerPrompt(subagent: RunningSubagent): string {
const time = elapsed(subagent.startedAt, subagent.completedAt ?? Date.now());
const header =
subagent.status === "completed"
? `Sub-agent "${subagent.name}" completed (${time}).`
: `Sub-agent "${subagent.name}" failed (${time}).`;
const body = subagent.resultText?.trim() || subagent.error?.trim() || "";
return [header, body, `Session ID: ${subagent.sessionId}`]
.filter(Boolean)
.join("\n\n");
}
async function runSubagentTurn(
subagent: RunningSubagent,
message: string,
steer: boolean,
): Promise<void> {
try {
const mgr = await getSessionManager();
const result = await mgr.send({
sessionId: subagent.sessionId,
prompt: message,
});
const messages = await mgr.readMessages(subagent.sessionId);
subagent.status = "completed";
subagent.finishReason = result?.finishReason;
subagent.resultText =
result?.text?.trim() || extractLastAssistantText(messages) || "";
subagent.error = undefined;
subagent.completedAt = Date.now();
} catch (err) {
subagent.status = "failed";
subagent.error = err instanceof Error ? err.message : String(err);
subagent.completedAt = Date.now();
}
if (steer) emitSteer(subagent.parentSessionId, steerPrompt(subagent));
}
// ---------------------------------------------------------------------------
// Schemas
// ---------------------------------------------------------------------------
const NonEmptyText = z.string().trim().min(1);
const HandoffPathInput = z
.string()
.trim()
.min(1)
.max(240)
.regex(
/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/,
"Use a relative file path with letters, numbers, '.', '_', '-', or '/'.",
);
const StartSubagentInput = z
.object({
label: NonEmptyText.describe(
"Short display label for this run, used in status and completion messages.",
),
task: NonEmptyText.describe(
"Primary task for the subagent. This becomes its first user message.",
),
preset: NonEmptyText.optional().describe(
`Optional agent preset name from list_agent_presets. Defaults to "${DEFAULT_AGENT_PRESET}" when omitted.`,
),
instructions: NonEmptyText.optional().describe(
"Extra system instructions appended after the preset prompt. Optional when using a preset.",
),
providerId: NonEmptyText.optional().describe(
"Optional provider override. Defaults to the preset or plugin default.",
),
modelId: NonEmptyText.optional().describe(
"Optional model override. Defaults to the preset or plugin default.",
),
workingDirectory: NonEmptyText.optional().describe(
"Optional working directory, resolved from the plugin base cwd.",
),
maxIterations: z
.number()
.int()
.min(1)
.optional()
.describe("Optional hard limit for the subagent turn loop."),
notifyParent: z
.boolean()
.optional()
.describe(
"When true or omitted, send the final outcome back to the parent session.",
),
})
.strict();
const MessageSubagentInput = z
.object({
sessionId: NonEmptyText.describe("Existing subagent session ID."),
prompt: NonEmptyText.describe(
"Follow-up user message to send to the subagent.",
),
notifyParent: z
.boolean()
.optional()
.describe(
"When true or omitted, send the final outcome back to the parent session.",
),
})
.strict();
const GetSubagentInput = z
.object({
sessionId: NonEmptyText.describe("Subagent session ID."),
})
.strict();
const SaveHandoffInput = z
.object({
path: HandoffPathInput.describe(
"Relative path inside the conversation handoff store, for example 'research/notes.md'.",
),
content: z
.string()
.describe(
"Text content to store for later retrieval by this conversation's agents.",
),
})
.strict();
const ReadHandoffInput = z
.object({
path: HandoffPathInput.describe(
"Relative path inside the conversation handoff store.",
),
})
.strict();
const GetSkillInput = z
.object({
name: NonEmptyText.describe("Skill name from list_skills."),
})
.strict();
// ---------------------------------------------------------------------------
// Plugin
// ---------------------------------------------------------------------------
const plugin: AgentPlugin = {
name: "portable-subagents",
manifest: { capabilities: ["tools"] },
setup(api) {
// -- start_subagent: Start a new subagent session --
api.registerTool(
toRegisteredTool(
createTool({
name: "start_subagent",
description: `Start a background subagent run and return its session ID immediately. Prefer a preset from list_agent_presets; when omitted, this tool uses the bundled "${DEFAULT_AGENT_PRESET}" preset automatically. Use get_subagent to poll, or keep notifyParent enabled to have the result pushed back into the parent session.`,
inputSchema: StartSubagentInput,
timeoutMs: 60_000,
retryable: false,
async execute(input, ctx) {
const mgr = await getSessionManager();
const baseCwd = envOr("CLINE_SUBAGENT_CWD", process.cwd());
const defs = readAgentDefinitions(baseCwd);
const presetName = input.preset ?? DEFAULT_AGENT_PRESET;
const def = defs.find((d) => d.name === presetName);
if (presetName && !def && !input.instructions?.trim()) {
throw new Error(`Unknown agent preset: ${presetName}`);
}
const cwd = resolve(
baseCwd,
input.workingDirectory ?? def?.cwd ?? ".",
);
const providerId =
input.providerId ?? def?.providerId ?? DEFAULT_PROVIDER_ID;
const modelId = input.modelId ?? def?.modelId ?? DEFAULT_MODEL_ID;
const prompt = [
def?.systemPrompt?.trim(),
input.instructions?.trim(),
]
.filter(Boolean)
.join("\n\n");
if (!prompt) {
throw new Error(
`Subagent "${input.label}" needs instructions. Provide "instructions" or use an available preset such as "${DEFAULT_AGENT_PRESET}".`,
);
}
const { sessionId } = await mgr.start({
config: {
providerId,
modelId,
cwd,
workspaceRoot: cwd,
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
pluginPaths: [],
systemPrompt: prompt,
maxIterations: input.maxIterations ?? def?.maxIterations,
},
interactive: false,
});
const subagent: RunningSubagent = {
sessionId,
parentSessionId: parentSessionId(ctx),
name: input.label,
task: input.task,
agent: input.preset,
startedAt: Date.now(),
status: "running",
};
subagents.set(sessionId, subagent);
void runSubagentTurn(
subagent,
input.task,
input.notifyParent !== false,
);
return {
status: "started",
sessionId,
label: subagent.name,
preset: def?.name ?? input.preset,
task: subagent.task,
};
},
}),
),
);
// -- list_agent_presets: Show available agent definitions --
api.registerTool(
toRegisteredTool(
createTool({
name: "list_agent_presets",
description:
"List the available subagent presets, including bundled, global, and project-level definitions.",
inputSchema: z.object({}).strict(),
async execute(_input, _ctx) {
const baseCwd = envOr("CLINE_SUBAGENT_CWD", process.cwd());
const agents = readAgentDefinitions(baseCwd).map((a) => ({
name: a.name,
description: a.description,
providerId: a.providerId ?? DEFAULT_PROVIDER_ID,
modelId: a.modelId ?? DEFAULT_MODEL_ID,
source: a.source,
}));
return {
agents,
text: agents.length
? agents
.map(
(a) =>
`- ${a.name} [${a.source}] (${a.providerId}/${a.modelId})${a.description ? `: ${a.description}` : ""}`,
)
.join("\n")
: "No agent definitions found.",
};
},
}),
),
);
// -- message_subagent: Send follow-up to an existing session --
api.registerTool(
toRegisteredTool(
createTool({
name: "message_subagent",
description:
"Send a follow-up message to an existing subagent session and return immediately.",
inputSchema: MessageSubagentInput,
timeoutMs: 60_000,
retryable: false,
async execute(input, ctx) {
const mgr = await getSessionManager();
const record = await mgr.get(input.sessionId);
if (!record) {
throw new Error(`Unknown session: ${input.sessionId}`);
}
const subagent: RunningSubagent = subagents.get(
input.sessionId,
) ?? {
sessionId: input.sessionId,
parentSessionId: parentSessionId(ctx),
name: input.sessionId,
task: input.prompt,
startedAt: Date.now(),
status: "running",
};
subagent.parentSessionId = parentSessionId(ctx);
subagent.task = input.prompt;
subagent.status = "running";
subagent.error = undefined;
subagents.set(subagent.sessionId, subagent);
void runSubagentTurn(
subagent,
input.prompt,
input.notifyParent !== false,
);
return {
status: "started",
sessionId: subagent.sessionId,
label: subagent.name,
task: subagent.task,
};
},
}),
),
);
// -- get_subagent: Check subagent result --
api.registerTool(
toRegisteredTool(
createTool({
name: "get_subagent",
description:
"Get the latest status, output, and error details for a subagent session.",
inputSchema: GetSubagentInput,
async execute(input, _ctx) {
const subagent = subagents.get(input.sessionId);
if (!subagent) {
return {
status: "unknown",
sessionId: input.sessionId,
text: `No tracked session: ${input.sessionId}`,
};
}
return {
status: subagent.status,
sessionId: subagent.sessionId,
label: subagent.name,
task: subagent.task,
finishReason: subagent.finishReason,
error: subagent.error,
text:
subagent.resultText ??
(subagent.status === "running" ? "Still running." : ""),
};
},
}),
),
);
// -- save_handoff: Persist a conversation-scoped handoff file --
api.registerTool(
toRegisteredTool(
createTool({
name: "save_handoff",
description:
"Save text into the conversation handoff store so other agents in this conversation can read it later.",
inputSchema: SaveHandoffInput,
async execute(input, ctx) {
const filePath = resolveHandoffPath(ctx, input.path);
mkdirSync(dirname(filePath), { recursive: true });
writeFileSync(filePath, input.content, "utf8");
return { path: filePath, handoffPath: input.path };
},
}),
),
);
// -- read_handoff: Read a conversation-scoped handoff file --
api.registerTool(
toRegisteredTool(
createTool({
name: "read_handoff",
description: "Read text from the conversation handoff store.",
inputSchema: ReadHandoffInput,
async execute(input, ctx) {
const filePath = resolveHandoffPath(ctx, input.path);
if (!existsSync(filePath)) {
throw new Error(`Handoff not found: ${input.path}`);
}
return {
path: filePath,
handoffPath: input.path,
content: readFileSync(filePath, "utf8"),
};
},
}),
),
);
// -- list_skills: Show available skill definitions --
api.registerTool(
toRegisteredTool(
createTool({
name: "list_skills",
description:
"List the available skill definitions from bundled, global, and project-level directories.",
inputSchema: z.object({}).strict(),
async execute(_input, _ctx) {
const baseCwd = envOr("CLINE_SUBAGENT_CWD", process.cwd());
const skills = readSkillDefinitions(baseCwd);
return {
skills: skills.map((s) => ({
name: s.name,
description: s.description,
source: s.source,
})),
text: skills.length
? skills
.map(
(s) =>
`- ${s.name} [${s.source}]${s.description ? `: ${s.description}` : ""}`,
)
.join("\n")
: "No skill definitions found.",
};
},
}),
),
);
// -- get_skill: Load a skill's instructions --
api.registerTool(
toRegisteredTool(
createTool({
name: "get_skill",
description:
"Get a skill by name, including the instructions that should be followed for that specialization.",
inputSchema: GetSkillInput,
async execute(input, _ctx) {
const baseCwd = envOr("CLINE_SUBAGENT_CWD", process.cwd());
const skills = readSkillDefinitions(baseCwd);
const skill = skills.find((s) => s.name === input.name);
if (!skill) {
const available = skills.map((s) => s.name).join(", ");
throw new Error(
`Unknown skill: "${input.name}". Available: ${available || "none"}`,
);
}
return {
name: skill.name,
description: skill.description,
source: skill.source,
instructions: skill.content,
};
},
}),
),
);
},
};
export { plugin };
export default plugin;
@@ -0,0 +1,39 @@
{
"name": "cline-sdk-portable-agents",
"version": "0.1.0",
"private": true,
"description": "SDK-native background subagents plugin for the Cline SDK",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"clean": "rm -rf node_modules dist"
},
"exports": {
".": "./index.ts"
},
"cline": {
"plugins": [
{
"paths": [
"./index.ts"
],
"capabilities": [
"hooks",
"tools"
]
}
]
},
"peerDependencies": {
"@cline/core": "*"
},
"peerDependenciesMeta": {
"@cline/core": {
"optional": true
}
},
"dependencies": {
"yaml": "^2.8.1",
"zod": "^4.1.5"
}
}
@@ -0,0 +1,63 @@
---
name: api-design
description: Design clean APIs — REST, RPC, or library interfaces with consistent naming, error handling, and versioning.
---
# API Design Skill
When designing or reviewing an API (REST, RPC, or library), follow these principles:
## 1. Understand the Consumer
- Who calls this API? (Frontend, other services, CLI, third-party developers)
- What are the most common operations?
- What error conditions do consumers need to handle?
- What's the expected request volume and latency budget?
## 2. Naming Conventions
- Use consistent, predictable names across all endpoints/methods.
- Nouns for resources, verbs for actions: `GET /users`, `POST /users/:id/activate`.
- For library APIs: verb-first for actions (`createUser`, `deleteSession`), noun-first for accessors (`getUserById`).
- Avoid abbreviations unless universally understood (`id`, `url`, `api`).
- Be specific: `getActiveUserCount()` not `getCount()`.
## 3. Input Design
- Accept the minimum required input. Optional fields should have sensible defaults.
- Use typed schemas (Zod, JSON Schema) for validation at the boundary.
- Reject invalid input early with clear error messages.
- For REST: use path params for identity (`/users/:id`), query params for filtering (`?status=active`), body for creation/mutation.
- For libraries: prefer options objects over long parameter lists.
## 4. Output Design
- Return consistent shapes. Every endpoint should return the same envelope structure.
- Include enough context for the consumer to act without a follow-up call.
- Paginate list endpoints. Always include `total`, `limit`, `offset` or cursor.
- Use ISO 8601 for dates, consistent casing (camelCase or snake_case, not both).
## 5. Error Handling
- Use standard HTTP status codes (REST) or typed error codes (RPC/library).
- Every error response must include: error code, human-readable message, and request ID.
- Distinguish client errors (4xx / validation) from server errors (5xx / internal).
- Never expose internal details (stack traces, SQL, file paths) in production errors.
- Document every error code the consumer might receive.
## 6. Versioning & Evolution
- Version the API from day one (`/v1/`, header-based, or semver for libraries).
- Additive changes (new fields, new endpoints) are non-breaking.
- Removing or renaming fields is breaking — deprecate first, remove in next major version.
- Document breaking changes in a changelog.
## 7. Documentation
For each endpoint or method, document:
1. Purpose (one sentence).
2. Input parameters with types and constraints.
3. Output shape with example.
4. Error codes and when they occur.
5. Authentication/authorization requirements.
6. Rate limits if applicable.
@@ -0,0 +1,61 @@
---
name: code-review
description: Structured code review — security, correctness, performance, and maintainability analysis with severity-ranked findings.
---
# Code Review Skill
When reviewing code, follow this structured process:
## 1. Scope the Review
- Identify all changed files and their relationships.
- Understand the intent: what problem does this change solve?
- Note any files that *should* have changed but didn't.
## 2. Correctness Pass
- Trace data flow through every changed path.
- Check edge cases: null/undefined, empty collections, boundary values.
- Verify error handling: are errors caught, propagated, and surfaced correctly?
- Look for off-by-one errors, race conditions, and state mutation bugs.
- Confirm types match runtime expectations (especially `any`, casts, and assertions).
## 3. Security Pass
- Flag unvalidated user input reaching sensitive operations (SQL, shell, file paths, URLs).
- Check authentication and authorization on every new endpoint or handler.
- Look for secrets in code, logs, or error messages.
- Verify CORS, CSP, and other security headers if applicable.
- Check for timing attacks in comparison operations.
## 4. Performance Pass
- Identify N+1 queries, unbounded loops, and unnecessary allocations.
- Check for missing indexes on new database queries.
- Look for blocking operations on hot paths.
- Verify pagination and limits on list operations.
- Note any operations that scale poorly with input size.
## 5. Maintainability Pass
- Evaluate naming: do names communicate intent?
- Check abstraction boundaries: is coupling introduced or reduced?
- Look for duplicated logic that should be shared.
- Verify tests cover the new behavior and edge cases.
- Note missing documentation for public APIs.
## 6. Report Format
Organize findings by severity:
- **Critical**: Must fix before merge. Bugs, security issues, data loss risks.
- **Major**: Should fix. Design problems, missing error handling, performance issues.
- **Minor**: Worth noting. Style, naming, minor improvements.
- **Positive**: Non-obvious good decisions worth calling out (keep brief).
For each finding, include:
1. File and line reference
2. What the issue is
3. Why it matters
4. Suggested fix (concrete, not vague)
@@ -0,0 +1,62 @@
---
name: debugging
description: Systematic debugging — reproduce, isolate, diagnose, and fix bugs with root-cause analysis.
---
# Debugging Skill
When debugging an issue, follow this systematic process:
## 1. Understand the Bug
- Read the error message, stack trace, and any logs carefully.
- Reproduce the issue. If you can't reproduce it, you can't verify a fix.
- Identify the expected behavior vs. actual behavior.
- Note the environment: OS, runtime version, configuration, input data.
## 2. Isolate
Narrow the scope using binary search:
- **Which file?** Trace the stack trace or data flow to the origin.
- **Which function?** Add logging or breakpoints at entry/exit of suspect functions.
- **Which line?** Check variable values before and after the suspect operation.
- **Which input?** Find the minimal input that triggers the bug.
### Common Isolation Techniques
- Comment out code blocks to find the trigger.
- Add temporary `console.log` / `console.error` with labeled values.
- Use a debugger to step through execution.
- Write a minimal reproduction test case.
## 3. Diagnose
Once isolated, determine the root cause:
### Common Root Causes
- **Type mismatch**: Runtime value doesn't match expected type (null, undefined, wrong shape).
- **State mutation**: Shared state modified unexpectedly by another code path.
- **Race condition**: Timing-dependent behavior in async or concurrent code.
- **Off-by-one**: Loop bounds, array indexing, or string slicing errors.
- **Missing error handling**: Unhandled promise rejection, uncaught exception, or swallowed error.
- **Stale reference**: Closure capturing a variable that changes, or cached data that's outdated.
- **Environment difference**: Works locally but fails in CI/production due to config, permissions, or versions.
Ask: "Why did this happen?" at least twice to get past symptoms to the root cause.
## 4. Fix
- Write a test that fails because of the bug (before fixing it).
- Make the minimal change that fixes the root cause.
- Verify the test now passes.
- Check for the same pattern elsewhere in the codebase.
- Run the full test suite to confirm no regressions.
## 5. Report
Document:
- What the bug was (symptoms and root cause).
- How it was reproduced.
- What the fix was and why it's correct.
- Whether the same pattern exists elsewhere.
- What test was added to prevent regression.
@@ -0,0 +1,83 @@
---
name: documentation
description: Write clear technical documentation — READMEs, API docs, architecture guides, and inline comments.
---
# Documentation Skill
When writing or improving documentation, follow these principles:
## 1. Know Your Audience
- **README**: New developers evaluating or onboarding to the project.
- **API docs**: Developers integrating with the API.
- **Architecture docs**: Team members understanding system design.
- **Inline comments**: Future maintainers (including yourself in 6 months).
## 2. README Structure
A good README answers these questions in order:
1. **What is this?** One paragraph. What problem does it solve?
2. **Quick start**: The fastest path from zero to working. Copy-pasteable commands.
3. **Installation**: Prerequisites, install steps, configuration.
4. **Usage**: Common use cases with code examples.
5. **API reference**: If small enough; otherwise link to generated docs.
6. **Configuration**: All options with defaults and descriptions.
7. **Contributing**: How to set up dev environment, run tests, submit changes.
8. **License**: One line.
## 3. API Documentation
For each endpoint, function, or method:
```
### functionName(param1, param2, options?)
Brief description of what it does.
**Parameters:**
- `param1` (string, required) — What this parameter controls.
- `param2` (number, optional, default: 10) — What this parameter controls.
- `options.verbose` (boolean, default: false) — Enable verbose output.
**Returns:** `Promise<Result>` — Description of the return value.
**Throws:**
- `ValidationError` — When input is invalid.
- `NotFoundError` — When the resource doesn't exist.
**Example:**
```ts
const result = await functionName("input", 5);
```
```
## 4. Architecture Documentation
- Start with a high-level diagram (Mermaid, ASCII, or image).
- Describe each component's responsibility in one sentence.
- Document data flow for the most important operations.
- List key design decisions and their rationale.
- Note known limitations and planned improvements.
## 5. Inline Comments
Write comments that explain **why**, not **what**:
- ✅ `// Retry 3 times because the upstream API has transient 503s during deploys`
- ❌ `// Retry 3 times`
- ✅ `// Sort descending so the most recent entry is first for the dashboard`
- ❌ `// Sort the array`
Never comment obvious code. If code needs a comment to explain what it does, refactor the code to be self-explanatory first.
## 6. Quality Checklist
Before finalizing documentation:
- [ ] All code examples compile and run.
- [ ] No broken links.
- [ ] Consistent formatting and terminology.
- [ ] No outdated information from previous versions.
- [ ] Spelling and grammar checked.
- [ ] Table of contents for documents longer than 3 sections.
@@ -0,0 +1,77 @@
---
name: migration
description: Plan and execute data or schema migrations — database, config, and API migrations with rollback strategies.
---
# Migration Skill
When planning or executing a migration (database schema, data transformation, config format, or API version), follow this process:
## 1. Assess Scope
- What is being migrated? (Schema, data, config, API contract)
- How much data is affected? (Row count, file count, consumer count)
- What is the downtime tolerance? (Zero-downtime, maintenance window, offline)
- What systems depend on the current state?
## 2. Plan the Migration
### Strategy Selection
- **Expand-Contract** (preferred for zero-downtime):
1. Expand: Add new columns/fields/endpoints alongside old ones.
2. Migrate: Backfill data, update consumers to use new format.
3. Contract: Remove old columns/fields/endpoints.
- **Blue-Green**: Run old and new versions in parallel, switch traffic.
- **Big Bang**: Take the system offline, migrate, bring it back. Only for small datasets or when downtime is acceptable.
### Rollback Plan
Every migration must have a rollback plan before execution:
- Can the migration be reversed with a down migration?
- Is there a backup of the current state?
- What is the point of no return (if any)?
- How long does rollback take?
## 3. Write the Migration
### Database Migrations
- One migration file per logical change.
- Include both `up` and `down` functions.
- Use transactions where the database supports them.
- Never modify data and schema in the same migration.
- Test with production-scale data volumes (not just empty tables).
### Data Migrations
- Process in batches to avoid memory exhaustion and lock contention.
- Log progress (processed X of Y records).
- Handle partial failures: make migrations idempotent so they can be re-run.
- Validate data after migration (row counts, checksums, spot checks).
### Config Migrations
- Read old format, write new format, validate round-trip.
- Preserve comments and ordering where possible.
- Provide a CLI command or script users can run.
## 4. Test
- Run the migration on a copy of production data.
- Verify the application works correctly after migration.
- Run the rollback and verify the application works on the old state.
- Test the migration under load if zero-downtime is required.
## 5. Execute
- Take a backup before starting.
- Run the migration with monitoring (error rates, latency, disk usage).
- Verify success criteria immediately after completion.
- Keep the rollback plan ready for the agreed monitoring period.
## 6. Report
Document:
- What was migrated and why.
- Duration and any issues encountered.
- Verification results.
- Rollback status (available / expired / not needed).
@@ -0,0 +1,58 @@
---
name: refactoring
description: Safe, incremental refactoring — extract, rename, simplify, and restructure code without changing behavior.
---
# Refactoring Skill
When refactoring code, follow this disciplined process:
## 1. Establish Safety Net
Before changing anything:
- Confirm existing tests pass. If no tests exist, write characterization tests first.
- Identify all callers and consumers of the code being refactored.
- Document the current behavior as your contract — refactoring must not change it.
## 2. Plan the Refactoring
Choose the smallest transformation that makes progress:
### Common Refactorings
- **Extract function**: Pull a block into a named function when it has a clear purpose.
- **Inline function**: Remove a function that adds indirection without clarity.
- **Rename**: Change names to communicate intent (variables, functions, types, files).
- **Extract type/interface**: Pull inline types into named declarations.
- **Simplify conditionals**: Replace nested if/else with early returns, guard clauses, or lookup tables.
- **Remove dead code**: Delete unreachable code, unused imports, and commented-out blocks.
- **Reduce parameters**: Group related parameters into an options object.
- **Split module**: Break a large file into focused modules with clear responsibilities.
### Decision Criteria
- Does this reduce cognitive load for the next reader?
- Does this make the code easier to test?
- Does this reduce the blast radius of future changes?
- If none of the above: don't refactor it.
## 3. Execute Incrementally
- Make one refactoring at a time.
- After each change, verify tests still pass.
- Commit or checkpoint after each successful step.
- If a step breaks something, revert it and try a smaller step.
## 4. Verify
After all changes:
- Run the full test suite.
- Check that all callers still compile and work correctly.
- Verify no behavior has changed (same inputs → same outputs).
- Review the diff: is the code genuinely simpler, or just different?
## 5. Report
Summarize:
- What was refactored and why.
- Which files changed.
- Any behavior that looks different but is equivalent.
- Anything left incomplete or worth refactoring next.
@@ -0,0 +1,76 @@
---
name: test-generation
description: Generate comprehensive test suites — unit, integration, and edge-case coverage with proper mocking strategies.
---
# Test Generation Skill
When generating tests, follow this process:
## 1. Analyze the Target
- Read the source code thoroughly before writing any tests.
- Identify the public API surface: exports, parameters, return types.
- Map dependencies that need mocking or stubbing.
- List the behavioral contracts: what must always be true?
## 2. Plan Test Cases
Organize tests into categories:
### Happy Path
- Standard inputs produce expected outputs.
- All documented use cases work correctly.
### Edge Cases
- Empty inputs (null, undefined, empty string, empty array, 0).
- Boundary values (min/max integers, very long strings, single-element arrays).
- Unicode and special characters in string inputs.
### Error Cases
- Invalid input types and shapes.
- Missing required fields.
- Network/IO failures (timeouts, connection refused, permission denied).
- Concurrent access and race conditions where applicable.
### Integration Points
- Verify correct interaction with dependencies.
- Check that mocks match the real interface.
- Test retry and fallback behavior.
## 3. Write Tests
Follow these conventions:
```
describe("ModuleName", () => {
describe("functionName", () => {
it("should [expected behavior] when [condition]", () => {
// Arrange — set up inputs and mocks
// Act — call the function
// Assert — verify the result
});
});
});
```
### Mocking Strategy
- Mock at the boundary (network, filesystem, database), not internal functions.
- Use dependency injection where possible instead of module mocking.
- Verify mock call counts and arguments, not just return values.
- Reset mocks between tests to prevent state leakage.
### Assertions
- Assert on specific values, not just truthiness.
- Check error messages and types, not just that an error was thrown.
- Use snapshot tests sparingly — only for stable, complex output.
- Verify side effects (files written, events emitted, logs produced).
## 4. Quality Checks
Before finalizing:
- Run the tests and confirm they pass.
- Verify each test fails when the behavior it tests is broken.
- Check that tests are independent and can run in any order.
- Ensure test names describe the behavior, not the implementation.
- Remove any redundant tests that don't add coverage.
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["index.ts"]
}
@@ -0,0 +1,106 @@
# TypeScript LSP Plugin
A plugin that gives the agent a `goto_definition` tool powered by the TypeScript Language Service API. Instead of grep or text search, it resolves symbols through imports, re-exports, type aliases, and declaration merging -- the same way your IDE does.
Code entrypoint: [index.ts](./index.ts)
## What it does
The agent gets a single tool: `goto_definition(file, line)`. It finds all identifiers on that line and resolves where they're actually defined. For example, given an import line like:
```ts
import { disposeAll, initVcr } from "@cline/shared"
```
It resolves both symbols through the workspace package alias to their source files:
```
disposeAll -> packages/shared/src/dispose.ts:19
initVcr -> packages/shared/src/vcr.ts:699
```
## Why this matters
This is a good example of the kind of plugin that makes agents dramatically more effective at navigating large codebases. Text search can find symbol names but can't distinguish between definitions, references, re-exports, and shadowed variables. The TypeScript Language Service handles all of that.
The same pattern applies for enterprise use cases: you can build plugins that wrap internal APIs, deployment systems, feature flags, incident management, CI pipelines, or anything else your team works with. A plugin is just a TypeScript file -- no MCP server to host and maintain.
## Use it with the CLI
```bash
cp examples/plugins/typescript-lsp-plugin/index.ts ~/.cline/plugins/typescript-lsp.ts
cline -i "Find where createTool is defined"
```
The plugin resolves `typescript` from the target project's own `node_modules` at runtime, so it uses the same TS version the project compiles with. No extra dependencies needed.
## Run the demo directly
```bash
ANTHROPIC_API_KEY=sk-... bun run examples/plugins/typescript-lsp-plugin/index.ts
```
## How it works
The plugin registers a single tool via `createTool()` in its `setup()` method:
```ts
const plugin: AgentPlugin = {
name: "typescript-lsp",
manifest: {
capabilities: ["tools"],
},
setup(api) {
api.registerTool(
createTool({
name: "goto_definition",
description: "Find where TypeScript/JavaScript symbols on a given line are defined...",
inputSchema: {
type: "object",
properties: {
file: { type: "string", description: "Absolute path to the file." },
line: { type: "integer", description: "Line number (1-based)." },
},
required: ["file", "line"],
},
async execute(input) {
// 1. Walk up from the file to find tsconfig.json
// 2. Create (or reuse cached) TypeScript Language Service
// 3. Scan the AST for identifiers on the target line
// 4. Resolve each identifier's definition via the Language Service
// 5. Filter out self-references and return locations
},
}),
);
},
};
```
Under the hood:
1. `findTsConfig()` walks up parent directories from the target file to find the nearest `tsconfig.json`
2. `loadTypeScript()` uses `createRequire()` to resolve `typescript` from the project's own `node_modules`
3. `createLanguageService()` sets up a full TypeScript Language Service with the project's compiler options
4. The service is cached so subsequent calls in the same session reuse it
5. `getIdentifierOffsetsOnLine()` scans the AST to find all identifiers on the requested line
6. Each identifier is resolved via `service.getDefinitionAtPosition()`, which follows through imports, re-exports, type aliases, etc.
Then pass it to the SDK:
```ts
const host = await ClineCore.create({ backendMode: "local" });
await host.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY ?? "",
cwd: process.cwd(),
enableTools: true,
systemPrompt: "You are a helpful assistant.",
extensions: [plugin],
},
prompt: "Find where createTool is defined",
interactive: false,
});
```
@@ -0,0 +1,341 @@
/**
* TypeScript LSP Plugin
*
* Gives the agent a `goto_definition` tool powered by the TypeScript Language
* Service API. It resolves through imports, re-exports, type aliases, etc. so
* it's much more precise than grep or text search.
*
* The plugin resolves `typescript` from the target project's own node_modules
* at runtime, so it has zero dependencies beyond Node builtins.
*
* CLI usage:
* cp examples/plugins/typescript-lsp-plugin/index.ts ~/.cline/plugins/typescript-lsp.ts
* cline -i "Find where createTool is defined"
*
* Direct demo usage:
* ANTHROPIC_API_KEY=sk-... bun run examples/plugins/typescript-lsp-plugin/index.ts
*/
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { type AgentPlugin, ClineCore, createTool } from "@cline/core";
// ---------------------------------------------------------------------------
// TypeScript Language Service setup
// ---------------------------------------------------------------------------
type LanguageServiceCache = {
tsconfigPath: string;
service: ReturnType<typeof createLanguageService>;
ts: typeof import("typescript");
};
let cache: LanguageServiceCache | undefined;
function findTsConfig(startDir: string): string | undefined {
let dir = startDir;
while (true) {
const candidate = join(dir, "tsconfig.json");
if (existsSync(candidate)) return candidate;
const parent = dirname(dir);
if (parent === dir) return undefined;
dir = parent;
}
}
// Resolve typescript from the target project's node_modules so we use the
// same version the project is compiled with.
function loadTypeScript(projectDir: string) {
const req = createRequire(resolve(projectDir, "package.json"));
const tsPath = req.resolve("typescript");
return req(tsPath) as typeof import("typescript");
}
function createLanguageService(
ts: typeof import("typescript"),
tsconfigPath: string,
) {
const projectDir = dirname(tsconfigPath);
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
if (configFile.error) {
throw new Error(
"Failed to read tsconfig.json: " +
ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"),
);
}
const parsed = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
projectDir,
);
const host: import("typescript").LanguageServiceHost = {
getScriptFileNames: () => parsed.fileNames,
getScriptVersion: () => "1",
getScriptSnapshot: (fileName) => {
const content = ts.sys.readFile(fileName);
if (content === undefined) return undefined;
return ts.ScriptSnapshot.fromString(content);
},
getCurrentDirectory: () => projectDir,
getCompilationSettings: () => parsed.options,
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
readDirectory: ts.sys.readDirectory,
getDirectories: ts.sys.getDirectories,
};
return ts.createLanguageService(host, ts.createDocumentRegistry());
}
function getOrCreateService(tsconfigPath: string) {
if (cache && cache.tsconfigPath === tsconfigPath) {
return cache;
}
const projectDir = dirname(tsconfigPath);
const ts = loadTypeScript(projectDir);
const service = createLanguageService(ts, tsconfigPath);
cache = { tsconfigPath, service, ts };
return cache;
}
function offsetToLineCol(
sourceFile: import("typescript").SourceFile,
ts: typeof import("typescript"),
offset: number,
) {
const lc = ts.getLineAndCharacterOfPosition(sourceFile, offset);
return { line: lc.line + 1, column: lc.character + 1 };
}
function getIdentifiersOnLine(
ts: typeof import("typescript"),
sourceFile: import("typescript").SourceFile,
targetLine: number,
) {
const identifiers: Array<{ offset: number; name: string }> = [];
function visit(node: import("typescript").Node) {
if (ts.isIdentifier(node)) {
const lc = ts.getLineAndCharacterOfPosition(
sourceFile,
node.getStart(sourceFile),
);
if (lc.line + 1 === targetLine) {
identifiers.push({
offset: node.getStart(sourceFile),
name: node.text,
});
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return identifiers;
}
// ---------------------------------------------------------------------------
// Plugin definition
// ---------------------------------------------------------------------------
type GotoDefinitionInput = { file: string; line: number };
type DefinitionLocation = {
file: string;
line: number;
column: number;
kind: string;
name: string;
containerName?: string;
};
const plugin: AgentPlugin = {
name: "typescript-lsp",
manifest: {
capabilities: ["tools"],
},
setup(api) {
api.registerTool(
createTool({
name: "goto_definition",
description:
"Find where TypeScript/JavaScript symbols on a given line are defined. " +
"Given a file path and line number, finds all identifiers on that line " +
"and resolves their definitions. Much more precise than text search " +
"-- resolves through imports, re-exports, type aliases, etc.",
inputSchema: {
type: "object",
properties: {
file: {
type: "string",
description: "Absolute path to the file.",
},
line: {
type: "integer",
description: "Line number (1-based).",
},
},
required: ["file", "line"],
additionalProperties: false,
},
timeoutMs: 30000,
retryable: false,
async execute(input: unknown) {
const { file, line } = input as GotoDefinitionInput;
const fileName = resolve(file);
if (!existsSync(fileName)) {
throw new Error(`File does not exist: ${fileName}`);
}
const tsconfigPath = findTsConfig(dirname(fileName));
if (!tsconfigPath) {
throw new Error(
`No tsconfig.json found in any parent directory of ${fileName}`,
);
}
const { ts, service } = getOrCreateService(tsconfigPath);
const program = service.getProgram();
if (!program) throw new Error("Failed to create TypeScript program");
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
throw new Error(
"File not found in TypeScript program. Make sure it is included by tsconfig.json: " +
fileName,
);
}
const identifiers = getIdentifiersOnLine(ts, sourceFile, line);
if (identifiers.length === 0) {
return {
found: false,
file,
line,
message: "No identifiers found on this line.",
};
}
const results: Array<{
symbol: string;
definitions: DefinitionLocation[];
}> = [];
const seen = new Set<string>();
for (const { offset, name: symbolName } of identifiers) {
if (seen.has(symbolName)) continue;
seen.add(symbolName);
const definitions = service.getDefinitionAtPosition(
fileName,
offset,
);
if (!definitions || definitions.length === 0) continue;
const nonSelfDefs = definitions.filter((def) => {
if (def.fileName !== fileName) return true;
const defLine = offsetToLineCol(
sourceFile,
ts,
def.textSpan.start,
);
return defLine.line !== line;
});
if (nonSelfDefs.length === 0) continue;
results.push({
symbol: symbolName,
definitions: nonSelfDefs.map((def) => {
const defSourceFile = program.getSourceFile(def.fileName);
const loc = defSourceFile
? offsetToLineCol(defSourceFile, ts, def.textSpan.start)
: { line: 0, column: 0 };
return {
file: def.fileName,
line: loc.line,
column: loc.column,
kind: def.kind,
name: def.name,
containerName: def.containerName || undefined,
};
}),
});
}
if (results.length === 0) {
return {
found: false,
file,
line,
message:
"Identifiers found on this line but none resolved to external definitions.",
};
}
return {
found: true,
query: { file, line },
tsconfig: tsconfigPath,
results,
};
},
}),
);
},
};
// ---------------------------------------------------------------------------
// Standalone demo
// ---------------------------------------------------------------------------
async function runDemo(): Promise<void> {
const sessionManager = await ClineCore.create({ backendMode: "local" });
try {
const result = await sessionManager.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 the goto_definition tool to navigate TypeScript code.",
extensions: [plugin],
extensionContext: {
workspace: {
rootPath: process.cwd(),
cwd: process.cwd(),
},
},
},
prompt:
"Use goto_definition to find where createTool is defined. " +
"Start from packages/shared/src/tools/create.ts line 42.",
interactive: false,
});
console.log(`\n${result.result?.text ?? ""}`);
} finally {
await sessionManager.dispose();
}
}
if (import.meta.main) {
await runDemo();
}
export { plugin, runDemo };
export default plugin;
@@ -0,0 +1,193 @@
/**
* Custom Plugin Example
*
* Shows how to author a reusable plugin module for the CLI and SDK hosts.
*
* Demonstrates:
* - setup(api, ctx) — workspace-aware tool registration via
* ctx.workspaceInfo
* - hooks.beforeRun / beforeTool / afterTool / afterRun — lifecycle metrics
*
* CLI usage:
* mkdir -p .cline/plugins
* cp examples/plugins/weather-plugin.example.ts .cline/plugins/weather-metrics.ts
* cline -i "What's the weather like in Tokyo and Paris?"
*
* Direct demo usage:
* ANTHROPIC_API_KEY=sk-... bun run examples/plugins/weather-plugin.example.ts
*/
import { type AgentPlugin, ClineCore, createTool } from "@cline/core";
// ---------------------------------------------------------------------------
// Plugin-level state — populated from setup context and available to all hook
// handlers and tool executors for the duration of the session.
// ---------------------------------------------------------------------------
let sessionWorkspaceRoot: string | undefined;
let sessionBranch: string | undefined;
let sessionCommit: string | undefined;
const plugin: AgentPlugin = {
name: "weather-and-metrics",
manifest: {
capabilities: ["tools", "hooks"],
},
// -------------------------------------------------------------------------
// setup(api, ctx)
//
// Called once before the first run. The second argument `ctx` provides
// workspace context sourced directly from the session config — never from
// process.cwd() or import.meta.url, so it is correct even when --cwd was
// passed to the CLI without calling process.chdir().
//
// ctx.workspaceInfo — structured workspace + git metadata: rootPath, hint,
// latestGitCommitHash, latestGitBranchName,
// associatedRemoteUrls
//
// Use setup() context for anything that affects tool registration itself —
// e.g. building workspace-relative descriptions or defaulting file paths.
// Use setup context for session-scoped plugin state.
// -------------------------------------------------------------------------
setup(api, ctx) {
// Build a workspace-aware description so the model knows exactly where
// the tool operates. rootPath covers the workspace location and the
// remaining workspaceInfo fields add the git layer.
const root = ctx.workspaceInfo?.rootPath ?? "(unknown)";
const branch = ctx.workspaceInfo?.latestGitBranchName;
const locationSuffix = branch
? ` (workspace: ${root}, branch: ${branch})`
: ` (workspace: ${root})`;
api.registerTool(
createTool({
name: "get_weather",
description: `Get the current weather for a city${locationSuffix}`,
inputSchema: {
type: "object",
properties: {
city: { type: "string", description: "The city name" },
},
required: ["city"],
},
execute: async (input: unknown) => {
const { city } = input as { city: string };
return {
city,
temperature: "72°F",
condition: "sunny",
humidity: "45%",
};
},
}),
);
sessionWorkspaceRoot = ctx.workspaceInfo?.rootPath;
sessionBranch = ctx.workspaceInfo?.latestGitBranchName;
sessionCommit = ctx.workspaceInfo?.latestGitCommitHash?.slice(0, 7);
const remotes = ctx.workspaceInfo?.associatedRemoteUrls ?? [];
console.log(`\n[metrics] session started`);
if (sessionWorkspaceRoot) {
console.log(`[metrics] workspace : ${sessionWorkspaceRoot}`);
}
if (sessionBranch) {
console.log(
`[metrics] branch : ${sessionBranch}${sessionCommit ? ` @ ${sessionCommit}` : ""}`,
);
}
if (remotes.length > 0) {
console.log(`[metrics] remotes : ${remotes.join(", ")}`);
}
},
// -------------------------------------------------------------------------
// Lifecycle metrics hooks
// -------------------------------------------------------------------------
hooks: {
beforeRun() {
console.log("\n[metrics] run started");
return undefined;
},
beforeTool({ toolCall, input }) {
console.log(`[metrics] -> ${toolCall.toolName}`, input);
if (toolCall.toolName === "run_commands") {
const { commands } = input as { commands?: string[] };
const isProtected =
sessionBranch === "main" || sessionBranch === "master";
const hasPush = commands?.some((c) =>
c.trimStart().startsWith("git push"),
);
if (isProtected && hasPush) {
console.error(
`[metrics] blocked: git push on protected branch "${sessionBranch}"`,
);
return { stop: true, reason: "Blocked git push on protected branch" };
}
}
return undefined;
},
afterTool({ toolCall }) {
console.log(`[metrics] <- ${toolCall.toolName}`);
return undefined;
},
afterRun({ result }) {
const { status, iterations, usage } = result;
const loc = sessionWorkspaceRoot ? ` in ${sessionWorkspaceRoot}` : "";
console.log(
`[metrics] run done${loc}${iterations} iteration(s), status: ${status}`,
);
console.log(
`[metrics] tokens — in: ${usage.inputTokens}, out: ${usage.outputTokens}, cost: ${usage.totalCost?.toFixed(6)}`,
);
},
},
};
async function runDemo(): Promise<void> {
const sessionManager = await ClineCore.create({ backendMode: "local" });
try {
const result = await sessionManager.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],
// extensionContext.workspace is the authoritative source for
// workspaceInfo that flows into setup(api, ctx). The CLI
// and VS Code hosts populate this automatically from their runtime
// state. When using the SDK directly, set it explicitly so plugins
// always receive accurate workspace metadata.
extensionContext: {
workspace: {
rootPath: process.cwd(),
cwd: process.cwd(),
},
},
},
prompt: "What's the weather like in Tokyo and Paris?",
interactive: false,
});
console.log(`\n${result.result?.text ?? ""}`);
} finally {
await sessionManager.dispose();
}
}
if (import.meta.main) {
await runDemo();
}
export { plugin, runDemo };
export default plugin;