mirror of
https://github.com/cline/cline.git
synced 2026-09-24 23:20:16 +08:00
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:
committed by
GitHub
co-authored by
abeatrix
parent
2b2ff9667d
commit
e215d3d91c
@@ -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
|
||||
@@ -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!
|
||||
Executable
+62
@@ -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()
|
||||
Executable
+17
@@ -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 '{}'
|
||||
Executable
+86
@@ -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);
|
||||
});
|
||||
Executable
+34
@@ -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()
|
||||
Executable
+16
@@ -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 '{}'
|
||||
Executable
+66
@@ -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
@@ -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
@@ -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
@@ -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 '{}'
|
||||
Executable
+125
@@ -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
@@ -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 '{}'
|
||||
@@ -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
|
||||
Executable
+14
@@ -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 '{}'
|
||||
Executable
+27
@@ -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'
|
||||
Executable
+14
@@ -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;
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user