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
+169
View File
@@ -0,0 +1,169 @@
# Cline SDK Examples
Learn how to build with the Cline SDK through practical, runnable examples.
## 📁 Plugin, Hook, and Automation Examples
Plugins extend the CLI and SDK with custom capabilities. Install them in `~/.cline/plugins/`:
### [`../../examples/plugins/`](../../examples/plugins/)
**Plugin module examples** showing how to extend the CLI and SDK with custom capabilities:
- Register custom tools
- Hook into agent lifecycle events
- Export a reusable plugin module for `.cline/plugins`
Examples include:
- `weather-plugin.example.ts` - Weather query tool
- `mac-notify.ts` - macOS Notification Center alerts
- `custom-compaction.ts` - Custom context compaction
- `automation-events.ts` - Plugin event emission
- `background-terminal.ts` - Background shell jobs with logging
```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?"
```
### [`../../examples/plugins/typescript-lsp-plugin/`](../../examples/plugins/typescript-lsp-plugin)
TypeScript LSP plugin that gives the agent a `goto_definition` tool powered by the TypeScript Language Service API. Resolves through imports, re-exports, and type aliases -- much more precise than text search.
- Register a tool via `createTool()` and `AgentExtension`
- Use the TypeScript Language Service to resolve symbol definitions
- Cache the language service for efficient repeated lookups
- Zero extra dependencies -- resolves `typescript` from the target project
```bash
cp examples/plugins/typescript-lsp-plugin/index.ts ~/.cline/plugins/typescript-lsp.ts
cline -i "Find where createTool is defined"
```
### [`../../examples/plugins/subagent-plugin/`](../../examples/plugins/subagent-plugin)
**Portable subagent plugin** that adds background agent orchestration tools to the CLI and SDK:
- Export a reusable plugin module for `.cline/plugins`
- Start background subagents from the main session
- Load bundled or custom agent presets and skills
Includes pre-configured agents:
- **Anvil** - Build and compile
- **Inquisitor** - Investigation and discovery
- **Oracle** - Planning and architecture
- **Phantom** - Stealth and optimization
Skills available:
- API design, code review, debugging, documentation, migration, refactoring, test generation
```bash
mkdir -p ~/.cline/plugins
cp examples/plugins/subagent-plugin/index.ts ~/.cline/plugins/portable-subagents.ts
cline -i "Use subagents to inspect this repository and report back."
```
Once loaded, the agent can call tools like `start_subagent`, `message_subagent`, `get_subagent`, `list_agent_presets`, `list_skills`, and the handoff tools.
## 📁 App Examples
### [`../../examples/cron/`](../../examples/cron)
**Example file-based and event-driven automation specs** for global `~/.cline/cron/`:
Recurring jobs for continuous quality:
- **changelog-generator** — Auto-generate CHANGELOG from commits
- **dependency-check** — Weekly security and update audits
- **test-coverage-report** — Daily coverage metrics
- **performance-baseline** — Build time and bundle size tracking
- **type-check-strict** — TypeScript type safety audits
- **code-style-audit** — Linting and formatting checks
- **dead-code-finder** — Identify unused code
- **documentation-check** — API documentation coverage
- **weekly-metrics-summary** — Fun team metrics report 🎉
Event-driven jobs for PR workflows:
- **pr-changelog-check** — Verify CHANGELOG is updated in PRs
- **pr-test-coverage** — Analyze coverage impact of changes
```bash
mkdir -p ~/.cline/cron
cp examples/cron/changelog-generator.cron.md ~/.cline/cron/
mkdir -p ~/.cline/cron/events
cp examples/cron/events/pr-changelog-check.event.md ~/.cline/cron/events/
```
See [cron/README.md](../../examples/cron/README.md) for full descriptions and usage patterns.
### [`../../examples/hooks/`](../../examples/hooks)
**Lifecycle hooks** written in bash, Python, or TypeScript that intercept agent actions at key points:
- Log all tool calls (PreToolUse) and results (PostToolUse)
- Block destructive operations
- Require review for critical files
- Inject contextual information
- Track lifecycle events (TaskStart, TaskComplete, SessionShutdown)
Hooks live in `.cline/hooks/` and are named after the event they handle (PreToolUse, PostToolUse, TaskStart, etc.):
```bash
mkdir -p ~/.cline/hooks
# Bash hook
cp examples/hooks/PreToolUse.sh ~/.cline/hooks/
chmod +x ~/.cline/hooks/PreToolUse.sh
# Or Python
cp examples/hooks/PreToolUse.py ~/.cline/hooks/PreToolUse.py
chmod +x ~/.cline/hooks/PreToolUse.py
# Or TypeScript (runs via bun)
cp examples/hooks/PreToolUse.ts ~/.cline/hooks/PreToolUse.ts
chmod +x ~/.cline/hooks/PreToolUse.ts
cline -i "do something" # Hooks will execute automatically
```
## 🚀 Quick Start
To use the SDK in your own Node app (outside this monorepo), start with:
```bash
npm add @cline/core
```
Add `@cline/agents` or `@cline/llms` only if you intentionally want lower-level control. For RPC client helpers, prefer importing from `@cline/core` when you want the app-facing SDK surface.
Current SDK layering:
- `@cline/core` owns config discovery/watchers, runtime plugin loading, and the context pipeline
- context compaction is core-owned and runs through turn preparation before model calls
- most app integrations should stay on the `@cline/core` surface unless they intentionally need lower-level agent or model control
## 📚 Learning Path
**Building plugins?**
- Start with [`../../examples/plugins/`](../../examples/plugins/) for basic tool and event patterns
- Explore [`../../examples/plugins/typescript-lsp-plugin/`](../../examples/plugins/typescript-lsp-plugin) for integration with language services
- See [`../../examples/plugins/subagent-plugin/`](../../examples/plugins/subagent-plugin) for advanced agent orchestration
**Building integrations?**
- Review [`../../examples/cron/`](../../examples/cron) for automation and event-driven workflows
- Explore [`desktop-app/`](./desktop-app), [`vscode/`](./vscode), and [`menubar/`](./menubar) for app integration patterns
**Controlling agent behavior?**
- Explore [`../../examples/hooks/`](../../examples/hooks) to intercept and modify tool execution, log actions, or enforce policies
## 📖 Documentation
- [Cline SDK README](../../packages/README.md)
- [Architecture Guide](../../ARCHITECTURE.md)
- [Individual Package Docs](../../packages/)
## 🛠️ Requirements
- **Node.js 22+** - For package compatibility
- **API Key** - Set `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or provider-specific key (for SDK examples)
- **Bun** - Optional, install from [bun.sh](https://bun.sh) for running examples
+420
View File
@@ -0,0 +1,420 @@
# Cline Automation Examples
This directory contains example automation specs for file-based and event-driven automation in Cline. Use these as templates to set up your own recurring or event-driven tasks.
## 🚀 Quick Start: Pick Your Automation
| Goal | Spec | Schedule | Mode |
|------|------|----------|------|
| 🔄 Review code | `daily-code-review` | Mon-Fri 9 AM | act |
| 📝 Update CHANGELOG | `changelog-generator` | Friday 6 PM | act |
| 🔒 Check security | `dependency-check` | Monday 10 AM | act |
| ✅ Verify tests | `test-coverage-report` | Daily 10 PM | act |
| ⚡ Track performance | `performance-baseline` | Daily 2 AM | act |
| 🏷️ Check types | `type-check-strict` | Daily 6 AM | plan |
| 🎨 Audit style | `code-style-audit` | Wednesday 3 AM | act |
| 🗑️ Find dead code | `dead-code-finder` | Sunday 4 AM | plan |
| 📚 Check docs | `documentation-check` | Thursday 5 AM | plan |
| 🎉 Weekly wins | `weekly-metrics-summary` | Friday 5 PM | act |
| 👀 Review PRs | `pr-review` | On PR opened | act |
| 📋 Check PR changelog | `pr-changelog-check` | On PR opened | act |
| 📊 PR coverage | `pr-test-coverage` | On PR updated | act |
## 📋 Overview
Cline automation supports two types of specs:
1. **Recurring specs** (`.cron.md`) — Run on a schedule
2. **Event-driven specs** (`.event.md`) — Run when an event occurs
Both can be enabled in `.cline/cron/` to be picked up by the hub or SDK.
## 🔄 Recurring Specs
### [`daily-code-review.cron.md`](./daily-code-review.cron.md)
A production-ready example that runs a code review automation on weekday mornings.
**Key fields:**
- `schedule: "0 9 * * MON-FRI"` — 9 AM on weekdays (cron format)
- `tools: run_commands,read_files` — Restrict tools to specific actions
- `mode: act` — Execute commands (vs. `plan` or `yolo`)
- `timeoutSeconds: 1800` — 30-minute timeout
- `modelSelection` — Override model/provider for this run
- `notesDirectory` — Durable automation notes for multi-run state
**Usage:**
```bash
mkdir -p ~/.cline/cron
cp examples/cron/daily-code-review.cron.md ~/.cline/cron/
# Edit the spec: set workspaceRoot, model, etc.
# Spec is reconciled on startup; next run enqueued automatically
```
**One-off specs:**
For one-time tasks, save as `.cline/cron/<name>.md` (no `.cron` infix) and omit the `schedule` field.
### Additional Recurring Spec Examples
#### [`changelog-generator.cron.md`](./changelog-generator.cron.md)
**Auto-generate changelog from recent commits**
Runs every Friday at 6 PM. Reviews commits in a directory (e.g., `apps/cli/`) and generates a changelog entry summarizing new features, bug fixes, and breaking changes. Updates `CHANGELOG.md` without bumping the version.
**Best for:** Projects with frequent releases and manual changelog maintenance overhead.
#### [`dependency-check.cron.md`](./dependency-check.cron.md)
**Weekly dependency health check**
Runs every Monday at 10 AM. Checks for outdated packages, security vulnerabilities, unused dependencies, and major version upgrades. Generates a prioritized report for action.
**Best for:** Teams that want proactive dependency maintenance without daily alerts.
#### [`test-coverage-report.cron.md`](./test-coverage-report.cron.md)
**Daily test coverage metrics**
Runs every day at 10 PM. Runs the full test suite, generates coverage reports, and identifies files needing more tests. Creates a markdown summary with visual indicators.
**Best for:** Maintaining code quality standards and tracking coverage trends over time.
#### [`performance-baseline.cron.md`](./performance-baseline.cron.md)
**Track performance metrics overnight**
Runs daily at 2 AM. Measures build time, bundle size, and cold start performance. Detects regressions and alerts if metrics exceed thresholds.
**Best for:** CLI tools, libraries, or services where performance is critical.
#### [`type-check-strict.cron.md`](./type-check-strict.cron.md)
**Strict TypeScript type checking**
Runs every morning at 6 AM in `plan` mode. Reports all type errors with strict compiler options and categorizes them by issue type.
**Best for:** Gradually improving type safety without blocking development.
#### [`code-style-audit.cron.md`](./code-style-audit.cron.md)
**Code style and linting audit**
Runs every Wednesday at 3 AM. Runs ESLint and Prettier, identifies unused code, detects anti-patterns, and provides a summary of violations.
**Best for:** Maintaining code consistency across a team.
#### [`dead-code-finder.cron.md`](./dead-code-finder.cron.md)
**Find and report dead code**
Runs every Sunday at 4 AM in `plan` mode. Identifies unused exports, unreachable code, and deprecated patterns. Prioritizes safe removals vs. ones requiring review.
**Best for:** Regular codebase cleanup and reducing technical debt.
#### [`documentation-check.cron.md`](./documentation-check.cron.md)
**Documentation coverage audit**
Runs every Thursday at 5 AM in `plan` mode. Analyzes documentation completeness, identifies missing JSDoc comments, checks for outdated docs, and evaluates overall documentation structure.
**Best for:** Improving code maintainability and onboarding of new team members.
#### [`weekly-metrics-summary.cron.md`](./weekly-metrics-summary.cron.md)
**Fun weekly metrics summary for the team** 🎉
Runs every Friday at 5 PM. Collects a week's worth of data: commits, test coverage, performance, PR activity, and contributor stats. Generates a celebratory markdown report with emojis, top contributors, metrics trends, and fun facts.
**Best for:** Team morale, tracking velocity, and celebrating wins. Great for Friday morning stand-ups or team channels.
## 🎯 Event-Driven Specs
Event-driven specs live in `.cline/cron/events/` and trigger when normalized events are ingested.
### [`events/pr-review.event.md`](./events/pr-review.event.md)
Runs a pull request review whenever a new PR opens on the `main` branch.
**Key fields:**
- `event: github.pull_request.opened` — Trigger on this event type
- `filters` — Narrow scope: match repository, branch, labels, etc.
- `debounceSeconds: 30` — Wait 30s for more events before triggering
- `dedupeWindowSeconds: 600` — Ignore duplicate events within 10 minutes
- `cooldownSeconds: 120` — Wait 2 minutes after a run before next trigger
- `maxParallel: 2` — Run at most 2 in parallel
**Usage:**
```bash
mkdir -p ~/.cline/cron/events
cp examples/cron/events/pr-review.event.md ~/.cline/cron/events/
# Configure your repository, branch, and workspace
# Wire up GitHub App or webhook to ingest events
```
**Ingesting events:**
Events are ingested via:
- GitHub App or webhook receiver
- Plugin-emitted events (see `plugins/automation-events.ts`)
- Connector adapters
- `cline.automation.ingestEvent()` in the SDK
### [`events/local-manual-test.event.md`](./events/local-manual-test.event.md)
Local test spec for verifying event-driven automation without external services.
**Key fields:**
- `event: local.manual_test` — Local event type (no external dependency)
- `filters: { topic: cron-feature-2 }` — Match on event payload fields
- `debounceSeconds: 0` — Trigger immediately
- `maxIterations: 5` — Quick timeout for testing
**Usage:**
```bash
mkdir -p ~/.cline/cron/events
cp examples/cron/events/local-manual-test.event.md ~/.cline/cron/events/
# Start the hub with automation enabled
# In another shell, ingest a test event
node -e "
const { HubWebSocketClient } = require('@cline/core');
const client = new HubWebSocketClient('ws://localhost:8000');
client.send('cron.event.ingest', {
eventType: 'local.manual_test',
envelope: { subject: 'test', topic: 'cron-feature-2', message: 'hello' }
});
"
```
### [`events/local-plugin-event.event.md`](./events/local-plugin-event.event.md)
Test spec for plugin-emitted events. Pairs with `plugins/automation-events.ts`.
### Additional Event-Driven Examples
#### [`events/pr-changelog-check.event.md`](./events/pr-changelog-check.event.md)
**Verify CHANGELOG updates in PRs**
Triggers when a PR opens on `main`. If the PR modifies source code but doesn't update CHANGELOG, posts a comment suggesting what should be added. If CHANGELOG is updated, verifies the format.
**Best for:** Maintaining an up-to-date changelog without manual reminders. Reduces reviewer burden.
#### [`events/pr-test-coverage.event.md`](./events/pr-test-coverage.event.md)
**Analyze test coverage impact of PRs**
Triggers when a PR is opened or updated. Runs test coverage against the PR branch, compares to main, and posts a comment showing:
- Which new code is covered vs. uncovered
- Coverage impact percentage
- Files with decreased coverage
- Recommendations for additional tests
**Best for:** Maintaining test coverage standards while being helpful rather than blocking. Guides authors toward better test practices.
**Key fields:**
- `event: local.plugin_event` — Custom event emitted by the plugin
- `filters: { topic: plugin-demo }` — Match on plugin event attributes
- Minimal throttling for responsive testing
**Usage:**
```bash
mkdir -p ~/.cline/cron/events
cp examples/cron/events/local-plugin-event.event.md ~/.cline/cron/events/
# Load the plugin that emits these events
mkdir -p ~/.cline/plugins
cp examples/plugins/automation-events.ts ~/.cline/plugins/
# Run CLI with automation enabled; the plugin emits events
clite --enable-automation -i "Test automation events"
```
## 🚀 Getting Started
### 1. Set up the spec directory
```bash
mkdir -p ~/.cline/cron/events
```
### 2. Copy a template
- **For scheduled tasks:** Copy `daily-code-review.cron.md`
- **For GitHub events:** Copy `pr-review.event.md`
- **For local testing:** Copy `local-manual-test.event.md`
- **For plugin events:** Copy `local-plugin-event.event.md`
### 3. Customize the spec
Edit your copied spec:
- Set `workspaceRoot` to your project path
- Set `modelSelection` if using a non-default model
- Update `filters` to match your repos/branches
- Adjust timeout, iterations, and tool restrictions
- Refine the prompt (the YAML body)
### 4. Enable automation
**In the hub:**
```bash
new HubWebSocketServer({
cronOptions: { workspaceRoot: "/absolute/workspace" }
});
```
**In the SDK:**
```ts
const cline = await ClineCore.create({
automation: true, // Enable automation
// ... other options
});
```
**In the CLI:**
```bash
clite --enable-automation
```
### 5. Monitor runs
Completed and failed runs are reported to `.cline/cron/reports/<run-id>.md` with:
- YAML frontmatter (run ID, status, timing, token usage)
- Summary of work performed
- Tool calls and results
- For events: trigger event context
## 📖 Field Reference
### Common fields
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `id` | string | yes | Unique identifier (alphanumeric, hyphens) |
| `title` | string | yes | Human-readable title |
| `workspaceRoot` | string | yes | Absolute path to the project |
| `mode` | string | no | `yolo` (default), `act`, or `plan` |
| `tools` | string/array | no | Comma-separated tool names; empty disables work tools |
| `systemPrompt` | string | no | Custom system prompt |
| `modelSelection` | object | no | `{ providerId, modelId }` |
| `maxIterations` | number | no | Iteration limit |
| `timeoutSeconds` | number | no | Run timeout |
| `extensions` | array | no | `rules`, `skills`, `plugins` |
| `tags` | array | no | Arbitrary tags for grouping |
| `metadata` | object | no | Custom metadata |
### Recurring-only fields (`.cron.md`)
| Field | Type | Notes |
|-------|------|-------|
| `schedule` | string | **Required**. Cron expression (5 fields: minute, hour, day, month, day-of-week) |
| `timezone` | string | Optional. IANA timezone (e.g., `America/New_York`). Defaults to system timezone. |
### Event-only fields (`.event.md`)
| Field | Type | Notes |
|-------|------|-------|
| `event` | string | **Required**. Event type (e.g., `github.pull_request.opened`, `local.manual_test`) |
| `filters` | object | Optional. Match event fields (supports dot paths) |
| `debounceSeconds` | number | Optional. Coalesce events within N seconds (default 0) |
| `dedupeWindowSeconds` | number | Optional. Skip duplicates within N seconds (default 0) |
| `cooldownSeconds` | number | Optional. Wait N seconds after a run (default 0) |
| `maxParallel` | number | Optional. Max concurrent runs (default unbounded) |
## 📚 Tools Reference
Available tool names:
- `read_files` — Read file contents
- `search_codebase` — Search across the project
- `run_commands` — Execute shell commands
- `fetch_web_content` — Fetch URLs
- `apply_patch` — Apply code patches
- `editor` — Edit files
- `skills` — Call custom skills
- `ask_question` — Query the user
- `submit_and_exit` — Complete the run
## 💡 Practical Automation Workflows
### Complete Development Automation Suite
Combine multiple specs for comprehensive automation:
```
Monday 10 AM → dependency-check.cron.md (Check dependencies)
Tuesday 3 AM → code-style-audit.cron.md (Lint and format)
Wednesday 5 AM → documentation-check.cron.md (Doc coverage)
Thursday 4 AM → dead-code-finder.cron.md (Find cleanup opportunities)
Friday 6 PM → changelog-generator.cron.md (Auto-generate changelog)
Daily 2 AM → performance-baseline.cron.md (Track metrics)
Daily 10 PM → test-coverage-report.cron.md (Coverage trends)
Daily 6 AM → type-check-strict.cron.md (Type safety)
On every PR:
→ pr-changelog-check.event.md (Verify CHANGELOG)
→ pr-test-coverage.event.md (Coverage impact)
```
This provides continuous quality monitoring without developers having to remember to run checks manually.
### Team Workflows by Role
**For Team Leads:**
- `dependency-check.cron.md` — Weekly security review
- `dead-code-finder.cron.md` — Quarterly cleanup planning
- `performance-baseline.cron.md` — Monitor system health
**For QA Engineers:**
- `test-coverage-report.cron.md` — Track trends
- `pr-test-coverage.event.md` — PR-level feedback
**For Backend Teams:**
- `performance-baseline.cron.md` — Build time, API response time
- `type-check-strict.cron.md` — Type safety
**For Frontend Teams:**
- `performance-baseline.cron.md` — Bundle size, cold start
- `code-style-audit.cron.md` — Consistent styling
## 🔍 Examples in Action
### Schedule a daily security audit
```md
---
id: daily-security-audit
title: Daily Security Audit
workspaceRoot: /path/to/repo
schedule: "0 2 * * *" # 2 AM daily
tools: read_files,search_codebase
mode: act
timeoutSeconds: 3600
extensions:
- skills
---
Search for hardcoded secrets, outdated dependencies, and insecure patterns.
Report findings to the team.
```
### Review all new PRs on main
```md
---
id: pr-security-review
title: Security Review for PRs
workspaceRoot: /path/to/repo
event: github.pull_request.opened
filters:
pullRequest:
baseBranch: main
cooldownSeconds: 300
maxParallel: 3
---
Summarize the changes, check for security risks, and recommend approval or changes.
```
## 🔗 See Also
- [Architecture automation overview](../../ARCHITECTURE.md#automation) — Runtime architecture and flow details
- [`plugins/automation-events.ts`](../plugins/automation-events.ts) — Plugin event emission
- [Cline SDK Examples](../) — Other integration examples
@@ -0,0 +1,37 @@
---
id: changelog-generator
title: Auto-Generate Changelog from Commits
workspaceRoot: /absolute/path/to/repo
schedule: "0 18 * * FRI"
tools: run_commands,read_files,editor
mode: act
enabled: false
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 1800
maxIterations: 20
tags:
- automation
- changelog
- documentation
metadata:
owner: development
targetFile: apps/cli/CHANGELOG.md
trackDirectory: apps/cli
---
Review recent commits in the `apps/cli` directory since the last CHANGELOG entry.
Generate a summary of significant changes (new features, bug fixes, breaking changes).
Update `apps/cli/CHANGELOG.md` with a new version entry at the top following this format:
## [VERSION] (YYYY-MM-DD)
- Feature: [description]
- Fix: [description]
- Breaking: [description]
Do NOT bump the version number in package.json.
Do NOT override existing entries.
Focus on user-facing changes, not internal refactors.
Follow the existing changelog style and format from previous entries.
@@ -0,0 +1,47 @@
---
id: code-style-audit
title: Code Style and Linting Audit
workspaceRoot: /absolute/path/to/repo
schedule: "0 3 * * WED"
tools: run_commands,read_files
mode: act
enabled: false
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 1800
maxIterations: 20
tags:
- automation
- quality
- style
metadata:
owner: development
reportFormat: markdown
---
Run comprehensive code style and linting checks:
1. Run ESLint: `npm run lint` or `eslint .`
2. Run Prettier check: `prettier --check .` or equivalent
3. Check for common issues:
- Unused variables or imports
- Dead code
- TODO/FIXME comments left in main branch
- Console.log statements in production code
- Magic numbers without explanation
Generate a report showing:
- Linting violations by rule (top 10)
- Files with most violations
- Formatting inconsistencies
- Pattern analysis (e.g., common TODO reasons, unused import patterns)
Provide statistics:
- Total violations found
- Fixable vs non-fixable violations
- Trend compared to previous week (if data exists)
Recommendations:
- Quick wins: violations that can be auto-fixed
- Standards improvements: patterns to establish
- Review-needed: complex issues requiring human judgment
@@ -0,0 +1,28 @@
---
id: daily-code-review
title: Daily Code Review
workspaceRoot: /absolute/path/to/repo
schedule: "0 9 * * MON-FRI"
tools: run_commands,read_files
mode: act
enabled: true
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 1800
systemPrompt: You are a precise automation agent that reports only actionable review findings.
maxIterations: 20
tags:
- automation
- review
metadata:
owner: platform
notesDirectory: /absolute/path/to/notes
extensions:
- rules
- skills
- plugins
source: user
---
Review the open pull requests, identify the highest-risk changes, run the
relevant checks if needed, and write a concise summary of findings.
@@ -0,0 +1,58 @@
---
id: dead-code-finder
title: Find and Report Dead Code
workspaceRoot: /absolute/path/to/repo
schedule: "0 4 * * SUN"
tools: run_commands,read_files,search_codebase
mode: plan
enabled: false
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 2400
maxIterations: 30
tags:
- automation
- quality
- refactoring
metadata:
owner: development
reportType: analysis
---
Scan the codebase for dead code and unused exports:
1. Identify unused exports:
- Functions not called within the module or by external modules
- Classes with no instantiations
- Constants/variables not referenced
- Type definitions not used
2. Detect unreachable code patterns:
- Statements after return/throw
- Unreachable branches in conditionals
- Dead try-catch blocks
- Unused catch parameters
3. Analyze file-level patterns:
- Modules that are never imported
- Test files without corresponding implementation
- Example/demo code in main codebase
- Deprecated or marked-for-removal code
Generate a comprehensive report with:
- Dead code segments (with file locations and line numbers)
- Confidence level for each finding (high/medium/low)
- Safe-to-remove vs requires-review items
- Estimated code reduction if cleanup is done
Safe removals candidates:
- Variables only assigned, never read
- Functions declared but never called
- Exported items with no external references
Requires review:
- Code that might be called dynamically
- Public APIs (check if external consumers exist)
- Backwards-compatibility concerns
Use plan mode to suggest removals without applying them.
@@ -0,0 +1,35 @@
---
id: dependency-check
title: Weekly Dependency Health Check
workspaceRoot: /absolute/path/to/repo
schedule: "0 10 * * MON"
tools: run_commands,read_files
mode: act
enabled: false
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 1800
maxIterations: 15
tags:
- automation
- security
- dependencies
metadata:
owner: platform
---
Run a comprehensive dependency health check:
1. Check for outdated packages: `npm outdated` (or yarn/pnpm equivalent)
2. Check for security vulnerabilities: `npm audit`
3. List packages with available major version upgrades
4. Identify unused dependencies (if possible)
5. Check for dependency conflicts or duplicate packages
Provide a summary report covering:
- Critical security vulnerabilities (if any)
- Count of outdated packages by severity (minor, patch, major)
- Recommended immediate actions
- Packages safe to update to latest versions
Focus on actionable insights. Ignore known false positives and dev-only dependencies.
@@ -0,0 +1,66 @@
---
id: documentation-check
title: Documentation Coverage Audit
workspaceRoot: /absolute/path/to/repo
schedule: "0 5 * * THU"
tools: run_commands,read_files,search_codebase
mode: plan
enabled: false
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 1800
maxIterations: 25
tags:
- automation
- documentation
- quality
metadata:
owner: documentation
checkAreas:
- publicAPIs
- complexFunctions
- typeDefinitions
- modules
---
Audit documentation coverage across the codebase:
1. Check public API documentation:
- Functions exported from public modules
- Classes and interfaces
- Type definitions and generics
- Decorators and annotations
2. Identify missing documentation:
- Public functions without JSDoc comments
- Complex functions without explanation
- Public types without descriptions
- Exported modules without README or header comments
3. Evaluate existing documentation quality:
- JSDoc comments without @param or @return tags
- Comments that are outdated or misleading
- Code samples in docs that may be broken
- Links in documentation that point to removed code
4. Analyze documentation structure:
- Main README quality and completeness
- Architecture documentation
- Contributing guide existence
- API reference documentation
- Changelog maintenance
Generate report with sections:
- Documentation coverage percentage by module
- Top 10 undocumented public APIs
- Types with missing descriptions
- Files with complex logic needing explanation
- Outdated documentation instances
Recommendations:
- High-priority items (public APIs without docs)
- Documentation style improvements
- Template suggestions for JSDoc
- Links that need updating
Use plan mode to suggest improvements without applying changes.
@@ -0,0 +1,28 @@
---
id: local-manual-test
title: Local Manual Event Test
workspaceRoot: /absolute/path/to/repo
cwd: /absolute/path/to/repo
event: local.manual_test
filters:
topic: cron-feature-2
debounceSeconds: 0
dedupeWindowSeconds: 60
cooldownSeconds: 0
maxParallel: 1
mode: act
enabled: true
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 300
maxIterations: 5
tags:
- automation
- local-test
metadata:
owner: platform
source: local-smoke-test
---
Use the normalized trigger event context to confirm event-driven automation is
working locally. Summarize the event id, subject, topic, and payload message.
@@ -0,0 +1,20 @@
---
id: local-plugin-event
title: Local Plugin Event
workspaceRoot: /absolute/path/to/repo
cwd: /absolute/path/to/repo
event: local.plugin_event
filters:
topic: plugin-demo
dedupeWindowSeconds: 5
cooldownSeconds: 5
maxParallel: 1
tags:
- local
- plugin
- automation
metadata:
source: examples/plugins/automation-events.ts
---
Summarize the local plugin event and report the event subject, topic, and
message payload.
@@ -0,0 +1,42 @@
---
id: pr-changelog-check
title: Check for Changelog Updates in PRs
workspaceRoot: /absolute/path/to/repo
event: github.pull_request.opened
filters:
repository: your-org/your-repo
pullRequest:
baseBranch: main
debounceSeconds: 10
cooldownSeconds: 60
maxParallel: 3
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
tags:
- automation
- github
- documentation
metadata:
owner: development
---
Automatically check if a PR that modifies code also updates the CHANGELOG:
1. Detect if the PR modifies source files (src/, lib/, etc.)
2. Check if the PR also includes changes to CHANGELOG.md or relevant changelogs
3. If significant code changes but no changelog update:
- Extract a summary of the changes
- Suggest what should be added to CHANGELOG
- Request the author to add an entry
4. If CHANGELOG is updated:
- Verify the format matches the project style
- Check that entry is concise and user-facing
- Ensure version number is appropriate
Provide feedback as a comment on the PR:
- ✅ CHANGELOG properly updated
- ⚠️ No CHANGELOG changes detected - please add an entry
- 🤔 CHANGELOG entry format seems off - consider [example]
This helps maintain an up-to-date CHANGELOG without manual reminders.
@@ -0,0 +1,32 @@
---
id: pr-review
title: Review New Pull Requests
workspaceRoot: /absolute/path/to/repo
cwd: /absolute/path/to/repo
event: github.pull_request.opened
filters:
repository: acme/api
pullRequest:
baseBranch: main
debounceSeconds: 30
dedupeWindowSeconds: 600
cooldownSeconds: 120
maxParallel: 2
mode: act
enabled: true
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 1800
maxIterations: 20
tags:
- automation
- github
- review
metadata:
owner: platform
source: normalized-event-ingress
---
Review the opened pull request from the trigger event context. Summarize the
highest-risk changes, call out missing tests or migration risks, and recommend
the next action for the author.
@@ -0,0 +1,55 @@
---
id: pr-test-coverage
title: Analyze Test Coverage Impact of PR
workspaceRoot: /absolute/path/to/repo
event: github.pull_request.synchronize
filters:
repository: your-org/your-repo
pullRequest:
baseBranch: main
debounceSeconds: 30
cooldownSeconds: 120
maxParallel: 2
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
tags:
- automation
- github
- testing
metadata:
owner: qa
checkMetrics:
- lineCoverage
- branchCoverage
- newFilesCoverage
---
Analyze test coverage impact when a PR is updated:
1. Checkout the PR branch
2. Run test coverage: `npm run test:coverage`
3. Compare coverage to main branch:
- Lines added with coverage
- Lines added without coverage
- Files with decreased coverage
- New files with low coverage
4. Generate a coverage impact report showing:
- Coverage change percentage
- Files with added uncovered code
- Critical gaps in new functionality
- Suggestions for missing tests
5. Post results as PR comment with:
- Overall coverage impact (↑ or ↓)
- File-by-file breakdown
- Specific line ranges needing tests
- Recommendations for test additions
Color code the feedback:
- 🟢 Coverage improved
- 🟡 Coverage maintained
- 🔴 Coverage decreased
- ⚫ New code without tests
Help maintain test quality standards without blocking the PR.
@@ -0,0 +1,45 @@
---
id: performance-baseline
title: Track Performance Metrics
workspaceRoot: /absolute/path/to/repo
schedule: "0 2 * * *"
tools: run_commands,read_files,editor
mode: act
enabled: false
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 2400
maxIterations: 20
tags:
- automation
- performance
- monitoring
metadata:
owner: platform
metricsFile: .perf-baseline.json
---
Measure and track performance baselines for the project:
1. Build the project and measure build time: `npm run build`
2. Bundle size analysis (if applicable): Run bundler with size reporting
3. Cold start time if this is a CLI/server tool
4. Run performance benchmarks if they exist
Create or update `.perf-baseline.json` with:
```json
{
"timestamp": "ISO-8601",
"buildTime": "milliseconds",
"bundleSize": "bytes",
"coldStart": "milliseconds",
"metrics": {...}
}
```
Detect performance regressions:
- If build time increased by >10%, flag as warning
- If bundle size increased by >5%, flag as concern
- Compare to previous day's baseline
Report findings with recommendations for optimization if regressions detected.
@@ -0,0 +1,41 @@
---
id: test-coverage-report
title: Generate Test Coverage Report
workspaceRoot: /absolute/path/to/repo
schedule: "0 22 * * *"
tools: run_commands,read_files
mode: act
enabled: false
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 2400
maxIterations: 25
tags:
- automation
- testing
- quality
metadata:
owner: qa
reportFormat: markdown
---
Run test suite and generate a coverage report:
1. Run the full test suite: `npm test` or equivalent
2. Generate coverage report in JSON format: `npm run test:coverage`
3. Parse the coverage data to identify:
- Overall coverage percentage (lines, branches, functions, statements)
- Files with coverage below 80%
- Files with coverage below 50% (critical)
- Coverage trends (if previous reports exist)
Create a markdown summary showing:
- Overall coverage metrics with visual progress bars
- Top 5 files needing coverage improvements
- Test results: total tests, passed, failed, skipped
- Recommendations for improving test coverage
Include emoji indicators for health status:
- 🟢 Excellent (>90%)
- 🟡 Good (70-90%)
- 🔴 Needs attention (<70%)
@@ -0,0 +1,44 @@
---
id: type-check-strict
title: Strict TypeScript Type Checking
workspaceRoot: /absolute/path/to/repo
schedule: "0 6 * * *"
tools: run_commands,read_files
mode: plan
enabled: false
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 1800
maxIterations: 20
tags:
- automation
- quality
- typescript
metadata:
owner: development
strictLevel: strict
---
Run TypeScript type checking with strict compiler options:
1. Run `tsc --noEmit` with strict mode settings
2. Collect all type errors and warnings
3. Categorize errors:
- Missing type annotations
- Implicit any types
- Null/undefined safety issues
- Generic type issues
- Import/export mismatches
Generate a detailed report showing:
- Total type errors
- Errors by category with counts
- Top 10 files with most type errors
- Specific recommendations for each category
Suggest improvements:
- Files that would benefit from JSDoc
- Places where explicit types would improve clarity
- Breaking changes if we made types more strict
Use plan mode to suggest fixes without applying them automatically.
@@ -0,0 +1,67 @@
---
id: weekly-metrics-summary
title: Weekly Project Metrics Summary
workspaceRoot: /absolute/path/to/repo
schedule: "0 17 * * FRI"
tools: run_commands,read_files,search_codebase
mode: act
enabled: false
modelSelection:
providerId: cline
modelId: anthropic/claude-opus-4.7
timeoutSeconds: 1800
maxIterations: 20
tags:
- automation
- metrics
- team
metadata:
owner: leadership
reportFormat: markdown
---
Generate a fun and insightful weekly metrics summary for the team:
Collect metrics from the past 7 days:
1. **Code Activity:**
- Total commits this week
- Lines added/deleted
- Most active contributors
- Most modified files
2. **Quality Metrics:**
- Test pass rate
- Test coverage trend (up/down %)
- New issues introduced vs. fixed
- Type check errors (trend)
3. **Performance:**
- Build time trend
- Bundle size changes
- Performance regressions detected
4. **Pull Requests:**
- PRs opened vs. closed
- Average review time
- PRs by author
- Most reviewed files
5. **Development Velocity:**
- Story points completed (if using)
- Bugs fixed vs. features added
- On-schedule vs. blocked tasks
Create a fun markdown report with:
- 🏆 Top contributor of the week (most commits/reviews)
- 📈 Metrics trending up/down with arrows
- 🎯 Week's accomplishments summary
- ⚠️ Metrics needing attention
- 💡 Insights (e.g., "Performance improved 5% this week!")
- 🔥 "Hot spots" (most frequently modified files)
Include emoji indicators and fun facts:
- 🚀 Most commits in a single day
- 👀 Most reviewed PR
- 🐛 Most bug fixes by individual
Make it celebratory but data-driven. Perfect for team morale on Friday!
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""
Cline Hook: PostToolUse (Python)
Logs tool results after execution and enriches with environment context.
Copy to ~/.cline/hooks/PostToolUse.py and chmod +x
"""
import sys
import json
import os
import subprocess
def get_git_branch():
"""Get current git branch if in a repo."""
try:
result = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True,
text=True,
timeout=2
)
return result.stdout.strip() if result.returncode == 0 else None
except Exception:
return None
def main():
# Read event payload from stdin
try:
event = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(json.dumps({"errorMessage": f"Failed to parse hook input: {e}"}))
return
# Extract tool result information
tool_result = event.get("tool_result", {})
post_tool_use = event.get("postToolUse", {})
tool_name = tool_result.get("name", "unknown")
success = post_tool_use.get("success", False)
duration_ms = post_tool_use.get("executionTimeMs", 0)
# Log result
status = "✅" if success else "❌"
print(f"{status} Tool completed: {tool_name} ({duration_ms}ms)", file=sys.stderr)
# For run_commands, inject environment context
if tool_name == "run_commands":
git_branch = get_git_branch()
context_parts = []
if git_branch:
context_parts.append(f"git branch: {git_branch}")
if context_parts:
context = "Environment: " + ", ".join(context_parts)
print(json.dumps({"context": context}))
return
# Return empty control object
print(json.dumps({}))
if __name__ == "__main__":
main()
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Cline Hook: PostToolUse
# Logs tool results after execution
# Copy to ~/.cline/hooks/PostToolUse.sh and chmod +x
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_result.name // "unknown"')
success=$(echo "$input" | jq -r '.postToolUse.success // false')
duration=$(echo "$input" | jq -r '.postToolUse.executionTimeMs // 0')
status="✅"
[ "$success" != "true" ] && status="❌"
echo "$status Tool completed: $tool (${duration}ms)" >&2
# Return empty object (PostToolUse events are informational)
echo '{}'
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bun
/**
* Cline Hook: PostToolUse (TypeScript)
* Logs tool results with structured output.
* Copy to ~/.cline/hooks/PostToolUse.ts and chmod +x
*/
interface ToolResult {
id: string;
name: string;
input: Record<string, unknown>;
output: unknown;
error: unknown;
durationMs: number;
}
interface HookEvent {
hookName: string;
tool_result: ToolResult;
postToolUse: {
toolName: string;
parameters: Record<string, unknown>;
result: string;
success: boolean;
executionTimeMs: number;
};
}
interface HookControl {
context?: string;
errorMessage?: string;
}
async function getGitBranch(): Promise<string | null> {
try {
const proc = Bun.spawn(["git", "branch", "--show-current"]);
const output = await new Response(proc.stdout).text();
return output.trim() || null;
} catch {
return null;
}
}
async function main(): Promise<void> {
let event: HookEvent;
try {
const input = await Bun.stdin.text();
event = JSON.parse(input) as HookEvent;
} catch (error) {
const message =
error instanceof Error ? error.message : "Unknown parse error";
console.log(
JSON.stringify({ errorMessage: `Failed to parse: ${message}` }),
);
return;
}
const toolName = event.tool_result.name;
const success = event.postToolUse.success;
const durationMs = event.postToolUse.executionTimeMs;
// Log result
const status = success ? "✅" : "❌";
console.error(`${status} Tool: ${toolName} (${durationMs}ms)`);
// For run_commands, inject environment context
if (toolName === "run_commands") {
const branch = await getGitBranch();
if (branch) {
const control: HookControl = {
context: `Environment: git branch: ${branch}`,
};
console.log(JSON.stringify(control));
return;
}
}
// Return empty control
console.log(JSON.stringify({}));
}
main().catch((error) => {
console.error("Hook error:", error);
process.exit(1);
});
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""
Cline Hook: PreToolUse (Python)
Logs every tool call before it executes.
Copy to ~/.cline/hooks/PreToolUse.py and chmod +x
"""
import sys
import json
def main():
# Read event payload from stdin
try:
event = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(json.dumps({"errorMessage": f"Failed to parse hook input: {e}"}))
return
# Extract tool information
tool_call = event.get("tool_call", {})
tool_name = tool_call.get("name", "unknown")
parameters = event.get("preToolUse", {}).get("parameters", {})
# Log to stderr
print(f"🔧 Tool: {tool_name}", file=sys.stderr)
if parameters:
param_str = ", ".join(f"{k}={v}" for k, v in parameters.items())
print(f" Args: {param_str}", file=sys.stderr)
# Return empty control object (allow the tool to execute)
print(json.dumps({}))
if __name__ == "__main__":
main()
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Cline Hook: PreToolUse
# Logs every tool call before it executes
# Copy to ~/.cline/hooks/PreToolUse.sh and chmod +x
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name // "unknown"')
args=$(echo "$input" | jq -r '.preToolUse.parameters | to_entries | map("\(.key)=\(.value)") | join(", ")' 2>/dev/null || echo "")
echo "🔧 Tool: $tool" >&2
if [ -n "$args" ] && [ "$args" != "null" ]; then
echo " Args: $args" >&2
fi
# Return empty object to allow the call to proceed
echo '{}'
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bun
/**
* Cline Hook: PreToolUse (TypeScript)
* Logs and filters tool calls with type safety.
* Copy to ~/.cline/hooks/PreToolUse.ts and chmod +x
*/
interface ToolCall {
id: string;
name: string;
input: Record<string, unknown>;
}
interface HookEvent {
hookName: string;
tool_call: ToolCall;
preToolUse: {
toolName: string;
parameters: Record<string, unknown>;
};
}
interface HookControl {
cancel?: boolean;
review?: boolean;
context?: string;
errorMessage?: string;
overrideInput?: unknown;
}
async function main(): Promise<void> {
let event: HookEvent;
try {
const input = await Bun.stdin.text();
event = JSON.parse(input) as HookEvent;
} catch (error) {
const message =
error instanceof Error ? error.message : "Unknown parse error";
console.log(
JSON.stringify({ errorMessage: `Failed to parse: ${message}` }),
);
return;
}
const toolName = event.tool_call.name;
const parameters = event.preToolUse.parameters;
// Log to stderr
console.error(`🔧 Tool: ${toolName}`);
if (Object.keys(parameters).length > 0) {
const paramStr = Object.entries(parameters)
.map(([k, v]) => `${k}=${v}`)
.join(", ");
console.error(` Args: ${paramStr}`);
}
// Return empty control (allow execution)
const control: HookControl = {};
console.log(JSON.stringify(control));
}
main().catch((error) => {
console.error("Hook error:", error);
process.exit(1);
});
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Cline Hook: PreToolUse (Block Destructive)
# Blocks dangerous operations
# Copy to ~/.cline/hooks/PreToolUse.sh and chmod +x
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name // ""')
cmd=$(echo "$input" | jq -r '.tool_call.input.command // ""')
# Block dangerous git commands
if [ "$tool" = "run_commands" ]; then
if [[ $cmd =~ (git\ reset\ --hard|git\ push\ --force|git\ push\ -f) ]]; then
jq -n --arg cmd "$cmd" '{cancel: true, errorMessage: "Destructive git command blocked: \($cmd). Use with explicit approval."}'
exit 0
fi
# Block bulk deletes
if [[ $cmd =~ (rm\ -rf|rm\ -r.*\*) ]]; then
jq -n --arg cmd "$cmd" '{cancel: true, errorMessage: "Bulk delete blocked: \($cmd). This is too risky."}'
exit 0
fi
fi
# Allow everything else
echo '{}'
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
Cline Hook: PreToolUse (Inject Context)
Injects file context and environment info before tool execution.
Copy to ~/.cline/hooks/PreToolUse.py and chmod +x
"""
import sys
import json
import os
import subprocess
from pathlib import Path
def get_git_branch() -> str | None:
"""Get current git branch."""
try:
result = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True,
text=True,
timeout=2
)
branch = result.stdout.strip()
return branch if branch else None
except Exception:
return None
def get_node_version() -> str | None:
"""Get Node.js version."""
try:
result = subprocess.run(
["node", "--version"],
capture_output=True,
text=True,
timeout=2
)
return result.stdout.strip() if result.returncode == 0 else None
except Exception:
return None
def get_file_context(file_path: str) -> str | None:
"""Get context about related files."""
path = Path(file_path)
# For TypeScript files, check for test files
if path.suffix in [".ts", ".tsx"]:
test_file = path.with_stem(path.stem).with_suffix(".test.ts")
if test_file.exists():
return f"Associated test file exists: {test_file}"
# For package.json, mention lock files
if path.name == "package.json":
lock_files = []
for lock in ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]:
if (path.parent / lock).exists():
lock_files.append(lock)
if lock_files:
return f"Related lock files: {', '.join(lock_files)}"
# For config files, mention related configs
if path.name.endswith(".config.ts") or path.name.endswith(".config.js"):
basename = path.stem.replace(".config", "")
parent = path.parent
related = []
for ext in [".ts", ".js", ".json"]:
env_file = parent / f"{basename}.env{ext}"
if env_file.exists():
related.append(f"{env_file.name}")
if related:
return f"Related config files: {', '.join(related)}"
return None
def main():
try:
event = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(json.dumps({"errorMessage": f"Failed to parse: {e}"}))
return
tool_call = event.get("tool_call", {})
tool_name = tool_call.get("name", "")
tool_input = tool_call.get("input", {})
# Inject file context when reading files
if tool_name == "read_files":
file_path = tool_input.get("filePath", "")
if file_path:
context = get_file_context(file_path)
if context:
print(json.dumps({"context": context}))
return
# Inject environment context for run_commands
if tool_name == "run_commands":
context_parts = []
node_version = get_node_version()
if node_version:
context_parts.append(f"node {node_version}")
git_branch = get_git_branch()
if git_branch:
context_parts.append(f"git branch: {git_branch}")
if context_parts:
context = "Environment: " + ", ".join(context_parts)
print(json.dumps({"context": context}))
return
# Allow other tools without modification
print(json.dumps({}))
if __name__ == "__main__":
main()
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Cline Hook: PreToolUse (Inject File Context)
# Extracts and injects file context before tool execution
# Copy to ~/.cline/hooks/PreToolUse.sh and chmod +x
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name // ""')
filePath=$(echo "$input" | jq -r '.tool_call.input.filePath // ""')
# When reading a file, check for related files to provide context
if [ "$tool" = "read_files" ] && [ -n "$filePath" ]; then
# Extract directory and filename
dir=$(dirname "$filePath")
name=$(basename "$filePath")
# Check for related files (e.g., if reading .ts, mention .test.ts)
context_files=""
# For TypeScript files, check for test files
if [[ $filePath == *.ts || $filePath == *.tsx ]]; then
test_file="${filePath%.ts*}.test.ts"
if [ -f "$test_file" ]; then
context_files="Associated test file exists: $test_file"
fi
fi
# For package.json, suggest related files
if [ "$name" = "package.json" ]; then
related=""
[ -f "$dir/package-lock.json" ] && related="$related package-lock.json"
[ -f "$dir/yarn.lock" ] && related="$related yarn.lock"
[ -f "$dir/pnpm-lock.yaml" ] && related="$related pnpm-lock.yaml"
if [ -n "$related" ]; then
context_files="Related lock files:$related"
fi
fi
if [ -n "$context_files" ]; then
jq -n --arg ctx "$context_files" '{context: $ctx}'
exit 0
fi
fi
# For run_commands, inject environment info
if [ "$tool" = "run_commands" ]; then
node_version=$(node --version 2>/dev/null || echo "not installed")
git_branch=$(git branch --show-current 2>/dev/null || echo "")
context="Environment: node $node_version"
[ -n "$git_branch" ] && context="$context, branch: $git_branch"
jq -n --arg ctx "$context" '{context: $ctx}'
exit 0
fi
# Allow other tools to proceed without modification
echo '{}'
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bun
/**
* Cline Hook: PreToolUse (Modify Input)
* Demonstrates how to modify tool inputs before execution.
* Useful for: normalizing paths, adding default options, sanitizing inputs.
* Copy to ~/.cline/hooks/PreToolUse.ts and chmod +x
*/
interface ToolInput {
[key: string]: unknown;
}
interface ToolCall {
id: string;
name: string;
input: ToolInput;
}
interface HookEvent {
hookName: string;
tool_call: ToolCall;
preToolUse: {
toolName: string;
parameters: Record<string, unknown>;
};
}
interface HookControl {
context?: string;
overrideInput?: Record<string, unknown>;
}
function normalizeFilePath(path: string): string {
// Convert backslashes to forward slashes (Windows paths)
let normalized = path.replace(/\\/g, "/");
// Resolve ~/ to home directory
const home = process.env.HOME || process.env.USERPROFILE;
if (normalized.startsWith("~/") && home) {
normalized = normalized.replace(/^~/, home);
}
return normalized;
}
async function main(): Promise<void> {
let event: HookEvent;
try {
const input = await Bun.stdin.text();
event = JSON.parse(input) as HookEvent;
} catch (error) {
const message =
error instanceof Error ? error.message : "Unknown parse error";
console.log(
JSON.stringify({ errorMessage: `Failed to parse: ${message}` }),
);
return;
}
const toolCall = event.tool_call;
const toolName = toolCall.name;
const toolInput = toolCall.input as Record<string, unknown>;
// Modify read_files input: normalize paths
if (toolName === "read_files" && typeof toolInput.filePath === "string") {
const normalized = normalizeFilePath(toolInput.filePath);
if (normalized !== toolInput.filePath) {
const control: HookControl = {
context: `Normalized file path from "${toolInput.filePath}" to "${normalized}"`,
overrideInput: {
...toolInput,
filePath: normalized,
},
};
console.log(JSON.stringify(control));
return;
}
}
// Modify editor input: add safety defaults
if (toolName === "editor" && typeof toolInput.filePath === "string") {
// For critical files, add a reminder
const filePath = toolInput.filePath as string;
if (
filePath.includes("package.json") ||
filePath.includes(".env") ||
filePath.includes("tsconfig")
) {
const control: HookControl = {
context:
"WARNING: You are about to modify a critical configuration file. Verify changes carefully.",
};
console.log(JSON.stringify(control));
return;
}
}
// Modify run_commands input: add safety flags
if (toolName === "run_commands" && typeof toolInput.command === "string") {
const cmd = toolInput.command as string;
// Add safety flags to npm/yarn installs
if (cmd.includes("npm install") && !cmd.includes("--save")) {
const newCmd = cmd.replace("npm install", "npm install --save-exact");
const control: HookControl = {
context: "Added --save-exact flag for reproducible installs",
overrideInput: {
...toolInput,
command: newCmd,
},
};
console.log(JSON.stringify(control));
return;
}
}
// No modifications needed
console.log(JSON.stringify({}));
}
main().catch((error) => {
console.error("Hook error:", error);
process.exit(1);
});
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Cline Hook: PreToolUse (Require Review)
# Pauses for review before writing to critical files
# Copy to ~/.cline/hooks/PreToolUse.sh and chmod +x
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name // ""')
file=$(echo "$input" | jq -r '.tool_call.input.filePath // ""')
# Require review for critical files
if [ "$tool" = "editor" ] || [ "$tool" = "write_file" ]; then
if [[ $file =~ (package\.json|tsconfig|\.env|secrets|config\.(ts|js)|\.cline) ]]; then
jq -n --arg file "$file" '{review: true, context: "This will modify a critical file: \($file)"}'
exit 0
fi
fi
# Allow everything else
echo '{}'
+436
View File
@@ -0,0 +1,436 @@
# Hook Examples
Examples for file-based hooks and runtime hooks.
## Hook Terminology
Use these terms consistently:
- **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`.
File hooks are an adapter on top of the runtime hook layer. Core discovers hook files, maps their event names onto runtime hook callbacks, then executes the matching script with a JSON payload on stdin.
## File Hooks vs Runtime Hooks
| File Hook File Name | File Hook Event | 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** when you want a workspace or user-configured shell/Python script. **Use runtime hooks** when writing a plugin and needing typed, in-process access to runtime state or wanting to influence model/tool execution.
`beforeRun` and `afterRun` wrap one runtime `run()` or `continue()` invocation. In an interactive session, that means one submitted user turn. `afterRun` fires for completed, aborted, and failed runs; check `result.status` if you only want successful task completion.
For file hooks, successful task completion maps to the `agent_end` event. For a plugin, use `afterRun` and check `result.status === "completed"`.
## 📂 Examples in This Directory
### Bash Examples
#### `PreToolUse.sh`
Log every tool call and its inputs. Useful for auditing what the agent is about to do.
```bash
mkdir -p .cline/hooks
cp examples/hooks/PreToolUse.sh .cline/hooks/
chmod +x .cline/hooks/PreToolUse.sh
cline -i "do something" # See tool calls logged to stderr
```
#### `PostToolUse.sh`
Inspect tool results and add supplementary context.
```bash
mkdir -p .cline/hooks
cp examples/hooks/PostToolUse.sh .cline/hooks/
chmod +x .cline/hooks/PostToolUse.sh
cline -i "do something" # See tool results logged and enriched
```
#### `PreToolUse_BlockDestructive.sh`
Prevent destructive operations like force pushes or bulk deletes.
```bash
mkdir -p .cline/hooks
cp examples/hooks/PreToolUse_BlockDestructive.sh .cline/hooks/PreToolUse.sh
chmod +x .cline/hooks/PreToolUse.sh
cline -i "clean up the repo" # Destructive operations will be blocked
```
#### `PreToolUse_RequireReview.sh`
Require user review before certain operations (file writes to critical files).
```bash
mkdir -p .cline/hooks
cp examples/hooks/PreToolUse_RequireReview.sh .cline/hooks/PreToolUse.sh
chmod +x .cline/hooks/PreToolUse.sh
cline -i "update dependencies" # Critical file writes will pause for review
```
#### `PreToolUse_InjectFileContext.sh`
Extract and inject file context before tool execution (related test files, lock files, environment context).
```bash
mkdir -p .cline/hooks
cp examples/hooks/PreToolUse_InjectFileContext.sh .cline/hooks/PreToolUse.sh
chmod +x .cline/hooks/PreToolUse.sh
cline -i "review the configuration" # Related files will be mentioned automatically
```
#### `TaskStart.sh`, `TaskComplete.sh`, `SessionShutdown.sh`
Track agent session lifecycle events (start, end, shutdown).
```bash
mkdir -p .cline/hooks
cp examples/hooks/TaskStart.sh .cline/hooks/
cp examples/hooks/TaskComplete.sh .cline/hooks/
cp examples/hooks/SessionShutdown.sh .cline/hooks/
chmod +x .cline/hooks/Task*.sh .cline/hooks/SessionShutdown.sh
cline -i "do something" # Session lifecycle will be logged
```
### Python Examples
#### `PreToolUse.py`
Python-based hook to log and filter tool calls.
```bash
mkdir -p .cline/hooks
cp examples/hooks/PreToolUse.py .cline/hooks/
chmod +x .cline/hooks/PreToolUse.py
cline -i "do something" # Python hook will log tool calls
```
#### `PostToolUse.py`
Python-based post-tool-use hook for result enrichment.
```bash
mkdir -p .cline/hooks
cp examples/hooks/PostToolUse.py .cline/hooks/
chmod +x .cline/hooks/PostToolUse.py
cline -i "do something" # Python hook will enrich tool results
```
#### `PreToolUse_InjectContext.py`
Python-based context injection with file analysis (test files, config files, lock files, Node.js version, git branch).
```bash
mkdir -p .cline/hooks
cp examples/hooks/PreToolUse_InjectContext.py .cline/hooks/PreToolUse.py
chmod +x .cline/hooks/PreToolUse.py
cline -i "add a new feature" # Related files and environment will be injected
```
### TypeScript Examples
#### `PreToolUse.ts`
TypeScript hook for advanced tool call filtering and logging.
```bash
mkdir -p .cline/hooks
cp examples/hooks/PreToolUse.ts .cline/hooks/
chmod +x .cline/hooks/PreToolUse.ts
cline -i "do something" # TypeScript hook will execute via bun
```
#### `PostToolUse.ts`
TypeScript hook for post-execution actions.
```bash
mkdir -p .cline/hooks
cp examples/hooks/PostToolUse.ts .cline/hooks/
chmod +x .cline/hooks/PostToolUse.ts
cline -i "do something" # TypeScript hook will execute via bun
```
#### `PreToolUse_ModifyInput.ts`
Modify tool inputs before execution (normalize paths, add defaults, sanitize).
```bash
mkdir -p .cline/hooks
cp examples/hooks/PreToolUse_ModifyInput.ts .cline/hooks/PreToolUse.ts
chmod +x .cline/hooks/PreToolUse.ts
cline -i "install dependencies" # npm install will have --save-exact added automatically
```
## Getting Started
### 1. Copy a hook to your project
**File hooks** go in `.cline/hooks/` and must be named after the event they handle:
```bash
mkdir -p .cline/hooks
# Copy PreToolUse example (pick your language)
cp examples/hooks/PreToolUse.sh .cline/hooks/PreToolUse.sh # Bash
cp examples/hooks/PreToolUse.py .cline/hooks/PreToolUse.py # Python
cp examples/hooks/PreToolUse.ts .cline/hooks/PreToolUse.ts # TypeScript
# Copy PostToolUse example
cp examples/hooks/PostToolUse.sh .cline/hooks/PostToolUse.sh # Bash
cp examples/hooks/PostToolUse.py .cline/hooks/PostToolUse.py # Python
cp examples/hooks/PostToolUse.ts .cline/hooks/PostToolUse.ts # TypeScript
```
### 2. Make it executable
```bash
chmod +x .cline/hooks/PreToolUse.*
chmod +x .cline/hooks/PostToolUse.*
```
### 3. Test it
```bash
cline -i "test prompt"
# Or load from a custom hooks directory:
cline --hooks-dir ./my-hooks -i "test prompt"
```
## Hook Input/Output Format
All hooks receive a detailed JSON event on stdin and must return JSON on stdout.
### Input
**PreToolUse event:**
```json
{
"hookName": "tool_call",
"clineVersion": "1.0.0",
"timestamp": "2026-01-15T10:30:00Z",
"taskId": "conv-123",
"workspaceRoots": ["/path/to/repo"],
"userId": "user",
"iteration": 1,
"tool_call": {
"id": "call-456",
"name": "read_files",
"input": {"filePath": "/path/to/file.ts"}
}
}
```
**PostToolUse event:**
```json
{
"hookName": "tool_result",
"clineVersion": "1.0.0",
"timestamp": "2026-01-15T10:30:00Z",
"tool_result": {
"id": "call-456",
"name": "read_files",
"input": {"filePath": "/path/to/file.ts"},
"output": "file contents here",
"error": null,
"durationMs": 45
}
}
```
**TaskStart and other lifecycle events:**
```json
{
"hookName": "agent_start",
"clineVersion": "1.0.0",
"timestamp": "2026-01-15T10:30:00Z",
"taskId": "conv-123",
"workspaceRoots": ["/path/to/repo"],
"userId": "user"
}
```
### Output
Return a JSON object from stdout. Empty `{}` means "do nothing."
**Available fields:**
| Field | Type | Effect | Event(s) |
|-------|------|--------|----------|
| `cancel` | boolean | Cancels the pending tool call | `PreToolUse` |
| `review` | boolean | Pauses and prompts for user review | `PreToolUse` |
| `context` | string | Injects context into agent's next turn | `PreToolUse`, `PostToolUse` |
| `errorMessage` | string | Surfaces an error to the agent | `PreToolUse` |
| `overrideInput` | object | Replaces tool input before execution | `PreToolUse` |
## Common Patterns
### Log and proceed (bash)
```bash
#!/usr/bin/env bash
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name')
echo "Action: $tool" >&2
echo '{}'
```
### Inject context into next turn
```bash
#!/usr/bin/env bash
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name')
if [ "$tool" = "run_commands" ]; then
branch=$(git branch --show-current 2>/dev/null)
echo "{\"context\": \"Current branch: $branch\"}"
else
echo '{}'
fi
```
### Modify tool input before execution
```bash
#!/usr/bin/env bash
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name')
file=$(echo "$input" | jq -r '.tool_call.input.filePath')
if [ "$tool" = "read_files" ] && [[ $file == ~/* ]]; then
normalized="${file/#\~/$HOME}"
echo "{\"overrideInput\": {\"filePath\": \"$normalized\"}}"
else
echo '{}'
fi
```
### Block specific tools or commands
```bash
#!/usr/bin/env bash
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name')
cmd=$(echo "$input" | jq -r '.tool_call.input.command // empty')
if [ "$tool" = "run_commands" ] && [[ $cmd =~ git\ push\ --force ]]; then
echo '{"cancel": true, "errorMessage": "Force push is blocked."}'
else
echo '{}'
fi
```
### Require review for sensitive files
```bash
#!/usr/bin/env bash
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_call.name')
file=$(echo "$input" | jq -r '.tool_call.input.filePath // empty')
if ([ "$tool" = "editor" ] || [ "$tool" = "write_file" ]) && \
[[ $file =~ (package\.json|\.env|secrets|tsconfig) ]]; then
echo '{"review": true, "context": "This will modify a critical file"}'
else
echo '{}'
fi
```
### Python: Parse and manipulate JSON
```python
#!/usr/bin/env python3
import sys
import json
event = json.load(sys.stdin)
tool_name = event.get("tool_call", {}).get("name", "")
tool_input = event.get("tool_call", {}).get("input", {})
if tool_name == "read_files":
file_path = tool_input.get("filePath", "")
if file_path.endswith(".test.ts"):
print(json.dumps({"context": "This is a test file"}))
else:
print(json.dumps({}))
else:
print(json.dumps({}))
```
### TypeScript: Type-safe hook with async operations
```typescript
#!/usr/bin/env bun
interface HookEvent {
tool_call: { name: string; input: Record<string, unknown> };
}
const event: HookEvent = JSON.parse(await Bun.stdin.text());
const toolName = event.tool_call.name;
if (toolName === "run_commands") {
const branch = await getGitBranch();
console.log(JSON.stringify({ context: `Branch: ${branch}` }));
} else {
console.log(JSON.stringify({}));
}
async function getGitBranch(): Promise<string> {
return "main";
}
```
## Debugging Hooks
### Print hook invocations
```bash
cline --verbose "your prompt"
```
### Test a hook manually
```bash
echo '{"tool_call": {"name": "read_files", "input": {"filePath": "test.ts"}}}' | .cline/hooks/PreToolUse.sh
```
### Check hook output
```bash
.cline/hooks/PreToolUse.sh < input.json | jq .
```
## Runtime Hooks: Custom Compaction
File hooks observe lifecycle events. For more advanced use cases like message compaction, use a TypeScript **runtime hook** plugin:
```bash
mkdir -p .cline/plugins
cp examples/hooks/custom-compaction-hook.example.ts .cline/plugins/custom-compaction-hook.ts
cline -i "Search the codebase for dispatcher usage, then summarize it"
```
This example uses `hooks.beforeModel` to estimate request size and replace older middle history with a summary message before the provider request.
### Runtime Hook vs Message-Builder Compaction
| Example | Extension Point | Message Shape | Best For |
| ------- | --------------- | ------------- | -------- |
| `custom-compaction-hook.example.ts` (in `.cline/plugins/`) | `hooks.beforeModel` runtime hook | Agent runtime request messages with runtime parts such as `tool-call`, `tool-result`, `reasoning`, `image`, and `file` | Cases needing runtime-hook context, current runtime snapshot, or direct request mutation |
| `plugins/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 |
**Prefer `registerMessageBuilder()`** for normal plugin-owned provider-message rewrites because it runs in the core message pipeline before the built-in provider-safety builder. **Use `beforeModel`** when the compaction logic needs runtime hook context or needs to inspect the exact runtime request object.
## Tips
- **Hooks are disabled in `--yolo` mode** — use `--act` or `--plan` to enable them
- **Use stderr for logging** — stdout is reserved for JSON output
- **Keep hooks fast** — they run before every tool call, so performance matters
- **Test with `jq`** — JSON parsing is finicky; use `jq` for safe extraction
- **Use multiple hooks** — different event files can coexist in `.cline/hooks/`
- **Load from custom dirs** — use `--hooks-dir ./ci/hooks` to load from elsewhere
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Cline Hook: SessionShutdown
# Logs when a session shuts down (normally or due to error)
# Copy to ~/.cline/hooks/SessionShutdown.sh and chmod +x
input=$(cat)
timestamp=$(echo "$input" | jq -r '.timestamp // "unknown"')
reason=$(echo "$input" | jq -r '.reason // "unknown"')
echo "🔌 Session shutdown at $timestamp" >&2
echo " Reason: $reason" >&2
# Lifecycle events are informational only
echo '{}'
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
# File hook example: runs when the agent session ends successfully.
#
# Install:
# mkdir -p .cline/hooks
# cp examples/hooks/agent_end.sh .cline/hooks/agent_end.sh
# chmod +x .cline/hooks/agent_end.sh
payload="$(cat)"
if command -v jq >/dev/null 2>&1; then
task_id="$(printf '%s' "$payload" | jq -r '.taskId // .conversation_id // "unknown"')"
output="$(printf '%s' "$payload" | jq -r '.turn.outputText // ""' | tr '\n' ' ' | cut -c 1-180)"
else
task_id="unknown"
output=""
fi
printf '[hook:agent_end] task completed: %s\n' "$task_id" >&2
if [ -n "$output" ]; then
printf '[hook:agent_end] output: %s\n' "$output" >&2
fi
# Empty JSON means "no control changes".
printf '{}\n'
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Cline Hook: TaskStart
# Logs when an agent task starts
# Copy to ~/.cline/hooks/TaskStart.sh and chmod +x
input=$(cat)
timestamp=$(echo "$input" | jq -r '.timestamp // "unknown"')
sessionId=$(echo "$input" | jq -r '.sessionContext.rootSessionId // "unknown"')
echo "🚀 Task started at $timestamp" >&2
echo " Session: $sessionId" >&2
# Lifecycle events are informational only
echo '{}'
@@ -0,0 +1,246 @@
/**
* Custom Compaction Runtime Hook Example
*
* Demonstrates message compaction through a plugin runtime hook. The hook runs
* in `beforeModel`, estimates request size, preserves the first user message
* and recent working context, and replaces older middle history with one
* summary message before the provider request.
*
* Installation:
* mkdir -p .cline/plugins
* cp examples/hooks/custom-compaction-hook.example.ts .cline/plugins/custom-compaction-hook.ts
*
* Usage:
* cline -i "Search the codebase for dispatcher usage, then summarize it"
*
* Note: for most plugin-owned message rewrites, prefer registerMessageBuilder().
* This example exists for cases where you specifically need runtime-hook access
* to the current request and snapshot.
*/
import type { AgentPlugin } from "@cline/core";
type PluginHooks = NonNullable<AgentPlugin["hooks"]>;
type BeforeModelHook = NonNullable<PluginHooks["beforeModel"]>;
type BeforeModelContext = Parameters<BeforeModelHook>[0];
type AgentMessage = BeforeModelContext["request"]["messages"][number];
type AgentMessagePart = AgentMessage["content"][number];
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 {
const trimmed = text.trim();
if (trimmed.length <= limit) {
return trimmed;
}
return `${trimmed.slice(0, limit).trim()}\n...[${trimmed.length - limit} more chars summarized]`;
}
function stringifyUnknown(value: unknown): string {
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function serializePart(message: AgentMessage, part: AgentMessagePart): string {
switch (part.type) {
case "text":
return `[${message.role}]: ${part.text}`;
case "reasoning":
return `[assistant reasoning]: ${preview(part.text, 300)}`;
case "image":
return `[${message.role} image]: ${part.mediaType ?? "unknown"}`;
case "file":
return `[${message.role} file ${part.path}]: ${preview(part.content, 500)}`;
case "tool-call":
return `[assistant tool call]: ${part.toolName}(${stringifyUnknown(part.input)})`;
case "tool-result":
return `[tool result ${part.toolName}]: ${preview(stringifyUnknown(part.output), 500)}`;
}
}
function serializeMessage(message: AgentMessage): string {
return message.content.map((part) => serializePart(message, part)).join("\n");
}
function estimateMessageTokens(message: AgentMessage): number {
return estimateTokens(serializeMessage(message));
}
function isTurnStartMessage(message: AgentMessage): boolean {
return (
message.role === "user" &&
message.content.some((part) => part.type === "text")
);
}
function findFirstUserIndex(messages: readonly AgentMessage[]): number {
return messages.findIndex(isTurnStartMessage);
}
function findRecentStartIndex(messages: readonly AgentMessage[]): 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: readonly AgentMessage[]): string[] {
const names = new Set<string>();
for (const message of messages) {
for (const part of message.content) {
if (part.type === "tool-call" || part.type === "tool-result") {
names.add(part.toolName);
}
}
}
return [...names].sort((left, right) => left.localeCompare(right));
}
function collectTouchedFiles(messages: readonly AgentMessage[]): string[] {
const paths = new Set<string>();
for (const message of messages) {
for (const part of message.content) {
if (part.type === "file") {
paths.add(part.path);
}
if (part.type === "tool-call") {
for (const value of Object.values(
typeof part.input === "object" && part.input !== null
? part.input
: {},
)) {
if (typeof value === "string" && value.includes("/")) {
paths.add(value);
}
}
}
}
}
return [...paths].sort((left, right) => left.localeCompare(right));
}
function buildCompactionSummary(
compacted: readonly AgentMessage[],
tokensBefore: number,
): AgentMessage {
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 {
id: `hook_compaction_${Date.now()}`,
role: "user",
createdAt: Date.now(),
metadata: {
kind: "example_hook_compaction_summary",
tokensBefore,
messagesCompacted: compacted.length,
},
content: [
{
type: "text",
text: `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.`,
},
],
};
}
const plugin: AgentPlugin = {
name: "custom-compaction-before-model-hook",
manifest: {
capabilities: ["hooks"],
},
hooks: {
beforeModel({ request }) {
const messages = request.messages;
const totalTokens = messages.reduce(
(total, message) => total + estimateMessageTokens(message),
0,
);
if (totalTokens < CONTEXT_WINDOW_TOKENS * COMPACT_AT_RATIO) {
return undefined;
}
const firstUserIndex = findFirstUserIndex(messages);
if (firstUserIndex < 0) {
return undefined;
}
const recentStartIndex = Math.max(
firstUserIndex + 1,
findRecentStartIndex(messages),
);
if (recentStartIndex <= firstUserIndex + 1) {
return undefined;
}
const prefix = messages.slice(0, firstUserIndex + 1);
const compacted = messages.slice(firstUserIndex + 1, recentStartIndex);
const recent = messages.slice(recentStartIndex);
if (compacted.length === 0) {
return undefined;
}
return {
messages: [
...prefix,
buildCompactionSummary(compacted, totalTokens),
...recent,
],
};
},
},
};
export { plugin };
export default plugin;
+19
View File
@@ -0,0 +1,19 @@
{
"name": "examples",
"description": "Example apps for the Cline SDK",
"type": "module",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"clean": "rm -rf node_modules dist"
},
"devDependencies": {
"@types/bun": "^1.3.10"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@cline/core": "workspace:*"
}
}
+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 2–3 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;
+31
View File
@@ -0,0 +1,31 @@
{
"extends": "../apps/tsconfig.apps.json",
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"types": ["bun"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": false,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}