mirror of
https://github.com/cline/cline.git
synced 2026-09-09 06:45:53 +08:00
Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd6fabe8ae | ||
|
|
f289b924d1 | ||
|
|
7b9796cc75 | ||
|
|
a8c7978b5c | ||
|
|
ce3b53e237 | ||
|
|
d837712366 | ||
|
|
d966dbf25b | ||
|
|
97fd423583 | ||
|
|
4344eab56a | ||
|
|
ef943d49ff | ||
|
|
154cce1f71 | ||
|
|
3994ac4159 | ||
|
|
1e5af11df2 | ||
|
|
9a4fec5775 | ||
|
|
33f5d5ff99 | ||
|
|
bcb5457c04 | ||
|
|
8dc3fe673d | ||
|
|
d4a71eb269 | ||
|
|
9f98e0d9d2 | ||
|
|
1938ca5744 | ||
|
|
0c12f5220c | ||
|
|
0fe207c018 | ||
|
|
3ed2097c28 | ||
|
|
64d00e1344 | ||
|
|
cdeca5fc50 | ||
|
|
75d5b96927 | ||
|
|
6a99ed7337 | ||
|
|
f516b2d355 | ||
|
|
546fb1f229 | ||
|
|
95a4b910c8 | ||
|
|
53a5433e72 | ||
|
|
842a68d5d7 | ||
|
|
c42add2e1e | ||
|
|
55e2c1a772 | ||
|
|
ab8eaa00d0 | ||
|
|
6af7624142 | ||
|
|
e2661ffacc | ||
|
|
28f6fb016e | ||
|
|
5720f272a0 | ||
|
|
22c2ec7570 | ||
|
|
f68fbbaf84 | ||
|
|
76d36c4797 | ||
|
|
796a6b90c9 | ||
|
|
a4ba38a776 | ||
|
|
bea291576d | ||
|
|
a72c07a5bf | ||
|
|
97333c1f9e | ||
|
|
66f7bca93e | ||
|
|
7e44407344 | ||
|
|
acd9fe5c6a | ||
|
|
f27a5648f6 | ||
|
|
84591294b7 | ||
|
|
f291104aad | ||
|
|
46ebcf548d | ||
|
|
4bd8c3d422 | ||
|
|
8d318dec47 | ||
|
|
7e86ec8766 | ||
|
|
d3f3901549 | ||
|
|
397910a957 | ||
|
|
2e0c975e98 | ||
|
|
519946a14c | ||
|
|
fd0402b2fc | ||
|
|
0ab4deb91b | ||
|
|
1dcddfada4 | ||
|
|
9cbed22c84 | ||
|
|
674a02a6b9 | ||
|
|
4afa9d3ea3 | ||
|
|
22f300c945 | ||
|
|
655917bd3a | ||
|
|
3f39d856be | ||
|
|
68c37dc52d | ||
|
|
e57663aef9 | ||
|
|
a508e4dbab | ||
|
|
1cb691a496 | ||
|
|
e4340039ea | ||
|
|
d35c4ba893 | ||
|
|
91abcea979 | ||
|
|
2df16d91e2 | ||
|
|
1ef9542b85 | ||
|
|
1cb41432a4 | ||
|
|
f0f08a4205 | ||
|
|
4c248208e8 | ||
|
|
446507ed06 | ||
|
|
ca56aa9bfe | ||
|
|
219eab9372 | ||
|
|
dbd8604bd4 | ||
|
|
6f6fbdaabd | ||
|
|
5dc3bbde78 |
@@ -1,208 +0,0 @@
|
||||
---
|
||||
name: cline-sdk
|
||||
description: Comprehensive Cline SDK skill for building AI agents. Covers the Agent runtime, ClineCore sessions, custom tools, plugins, events, LLM providers, scheduling, multi-agent teams, and production deployment. Use for any task involving @cline/sdk or its sub-packages.
|
||||
metadata:
|
||||
references: agent, clinecore
|
||||
---
|
||||
|
||||
# Cline SDK Skill
|
||||
|
||||
Consolidated skill for building AI agents with the Cline SDK. Use the decision trees below to find the right entry point and API surface, then load detailed references.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
Follow these rules in all Cline SDK code:
|
||||
|
||||
1. Install with `npm install @cline/sdk`. The `@cline/sdk` package re-exports everything from `@cline/core`, `@cline/agents`, `@cline/llms`, and `@cline/shared`.
|
||||
2. Requires Node.js 22 or later.
|
||||
3. Use `createTool()` from `@cline/sdk` (or `@cline/shared`) to define tools. Tool names must be `snake_case`.
|
||||
4. Return errors as structured data from tool `execute` functions. Throwing counts as a "mistake" against the agent's mistake limit.
|
||||
5. Use `lifecycle: { completesRun: true }` on tools that should end the agent loop (e.g. a "submit answer" tool).
|
||||
6. When using `ClineCore`, always call `dispose()` when done to clean up resources.
|
||||
7. The standalone `Agent` and `ClineCore` have different event systems. For `Agent`: use `agent.subscribe()` to get `AgentRuntimeEvent` types (text streaming is `"assistant-text-delta"`, result text is `result.outputText`). For `ClineCore`: use `cline.subscribe()` to get `CoreSessionEvent` types (text streaming is `"chunk"` with `payload.type === "text"`, result text is `result.text`). There is no top-level `onEvent` field on `AgentRuntimeConfig` -- use `agent.subscribe()` or `hooks.onEvent` instead. Do not use event types like `"content_update"` or `"content_start"` with `agent.subscribe()` -- those are internal legacy types from the ClineCore adapter layer.
|
||||
|
||||
## How to Use This Skill
|
||||
|
||||
### Reference File Structure
|
||||
|
||||
The two main API surfaces (`Agent` and `ClineCore`) follow a 4-file pattern. Cross-cutting concepts are single-file guides.
|
||||
|
||||
Each main API surface in `./references/<api>/` contains:
|
||||
|
||||
| File | Purpose | When to Read |
|
||||
|------|---------|--------------|
|
||||
| `REFERENCE.md` | Overview, when to use, quick start | Always read first |
|
||||
| `api.md` | Full API: classes, methods, config, types | Writing code |
|
||||
| `patterns.md` | Common patterns, best practices | Implementation guidance |
|
||||
| `gotchas.md` | Pitfalls, limitations, debugging | Troubleshooting |
|
||||
|
||||
Cross-cutting concepts in `./references/<concept>/` have `REFERENCE.md` as the entry point.
|
||||
|
||||
### Reading Order
|
||||
|
||||
1. Start with `REFERENCE.md` for your chosen API surface
|
||||
2. Then read additional files relevant to your task:
|
||||
- Writing agent code -> `api.md`
|
||||
- Common patterns -> `patterns.md`
|
||||
- Creating tools -> `tools/REFERENCE.md`
|
||||
- Adding plugins/hooks -> `plugins/REFERENCE.md`
|
||||
- Configuring LLM providers -> `providers/REFERENCE.md`
|
||||
- Streaming events -> `events/REFERENCE.md`
|
||||
- Deploying to production -> `production/REFERENCE.md`
|
||||
- Scheduling agents -> `scheduling/REFERENCE.md`
|
||||
- Multi-agent orchestration -> `multi-agent/REFERENCE.md`
|
||||
- Debugging -> `gotchas.md`
|
||||
|
||||
### Example Paths
|
||||
|
||||
```
|
||||
./references/agent/REFERENCE.md # Start here for lightweight agents
|
||||
./references/clinecore/REFERENCE.md # Start here for full runtime
|
||||
./references/agent/api.md # Agent class, config, methods
|
||||
./references/tools/REFERENCE.md # Creating and using tools
|
||||
./references/plugins/REFERENCE.md # Plugin system
|
||||
./references/providers/REFERENCE.md # LLM provider configuration
|
||||
```
|
||||
|
||||
## Quick Decision Trees
|
||||
|
||||
### "Which API surface should I use?"
|
||||
|
||||
```
|
||||
Which API?
|
||||
+-- I want a simple, stateless agent with custom tools
|
||||
| +-- agent/ (Agent class from @cline/agents)
|
||||
+-- I need session persistence, built-in tools, config discovery
|
||||
| +-- clinecore/ (ClineCore from @cline/core)
|
||||
+-- I want built-in file/shell/search/web tools
|
||||
| +-- clinecore/ (has built-in tools; Agent does not)
|
||||
+-- I want scheduled or recurring agents
|
||||
| +-- clinecore/ (automation API)
|
||||
+-- I need multi-process or multi-client session sharing
|
||||
| +-- clinecore/ (hub-backed runtime)
|
||||
+-- I'm building a browser-compatible agent
|
||||
| +-- agent/ (no Node.js dependencies)
|
||||
```
|
||||
|
||||
### "I need to create tools"
|
||||
|
||||
```
|
||||
Tools?
|
||||
+-- Define a custom tool with schema -> tools/REFERENCE.md
|
||||
+-- Use built-in tools (bash, editor, read_files) -> tools/REFERENCE.md (built-in section)
|
||||
+-- Control tool approval/policies -> tools/REFERENCE.md (policies section)
|
||||
+-- Tool that ends the agent loop -> tools/REFERENCE.md (completion tools)
|
||||
+-- Package tools as a reusable plugin -> plugins/REFERENCE.md
|
||||
```
|
||||
|
||||
### "I need to handle events"
|
||||
|
||||
```
|
||||
Events?
|
||||
+-- Stream text/reasoning in real time -> events/REFERENCE.md
|
||||
+-- Track token usage and costs -> events/REFERENCE.md
|
||||
+-- Watch tool calls -> events/REFERENCE.md
|
||||
+-- Detect completion/errors -> events/REFERENCE.md
|
||||
+-- Hook into lifecycle stages -> plugins/REFERENCE.md
|
||||
```
|
||||
|
||||
### "I need to configure a model provider"
|
||||
|
||||
```
|
||||
Providers?
|
||||
+-- Anthropic (Claude) -> providers/REFERENCE.md
|
||||
+-- OpenAI (GPT) -> providers/REFERENCE.md
|
||||
+-- Google (Gemini/Vertex) -> providers/REFERENCE.md
|
||||
+-- AWS Bedrock -> providers/REFERENCE.md
|
||||
+-- Mistral -> providers/REFERENCE.md
|
||||
+-- OpenAI-compatible (vLLM, Together, etc.) -> providers/REFERENCE.md
|
||||
+-- Custom/self-hosted provider -> providers/REFERENCE.md
|
||||
```
|
||||
|
||||
### "I need plugins or hooks"
|
||||
|
||||
```
|
||||
Plugins?
|
||||
+-- Package tools + hooks together -> plugins/REFERENCE.md
|
||||
+-- Observe tool calls (logging, metrics) -> plugins/REFERENCE.md
|
||||
+-- Intercept lifecycle events -> plugins/REFERENCE.md
|
||||
+-- Add system prompt rules -> plugins/REFERENCE.md
|
||||
+-- Distribute via npm/git -> plugins/REFERENCE.md
|
||||
```
|
||||
|
||||
### "I need multi-agent coordination"
|
||||
|
||||
```
|
||||
Multi-agent?
|
||||
+-- Spawn one-off background agents -> multi-agent/REFERENCE.md (sub-agents)
|
||||
+-- Persistent cross-session teams -> multi-agent/REFERENCE.md (teams)
|
||||
+-- Parent-child delegation -> multi-agent/REFERENCE.md (sub-agents)
|
||||
+-- Peer-to-peer task board -> multi-agent/REFERENCE.md (teams)
|
||||
```
|
||||
|
||||
### "I need scheduling or automation"
|
||||
|
||||
```
|
||||
Scheduling?
|
||||
+-- Recurring cron jobs -> scheduling/REFERENCE.md
|
||||
+-- One-off scheduled tasks -> scheduling/REFERENCE.md
|
||||
+-- Event-driven triggers -> scheduling/REFERENCE.md
|
||||
+-- CLI schedule management -> scheduling/REFERENCE.md
|
||||
```
|
||||
|
||||
### "I need to go to production"
|
||||
|
||||
```
|
||||
Production?
|
||||
+-- Error handling and status checks -> production/REFERENCE.md
|
||||
+-- Cost control and token limits -> production/REFERENCE.md
|
||||
+-- Observability (OpenTelemetry) -> production/REFERENCE.md
|
||||
+-- Security and sandboxing -> production/REFERENCE.md
|
||||
+-- Deployment patterns -> production/REFERENCE.md
|
||||
```
|
||||
|
||||
### Troubleshooting Index
|
||||
|
||||
- Agent loop not stopping -> `tools/REFERENCE.md` (completion tools)
|
||||
- Tool errors crashing the agent -> `agent/gotchas.md` or `clinecore/gotchas.md`
|
||||
- Provider auth failures -> `providers/REFERENCE.md`
|
||||
- Session not persisting -> `clinecore/gotchas.md`
|
||||
- Token usage too high -> `production/REFERENCE.md` (cost control)
|
||||
- Hub connection issues -> `clinecore/gotchas.md`
|
||||
- Plugin not loading -> `plugins/REFERENCE.md`
|
||||
- Events not firing -> `events/REFERENCE.md`
|
||||
|
||||
## Product Index
|
||||
|
||||
### API Surfaces
|
||||
| API | Entry File | Description |
|
||||
|-----|------------|-------------|
|
||||
| Agent | `./references/agent/REFERENCE.md` | Lightweight stateless agent loop |
|
||||
| ClineCore | `./references/clinecore/REFERENCE.md` | Full runtime with sessions, persistence, built-in tools |
|
||||
|
||||
### Cross-Cutting Concepts
|
||||
| Concept | Entry File | Description |
|
||||
|---------|------------|-------------|
|
||||
| Tools | `./references/tools/REFERENCE.md` | Built-in and custom tool creation |
|
||||
| Plugins | `./references/plugins/REFERENCE.md` | Extension system with hooks |
|
||||
| Events | `./references/events/REFERENCE.md` | Real-time streaming events |
|
||||
| Providers | `./references/providers/REFERENCE.md` | LLM provider configuration |
|
||||
| Production | `./references/production/REFERENCE.md` | Deployment, security, observability |
|
||||
| Scheduling | `./references/scheduling/REFERENCE.md` | Cron jobs and automation |
|
||||
| Multi-Agent | `./references/multi-agent/REFERENCE.md` | Teams and sub-agents |
|
||||
|
||||
### Package Map
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `@cline/sdk` | Everything you need, install this one |
|
||||
| `@cline/core` | Sessions, persistence, built-in tools, config, hub |
|
||||
| `@cline/agents` | Stateless agent loop, tool orchestration, streaming |
|
||||
| `@cline/llms` | LLM provider gateway |
|
||||
| `@cline/shared` | Types, tool helpers, hook engine |
|
||||
|
||||
## Resources
|
||||
|
||||
Repository: https://github.com/cline/cline
|
||||
SDK Source: https://github.com/cline/cline/tree/main/sdk
|
||||
Documentation: https://docs.cline.bot/sdk/overview
|
||||
Discord: https://discord.gg/cline
|
||||
@@ -1,107 +0,0 @@
|
||||
# Agent Runtime
|
||||
|
||||
The `Agent` class (also exported as `AgentRuntime`) is the lightweight, stateless agent loop from `@cline/agents`. It handles the core iteration cycle: send messages to an LLM, execute tool calls, collect results, and repeat until the task is done.
|
||||
|
||||
## When to Use Agent
|
||||
|
||||
| Use Agent when... | Use ClineCore instead when... |
|
||||
|---|---|
|
||||
| You want a simple agent with custom tools | You need built-in tools (bash, editor, etc.) |
|
||||
| You want minimal dependencies | You need session persistence |
|
||||
| You need browser compatibility | You need config discovery from `.cline/` |
|
||||
| You're building a stateless worker | You need multi-process session sharing |
|
||||
| You want full control over the runtime | You want batteries-included setup |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@cline/sdk"
|
||||
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
tools: [],
|
||||
})
|
||||
|
||||
const result = await agent.run("What is the capital of France?")
|
||||
console.log(result.outputText)
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
The Agent operates in a loop:
|
||||
1. Accept user input (string, message, or array of messages)
|
||||
2. Build turn context (system prompt, messages, tools)
|
||||
3. Call the LLM provider
|
||||
4. If the model returns tool calls, execute them and loop back to step 3
|
||||
5. If the model returns text without tool calls, the run completes
|
||||
6. Emit events throughout for streaming
|
||||
|
||||
The agent is stateless in the sense that it does not persist anything to disk. Conversation history is held in memory and can be accessed via `snapshot()`.
|
||||
|
||||
## Key APIs
|
||||
|
||||
- `new Agent(config)` or `createAgent(config)` - Create an agent
|
||||
- `agent.run(input)` - Start a run with user input
|
||||
- `agent.continue(input?)` - Continue an existing conversation
|
||||
- `agent.abort(reason?)` - Cancel an active run
|
||||
- `agent.subscribe(listener)` - Listen to streaming events
|
||||
- `agent.snapshot()` - Get current runtime state
|
||||
- `agent.restore(messages)` - Replace message history
|
||||
|
||||
See `api.md` for full API details.
|
||||
|
||||
## Multi-Turn Conversations
|
||||
|
||||
```typescript
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
tools: [],
|
||||
})
|
||||
|
||||
const first = await agent.run("What is 2 + 2?")
|
||||
console.log(first.outputText)
|
||||
|
||||
const second = await agent.continue("Now multiply that by 3")
|
||||
console.log(second.outputText)
|
||||
```
|
||||
|
||||
Use `agent.hasRun` to check if a run has already been executed, which determines whether to call `run()` or `continue()`.
|
||||
|
||||
## Event Streaming
|
||||
|
||||
Use `agent.subscribe()` to stream events in real time. Register the listener before calling `run()` to avoid missing early events.
|
||||
|
||||
There is no top-level `onEvent` field on the Agent config. For an async alternative, use `hooks.onEvent` (see `api.md` and `gotchas.md`).
|
||||
|
||||
```typescript
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
tools: [],
|
||||
})
|
||||
|
||||
agent.subscribe((event) => {
|
||||
if (event.type === "assistant-text-delta") {
|
||||
process.stdout.write(event.text)
|
||||
}
|
||||
})
|
||||
|
||||
const result = await agent.run("What is the capital of France?")
|
||||
```
|
||||
|
||||
See `events/REFERENCE.md` for the full event type catalog.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- `api.md` - Full Agent API reference
|
||||
- `patterns.md` - Common patterns and best practices
|
||||
- `gotchas.md` - Pitfalls and debugging
|
||||
- `../tools/REFERENCE.md` - Creating custom tools
|
||||
- `../events/REFERENCE.md` - Event system details
|
||||
- `../providers/REFERENCE.md` - Provider configuration
|
||||
@@ -1,231 +0,0 @@
|
||||
# Agent API Reference
|
||||
|
||||
## Constructor
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@cline/sdk"
|
||||
|
||||
const agent = new Agent(config: AgentRuntimeConfig)
|
||||
```
|
||||
|
||||
Also available via factory function:
|
||||
|
||||
```typescript
|
||||
import { createAgent } from "@cline/sdk"
|
||||
|
||||
const agent = createAgent(config)
|
||||
```
|
||||
|
||||
## AgentRuntimeConfig
|
||||
|
||||
Two config forms exist as a discriminated union:
|
||||
|
||||
### With Provider ID (recommended)
|
||||
|
||||
```typescript
|
||||
interface AgentRuntimeConfigWithProvider {
|
||||
providerId: string // e.g. "anthropic", "openai", "gemini"
|
||||
modelId: string // e.g. "claude-sonnet-4-6", "gpt-5.5"
|
||||
apiKey?: string // provider API key
|
||||
baseUrl?: string // custom endpoint
|
||||
headers?: Record<string, string>
|
||||
|
||||
systemPrompt?: string
|
||||
tools?: AgentTool[]
|
||||
initialMessages?: AgentMessage[]
|
||||
toolPolicies?: Record<string, ToolPolicy>
|
||||
hooks?: Partial<AgentRuntimeHooks>
|
||||
plugins?: AgentPlugin[]
|
||||
}
|
||||
```
|
||||
|
||||
### With Pre-built Model
|
||||
|
||||
```typescript
|
||||
interface AgentRuntimeConfigWithModel {
|
||||
model: AgentModel // pre-built model from gateway
|
||||
|
||||
systemPrompt?: string
|
||||
tools?: AgentTool[]
|
||||
initialMessages?: AgentMessage[]
|
||||
toolPolicies?: Record<string, ToolPolicy>
|
||||
hooks?: Partial<AgentRuntimeHooks>
|
||||
plugins?: AgentPlugin[]
|
||||
}
|
||||
```
|
||||
|
||||
Note: there is no top-level `onEvent` field on `AgentRuntimeConfig`. For event streaming, use `agent.subscribe()` or `hooks.onEvent` (see AgentRuntimeHooks below).
|
||||
|
||||
## Methods
|
||||
|
||||
### run(input)
|
||||
|
||||
Start the agent with user input. Returns when the agent loop completes.
|
||||
|
||||
```typescript
|
||||
const result: AgentRunResult = await agent.run("Build a REST API")
|
||||
```
|
||||
|
||||
Input can be a string, an `AgentMessage`, or an array of `AgentMessage[]`.
|
||||
|
||||
### continue(input?)
|
||||
|
||||
Continue an existing conversation with optional new input.
|
||||
|
||||
```typescript
|
||||
const result = await agent.continue("Now add authentication")
|
||||
```
|
||||
|
||||
### abort(reason?)
|
||||
|
||||
Cancel the currently active run.
|
||||
|
||||
```typescript
|
||||
agent.abort("User cancelled")
|
||||
```
|
||||
|
||||
### subscribe(listener)
|
||||
|
||||
Register a listener for streaming events.
|
||||
|
||||
```typescript
|
||||
const unsubscribe = agent.subscribe((event: AgentRuntimeEvent) => {
|
||||
// handle event
|
||||
})
|
||||
|
||||
// Later: stop listening
|
||||
unsubscribe()
|
||||
```
|
||||
|
||||
### snapshot()
|
||||
|
||||
Get the current runtime state including message history.
|
||||
|
||||
```typescript
|
||||
const state: AgentRuntimeStateSnapshot = agent.snapshot()
|
||||
```
|
||||
|
||||
### restore(messages)
|
||||
|
||||
Replace the agent's message history.
|
||||
|
||||
```typescript
|
||||
agent.restore(previousMessages)
|
||||
```
|
||||
|
||||
### hasRun
|
||||
|
||||
Boolean property indicating whether `run()` has been called at least once.
|
||||
|
||||
```typescript
|
||||
if (agent.hasRun) {
|
||||
await agent.continue(input)
|
||||
} else {
|
||||
await agent.run(input)
|
||||
}
|
||||
```
|
||||
|
||||
## AgentRunResult
|
||||
|
||||
Returned by `run()` and `continue()`.
|
||||
|
||||
```typescript
|
||||
interface AgentRunResult {
|
||||
agentId: string
|
||||
agentRole?: string
|
||||
runId: string
|
||||
status: "completed" | "aborted" | "failed"
|
||||
iterations: number
|
||||
outputText: string
|
||||
messages: readonly AgentMessage[]
|
||||
usage: AgentUsage
|
||||
error?: Error
|
||||
}
|
||||
```
|
||||
|
||||
### Status Values
|
||||
|
||||
- `"completed"` - Agent finished normally
|
||||
- `"aborted"` - Cancelled via `abort()`
|
||||
- `"failed"` - Unrecoverable error
|
||||
|
||||
## AgentMessage
|
||||
|
||||
```typescript
|
||||
interface AgentMessage {
|
||||
id: string
|
||||
role: "user" | "assistant" | "tool"
|
||||
content: AgentMessagePart[]
|
||||
createdAt: number
|
||||
metadata?: Record<string, unknown>
|
||||
modelInfo?: { id: string; provider: string; family?: string }
|
||||
metrics?: {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheReadTokens?: number
|
||||
cacheWriteTokens?: number
|
||||
cost?: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AgentUsage
|
||||
|
||||
```typescript
|
||||
interface AgentUsage {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheWriteTokens: number
|
||||
totalInputTokens: number
|
||||
totalOutputTokens: number
|
||||
totalCost?: number
|
||||
}
|
||||
```
|
||||
|
||||
## AgentRuntimeHooks
|
||||
|
||||
```typescript
|
||||
interface AgentRuntimeHooks {
|
||||
beforeRun?(context): AgentStopControl | undefined
|
||||
afterRun?(context): void
|
||||
beforeModel?(context): AgentBeforeModelResult | undefined
|
||||
afterModel?(context): AgentStopControl | undefined
|
||||
beforeTool?(context): AgentBeforeToolResult | undefined
|
||||
afterTool?(context): AgentAfterToolResult | undefined
|
||||
onEvent?(event: AgentRuntimeEvent): void | Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
Hooks can intercept and modify behavior at each stage. Return a stop control from `beforeRun`, `afterModel`, or `beforeTool` to halt the agent loop.
|
||||
|
||||
`hooks.onEvent` receives the same `AgentRuntimeEvent` types as `agent.subscribe()`, but hook callbacks are awaited (can be async), while `subscribe()` listeners are called synchronously. Use `subscribe()` for UI streaming and `hooks.onEvent` for async side effects like logging to an external service.
|
||||
|
||||
## AgentRuntimeStateSnapshot
|
||||
|
||||
```typescript
|
||||
interface AgentRuntimeStateSnapshot {
|
||||
messages: readonly AgentMessage[]
|
||||
usage: AgentUsage
|
||||
iterations: number
|
||||
status: string
|
||||
}
|
||||
```
|
||||
|
||||
## Factory: createAgentRuntime
|
||||
|
||||
Lower-level factory that returns the same `Agent` class:
|
||||
|
||||
```typescript
|
||||
import { createAgentRuntime } from "@cline/sdk"
|
||||
|
||||
const runtime = createAgentRuntime(config)
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `REFERENCE.md` - Overview and quick start
|
||||
- `patterns.md` - Common patterns
|
||||
- `../tools/REFERENCE.md` - Tool creation
|
||||
- `../events/REFERENCE.md` - Event types
|
||||
- `../providers/REFERENCE.md` - Provider setup
|
||||
@@ -1,134 +0,0 @@
|
||||
# Agent Gotchas
|
||||
|
||||
## Agent Loop Never Stops
|
||||
|
||||
If the agent keeps iterating without completing:
|
||||
|
||||
- Make sure at least one tool has `lifecycle: { completesRun: true }` if you want the agent to explicitly finish.
|
||||
- Without any tools, the agent will complete after the model returns text without tool calls.
|
||||
- If using tools, ensure the system prompt guides the model toward calling the completion tool when done.
|
||||
- Check that `completesRun` tools return successfully (not throwing errors).
|
||||
|
||||
## Tool Errors Count as Mistakes
|
||||
|
||||
When a tool's `execute` function throws an exception, the SDK counts it as a "mistake." After too many mistakes, the agent stops with a `mistake_limit` finish reason.
|
||||
|
||||
Instead, return errors as structured data:
|
||||
|
||||
```typescript
|
||||
// Bad: throwing
|
||||
execute: async (input) => {
|
||||
throw new Error("File not found")
|
||||
}
|
||||
|
||||
// Good: returning error data
|
||||
execute: async (input) => {
|
||||
return { error: "File not found", path: input.path }
|
||||
}
|
||||
```
|
||||
|
||||
## run() vs continue()
|
||||
|
||||
- Call `run()` for the first interaction. It sets up the conversation.
|
||||
- Call `continue()` for subsequent messages. It appends to the existing conversation.
|
||||
- Calling `run()` a second time resets the conversation history.
|
||||
- Use `agent.hasRun` to check which method to call.
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
`@cline/agents` (and by extension, the `Agent` class) is browser-safe with no Node.js dependencies. However, `@cline/core` and `ClineCore` require Node.js 22+. If you import from `@cline/sdk`, you get everything including the Node-only code. For browser usage, import directly from `@cline/agents`:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@cline/agents"
|
||||
```
|
||||
|
||||
## No Top-Level onEvent on Agent Config
|
||||
|
||||
`AgentRuntimeConfig` does not have a top-level `onEvent` field. Passing `onEvent` to `new Agent({ onEvent: ... })` has no effect. There are two ways to receive events:
|
||||
|
||||
```typescript
|
||||
// Option 1: subscribe() - synchronous, best for UI streaming
|
||||
const agent = new Agent({ ...config })
|
||||
agent.subscribe((event) => {
|
||||
if (event.type === "assistant-text-delta") {
|
||||
process.stdout.write(event.text)
|
||||
}
|
||||
})
|
||||
|
||||
// Option 2: hooks.onEvent - awaited, best for async side effects
|
||||
const agent = new Agent({
|
||||
...config,
|
||||
hooks: {
|
||||
onEvent: async (event) => {
|
||||
if (event.type === "assistant-text-delta") {
|
||||
await logToService(event.text)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Both receive the same `AgentRuntimeEvent` types. Prefer `subscribe()` for streaming UI.
|
||||
|
||||
## Event Listener Timing
|
||||
|
||||
Register event listeners via `subscribe()` before calling `run()`:
|
||||
|
||||
```typescript
|
||||
// Good: subscribe before run
|
||||
agent.subscribe(handler)
|
||||
const result = await agent.run(input)
|
||||
|
||||
// Bad: subscribing after run starts loses early events
|
||||
const promise = agent.run(input)
|
||||
agent.subscribe(handler) // may miss events
|
||||
```
|
||||
|
||||
## Tool Input Schema Matters
|
||||
|
||||
The model uses the tool's `inputSchema` to decide what arguments to pass. A vague or missing schema leads to incorrect tool calls.
|
||||
|
||||
- Use `z.enum()` for fixed value sets, not free-form strings
|
||||
- Describe every property with `.describe()` in Zod or `description` in JSON Schema
|
||||
- Include constraints (rate limits, max values) in the tool description
|
||||
|
||||
## Memory and Long Conversations
|
||||
|
||||
The Agent holds all messages in memory. For long-running conversations, memory usage grows with each turn. Consider:
|
||||
|
||||
- Using `ClineCore` with compaction for long sessions
|
||||
- Periodically creating a new agent with a summary of the conversation
|
||||
- Monitoring `result.usage.totalInputTokens` to track context growth
|
||||
|
||||
## Abort Signal Handling in Tools
|
||||
|
||||
Long-running tools should respect the abort signal:
|
||||
|
||||
```typescript
|
||||
execute: async (input, context) => {
|
||||
for (const item of items) {
|
||||
if (context.abortSignal?.aborted) {
|
||||
return { partial: results, aborted: true }
|
||||
}
|
||||
results.push(await process(item))
|
||||
}
|
||||
return { results }
|
||||
}
|
||||
```
|
||||
|
||||
## Provider API Key
|
||||
|
||||
If you get authentication errors, check:
|
||||
|
||||
- `apiKey` is set in the config or via environment variables
|
||||
- The key matches the `providerId` (e.g., Anthropic key for `providerId: "anthropic"`)
|
||||
- For OpenAI-compatible providers, both `apiKey` and `baseUrl` are set
|
||||
|
||||
See `../providers/REFERENCE.md` for provider-specific setup.
|
||||
|
||||
## See Also
|
||||
|
||||
- `api.md` - Full API reference
|
||||
- `patterns.md` - Common patterns
|
||||
- `../tools/REFERENCE.md` - Tool creation
|
||||
- `../clinecore/REFERENCE.md` - Use ClineCore for persistence
|
||||
@@ -1,258 +0,0 @@
|
||||
# Agent Patterns
|
||||
|
||||
## Interactive CLI Agent
|
||||
|
||||
A multi-turn conversational agent in the terminal with streaming output:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@cline/sdk"
|
||||
import * as readline from "node:readline"
|
||||
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
systemPrompt: "You are a helpful assistant. Keep responses concise.",
|
||||
tools: [],
|
||||
})
|
||||
|
||||
agent.subscribe((event) => {
|
||||
if (event.type === "assistant-text-delta") {
|
||||
process.stdout.write(event.text)
|
||||
}
|
||||
})
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
|
||||
function prompt(): void {
|
||||
rl.question("\nYou: ", async (input) => {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed || trimmed === "exit") {
|
||||
rl.close()
|
||||
return
|
||||
}
|
||||
|
||||
process.stdout.write("\nAssistant: ")
|
||||
|
||||
if (agent.hasRun) {
|
||||
await agent.continue(trimmed)
|
||||
} else {
|
||||
await agent.run(trimmed)
|
||||
}
|
||||
|
||||
process.stdout.write("\n")
|
||||
prompt()
|
||||
})
|
||||
}
|
||||
|
||||
prompt()
|
||||
```
|
||||
|
||||
## Conversational Agent (Slack Bot, Chat App)
|
||||
|
||||
Maintain per-thread agents with conversation memory:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@cline/sdk"
|
||||
|
||||
const agents = new Map<string, Agent>()
|
||||
|
||||
async function handleMessage(threadId: string, message: string) {
|
||||
let agent = agents.get(threadId)
|
||||
if (!agent) {
|
||||
agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
systemPrompt: "You are a concise assistant.",
|
||||
tools: [],
|
||||
})
|
||||
agents.set(threadId, agent)
|
||||
}
|
||||
|
||||
const result = agent.hasRun
|
||||
? await agent.continue(message)
|
||||
: await agent.run(message)
|
||||
|
||||
return result.outputText
|
||||
}
|
||||
```
|
||||
|
||||
## Streaming UI
|
||||
|
||||
Build a real-time UI by handling events via `subscribe()`:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
tools: [myTool],
|
||||
})
|
||||
|
||||
agent.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case "assistant-text-delta":
|
||||
ui.appendText(event.text)
|
||||
break
|
||||
case "assistant-message":
|
||||
ui.endText()
|
||||
break
|
||||
case "turn-started":
|
||||
ui.startTurn(event.iteration)
|
||||
break
|
||||
case "turn-finished":
|
||||
if (event.toolCallCount > 0) ui.showToolCount(event.toolCallCount)
|
||||
break
|
||||
case "usage-updated":
|
||||
ui.updateUsage(event.usage.inputTokens, event.usage.outputTokens)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
const result = await agent.run("Hello!")
|
||||
```
|
||||
|
||||
## Structured Output via Completion Tool
|
||||
|
||||
Use a tool with `completesRun: true` to extract structured data:
|
||||
|
||||
```typescript
|
||||
import { Agent, createTool } from "@cline/sdk"
|
||||
import { z } from "zod"
|
||||
|
||||
const submitReview = createTool({
|
||||
name: "submit_review",
|
||||
description: "Submit the final code review with structured feedback.",
|
||||
inputSchema: z.object({
|
||||
summary: z.string(),
|
||||
issues: z.array(z.object({
|
||||
file: z.string(),
|
||||
line: z.number(),
|
||||
severity: z.enum(["error", "warning", "info"]),
|
||||
message: z.string(),
|
||||
})),
|
||||
approved: z.boolean(),
|
||||
}),
|
||||
lifecycle: { completesRun: true },
|
||||
execute: async (input) => input,
|
||||
})
|
||||
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
systemPrompt: "Review the code diff and submit structured feedback.",
|
||||
tools: [submitReview],
|
||||
})
|
||||
|
||||
const result = await agent.run(diffContent)
|
||||
const review = result.toolCalls.find(tc => tc.name === "submit_review")
|
||||
console.log(review?.output)
|
||||
```
|
||||
|
||||
## Agent with Abort/Timeout
|
||||
|
||||
```typescript
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
systemPrompt: "Analyze this data.",
|
||||
tools: [],
|
||||
})
|
||||
|
||||
const timeout = setTimeout(() => agent.abort("Timeout"), 30_000)
|
||||
|
||||
try {
|
||||
const result = await agent.run(data)
|
||||
if (result.status === "aborted") {
|
||||
console.log("Agent was aborted")
|
||||
} else {
|
||||
console.log(result.outputText)
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
```
|
||||
|
||||
## Agent with Plugins
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@cline/sdk"
|
||||
import type { AgentPlugin } from "@cline/sdk"
|
||||
|
||||
const loggingPlugin: AgentPlugin = {
|
||||
name: "logging",
|
||||
manifest: { capabilities: ["hooks"] },
|
||||
setup() {},
|
||||
hooks: {
|
||||
beforeTool({ toolCall }) {
|
||||
console.log(`Calling tool: ${toolCall.toolName}`)
|
||||
},
|
||||
afterRun({ result }) {
|
||||
console.log(`Completed in ${result.iterations} iterations`)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
tools: [myTool],
|
||||
plugins: [loggingPlugin],
|
||||
})
|
||||
```
|
||||
|
||||
## Restoring State Across Sessions
|
||||
|
||||
Save and restore agent state manually:
|
||||
|
||||
```typescript
|
||||
// Save state
|
||||
const snapshot = agent.snapshot()
|
||||
const serialized = JSON.stringify(snapshot.messages)
|
||||
|
||||
// Later: restore
|
||||
const agent2 = new Agent({ ...config })
|
||||
const messages = JSON.parse(serialized)
|
||||
agent2.restore(messages)
|
||||
const result = await agent2.continue("Continue where we left off")
|
||||
```
|
||||
|
||||
For automatic persistence, use `ClineCore` instead.
|
||||
|
||||
## Pre-Built Model via Gateway
|
||||
|
||||
For advanced provider configuration:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@cline/sdk"
|
||||
import { createGateway } from "@cline/llms"
|
||||
|
||||
const gateway = createGateway({
|
||||
providerConfigs: [
|
||||
{ providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
|
||||
{ providerId: "openai", apiKey: process.env.OPENAI_API_KEY },
|
||||
],
|
||||
})
|
||||
|
||||
const model = gateway.createAgentModel({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-opus-4-7",
|
||||
})
|
||||
|
||||
const agent = new Agent({
|
||||
model,
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
tools: [],
|
||||
})
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `api.md` - Full API reference
|
||||
- `gotchas.md` - Common pitfalls
|
||||
- `../tools/REFERENCE.md` - Creating tools
|
||||
- `../plugins/REFERENCE.md` - Plugin system
|
||||
@@ -1,131 +0,0 @@
|
||||
# ClineCore Runtime
|
||||
|
||||
`ClineCore` is the full-featured runtime from `@cline/core`. It wraps the `Agent` loop with session persistence, built-in tools (bash, editor, file reading, search, web fetch), config discovery, plugin loading, and optional hub-backed multi-process support.
|
||||
|
||||
## When to Use ClineCore
|
||||
|
||||
| Use ClineCore when... | Use Agent instead when... |
|
||||
|---|---|
|
||||
| You need built-in tools (bash, editor, etc.) | You only need custom tools |
|
||||
| You want session persistence to disk | Stateless is fine |
|
||||
| You need config discovery from `.cline/` dirs | You handle config yourself |
|
||||
| You want scheduled/automated agents | You don't need scheduling |
|
||||
| You need multi-client session sharing | Single-process is fine |
|
||||
| You're building a full application | You want minimal dependencies |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { ClineCore } from "@cline/sdk"
|
||||
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
|
||||
const session = await cline.start({
|
||||
prompt: "Set up CI with GitHub Actions",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
cwd: "/path/to/project",
|
||||
enableTools: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(session.result?.text)
|
||||
await cline.dispose()
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Sessions
|
||||
|
||||
Every `cline.start()` call creates a session with a unique ID. Sessions persist their messages and metadata to SQLite. You can list, read, resume, and delete sessions.
|
||||
|
||||
### Built-in Tools
|
||||
|
||||
ClineCore provides these tools automatically when `enableTools: true`:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `bash` | Execute shell commands |
|
||||
| `editor` | Edit files |
|
||||
| `read_files` | Read file contents |
|
||||
| `apply_patch` | Apply unified diffs |
|
||||
| `search` | Search file contents and structure |
|
||||
| `fetch_web` | HTTP requests and web content |
|
||||
|
||||
### Config Discovery
|
||||
|
||||
ClineCore watches `.cline/` directories for:
|
||||
- Rules (system prompt additions)
|
||||
- Skills (domain knowledge)
|
||||
- Workflows (multi-step procedures)
|
||||
- Hooks (lifecycle logic)
|
||||
- Plugins (tool + hook bundles)
|
||||
- MCP servers (external tool providers)
|
||||
|
||||
### Backend Modes
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| `"auto"` (default) | Tries to connect to a local hub; falls back to in-process if unavailable |
|
||||
| `"local"` | In-process execution, local SQLite storage, no hub |
|
||||
| `"hub"` | Requires a compatible local WebSocket hub; fails if unavailable |
|
||||
| `"remote"` | Connects to an explicit remote hub endpoint |
|
||||
|
||||
The default mode is `"auto"`. For simple scripts and CLI tools, `"local"` avoids hub discovery overhead. Hub mode enables multi-client session sharing (e.g., a dashboard watching a running session from another process).
|
||||
|
||||
## Key APIs
|
||||
|
||||
- `ClineCore.create(options)` - Create and initialize
|
||||
- `cline.start(input)` - Start a new session
|
||||
- `cline.send({ sessionId, prompt })` - Send follow-up message
|
||||
- `cline.subscribe(listener)` - Listen to session events
|
||||
- `cline.list()` - List sessions
|
||||
- `cline.get(sessionId)` - Get session metadata
|
||||
- `cline.readMessages(sessionId)` - Read persisted messages
|
||||
- `cline.getAccumulatedUsage(sessionId)` - Token/cost totals
|
||||
- `cline.abort(sessionId)` - Abort a session
|
||||
- `cline.delete(sessionId)` - Delete a session
|
||||
- `cline.dispose()` - Clean up resources
|
||||
|
||||
See `api.md` for full API details.
|
||||
|
||||
## Event Streaming
|
||||
|
||||
`cline.subscribe()` emits `CoreSessionEvent` types. These are different from the `AgentRuntimeEvent` types emitted by the standalone `Agent` class -- see `../events/REFERENCE.md` for the full comparison.
|
||||
|
||||
```typescript
|
||||
cline.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case "chunk":
|
||||
if (event.payload.type === "text") {
|
||||
process.stdout.write(event.payload.text)
|
||||
}
|
||||
break
|
||||
case "ended":
|
||||
console.log(`Session ended: ${event.payload.finishReason}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
ClineCore results use `AgentResult` with `.text` (not `.outputText` like the standalone Agent's `AgentRunResult`).
|
||||
|
||||
## Session Persistence
|
||||
|
||||
Sessions are stored at:
|
||||
```
|
||||
~/.cline/data/sessions/
|
||||
sessions.db # SQLite database
|
||||
[session-id].json # Message history
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- `api.md` - Full ClineCore API reference
|
||||
- `patterns.md` - Common patterns and best practices
|
||||
- `gotchas.md` - Pitfalls and debugging
|
||||
- `../tools/REFERENCE.md` - Custom tool creation
|
||||
- `../plugins/REFERENCE.md` - Plugin system
|
||||
- `../scheduling/REFERENCE.md` - Scheduled agents
|
||||
@@ -1,304 +0,0 @@
|
||||
# ClineCore API Reference
|
||||
|
||||
## Creating ClineCore
|
||||
|
||||
```typescript
|
||||
import { ClineCore } from "@cline/sdk"
|
||||
|
||||
const cline = await ClineCore.create(options: ClineCoreOptions)
|
||||
```
|
||||
|
||||
### ClineCoreOptions
|
||||
|
||||
```typescript
|
||||
interface ClineCoreOptions {
|
||||
clientName: string // identifies your app
|
||||
distinctId?: string // user/instance identifier
|
||||
backendMode?: "auto" | "local" | "hub" | "remote"
|
||||
hub?: HubOptions
|
||||
remote?: RemoteOptions
|
||||
capabilities?: RuntimeCapabilities
|
||||
toolPolicies?: Record<string, ToolPolicy>
|
||||
automation?: boolean | ClineCoreAutomationOptions
|
||||
fetch?: typeof fetch
|
||||
}
|
||||
```
|
||||
|
||||
### RuntimeCapabilities
|
||||
|
||||
```typescript
|
||||
interface RuntimeCapabilities {
|
||||
requestToolApproval?: (request: ToolApprovalRequest) => Promise<ToolApprovalResult>
|
||||
// ... other capability callbacks
|
||||
}
|
||||
```
|
||||
|
||||
## Starting Sessions
|
||||
|
||||
### start(input)
|
||||
|
||||
```typescript
|
||||
const session = await cline.start(input: ClineCoreStartInput)
|
||||
```
|
||||
|
||||
Returns a `StartSessionResult`:
|
||||
|
||||
```typescript
|
||||
interface StartSessionResult {
|
||||
sessionId: string
|
||||
manifest: SessionManifest
|
||||
manifestPath: string
|
||||
messagesPath: string
|
||||
result?: AgentResult
|
||||
}
|
||||
```
|
||||
|
||||
### ClineCoreStartInput
|
||||
|
||||
```typescript
|
||||
interface ClineCoreStartInput {
|
||||
prompt: string
|
||||
config: CoreSessionConfig
|
||||
source?: string
|
||||
interactive?: boolean
|
||||
sessionMetadata?: Record<string, unknown>
|
||||
initialMessages?: AgentMessage[]
|
||||
toolPolicies?: Record<string, ToolPolicy>
|
||||
capabilities?: RuntimeCapabilities
|
||||
}
|
||||
```
|
||||
|
||||
### CoreSessionConfig
|
||||
|
||||
```typescript
|
||||
interface CoreSessionConfig {
|
||||
cwd?: string // working directory
|
||||
providerId: string // LLM provider
|
||||
modelId: string // model identifier
|
||||
apiKey?: string // provider API key
|
||||
systemPrompt?: string // custom system prompt
|
||||
tools?: readonly AgentTool[] // additional custom tools
|
||||
enableTools?: boolean // enable built-in tools
|
||||
hooks?: Partial<AgentRuntimeHooks> // runtime hooks
|
||||
extensions?: AgentPlugin[] // plugins loaded inline
|
||||
pluginPaths?: string[] // paths to plugin packages
|
||||
extensionLoading?: "isolated" | "direct"
|
||||
extensionContext?: { // context passed to plugin setup()
|
||||
workspace?: { rootPath: string; cwd: string }
|
||||
}
|
||||
checkpointConfig?: CoreCheckpointConfig
|
||||
compactionConfig?: CoreCompactionConfig
|
||||
telemetry?: ITelemetryService
|
||||
logger?: BasicLogger
|
||||
enableSpawnAgent?: boolean // enable sub-agent spawning
|
||||
enableAgentTeams?: boolean // enable team coordination
|
||||
teamName?: string // team identifier
|
||||
}
|
||||
```
|
||||
|
||||
`extensions` passes plugin objects directly. `pluginPaths` points to directories with `package.json` containing a `cline.plugins` field. Set `extensionContext.workspace` so plugins receive `ctx.workspaceInfo` in their `setup()` call -- without it, `ctx.workspaceInfo` is undefined.
|
||||
|
||||
## Follow-Up Messages
|
||||
|
||||
### send({ sessionId, prompt })
|
||||
|
||||
Send a follow-up message to an existing session:
|
||||
|
||||
```typescript
|
||||
const result = await cline.send({
|
||||
sessionId: session.sessionId,
|
||||
prompt: "Now add authentication",
|
||||
})
|
||||
```
|
||||
|
||||
Returns `AgentResult | undefined`.
|
||||
|
||||
## Event Subscription
|
||||
|
||||
### subscribe(listener, options?)
|
||||
|
||||
```typescript
|
||||
const unsubscribe = cline.subscribe(
|
||||
(event: CoreSessionEvent) => {
|
||||
// handle events
|
||||
},
|
||||
{ sessionId: "optional-filter" }
|
||||
)
|
||||
```
|
||||
|
||||
### CoreSessionEvent
|
||||
|
||||
```typescript
|
||||
type CoreSessionEvent =
|
||||
| { type: "chunk"; payload: SessionChunkEvent }
|
||||
| { type: "agent_event"; payload: { sessionId: string, event: AgentEvent } }
|
||||
| { type: "ended"; payload: SessionEndedEvent }
|
||||
| { type: "team_progress"; payload: SessionTeamProgressEvent }
|
||||
| { type: "status"; payload: { sessionId: string, status: string } }
|
||||
| { type: "hook"; payload: SessionToolEvent }
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
### list(limit?, options?)
|
||||
|
||||
```typescript
|
||||
const sessions: SessionRecord[] = await cline.list(50)
|
||||
```
|
||||
|
||||
### get(sessionId)
|
||||
|
||||
```typescript
|
||||
const session: SessionRecord = await cline.get(sessionId)
|
||||
```
|
||||
|
||||
### readMessages(sessionId)
|
||||
|
||||
```typescript
|
||||
const messages: AgentMessage[] = await cline.readMessages(sessionId)
|
||||
```
|
||||
|
||||
### getAccumulatedUsage(sessionId)
|
||||
|
||||
```typescript
|
||||
const usage = await cline.getAccumulatedUsage(sessionId)
|
||||
// usage.usage - root agent only
|
||||
// usage.aggregateUsage - root + subagents/teammates
|
||||
```
|
||||
|
||||
### update(sessionId, updates)
|
||||
|
||||
```typescript
|
||||
await cline.update(sessionId, { title: "New title" })
|
||||
```
|
||||
|
||||
### abort(sessionId, reason?)
|
||||
|
||||
```typescript
|
||||
await cline.abort(sessionId, "User cancelled")
|
||||
```
|
||||
|
||||
### stop(sessionId)
|
||||
|
||||
```typescript
|
||||
await cline.stop(sessionId)
|
||||
```
|
||||
|
||||
### delete(sessionId)
|
||||
|
||||
```typescript
|
||||
await cline.delete(sessionId)
|
||||
```
|
||||
|
||||
### restore(input)
|
||||
|
||||
Restore a session from a checkpoint:
|
||||
|
||||
```typescript
|
||||
await cline.restore({ sessionId, checkpointId })
|
||||
```
|
||||
|
||||
### dispose(reason?)
|
||||
|
||||
Clean up all resources. Always call this when done:
|
||||
|
||||
```typescript
|
||||
await cline.dispose("Shutting down")
|
||||
```
|
||||
|
||||
## AgentResult
|
||||
|
||||
Returned by session operations:
|
||||
|
||||
```typescript
|
||||
interface AgentResult {
|
||||
text: string
|
||||
usage: LegacyAgentUsage
|
||||
messages: MessageWithMetadata[]
|
||||
toolCalls: ToolCallRecord[]
|
||||
iterations: number
|
||||
finishReason: "completed" | "max_iterations" | "aborted" | "mistake_limit" | "error"
|
||||
model: { id: string; provider: string; info?: ModelInfo }
|
||||
startedAt: Date
|
||||
endedAt: Date
|
||||
durationMs: number
|
||||
}
|
||||
```
|
||||
|
||||
## Tool Policies
|
||||
|
||||
Control tool access at the session level:
|
||||
|
||||
```typescript
|
||||
const session = await cline.start({
|
||||
prompt: "Review the code",
|
||||
config: { ... },
|
||||
toolPolicies: {
|
||||
read_files: { autoApprove: true },
|
||||
bash: { autoApprove: false },
|
||||
editor: { enabled: false },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### ToolPolicy
|
||||
|
||||
```typescript
|
||||
interface ToolPolicy {
|
||||
enabled?: boolean // false = tool is hidden from the model
|
||||
autoApprove?: boolean // false = requires approval callback
|
||||
}
|
||||
```
|
||||
|
||||
## Interactive Approval
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "my-app",
|
||||
capabilities: {
|
||||
requestToolApproval: async (request) => {
|
||||
console.log(`Tool: ${request.toolName}, Input: ${JSON.stringify(request.input)}`)
|
||||
const approved = await askUser(`Allow ${request.toolName}?`)
|
||||
return { approved }
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Automation API
|
||||
|
||||
When `automation` is enabled in `ClineCore.create()`:
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "my-app",
|
||||
automation: true,
|
||||
})
|
||||
|
||||
// Access automation methods
|
||||
cline.automation.start()
|
||||
cline.automation.stop()
|
||||
cline.automation.reconcile(specs)
|
||||
cline.automation.ingestEvent(event)
|
||||
cline.automation.listEvents()
|
||||
cline.automation.listSpecs()
|
||||
cline.automation.listRuns()
|
||||
```
|
||||
|
||||
## Settings API
|
||||
|
||||
```typescript
|
||||
// Read settings
|
||||
const settings = await cline.settings.list()
|
||||
|
||||
// Toggle tools, plugins, MCP servers
|
||||
await cline.settings.toggle({ type: "tool", name: "bash", enabled: true })
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `REFERENCE.md` - Overview and quick start
|
||||
- `patterns.md` - Common patterns
|
||||
- `gotchas.md` - Pitfalls
|
||||
- `../tools/REFERENCE.md` - Tool creation
|
||||
- `../plugins/REFERENCE.md` - Plugin system
|
||||
@@ -1,148 +0,0 @@
|
||||
# ClineCore Gotchas
|
||||
|
||||
## Always Call dispose()
|
||||
|
||||
`ClineCore` holds resources (file watchers, database connections, hub connections). Failing to call `dispose()` can leave orphan processes and file locks.
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
try {
|
||||
// ... use cline
|
||||
} finally {
|
||||
await cline.dispose()
|
||||
}
|
||||
```
|
||||
|
||||
## Node.js 22 Required
|
||||
|
||||
ClineCore and `@cline/core` require Node.js 22 or later. If you're on an older version, you'll get runtime errors. Check with `node --version`.
|
||||
|
||||
## Session Config vs Global Config
|
||||
|
||||
Tool policies can be set at two levels:
|
||||
- Global: in `ClineCore.create({ toolPolicies })` -- applies to all sessions
|
||||
- Per-session: in `cline.start({ toolPolicies })` -- overrides global for that session
|
||||
|
||||
Per-session policies take precedence.
|
||||
|
||||
## enableTools Must Be Explicit
|
||||
|
||||
Built-in tools (bash, editor, read_files, etc.) are not available unless you set `enableTools: true` in the session config:
|
||||
|
||||
```typescript
|
||||
await cline.start({
|
||||
prompt: "Read package.json",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
enableTools: true, // required for built-in tools
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Without this, the agent only has access to custom tools you provide via `config.tools`.
|
||||
|
||||
## cwd Matters for Built-in Tools
|
||||
|
||||
Built-in tools like `bash`, `editor`, and `read_files` operate relative to `config.cwd`. If not set, they use the process working directory. Always set it explicitly for predictable behavior:
|
||||
|
||||
```typescript
|
||||
config: {
|
||||
cwd: "/absolute/path/to/project",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Hub Startup Latency
|
||||
|
||||
With `backendMode: "auto"`, the first session may be slow if a hub daemon needs to be spawned. For immediate responsiveness:
|
||||
- Use `backendMode: "local"` for in-process execution (fastest startup)
|
||||
- Pre-warm the hub with `cline hub ensure` CLI command
|
||||
- Accept the one-time startup cost and let subsequent sessions reuse the hub
|
||||
|
||||
## Session Storage Location
|
||||
|
||||
Sessions are stored at `~/.cline/data/sessions/`. This includes:
|
||||
- `sessions.db` - SQLite database with session metadata
|
||||
- `[session-id].json` - Individual message history files
|
||||
|
||||
If you're running in a container or ephemeral environment, these paths may not persist across restarts.
|
||||
|
||||
## requestToolApproval Blocks Execution
|
||||
|
||||
When a tool policy has `autoApprove: false` and you provide a `requestToolApproval` callback, the agent loop blocks until your callback resolves. If your callback never resolves (e.g., waiting for user input that never comes), the session hangs.
|
||||
|
||||
For automated pipelines, either:
|
||||
- Set all tools to `autoApprove: true`
|
||||
- Implement a timeout in your approval callback
|
||||
|
||||
## Plugin Discovery Paths
|
||||
|
||||
ClineCore discovers plugins from:
|
||||
- Global: `~/.cline/plugins/`
|
||||
- Workspace: `.cline/plugins/`
|
||||
|
||||
For SDK consumers, pass plugins via `extensions: [plugin]` or `pluginPaths: ["./path"]` in the session config.
|
||||
|
||||
If a plugin isn't loading, verify:
|
||||
- The file is in one of the discovery directories, or passed via `extensions`/`pluginPaths`
|
||||
- The file exports a default plugin object with a non-empty `manifest.capabilities` array
|
||||
- Every `api.register*` call in `setup()` has a matching capability declared
|
||||
- If `hooks` is present on the plugin, `"hooks"` is in `capabilities`
|
||||
|
||||
## extensionContext.workspace Is Required for Plugins
|
||||
|
||||
If your plugins use `ctx.workspaceInfo` (e.g., to resolve workspace paths), you must set `extensionContext.workspace` in the session config. Without it, `ctx.workspaceInfo` is undefined:
|
||||
|
||||
```typescript
|
||||
await cline.start({
|
||||
config: {
|
||||
extensions: [myPlugin],
|
||||
extensionContext: {
|
||||
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The CLI sets this automatically, but SDK consumers must set it explicitly.
|
||||
|
||||
## send() Requires an Active Session
|
||||
|
||||
`cline.send()` only works on sessions that are still active. If a session has already completed, `send()` may return `undefined` or fail. Check session status with `cline.get(sessionId)` first.
|
||||
|
||||
## Result May Be Undefined
|
||||
|
||||
`session.result` can be `undefined` if the session was started but hasn't completed yet (e.g., in a non-blocking hub mode). Check for this:
|
||||
|
||||
```typescript
|
||||
const session = await cline.start({ ... })
|
||||
if (session.result) {
|
||||
console.log(session.result.text)
|
||||
} else {
|
||||
console.log("Session started but not yet complete")
|
||||
}
|
||||
```
|
||||
|
||||
## Compaction and Long Sessions
|
||||
|
||||
For long-running sessions, message history grows and eventually exceeds the model's context window. ClineCore handles this via compaction, which summarizes older messages. Configure it via `compactionConfig`:
|
||||
|
||||
```typescript
|
||||
config: {
|
||||
compactionConfig: {
|
||||
strategy: "summarize",
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The default strategy works for most cases, but extremely long sessions may benefit from tuning.
|
||||
|
||||
## See Also
|
||||
|
||||
- `api.md` - Full API reference
|
||||
- `patterns.md` - Common patterns
|
||||
- `../agent/gotchas.md` - Agent-level gotchas
|
||||
- `../tools/REFERENCE.md` - Tool troubleshooting
|
||||
- `../providers/REFERENCE.md` - Provider troubleshooting
|
||||
@@ -1,279 +0,0 @@
|
||||
# ClineCore Patterns
|
||||
|
||||
## Basic Session with Built-in Tools
|
||||
|
||||
```typescript
|
||||
import { ClineCore } from "@cline/sdk"
|
||||
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
|
||||
const session = await cline.start({
|
||||
prompt: "Read package.json and summarize the dependencies",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
cwd: process.cwd(),
|
||||
enableTools: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(session.result?.text)
|
||||
await cline.dispose()
|
||||
```
|
||||
|
||||
## Streaming Session with UI Updates
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
|
||||
cline.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case "chunk":
|
||||
if (event.payload.type === "text") {
|
||||
ui.appendText(event.payload.text)
|
||||
}
|
||||
break
|
||||
case "ended":
|
||||
ui.showComplete(event.payload.finishReason)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
await cline.start({
|
||||
prompt: "Refactor the auth module",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
cwd: "/path/to/project",
|
||||
enableTools: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Multi-Turn Session
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
|
||||
const session = await cline.start({
|
||||
prompt: "Create a new Express server",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
cwd: "/path/to/project",
|
||||
enableTools: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Follow-up
|
||||
const result = await cline.send({
|
||||
sessionId: session.sessionId,
|
||||
prompt: "Now add a health check endpoint",
|
||||
})
|
||||
|
||||
console.log(result?.text)
|
||||
await cline.dispose()
|
||||
```
|
||||
|
||||
## Tiered Permission Model
|
||||
|
||||
Auto-approve reads, require approval for writes:
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "my-app",
|
||||
toolPolicies: {
|
||||
read_files: { autoApprove: true },
|
||||
search: { autoApprove: true },
|
||||
fetch_web: { autoApprove: true },
|
||||
bash: { autoApprove: false },
|
||||
editor: { autoApprove: false },
|
||||
apply_patch: { autoApprove: false },
|
||||
},
|
||||
capabilities: {
|
||||
requestToolApproval: async (request) => {
|
||||
const approved = await promptUser(
|
||||
`Allow ${request.toolName}?\n${JSON.stringify(request.input, null, 2)}`
|
||||
)
|
||||
return { approved }
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Custom Tools Alongside Built-ins
|
||||
|
||||
```typescript
|
||||
import { ClineCore, createTool } from "@cline/sdk"
|
||||
import { z } from "zod"
|
||||
|
||||
const deployTool = createTool({
|
||||
name: "deploy",
|
||||
description: "Deploy the application to the specified environment.",
|
||||
inputSchema: z.object({
|
||||
environment: z.enum(["staging", "production"]),
|
||||
}),
|
||||
execute: async (input) => {
|
||||
const result = await runDeployment(input.environment)
|
||||
return { url: result.url, status: "deployed" }
|
||||
},
|
||||
})
|
||||
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
|
||||
await cline.start({
|
||||
prompt: "Deploy the app to staging",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
cwd: process.cwd(),
|
||||
enableTools: true,
|
||||
tools: [deployTool],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Session with Plugins
|
||||
|
||||
Load plugins inline with `extensions` and provide workspace context so plugins can access `ctx.workspaceInfo`:
|
||||
|
||||
```typescript
|
||||
import { ClineCore } from "@cline/sdk"
|
||||
import myPlugin from "./my-plugin"
|
||||
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "my-app",
|
||||
backendMode: "local",
|
||||
})
|
||||
|
||||
await cline.start({
|
||||
prompt: "Do the thing my plugin enables",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
cwd: process.cwd(),
|
||||
enableTools: true,
|
||||
extensions: [myPlugin],
|
||||
extensionContext: {
|
||||
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await cline.dispose()
|
||||
```
|
||||
|
||||
For directory-based plugin packages, use `pluginPaths` instead:
|
||||
|
||||
```typescript
|
||||
config: {
|
||||
pluginPaths: ["./my-cline-plugin"],
|
||||
extensionContext: {
|
||||
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See `../plugins/REFERENCE.md` for the full plugin authoring guide.
|
||||
|
||||
## Session Listing and Replay
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
|
||||
// List recent sessions
|
||||
const sessions = await cline.list(10)
|
||||
for (const session of sessions) {
|
||||
console.log(`${session.id}: ${session.title}`)
|
||||
}
|
||||
|
||||
// Read messages from a past session
|
||||
const messages = await cline.readMessages(sessions[0].id)
|
||||
for (const msg of messages) {
|
||||
console.log(`[${msg.role}] ${msg.content}`)
|
||||
}
|
||||
|
||||
// Check usage
|
||||
const usage = await cline.getAccumulatedUsage(sessions[0].id)
|
||||
console.log(`Total tokens: ${usage.aggregateUsage.totalInputTokens + usage.aggregateUsage.totalOutputTokens}`)
|
||||
```
|
||||
|
||||
## Graceful Shutdown
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
await cline.dispose("SIGTERM received")
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
// Run sessions...
|
||||
```
|
||||
|
||||
## Stateless Worker Pattern
|
||||
|
||||
For request/response workloads (API endpoints, queue consumers):
|
||||
|
||||
```typescript
|
||||
import { ClineCore } from "@cline/sdk"
|
||||
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "worker",
|
||||
backendMode: "local",
|
||||
})
|
||||
|
||||
async function handleRequest(prompt: string, workspace: string) {
|
||||
const session = await cline.start({
|
||||
prompt,
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
cwd: workspace,
|
||||
enableTools: true,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
text: session.result?.text,
|
||||
usage: session.result?.usage,
|
||||
sessionId: session.sessionId,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hub-Backed Multi-Client
|
||||
|
||||
Multiple clients can attach to the same session:
|
||||
|
||||
```typescript
|
||||
// Process 1: start session
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "backend",
|
||||
backendMode: "hub",
|
||||
})
|
||||
|
||||
const session = await cline.start({
|
||||
prompt: "Long running refactor task",
|
||||
config: { ... },
|
||||
})
|
||||
|
||||
// Process 2: attach and stream events
|
||||
const viewer = await ClineCore.create({
|
||||
clientName: "dashboard",
|
||||
backendMode: "hub",
|
||||
})
|
||||
|
||||
viewer.subscribe((event) => {
|
||||
dashboard.render(event)
|
||||
}, { sessionId: session.sessionId })
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `api.md` - Full API reference
|
||||
- `gotchas.md` - Common pitfalls
|
||||
- `../tools/REFERENCE.md` - Tool creation
|
||||
- `../plugins/REFERENCE.md` - Plugin system
|
||||
- `../scheduling/REFERENCE.md` - Scheduled agents
|
||||
@@ -1,269 +0,0 @@
|
||||
# Events
|
||||
|
||||
The Cline SDK has three event layers. Which one you use depends on whether you're working with the standalone `Agent` class or `ClineCore`.
|
||||
|
||||
## Which Events Do I Get?
|
||||
|
||||
| If you use... | You subscribe with... | You receive... | Text streaming event |
|
||||
|---|---|---|---|
|
||||
| Standalone `Agent` | `agent.subscribe()` | `AgentRuntimeEvent` | `assistant-text-delta` |
|
||||
| `ClineCore` | `cline.subscribe()` | `CoreSessionEvent` | `chunk` (with `payload.type === "text"`) |
|
||||
|
||||
These are different event types with different shapes. Do not mix them up.
|
||||
|
||||
## Layer 1: AgentRuntimeEvent (Standalone Agent)
|
||||
|
||||
Emitted by the `Agent` class via `agent.subscribe()`. This is what you get when using `new Agent(...)` directly. Every event includes a `snapshot` field with the current `AgentRuntimeStateSnapshot`.
|
||||
|
||||
### Run Lifecycle
|
||||
|
||||
```typescript
|
||||
{ type: "run-started", snapshot }
|
||||
{ type: "run-finished", snapshot, result: AgentRunResult }
|
||||
{ type: "run-failed", snapshot, error: Error }
|
||||
```
|
||||
|
||||
### Turns
|
||||
|
||||
```typescript
|
||||
{ type: "turn-started", snapshot, iteration: number }
|
||||
{ type: "turn-finished", snapshot, iteration: number, toolCallCount: number }
|
||||
```
|
||||
|
||||
### Text Streaming
|
||||
|
||||
```typescript
|
||||
// Streaming text delta (arrives as chunks during generation)
|
||||
{ type: "assistant-text-delta", snapshot, iteration: number, text: string, accumulatedText: string }
|
||||
|
||||
// Streaming reasoning delta (when model uses extended thinking)
|
||||
{ type: "assistant-reasoning-delta", snapshot, iteration: number, text: string }
|
||||
|
||||
// Complete assistant message after model finishes
|
||||
{ type: "assistant-message", snapshot, iteration: number, message: AgentMessage, finishReason: string }
|
||||
```
|
||||
|
||||
### Messages
|
||||
|
||||
```typescript
|
||||
// Fired when any message (user or assistant) is added to conversation history
|
||||
{ type: "message-added", snapshot, message: AgentMessage }
|
||||
```
|
||||
|
||||
### Tool Events
|
||||
|
||||
```typescript
|
||||
{ type: "tool-started", snapshot, toolCall: { toolName: string, toolCallId: string, input: unknown } }
|
||||
{ type: "tool-updated", snapshot, toolCall: { toolName: string, toolCallId: string }, update: string }
|
||||
{ type: "tool-finished", snapshot, toolCall: { toolName: string, toolCallId: string }, message: AgentMessage }
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "usage-updated",
|
||||
snapshot,
|
||||
usage: {
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cacheReadTokens?: number,
|
||||
cacheWriteTokens?: number,
|
||||
totalCost?: number,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Notices
|
||||
|
||||
```typescript
|
||||
{ type: "status-notice", snapshot, message: string, metadata?: Record<string, unknown> }
|
||||
```
|
||||
|
||||
### Subscribing
|
||||
|
||||
Use `agent.subscribe()`. Register the listener before calling `run()` to avoid missing early events.
|
||||
|
||||
```typescript
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
tools: [],
|
||||
})
|
||||
|
||||
agent.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case "assistant-text-delta":
|
||||
process.stdout.write(event.text)
|
||||
break
|
||||
case "tool-started":
|
||||
console.log(`\nUsing tool: ${event.toolCall.toolName}`)
|
||||
break
|
||||
case "usage-updated":
|
||||
console.log(`Cost: $${event.usage.totalCost?.toFixed(4)}`)
|
||||
break
|
||||
case "run-finished":
|
||||
console.log(`\nDone: ${event.result.status}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
const result = await agent.run("Hello!")
|
||||
```
|
||||
|
||||
You can also receive events through hooks (these are awaited, so they can be async):
|
||||
|
||||
```typescript
|
||||
const agent = new Agent({
|
||||
...config,
|
||||
hooks: {
|
||||
onEvent: async (event) => {
|
||||
// Same AgentRuntimeEvent types as subscribe()
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Layer 2: AgentEvent (ClineCore Internal)
|
||||
|
||||
When using `ClineCore`, a `RuntimeEventAdapter` translates Layer 1 events into a legacy format called `AgentEvent`. You do not interact with this layer directly -- it is projected into `CoreSessionEvent` for subscribers. The key mappings:
|
||||
|
||||
| AgentRuntimeEvent (Layer 1) | AgentEvent (Layer 2) |
|
||||
|---|---|
|
||||
| `turn-started` | `iteration_start` |
|
||||
| `turn-finished` | `iteration_end` |
|
||||
| `assistant-text-delta` | `content_start` (text) |
|
||||
| `assistant-message` | `content_end` (text) |
|
||||
| `tool-started` | `content_start` (tool) |
|
||||
| `tool-updated` | `content_update` (tool) |
|
||||
| `tool-finished` | `content_end` (tool) |
|
||||
| `usage-updated` | `usage` (with computed deltas) |
|
||||
| `run-finished` | `done` |
|
||||
| `run-failed` | `error` |
|
||||
| `run-started`, `message-added` | (suppressed, not emitted) |
|
||||
|
||||
This layer exists for backwards compatibility. If you see event types like `content_update` or `iteration_start` in other documentation, they refer to this layer, not to what `agent.subscribe()` emits.
|
||||
|
||||
## Layer 3: CoreSessionEvent (ClineCore Subscriber)
|
||||
|
||||
Emitted by `ClineCore` via `cline.subscribe()`. These are higher-level session events.
|
||||
|
||||
```typescript
|
||||
type CoreSessionEvent =
|
||||
| { type: "chunk"; payload: SessionChunkEvent }
|
||||
| { type: "agent_event"; payload: { sessionId: string, event: AgentEvent } }
|
||||
| { type: "ended"; payload: SessionEndedEvent }
|
||||
| { type: "team_progress"; payload: SessionTeamProgressEvent }
|
||||
| { type: "status"; payload: { sessionId: string, status: string } }
|
||||
| { type: "hook"; payload: SessionToolEvent }
|
||||
```
|
||||
|
||||
### SessionChunkEvent
|
||||
|
||||
```typescript
|
||||
interface SessionChunkEvent {
|
||||
type: "text" | "reasoning"
|
||||
text: string
|
||||
sessionId: string
|
||||
}
|
||||
```
|
||||
|
||||
### SessionEndedEvent
|
||||
|
||||
```typescript
|
||||
interface SessionEndedEvent {
|
||||
sessionId: string
|
||||
finishReason: "completed" | "max_iterations" | "aborted" | "mistake_limit" | "error"
|
||||
result?: AgentResult
|
||||
}
|
||||
```
|
||||
|
||||
### Subscribing
|
||||
|
||||
```typescript
|
||||
cline.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case "chunk":
|
||||
if (event.payload.type === "text") {
|
||||
process.stdout.write(event.payload.text)
|
||||
}
|
||||
break
|
||||
case "ended":
|
||||
console.log(`Finished: ${event.payload.finishReason}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Filter by session:
|
||||
|
||||
```typescript
|
||||
cline.subscribe(handler, { sessionId: "specific-session-id" })
|
||||
```
|
||||
|
||||
## Hub Events (Layer 3b)
|
||||
|
||||
When ClineCore runs in hub mode (via `backendMode: "hub"` or `"auto"` when a hub is available), events are projected over WebSocket using `HubEventName` types like `assistant.delta`, `iteration.started`, `tool.started`, etc. You do not interact with these directly -- `cline.subscribe()` still gives you `CoreSessionEvent` regardless of backend mode.
|
||||
|
||||
## Result Type Differences
|
||||
|
||||
The standalone Agent and ClineCore return different result types:
|
||||
|
||||
| API | Result type | Text property |
|
||||
|---|---|---|
|
||||
| `agent.run()` | `AgentRunResult` | `result.outputText` |
|
||||
| `cline.start()` / `cline.send()` | `AgentResult` | `result.text` |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Streaming Text (Standalone Agent)
|
||||
|
||||
```typescript
|
||||
agent.subscribe((event) => {
|
||||
if (event.type === "assistant-text-delta") {
|
||||
process.stdout.write(event.text)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Streaming Text (ClineCore)
|
||||
|
||||
```typescript
|
||||
cline.subscribe((event) => {
|
||||
if (event.type === "chunk" && event.payload.type === "text") {
|
||||
process.stdout.write(event.payload.text)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Usage Tracking (Standalone Agent)
|
||||
|
||||
```typescript
|
||||
agent.subscribe((event) => {
|
||||
if (event.type === "usage-updated" && event.usage.totalCost) {
|
||||
console.log(`Running cost: $${event.usage.totalCost.toFixed(4)}`)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Tool Call Logging (Standalone Agent)
|
||||
|
||||
```typescript
|
||||
agent.subscribe((event) => {
|
||||
if (event.type === "tool-started") {
|
||||
console.log(`Tool started: ${event.toolCall.toolName}`)
|
||||
}
|
||||
if (event.type === "tool-finished") {
|
||||
console.log(`Tool finished: ${event.toolCall.toolName}`)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `../agent/REFERENCE.md` - Agent runtime overview
|
||||
- `../clinecore/REFERENCE.md` - ClineCore session management
|
||||
- `../plugins/REFERENCE.md` - Plugin hooks for lifecycle events
|
||||
- `../production/REFERENCE.md` - Observability in production
|
||||
@@ -1,157 +0,0 @@
|
||||
# Multi-Agent Coordination
|
||||
|
||||
The Cline SDK supports two models for multi-agent work: sub-agents (parent-child) and teams (peer-to-peer).
|
||||
|
||||
## Sub-Agents vs Teams
|
||||
|
||||
| Feature | Sub-Agents | Teams |
|
||||
|---------|-----------|-------|
|
||||
| Enable with | `enableSpawnAgent: true` | `enableAgentTeams: true` |
|
||||
| Persistence | Session-scoped only | Across sessions |
|
||||
| Coordination | Parent-child hierarchy | Peer-to-peer |
|
||||
| Shared state | None | Task board, mailbox, mission log |
|
||||
| Best for | One-off delegation | Complex multi-session projects |
|
||||
|
||||
## Sub-Agents
|
||||
|
||||
Sub-agents are spawned by a parent agent during a run. They execute independently and report results back.
|
||||
|
||||
### Enabling Sub-Agents
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
|
||||
await cline.start({
|
||||
prompt: "Refactor the auth module and update tests",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
enableSpawnAgent: true,
|
||||
enableTools: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
When `enableSpawnAgent` is true, the agent gets access to sub-agent tools:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `start_subagent` | Spawn a background agent with a task |
|
||||
| `message_subagent` | Send a message to a running sub-agent |
|
||||
| `handoff_to_agent` | Delegate the current task entirely |
|
||||
| `submit_and_exit` | Signal completion |
|
||||
|
||||
### How Sub-Agents Work
|
||||
|
||||
1. The parent agent decides a subtask can be delegated
|
||||
2. It calls `start_subagent` with a role, task description, and optionally a preset
|
||||
3. The sub-agent runs independently in the background
|
||||
4. The parent can check status or send follow-up messages
|
||||
5. Sub-agent results are available to the parent when complete
|
||||
|
||||
## Teams
|
||||
|
||||
Teams provide persistent, cross-session coordination between agents.
|
||||
|
||||
### Enabling Teams
|
||||
|
||||
```typescript
|
||||
await cline.start({
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
enableAgentTeams: true,
|
||||
teamName: "auth-sprint",
|
||||
enableTools: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Team Tools
|
||||
|
||||
When `enableAgentTeams` is true, the coordinator agent gets:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `team_spawn_teammate` | Create a new agent with a role and task |
|
||||
| `team_delegate_task` | Assign a task to an existing teammate |
|
||||
| `team_check_status` | Check on a delegated task's progress |
|
||||
| `team_get_result` | Get the completed result from a teammate |
|
||||
|
||||
### Team Persistence
|
||||
|
||||
Teams store shared state in:
|
||||
|
||||
```
|
||||
~/.cline/data/teams/[team-name]/
|
||||
task-board.json # task assignments and status
|
||||
mailbox.json # inter-agent messages
|
||||
mission-log.json # coordination log
|
||||
```
|
||||
|
||||
This state persists across sessions, so team members can pick up where they left off.
|
||||
|
||||
### CLI Team Access
|
||||
|
||||
```bash
|
||||
cline --team-name auth-sprint "Continue the auth refactor"
|
||||
```
|
||||
|
||||
## Choosing Between Sub-Agents and Teams
|
||||
|
||||
Use sub-agents when:
|
||||
- You need one-off parallel execution within a single session
|
||||
- Tasks are independent and don't need to communicate with each other
|
||||
- Results only matter to the parent agent
|
||||
|
||||
Use teams when:
|
||||
- Work spans multiple sessions over time
|
||||
- Agents need to coordinate and share progress
|
||||
- Tasks have dependencies between them
|
||||
- You want a persistent record of multi-agent collaboration
|
||||
|
||||
## Patterns
|
||||
|
||||
### Parallel Research with Sub-Agents
|
||||
|
||||
A parent agent spawns multiple sub-agents to research different topics simultaneously:
|
||||
|
||||
```typescript
|
||||
await cline.start({
|
||||
prompt: `Research these three topics in parallel:
|
||||
1. Current best practices for JWT auth
|
||||
2. OAuth 2.0 provider comparison
|
||||
3. Session management patterns
|
||||
Spawn a sub-agent for each topic, then synthesize the results.`,
|
||||
config: {
|
||||
enableSpawnAgent: true,
|
||||
enableTools: true,
|
||||
// ...
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Team Sprint
|
||||
|
||||
A coordinator manages a multi-session project:
|
||||
|
||||
```typescript
|
||||
await cline.start({
|
||||
prompt: `You are the coordinator for the auth-sprint team.
|
||||
Review the task board and delegate the next highest-priority task
|
||||
to a teammate. Check status on any in-progress tasks.`,
|
||||
config: {
|
||||
enableAgentTeams: true,
|
||||
teamName: "auth-sprint",
|
||||
enableTools: true,
|
||||
// ...
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `../clinecore/REFERENCE.md` - ClineCore runtime
|
||||
- `../clinecore/api.md` - Session config for teams
|
||||
- `../tools/REFERENCE.md` - Tool system
|
||||
- `../plugins/REFERENCE.md` - Plugin system
|
||||
@@ -1,649 +0,0 @@
|
||||
# Plugins
|
||||
|
||||
A Cline plugin is a TypeScript module that extends any agent built on the Cline SDK. The same plugin runs in the Cline CLI, VS Code and JetBrains extensions, and any custom app built on `@cline/core`.
|
||||
|
||||
A plugin can:
|
||||
|
||||
- Register tools the model can call.
|
||||
- Hook into the agent loop before/after runs, model calls, and tool calls.
|
||||
- Rewrite provider messages before they hit the model (custom compaction, redaction, context shaping).
|
||||
- Register slash commands, prompt rules, providers, and automation event types.
|
||||
|
||||
A plugin ships in one of two shapes:
|
||||
|
||||
1. Single-file plugin -- one `.ts` file that exports a default plugin object. Drop it in a discovery folder and it loads.
|
||||
2. Plugin package -- a directory with `package.json`, npm dependencies, and optionally bundled assets. Installable via `cline plugin install`.
|
||||
|
||||
Both shapes use the same plugin API.
|
||||
|
||||
## The Mental Model
|
||||
|
||||
When the host starts a session, it builds a registry of plugins and runs four phases:
|
||||
|
||||
1. resolve -- collect the plugin objects.
|
||||
2. validate -- check each plugin's `manifest`. Capabilities must be non-empty; declared hook stages must have matching handlers; if `hooks` is present, `"hooks"` must be in `capabilities`.
|
||||
3. setup -- call each plugin's `setup(api, ctx)` once. This is where you `registerTool`, `registerCommand`, etc.
|
||||
4. activate -- registry is frozen, the agent loop starts, and your hooks/tools are live.
|
||||
|
||||
Two invariants the registry enforces:
|
||||
|
||||
- Every contribution requires a matching capability. Calling `api.registerRule(...)` without `"rules"` in `manifest.capabilities` throws.
|
||||
- Capabilities and handlers must agree. Declaring `"hooks"` without a `hooks` object, or vice versa, fails validation.
|
||||
|
||||
After validation, registration is one-shot -- no dynamic register/unregister during the session.
|
||||
|
||||
## The Smallest Working Plugin
|
||||
|
||||
```typescript
|
||||
import type { AgentPlugin } from "@cline/core"
|
||||
import { createTool } from "@cline/core"
|
||||
|
||||
const plugin: AgentPlugin = {
|
||||
name: "hello-plugin",
|
||||
manifest: {
|
||||
capabilities: ["tools"],
|
||||
},
|
||||
setup(api, ctx) {
|
||||
api.registerTool(
|
||||
createTool({
|
||||
name: "say_hello",
|
||||
description: "Greet a person by name.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { name: { type: "string" } },
|
||||
required: ["name"],
|
||||
},
|
||||
async execute({ name }: { name: string }) {
|
||||
return { greeting: `Hello, ${name}!` }
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default plugin
|
||||
```
|
||||
|
||||
The agent will see `say_hello` as a callable tool.
|
||||
|
||||
## The Manifest
|
||||
|
||||
```typescript
|
||||
manifest: {
|
||||
capabilities: ["tools", "hooks"], // required, non-empty array
|
||||
paths?: string[], // optional, multi-entry packages
|
||||
providerIds?: string[], // optional, provider plugins
|
||||
modelIds?: string[], // optional, model plugins
|
||||
}
|
||||
```
|
||||
|
||||
### The Complete Capability List
|
||||
|
||||
| Capability | What It Unlocks in `api` |
|
||||
|-----------|--------------------------|
|
||||
| `"tools"` | `api.registerTool()` |
|
||||
| `"commands"` | `api.registerCommand()` (slash commands in chat surfaces) |
|
||||
| `"rules"` | `api.registerRule()` (string injected into the system prompt) |
|
||||
| `"messageBuilders"` | `api.registerMessageBuilder()` (rewrites provider-bound messages) |
|
||||
| `"providers"` | `api.registerProvider()` (custom model provider) |
|
||||
| `"automationEvents"` | `api.registerAutomationEventType()` and `ctx.automation?.ingestEvent()` |
|
||||
| `"hooks"` | The runtime `hooks` object on the plugin (lifecycle callbacks) |
|
||||
|
||||
You declare any combination -- most real plugins need 1-3 capabilities.
|
||||
|
||||
## setup(api, ctx) -- The Registration Phase
|
||||
|
||||
`setup()` runs once per session before the agent loop starts. Everything you register here is frozen for the lifetime of the session.
|
||||
|
||||
### The api Object
|
||||
|
||||
Each `register*` method requires the matching capability in your manifest:
|
||||
|
||||
```typescript
|
||||
api.registerTool(tool) // requires "tools"
|
||||
api.registerCommand({ name, description, handler }) // requires "commands"
|
||||
api.registerRule({ id, content, source }) // requires "rules"
|
||||
api.registerMessageBuilder({ name, build }) // requires "messageBuilders"
|
||||
api.registerProvider({ name, description }) // requires "providers"
|
||||
api.registerAutomationEventType({ eventType, source }) // requires "automationEvents"
|
||||
```
|
||||
|
||||
### The ctx Object -- Host-Provided Session Context
|
||||
|
||||
The second argument carries everything the host knows about the current session. All fields are optional, so feature-detect before using them -- the same plugin must work in hosts that supply less context (unit tests, sandboxed plugin processes).
|
||||
|
||||
```typescript
|
||||
ctx.session?.sessionId // string, stable core session id
|
||||
ctx.client?.name // host: "cline-cli", "cline-vscode", etc.
|
||||
ctx.user // authenticated user/org info, when available
|
||||
ctx.workspaceInfo // { rootPath, hint, latestGitBranchName,
|
||||
// latestGitCommitHash, associatedRemoteUrls }
|
||||
ctx.automation?.ingestEvent // emit normalized automation events
|
||||
ctx.logger?.log // structured logger scoped to this plugin
|
||||
ctx.telemetry // ITelemetryService, only present in-process
|
||||
```
|
||||
|
||||
Two rules about `ctx.workspaceInfo`:
|
||||
|
||||
1. Always prefer `ctx.workspaceInfo?.rootPath` over `process.cwd()`. The CLI may have been launched with `--cwd` without calling `chdir`, and VS Code workspaces don't share a single CWD. `workspaceInfo` is sourced from the session config and is always correct.
|
||||
2. Don't use `import.meta.url` tricks to find "the workspace". That gives you the plugin's own location, not the user's project.
|
||||
|
||||
### Persisting State Across Hooks
|
||||
|
||||
`setup()` runs first; hooks fire later. The simplest way to share state is module-level variables:
|
||||
|
||||
```typescript
|
||||
let sessionWorkspaceRoot: string | undefined
|
||||
let sessionBranch: string | undefined
|
||||
|
||||
const plugin: AgentPlugin = {
|
||||
name: "metrics",
|
||||
manifest: { capabilities: ["hooks"] },
|
||||
setup(api, ctx) {
|
||||
sessionWorkspaceRoot = ctx.workspaceInfo?.rootPath
|
||||
sessionBranch = ctx.workspaceInfo?.latestGitBranchName
|
||||
},
|
||||
hooks: {
|
||||
beforeTool({ toolCall, input }) {
|
||||
if (sessionBranch === "main" && toolCall.toolName === "run_commands") {
|
||||
// inspect input, optionally block
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
A single Node process may host multiple sessions concurrently. If your plugin will run in a multi-session host, key your state by `ctx.session?.sessionId`:
|
||||
|
||||
```typescript
|
||||
const stateBySession = new Map<string, MyState>()
|
||||
setup(api, ctx) {
|
||||
const id = ctx.session?.sessionId
|
||||
if (id) stateBySession.set(id, /* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
## Runtime Hooks
|
||||
|
||||
Runtime hooks are typed in-process callbacks on the same hook layer the runtime uses internally. They run inside the agent loop with full type information -- no IPC, no JSON marshaling.
|
||||
|
||||
Declare `"hooks"` in `manifest.capabilities`, then add a `hooks` property:
|
||||
|
||||
```typescript
|
||||
const plugin: AgentPlugin = {
|
||||
name: "metrics",
|
||||
manifest: { capabilities: ["hooks"] },
|
||||
hooks: {
|
||||
beforeRun(ctx) { /* ... */ },
|
||||
beforeTool({ toolCall, input }) { /* ... */ },
|
||||
afterTool({ toolCall, result }) { /* ... */ },
|
||||
afterRun({ result }) { /* ... */ },
|
||||
onEvent(event) { /* ... */ },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### The Seven Hooks
|
||||
|
||||
| Hook | Fires | Can Stop the Loop? | Common Uses |
|
||||
|------|-------|--------------------|-------------|
|
||||
| `beforeRun` | Before the runtime loop starts | Yes | Greet, log, attach session metadata |
|
||||
| `afterRun` | After the runtime loop finishes (success, abort, or fail) | No | Notifications, metrics, persistent logs |
|
||||
| `beforeModel` | Before each model request | Yes (mutate req) | Inject context, last-mile prompt edits |
|
||||
| `afterModel` | After each model response, before tool execution | Yes | Block based on model output |
|
||||
| `beforeTool` | Before each tool execution | Yes (`{ stop }`) | Audit, redact, block dangerous tools |
|
||||
| `afterTool` | After each tool execution | Can replace result | Post-process, redact secrets in tool output |
|
||||
| `onEvent` | On every `AgentRuntimeEvent` emitted by the runtime | No | Streaming UIs, telemetry pipes |
|
||||
|
||||
### Stopping the Loop from a Hook
|
||||
|
||||
Several hooks return an optional control object. The most common pattern is `beforeTool` blocking a destructive tool call:
|
||||
|
||||
```typescript
|
||||
beforeTool({ toolCall, input }) {
|
||||
if (toolCall.toolName === "run_commands") {
|
||||
const { commands } = input as { commands?: string[] }
|
||||
if (sessionBranch === "main" && commands?.some(c => c.startsWith("git push"))) {
|
||||
return { stop: true, reason: "Blocked git push on protected branch" }
|
||||
}
|
||||
}
|
||||
return undefined // explicit "continue"
|
||||
}
|
||||
```
|
||||
|
||||
Returning `undefined` (or omitting `return`) lets execution continue normally.
|
||||
|
||||
### afterRun Semantics
|
||||
|
||||
`afterRun` fires for every terminal status -- `completed`, `aborted`, `failed`. If you only want to act on success:
|
||||
|
||||
```typescript
|
||||
afterRun({ result }) {
|
||||
if (result.status !== "completed") return
|
||||
// notify, log success metrics, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin Hooks vs File Hooks
|
||||
|
||||
The runtime supports two hook systems:
|
||||
|
||||
- File hooks -- external scripts in `.cline/hooks/` invoked with serialized JSON. Right for user/workspace-specific scripts that don't ship with code.
|
||||
- Plugin runtime hooks -- typed in-process callbacks. Right when the behavior belongs to a reusable extension and needs typed access to the runtime.
|
||||
|
||||
Core adapts file hooks onto the runtime hook layer, so you don't need both. If you're shipping a plugin, write it as runtime hooks.
|
||||
|
||||
## Message Builders
|
||||
|
||||
Message builders rewrite the provider-bound message list before the model call. They run after runtime messages are converted into SDK message blocks but before core's built-in safety builder.
|
||||
|
||||
Use them for:
|
||||
|
||||
- Custom compaction policies (replace middle history with a summary).
|
||||
- Redacting PII or secrets before they reach the provider.
|
||||
- Reshaping context for a specific model's strengths.
|
||||
|
||||
```typescript
|
||||
api.registerMessageBuilder({
|
||||
name: "summarize-middle-history",
|
||||
build(messages) {
|
||||
if (estimateTokens(messages) < THRESHOLD) return messages
|
||||
return [...prefix, summary, ...recent]
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Multiple builders run in registration order; the output of one is the input of the next.
|
||||
|
||||
When to use `beforeModel` instead: reach for the `beforeModel` hook only if you need the runtime snapshot or want to mutate the request object itself. Pure message rewrites belong in a builder.
|
||||
|
||||
## Automation Events
|
||||
|
||||
Plugins can declare normalized event types and emit them into Cline automation. Hosts that don't have automation enabled simply ignore both -- feature-detect `ctx.automation`.
|
||||
|
||||
```typescript
|
||||
manifest: { capabilities: ["automationEvents"] },
|
||||
|
||||
setup(api, ctx) {
|
||||
api.registerAutomationEventType({
|
||||
eventType: "github.pull_request.opened",
|
||||
source: "github",
|
||||
description: "A new GitHub PR was opened",
|
||||
attributesSchema: { /* JSON Schema for envelope.attributes */ },
|
||||
})
|
||||
|
||||
if (!ctx.automation) return // host has no automation
|
||||
ctx.automation.ingestEvent({
|
||||
eventId: "pr-1234",
|
||||
eventType: "github.pull_request.opened",
|
||||
source: "github",
|
||||
subject: "owner/repo#1234",
|
||||
occurredAt: new Date().toISOString(),
|
||||
attributes: { /* ... */ },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Loading a Plugin
|
||||
|
||||
There are three ways a plugin gets into a session:
|
||||
|
||||
### Auto-Discovery (CLI)
|
||||
|
||||
The CLI scans these directories on startup:
|
||||
|
||||
- `<workspace>/.cline/plugins/` -- project-scoped plugins.
|
||||
- `~/.cline/plugins/` -- user-scoped plugins.
|
||||
|
||||
Drop a `.ts` or `.js` file in, run `cline`, done:
|
||||
|
||||
```bash
|
||||
mkdir -p .cline/plugins
|
||||
cp my-plugin.ts .cline/plugins/
|
||||
cline -i "do the thing my plugin enables"
|
||||
```
|
||||
|
||||
### Explicit extensions in SDK Config
|
||||
|
||||
When you build your own host with `ClineCore`, pass the plugin object directly:
|
||||
|
||||
```typescript
|
||||
import plugin from "./my-plugin"
|
||||
import { ClineCore } from "@cline/core"
|
||||
|
||||
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],
|
||||
extensionContext: {
|
||||
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
|
||||
},
|
||||
},
|
||||
prompt: "...",
|
||||
interactive: false,
|
||||
})
|
||||
```
|
||||
|
||||
### pluginPaths for Directory-Based Plugins
|
||||
|
||||
When the plugin is a directory with `package.json`, point `pluginPaths` at the directory:
|
||||
|
||||
```typescript
|
||||
config: {
|
||||
pluginPaths: ["./path/to/my-plugin-package"],
|
||||
}
|
||||
```
|
||||
|
||||
Or install with the CLI:
|
||||
|
||||
```bash
|
||||
cline plugin install ./path/to/my-plugin-package
|
||||
cline plugin install @scope/my-cline-plugin # from npm
|
||||
cline plugin install --git github.com/owner/repo # from git
|
||||
```
|
||||
|
||||
## Single-File Plugin Template
|
||||
|
||||
Save as `my-plugin.ts`, drop in `.cline/plugins/`:
|
||||
|
||||
```typescript
|
||||
import { type AgentPlugin, ClineCore, createTool } from "@cline/core"
|
||||
|
||||
let sessionRoot: string | undefined
|
||||
|
||||
const plugin: AgentPlugin = {
|
||||
name: "my-plugin",
|
||||
manifest: {
|
||||
capabilities: ["tools", "hooks"],
|
||||
},
|
||||
|
||||
setup(api, ctx) {
|
||||
sessionRoot = ctx.workspaceInfo?.rootPath
|
||||
|
||||
api.registerTool(
|
||||
createTool({
|
||||
name: "do_thing",
|
||||
description: "Do the thing this plugin exists for.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { target: { type: "string" } },
|
||||
required: ["target"],
|
||||
},
|
||||
async execute(input) {
|
||||
const { target } = input as { target: string }
|
||||
return { ok: true, target, root: sessionRoot }
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
||||
hooks: {
|
||||
beforeRun() {
|
||||
console.log("[my-plugin] run started")
|
||||
},
|
||||
afterRun({ result }) {
|
||||
if (result.status !== "completed") return
|
||||
console.log(`[my-plugin] done in ${result.iterations} iteration(s)`)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async function runDemo(): Promise<void> {
|
||||
const host = await ClineCore.create({ backendMode: "local" })
|
||||
try {
|
||||
const result = await host.start({
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY ?? "",
|
||||
cwd: process.cwd(),
|
||||
enableTools: true,
|
||||
systemPrompt: "You are a helpful assistant. Use tools when needed.",
|
||||
extensions: [plugin],
|
||||
extensionContext: {
|
||||
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
|
||||
},
|
||||
},
|
||||
prompt: "Use do_thing on the target 'world'.",
|
||||
interactive: false,
|
||||
})
|
||||
console.log(result.result?.text ?? "")
|
||||
} finally {
|
||||
await host.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
await runDemo()
|
||||
}
|
||||
|
||||
export { plugin, runDemo }
|
||||
export default plugin
|
||||
```
|
||||
|
||||
Copy it, rename the tool, swap in your logic. The `runDemo()` function lets you test with `ANTHROPIC_API_KEY=sk-... bun run my-plugin.ts`.
|
||||
|
||||
## Plugin Package
|
||||
|
||||
Use a plugin package when you need npm dependencies, multiple entry points, bundled assets, or npm/git distribution.
|
||||
|
||||
### Layout
|
||||
|
||||
```
|
||||
my-cline-plugin/
|
||||
+-- package.json
|
||||
+-- tsconfig.json (optional, for local typechecking)
|
||||
+-- index.ts (the plugin entry point)
|
||||
+-- README.md
|
||||
+-- assets/ (optional, bundled content)
|
||||
+-- templates/
|
||||
+-- schemas/
|
||||
```
|
||||
|
||||
### package.json -- The Discovery Contract
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-cline-plugin",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "What this plugin does, in one sentence.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
},
|
||||
"cline": {
|
||||
"plugins": [
|
||||
{
|
||||
"paths": ["./index.ts"],
|
||||
"capabilities": ["tools", "hooks"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cline/core": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@cline/core": { "optional": true }
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^4.1.5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key fields:
|
||||
|
||||
- `type: "module"` -- required. Cline plugins are ES modules.
|
||||
- `cline.plugins` -- the discovery contract. Array of entries, each with `paths` (entry files) and `capabilities` (pre-declared, validated before importing).
|
||||
- `peerDependencies` for `@cline/core` -- the host already provides it. Marking it optional lets you typecheck in isolation.
|
||||
|
||||
### Bundling Assets
|
||||
|
||||
Resolve asset paths with `import.meta.url`, not `process.cwd()`:
|
||||
|
||||
```typescript
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { readFileSync, existsSync } from "node:fs"
|
||||
|
||||
const MODULE_DIR = dirname(fileURLToPath(import.meta.url))
|
||||
const TEMPLATES_DIR = join(MODULE_DIR, "assets", "templates")
|
||||
|
||||
function loadTemplate(name: string): string | undefined {
|
||||
const path = join(TEMPLATES_DIR, `${name}.md`)
|
||||
return existsSync(path) ? readFileSync(path, "utf8") : undefined
|
||||
}
|
||||
```
|
||||
|
||||
This is the only place `import.meta.url` is appropriate in a plugin -- locating files inside the plugin package. For workspace paths, always use `ctx.workspaceInfo?.rootPath`.
|
||||
|
||||
### The Override Pattern (Bundled / Global / Project)
|
||||
|
||||
A package can ship default assets and let users override them. The convention is a three-tier lookup, last write wins by `name`:
|
||||
|
||||
1. bundled -- files inside the plugin package (defaults shipped with the plugin).
|
||||
2. global -- files under `~/.cline/data/settings/<kind>/` (user overrides).
|
||||
3. project -- files under `<workspace>/.cline/<kind>/` (project overrides).
|
||||
|
||||
### Multiple Plugin Entries
|
||||
|
||||
If your package exposes more than one plugin, list each in `cline.plugins`:
|
||||
|
||||
```json
|
||||
"cline": {
|
||||
"plugins": [
|
||||
{ "paths": ["./tools-plugin.ts"], "capabilities": ["tools"] },
|
||||
{ "paths": ["./hooks-plugin.ts"], "capabilities": ["hooks"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each entry file should `export default` its own plugin object.
|
||||
|
||||
## Testing Your Plugin
|
||||
|
||||
### Unit Tests
|
||||
|
||||
The plugin object is plain data. Drive `setup()` against a minimal context and exercise tools directly:
|
||||
|
||||
```typescript
|
||||
import plugin from "../my-plugin"
|
||||
|
||||
const tools: unknown[] = []
|
||||
const api = {
|
||||
registerTool: (t: unknown) => tools.push(t),
|
||||
registerCommand: () => {},
|
||||
registerRule: () => {},
|
||||
registerMessageBuilder: () => {},
|
||||
registerProvider: () => {},
|
||||
registerAutomationEventType: () => {},
|
||||
}
|
||||
await plugin.setup?.(api as never, {
|
||||
workspaceInfo: { rootPath: "/tmp/fake-workspace" },
|
||||
})
|
||||
|
||||
// Now `tools` contains the registered tools -- call tool.execute(input, ctx)
|
||||
```
|
||||
|
||||
### End-to-End with runDemo()
|
||||
|
||||
Add a `runDemo()` in your plugin file (see the single-file template above) that boots a real `ClineCore` session:
|
||||
|
||||
```bash
|
||||
ANTHROPIC_API_KEY=sk-... bun run my-plugin.ts
|
||||
```
|
||||
|
||||
### CLI Smoke Test
|
||||
|
||||
```bash
|
||||
mkdir -p .cline/plugins
|
||||
cp my-plugin.ts .cline/plugins/
|
||||
cline -i "trigger something that exercises the plugin"
|
||||
```
|
||||
|
||||
For packages:
|
||||
|
||||
```bash
|
||||
cline plugin install ./my-cline-plugin
|
||||
cline -i "..."
|
||||
```
|
||||
|
||||
If the plugin fails validation or setup, the CLI prints a clear error and continues without it.
|
||||
|
||||
## Common Gotchas
|
||||
|
||||
- "capabilities must be a non-empty array" -- you forgot `manifest.capabilities`, or it's `[]`.
|
||||
- "registerRule requires the 'rules' capability" -- capability/handler drift. Add `"rules"` to capabilities, or stop calling `registerRule`.
|
||||
- Tool not visible to the model -- check `enableTools: true` on the session config, and that you're declaring `"tools"` in capabilities.
|
||||
- `ctx.workspaceInfo` is undefined in SDK tests -- the host didn't pass `extensionContext.workspace`. In SDK code, set it explicitly (see the ClineCore loading example above).
|
||||
- State leaking across sessions -- module-level variables are shared across sessions in the same process. Key by `ctx.session?.sessionId` if your host runs multiple sessions concurrently.
|
||||
- `afterRun` firing on aborts -- guard with `if (result.status !== "completed") return`.
|
||||
- Heavy work in `setup()` -- `setup()` blocks session start. Defer expensive work into the first tool call or `beforeRun`.
|
||||
- Importing host internals -- only import from `@cline/core`. Reaching into host-specific packages (e.g. CLI internals) will break in non-CLI hosts.
|
||||
- Sandboxed plugins and `telemetry` -- telemetry is process-local. Feature-detect `ctx.telemetry` and expect it to be undefined in sandboxed plugin processes.
|
||||
- Resolving bundled assets -- use `import.meta.url` + `fileURLToPath` to find files inside your package; never `process.cwd()`. For workspace paths, do the opposite: use `ctx.workspaceInfo?.rootPath`, never `import.meta.url`.
|
||||
- Plugin name collisions -- `name` must be unique within a session. If two plugins share a name, validation fails. Namespace by package (`my-org-redactor`, not `redactor`).
|
||||
|
||||
## Decision Guide -- Which Extension Point?
|
||||
|
||||
| You want to... | Use |
|
||||
|----------------|-----|
|
||||
| Give the model a new capability | `registerTool` |
|
||||
| Add a slash command in chat surfaces | `registerCommand` |
|
||||
| Inject text into the system prompt | `registerRule` |
|
||||
| Rewrite messages before they hit the provider | `registerMessageBuilder` |
|
||||
| Add a custom model provider | `registerProvider` |
|
||||
| Emit normalized cron/webhook events | `registerAutomationEventType` + `ctx.automation` |
|
||||
| Observe or steer the agent loop | `hooks.*` |
|
||||
| Block a dangerous tool call | `hooks.beforeTool` returning `{ stop: true }` |
|
||||
| Notify on completion | `hooks.afterRun` (gate on `status === "completed"`) |
|
||||
| Tweak each model request | `hooks.beforeModel` |
|
||||
| Stream events to a UI | `hooks.onEvent` |
|
||||
| Ship reusable templates with the plugin | Bundle assets next to `index.ts`, resolve via `import.meta.url` |
|
||||
| Let users override defaults globally or per-project | Three-tier lookup: bundled / global / project |
|
||||
|
||||
## Pre-Ship Checklist
|
||||
|
||||
- `manifest.capabilities` is a non-empty array.
|
||||
- Every `api.register*` call has a matching capability declared.
|
||||
- If `hooks` is present, `"hooks"` is in `capabilities`.
|
||||
- `ctx.workspaceInfo?.rootPath` is used for workspace paths (not `process.cwd()`).
|
||||
- Optional `ctx` fields are feature-detected.
|
||||
- Tool names are snake_case verbs; descriptions are written for the model.
|
||||
- Tool inputs have JSON Schema with `required` set.
|
||||
- `afterRun` handlers gate on `result.status === "completed"` if they only want successes.
|
||||
- State that must not leak between concurrent sessions is keyed by `ctx.session?.sessionId`.
|
||||
- (Package) `package.json` has `type: "module"`, `cline.plugins`, and `@cline/core` as an optional peer dep.
|
||||
- (Package) Bundled assets resolved via `import.meta.url`, not `process.cwd()`.
|
||||
- Smoke test: drop the plugin into `.cline/plugins/` (or `cline plugin install`), run `cline -i "..."`, watch it work.
|
||||
|
||||
## Plugin Examples from SDK
|
||||
|
||||
The SDK repo includes these example plugins:
|
||||
|
||||
| Plugin | Description |
|
||||
|--------|-------------|
|
||||
| `weather-metrics.ts` | Tool registration + lifecycle metrics |
|
||||
| `mac-notify.ts` | macOS Notification Center alerts |
|
||||
| `custom-compaction.ts` | Custom message compaction via message builders |
|
||||
| `background-terminal.ts` | Detached shell job management |
|
||||
| `automation-events.ts` | Plugin-emitted automation events |
|
||||
| `gitignore-read-files-guard.ts` | File access policy enforcement via beforeTool |
|
||||
| `web-search.ts` | Web search via Exa API |
|
||||
| `typescript-lsp/` | TypeScript Language Service tools (plugin package) |
|
||||
| `agents-squad/` | Multi-agent team orchestration (plugin package) |
|
||||
|
||||
## See Also
|
||||
|
||||
- `../tools/REFERENCE.md` - Tool creation
|
||||
- `../events/REFERENCE.md` - Event system
|
||||
- `../agent/REFERENCE.md` - Using plugins with Agent
|
||||
- `../clinecore/REFERENCE.md` - Using plugins with ClineCore
|
||||
@@ -1,253 +0,0 @@
|
||||
# Going to Production
|
||||
|
||||
Guidelines for deploying Cline SDK agents in production environments.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Always check the result status:
|
||||
|
||||
```typescript
|
||||
const result = await agent.run(input)
|
||||
|
||||
switch (result.status) {
|
||||
case "completed":
|
||||
console.log("Success:", result.outputText)
|
||||
break
|
||||
case "aborted":
|
||||
console.log("Cancelled:", result.error?.message)
|
||||
break
|
||||
case "failed":
|
||||
console.error("Failed:", result.error)
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
For ClineCore, check `finishReason`:
|
||||
|
||||
```typescript
|
||||
const session = await cline.start({ ... })
|
||||
|
||||
switch (session.result?.finishReason) {
|
||||
case "completed":
|
||||
// normal completion
|
||||
break
|
||||
case "max_iterations":
|
||||
// agent hit iteration limit
|
||||
break
|
||||
case "aborted":
|
||||
// manually cancelled
|
||||
break
|
||||
case "mistake_limit":
|
||||
// too many tool errors
|
||||
break
|
||||
case "error":
|
||||
// unrecoverable error
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
## Cost Control
|
||||
|
||||
### Token Limits
|
||||
|
||||
Set maximum tokens per turn and iteration limits:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
maxTokensPerTurn: 4096,
|
||||
maxIterations: 10,
|
||||
tools: [...],
|
||||
})
|
||||
```
|
||||
|
||||
### Model Selection
|
||||
|
||||
Use cheaper models for simple tasks:
|
||||
|
||||
```typescript
|
||||
// Simple classification or formatting
|
||||
{ providerId: "anthropic", modelId: "claude-haiku-4-5" }
|
||||
|
||||
// Complex reasoning and code generation
|
||||
{ providerId: "anthropic", modelId: "claude-sonnet-4-6" }
|
||||
|
||||
// Hardest tasks requiring deep reasoning
|
||||
{ providerId: "anthropic", modelId: "claude-opus-4-7" }
|
||||
```
|
||||
|
||||
### Usage Tracking
|
||||
|
||||
Monitor spending in real time:
|
||||
|
||||
```typescript
|
||||
agent.subscribe((event) => {
|
||||
if (event.type === "usage-updated" && event.usage.totalCost) {
|
||||
if (event.usage.totalCost > MAX_BUDGET) {
|
||||
agent.abort("Budget exceeded")
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Observability
|
||||
|
||||
### OpenTelemetry Integration
|
||||
|
||||
The SDK supports OpenTelemetry for traces, metrics, and logs:
|
||||
|
||||
```typescript
|
||||
import { ClineCore } from "@cline/sdk"
|
||||
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "my-app",
|
||||
// OpenTelemetry config is picked up from environment
|
||||
// OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, etc.
|
||||
})
|
||||
```
|
||||
|
||||
### Structured Logging
|
||||
|
||||
Use the `BasicLogger` interface for injectable logging:
|
||||
|
||||
```typescript
|
||||
import type { BasicLogger } from "@cline/sdk"
|
||||
|
||||
const logger: BasicLogger = {
|
||||
debug: (msg, meta) => console.debug(msg, meta),
|
||||
log: (msg, meta) => console.log(msg, meta),
|
||||
error: (msg, meta) => console.error(msg, meta),
|
||||
}
|
||||
|
||||
await cline.start({
|
||||
config: {
|
||||
logger,
|
||||
// ...
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Custom Metrics via Plugins
|
||||
|
||||
```typescript
|
||||
const metricsPlugin: AgentPlugin = {
|
||||
name: "metrics",
|
||||
manifest: { capabilities: ["hooks"] },
|
||||
setup() {},
|
||||
hooks: {
|
||||
beforeRun() {
|
||||
metrics.increment("agent.runs.started")
|
||||
},
|
||||
afterRun({ result }) {
|
||||
metrics.increment("agent.runs.completed")
|
||||
metrics.histogram("agent.iterations", result.iterations)
|
||||
metrics.histogram("agent.tokens.output", result.usage.outputTokens)
|
||||
},
|
||||
beforeTool({ toolCall }) {
|
||||
metrics.increment(`agent.tools.${toolCall.toolName}`)
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Sandbox Tool Execution
|
||||
|
||||
Validate tool inputs to prevent path traversal and injection:
|
||||
|
||||
```typescript
|
||||
execute: async (input) => {
|
||||
const safePath = path.resolve(WORKSPACE_ROOT, input.path)
|
||||
if (!safePath.startsWith(WORKSPACE_ROOT)) {
|
||||
return { error: "Path traversal attempt blocked" }
|
||||
}
|
||||
return await readFile(safePath, "utf-8")
|
||||
}
|
||||
```
|
||||
|
||||
### API Key Management
|
||||
|
||||
- Use environment variables, never hardcode keys
|
||||
- Rotate keys regularly
|
||||
- Use different keys for development and production
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY, // never a literal string
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Policy Hardening
|
||||
|
||||
Disable tools you don't need and require approval for dangerous ones:
|
||||
|
||||
```typescript
|
||||
toolPolicies: {
|
||||
read_files: { autoApprove: true },
|
||||
search: { autoApprove: true },
|
||||
bash: { autoApprove: false }, // require approval
|
||||
editor: { autoApprove: false },
|
||||
apply_patch: { autoApprove: false },
|
||||
fetch_web: { enabled: false }, // disable entirely
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment Patterns
|
||||
|
||||
### Stateless Worker
|
||||
|
||||
For request/response workloads (API endpoints, queue consumers):
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "worker",
|
||||
backendMode: "local",
|
||||
})
|
||||
|
||||
app.post("/agent", async (req, res) => {
|
||||
const session = await cline.start({
|
||||
prompt: req.body.prompt,
|
||||
config: { ... },
|
||||
})
|
||||
res.json({ text: session.result?.text, usage: session.result?.usage })
|
||||
})
|
||||
```
|
||||
|
||||
### Persistent Service
|
||||
|
||||
For long-running services with session management:
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "service",
|
||||
backendMode: "hub",
|
||||
})
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
await cline.dispose("SIGTERM")
|
||||
process.exit(0)
|
||||
})
|
||||
```
|
||||
|
||||
### Scheduled Automation
|
||||
|
||||
See `../scheduling/REFERENCE.md` for recurring agent tasks.
|
||||
|
||||
## Retry and Resilience
|
||||
|
||||
- Tool `execute` functions support `retryable: true` (default) and `maxRetries: 3` (default)
|
||||
- Provider API calls are retried automatically on transient failures
|
||||
- Use `timeoutMs` on tools to prevent hanging
|
||||
- Monitor `mistake_limit` finish reason to detect systematic tool failures
|
||||
|
||||
## See Also
|
||||
|
||||
- `../agent/REFERENCE.md` - Agent overview
|
||||
- `../clinecore/REFERENCE.md` - ClineCore overview
|
||||
- `../tools/REFERENCE.md` - Tool configuration
|
||||
- `../plugins/REFERENCE.md` - Metrics plugins
|
||||
- `../scheduling/REFERENCE.md` - Scheduled agents
|
||||
@@ -1,257 +0,0 @@
|
||||
# Model Providers
|
||||
|
||||
The Cline SDK supports every major LLM provider out of the box via `@cline/llms`.
|
||||
|
||||
## Supported Providers
|
||||
|
||||
| Provider ID | Models |
|
||||
|-------------|--------|
|
||||
| `"anthropic"` | Claude Opus 4.7, Sonnet 4.6, Haiku 4.5 |
|
||||
| `"openai"` | GPT-5.5, GPT-5.3 Codex |
|
||||
| `"gemini"` | Gemini 3.1 Pro Preview, Gemini 3 Flash Preview |
|
||||
| `"vertex"` | Google models via Vertex AI |
|
||||
| `"bedrock"` | Claude, Llama via AWS Bedrock |
|
||||
| `"mistral"` | Mistral Large, Codestral |
|
||||
| `"openai-compatible"` | vLLM, Together, Fireworks, Groq, etc. |
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
### With Agent
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@cline/sdk"
|
||||
|
||||
const agent = new Agent({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
tools: [],
|
||||
})
|
||||
```
|
||||
|
||||
### With ClineCore
|
||||
|
||||
```typescript
|
||||
import { ClineCore } from "@cline/sdk"
|
||||
|
||||
const cline = await ClineCore.create({ clientName: "my-app" })
|
||||
|
||||
await cline.start({
|
||||
prompt: "Hello",
|
||||
config: {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Provider-Specific Configuration
|
||||
|
||||
### Anthropic
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-opus-4-7", // or "claude-sonnet-4-6", "claude-haiku-4-5"
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAI
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.5",
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
}
|
||||
```
|
||||
|
||||
### Google (Gemini)
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "gemini",
|
||||
modelId: "gemini-3.1-pro-preview",
|
||||
apiKey: process.env.GOOGLE_API_KEY,
|
||||
}
|
||||
```
|
||||
|
||||
### Google (Vertex AI)
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "vertex",
|
||||
modelId: "gemini-3.1-pro-preview",
|
||||
// Uses application default credentials or service account
|
||||
}
|
||||
```
|
||||
|
||||
### AWS Bedrock
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "bedrock",
|
||||
modelId: "anthropic.claude-sonnet-4-6",
|
||||
// Uses AWS credential chain (env vars, config file, IAM role)
|
||||
// Set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
```
|
||||
|
||||
### Mistral
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "mistral",
|
||||
modelId: "mistral-large-latest",
|
||||
apiKey: process.env.MISTRAL_API_KEY,
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAI-Compatible
|
||||
|
||||
For any provider with an OpenAI-compatible API:
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "openai-compatible",
|
||||
modelId: "my-model",
|
||||
apiKey: process.env.API_KEY,
|
||||
baseUrl: "https://api.together.xyz/v1",
|
||||
}
|
||||
```
|
||||
|
||||
Works with: vLLM, Together AI, Fireworks, Groq, Ollama, LiteLLM, etc.
|
||||
|
||||
## Custom Base URL
|
||||
|
||||
Override the API endpoint for any provider:
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
apiKey: process.env.API_KEY,
|
||||
baseUrl: "https://my-proxy.example.com/v1",
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Headers
|
||||
|
||||
Pass additional headers to API requests:
|
||||
|
||||
```typescript
|
||||
{
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.5",
|
||||
apiKey: process.env.API_KEY,
|
||||
headers: {
|
||||
"X-Custom-Header": "value",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Gateway API
|
||||
|
||||
For advanced multi-provider setups, use the Gateway directly:
|
||||
|
||||
```typescript
|
||||
import { createGateway, DefaultGateway } from "@cline/llms"
|
||||
|
||||
const gateway = createGateway({
|
||||
providerConfigs: [
|
||||
{ providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
|
||||
{ providerId: "openai", apiKey: process.env.OPENAI_API_KEY },
|
||||
],
|
||||
})
|
||||
|
||||
// Create a model for a specific provider
|
||||
const model = gateway.createAgentModel({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-opus-4-7",
|
||||
})
|
||||
|
||||
// Use with Agent
|
||||
const agent = new Agent({ model, systemPrompt: "...", tools: [] })
|
||||
```
|
||||
|
||||
### Gateway Methods
|
||||
|
||||
```typescript
|
||||
gateway.registerProvider(registration) // add a custom provider
|
||||
gateway.configureProvider(config) // update provider settings
|
||||
gateway.listProviders() // list available providers
|
||||
gateway.listModels(providerId?) // list available models
|
||||
gateway.createAgentModel(selection) // create model for agent
|
||||
gateway.stream(request) // raw streaming (AsyncIterable)
|
||||
```
|
||||
|
||||
## Provider Registry
|
||||
|
||||
Query and register providers programmatically:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
getAllProviders,
|
||||
getProviderIds,
|
||||
getProvider,
|
||||
getModelsForProvider,
|
||||
registerProvider,
|
||||
registerModel,
|
||||
createHandler,
|
||||
} from "@cline/llms"
|
||||
|
||||
// List all registered providers
|
||||
const providers = getAllProviders()
|
||||
|
||||
// Get models for a provider
|
||||
const models = getModelsForProvider("anthropic")
|
||||
|
||||
// Register a custom provider
|
||||
registerProvider({
|
||||
id: "my-provider",
|
||||
name: "My Custom Provider",
|
||||
handler: createHandler({ ... }),
|
||||
})
|
||||
```
|
||||
|
||||
## Model Metadata
|
||||
|
||||
Access model info (context window, pricing, capabilities):
|
||||
|
||||
```typescript
|
||||
import { getModelsForProvider } from "@cline/llms"
|
||||
|
||||
const models = getModelsForProvider("anthropic")
|
||||
for (const model of models) {
|
||||
console.log(`${model.id}: context=${model.contextWindow}, input=$${model.inputPrice}/MTok`)
|
||||
}
|
||||
```
|
||||
|
||||
## Cost Tracking
|
||||
|
||||
Track per-request and cumulative costs:
|
||||
|
||||
```typescript
|
||||
// Via events
|
||||
agent.subscribe((event) => {
|
||||
if (event.type === "usage-updated") {
|
||||
console.log(`Cost: $${event.usage.totalCost?.toFixed(4)}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Via result
|
||||
const result = await agent.run("...")
|
||||
console.log(`Total cost: $${result.usage.totalCost?.toFixed(4)}`)
|
||||
|
||||
// Via ClineCore accumulated usage
|
||||
const usage = await cline.getAccumulatedUsage(sessionId)
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `../agent/REFERENCE.md` - Using providers with Agent
|
||||
- `../clinecore/REFERENCE.md` - Using providers with ClineCore
|
||||
- `../production/REFERENCE.md` - Cost control in production
|
||||
@@ -1,227 +0,0 @@
|
||||
# Scheduling and Automation
|
||||
|
||||
The Cline SDK supports scheduled, one-off, and event-driven agent execution through the automation subsystem in `@cline/core`.
|
||||
|
||||
## Overview
|
||||
|
||||
Three trigger types:
|
||||
|
||||
| Trigger | Description |
|
||||
|---------|-------------|
|
||||
| `schedule` | Recurring jobs via cron expressions |
|
||||
| `one_off` | Single execution tasks |
|
||||
| `event` | Triggered by external events (GitHub, Linear, custom) |
|
||||
|
||||
## CLI Schedule Management
|
||||
|
||||
```bash
|
||||
# Create a recurring schedule
|
||||
cline schedule create "Daily standup" \
|
||||
--cron "0 9 * * MON-FRI" \
|
||||
--prompt "Summarize open PRs and blockers" \
|
||||
--workspace /path/to/project \
|
||||
--model anthropic/claude-sonnet-4-6
|
||||
|
||||
# List schedules
|
||||
cline schedule list
|
||||
|
||||
# Trigger a schedule immediately
|
||||
cline schedule trigger <schedule-id>
|
||||
|
||||
# Pause/resume
|
||||
cline schedule pause <schedule-id>
|
||||
cline schedule resume <schedule-id>
|
||||
|
||||
# Delete
|
||||
cline schedule delete <schedule-id>
|
||||
|
||||
# View past executions
|
||||
cline schedule executions <schedule-id>
|
||||
```
|
||||
|
||||
## Cron Expressions
|
||||
|
||||
| Expression | Meaning |
|
||||
|-----------|---------|
|
||||
| `0 9 * * MON-FRI` | 9 AM weekdays |
|
||||
| `0 */6 * * *` | Every 6 hours |
|
||||
| `0 8 * * MON` | Mondays at 8 AM |
|
||||
| `*/30 * * * *` | Every 30 minutes |
|
||||
| `0 0 1 * *` | First of every month |
|
||||
|
||||
## File-Based Specs
|
||||
|
||||
Create Markdown files in `~/.cline/cron/` (global) or `.cline/cron/` (workspace):
|
||||
|
||||
### Recurring Schedule
|
||||
|
||||
```markdown
|
||||
---
|
||||
trigger: schedule
|
||||
schedule: "0 9 * * MON-FRI"
|
||||
timezone: America/New_York
|
||||
mode: exclusive
|
||||
prompt: "Check for dependency updates and create PRs for any outdated packages."
|
||||
modelSelection:
|
||||
providerId: anthropic
|
||||
modelId: claude-sonnet-4-6
|
||||
tools:
|
||||
enabled: true
|
||||
---
|
||||
|
||||
Additional context or instructions for the agent go in the body.
|
||||
```
|
||||
|
||||
### One-Off Task
|
||||
|
||||
```markdown
|
||||
---
|
||||
trigger: one_off
|
||||
prompt: "Generate a comprehensive test coverage report."
|
||||
modelSelection:
|
||||
providerId: anthropic
|
||||
modelId: claude-sonnet-4-6
|
||||
---
|
||||
```
|
||||
|
||||
### Event-Driven
|
||||
|
||||
```markdown
|
||||
---
|
||||
trigger: event
|
||||
eventType: github.pull_request.opened
|
||||
filters:
|
||||
repository: myorg/myrepo
|
||||
debounceMs: 5000
|
||||
cooldownMs: 60000
|
||||
prompt: "Review the PR for security issues and code quality."
|
||||
modelSelection:
|
||||
providerId: anthropic
|
||||
modelId: claude-sonnet-4-6
|
||||
---
|
||||
```
|
||||
|
||||
## CronSpec Types
|
||||
|
||||
```typescript
|
||||
interface CronScheduleSpec {
|
||||
trigger: "schedule"
|
||||
schedule: string // cron expression
|
||||
timezone?: string
|
||||
mode?: "exclusive" | "concurrent"
|
||||
prompt: string
|
||||
modelSelection?: { providerId: string; modelId?: string }
|
||||
extensionLoading?: "isolated" | "direct"
|
||||
configExtensions?: RuntimeConfigExtensionKind[]
|
||||
tools?: { enabled?: boolean; names?: string[] }
|
||||
}
|
||||
|
||||
interface CronOneOffSpec {
|
||||
trigger: "one_off"
|
||||
prompt: string
|
||||
modelSelection?: { providerId: string; modelId?: string }
|
||||
}
|
||||
|
||||
interface CronEventSpec {
|
||||
trigger: "event"
|
||||
eventType: string // e.g., "github.pull_request.opened"
|
||||
filters?: Record<string, unknown>
|
||||
debounceMs?: number
|
||||
cooldownMs?: number
|
||||
prompt: string
|
||||
modelSelection?: { providerId: string; modelId?: string }
|
||||
}
|
||||
```
|
||||
|
||||
## Programmatic Automation API
|
||||
|
||||
```typescript
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "my-app",
|
||||
automation: true,
|
||||
})
|
||||
|
||||
// Start automation service
|
||||
cline.automation.start()
|
||||
|
||||
// Ingest an external event
|
||||
cline.automation.ingestEvent({
|
||||
eventId: "evt-123",
|
||||
eventType: "github.pull_request.opened",
|
||||
source: "github",
|
||||
timestamp: Date.now(),
|
||||
payload: { pr: { number: 42, title: "..." } },
|
||||
})
|
||||
|
||||
// List specs, runs, events
|
||||
const specs = await cline.automation.listSpecs()
|
||||
const runs = await cline.automation.listRuns()
|
||||
const events = await cline.automation.listEvents()
|
||||
|
||||
// Reconcile specs from directory
|
||||
await cline.automation.reconcile(specDirectory)
|
||||
|
||||
// Stop automation
|
||||
cline.automation.stop()
|
||||
```
|
||||
|
||||
## Event Ingestion from Plugins
|
||||
|
||||
Plugins can declare and emit automation events:
|
||||
|
||||
```typescript
|
||||
const webhookPlugin: AgentPlugin = {
|
||||
name: "webhook-events",
|
||||
manifest: { capabilities: ["automationEvents"] },
|
||||
setup(api) {
|
||||
api.registerAutomationEventType({
|
||||
type: "webhook.received",
|
||||
description: "External webhook received",
|
||||
})
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Submit events via the plugin context:
|
||||
|
||||
```typescript
|
||||
ctx.automation.ingestEvent({
|
||||
eventId: "evt-456",
|
||||
eventType: "webhook.received",
|
||||
source: "custom",
|
||||
timestamp: Date.now(),
|
||||
payload: { ... },
|
||||
})
|
||||
```
|
||||
|
||||
## Concurrency Control
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `"exclusive"` | Skip if previous run still active |
|
||||
| `"concurrent"` | Allow overlapping runs |
|
||||
|
||||
## Run Reports
|
||||
|
||||
Each completed run writes a Markdown report to `.cline/cron/reports/<run-id>.md` with:
|
||||
- Run metadata (spec, trigger, timing)
|
||||
- Summary of agent output
|
||||
- Usage (tokens, cost)
|
||||
- Tool calls made
|
||||
- Trigger event context (for event-driven runs)
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Daily standup summaries
|
||||
- Automated dependency update checks
|
||||
- PR review on open
|
||||
- Codebase health reports
|
||||
- Scheduled security scans
|
||||
- Event-driven CI/CD workflows
|
||||
|
||||
## See Also
|
||||
|
||||
- `../clinecore/REFERENCE.md` - ClineCore runtime
|
||||
- `../clinecore/api.md` - Automation API details
|
||||
- `../plugins/REFERENCE.md` - Plugin events
|
||||
- `../production/REFERENCE.md` - Production deployment
|
||||
@@ -1,259 +0,0 @@
|
||||
# Tools
|
||||
|
||||
Tools are how agents interact with the world. The Cline SDK supports both built-in tools (via ClineCore) and custom tools you define yourself.
|
||||
|
||||
## Creating Custom Tools
|
||||
|
||||
Use `createTool()` from `@cline/sdk` (or `@cline/shared`):
|
||||
|
||||
```typescript
|
||||
import { createTool } from "@cline/sdk"
|
||||
|
||||
const myTool = createTool({
|
||||
name: "search_issues",
|
||||
description: "Search GitHub issues by query. Returns up to 10 results.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query" },
|
||||
state: { type: "string", enum: ["open", "closed", "all"] },
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
execute: async (input) => {
|
||||
const issues = await github.searchIssues(input.query, input.state)
|
||||
return { issues, count: issues.length }
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### With Zod Schema
|
||||
|
||||
```typescript
|
||||
import { createTool } from "@cline/sdk"
|
||||
import { z } from "zod"
|
||||
|
||||
const deployTool = createTool({
|
||||
name: "deploy",
|
||||
description: "Deploy the app to the specified environment.",
|
||||
inputSchema: z.object({
|
||||
environment: z.enum(["staging", "production"]).describe("Target environment"),
|
||||
version: z.string().optional().describe("Version tag, defaults to latest"),
|
||||
}),
|
||||
execute: async (input) => {
|
||||
const result = await deploy(input.environment, input.version)
|
||||
return { url: result.url, status: "deployed" }
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Tool Config Options
|
||||
|
||||
```typescript
|
||||
createTool({
|
||||
name: string, // snake_case, unique per agent
|
||||
description: string, // what the tool does (model reads this)
|
||||
inputSchema: JSONSchema | ZodSchema, // input validation
|
||||
execute: async (input, context, onChange?) => output,
|
||||
timeoutMs?: number, // default: 30000
|
||||
retryable?: boolean, // default: true
|
||||
maxRetries?: number, // default: 3
|
||||
lifecycle?: {
|
||||
completesRun?: boolean // true = ends agent loop on success
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### AgentToolContext
|
||||
|
||||
The second argument to `execute` provides runtime context:
|
||||
|
||||
```typescript
|
||||
interface AgentToolContext {
|
||||
agentId: string
|
||||
conversationId: string
|
||||
iteration: number
|
||||
abortSignal?: AbortSignal
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
```
|
||||
|
||||
## Tool Naming Rules
|
||||
|
||||
- Names must be `snake_case` (e.g., `search_issues`, `deploy_app`)
|
||||
- Names must be unique within a single agent's tool set
|
||||
- Choose descriptive names since the model uses them to decide which tool to call
|
||||
|
||||
## Tool Descriptions Matter
|
||||
|
||||
The model reads the tool description to decide when and how to use it. Write clear, specific descriptions:
|
||||
|
||||
```typescript
|
||||
// Bad: vague
|
||||
description: "Does deployment stuff"
|
||||
|
||||
// Good: specific with constraints
|
||||
description: "Deploy the application to staging or production. " +
|
||||
"Staging deployments are immediate. Production requires a passing CI build. " +
|
||||
"Returns the deployment URL and status."
|
||||
```
|
||||
|
||||
Include constraints, rate limits, and expected behavior in the description.
|
||||
|
||||
## Error Handling in Tools
|
||||
|
||||
Return errors as structured data instead of throwing:
|
||||
|
||||
```typescript
|
||||
// Good: return error data
|
||||
execute: async (input) => {
|
||||
const file = await readFile(input.path).catch(() => null)
|
||||
if (!file) {
|
||||
return { error: "File not found", path: input.path }
|
||||
}
|
||||
return { content: file }
|
||||
}
|
||||
```
|
||||
|
||||
Thrown exceptions count as "mistakes" against the agent's mistake limit. Returned error data lets the agent adjust its approach.
|
||||
|
||||
## Completion Tools
|
||||
|
||||
Tools with `lifecycle: { completesRun: true }` end the agent loop when they execute successfully:
|
||||
|
||||
```typescript
|
||||
const submitAnswer = createTool({
|
||||
name: "submit_answer",
|
||||
description: "Submit the final answer and end the task.",
|
||||
inputSchema: z.object({
|
||||
answer: z.string(),
|
||||
confidence: z.number().min(0).max(1),
|
||||
}),
|
||||
lifecycle: { completesRun: true },
|
||||
execute: async (input) => input,
|
||||
})
|
||||
```
|
||||
|
||||
The model sees the tool result and the run ends. Access the output via `result.toolCalls`.
|
||||
|
||||
## Built-in Tools (ClineCore Only)
|
||||
|
||||
When using `ClineCore` with `enableTools: true`, these tools are available automatically:
|
||||
|
||||
| Tool | Name | What It Does |
|
||||
|------|------|-------------|
|
||||
| Shell | `bash` | Execute shell commands in the session workspace |
|
||||
| Editor | `editor` | Create and edit files |
|
||||
| Read | `read_files` | Read file contents |
|
||||
| Patch | `apply_patch` | Apply unified diffs to files |
|
||||
| Search | `search` | Search file contents and directory structure |
|
||||
| Web | `fetch_web` | Fetch web content via HTTP |
|
||||
|
||||
Built-in tools respect the `cwd` setting in `CoreSessionConfig`.
|
||||
|
||||
## Tool Policies
|
||||
|
||||
Control which tools are available and whether they require approval:
|
||||
|
||||
```typescript
|
||||
// In Agent config
|
||||
const agent = new Agent({
|
||||
tools: [toolA, toolB, toolC],
|
||||
toolPolicies: {
|
||||
tool_a: { autoApprove: true }, // runs without asking
|
||||
tool_b: { autoApprove: false }, // requires approval
|
||||
tool_c: { enabled: false }, // hidden from model
|
||||
},
|
||||
})
|
||||
|
||||
// In ClineCore session
|
||||
await cline.start({
|
||||
prompt: "...",
|
||||
config: { ... },
|
||||
toolPolicies: {
|
||||
bash: { autoApprove: true },
|
||||
editor: { autoApprove: false },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Policy Options
|
||||
|
||||
| Policy | Effect |
|
||||
|--------|--------|
|
||||
| `{ autoApprove: true }` | Tool runs without approval |
|
||||
| `{ autoApprove: false }` | Triggers approval callback before running |
|
||||
| `{ enabled: false }` | Tool is hidden from the model entirely |
|
||||
| No policy set | Defaults to enabled and auto-approved |
|
||||
|
||||
## Abort Signal in Long-Running Tools
|
||||
|
||||
Respect the abort signal for tools that take a long time:
|
||||
|
||||
```typescript
|
||||
execute: async (input, context) => {
|
||||
const results = []
|
||||
for (const item of input.items) {
|
||||
if (context.abortSignal?.aborted) {
|
||||
return { results, aborted: true, processed: results.length }
|
||||
}
|
||||
results.push(await processItem(item))
|
||||
}
|
||||
return { results, processed: results.length }
|
||||
}
|
||||
```
|
||||
|
||||
## Streaming Tool Output
|
||||
|
||||
Use the `onChange` callback (third argument) to stream partial results:
|
||||
|
||||
```typescript
|
||||
execute: async (input, context, onChange) => {
|
||||
let progress = 0
|
||||
for (const step of steps) {
|
||||
progress++
|
||||
onChange?.(`Processing step ${progress}/${steps.length}...`)
|
||||
await processStep(step)
|
||||
}
|
||||
return { completed: true }
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Tools
|
||||
|
||||
Tools are plain async functions, so they're straightforward to test:
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from "vitest"
|
||||
|
||||
describe("deploy tool", () => {
|
||||
it("deploys to staging", async () => {
|
||||
const context = { agentId: "test", conversationId: "test", iteration: 1 }
|
||||
const result = await deployTool.execute({ environment: "staging" }, context)
|
||||
expect(result.status).toBe("deployed")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## MCP Tool Integration
|
||||
|
||||
ClineCore can connect to MCP (Model Context Protocol) servers for additional tools. Configure in `.cline/mcp-servers.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"my-server": {
|
||||
"command": "node",
|
||||
"args": ["./mcp-server.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
MCP tools appear alongside built-in and custom tools automatically.
|
||||
|
||||
## See Also
|
||||
|
||||
- `../agent/REFERENCE.md` - Using tools with Agent
|
||||
- `../clinecore/REFERENCE.md` - Using tools with ClineCore
|
||||
- `../plugins/REFERENCE.md` - Packaging tools as plugins
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: use correct base URL for Vertex AI global endpoint with Claude models
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/cline-sdk
|
||||
@@ -0,0 +1,33 @@
|
||||
# CLI Development
|
||||
|
||||
The CLI lives in `cli/` and uses React Ink for terminal UI.
|
||||
|
||||
- If needed, look at `cli/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
|
||||
- Never use `dimColor` with gray (e.g. `<Text color="gray" dimColor>`) - it's too hard to read. Use `color="gray"` for secondary text and normal foreground (no color) for primary text.
|
||||
- When thinking about how to handle state or messages from core, look at webview for how it communicates with the vs code extension.
|
||||
- When updating the webview, consider and suggest to the user to update the CLI TUI since we want to provide a similar experience to our terminal users as we do our vs code extension users.
|
||||
|
||||
## Adding New API Providers
|
||||
|
||||
When adding a new API provider to the extension, you must also update the CLI:
|
||||
|
||||
1. **Update `cli/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
|
||||
```typescript
|
||||
import { newProviderDefaultModelId, newProviderModels } from "@/shared/api"
|
||||
|
||||
export const providerModels = {
|
||||
// ...existing providers
|
||||
"new-provider": { models: newProviderModels, defaultId: newProviderDefaultModelId },
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli/src/utils/provider-config.ts`:
|
||||
```typescript
|
||||
import { applyProviderConfig } from "../utils/provider-config"
|
||||
|
||||
// After successful auth:
|
||||
await applyProviderConfig({ providerId: "new-provider", controller })
|
||||
```
|
||||
This handles setting provider, default model, API key mapping, state persistence, and rebuilding the API handler.
|
||||
|
||||
3. **Provider-specific auth**: If the provider uses OAuth (like `openai-codex`), add handling in `SettingsPanelContent.tsx`'s `handleProviderSelect` callback. See the existing Codex OAuth flow as a reference.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Debug Harness
|
||||
|
||||
HTTP-controlled debugger for the VSCode extension at `src/dev/debug-harness/server.ts`.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Build extension first if needed (protos + esbuild):
|
||||
npm run protos && IS_DEV=true node esbuild.mjs
|
||||
|
||||
# Launch (skip-build if already built):
|
||||
npx tsx src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
|
||||
# In another terminal:
|
||||
curl localhost:19229/api -d '{"method":"status"}'
|
||||
```
|
||||
|
||||
## Key commands
|
||||
|
||||
All via `POST localhost:19229/api` with `{"method":"...", "params":{...}}`:
|
||||
|
||||
- **`launch`** / **`shutdown`** — lifecycle
|
||||
- **`ui.screenshot`** — screenshot to `/tmp/cline-debug/`; returns `{path}` — **use `read_file` on the path to examine, do NOT `open` the file** (Preview.app covers the VSCode window)
|
||||
- **`ui.open_sidebar`** — open the Cline sidebar
|
||||
- **`ext.set_breakpoint`** `{file, line, condition?}` — breakpoint by source file (sourcemap-resolved)
|
||||
- **`ext.evaluate`** `{expression, callFrameId?}` — eval in extension host
|
||||
- **`ext.resume`** / **`ext.step_over`** / **`ext.step_into`** — stepping
|
||||
- **`ext.call_stack`** — inspect when paused
|
||||
- **`web.evaluate`** `{expression}` — eval in webview
|
||||
- **`web.post_message`** `{message}` — send postMessage to extension host via exposed vsCodeApi
|
||||
- **`wait_for_pause`** `{timeout?}` — block until breakpoint hit
|
||||
- **`ui.locator`** `{role?, testId?, text?, action?, frame?}` — Playwright locator (auto-retries on stale sidebar frame)
|
||||
- **`ui.react_input`** `{text, selector?, clear?, submit?}` — set React textarea value via `execCommand('insertText')`; works reliably across multiple tasks
|
||||
- **`ui.send_message`** `{text, images?, files?, responseType?}` — send chat message bypassing the textarea entirely (via gRPC postMessage)
|
||||
- **`ui.command_palette`** `{command}` — run VSCode command
|
||||
|
||||
## Caveats
|
||||
|
||||
- **⚠️ Dismiss "Introducing Cline Kanban" overlay FIRST**: On fresh launches a full-screen promo overlay blocks the sidebar. **Dismiss it immediately after `ui.open_sidebar`**, before any other interaction or screenshot. Most reliable method:
|
||||
```bash
|
||||
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
|
||||
curl localhost:19229/api -d '{"method": "web.evaluate", "params": {"expression": "document.querySelector(\".sr-only\")?.parentElement?.click()"}}'
|
||||
```
|
||||
- **Screenshots — don't open the file**: `ui.screenshot` and `ui.sidebar_screenshot` save PNGs to `/tmp/cline-debug/` and return the `{path}`. Use `read_file` on the path to examine screenshots. Running `open <path>` launches Preview.app on macOS which covers the VSCode window.
|
||||
- **Scripts count = 0 after launch**: CDP connects after extension host starts, so scripts parsed during startup aren't tracked. Breakpoints still work via sourcemap resolution.
|
||||
- **Port 9230**: Extension host inspector. If another VSCode instance uses this port, the harness will fail to connect. Kill other debug instances first.
|
||||
- **macOS only** for now (Playwright Electron launch behavior).
|
||||
- **Webview CDP**: `connect_webview` may fail depending on Electron version. `web.evaluate` still works via Playwright's `frame.evaluate()` fallback.
|
||||
- **Sourcemap paths**: esbuild outputs relative paths like `../src/extension.ts` in the sourcemap. The resolver handles this, but if a file isn't found, use `ext.source_files` to see exact paths.
|
||||
|
||||
See `src/dev/debug-harness/README.md` for full API reference.
|
||||
@@ -176,7 +176,7 @@ Present a final summary:
|
||||
- Slack message copied to clipboard: yes
|
||||
|
||||
Remind the user to:
|
||||
1. Manually trigger the publish release GitHub Action at: https://github.com/cline/cline/actions/workflows/ext-vscode-publish-stable.yml (paste `v{VERSION}` as the tag)
|
||||
1. Manually trigger the publish release GitHub Action at: https://github.com/cline/cline/actions/workflows/publish.yml (paste `v{VERSION}` as the tag)
|
||||
2. Post the Slack message to announce the hotfix
|
||||
|
||||
## Important Notes
|
||||
|
||||
@@ -43,7 +43,7 @@ git push origin v<version>
|
||||
### 4) Trigger publish workflow
|
||||
|
||||
Tell the maintainer to run:
|
||||
https://github.com/cline/cline/actions/workflows/ext-vscode-publish-stable.yml
|
||||
https://github.com/cline/cline/actions/workflows/publish.yml
|
||||
|
||||
Use `v<version>` as the release tag.
|
||||
|
||||
|
||||
@@ -20,9 +20,8 @@ command = "chmod +x ./scripts/run-extension-host.sh && ./scripts/run-extension-h
|
||||
name = "CLI"
|
||||
icon = "run"
|
||||
command = '''
|
||||
cd sdk
|
||||
bun install
|
||||
bun run cli
|
||||
npm run cli:build
|
||||
npm run cli:run
|
||||
'''
|
||||
|
||||
[[actions]]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
webview-ui/src/assets/cline_kanban_demo.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
webview-ui/src/assets/cline_kanban_demo.webm filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
* text=auto eol=lf
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @dominiccooney
|
||||
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
|
||||
/README.md @saoudrizwan @juanpflores
|
||||
|
||||
@@ -26,12 +26,6 @@ body:
|
||||
placeholder: 'e.g., 1.2.3'
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: beta
|
||||
attributes:
|
||||
label: Beta version
|
||||
options:
|
||||
- label: I am using a beta version of Cline
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
|
||||
@@ -5,6 +5,7 @@ This is a VS Code extension. Read `.clinerules/general.md` for tribal knowledge
|
||||
## Architecture
|
||||
- **Core** (`src/`): `extension.ts` → `WebviewProvider` → `Controller` (single source of truth) → `Task` (agent loop).
|
||||
- **Webview** (`webview-ui/`): React/Vite app. State via `ExtensionStateContext.tsx`, synced through message passing.
|
||||
- **CLI** (`cli/`): React Ink terminal UI sharing core logic. Update CLI when changing webview features.
|
||||
- **Communication**: Protobuf-defined gRPC-like protocol over VS Code message passing. Schemas in `proto/`.
|
||||
- **MCP**: `src/services/mcp/McpHub.ts`.
|
||||
|
||||
@@ -27,7 +28,7 @@ Three proto conversion updates are **required** or the provider silently resets
|
||||
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts`.
|
||||
3. `convertProtoToApiProvider()` in the same file.
|
||||
|
||||
Also update: `src/shared/api.ts`, `src/shared/providers/providers.json`, `src/core/api/index.ts`, `webview-ui/.../providerUtils.ts`, `webview-ui/.../validate.ts`, `webview-ui/.../ApiOptions.tsx`.
|
||||
Also update: `src/shared/api.ts`, `src/shared/providers/providers.json`, `src/core/api/index.ts`, `webview-ui/.../providerUtils.ts`, `webview-ui/.../validate.ts`, `webview-ui/.../ApiOptions.tsx`, and `cli/src/components/ModelPicker.tsx`.
|
||||
|
||||
For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/model-utils.ts` and set `apiFormat: ApiFormat.OPENAI_RESPONSES` on models.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ version: 2
|
||||
updates:
|
||||
# Main extension dependencies
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/vscode"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Group all updates into a single PR
|
||||
@@ -20,7 +20,7 @@ updates:
|
||||
|
||||
# Webview UI dependencies
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/vscode/webview-ui"
|
||||
directory: "/webview-ui"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
|
||||
@@ -1,435 +0,0 @@
|
||||
name: cli-publish
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_target:
|
||||
description: "Which publish flow to run"
|
||||
required: true
|
||||
default: "main"
|
||||
type: choice
|
||||
options:
|
||||
- main
|
||||
- nightly
|
||||
git_tag:
|
||||
description: "Existing release tag to publish when publish_target=main, for example cli-v0.1.0"
|
||||
required: false
|
||||
type: string
|
||||
confirm_publish:
|
||||
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
|
||||
required: false
|
||||
type: string
|
||||
force_nightly_publish:
|
||||
description: "Force nightly publish even with no commits in last 24h"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
|
||||
jobs:
|
||||
publish-main:
|
||||
name: Publish cline
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
if: |
|
||||
github.repository == 'cline/cline' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.event.inputs.publish_target == 'main' &&
|
||||
github.event.inputs.confirm_publish == 'publish' &&
|
||||
!endsWith(github.actor, '[bot]')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.git_tag }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Verify publish tooling
|
||||
run: |
|
||||
NPM_VERSION=$(npm --version)
|
||||
echo "npm ${NPM_VERSION}"
|
||||
IFS=. read -r major minor patch <<EOF
|
||||
${NPM_VERSION}
|
||||
EOF
|
||||
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
|
||||
echo "npm 11.5.1 or newer is required for trusted publishing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Validate release tag
|
||||
id: version
|
||||
env:
|
||||
TAG: ${{ github.event.inputs.git_tag }}
|
||||
run: |
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "git_tag is required when publish_target=main"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf "%s\n" "$TAG" | grep -Eq '^cli-v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
|
||||
echo "git_tag must look like cli-vX.Y.Z, got: ${TAG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="${TAG#cli-v}"
|
||||
PACKAGE_VERSION=$(node -p "require('./apps/cli/package.json').version")
|
||||
|
||||
if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
|
||||
echo "sdk/apps/cli/package.json version ${PACKAGE_VERSION} does not match ${TAG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf "%s\n" "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
|
||||
echo "sdk/apps/cli/package.json has invalid version: ${VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG_COMMIT=$(git rev-parse "${TAG}^{commit}")
|
||||
HEAD_COMMIT=$(git rev-parse HEAD)
|
||||
if [ "$TAG_COMMIT" != "$HEAD_COMMIT" ]; then
|
||||
echo "${TAG} does not point at the checked out commit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch origin +main:refs/remotes/origin/main
|
||||
if ! git merge-base --is-ancestor "$HEAD_COMMIT" origin/main; then
|
||||
echo "${TAG} is not reachable from origin/main"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build SDK packages
|
||||
run: bun run build:sdk
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
|
||||
- name: Run tests
|
||||
run: bun run test
|
||||
|
||||
- name: Build platform binaries
|
||||
run: bun script/build.ts --install-native-variants --skip-sdk-build
|
||||
working-directory: sdk/apps/cli
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
|
||||
- name: Verify build output
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
EXPECTED=(
|
||||
"@cline/cli-darwin-arm64"
|
||||
"@cline/cli-darwin-x64"
|
||||
"@cline/cli-linux-arm64"
|
||||
"@cline/cli-linux-x64"
|
||||
"@cline/cli-windows-arm64"
|
||||
"@cline/cli-windows-x64"
|
||||
)
|
||||
|
||||
for package_name in "${EXPECTED[@]}"; do
|
||||
dir="apps/cli/dist/${package_name#@cline/}"
|
||||
if [ ! -f "$dir/package.json" ]; then
|
||||
echo "Missing package manifest: $dir/package.json"
|
||||
exit 1
|
||||
fi
|
||||
actual_name=$(node -p "require('./$dir/package.json').name")
|
||||
actual_version=$(node -p "require('./$dir/package.json').version")
|
||||
if [ "$actual_name" != "$package_name" ]; then
|
||||
echo "Expected $package_name, got $actual_name"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$actual_version" != "$VERSION" ]; then
|
||||
echo "Expected $package_name@$VERSION, got $actual_version"
|
||||
exit 1
|
||||
fi
|
||||
ls -lh "$dir/bin/"
|
||||
done
|
||||
|
||||
- name: Publish to NPM with latest tag
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: bun script/publish-npm.ts --tag latest
|
||||
working-directory: sdk/apps/cli
|
||||
|
||||
- name: Get Previous CLI Tag
|
||||
id: prev_tag
|
||||
env:
|
||||
CURRENT_TAG: ${{ steps.version.outputs.tag }}
|
||||
run: |
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 --match 'cli-v*' "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Changelog Entry
|
||||
id: changelog
|
||||
run: |
|
||||
# Grab content between the first "## " header and the next one in apps/cli/CHANGELOG.md
|
||||
CONTENT=$(awk '/^## [0-9]/{if(found) exit; found=1; next} found{print}' apps/cli/CHANGELOG.md)
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.tag }}
|
||||
name: "CLI v${{ steps.version.outputs.version }}"
|
||||
body: |
|
||||
${{ steps.changelog.outputs.content }}
|
||||
|
||||
${{ steps.prev_tag.outputs.prev_tag != '' && format('**Full Changelog**: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.tag) || '' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Published cline@${VERSION} to npm with dist-tag 'latest'"
|
||||
echo "Install with: npm install -g cline"
|
||||
|
||||
- name: Post release to Slack
|
||||
uses: slackapi/slack-github-action@v3.0.1
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: "C0APVKGGZFC"
|
||||
text: "Cline CLI v${{ steps.version.outputs.version }}"
|
||||
blocks:
|
||||
- type: "section"
|
||||
text:
|
||||
type: "mrkdwn"
|
||||
text: "Cline CLI v${{ steps.version.outputs.version }}"
|
||||
- type: "section"
|
||||
text:
|
||||
type: "mrkdwn"
|
||||
text: ${{ toJSON(steps.changelog.outputs.content) }}
|
||||
- type: "context"
|
||||
elements:
|
||||
- type: "mrkdwn"
|
||||
text: "<https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}|View on npm>${{ steps.prev_tag.outputs.prev_tag != '' && format(' | Full Changelog: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.tag) || '' }}"
|
||||
|
||||
publish-nightly:
|
||||
name: Publish cline nightly
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
if: |
|
||||
github.repository == 'cline/cline' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
(
|
||||
github.event_name == 'schedule' ||
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.event.inputs.publish_target == 'nightly'
|
||||
)
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for recent commits
|
||||
id: check_commits
|
||||
env:
|
||||
FORCE_PUBLISH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
|
||||
run: |
|
||||
if [ "$FORCE_PUBLISH" = "true" ]; then
|
||||
echo "force_nightly_publish enabled, proceeding with publish"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(git rev-list --count HEAD --since='24 hours ago')" -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, skipping publish"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Found recent commits, proceeding with publish"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Verify publish tooling
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
NPM_VERSION=$(npm --version)
|
||||
echo "npm ${NPM_VERSION}"
|
||||
IFS=. read -r major minor patch <<EOF
|
||||
${NPM_VERSION}
|
||||
EOF
|
||||
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
|
||||
echo "npm 11.5.1 or newer is required for trusted publishing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build SDK packages
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun run build:sdk
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
|
||||
- name: Run tests
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun run test
|
||||
|
||||
- name: Generate nightly version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
BASE_VERSION=$(node -p "require('./apps/cli/package.json').version")
|
||||
TIMESTAMP=$(date +%s)
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
|
||||
echo "Base version: ${BASE_VERSION}"
|
||||
echo "Generated nightly version: ${VERSION}"
|
||||
echo "base_version=${BASE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update nightly package version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const path = "apps/cli/package.json";
|
||||
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
pkg.version = process.env.VERSION;
|
||||
fs.writeFileSync(path, `${JSON.stringify(pkg, null, "\t")}\n`);
|
||||
'
|
||||
cat apps/cli/package.json | grep '"version"'
|
||||
|
||||
- name: Build platform binaries
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun script/build.ts --install-native-variants --skip-sdk-build
|
||||
working-directory: sdk/apps/cli
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
|
||||
- name: Verify build output
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
EXPECTED=(
|
||||
"@cline/cli-darwin-arm64"
|
||||
"@cline/cli-darwin-x64"
|
||||
"@cline/cli-linux-arm64"
|
||||
"@cline/cli-linux-x64"
|
||||
"@cline/cli-windows-arm64"
|
||||
"@cline/cli-windows-x64"
|
||||
)
|
||||
|
||||
for package_name in "${EXPECTED[@]}"; do
|
||||
dir="apps/cli/dist/${package_name#@cline/}"
|
||||
if [ ! -f "$dir/package.json" ]; then
|
||||
echo "Missing package manifest: $dir/package.json"
|
||||
exit 1
|
||||
fi
|
||||
actual_name=$(node -p "require('./$dir/package.json').name")
|
||||
actual_version=$(node -p "require('./$dir/package.json').version")
|
||||
if [ "$actual_name" != "$package_name" ]; then
|
||||
echo "Expected $package_name, got $actual_name"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$actual_version" != "$VERSION" ]; then
|
||||
echo "Expected $package_name@$VERSION, got $actual_version"
|
||||
exit 1
|
||||
fi
|
||||
ls -lh "$dir/bin/"
|
||||
done
|
||||
|
||||
- name: Publish to NPM with nightly tag
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: bun script/publish-npm.ts --tag nightly
|
||||
working-directory: sdk/apps/cli
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Published cline@${VERSION} to npm with dist-tag 'nightly'"
|
||||
echo "Install with: npm install -g cline@nightly"
|
||||
@@ -0,0 +1,83 @@
|
||||
name: CLI TUI Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cli-tui-tests:
|
||||
name: CLI TUI Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build CLI
|
||||
run: npm run cli:build
|
||||
|
||||
- name: Run TUI Tests
|
||||
id: tui_tests
|
||||
run: |
|
||||
npm run test:e2e:cli:tui 2>&1 | tee tui-test-output.log
|
||||
exit_code=${PIPESTATUS[0]}
|
||||
echo "tui_exit_code=$exit_code" >> $GITHUB_OUTPUT
|
||||
exit $exit_code
|
||||
|
||||
- name: Write failure summary
|
||||
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
|
||||
run: |
|
||||
echo "## ❌ CLI TUI Tests Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Step outcome:** \`${{ steps.tui_tests.outcome }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Test Output" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
if [ -f tui-test-output.log ]; then
|
||||
cat tui-test-output.log >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "(no test output captured — process may have been killed before output was flushed)" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Debugging" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **TUI traces** are attached as artifacts below — download and inspect them to see terminal state at the point of failure." >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **To view a trace replay/Run a TUI Trace: ** run \`npx tui-test show-trace path/to/trace/file\` in your terminal" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Full test log** is also attached as an artifact." >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Tests run with \`retries: 2\` so any failure shown is a consistent failure, not a flake." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload TUI traces
|
||||
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tui-test-traces
|
||||
path: tests/e2e/cli/tui-traces/
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload test log
|
||||
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tui-test-log
|
||||
path: tui-test-output.log
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,85 @@
|
||||
name: Smoke Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'src/core/**'
|
||||
- 'src/shared/**'
|
||||
- 'proto/**'
|
||||
- 'evals/**'
|
||||
- '.github/workflows/cline-evals-regression.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/core/**'
|
||||
- 'src/shared/**'
|
||||
- 'proto/**'
|
||||
- 'evals/**'
|
||||
- '.github/workflows/cline-evals-regression.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: smoke-tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
smoke-tests:
|
||||
name: Smoke Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build and install CLI
|
||||
run: |
|
||||
npm run protos
|
||||
cd cli && npm install && npm run build && npm link
|
||||
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Verify CLI
|
||||
run: cline --version
|
||||
|
||||
- name: Run smoke tests
|
||||
env:
|
||||
CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }}
|
||||
run: |
|
||||
cline auth -p cline -k "$CLINE_API_KEY" -m "anthropic/claude-sonnet-4.5"
|
||||
max_attempts=3
|
||||
for attempt in $(seq 1 $max_attempts); do
|
||||
echo "::group::Attempt $attempt of $max_attempts"
|
||||
if npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel; then
|
||||
echo "::endgroup::"
|
||||
echo "Smoke tests passed on attempt $attempt"
|
||||
exit 0
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
if [ $attempt -lt $max_attempts ]; then
|
||||
echo "::warning::Smoke tests failed on attempt $attempt, retrying..."
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
echo "::error::Smoke tests failed after $max_attempts attempts"
|
||||
exit 1
|
||||
|
||||
- name: Generate summary
|
||||
if: always()
|
||||
run: cat evals/smoke-tests/results/latest/summary.md >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: smoke-test-results-${{ github.run_id }}
|
||||
path: evals/smoke-tests/results/latest/
|
||||
retention-days: 30
|
||||
@@ -1,4 +1,4 @@
|
||||
name: ext-vscode-test-e2e
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -12,53 +12,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
name: Detect Changes
|
||||
outputs:
|
||||
e2e: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.e2e == 'true' }}
|
||||
steps:
|
||||
- id: force
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
run: echo "run_all=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
if: steps.force.outputs.run_all != 'true'
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
if: steps.force.outputs.run_all != 'true'
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
e2e:
|
||||
- 'apps/vscode/src/**'
|
||||
- 'apps/vscode/webview-ui/**'
|
||||
- 'apps/vscode/proto/**'
|
||||
- 'apps/vscode/tests/**'
|
||||
- 'apps/vscode/scripts/**'
|
||||
- 'apps/vscode/standalone/**'
|
||||
- 'apps/vscode/assets/**'
|
||||
- 'apps/vscode/walkthrough/**'
|
||||
- 'apps/vscode/package.json'
|
||||
- 'apps/vscode/package-lock.json'
|
||||
- 'apps/vscode/buf.yaml'
|
||||
- 'apps/vscode/tsconfig*.json'
|
||||
- 'apps/vscode/biome.jsonc'
|
||||
- 'apps/vscode/esbuild.mjs'
|
||||
- 'apps/vscode/.mocharc.json'
|
||||
- 'apps/vscode/.vscode-test.mjs'
|
||||
- 'apps/vscode/.vscodeignore'
|
||||
- 'apps/vscode/playwright*.ts'
|
||||
- '.github/workflows/ext-vscode-test-e2e.yml'
|
||||
|
||||
matrix_prep:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.e2e == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
@@ -68,8 +23,7 @@ jobs:
|
||||
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
|
||||
|
||||
e2e:
|
||||
needs: [detect-changes, matrix_prep]
|
||||
if: needs.detect-changes.outputs.e2e == 'true'
|
||||
needs: matrix_prep
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -79,9 +33,6 @@ jobs:
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js environment
|
||||
@@ -94,24 +45,24 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: apps/vscode/node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('apps/vscode/package-lock.json') }}
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: apps/vscode/webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('apps/vscode/webview-ui/package-lock.json') }}
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
# Cache VS Code installation
|
||||
- name: Cache VS Code
|
||||
uses: actions/cache@v4
|
||||
id: vscode-cache
|
||||
with:
|
||||
path: apps/vscode/.vscode-test
|
||||
key: vscode-${{ runner.os }}-stable-${{ hashFiles('apps/vscode/.vscode-test.mjs', 'apps/vscode/package.json') }}
|
||||
path: .vscode-test
|
||||
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
|
||||
restore-keys: |
|
||||
vscode-${{ runner.os }}-stable-
|
||||
|
||||
@@ -124,7 +75,7 @@ jobs:
|
||||
~/.cache/ms-playwright
|
||||
~/Library/Caches/ms-playwright
|
||||
~/AppData/Local/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('apps/vscode/package-lock.json') }}
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
name: ext-vscode-publish-nightly
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
run-name: "Publish Nightly from ${{ github.ref_name }} @ ${{ github.sha }}"
|
||||
|
||||
# Prevent concurrent publish runs on the same branch. The nightly publish script
|
||||
# generates the extension version from a seconds-resolution timestamp, so parallel
|
||||
# runs on the same ref can collide on the same version and cause publish failures
|
||||
# or inconsistent tagging. Runs on different branches proceed independently.
|
||||
concurrency:
|
||||
group: ext-vscode-publish-nightly-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
uses: ./.github/workflows/ext-vscode-test.yml
|
||||
|
||||
publish:
|
||||
needs: test
|
||||
permissions:
|
||||
contents: write
|
||||
name: Publish Cline (Nightly) Extension
|
||||
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
|
||||
runs-on: ubuntu-latest
|
||||
environment: PublishNightly
|
||||
|
||||
steps:
|
||||
- name: Checkout selected branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
lfs: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Show build source
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
echo "Building ref: $GITHUB_REF"
|
||||
echo "Building sha: $GITHUB_SHA"
|
||||
git --no-pager log -1 --oneline
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
|
||||
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
|
||||
node-version: 22
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
- name: Publish Nightly Extension
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
|
||||
- name: Tag published commit
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
SAFE_REF=$(echo "$GITHUB_REF_NAME" | tr '/[:upper:]' '-[:lower:]' | tr -cd 'a-z0-9._-')
|
||||
SHORT_SHA=$(git rev-parse --short=12 HEAD)
|
||||
TIMESTAMP=$(date -u +"%Y%m%d%H%M%S")
|
||||
TAG="nightly-${SAFE_REF}-${TIMESTAMP}-${SHORT_SHA}"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "$TAG" -m "Cline Nightly published from ${GITHUB_REF_NAME} at ${GITHUB_SHA}"
|
||||
# Use an explicit HTTPS remote with GH_TOKEN because checkout was run with
|
||||
# persist-credentials: false, so actions/checkout did not persist a git credential helper.
|
||||
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${TAG}"
|
||||
|
||||
echo "Tagged published commit: $TAG"
|
||||
@@ -1,357 +0,0 @@
|
||||
name: ext-vscode-test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_call:
|
||||
|
||||
# Set default permissions for all jobs
|
||||
permissions:
|
||||
contents: read # Needed to check out code
|
||||
pull-requests: read # Needed for changed-file detection on pull requests
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
name: Detect Changes
|
||||
outputs:
|
||||
vscode: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.vscode == 'true' }}
|
||||
testing_platform: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.testing_platform == 'true' }}
|
||||
steps:
|
||||
- id: force
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
|
||||
run: echo "run_all=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
if: steps.force.outputs.run_all != 'true'
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
if: steps.force.outputs.run_all != 'true'
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
vscode:
|
||||
- 'apps/vscode/src/**'
|
||||
- 'apps/vscode/webview-ui/**'
|
||||
- 'apps/vscode/proto/**'
|
||||
- 'apps/vscode/tests/**'
|
||||
- 'apps/vscode/scripts/**'
|
||||
- 'apps/vscode/standalone/**'
|
||||
- 'apps/vscode/assets/**'
|
||||
- 'apps/vscode/walkthrough/**'
|
||||
- 'apps/vscode/package.json'
|
||||
- 'apps/vscode/package-lock.json'
|
||||
- 'apps/vscode/buf.yaml'
|
||||
- 'apps/vscode/tsconfig*.json'
|
||||
- 'apps/vscode/biome.jsonc'
|
||||
- 'apps/vscode/esbuild.mjs'
|
||||
- 'apps/vscode/.mocharc.json'
|
||||
- 'apps/vscode/.nycrc*.json'
|
||||
- 'apps/vscode/.vscode-test.mjs'
|
||||
- 'apps/vscode/test-setup.js'
|
||||
- '.github/workflows/ext-vscode-test.yml'
|
||||
testing_platform:
|
||||
- 'apps/vscode/src/**'
|
||||
- 'apps/vscode/proto/**'
|
||||
- 'apps/vscode/standalone/**'
|
||||
- 'apps/vscode/testing-platform/**'
|
||||
- 'apps/vscode/tests/specs/**'
|
||||
- 'apps/vscode/package.json'
|
||||
- 'apps/vscode/package-lock.json'
|
||||
- 'apps/vscode/buf.yaml'
|
||||
- 'apps/vscode/tsconfig*.json'
|
||||
- 'apps/vscode/esbuild.mjs'
|
||||
- 'apps/vscode/.vscodeignore'
|
||||
- 'apps/vscode/scripts/**'
|
||||
- '.github/workflows/ext-vscode-test.yml'
|
||||
|
||||
quality-checks:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.vscode == 'true' || needs.detect-changes.outputs.testing_platform == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
name: Quality Checks
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Run Quality Checks (Parallel)
|
||||
run: npm run ci:check-all
|
||||
|
||||
vscode-test:
|
||||
needs: [detect-changes, quality-checks]
|
||||
if: needs.detect-changes.outputs.vscode == 'true'
|
||||
env:
|
||||
VSCODE_TEST_VERSION: 1.103.0
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.os == 'ubuntu-latest' && 'vscode test' || format('vscode test ({0})', matrix.os) }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: apps/vscode
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
|
||||
- name: Cache VS Code test runtime
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .vscode-test
|
||||
key: vscode-test-runtime-${{ runner.os }}-${{ env.VSCODE_TEST_VERSION }}
|
||||
|
||||
# Build the extension and tests (without redundant checks)
|
||||
- name: Build Tests and Extension
|
||||
id: build_step
|
||||
run: npm run ci:build
|
||||
|
||||
- name: Unit Tests with coverage - Linux
|
||||
id: unit_tests_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
|
||||
run: |
|
||||
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
|
||||
|
||||
- name: Unit Tests - Non-Linux
|
||||
id: unit_tests_non_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
|
||||
run: |
|
||||
npm run test:unit
|
||||
|
||||
- name: Extension Integration Tests - Linux
|
||||
id: integration_tests_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
|
||||
run: xvfb-run -a npm run test:coverage
|
||||
|
||||
- name: Extension Integration Tests - Non-Linux
|
||||
id: integration_tests_non_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
echo "Running extension integration tests (attempt ${attempt}/3)"
|
||||
if npm run test:integration; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "Extension integration tests failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Extension integration tests failed; retrying after short delay"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
- name: Webview Tests with Coverage
|
||||
id: webview_tests
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
|
||||
run: |
|
||||
cd webview-ui
|
||||
npm run test:coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
# Only upload artifacts on Linux - We only need coverage from one OS
|
||||
if: runner.os == 'Linux'
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: |
|
||||
apps/vscode/coverage-unit/lcov.info
|
||||
apps/vscode/webview-ui/coverage/lcov.info
|
||||
|
||||
test-platform-integration:
|
||||
needs: [detect-changes, quality-checks]
|
||||
if: needs.detect-changes.outputs.testing_platform == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
apps/vscode/testing-platform/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
run: npm run download-ripgrep
|
||||
|
||||
- name: Compile Standalone
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
run: cd testing-platform && npm ci
|
||||
|
||||
- name: Running testing platform integration spec tests
|
||||
timeout-minutes: 7
|
||||
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-platform-integration-core-coverage
|
||||
path: apps/vscode/coverage/**/lcov.info
|
||||
|
||||
# Keep the required "test" check as a tiny aggregate gate instead of the conditional
|
||||
# VS Code matrix. GitHub treats conditionally skipped jobs as successful required
|
||||
# checks, so the gate below preserves the old required check name while making sure
|
||||
# whichever filtered test jobs were selected actually passed.
|
||||
test:
|
||||
needs: [detect-changes, quality-checks, vscode-test, test-platform-integration]
|
||||
if: ${{ !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
name: test
|
||||
steps:
|
||||
- name: Verify selected test jobs
|
||||
env:
|
||||
DETECT_CHANGES_RESULT: ${{ needs.detect-changes.result }}
|
||||
QUALITY_CHECKS_RESULT: ${{ needs.quality-checks.result }}
|
||||
VSCODE_CHANGED: ${{ needs.detect-changes.outputs.vscode }}
|
||||
TESTING_PLATFORM_CHANGED: ${{ needs.detect-changes.outputs.testing_platform }}
|
||||
VSCODE_TEST_RESULT: ${{ needs.vscode-test.result }}
|
||||
TEST_PLATFORM_RESULT: ${{ needs.test-platform-integration.result }}
|
||||
run: |
|
||||
if [ "$DETECT_CHANGES_RESULT" != "success" ]; then
|
||||
echo "detect-changes did not succeed: $DETECT_CHANGES_RESULT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$VSCODE_CHANGED" != "true" ] && [ "$TESTING_PLATFORM_CHANGED" != "true" ]; then
|
||||
echo "No root test paths changed; skipping root test requirements."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$QUALITY_CHECKS_RESULT" != "success" ]; then
|
||||
echo "quality-checks did not succeed: $QUALITY_CHECKS_RESULT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$VSCODE_CHANGED" = "true" ] && [ "$VSCODE_TEST_RESULT" != "success" ]; then
|
||||
echo "vscode-test did not succeed: $VSCODE_TEST_RESULT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$TESTING_PLATFORM_CHANGED" = "true" ] && [ "$TEST_PLATFORM_RESULT" != "success" ]; then
|
||||
echo "test-platform-integration did not succeed: $TEST_PLATFORM_RESULT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Selected root test jobs passed."
|
||||
|
||||
qlty:
|
||||
needs: [detect-changes, quality-checks, vscode-test, test-platform-integration]
|
||||
if: ${{ !cancelled() && needs.quality-checks.result == 'success' && (needs.vscode-test.result == 'success' || needs.vscode-test.result == 'skipped') && (needs.test-platform-integration.result == 'success' || needs.test-platform-integration.result == 'skipped') && (needs.detect-changes.outputs.vscode == 'true' || needs.detect-changes.outputs.testing_platform == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
# Run on PRs to main, pushes to main, and manual dispatches
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download unit tests coverage reports
|
||||
if: needs.detect-changes.outputs.vscode == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: apps/vscode
|
||||
|
||||
- name: Upload core unit tests coverage to Qlty
|
||||
if: needs.detect-changes.outputs.vscode == 'true'
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
# we can merge multiple files if necessary
|
||||
files: |
|
||||
apps/vscode/coverage-unit/lcov.info
|
||||
tag: unit:core
|
||||
|
||||
- name: Upload webview-ui unit tests coverage to Qlty
|
||||
if: needs.detect-changes.outputs.vscode == 'true'
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
# we can merge multiple files if necessary
|
||||
files: |
|
||||
apps/vscode/webview-ui/coverage/lcov.info
|
||||
tag: unit:webview-ui
|
||||
add-prefix: webview-ui/
|
||||
|
||||
- name: Download test platform integration core coverage artifact
|
||||
if: needs.detect-changes.outputs.testing_platform == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
id: download-integration-coverage
|
||||
with:
|
||||
name: test-platform-integration-core-coverage
|
||||
path: apps/vscode/integration-core-coverage-reports
|
||||
|
||||
- name: Upload core integration tests coverage to Qlty
|
||||
if: needs.detect-changes.outputs.testing_platform == 'true' && steps.download-integration-coverage.outcome == 'success'
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
files: apps/vscode/integration-core-coverage-reports/**/lcov.info
|
||||
tag: integration:core
|
||||
+2
-14
@@ -1,4 +1,4 @@
|
||||
name: repo-label-issues
|
||||
name: Auto-label Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
script: |
|
||||
const body = context.payload.issue.body || '';
|
||||
const labels = context.payload.issue.labels.map(l => l.name);
|
||||
|
||||
|
||||
// Check if JetBrains Plugin is selected
|
||||
if (body.match(/###\s*Plugin Type\s*\n+JetBrains Plugin/i)) {
|
||||
if (!labels.includes('JetBrains')) {
|
||||
@@ -51,15 +51,3 @@ jobs:
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if beta version checkbox is checked
|
||||
if (body.includes('- [X] I am using a beta version of Cline') || body.includes('- [x] I am using a beta version of Cline')) {
|
||||
if (!labels.includes('beta')) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['beta']
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Publish NPM Release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
confirm_publish:
|
||||
description: 'Type "publish" to confirm you want to publish to NPM'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write # Required for pushing tags
|
||||
id-token: write # Required for npm trusted publishing (OIDC)
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish-npm-release:
|
||||
needs: test
|
||||
name: Publish Cline CLI to NPM
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && inputs.confirm_publish == 'publish'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install root dependencies and CLI dependencies
|
||||
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
|
||||
|
||||
- name: Generate Protos
|
||||
run: npm run protos
|
||||
|
||||
- name: Read release version
|
||||
id: version
|
||||
run: |
|
||||
# Read version from cli/package.json
|
||||
VERSION=$(node -p "require('./cli/package.json').version")
|
||||
echo "Release version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build standalone NPM package
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
OTEL_TELEMETRY_ENABLED: "1"
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: node scripts/package-npm.mjs
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Verifying CLI binaries..."
|
||||
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with latest tag
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
|
||||
cd dist-standalone
|
||||
npm publish --tag latest --access public
|
||||
|
||||
- name: Tag release
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag "v${{ steps.version.outputs.version }}-cli"
|
||||
git push origin "v${{ steps.version.outputs.version }}-cli"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Post release to Slack
|
||||
uses: slackapi/slack-github-action@v3.0.1
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: "C0APVKGGZFC"
|
||||
text: "Cline CLI v${{ steps.version.outputs.version }}"
|
||||
blocks:
|
||||
- type: "section"
|
||||
text:
|
||||
type: "mrkdwn"
|
||||
text: "*Cline CLI v${{ steps.version.outputs.version }}*"
|
||||
- type: "context"
|
||||
elements:
|
||||
- type: "mrkdwn"
|
||||
text: "<https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}|View on npm>"
|
||||
@@ -0,0 +1,134 @@
|
||||
name: Publish NPM Nightly
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
force_publish:
|
||||
description: "Force publish even if there are no commits in the last 24 hours"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for npm trusted publishing (OIDC)
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish-npm-nightly:
|
||||
needs: test
|
||||
name: Publish Cline CLI (Nightly) to NPM
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for recent commits
|
||||
id: check_commits
|
||||
run: |
|
||||
if [ "${{ inputs.force_publish }}" = "true" ]; then
|
||||
echo "force_publish enabled, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Found recent commits, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install root dependencies and CLI dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
|
||||
|
||||
- name: Generate Protos
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run protos
|
||||
|
||||
- name: Generate nightly version with timestamp
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
# Read base version from cli/package.json (e.g., "2.0.0")
|
||||
BASE_VERSION=$(node -p "require('./cli/package.json').version")
|
||||
|
||||
# Generate timestamp (Unix epoch seconds)
|
||||
TIMESTAMP=$(date +%s)
|
||||
|
||||
# Create unique nightly version: 1.0.9-nightly.1736365200
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "Generated nightly version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update cli/package.json with nightly version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
# Update version with timestamp-based nightly version
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('cli/package.json', 'utf8'));
|
||||
pkg.version = '${{ steps.version.outputs.version }}';
|
||||
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
|
||||
"
|
||||
|
||||
echo "Using version ${{ steps.version.outputs.version }} for build"
|
||||
cat cli/package.json | grep '"version"'
|
||||
|
||||
- name: Build and package CLI
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
OTEL_TELEMETRY_ENABLED: "1"
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: node scripts/package-npm.mjs
|
||||
|
||||
- name: Verify build output
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Verifying CLI binaries..."
|
||||
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with nightly tag
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
|
||||
cd dist-standalone
|
||||
npm publish --tag nightly --access public
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline@nightly"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
@@ -0,0 +1,215 @@
|
||||
# Build and Pack CLI
|
||||
#
|
||||
# Builds a CLI tarball from any branch/commit and publishes it as a GitHub Release.
|
||||
# Requires write access to the repository (maintainers/collaborators only).
|
||||
#
|
||||
# Security: Split into two jobs to isolate untrusted build code from write tokens.
|
||||
# The build job runs arbitrary ref code with zero permissions. The release job
|
||||
# only runs trusted GitHub Actions with write scope.
|
||||
#
|
||||
# Usage (helper script, auto-detects current branch):
|
||||
# ./scripts/build-cli-artifact.sh
|
||||
# ./scripts/build-cli-artifact.sh feature/my-changes
|
||||
# ./scripts/build-cli-artifact.sh feature/my-changes 1234 # comments on PR
|
||||
#
|
||||
# Usage (gh CLI directly):
|
||||
# gh workflow run pack-cli.yml -f ref=main
|
||||
# gh workflow run pack-cli.yml -f ref=abc123 -f pr_number=1234
|
||||
#
|
||||
# Install the built CLI (no auth required):
|
||||
# npm install -g https://github.com/cline/cline/releases/download/cli-build-<sha>/cline-<ver>.tgz
|
||||
#
|
||||
# Find releases:
|
||||
# gh release list --limit 10
|
||||
|
||||
name: Build and Pack CLI
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Branch, tag, or commit SHA to build (leave empty for default branch)'
|
||||
required: false
|
||||
type: string
|
||||
pr_number:
|
||||
description: 'PR number to comment on with install instructions (optional)'
|
||||
required: false
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
# ── Build job: runs untrusted ref code with ZERO permissions ──
|
||||
build:
|
||||
name: Build CLI
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
outputs:
|
||||
commit_sha: ${{ steps.commit.outputs.sha }}
|
||||
tarball: ${{ steps.pack.outputs.tarball }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Get commit SHA
|
||||
id: commit
|
||||
run: |
|
||||
COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "Building from commit: $COMMIT_SHA"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Generate Protos
|
||||
run: npm run protos
|
||||
|
||||
- name: Build standalone package
|
||||
run: node scripts/package-npm.mjs
|
||||
|
||||
- name: Create Tarball
|
||||
id: pack
|
||||
run: |
|
||||
cd dist-standalone
|
||||
TARBALL=$(npm pack)
|
||||
echo "tarball=$TARBALL" >> $GITHUB_OUTPUT
|
||||
echo "Created tarball: $TARBALL"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cli-tarball
|
||||
path: dist-standalone/*.tgz
|
||||
|
||||
# ── Release job: only trusted Actions code, with write permissions ──
|
||||
release:
|
||||
name: Release CLI
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cli-tarball
|
||||
path: dist-standalone
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const commit = '${{ needs.build.outputs.commit_sha }}';
|
||||
const tarball = '${{ needs.build.outputs.tarball }}';
|
||||
|
||||
// Delete existing release/tag if re-running for the same commit
|
||||
const tagName = `cli-build-${commit}`;
|
||||
try {
|
||||
const existing = await github.rest.repos.getReleaseByTag({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag: tagName
|
||||
});
|
||||
await github.rest.repos.deleteRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: existing.data.id
|
||||
});
|
||||
await github.rest.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `tags/${tagName}`
|
||||
});
|
||||
core.info(`Deleted existing release for ${tagName}`);
|
||||
} catch (e) {
|
||||
// Release doesn't exist yet, that's fine
|
||||
}
|
||||
|
||||
// Create a release
|
||||
const release = await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: tagName,
|
||||
name: `CLI Build (${commit})`,
|
||||
body: `Automated CLI build from commit ${commit}\n\nInstall with:\n\`\`\`bash\nnpm install -g https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}\n\`\`\``,
|
||||
draft: false,
|
||||
prerelease: true
|
||||
});
|
||||
|
||||
// Upload the tarball as a release asset
|
||||
const tarballPath = path.join('dist-standalone', tarball);
|
||||
const tarballData = fs.readFileSync(tarballPath);
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: release.data.id,
|
||||
name: tarball,
|
||||
data: tarballData
|
||||
});
|
||||
|
||||
const downloadUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}`;
|
||||
core.setOutput('release_url', release.data.html_url);
|
||||
core.setOutput('download_url', downloadUrl);
|
||||
|
||||
- name: Comment on PR with download instructions
|
||||
if: inputs.pr_number != ''
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const commit = '${{ needs.build.outputs.commit_sha }}';
|
||||
const releaseUrl = '${{ steps.create_release.outputs.release_url }}';
|
||||
const downloadUrl = '${{ steps.create_release.outputs.download_url }}';
|
||||
const prNumber = ${{ inputs.pr_number || 0 }};
|
||||
if (!prNumber) return;
|
||||
|
||||
const comment = `## 📦 CLI Build Ready
|
||||
|
||||
A CLI build has been created for commit \`${commit}\`.
|
||||
|
||||
### Install Directly from URL (No Authentication Required!)
|
||||
|
||||
\`\`\`bash
|
||||
npm install -g ${downloadUrl}
|
||||
\`\`\`
|
||||
|
||||
### Alternative: Download and Install
|
||||
|
||||
\`\`\`bash
|
||||
curl -L ${downloadUrl} -o cline.tgz
|
||||
npm install -g ./cline.tgz
|
||||
\`\`\`
|
||||
|
||||
📦 [View Release](${releaseUrl})
|
||||
`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: prNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: comment
|
||||
});
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "✅ CLI build complete!"
|
||||
echo ""
|
||||
echo "📦 Release: ${{ steps.create_release.outputs.release_url }}"
|
||||
echo "🔗 Download URL: ${{ steps.create_release.outputs.download_url }}"
|
||||
echo ""
|
||||
echo "Install from anywhere (no authentication required):"
|
||||
echo " npm install -g ${{ steps.create_release.outputs.download_url }}"
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Publish CLI (Trusted)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_target:
|
||||
description: "Which publish flow to run"
|
||||
required: true
|
||||
default: "main"
|
||||
type: choice
|
||||
options:
|
||||
- main
|
||||
- nightly
|
||||
confirm_publish:
|
||||
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
|
||||
required: false
|
||||
type: string
|
||||
force_nightly_publish:
|
||||
description: "Force nightly publish even with no commits in last 24h"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for npm trusted publishing (OIDC)
|
||||
contents: write # Required because npm-main creates/pushes git tags
|
||||
checks: write # Required by nested reusable test workflow
|
||||
pull-requests: write # Required by nested reusable test workflow
|
||||
|
||||
jobs:
|
||||
cli-tui-tests:
|
||||
uses: ./.github/workflows/cli-tui-tests.yml
|
||||
|
||||
publish-main:
|
||||
needs: cli-tui-tests
|
||||
if: |
|
||||
github.repository == 'cline/cline' && (
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.event.inputs.publish_target == 'main' &&
|
||||
github.event.inputs.confirm_publish == 'publish' &&
|
||||
!endsWith(github.actor, '[bot]')
|
||||
)
|
||||
uses: ./.github/workflows/npm-main.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
confirm_publish: ${{ github.event.inputs.confirm_publish }}
|
||||
|
||||
publish-nightly:
|
||||
needs: cli-tui-tests
|
||||
if: |
|
||||
github.repository == 'cline/cline' && (
|
||||
github.event_name == 'schedule' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish_target == 'nightly')
|
||||
)
|
||||
uses: ./.github/workflows/npm-nightly.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
force_publish: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
|
||||
@@ -0,0 +1,77 @@
|
||||
name: "Publish Nightly Release"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish:
|
||||
needs: test
|
||||
name: Publish Cline (Nightly) Extension
|
||||
if: github.repository == 'cline/cline'
|
||||
runs-on: ubuntu-latest
|
||||
environment: PublishNightly
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: Check for recent commits
|
||||
run: |
|
||||
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, exiting"
|
||||
exit 0
|
||||
fi
|
||||
echo "Found recent commits, proceeding with build"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
|
||||
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
|
||||
node-version: 22
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
- name: Verify LFS media assets are resolved
|
||||
run: |
|
||||
for FILE in webview-ui/src/assets/cline_kanban_demo.mp4 webview-ui/src/assets/cline_kanban_demo.webm; do
|
||||
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
|
||||
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Publish Extension as Pre-release
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
@@ -1,4 +1,4 @@
|
||||
name: ext-vscode-publish-stable
|
||||
name: "Publish Release"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -29,16 +29,13 @@ permissions:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/ext-vscode-test.yml
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish:
|
||||
needs: test
|
||||
name: Publish Extension
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -50,11 +47,9 @@ jobs:
|
||||
|
||||
- name: Resolve Release Tag
|
||||
id: resolve_tag
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
TAG: ${{ github.event.inputs.tag }}
|
||||
AUTO_CREATE: ${{ github.event.inputs.auto_create_tag_from_main }}
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
AUTO_CREATE="${{ github.event.inputs.auto_create_tag_from_main }}"
|
||||
TESTED_SHA="${{ github.sha }}"
|
||||
WORKFLOW_REF="${{ github.ref }}"
|
||||
|
||||
@@ -139,6 +134,15 @@ jobs:
|
||||
fi
|
||||
echo "Tag and package version match: $TAG"
|
||||
|
||||
- name: Verify LFS media assets are resolved
|
||||
run: |
|
||||
for FILE in webview-ui/src/assets/cline_kanban_demo.mp4 webview-ui/src/assets/cline_kanban_demo.webm; do
|
||||
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
|
||||
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Package and Publish Extension
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
@@ -153,19 +157,11 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
RELEASE_TYPE: ${{ github.event.inputs.release-type }}
|
||||
run: |
|
||||
# Swap README.marketplace.md into README.md so both the GitHub
|
||||
# release artifact (vsce package below) and the marketplace
|
||||
# publish (npm run publish:marketplace below, which swaps
|
||||
# internally as an idempotent no-op) ship the same README.
|
||||
node scripts/marketplace-readme.mjs swap-in
|
||||
trap 'node scripts/marketplace-readme.mjs restore' EXIT
|
||||
|
||||
# Required to generate the .vsix
|
||||
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
if [ "$RELEASE_TYPE" = "pre-release" ]; then
|
||||
if [ "${{ github.event.inputs.release-type }}" = "pre-release" ]; then
|
||||
npm run publish:marketplace:prerelease
|
||||
echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
|
||||
else
|
||||
@@ -175,7 +171,6 @@ jobs:
|
||||
|
||||
- name: Get Previous Tag
|
||||
id: prev_tag
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
@@ -183,7 +178,6 @@ jobs:
|
||||
|
||||
- name: Get Changelog Entry
|
||||
id: changelog
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
# Get content between first ## [ and second ## [
|
||||
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
|
||||
@@ -195,7 +189,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.resolve_tag.outputs.tag }}
|
||||
files: "apps/vscode/*.vsix"
|
||||
files: "*.vsix"
|
||||
body: |
|
||||
${{ steps.changelog.outputs.content }}
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
name: sdk-publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: "Publish channel"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- nightly
|
||||
- latest
|
||||
default: nightly
|
||||
force_publish:
|
||||
description: "Force publish even if there are no commits in the last 24 hours"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
confirm_publish:
|
||||
description: 'Required when channel=latest. Type "publish" to confirm release publish.'
|
||||
required: false
|
||||
type: string
|
||||
schedule:
|
||||
# Run nightly at 2:00 AM UTC
|
||||
- cron: "0 2 * * *"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
|
||||
jobs:
|
||||
test:
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/sdk-test.yml
|
||||
|
||||
publish-sdk:
|
||||
needs: test
|
||||
name: Publish SDK Packages
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
if: |
|
||||
github.repository == 'cline/cline' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
(
|
||||
github.event_name != 'workflow_dispatch' ||
|
||||
inputs.channel != 'latest' ||
|
||||
(
|
||||
inputs.confirm_publish == 'publish' &&
|
||||
!endsWith(github.actor, '[bot]')
|
||||
)
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine publish channel
|
||||
id: channel
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
INPUT_CHANNEL: ${{ inputs.channel }}
|
||||
run: |
|
||||
# Default to nightly for scheduled runs
|
||||
if [ "$EVENT_NAME" = "schedule" ]; then
|
||||
echo "channel=nightly" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "channel=$INPUT_CHANNEL" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check for recent commits
|
||||
id: check_commits
|
||||
env:
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
FORCE_PUBLISH: ${{ inputs.force_publish }}
|
||||
run: |
|
||||
# Always publish for latest (production) releases
|
||||
if [ "$CHANNEL" = "latest" ]; then
|
||||
echo "Production release requested, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$FORCE_PUBLISH" = "true" ]; then
|
||||
echo "force_publish enabled, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(git rev-list --count HEAD --since="24 hours ago")" -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Found recent commits, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Verify trusted publishing context
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
|
||||
echo "GitHub OIDC request environment is unavailable. Ensure this job has id-token: write for npm trusted publishing."
|
||||
exit 1
|
||||
fi
|
||||
echo "GitHub OIDC request environment is available for npm trusted publishing."
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Verify publish tooling
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
NPM_VERSION=$(npm --version)
|
||||
echo "npm ${NPM_VERSION}"
|
||||
IFS=. read -r major minor patch <<EOF
|
||||
${NPM_VERSION}
|
||||
EOF
|
||||
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
|
||||
echo "npm 11.5.1 or newer is required for trusted publishing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build SDK
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Generate shared version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
env:
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
run: |
|
||||
BASE_VERSION=$(node -p "require('./packages/llms/package.json').version")
|
||||
|
||||
if [ "$CHANNEL" = "nightly" ]; then
|
||||
TIMESTAMP=$(date +%s)
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
else
|
||||
VERSION="$BASE_VERSION"
|
||||
fi
|
||||
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "Channel: $CHANNEL"
|
||||
echo "Publish version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update all package versions and lockfile
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: bun scripts/version.ts "$VERSION"
|
||||
|
||||
- name: Verify publishability
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun scripts/check-publish.ts
|
||||
|
||||
- name: Prepare package tarball directory
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: mkdir -p "$RUNNER_TEMP/sdk-npm-packs"
|
||||
|
||||
# Pack with Bun so workspace/catalog protocols are resolved in the tarball,
|
||||
# then publish that tarball with npm so npm trusted publishing can use GitHub OIDC.
|
||||
# Publish sequentially in dependency order: shared → llms → agents → core → sdk
|
||||
- name: Publish @cline/shared
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/shared@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/shared
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Publish @cline/llms
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/llms@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/llms
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Publish @cline/agents
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/agents@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/agents
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Publish @cline/core
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/core@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/core
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Publish @cline/sdk
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/sdk@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/sdk
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
- name: Create package tags for production publish
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.channel.outputs.channel == 'latest'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
for PKG in shared llms agents core sdk; do
|
||||
TAG="sdk/${PKG}/v${VERSION}"
|
||||
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||
echo "Tag already exists locally: ${TAG}"
|
||||
else
|
||||
git tag -a "${TAG}" -m "@cline/${PKG}@${VERSION}"
|
||||
echo "Created tag: ${TAG}"
|
||||
fi
|
||||
|
||||
# Ensure remote has the tag; this is idempotent if tag already exists remotely.
|
||||
git push origin "refs/tags/${TAG}"
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
run: |
|
||||
echo "Published SDK packages with tag '${CHANNEL}':"
|
||||
echo " - @cline/shared@${VERSION}"
|
||||
echo " - @cline/llms@${VERSION}"
|
||||
echo " - @cline/agents@${VERSION}"
|
||||
echo " - @cline/core@${VERSION}"
|
||||
echo " - @cline/sdk@${VERSION}"
|
||||
if [ "$CHANNEL" = "latest" ]; then
|
||||
echo "Created git tags:"
|
||||
echo " - sdk/shared/v${VERSION}"
|
||||
echo " - sdk/llms/v${VERSION}"
|
||||
echo " - sdk/agents/v${VERSION}"
|
||||
echo " - sdk/core/v${VERSION}"
|
||||
echo " - sdk/sdk/v${VERSION}"
|
||||
fi
|
||||
@@ -1,112 +0,0 @@
|
||||
name: sdk-test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "sdk/**"
|
||||
- ".github/workflows/sdk-test.yml"
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "sdk/**"
|
||||
- ".github/workflows/sdk-test.yml"
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
|
||||
jobs:
|
||||
quality-checks:
|
||||
runs-on: ubuntu-latest
|
||||
name: Quality Checks
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Typecheck
|
||||
run: |
|
||||
bun run build:sdk
|
||||
bun run -F @cline/cli build
|
||||
bun run types
|
||||
|
||||
- name: Lint & Format
|
||||
run: bun run lint
|
||||
|
||||
test:
|
||||
needs: quality-checks
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
node-version: "24.x"
|
||||
- os: windows-latest
|
||||
node-version: "24.x"
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: Test (${{ matrix.os }}, Node ${{ matrix.node-version }})
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.13"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build SDK
|
||||
id: build_sdk_step
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Build CLI
|
||||
id: build_cli_step
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' }}
|
||||
run: bun -F @cline/cli build
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os != 'windows-latest' }}
|
||||
run: bun run test
|
||||
|
||||
- name: Run SDK Tests (Windows)
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'windows-latest' }}
|
||||
run: bun -F './packages/**' test
|
||||
|
||||
- name: Smoke test SQLite under Node
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && matrix.os != 'windows-latest' }}
|
||||
timeout-minutes: 10
|
||||
run: bun scripts/ci-node-smoke.ts
|
||||
|
||||
- name: Run TUI e2e tests
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
|
||||
run: bun -F @cline/cli test:e2e:cli:tui
|
||||
|
||||
- name: Verify packages are publishable
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
|
||||
run: bun scripts/check-publish.ts
|
||||
@@ -1,6 +1,6 @@
|
||||
# This workflow will only label and/or close 30 issues at a time in order to avoid exceeding a rate limit.
|
||||
# More info: https://docs.github.com/en/actions/use-cases-and-examples/project-management/closing-inactive-issues
|
||||
name: repo-stale-issues
|
||||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Test Stale Issues Workflow
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days-before-stale:
|
||||
description: "Days before an issue becomes stale"
|
||||
required: true
|
||||
default: "1"
|
||||
days-before-close:
|
||||
description: "Days before a stale issue is closed"
|
||||
required: true
|
||||
default: "1"
|
||||
|
||||
jobs:
|
||||
test-stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@28ca103
|
||||
with:
|
||||
days-before-issue-stale: ${{ github.event.inputs.days-before-stale }}
|
||||
days-before-issue-close: ${{ github.event.inputs.days-before-close }}
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for ${{ github.event.inputs.days-before-stale }} days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for ${{ github.event.inputs.days-before-close }} days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: "pinned,security"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
debug-only: true
|
||||
@@ -0,0 +1,224 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_call:
|
||||
|
||||
# Set default permissions for all jobs
|
||||
permissions:
|
||||
contents: read # Needed to check out code
|
||||
checks: write # Needed to report test results
|
||||
pull-requests: write # Needed to add comments/annotations to PRs
|
||||
|
||||
jobs:
|
||||
quality-checks:
|
||||
runs-on: ubuntu-latest
|
||||
name: Quality Checks
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Run Quality Checks (Parallel)
|
||||
run: npm run ci:check-all
|
||||
|
||||
test:
|
||||
needs: quality-checks
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.os == 'ubuntu-latest' && 'test' || format('test ({0})', matrix.os) }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
|
||||
# Build the extension and tests (without redundant checks)
|
||||
- name: Build Tests and Extension
|
||||
id: build_step
|
||||
run: npm run ci:build
|
||||
|
||||
- name: Unit Tests with coverage - Linux
|
||||
id: unit_tests_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
|
||||
run: |
|
||||
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
|
||||
|
||||
- name: Unit Tests - Non-Linux
|
||||
id: unit_tests_non_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
|
||||
run: |
|
||||
npm run test:unit
|
||||
|
||||
- name: Extension Integration Tests - Linux
|
||||
id: integration_tests_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
|
||||
run: xvfb-run -a npm run test:coverage
|
||||
|
||||
- name: Extension Integration Tests - Non-Linux
|
||||
id: integration_tests_non_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
|
||||
run: npm run test:integration
|
||||
|
||||
- name: Webview Tests with Coverage
|
||||
id: webview_tests
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
|
||||
run: |
|
||||
cd webview-ui
|
||||
npm run test:coverage
|
||||
|
||||
- name: CLI Tests
|
||||
id: cli_tests
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
|
||||
run: cd cli && npm run test:run
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
# Only upload artifacts on Linux - We only need coverage from one OS
|
||||
if: runner.os == 'Linux'
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: |
|
||||
coverage-unit/lcov.info
|
||||
webview-ui/coverage/lcov.info
|
||||
|
||||
test-platform-integration:
|
||||
needs: quality-checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
testing-platform/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
run: npm run download-ripgrep
|
||||
|
||||
- name: Compile Standalone
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
run: cd testing-platform && npm ci
|
||||
|
||||
- name: Running testing platform integration spec tests
|
||||
timeout-minutes: 7
|
||||
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-platform-integration-core-coverage
|
||||
path: coverage/**/lcov.info
|
||||
|
||||
qlty:
|
||||
needs: [test, test-platform-integration]
|
||||
runs-on: ubuntu-latest
|
||||
# Run on PRs to main, pushes to main, and manual dispatches
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download unit tests coverage reports
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: .
|
||||
|
||||
- name: Upload core unit tests coverage to Qlty
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
# we can merge multiple files if necessary
|
||||
files: |
|
||||
coverage-unit/lcov.info
|
||||
tag: unit:core
|
||||
|
||||
- name: Upload webview-ui unit tests coverage to Qlty
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
# we can merge multiple files if necessary
|
||||
files: |
|
||||
webview-ui/coverage/lcov.info
|
||||
tag: unit:webview-ui
|
||||
add-prefix: webview-ui/
|
||||
|
||||
- name: Download test platform integration core coverage artifact
|
||||
uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
id: download-integration-coverage
|
||||
with:
|
||||
name: test-platform-integration-core-coverage
|
||||
path: integration-core-coverage-reports
|
||||
|
||||
- name: Upload core integration tests coverage to Qlty
|
||||
if: steps.download-integration-coverage.outcome == 'success'
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
files: integration-core-coverage-reports/**/lcov.info
|
||||
tag: integration:core
|
||||
+5
-7
@@ -1,4 +1,4 @@
|
||||
name: ext-jb-test-integration
|
||||
name: Trigger Jetbrains Plugin <-> Cline Tests
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
@@ -15,11 +15,9 @@ jobs:
|
||||
trigger-integration-test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
# Auto-run only for trusted PR authors. Anyone else needs a maintainer
|
||||
# to opt their PR in by commenting /test-jetbrains.
|
||||
# Run on PR open/reopen, or when someone comments /test-jetbrains on a PR
|
||||
if: |
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.pull_request.author_association)) ||
|
||||
github.event_name == 'pull_request_target' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/test-jetbrains') &&
|
||||
@@ -29,8 +27,8 @@ jobs:
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ vars.CLINE_JETBRAINS_APP_ID }}
|
||||
private-key: ${{ secrets.CLINE_JETBRAINS_APP_KEY }}
|
||||
app-id: 1998650
|
||||
private-key: ${{ secrets.CLINE_JETBRAINS_WORKFLOW_KEY }}
|
||||
owner: cline
|
||||
repositories: intellij-plugin
|
||||
|
||||
+5
-19
@@ -17,8 +17,8 @@ pnpm-lock.yaml
|
||||
.actrc
|
||||
CLAUDE.local.md
|
||||
|
||||
apps/vscode/webview-ui/src/**/*.js
|
||||
apps/vscode/webview-ui/src/**/*.js.map
|
||||
webview-ui/src/**/*.js
|
||||
webview-ui/src/**/*.js.map
|
||||
|
||||
# Ignore coverage directories and files
|
||||
coverage
|
||||
@@ -35,9 +35,9 @@ coverage-unit
|
||||
.worktrees
|
||||
|
||||
## Generated files ##
|
||||
apps/vscode/src/generated/
|
||||
apps/vscode/src/shared/proto/
|
||||
apps/vscode/webview-ui/src/services/grpc-client.ts
|
||||
src/generated/
|
||||
src/shared/proto/
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
*.tsbuildinfo
|
||||
|
||||
# E2E Tests
|
||||
@@ -56,17 +56,3 @@ evals/smoke-tests/results/
|
||||
secrets.json
|
||||
tui-traces
|
||||
tests/**/cache
|
||||
|
||||
# Backup created by scripts/marketplace-readme.mjs while publishing.
|
||||
# Should never be committed: only exists if a publish aborts mid-swap.
|
||||
.README.github.bak
|
||||
|
||||
|
||||
# SDK Session files / User data
|
||||
.cline/data
|
||||
.cline/tmp
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
.cline/**/managed.json
|
||||
.cline/**/bundle.json
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,9 @@
|
||||
"src/**/__tests__/*.ts",
|
||||
"src/test/services/**/*.test.ts"
|
||||
],
|
||||
"ignore": [
|
||||
"src/sdk/**"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register",
|
||||
"source-map-support/register",
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
bun 1.3.13
|
||||
node 22
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineConfig } from "@vscode/test-cli"
|
||||
import path from "path"
|
||||
const vscodeTestVersion = process.env.VSCODE_TEST_VERSION ?? "stable"
|
||||
|
||||
export default defineConfig({
|
||||
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
|
||||
@@ -13,7 +12,7 @@ export default defineConfig({
|
||||
require: ["./test-setup.js"],
|
||||
},
|
||||
workspaceFolder: "test-workspace",
|
||||
version: vscodeTestVersion,
|
||||
version: "stable",
|
||||
extensionDevelopmentPath: path.resolve("./"),
|
||||
launchArgs: ["--disable-extensions"],
|
||||
})
|
||||
Vendored
+1
-2
@@ -5,7 +5,6 @@
|
||||
"connor4312.esbuild-problem-matchers",
|
||||
"ms-vscode.extension-test-runner",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"biomejs.biome",
|
||||
"oven.bun-vscode"
|
||||
"biomejs.biome"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+31
-144
@@ -10,23 +10,23 @@
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}/apps/vscode",
|
||||
"${workspaceFolder}",
|
||||
"--disable-extensions"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js"
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "production"
|
||||
}
|
||||
},
|
||||
@@ -35,22 +35,22 @@
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}/apps/vscode"
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js"
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "staging"
|
||||
}
|
||||
},
|
||||
@@ -59,22 +59,22 @@
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}/apps/vscode"
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js"
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
}
|
||||
},
|
||||
@@ -84,27 +84,27 @@
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--user-data-dir=${workspaceFolder}/apps/vscode/dist/tmp/user",
|
||||
"--user-data-dir=${workspaceFolder}/dist/tmp/user",
|
||||
"--profile-temp",
|
||||
"--sync=off",
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
|
||||
"${workspaceFolder}/apps/vscode"
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js"
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "clean-tmp-user",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"postDebugTask": "stop",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"TEMP_PROFILE": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "production"
|
||||
}
|
||||
},
|
||||
@@ -117,13 +117,13 @@
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/apps/vscode/**",
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js",
|
||||
"${workspaceFolder}/apps/vscode/dist-standalone/**/*.js"
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
"${workspaceFolder}/dist-standalone/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "compile-standalone",
|
||||
"runtimeExecutable": "npx",
|
||||
@@ -131,11 +131,11 @@
|
||||
"tsx"
|
||||
],
|
||||
"program": "scripts/test-standalone-core-api-server.ts",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"PROTOBUS_PORT": "26040",
|
||||
"HOSTBRIDGE_PORT": "26041",
|
||||
"WORKSPACE_DIR": "${workspaceFolder}/apps/vscode",
|
||||
"WORKSPACE_DIR": "${workspaceFolder}",
|
||||
"E2E_TEST": "true",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
},
|
||||
@@ -151,10 +151,10 @@
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/apps/vscode/**",
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": [
|
||||
"mocha"
|
||||
@@ -169,7 +169,7 @@
|
||||
"--exit",
|
||||
"${file}"
|
||||
],
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
|
||||
"NODE_ENV": "test",
|
||||
@@ -188,7 +188,7 @@
|
||||
"run",
|
||||
"storybook"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/apps/vscode/webview-ui",
|
||||
"cwd": "${workspaceFolder}/webview-ui",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"serverReadyAction": {
|
||||
@@ -199,119 +199,6 @@
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch Bun CLI (Prompt)",
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/sdk/apps/cli",
|
||||
"runtime": "bun",
|
||||
"runtimeArgs": [
|
||||
"--conditions=development"
|
||||
],
|
||||
"program": "${workspaceFolder}/sdk/apps/cli/src/index.ts",
|
||||
"args": [
|
||||
"${input:cliPrompt}"
|
||||
],
|
||||
"env": {
|
||||
"CLINE_BUILD_ENV": "development"
|
||||
},
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch RPC Server",
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/sdk/apps/cli",
|
||||
"runtime": "bun",
|
||||
"runtimeArgs": [
|
||||
"--conditions=development"
|
||||
],
|
||||
"program": "${workspaceFolder}/sdk/apps/cli/src/index.ts",
|
||||
"args": [
|
||||
"rpc",
|
||||
"start"
|
||||
],
|
||||
"env": {
|
||||
"CLINE_BUILD_ENV": "development",
|
||||
"CLINE_DEBUG_PORT_BASE": "9230"
|
||||
},
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Attach RPC Runtime (9230)",
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"url": "ws://127.0.0.1:9230",
|
||||
"localRoot": "${workspaceFolder}/sdk",
|
||||
"remoteRoot": "${workspaceFolder}/sdk",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Attach Hook Worker (9231)",
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"url": "ws://127.0.0.1:9231",
|
||||
"localRoot": "${workspaceFolder}/sdk",
|
||||
"remoteRoot": "${workspaceFolder}/sdk",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Attach Plugin Sandbox (9232)",
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"url": "ws://127.0.0.1:9232",
|
||||
"localRoot": "${workspaceFolder}/sdk",
|
||||
"remoteRoot": "${workspaceFolder}/sdk",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Attach Connector Child (9233)",
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"url": "ws://127.0.0.1:9233",
|
||||
"localRoot": "${workspaceFolder}/sdk",
|
||||
"remoteRoot": "${workspaceFolder}/sdk",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Launch RPC Server Debugger",
|
||||
"configurations": [
|
||||
"Launch RPC Server",
|
||||
"Attach RPC Runtime (9230)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch CLI Debugger",
|
||||
"configurations": [
|
||||
"Launch Bun CLI (Prompt)",
|
||||
"Attach RPC Runtime (9230)",
|
||||
"Attach Hook Worker (9231)",
|
||||
"Attach Plugin Sandbox (9232)",
|
||||
"Attach Connector Child (9233)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "cliPrompt",
|
||||
"type": "promptString",
|
||||
"description": "Prompt to send to the CLI",
|
||||
"default": "hey"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+1
-2
@@ -1,6 +1,5 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.insertFinalNewline": true,
|
||||
"files.exclude": {
|
||||
"out": false, // set this to true to hide the "out" folder with the compiled JS files
|
||||
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
|
||||
@@ -17,7 +16,7 @@
|
||||
// Protobuf settings
|
||||
"protoc": {
|
||||
"options": [
|
||||
"--proto_path=apps/vscode/proto"
|
||||
"--proto_path=proto"
|
||||
]
|
||||
},
|
||||
// Enable Lint and format using Biome
|
||||
|
||||
Vendored
+2
-29
@@ -11,9 +11,6 @@
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -26,7 +23,6 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
@@ -78,7 +74,6 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
@@ -99,7 +94,6 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true"
|
||||
@@ -137,7 +131,6 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
@@ -176,7 +169,6 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
@@ -215,7 +207,6 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true"
|
||||
@@ -235,9 +226,6 @@
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -252,10 +240,7 @@
|
||||
"reveal": "always",
|
||||
"group": "watchers"
|
||||
},
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode"
|
||||
}
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "tasks: watch-tests",
|
||||
@@ -277,7 +262,7 @@
|
||||
"dependsOn": [
|
||||
"watch"
|
||||
],
|
||||
"command": "rm -rf ${workspaceFolder}/apps/vscode/dist/tmp/user && mkdir -p ${workspaceFolder}/apps/vscode/dist/tmp/user"
|
||||
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
@@ -294,22 +279,10 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build-sdk",
|
||||
"type": "shell",
|
||||
"command": "bun run build:sdk",
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/sdk"
|
||||
}
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
.worktrees/**
|
||||
# Agent tooling, never shipped in the VSIX
|
||||
.agents/**
|
||||
.claude/**
|
||||
.codex/**
|
||||
CLAUDE.local.md
|
||||
out/
|
||||
dist-standalone/
|
||||
@@ -26,14 +22,8 @@ eslint-rules/**
|
||||
.husky/**
|
||||
.env
|
||||
|
||||
# sdk (separate monorepo with its own build/release pipeline)
|
||||
sdk/**
|
||||
|
||||
# Source-of-truth for the marketplace README (the .vsix only ever sees the
|
||||
# README.md that scripts/marketplace-readme.mjs swaps into place). The backup
|
||||
# only exists if a publish aborts mid-swap; neither should ship in the .vsix.
|
||||
README.marketplace.md
|
||||
.README.github.bak
|
||||
# cli
|
||||
cli/**
|
||||
|
||||
# Custom
|
||||
**/demo.gif
|
||||
+940
@@ -0,0 +1,940 @@
|
||||
# SDK Migration — Architecture & Design
|
||||
|
||||
Evergreen reference for the Cline SDK migration project. For the
|
||||
living implementation plan, see `migration.md`.
|
||||
|
||||
## References
|
||||
|
||||
### Code References
|
||||
|
||||
Cline SDK is at ~/clients/cline/sdk-wip
|
||||
Cline (core, classic VSCode extension, CLI) is at ~/clients/cline/cline
|
||||
JetBrains Plugin is at ~/clients/cline/intellij-plugin
|
||||
IntelliJ open source reference is at ~/clients/cline/intellij-community
|
||||
JCEF (Java-Chromium embedded framework reference) is at ~/clients/cline/jcef
|
||||
|
||||
You can use kb_search with these identifiers to understand the
|
||||
existing code and the SDK code:
|
||||
|
||||
cline - Cline core, classic VSCode extension, CLI
|
||||
sdk - Cline SDK
|
||||
plugin - JetBrains plugin
|
||||
vscode - Visual Studio Code opens source
|
||||
ij - IntelliJ open source
|
||||
jcef - JCEF IntelliJ's embedded Chromium layer
|
||||
|
||||
Prototype VSCode extension on SDK is at ~/clients/cline/sdk-vscode-sample
|
||||
Prototype JetBrains plugin on SDK is at ~/clients/cline/sdk-intellij-plugin-sample
|
||||
|
||||
These are prototypes with features missing and added, so refer to them
|
||||
as examples, but don't overindex on them.
|
||||
|
||||
### Documentation references
|
||||
|
||||
See the ~/clients/cline/cline/docs for extension product documentation
|
||||
and ~/clients/cline/sdk-wip/*.md for SDK documentation.
|
||||
|
||||
### Background on the products & architecture
|
||||
|
||||
There's a VSCode extension in cline/src. A large part of its UI is a
|
||||
React-based webview in cline/webview-ui.
|
||||
|
||||
There's a JetBrains plugin in the intellij-plugin repo. It packages
|
||||
the core of the VSCode extension, including the webview, and
|
||||
communicates with it with protobufs. There's a bunch of stuff in the
|
||||
cline repo called "standalone" which is what JetBrains communicates
|
||||
with.
|
||||
|
||||
In cline/cli there's a CLI and Kanban tool. Those use the SDK/are
|
||||
being ported separately to the SDK, so you don't need to worry about
|
||||
them. It is OK if you have to break them. Just ignore them.
|
||||
|
||||
Note, there was an earlier, failed attempt at a cli in go. The go cli
|
||||
used to use "standalone" like JetBrains. If there's any old go support
|
||||
cluttering up the repo, it is fine to delete it and clean it up.
|
||||
|
||||
The source code and docs mentioned above are the best reference to the
|
||||
product architecture, behavior, etc. Feel free to ask clarifying
|
||||
questions when necessary.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Features to remove
|
||||
|
||||
Terminal integration: There is legacy code in the VSCode extension,
|
||||
and stubs in the RPC system for JetBrains, which used the IDE's
|
||||
integrated terminals. We have decided we don't need these old modes
|
||||
and they should be removed. Instead we will rely on "background
|
||||
terminal". This literally means the node code forks and execs a shell
|
||||
and uses pipes to communicate with it.
|
||||
|
||||
Browser automation: Remove the system that uses Playwright to automate
|
||||
browsers. These use cases are now well served by third-party MCP
|
||||
tools.
|
||||
|
||||
"Shadow git" checkpointing system. This is too slow, especially on
|
||||
Windows. The way the Kanban project uses the existing git repo to
|
||||
store references, and only after each user message, is better. So we
|
||||
will drop the "shadow copy" git checkpointing system.
|
||||
|
||||
Memory bank, structured context: multi-file documentation
|
||||
(projectbrief, productContext, activeContext, systemPatterns,
|
||||
techContext, progress.)
|
||||
|
||||
Memory bank, persistence: cross-session context preservation.
|
||||
|
||||
Focus chain, task tracking: Auto-generated to-do list with real-time
|
||||
progress indicators.
|
||||
|
||||
Focus chain, integration: Editable focus chain integration with deep
|
||||
planning and reminders.
|
||||
|
||||
Deep planning exploration, output and the /deep-planning command.
|
||||
|
||||
Workflows (definition, natural language + XML tool syntax, MCP tools
|
||||
and user input prompts) ... these have been superceded by SKILLS
|
||||
moving forward.
|
||||
|
||||
Slash commands no longer necessary:
|
||||
|
||||
/deep-planning (codebase investigation + plan... we have plan/act mode)
|
||||
/reportbug (bug reporting with diagnostics)
|
||||
Custom workflows (/workflow.md for user-defined workflows... we have "skills" now.)
|
||||
|
||||
### Core features
|
||||
|
||||
These must work:
|
||||
|
||||
File operations: read, write, search, replace, list files, inspect
|
||||
code definitions (functions, classes, methods)
|
||||
|
||||
Terminal integration: "Background terminal" must work. The agent
|
||||
relies on this to run npm, git, docker, etc.
|
||||
|
||||
Multi-provider AI models: 30+ providers with seamless
|
||||
switching. There's one provider of note: VSCode has a provider which
|
||||
hooks up to Copilot using the VSCode LM Provider API. It would be good
|
||||
to support this *if possible.*
|
||||
|
||||
Auto-approve & YOLO mode
|
||||
- Granular per-tool permission controls
|
||||
- YOLO mode for maximum automation
|
||||
- Monitoring, notifications for long-running commands
|
||||
|
||||
Auto-compaction
|
||||
- Summarization automatically compresses conversations when context fills
|
||||
- Model support for Claude, Gemini, GPT-5, Grok, etc.
|
||||
|
||||
Subagents
|
||||
- Parallel execution of independent research agents
|
||||
- Isolation with separate context windows
|
||||
- Cost tracking for task usage per subagent
|
||||
|
||||
Web search and web fetch
|
||||
|
||||
Worktrees
|
||||
- git worktrees for parallel sessions
|
||||
- branch management and .worktreeinclude support
|
||||
- conflict resolution and merging
|
||||
|
||||
Workspaces
|
||||
- "multi-root" workspaces/projects with multiple root folders
|
||||
- @workspace:path scoped references
|
||||
|
||||
Jupyter Notebooks
|
||||
- Generate, explain, and improve notebook cells
|
||||
|
||||
Cline Rules
|
||||
- project-specific .cline/rules and global instructions.
|
||||
- Conditional logic: Path-based activation of rules.
|
||||
- Compatibility: Works with Cursor Rules, Windsurf rules, AGENTS.md
|
||||
|
||||
Skills
|
||||
- SKILL.md format with YAML frontmatter
|
||||
- Loading levels: Metadata, instructions and resources
|
||||
- Scope: Global and project-specific; toggleable
|
||||
|
||||
Hooks
|
||||
- Events: Task lifecycle + tool events (TaskStart, PreToolUse, etc.)
|
||||
- Runtimes: bash, powershell for Windows
|
||||
- IO: JSON
|
||||
- Context injection: Be able to modify or inject context dynamically
|
||||
|
||||
.clineignore
|
||||
- Exclusion rules, gitignore-style file/directory exclusion
|
||||
- Exceptions: ! prefix for overrides
|
||||
- Override behavior: Explicit @ mentions bypass ignore rules
|
||||
|
||||
MCP (Model Context Protocol)
|
||||
- Server management: Discovery, enable/disable, restart, config editing
|
||||
- Transport: stdio (local) and SSE (remote)
|
||||
- ...all the typical use cases for MCP: APIs, browser automation, db queries, etc.
|
||||
|
||||
### Core workflows
|
||||
|
||||
These must work:
|
||||
|
||||
Task Management
|
||||
|
||||
Task lifecycle - create and resume tasks; view task history
|
||||
Cost tracking - token using and cost monitoring per task
|
||||
|
||||
Plan & Act Mode
|
||||
Plan mode - Explore and investigate without modifying files
|
||||
Act mode - Implementation with approval gates
|
||||
Model config - Separate model configuration for plan and act mode if
|
||||
the user desires
|
||||
State persistence - Mode switching, task switching preserves history
|
||||
|
||||
File context (@-mentions)
|
||||
Context referencing - Reference files, folders, terminal output, git
|
||||
changes, URLs, commits via @
|
||||
|
||||
Slash commands
|
||||
/newtask (new task)
|
||||
/smol (compress history)
|
||||
/newrule (create rules)
|
||||
|
||||
### Model Configuration
|
||||
|
||||
We want to continue supporting our 30+ providers (Anthropic, OpenAI,
|
||||
OpenAI Codex, OpenRouter, Google Gemini, AWS Bedrock, DeepSeek,
|
||||
Cerebras, Qwen, Mistral, Groq, Fireworks, Together, xAI Grok,
|
||||
Moonshot, Nebius, HuggingFace, LiteLLM, Ollama, LM Studio, and more.)
|
||||
|
||||
THE MOST IMPORTANT REQUIREMENT HERE, after continuing to support them,
|
||||
is to USE THE CREDENTIALS, MODEL NAMES, CONFIGS, etc. WE HAVE
|
||||
SAVED. Logging people out of their providers is really annoying to
|
||||
users; regenerating API keys is painful for them.
|
||||
|
||||
VSCode LM API provider may be an interesting/unusual provider out of
|
||||
this set; it only works in VSCode by calling a specific API.
|
||||
|
||||
We must continue supporting local models like Ollama and LM Studio.
|
||||
|
||||
We must support the Cline provider with unified auth (open a
|
||||
webbrowser, handle the SSO redirect), built-in billing and credit
|
||||
display, banners advertising new or free models, stealth/early access
|
||||
models, organization switching.
|
||||
|
||||
### Enterprise Features
|
||||
|
||||
Security and governance
|
||||
- Client-side execution only (no data transmission outside of limited
|
||||
Telemetry and inference; no remote codebase indexing)
|
||||
- SSO role-based access control (member, admin, owner)
|
||||
- Model and tool controls per team
|
||||
- Remote configuration downloaded and applied by the extension
|
||||
|
||||
Observability
|
||||
- OpenTelemetry, Datadog, Grafana, Splunk integrations
|
||||
- Real-time analytics, cost breakdown by team, selective audit logging
|
||||
|
||||
Infrastructure
|
||||
- AWS Bedrock, Google Vertex AI, Azure OpenAI integration
|
||||
- Bring-your-own-inference with custom endpoints
|
||||
|
||||
### Priority "P1" (mid priority) items
|
||||
|
||||
Checkpoints - automatic file snapshots after each change. Note, the
|
||||
snapshot system in the VSCode extension and JetBrains plugin which
|
||||
copies the whole repository is slow, *especially on Windows*, so we
|
||||
should replace it with one that writes refs directly into the local
|
||||
git repo. Look at the way the kanban project does it; this is
|
||||
preferred (and maybe we should extract and share this code.)
|
||||
|
||||
Diffing - compare changes between checkpoints
|
||||
|
||||
Restore - restore files, task to a point, or both
|
||||
|
||||
MCP Marketplace - we could get rid of this, but ultimately we do want
|
||||
this feature with major improvements like allowing remote install. For
|
||||
now, consider removing it, but if it is easier to keep it around let's
|
||||
do that to lay the groundwork for improvements.
|
||||
|
||||
### Priority "P2" (lower priority) items
|
||||
|
||||
Task organization - favorites for task grouping and management
|
||||
|
||||
File context - Drag and drop files to add to context
|
||||
File context - actions - context menus to add to Cline, fix, explain, improve
|
||||
|
||||
Slash commands
|
||||
/explain-changes (git diff explanation)
|
||||
|
||||
---
|
||||
|
||||
## Detailed Design
|
||||
|
||||
### Naming: "Sdk..." considered harmful
|
||||
|
||||
Do not name types "SdkFoo" or folders "sdk". If you need to use SdkFoo
|
||||
as a way to keep two classes around in parallel while you're porting,
|
||||
that's OK, but when this project is done we want to have one clean,
|
||||
simple codebase; the SDK backing is an implementation detail so just
|
||||
use simple noun phrases for classes, etc. and don't litter "Sdk" all
|
||||
over identifiers and folders.
|
||||
|
||||
### Proto deprecation and removal
|
||||
|
||||
We don't need proto files to describe webview messages. The webview
|
||||
and extension backend are both in TypeScript and are versioned and
|
||||
shipped together. We just need to use shared TypeScript interfaces
|
||||
between them.
|
||||
|
||||
We *also* don't need proto to describe JetBrains <--> node
|
||||
exchanges. We just need something typed and in sync between Kotlin and
|
||||
TypeScript that we can serialize. JSON probably makes sense. Protos
|
||||
are OK but we have had problems with that setup creating a ton of GC
|
||||
pressure on the Kotlin side, hit maximum message size limits, etc.
|
||||
|
||||
There are proto build steps which we can remove, as we use proto less.
|
||||
|
||||
protos are useful for state which is serialized. If there are files
|
||||
that are persisted described by protos, it is ok/good to keep
|
||||
them. Don't expand the use of protos to places protos are not already
|
||||
used.
|
||||
|
||||
### Web View UI
|
||||
|
||||
The Webview UI is very dependent on state arising from implementation
|
||||
details of the pre-SDK implementation. At the same time, we don't want
|
||||
to build a new UI from scratch right now because it may be forcing too
|
||||
many changes upon our users at once. So we aim to reuse the existing
|
||||
webview, but with radical simplificiation in its state management now
|
||||
that we will have a cleaner architecture in the extension "backend"
|
||||
with the layering enforced by the SDK.
|
||||
|
||||
The webview UI had defects like showing the wrong keybindings for
|
||||
JetBrains, or using tons of memory or CPU cycles by spamming state
|
||||
updates really rapidly or sending n^2 state updates as they streamed
|
||||
in. The first principle of this migration to the SDK is not get worse,
|
||||
but at the same time, we expect the state clean-up necessary in the
|
||||
webview will lead to radical simplifications which make some
|
||||
low-hanging fruit available in performance. It's great to go make
|
||||
those improvements where they are available.
|
||||
|
||||
We don't need this UI to be pixel perfect identical. We need it to be
|
||||
FAMILIAR, NOT WORSE and preferably BETTER than the status quo.
|
||||
|
||||
### Data formats, settings
|
||||
|
||||
We MUST pick up existing on-disk state for settings, etc. We don't
|
||||
want to log users out of their inference providers as we make this
|
||||
change to the SDK.
|
||||
|
||||
The CLI, VSCode extension and JetBrains extension largely share state
|
||||
on disk. We should continue that situation. If data migrations are
|
||||
necessary, that's fine, but design them with care. In particular, we
|
||||
want the long term to be fast, so we should write breadcrumbs
|
||||
indicating when migration is done. In addition, users can upgrade and
|
||||
downgrade their extension versions, etc. and we want to be robust to
|
||||
that in addition to all kinds of failures. For example, in the past we
|
||||
had problems where we overwrote a JSON settings file, perhaps racily,
|
||||
and left trailing }s in the file and this caused the product to
|
||||
totally fail. That's a very serious issue for our users so pay extra
|
||||
effort and attention to what is happening on disk. (We want PRACTICAL
|
||||
solutions and robustness and not performative solutions that just add
|
||||
tons of code and complexity with no real benefit.)
|
||||
|
||||
Invalidating old checkpoints is acceptable, unless it is particularly
|
||||
cheap to support the classic checkpoints. We won't be authoring those
|
||||
checkpoints any more, and it would be heavy to migrate them.
|
||||
|
||||
We want to move from .clinerules (old style) to .cline/rules (new style.)
|
||||
|
||||
### Telemetry
|
||||
|
||||
We generally want to continue sending the same Telemetry events. If
|
||||
that is hard, make a detailed report and we can follow up with our
|
||||
backend team. Note some enterprise features depend on OTEL
|
||||
observability.
|
||||
|
||||
### Code Sharing
|
||||
|
||||
In general we should share code between IDEs where there are benefits
|
||||
to do so. However trivial tools, or tools specific to a given IDE, can
|
||||
be wired up directly from the extension through to the SDK. (This is
|
||||
something that was hard to do in the old architecture and we would
|
||||
like to make easier.)
|
||||
|
||||
---
|
||||
|
||||
## Research Findings
|
||||
|
||||
### SDK Session Backend Extensibility
|
||||
|
||||
**Question**: Does the SDK's `SessionBackend` interface support
|
||||
storing arbitrary per-task data (e.g., tool settings, hook
|
||||
configuration, auto-approve preferences per task)?
|
||||
|
||||
**Answer**: Partially. The `SessionRow` has a `metadata:
|
||||
Record<string, unknown> | null` field that can store arbitrary
|
||||
key-value data per session. This is sufficient for per-task settings
|
||||
like auto-approve preferences, tool configuration, etc.
|
||||
|
||||
The SDK supports three backend implementations:
|
||||
1. `SqliteSessionStore` — SQLite-backed (default, preferred)
|
||||
2. `FileSessionService` — JSON file-backed (fallback when SQLite
|
||||
unavailable)
|
||||
3. `RpcCoreSessionService` — delegates to an RPC server
|
||||
|
||||
For our migration, we'll use either `FileSessionService` or provide a
|
||||
custom `SessionPersistenceAdapter` that reads/writes our existing task
|
||||
history format. The `ClineCoreOptions.sessionService` field accepts
|
||||
any backend implementing `CoreSessionService | RpcCoreSessionService |
|
||||
FileSessionService`.
|
||||
|
||||
**Key finding**: The `SessionPersistenceAdapter` interface is the
|
||||
cleanest extension point. It requires implementing: `ensureSessionsDir`,
|
||||
`upsertSession`, `getSession`, `listSessions`, `updateSession`,
|
||||
`deleteSession`, `enqueueSpawnRequest`, `claimSpawnRequest`. Our
|
||||
`LegacySessionBackend` adapter wraps the existing
|
||||
`~/.cline/data/tasks/` directory and `taskHistory` JSON array in
|
||||
`globalState.json`, mapping between `SessionRow` fields and our
|
||||
`HistoryItem` type:
|
||||
|
||||
```
|
||||
HistoryItem.id → SessionRow.sessionId
|
||||
HistoryItem.ts → SessionRow.startedAt (ISO string)
|
||||
HistoryItem.task → SessionRow.prompt
|
||||
HistoryItem.tokensIn → metadata.tokensIn
|
||||
HistoryItem.tokensOut → metadata.tokensOut
|
||||
HistoryItem.totalCost → metadata.totalCost
|
||||
HistoryItem.modelId → SessionRow.model
|
||||
HistoryItem.isFavorited → metadata.isFavorited
|
||||
```
|
||||
|
||||
Per-task files (`api_conversation_history.json`, `ui_messages.json`)
|
||||
map to `SessionRow.messagesPath` and `SessionRow.transcriptPath`.
|
||||
|
||||
**Decision**: We will provide a custom `SessionPersistenceAdapter`
|
||||
that translates between our existing format and the SDK's interface.
|
||||
No need for a separate sidecar storage layer. The `metadata` field
|
||||
handles all per-task extensions.
|
||||
|
||||
### Telemetry Event Mapping
|
||||
|
||||
The extension currently emits telemetry events via a PostHog-based
|
||||
`TelemetryService`. The SDK has its own `TelemetryService` with
|
||||
pluggable adapters (`OpenTelemetryAdapter`, `LoggerTelemetryAdapter`).
|
||||
|
||||
**Mapping of current extension events → SDK events:**
|
||||
|
||||
| Extension Event | SDK CORE_TELEMETRY_EVENTS | Notes |
|
||||
|---|---|---|
|
||||
| `user.extension_activated` | `CLIENT.STARTED` ("extension.activated") | ✅ Same event name |
|
||||
| `user.auth_started` | `USER.AUTH_STARTED` | ✅ Direct match |
|
||||
| `user.auth_succeeded` | `USER.AUTH_SUCCEEDED` | ✅ Direct match |
|
||||
| `user.auth_failed` | `USER.AUTH_FAILED` | ✅ Direct match |
|
||||
| `user.auth_logged_out` | `USER.AUTH_LOGGED_OUT` | ✅ Direct match |
|
||||
| `task.created` | `TASK.CREATED` | ✅ Direct match |
|
||||
| `task.restarted` | `TASK.RESTARTED` | ✅ Direct match |
|
||||
| `task.completed` | `TASK.COMPLETED` | ✅ Direct match |
|
||||
| `task.conversation_turn` | `TASK.CONVERSATION_TURN` | ✅ Direct match |
|
||||
| `task.tokens` | `TASK.TOKEN_USAGE` | ✅ Direct match |
|
||||
| `task.mode` | `TASK.MODE_SWITCH` | ✅ Direct match |
|
||||
| `task.tool_used` | `TASK.TOOL_USED` | ✅ Direct match |
|
||||
| `task.skill_used` | `TASK.SKILL_USED` | ✅ Direct match |
|
||||
| `task.diff_edit_failed` | `TASK.DIFF_EDIT_FAILED` | ✅ Direct match |
|
||||
| `task.provider_api_error` | `TASK.PROVIDER_API_ERROR` | ✅ Direct match |
|
||||
| `task.mention_used` | `TASK.MENTION_USED` | ✅ Direct match |
|
||||
| `task.mention_failed` | `TASK.MENTION_FAILED` | ✅ Direct match |
|
||||
| `task.mention_search_results` | `TASK.MENTION_SEARCH_RESULTS` | ✅ Direct match |
|
||||
| `task.subagent_started` | `TASK.SUBAGENT_STARTED` | ✅ Direct match |
|
||||
| `task.subagent_completed` | `TASK.SUBAGENT_COMPLETED` | ✅ Direct match |
|
||||
| `hooks.discovery_completed` | `HOOKS.DISCOVERY_COMPLETED` | ✅ Direct match |
|
||||
| `session.started` | `SESSION.STARTED` | ✅ Direct match |
|
||||
| `session.ended` | `SESSION.ENDED` | ✅ Direct match |
|
||||
|
||||
**Extension events with NO SDK equivalent (need adapter-layer emit):**
|
||||
|
||||
| Extension Event | Action |
|
||||
|---|---|
|
||||
| `user.opt_out` / `user.opt_in` | Emit via SDK's `captureRequired()` |
|
||||
| `user.telemetry_enabled` | Emit via SDK's `capture()` |
|
||||
| `user.extension_storage_error` | Emit via SDK's `capture()` |
|
||||
| `user.onboarding_progress` | Emit via SDK's `capture()` |
|
||||
| `workspace.*` (initialized, vcs_detected, etc.) | Emit via SDK's `capture()` |
|
||||
| `task.feedback` | Emit via SDK's `capture()` |
|
||||
| `task.option_selected` / `task.options_ignored` | Emit via SDK's `capture()` |
|
||||
| `task.checkpoint_used` | Emit via SDK's `capture()` |
|
||||
| `task.mcp_tool_called` | Emit via SDK's `capture()` |
|
||||
| `task.historical_loaded` | Emit via SDK's `capture()` |
|
||||
| `task.retry_clicked` | Emit via SDK's `capture()` |
|
||||
| `task.slash_command_used` | Emit via SDK's `capture()` |
|
||||
| `task.feature_toggled` | Emit via SDK's `capture()` |
|
||||
| `task.rule_toggled` | Emit via SDK's `capture()` |
|
||||
| `task.auto_condense_toggled` | Emit via SDK's `capture()` |
|
||||
| `task.yolo_mode_toggled` | Emit via SDK's `capture()` |
|
||||
| `task.terminal_*` (execution, output_failure, hang) | Emit via SDK's `capture()` |
|
||||
| `task.initialization` | Emit via SDK's `capture()` |
|
||||
| `task.summarize_task` | Emit via SDK's `capture()` |
|
||||
| `ui.*` (model_selected, button_clicked, etc.) | Emit via SDK's `capture()` |
|
||||
| `hooks.enabled` / `hooks.disabled` | Emit via SDK's `capture()` |
|
||||
| `hooks.cancel_requested` | Emit via SDK's `capture()` |
|
||||
| `hooks.context_modified` | Emit via SDK's `capture()` |
|
||||
| `worktree.*` | Emit via SDK's `capture()` |
|
||||
| `host.detected` | Emit via SDK's `capture()` |
|
||||
|
||||
**Extension events being REMOVED (features deleted):**
|
||||
|
||||
| Extension Event | Reason |
|
||||
|---|---|
|
||||
| `task.browser_tool_start/end/error` | Browser automation removed |
|
||||
| `task.focus_chain_*` (6 events) | Focus chain removed |
|
||||
| `task.workspace_search_pattern` | Folded into SDK search |
|
||||
| `task.subagent_enabled/disabled` | Toggle events; SDK manages directly |
|
||||
| `task.cline_web_tools_toggled` | Feature simplified |
|
||||
| `cline.grpc.response.size_bytes` | gRPC being removed |
|
||||
|
||||
**Metrics (OTEL counters/histograms):**
|
||||
|
||||
The extension has ~30 OTEL metrics (`cline.turns.total`,
|
||||
`cline.tokens.input.total`, `cline.api.ttft.seconds`, etc.). The SDK
|
||||
telemetry service supports `recordCounter`, `recordHistogram`, and
|
||||
`recordGauge`. We will emit these same metrics from the adapter layer
|
||||
using `telemetry.recordCounter()` / `telemetry.recordHistogram()`.
|
||||
The metric names can stay the same.
|
||||
|
||||
**Decision**: The SDK's `ITelemetryService.capture()` is a generic
|
||||
event emitter — we can emit ALL extension events through it. The
|
||||
adapter layer will create a thin telemetry wrapper that provides
|
||||
the same `captureTaskCreated()`, `captureToolUsage()`, etc. methods
|
||||
but delegates to the SDK's telemetry service. Events where the SDK
|
||||
already has a helper function (listed in the first table) use those
|
||||
directly. Others use `capture({ event, properties })`. No backend
|
||||
team coordination needed for the initial migration.
|
||||
|
||||
### JetBrains IPC Design
|
||||
|
||||
#### Current Architecture
|
||||
|
||||
```
|
||||
┌── Kotlin Plugin ──────────────────────────────┐
|
||||
│ │
|
||||
│ CoreProcessManager │
|
||||
│ └─ launches Node.js process (cline-core) │
|
||||
│ └─ communicates via gRPC ProtoBus │
|
||||
│ (port 26040-26340) │
|
||||
│ │
|
||||
│ HostBridgeService (gRPC server, port 26041) │
|
||||
│ ├─ DiffService │
|
||||
│ ├─ WindowService (show file, open dialog) │
|
||||
│ ├─ WorkspaceService (paths, diagnostics) │
|
||||
│ ├─ EnvService │
|
||||
│ └─ TestingService (get webview HTML) │
|
||||
│ │
|
||||
│ ProtoBusProxyService │
|
||||
│ └─ proxies webview ↔ cline-core gRPC │
|
||||
│ │
|
||||
│ JsPostMessageHandler │
|
||||
│ └─ injects JS bridge into JCEF webview │
|
||||
│ └─ converts postMessage → gRPC request │
|
||||
│ │
|
||||
│ WebViewManager │
|
||||
│ └─ loads webview HTML in JCEF │
|
||||
│ └─ receives gRPC responses → postMessage │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Problems with this architecture:
|
||||
- **Proto size limits**: gRPC messages hit 256MB limits with large
|
||||
conversations. The `ProtoBusProxyService` logs warnings at 10MB+.
|
||||
- **Java heap pressure**: Serializing/deserializing large proto
|
||||
messages stresses the JVM heap.
|
||||
- **Build complexity**: Proto compilation required for both
|
||||
TypeScript and Java/Kotlin.
|
||||
- **Stateless-in-theory**: The design is somewhat stateless but we
|
||||
haven't leveraged restart-for-reliability because state
|
||||
reconstruction is expensive.
|
||||
|
||||
#### Target Architecture
|
||||
|
||||
```
|
||||
┌── Kotlin Plugin ──────────────────────────────┐
|
||||
│ │
|
||||
│ CoreProcessManager │
|
||||
│ └─ launches SDK sidecar (Node.js) │
|
||||
│ └─ communicates via JSON-RPC over stdio │
|
||||
│ │
|
||||
│ HostCallbackService (JSON-RPC server) │
|
||||
│ ├─ showTextDocument, openDialog │
|
||||
│ ├─ getWorkspacePaths, getDiagnostics │
|
||||
│ ├─ getEnvVars, clipboard │
|
||||
│ └─ (extensible for PSI, run configs, etc.) │
|
||||
│ │
|
||||
│ WebviewBridge │
|
||||
│ └─ receives JSON messages from sidecar │
|
||||
│ └─ forwards to JCEF via executeJavaScript │
|
||||
│ └─ receives postMessage from JCEF │
|
||||
│ └─ forwards to sidecar via stdio │
|
||||
│ │
|
||||
│ WebViewManager │
|
||||
│ └─ loads adapted webview in JCEF │
|
||||
└────────────────────────────────────────────────┘
|
||||
|
||||
┌── SDK Sidecar (Node.js) ──────────────────────┐
|
||||
│ │
|
||||
│ SidecarMain │
|
||||
│ └─ JSON-RPC over stdio (bidirectional) │
|
||||
│ └─ imports @clinebot/core │
|
||||
│ └─ shares SDK adapter layer with VSCode │
|
||||
│ │
|
||||
│ ClineCore instance │
|
||||
│ └─ session management │
|
||||
│ └─ tool execution │
|
||||
│ └─ provider handling │
|
||||
│ │
|
||||
│ HostCallbackClient │
|
||||
│ └─ calls back to Kotlin for IDE ops │
|
||||
│ └─ registered as tool executors in SDK │
|
||||
│ │
|
||||
│ WebviewBridge │
|
||||
│ └─ translates SDK events → webview messages │
|
||||
│ └─ same code as VSCode adapter │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### IPC Mechanism: JSON-RPC over stdio
|
||||
|
||||
**Why JSON-RPC over stdio instead of gRPC:**
|
||||
- **No message size limits**: JSON over stdio has no inherent size
|
||||
cap. Conversations with 100K+ tokens serialize to ~5-20MB JSON
|
||||
which flows fine over pipes.
|
||||
- **No heap pressure**: No proto serialization on the Java side.
|
||||
Kotlin reads/writes JSON strings directly. JCEF already works
|
||||
with JSON.
|
||||
- **No proto compilation**: Eliminates the Java protobuf dependency
|
||||
and the dual TypeScript/Java proto generation step.
|
||||
- **Simple**: Well-understood protocol. Easy to debug (just read
|
||||
the pipe).
|
||||
|
||||
**Protocol**: JSON-RPC 2.0 over stdin/stdout with newline-delimited
|
||||
JSON messages. The sidecar reads from stdin and writes to stdout.
|
||||
Stderr is reserved for logging.
|
||||
|
||||
```
|
||||
→ {"jsonrpc":"2.0","method":"session/start","params":{...},"id":1}
|
||||
← {"jsonrpc":"2.0","result":{"sessionId":"..."},"id":1}
|
||||
← {"jsonrpc":"2.0","method":"webview/message","params":{"type":"assistant_delta","text":"..."}}
|
||||
```
|
||||
|
||||
**Notifications** (no `id`) are used for streaming events
|
||||
(assistant deltas, tool events, state updates). The Kotlin plugin
|
||||
processes these and forwards them to the JCEF webview.
|
||||
|
||||
**Callbacks** from sidecar → Kotlin (host operations) use
|
||||
JSON-RPC requests in the reverse direction:
|
||||
|
||||
```
|
||||
← {"jsonrpc":"2.0","method":"host/showTextDocument","params":{"path":"..."},"id":100}
|
||||
→ {"jsonrpc":"2.0","result":{"success":true},"id":100}
|
||||
```
|
||||
|
||||
#### Code Sharing Between VSCode, JetBrains, and CLI
|
||||
|
||||
The shared SDK adapter layer contains:
|
||||
|
||||
```
|
||||
src/sdk-adapter/
|
||||
index.ts — ClineSdkHost (creates ClineCore instance)
|
||||
session-backend.ts — LegacySessionBackend adapter
|
||||
webview-bridge.ts — SDK events → webview message translation
|
||||
provider-migration.ts — Legacy provider settings migration
|
||||
approval-adapter.ts — Auto-approve settings → SDK tool policies
|
||||
telemetry-adapter.ts — Extension telemetry → SDK telemetry
|
||||
types.ts — WebviewInbound, WebviewOutbound types
|
||||
```
|
||||
|
||||
Each host then has a thin integration layer:
|
||||
|
||||
- **VSCode** (`src/hosts/vscode/sdk-extension.ts`): In-process.
|
||||
Creates `ClineSdkHost`, registers VSCode LM handler, manages
|
||||
webview lifecycle. Uses `postMessage` for webview communication.
|
||||
|
||||
- **JetBrains** (`src/sidecar/main.ts`): Separate process. Creates
|
||||
`ClineSdkHost`, reads/writes JSON-RPC on stdio. Registers
|
||||
`HostCallbackClient` for IDE operations. The webview bridge code
|
||||
is identical — it just sends messages over stdio instead of
|
||||
`postMessage`.
|
||||
|
||||
#### Statefulness and Reliability
|
||||
|
||||
The sidecar is **stateful** — it holds the `ClineCore` instance with
|
||||
active sessions in memory. However, it is designed for **graceful
|
||||
restart**:
|
||||
|
||||
- **Session persistence**: All session state is written to disk
|
||||
after each turn (messages, manifest, transcript). On restart, the
|
||||
sidecar re-reads the session index and can resume.
|
||||
- **Crash detection**: The Kotlin plugin monitors the sidecar
|
||||
process. If it exits unexpectedly, the plugin restarts it after a
|
||||
brief delay (same as current `CoreProcessManager.RESTART_DELAY`).
|
||||
- **Smaller messages**: Because the protocol is JSON-RPC with
|
||||
incremental streaming (notifications for each delta), the
|
||||
messages are much smaller than the current gRPC approach which
|
||||
sends full state snapshots. This eliminates the heap pressure
|
||||
that made the current system unreliable.
|
||||
- **Interrupted operations**: If the sidecar crashes mid-turn, the
|
||||
next startup detects the unfinished session (status = "running"
|
||||
but no live process) and marks it as interrupted, just like the
|
||||
current task resumption flow.
|
||||
|
||||
#### JetBrains-Specific Tools
|
||||
|
||||
The HostCallback pattern makes it easy to add JetBrains-specific
|
||||
capabilities without changing shared code:
|
||||
|
||||
1. **Registration**: The sidecar's `HostCallbackClient` declares
|
||||
what capabilities the host supports (e.g., `"psi"`, `"runConfigs"`).
|
||||
2. **Tool Executors**: JetBrains-specific tool executors are
|
||||
registered in `ClineCoreOptions.defaultToolExecutors` when the
|
||||
sidecar starts. For example, a `getDiagnostics` executor that
|
||||
calls `host/getDiagnostics` via JSON-RPC to get IntelliJ's PSI
|
||||
analysis results.
|
||||
3. **No shared code changes**: Adding a new JetBrains capability
|
||||
requires:
|
||||
- Implementing the handler in Kotlin (`HostCallbackService`)
|
||||
- Adding a JSON-RPC method in the sidecar's `HostCallbackClient`
|
||||
- Optionally registering a custom tool executor
|
||||
|
||||
Example for exposing JetBrains PSI:
|
||||
```kotlin
|
||||
// Kotlin side
|
||||
"host/getPsiStructure" -> {
|
||||
val file = PsiManager.getInstance(project).findFile(virtualFile)
|
||||
// ... extract structure
|
||||
respondWithJson(result)
|
||||
}
|
||||
```
|
||||
```typescript
|
||||
// Sidecar side - registered as a custom tool executor
|
||||
defaultToolExecutors: {
|
||||
list_code_definition_names: async (args) => {
|
||||
// Call back to JetBrains for richer PSI-based results
|
||||
const result = await hostCallback.call("host/getPsiStructure", { path: args.path });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Current Architecture
|
||||
```
|
||||
┌─── VSCode Extension ──┐ ┌── JetBrains Plugin ──┐ ┌──── CLI ────┐
|
||||
│ WebviewProvider │ │ Kotlin Plugin │ │ React Ink │
|
||||
│ Controller │ │ CoreProcessManager │ │ ClineAgent │
|
||||
│ Task │ │ ProtoBusProxy │ │ │
|
||||
│ API providers (30+) │ │ JCEF WebView │ │ │
|
||||
│ McpHub │ │ │ │ │
|
||||
│ Webview (React) │ │ ↓ gRPC │ │ │
|
||||
│ │ │ cline-core │ │ │
|
||||
│ proto/cline/*.proto │ │ (standalone Node) │ │ │
|
||||
└────────────────────────┘ └──────────────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### Target Architecture
|
||||
```
|
||||
┌─── VSCode Extension ──┐ ┌── JetBrains Plugin ──┐ ┌──── CLI ────┐
|
||||
│ SDK Adapter Layer │ │ Kotlin Plugin │ │ React Ink │
|
||||
│ @clinebot/core │ │ │ │ TUI │
|
||||
│ (in-process) │ │ ↓ JSON-RPC/stdio │ │ │
|
||||
│ Webview (adapted) │ │ SDK sidecar (Node) │ │ │
|
||||
│ │ │ @clinebot/core │ │ │
|
||||
│ registerHandler │ │ │ │ │
|
||||
│ ("vscode-lm", ...) │ │ Webview (adapted) │ │ │
|
||||
└────────────────────────┘ └──────────────────────┘ └─────────────┘
|
||||
|
||||
All clients backed by:
|
||||
@clinebot/core → @clinebot/agents → @clinebot/llms
|
||||
↓ ↓ ↓
|
||||
Sessions Tools/Hooks Providers
|
||||
Storage MCP Bridge Model Catalog
|
||||
Telemetry Teams/Spawn Handler Registry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What the SDK Already Provides
|
||||
|
||||
These capabilities exist in the SDK and do not need to be rebuilt:
|
||||
|
||||
1. **Legacy provider settings migration** —
|
||||
`migrateLegacyProviderSettings()` reads `globalState.json` +
|
||||
`secrets.json`, writes to `providers.json`. Handles Anthropic,
|
||||
OpenAI, OpenAI Codex OAuth, OpenRouter, Bedrock, custom
|
||||
OpenAI-compatible endpoints, etc. Existing providers are never
|
||||
overwritten. Migrated entries are tagged `tokenSource: "migration"`.
|
||||
|
||||
2. **30+ provider handlers** — Anthropic, OpenAI (chat + responses
|
||||
API), Google Gemini, AWS Bedrock, Vertex AI, DeepSeek, Ollama,
|
||||
LM Studio, Mistral, Groq, Fireworks, Together, xAI, Cerebras,
|
||||
LiteLLM, Nebius, HuggingFace, and more.
|
||||
|
||||
3. **Custom handler registry** — `registerHandler(id, factory)` and
|
||||
`registerAsyncHandler(id, factory)` for providers that need
|
||||
host-specific dependencies (e.g., VSCode LM API).
|
||||
|
||||
4. **MCP management** — `InMemoryMcpManager` with stdio, SSE, and
|
||||
streamableHttp transports. Config loader reads from
|
||||
`~/.cline/data/settings/mcp.json` with Zod validation. Supports
|
||||
legacy format migration.
|
||||
|
||||
5. **Tool framework** — 8 built-in tools: `read_files`,
|
||||
`search_codebase`, `run_commands`, `editor`, `apply_patch`,
|
||||
`fetch_web_content`, `skills`, `ask_question`. Preset system with
|
||||
`development` (act mode) and `readonly` (plan mode) presets.
|
||||
Per-tool enable/disable. Policy-based approval (auto-approve,
|
||||
require-approval, per-tool overrides). Model-aware tool routing
|
||||
(e.g., OpenAI models use `apply_patch` instead of `editor`).
|
||||
|
||||
6. **Session lifecycle** — `ClineCore.create()` → `host.start()` /
|
||||
`host.send()` / `host.abort()` / `host.stop()` / `host.subscribe()`
|
||||
Interactive mode with prompt queueing (`queue`/`steer` delivery).
|
||||
Event subscription for streaming.
|
||||
|
||||
7. **Telemetry** — `TelemetryService` with pluggable adapters:
|
||||
`OpenTelemetryAdapter` (for enterprise OTEL), `LoggerTelemetryAdapter`.
|
||||
Standard events: `session.started`, `session.ended`,
|
||||
`task.created`, `task.conversation_turn`, `task.tool_used`, etc.
|
||||
See "Telemetry Event Mapping" above for full mapping.
|
||||
|
||||
8. **Rules & Skills** — Discovery from `.clinerules/`,
|
||||
`~/Documents/Cline/Rules`, `~/.cline/data/settings/rules/`.
|
||||
SKILL.md format with YAML frontmatter. Global and project scopes.
|
||||
|
||||
9. **Hooks** — `HookEngine` with lifecycle events. Node subprocess
|
||||
hook helpers for bash/powershell execution.
|
||||
|
||||
10. **Subagents/Teams** — `AgentTeamsRuntime`, spawn tools, team
|
||||
coordination with concurrent teammate agents.
|
||||
|
||||
11. **System prompt generation** — `getClineDefaultSystemPrompt()`
|
||||
with platform-aware customization.
|
||||
|
||||
12. **OAuth token management** — `RuntimeOAuthTokenManager` handles
|
||||
automatic token refresh during sessions for OAuth providers
|
||||
(Cline, OpenAI Codex).
|
||||
|
||||
13. **Storage isolation** — `CLINE_DIR`, `CLINE_DATA_DIR`,
|
||||
`CLINE_SESSION_DATA_DIR` environment variables plus
|
||||
`setClineDir()` / `setHomeDir()` APIs for test isolation.
|
||||
|
||||
---
|
||||
|
||||
## Test Strategy
|
||||
|
||||
See `migration.md` for the phase-by-phase test plan. This section
|
||||
covers the evergreen test infrastructure and classification.
|
||||
|
||||
### Test Infrastructure
|
||||
|
||||
**Extension unit tests** use Mocha with a custom `requires.ts` that
|
||||
mocks `vscode` and `@integrations/checkpoints` modules. Config in
|
||||
`.mocharc.json`. These tests run without VSCode.
|
||||
|
||||
**Webview tests** use Vitest with React Testing Library. Independent
|
||||
from the extension — they test React components in isolation.
|
||||
|
||||
**E2E tests** use Playwright to drive a real VSCode instance with
|
||||
the extension loaded. They test chat, auth, diff editing, and editor
|
||||
integration against a mock API server on localhost:7777.
|
||||
|
||||
**SDK adapter tests** use Vitest (simpler setup, better TypeScript
|
||||
support, no need for vscode-mock since adapter layer is
|
||||
VSCode-independent). Config in `vitest.config.sdk.ts`.
|
||||
|
||||
### SDK Storage Isolation for Tests
|
||||
|
||||
The SDK fully supports isolated test environments via environment
|
||||
variables and API calls:
|
||||
|
||||
```typescript
|
||||
import { setClineDir, setHomeDir } from "@clinebot/shared/storage";
|
||||
const tempHome = mkdtempSync(join(tmpdir(), "test-home-"));
|
||||
process.env.HOME = tempHome;
|
||||
process.env.CLINE_DIR = join(tempHome, ".cline");
|
||||
process.env.CLINE_DATA_DIR = join(tempHome, ".cline", "data");
|
||||
setHomeDir(tempHome);
|
||||
setClineDir(process.env.CLINE_DIR);
|
||||
```
|
||||
|
||||
### Test Framework Decisions
|
||||
|
||||
- **Keep Mocha** for extension unit tests (existing infrastructure)
|
||||
- **Keep Vitest** for webview and CLI tests
|
||||
- **Keep Playwright** for VSCode E2E tests
|
||||
- **Add Vitest** for new SDK adapter tests
|
||||
- **Isolated home directories** for all new tests touching storage
|
||||
|
||||
---
|
||||
|
||||
## Manual QA Guide
|
||||
|
||||
This section is for the QA team. It describes what has been removed
|
||||
(so you don't file bugs for missing features) and what areas carry
|
||||
the most regression risk after the SDK migration.
|
||||
|
||||
### Removed Features — Do Not File Bugs
|
||||
|
||||
- **Browser automation** — The built-in Playwright browser tool is
|
||||
gone. Users should use third-party MCP browser tools instead.
|
||||
- **IDE terminal integration** — Commands now run exclusively in a
|
||||
"background terminal" (headless shell). No terminal tab opens.
|
||||
- **Shadow-git checkpoints** — Existing checkpoints are invalidated.
|
||||
- **Memory bank / structured context** — All memory bank files and
|
||||
UI removed.
|
||||
- **Focus chain** — No focus chain panel or inline indicators.
|
||||
- **Deep planning / `/deep-planning`** — Plan/Act mode remains as
|
||||
the replacement.
|
||||
- **`/reportbug`** — Removed.
|
||||
- **Workflows** — Skills (SKILL.md format) are the replacement.
|
||||
- **Custom workflow slash commands** — Skills replace this.
|
||||
|
||||
### Risk Areas — VSCode Extension
|
||||
|
||||
1. **Provider credentials & model selection** — Verify existing API
|
||||
keys survive the upgrade and downgrade.
|
||||
2. **Cline provider OAuth / SSO** — Test sign-in, sign-out, token
|
||||
refresh, org switching.
|
||||
3. **Chat streaming & message display** — Watch for missing/duplicated
|
||||
messages, broken streaming, performance regressions.
|
||||
4. **Tool approval flow** — Verify auto-approve, YOLO mode, per-tool
|
||||
permissions.
|
||||
5. **Plan/Act mode** — Verify toggling, separate model configs,
|
||||
state persistence.
|
||||
6. **Task history & resume** — Old tasks appear, can be resumed; new
|
||||
tasks are saved.
|
||||
7. **MCP servers** — Existing configs picked up, tools work.
|
||||
8. **VSCode LM provider (Copilot)** — Verify it still works.
|
||||
9. **Settings UI** — All toggles and inputs persist correctly.
|
||||
10. **Webview performance** — Long conversations should not cause
|
||||
sluggishness.
|
||||
|
||||
### Risk Areas — JetBrains Extension
|
||||
|
||||
1. **Sidecar process lifecycle** — Starts reliably, auto-restarts,
|
||||
shuts down cleanly.
|
||||
2. **Webview communication** — Messages arrive, state is fresh.
|
||||
3. **Large conversations** — 100K+ tokens without OOM.
|
||||
4. **Host operations** — Open file, diagnostics, clipboard all work.
|
||||
5. **Keybindings** — Correct for the JetBrains platform.
|
||||
|
||||
### Risk Areas — CLI
|
||||
|
||||
1. **Agent backend replacement** — Core loop works in TUI and
|
||||
headless modes.
|
||||
2. **Provider & model picker** — All providers appear, defaults
|
||||
correct.
|
||||
3. **Shared state with IDE clients** — Credentials and history
|
||||
visible across clients.
|
||||
4. **Slash commands** — Removed commands don't appear; remaining
|
||||
ones work.
|
||||
5. **Worktrees & `--cwd`** — Function correctly.
|
||||
6. **ACP (Agent Communication Protocol)** — Programmatic usage works.
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
# SDK Migration Caveats & Known Issues
|
||||
|
||||
Tracking issues found during the migration from the legacy inference system to the ClineCore SDK.
|
||||
|
||||
## Status Legend
|
||||
- 🔴 **Blocker** — prevents core functionality
|
||||
- 🟡 **Minor** — cosmetic or UX annoyance
|
||||
- 🟢 **Fixed** — resolved
|
||||
|
||||
NOTE:
|
||||
|
||||
1. Use your debugging tool (DEBUG-HARNESS.md) to reproduce issues.
|
||||
2. Use your debugging tool to validate your fixes.
|
||||
3. Commit one verified change together.
|
||||
4. Work on these in any order you prefer.
|
||||
|
||||
---
|
||||
|
||||
🟢 Under accounts, the when logged in the "current balance" is ---- and
|
||||
the reload button does nothing. **Fixed:** getUserCredits handler fetches
|
||||
balance from Cline API using stored auth token.
|
||||
|
||||
🟢 Under accounts, the "cline environment" dropdown doesn't change from
|
||||
production when you select "staging" or "local". **Fixed:** state-builder
|
||||
now reads `clineEnv` from globalState and maps to Environment enum;
|
||||
updateSettings handler persists clineEnv and clears auth on change.
|
||||
|
||||
🟢 Under accounts, the logout button does nothing. **Fixed:**
|
||||
accountLogoutClicked handler clears auth credentials from disk.
|
||||
|
||||
🟢 Reportedly under accounts you can't sign in. **Fixed:**
|
||||
accountLoginClicked handler (was STUB) now opens the Cline login page
|
||||
in the browser.
|
||||
|
||||
🔴 When you have a low credit balance, even after you change accounts
|
||||
(for example from one "org" to another) or refreshing you keep getting
|
||||
an error "Insufficient balance. Your Cline Credits balance ..."
|
||||
|
||||
🔴 In chat, a chat response has a "copy" button that is
|
||||
obscured/partially obscured by the last generated code block. In the
|
||||
classic extension, this appears with enough space around it to be
|
||||
visible.
|
||||
|
||||
🟢 Changing the model during a conversation does not, *apparently*,
|
||||
change the model used for inference. **Fixed:** updateSettings now
|
||||
updates the in-memory apiConfiguration (not just disk) so
|
||||
model/provider changes take effect immediately for subsequent sessions.
|
||||
|
||||
🔴 The OpenAI compatible provider produces "404 404 page not found"
|
||||
errors.
|
||||
|
||||
🔴 When running tools (for example, prompt the agent to use kb_status)
|
||||
output rectangles appear but they are blank.
|
||||
|
||||
🔴 When prompted with multiple-step work (like 1. Do this 2. Do that)
|
||||
the chat displays "0/0 TODOs".
|
||||
|
||||
🟡 Checkpoints appear in options, but checkpoints don't appear in
|
||||
chats; we need to overhaul the checkpoints system anyway see
|
||||
ARCHITECTURE.md.
|
||||
|
||||
🟢 In the history section, you can't mark chats as favorites.
|
||||
|
||||
🟡 Banners (for example "Try Claude Sonnet 4.6") can be dismissed, but
|
||||
there are no < and > buttons visible to page between them.
|
||||
|
||||
🟢 "Add to Cline" right click menu (use the command to trigger it)
|
||||
does not do anything. **Fixed:** sendAddToInputEvent now falls back to
|
||||
the SDK bridge's pushAddToInput when no classic gRPC subscriptions are
|
||||
active; WebviewGrpcBridge handles subscribeToAddToInput streaming and
|
||||
sends via both gRPC response and typed message.
|
||||
|
||||
🟢 When a task is cancelled, you can't enter a new chat and send that
|
||||
chat in addition. (The repro is: Run a task, click cancel relatively
|
||||
quickly, type a new prompt, try to hit enter/click the arrow.) **Fixed:**
|
||||
cancelTask now clears currentSession after abort so subsequent
|
||||
askResponse calls start a new task instead of sending to the aborted
|
||||
session.
|
||||
|
||||
🟢 MCP Servers tab never finishes loading (may be a workos: token
|
||||
prefix problem?) **Fixed:** subscribeToMcpServers now sends initial
|
||||
server data as a typed message (mcpServers) instead of only via gRPC
|
||||
streaming response, which the webview's dual-listen pattern picks up.
|
||||
|
||||
🟢 Attached images (via drag and drop or the + icon to attach an image
|
||||
file) aren't submitted to models. **Fixed:** newTask and askResponse
|
||||
now include the images array in ClineMessage objects so attached images
|
||||
appear in the chat UI and are passed to the SDK session.
|
||||
|
||||
🔴 Changing the account profile in the accounts tab (for example from
|
||||
Cline External, which has budget, to Cline Internal Testing Org, which
|
||||
doesn't) doesn't switch to that profile for inference.
|
||||
|
||||
🟡 Account panel may show logged-out state on launch despite being
|
||||
logged in. Inference still works. The `subscribeToAuthStatusUpdate`
|
||||
streaming subscription in the webview may not be established before
|
||||
the bridge pushes initial auth data, causing a race condition. On most
|
||||
launches the auth state loads correctly (verified via debug harness),
|
||||
but the user reports intermittent occurrences.
|
||||
|
||||
🔴 "Sign up with Cline" button does not do the IDE login flow — it
|
||||
opens the dashboard (`https://app.cline.bot/login`) instead. In
|
||||
origin/main, `accountLoginClicked` calls
|
||||
`AuthService.createAuthRequest()` which starts a local HTTP server for
|
||||
the OAuth callback, calls the Cline API auth endpoint with the
|
||||
callback URL, and opens the resulting OAuth redirect URL. The SDK does
|
||||
not have access to `AuthService` or `HostProvider.getCallbackUrl()`,
|
||||
so it falls back to opening the dashboard URL directly. Users who are
|
||||
not logged in cannot authenticate through the extension UI.
|
||||
**Requires:** SDK support for OAuth callback flows (see
|
||||
SDK-FEATURE-REQUESTS.md).
|
||||
|
||||
🔴 Buttons in the MCP Servers popup do nothing. The restart (🔄),
|
||||
enable/disable toggle, and delete (🔴) buttons on individual MCP
|
||||
servers are all no-ops. The gRPC handler stubs these methods:
|
||||
`restartMcpServer`, `deleteMcpServer`, `toggleMcpServer`,
|
||||
`toggleToolAutoApprove`, `authenticateMcpServer`, `updateMcpTimeout`.
|
||||
In origin/main these go through `controller.mcpHub` which manages live
|
||||
MCP server connections. The SDK reads MCP settings from disk but does
|
||||
not expose server lifecycle management to the webview. See SDK-MCP.md
|
||||
for details, we need to implement much more elaborate MCP support
|
||||
client side to work with the SDK, via a custom RuntimeBuilder and tool
|
||||
client factory that supports streamable HTTP; watches the file for
|
||||
changes and either restarts a session or causes the tool definitions
|
||||
to change; etc.
|
||||
|
||||
🔴 Buttons in the MCP Servers → Configure tab do nothing. Same root
|
||||
cause as above — the configure tab shows servers (e.g. "linear",
|
||||
"kamibiki") with restart/toggle/delete controls, but all interactions
|
||||
are stubbed. The "Configure MCP Servers" and "Advanced MCP Settings"
|
||||
links also depend on `openMcpSettings` which may or may not be wired.
|
||||
|
||||
🟡 MCP Marketplace never loads. The Marketplace tab shows "No MCP
|
||||
servers found in the marketplace". **Partial fix applied:**
|
||||
`subscribeToMcpMarketplaceCatalog` in `webview-grpc-bridge.ts` now
|
||||
reads from the disk cache (`~/.cline/data/cache/mcp_marketplace_catalog.json`)
|
||||
via `readMcpMarketplaceCatalogFromCache()` and pushes the catalog to
|
||||
the webview as a streaming response. This works if the cache was
|
||||
previously populated by the classic extension. However,
|
||||
`refreshMcpMarketplace` (which fetches fresh data from the API) is
|
||||
still stubbed because it requires an authenticated API call to
|
||||
`https://api.cline.bot/v1/mcp/marketplace`. If no cache file exists
|
||||
(fresh install), the marketplace will remain empty.
|
||||
**Note:** Could not validate with debug harness since it runs the
|
||||
classic extension, not the SDK adapter.
|
||||
-147
@@ -1,152 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.86.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add Claude Opus 4.8 provider support, including 1M-context variants where available.
|
||||
- Add Moonshot Kimi K2.6 model support.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show prompt-cache support for Qwen 3.7 Max in the Cline provider.
|
||||
- Fix the VS Code nightly publish workflow startup permissions.
|
||||
|
||||
### Changed
|
||||
|
||||
- Move the VS Code extension project into `apps/vscode`.
|
||||
|
||||
## [3.85.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add GPT-5.5 support to SAP AI Core.
|
||||
- Add DeepSeek V4 Flash and Pro models.
|
||||
- Add Gemini 3.5 Flash to Gemini and Vertex providers.
|
||||
- Add `/lg-task` URI webhook integration for LG dashboard flows.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Vertex AI global endpoint handling for Claude models.
|
||||
- Route Poolside Laguna models through next-gen prompts and native tool calling.
|
||||
|
||||
### Changed
|
||||
|
||||
- Update `diff` and `protobufjs` dependencies.
|
||||
|
||||
## [3.84.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add SAP AI Core support for additional hosted models
|
||||
|
||||
### Fixed
|
||||
|
||||
- Disable the MCP "Restart Server" button when a server is toggled off.
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove the Cline Kanban launch modal and bundled demo media from the VS Code extension startup flow.
|
||||
|
||||
## [3.83.0]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show a clear "Searching..." state in the @-mention file picker
|
||||
- Improve @-mention file search performance
|
||||
- Allow `write_to_file` to create or overwrite files with empty content.
|
||||
- Fix validation failures for MCP servers that require an object.
|
||||
- Enable OpenRouter prompt cache control for Qwen models.
|
||||
- Update Axios and SAP Connectivity dependencies
|
||||
|
||||
### Changed
|
||||
|
||||
- Use the VS Code-specific `README.marketplace.md` when packaging and publishing the VS Code extension
|
||||
- Add telemetry to @-mention search to help diagnose local, remote, and multi-root workspace search behavior.
|
||||
|
||||
## [3.82.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Restore VS Code foreground terminal support and settings.
|
||||
- Add latest OpenAI, SAP AI Core, and Z AI models.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix hook template JSON escaping.
|
||||
- Improve ripgrep file search error handling.
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove hardcoded model lists from docs.
|
||||
|
||||
## [3.81.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add GPT-5.5 model support for OpenAI Codex subscription users.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove hardcoded "What’s New" fallback items in webview; only remote-configured welcome banners are shown.
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve cline-core memory diagnostics used by the extension runtime:
|
||||
- enable near-heap-limit heap snapshots
|
||||
- add periodic memory usage logging
|
||||
- log discovered heap snapshots on abnormal exits for easier OOM debugging
|
||||
|
||||
## [3.80.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Wire up remote `globalSkills` from enterprise remote config with full UI, toggle support, and system prompt integration — enterprise-managed skills now appear under a dedicated "Enterprise Skills" section and support `alwaysEnabled` enforcement
|
||||
- Onboarding flow now uses dynamically fetched recommended models instead of a hardcoded list, with a fallback to the welcome view on failure
|
||||
- Add dedicated "Quota Exceeded" error message in the chat error UI when Cline account spend caps are hit
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix OOM crashes during long conversations by setting `--max-old-space-size=8192` for the cline-core Node.js process (was defaulting to ~2 GB)
|
||||
- Show detailed error information in the chat error row instead of a generic caught error message
|
||||
- Update `axios` to 1.15.0 across all packages
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove foreground terminal mode — all task command execution now defaults to background mode, removing the VS Code integrated terminal dependency and related settings UI
|
||||
- Remove old hardcoded announcement banners
|
||||
|
||||
## [3.79.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add Claude Opus 4.7 model support
|
||||
- Add Azure Blob Storage as a storage provider
|
||||
- Add `globalSkills` to remote config
|
||||
- Inline value reuse in user-level remote-config discovery
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix cache reflection for Cline and Vercel API handlers
|
||||
- Fix stuck `command_output` ask when terminal command ends unexpectedly
|
||||
- Add `use_subagents` to system prompt for GLM, Hermes, and XS models
|
||||
- Fix action injection security risk
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove deprecated evals tool
|
||||
|
||||
## [3.78.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add a dedicated "Spend Limit Reached" error UI when spend caps are hit
|
||||
- Docs updates
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show actual `read_file` line ranges in chat UI
|
||||
|
||||
## [3.77.0]
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
@.clinerules/general.md
|
||||
@.clinerules/network.md
|
||||
@.clinerules/cli.md
|
||||
|
||||
+8
-8
@@ -42,14 +42,15 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
```bash
|
||||
code cline
|
||||
```
|
||||
3. Install [bun](https://bun.com)
|
||||
4. Install the necessary dependencies for the extension and webview-gui:
|
||||
3. Install the necessary dependencies for the extension and webview-gui:
|
||||
```bash
|
||||
cd apps/vscode && npm run install:all && cd ../..
|
||||
cd sdk && bun run build && cd ..
|
||||
npm run install:all
|
||||
```
|
||||
5. Generate Protocol Buffer files (required before first build):
|
||||
6. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
|
||||
4. Generate Protocol Buffer files (required before first build):
|
||||
```bash
|
||||
npm run protos
|
||||
```
|
||||
5. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
|
||||
|
||||
|
||||
|
||||
@@ -61,7 +62,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
2. Push your branch and create a PR on GitHub. Our CI will:
|
||||
- Run tests and checks
|
||||
3. Testing
|
||||
- Run `cd apps/vscode && npm run test` to run tests locally.
|
||||
- Run `npm run test` to run tests locally.
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
|
||||
### Extension
|
||||
@@ -73,7 +74,6 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
- If you dismissed the prompts, you can install them manually from the Extensions panel
|
||||
|
||||
2. **Local Development**
|
||||
- cd into the vscode extension, `cd apps/vscode`
|
||||
- Run `npm run install:all` to install dependencies
|
||||
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
|
||||
- Run `npm run test` to run tests locally
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# E2E Debugging Visibility
|
||||
|
||||
This is a sub-project of the project described in migration.md. We are
|
||||
engaging in a big change to the VSCode extension. You gain visibility
|
||||
into the extension through tests, but sometimes that is not enough.
|
||||
|
||||
Your goal is to create way where you can launch the VSCode extension
|
||||
and have access to the node debugger and webview debugger so that you
|
||||
can set break points, evaluate expressions, inject input (consider
|
||||
Microsoft's work with Playwright in VSCode, but anything that works is
|
||||
fine) step, etc. so that you can observe execution and find and fix
|
||||
problems without the tedious cycle off adding print statements,
|
||||
running a test which may hang, fixing something, removing the print
|
||||
statements, etc.
|
||||
|
||||
For this step to be complete, you need to demonstrate you have the
|
||||
ability to:
|
||||
|
||||
1. Build and run the VSCode extension in an unminified form, including
|
||||
the webview unminified.
|
||||
|
||||
2. Add and remove breakpoints, including conditional breakpoints, on
|
||||
the extension side.
|
||||
|
||||
3. Add and remove breakpoints, including conditional breakpoints, on
|
||||
the webview side.
|
||||
|
||||
4. Evaluate expressions at breakpoints. You should be able to refer to
|
||||
local variables, that is, the extension code should be
|
||||
unminified. (Concatenated is fine as long as you can find your way
|
||||
around.)
|
||||
|
||||
5. Run, step at breakpoints.
|
||||
|
||||
6. Generate UI actions like opening the Cline sidebar, focusing
|
||||
elements, typing, etc.
|
||||
|
||||
7. Take screenshots that you can view.
|
||||
|
||||
You need to use this tool inside your agentic loop, that is, you will
|
||||
need to drive both of these debugees simultaneously from one loop, so
|
||||
you may need to write yourself a tool which blocks until one of the
|
||||
debugees hits a breakpoint; can use a timeout and let you break and
|
||||
examine isolates and stacks; things of that nature.
|
||||
|
||||
We are working on macOS, it is fine if this tool just works on macOS
|
||||
for now.
|
||||
|
||||
## Caveats
|
||||
|
||||
- **`CLINE_DIR` environment variable**: Because the debug harness
|
||||
spawns a fresh VSCode instance that runs the Cline extension, and
|
||||
because *you* (the agent) share state with that extension (API keys,
|
||||
provider settings, task history), you **must** set `CLINE_DIR=~/.cline`
|
||||
when launching the harness. Without it the debugee uses an isolated
|
||||
data directory and won't have your API keys or provider configuration,
|
||||
causing inference to fail silently (requests hang or error).
|
||||
```bash
|
||||
CLINE_DIR=~/.cline npx tsx src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
```
|
||||
|
||||
- **⚠️ "Introducing Cline Kanban" promotion — DISMISS FIRST**: On
|
||||
fresh launches, a full-screen promotional overlay ("Introducing Cline
|
||||
Kanban") may appear in the sidebar webview. It obscures all other UI
|
||||
elements, so screenshots will show only the promo and interactions
|
||||
with the chat input, settings buttons, etc. will fail. **You must
|
||||
dismiss it immediately after opening the sidebar, before doing
|
||||
anything else.** This is easy to forget — if your screenshots look
|
||||
wrong or interactions fail, this is almost certainly why.
|
||||
|
||||
**Method 1 — Click the close button via DOM** (most reliable):
|
||||
```bash
|
||||
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
|
||||
curl localhost:19229/api -d '{
|
||||
"method": "web.evaluate",
|
||||
"params": {"expression": "document.querySelector(\".sr-only\")?.parentElement?.click()"}
|
||||
}'
|
||||
```
|
||||
This finds the `<span class="sr-only">Close</span>` element and
|
||||
clicks its parent `<button>`.
|
||||
|
||||
**Method 2 — Press ESC** (simpler but less reliable):
|
||||
```bash
|
||||
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
|
||||
curl localhost:19229/api -d '{
|
||||
"method": "web.evaluate",
|
||||
"params": {"expression": "document.activeElement.dispatchEvent(new KeyboardEvent(\"keydown\", {key: \"Escape\", code: \"Escape\", keyCode: 27, bubbles: true}))"}
|
||||
}'
|
||||
```
|
||||
|
||||
If neither works, take a screenshot (`ui.screenshot`) to see what's
|
||||
on screen and identify the current dismiss control.
|
||||
|
||||
- **Screenshots**: `ui.screenshot` and `ui.sidebar_screenshot` save
|
||||
PNG files to `/tmp/cline-debug/` and return `{path}` in the JSON
|
||||
response. **Do NOT open the screenshot file with `open`** — on macOS
|
||||
this launches Preview.app which covers the VSCode window you're
|
||||
debugging. Use `read_file` on the returned path to examine the image
|
||||
without disrupting the debuggee.
|
||||
|
||||
- **Debuggee launch delay**: The post-launch activation delay is 1
|
||||
second. If the extension hasn't fully loaded by the time you interact
|
||||
with it, just retry — `ui.open_sidebar` and `findSidebar` have their
|
||||
own internal polling with timeouts.
|
||||
|
||||
- **Top toolbar buttons**: The "new task", "mcp servers", "history",
|
||||
"accounts", and "settings" toolbar buttons are not in the webview DOM
|
||||
(they're VSCode UI chrome). Instead of trying to click them, run the
|
||||
associated VSCode commands directly:
|
||||
- New Task: `cline.plusButtonClicked`
|
||||
- MCP Servers: `cline.showMcpServers`
|
||||
- History: `cline.showHistory`
|
||||
- Accounts: `cline.showAccount`
|
||||
- Settings: `cline.openSettings`
|
||||
|
||||
Example:
|
||||
```bash
|
||||
curl localhost:19229/api -d '{
|
||||
"method": "ui.command_palette",
|
||||
"params": {"command": "cline.openSettings"}
|
||||
}'
|
||||
```
|
||||
|
||||
- **CDP disconnects after window reload**: If you use
|
||||
`workbench.action.reloadWindow` (e.g., to pick up a rebuilt
|
||||
webview), the extension host CDP connection drops. You must do a full
|
||||
`shutdown` + relaunch of the debug harness to reconnect.
|
||||
|
||||
## References
|
||||
|
||||
You can see the vscode source code in ~/clients/cline/vscode and
|
||||
search it with kb_search vscode
|
||||
|
||||
You can search the cline source code (snapshot) with kb_search cline.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,237 @@
|
||||
These are issues the agent claims to have fixed, but should be verified:
|
||||
|
||||
## Fixed
|
||||
|
||||
### 1. 🟢 Remote inference fails with ECONNREFUSED (localhost:4000)
|
||||
**File:** `src/sdk/cline-session-factory.ts`
|
||||
**Symptom:** Sending any message with the Cline provider fails after 6 retries with "Cannot connect to API" / ECONNREFUSED to `http://localhost:4000/v1/chat/completions`.
|
||||
**Root cause:** Base URL overrides (`openAiBaseUrl`, `openRouterBaseUrl`, `liteLlmBaseUrl`) were applied unconditionally to ALL providers, clobbering the Cline API URL. A stale `openAiBaseUrl: "http://localhost:4000/v1"` in `~/.cline/data/globalState.json` overwrote `https://api.cline.bot/api/v1`.
|
||||
**Fix:** Guard each base URL override with a provider check so it only applies to its respective provider.
|
||||
|
||||
---
|
||||
|
||||
### 2. 🟢 Task history not persisted after completion
|
||||
**File:** `src/sdk/SdkController.ts`
|
||||
**Symptom:** After a task completes successfully, the task did NOT appear in the RECENT section when returning to the home screen.
|
||||
**Root cause:** `newTask()` created a `currentTaskItem` but never pushed it to `this.taskHistory[]`. `clearTask()` discarded it without saving. No disk persistence implementation existed.
|
||||
**Fix:** `SdkController` now persists tasks on three paths: (1) `done` event updates `currentTaskItem` with final usage and calls `persistCurrentTask()`, (2) `clearTask()` calls `persistCurrentTask()` before resetting, (3) `cancelTask()` persists the in-progress task. `LegacyStateReader` gained `saveTaskHistory()`, `saveUiMessages()`, and `deleteTaskDirectory()` methods for disk I/O.
|
||||
|
||||
### 3. 🟢 Task resumption not implemented
|
||||
**File:** `src/sdk/SdkController.ts`
|
||||
**Symptom:** Cannot resume a previous task from history.
|
||||
**Fix:** `showTaskWithId()` now finds the task in history, loads saved UI messages via `legacyState.readUiMessages()`, restores them into the translator, and sets `currentTaskItem`. The task view renders with full message history.
|
||||
|
||||
### 6. 🟢 Settings persistence is best-effort / incomplete
|
||||
**File:** `src/sdk/SdkController.ts`
|
||||
**Symptom:** `updateSettings()` was a no-op stub with a TODO comment.
|
||||
**Fix:** `updateSettings()` now persists settings to `globalState.json` via `legacyState.saveApiConfiguration()`. `updateAutoApprovalSettings()` also persists via the same mechanism.
|
||||
|
||||
### 7. 🟢 Completed task not appearing in RECENT section
|
||||
**Where:** Home screen → RECENT section
|
||||
**Symptom:** After completing a task and clicking "New Task", the completed task did not appear in the RECENT history list.
|
||||
**Fix:** Resolved by issue #2 fix — tasks are now persisted to `taskHistory` on completion, so they appear in RECENT.
|
||||
|
||||
### 8. 🟢 Top bar buttons are non-functional
|
||||
**Where:** Header bar — accounts, settings, new chat, history buttons
|
||||
**Symptom:** Clicking any of the top bar buttons (accounts icon, settings gear, new chat +, task history) does nothing. No navigation occurs, no panels open.
|
||||
**Root cause:** gRPC stub. The webview subscribes to `subscribeToSettingsButtonClicked`, etc. — these are event streams pushed from the extension host when VSCode title bar buttons are clicked.
|
||||
**Fix:** Extension.ts button commands now send typed `navigate` messages via `WebviewGrpcBridge.navigate()`, bypassing gRPC streaming subscriptions. Plus button also calls `clearSdkTask()` to reset the SDK session.
|
||||
|
||||
### 9. 🟢 @ mentions / autocomplete not working
|
||||
**Where:** Chat input textarea
|
||||
**Symptom:** Typing `@` in the chat input does not trigger any autocomplete dropdown. No filename suggestions, no context items offered.
|
||||
**Root cause:** gRPC stub. The `@` autocomplete calls `FileServiceClient.searchFiles()` to get matching file paths.
|
||||
**Fix:** Implemented `searchFiles` handler in grpc-handler.ts that delegates to `SdkController.searchFiles()`, which does a real filesystem walk of the workspace directory (max depth 8, skips node_modules/.git/etc). Returns results with `mentionsRequestId` for proper request correlation.
|
||||
|
||||
### 10. 🟢 Add files/images button (+) does nothing
|
||||
**Where:** Bottom bar, "+" button next to chat input
|
||||
**Symptom:** Clicking the "+" button to add files and images produces no response — no file picker, no dropdown, no action.
|
||||
**Root cause:** gRPC stub. The button calls `FileServiceClient.selectFiles()` which opens a native file picker dialog.
|
||||
**Fix:** Implemented `selectFiles` handler that returns `StringArrays` format (`values1` = image data URLs, `values2` = file paths). VscodeWebviewProvider callback reads image files as base64 data URLs and returns relative paths for non-images.
|
||||
|
||||
### 11. 🟢 Cannot switch from Plan mode back to Act mode
|
||||
**Where:** Bottom bar Plan/Act toggle
|
||||
**Symptom:** Clicking "Plan" successfully switches to Plan mode. However, clicking "Act" after that does NOT switch back to Act mode.
|
||||
**Root cause:** Proto enum conversion bug — the webview sends numeric enum values (0=PLAN, 1=ACT) but the handler expected string values.
|
||||
**Fix:** `handleTogglePlanActMode` now converts proto enum values: `0/"PLAN" → "plan"`, `1/"ACT" → "act"`, with fallback for already-converted string values.
|
||||
|
||||
### 12. 🟢 "Manage cline rules and workflows" still mentions workflows
|
||||
**Where:** ClineRulesToggleModal tooltip and aria-label
|
||||
**Symptom:** The tooltip and aria-label still said "Manage Cline Rules & Workflows".
|
||||
**Fix:** Updated tooltip to "Manage Cline Rules" and aria-label to "Show/Hide Cline Rules". Also simplified chat placeholder text to remove "workflows" mention.
|
||||
|
||||
### 4. 🟢 Input text not cleared immediately on send
|
||||
**Where:** Webview chat input
|
||||
**Symptom:** After typing a message and pressing send/enter, the text remains visible in the input field briefly before clearing. Creates a feeling of lag.
|
||||
**Root cause:** In `useMessageHandlers.ts`, `setInputValue("")` was called AFTER `await TaskServiceClient.newTask(...)` or `await TaskServiceClient.askResponse(...)` completed. The network round-trip caused visible delay before the input cleared.
|
||||
**Fix:** Moved `setInputValue("")`, `setActiveQuote(null)`, `setSelectedImages([])`, `setSelectedFiles([])` to execute immediately when `hasContent` is true, before any async gRPC calls. React schedules a re-render synchronously, clearing the input before the network round-trip.
|
||||
|
||||
### 5. 🟢 api_req_started fires with zeroed token counts
|
||||
**Where:** Message stream / ChatRow rendering
|
||||
**Symptom:** An `api_req_started` partial message fires with `{"tokensIn":0,"tokensOut":0,"cost":0}` before real counts arrive, causing a brief flash of "0 / 200.0k" in the token usage bar.
|
||||
**Root cause:** `ContextWindow.tsx` rendered the token bar whenever `tokenData` existed (i.e., when `contextWindow > 0`), regardless of whether `lastApiReqTotalTokens` was 0.
|
||||
**Fix:** Added `tokenData.used === 0` guard to the null-return check in `ContextWindow.tsx`. The token bar now only renders when real (non-zero) token data is available.
|
||||
|
||||
### 14. 🟢 "Delete chat" button shows placeholder size
|
||||
**Where:** Task history → delete button tooltip / label
|
||||
**Symptom:** The "Delete chat" button tooltip displays `Delete Task (size: --)` when task size data is unavailable.
|
||||
**Root cause:** `DeleteTaskButton.tsx` unconditionally rendered `(size: ${taskSize ? formatSize(taskSize) : "--"})`, showing "--" when `taskSize` is undefined.
|
||||
**Fix:** Changed to conditionally include size: `taskSize ? \`Delete Task (${formatSize(taskSize)})\` : "Delete Task"`. The tooltip now shows just "Delete Task" when size is unavailable, or "Delete Task (12.4 KB)" when it is.
|
||||
|
||||
### 13. 🟢 Terminal settings navigates to blank/stuck webview
|
||||
**Where:** Settings → Terminal tab
|
||||
**Symptom:** Opening terminal settings causes React to crash, leaving a blank webview.
|
||||
**Root cause:** `getAvailableTerminalProfiles` was a gRPC stub returning `{data:{}}`. The webview called `setAvailableTerminalProfiles(response.profiles)` where `response.profiles` was `undefined`, overwriting the default `[]`. Then `TerminalSettingsSection` called `profilesToShow.map()` on `undefined`, crashing React.
|
||||
**Fix:** Implemented real `handleGetAvailableTerminalProfiles()` handler in grpc-handler.ts that calls `getAvailableTerminalProfiles()` from `utils/shell.ts`, returning platform-specific profiles (Default, zsh, bash on macOS). Also added `availableTerminalProfiles: []` to state-builder.ts as a safety net, and wired `scrollToSettings` to fire `navigate("settings", { targetSection })` via the bridge.
|
||||
**Verified:** Debug harness confirmed handler returns `{data:{profiles:[{id:"default",...},{id:"zsh",...},{id:"bash",...}]}}`, Settings → Terminal tab renders "Default Terminal Profile" dropdown with all 3 options, shell integration timeout, and terminal reuse settings.
|
||||
|
||||
### 16. 🟢 Cline Rules popup still has a "Workflows" tab
|
||||
**Where:** Scales-of-justice icon → Cline Rules modal
|
||||
**Symptom:** The "Manage Cline Rules" popup contains a "Workflows" tab. Issue #12 fixed the tooltip text, but the tab itself still exists inside the modal.
|
||||
**Root cause:** The `ClineRulesToggleModal` component had a full "Workflows" tab with toggle lists for global, local, and remote workflows, plus a description section. Workflows are no longer a feature.
|
||||
**Fix:** Removed the Workflows tab button, workflows description text, workflows content section (remote/global/local workflow toggle lists), and the remote workflows banner condition from `ClineRulesToggleModal.tsx`. The `currentView` state type was narrowed from `"rules" | "workflows" | "hooks" | "skills"` to `"rules" | "hooks" | "skills"`.
|
||||
|
||||
### 17. 🟢 Account pane shows "Sign up with Cline" despite being logged in
|
||||
**Where:** Account panel / pane
|
||||
**Symptom:** Even when the user is already authenticated and logged in, the account pane still displays "Sign up with Cline" and other sign-up prompts as if the user were not authenticated.
|
||||
**Root cause:** `subscribeToAuthStatusUpdate` is a streaming subscription. The bridge's `handleStreamingRequest()` fell into the `default` no-op case, so auth state was never pushed to the webview.
|
||||
**Fix:** Added explicit `subscribeToAuthStatusUpdate` case in `handleStreamingRequest()` that reads auth credentials from disk and pushes them. Added `roles` to org data and null safety in `isAdminOrOwner()`.
|
||||
|
||||
### 18. 🟢 "Sign up with Cline" button does nothing (moot)
|
||||
**Where:** Account pane → Sign up button
|
||||
**Fix:** Resolved by #17 — the sign-up button is no longer shown when the user is already authenticated.
|
||||
|
||||
### 19. 🟢 Terminal settings still shows "Terminal Execution Mode" option
|
||||
**Where:** Settings → Terminal
|
||||
**Fix:** Removed the Terminal Execution Mode dropdown, its handler, and unused imports from `TerminalSettingsSection.tsx`.
|
||||
|
||||
### 20. 🟢 Cline provider model type-ahead search does not work
|
||||
**Where:** Settings → Model selector (Cline provider)
|
||||
**Root cause:** `refreshClineModelsRpc` was a gRPC stub returning `{}`. The webview never received any model data.
|
||||
**Fix:** Implemented `handleRefreshClineModels()` in grpc-handler.ts that reads from disk cache first, then falls back to fetching from the Cline API using `globalThis.fetch`. Converts API response to `ModelInfo` records and returns in protobuf format.
|
||||
|
||||
### 21. 🟢 Cline provider recommends possibly outdated model
|
||||
**Where:** Settings → Model selector (Cline provider)
|
||||
**Fix:** Updated fallback recommendation text in `ClineModelPicker.tsx` from `anthropic/claude-sonnet-4.5` to `anthropic/claude-sonnet-4.6`.
|
||||
|
||||
### 22. 🟢 "Use different models for Plan and Act" checkbox immediately unchecks
|
||||
**Where:** Settings → Model configuration
|
||||
**Root cause:** `updateSettings()` was writing raw settings instead of merging individual known keys.
|
||||
**Fix:** `updateSettings()` now iterates known settings keys and writes each one individually. `buildExtensionState()` reads `planActSeparateModels` from `globalState`.
|
||||
|
||||
### 23. 🟢 MCP settings Configure tab crashes React
|
||||
**Where:** MCP Servers → Configure tab
|
||||
**Root cause:** `refreshMcpMarketplace` stub returns `{}`, replacing the default `{ items: [] }` state, causing `items.find()` to crash.
|
||||
**Fix:** Added optional chaining (`?.items?.find`) in `getMcpServerDisplayName()`.
|
||||
|
||||
### 24. 🟢 History tab is empty and search does nothing
|
||||
**Where:** History tab (task history list)
|
||||
**Root cause:** `handleGetTaskHistory()` returned `{ data: { history } }` but webview reads `response.tasks`.
|
||||
**Fix:** Changed return to `{ data: { tasks, totalCount } }`. Implemented server-side filtering/sorting.
|
||||
|
||||
### 25. 🟢 Auto-approve options immediately uncheck when toggled
|
||||
**Where:** Auto-approve options flyout
|
||||
**Root cause:** Same as #22 — `updateAutoApprovalSettings()` was not persisting properly.
|
||||
**Fix:** Fixed alongside #22.
|
||||
|
||||
---
|
||||
|
||||
### 15. 🟢 MCP tools are missing / not visible to the agent
|
||||
**Where:** Agent tool execution
|
||||
**Symptom:** MCP tools that should be available to the agent are not discovered or listed. The agent cannot see or use any MCP-provided tools during task execution.
|
||||
**Root cause:** `ClineCoreSession` in `cline-session-factory.ts` didn't pass MCP configuration through `coreConfig` when calling `host.start()`. The MCP settings file existed at `~/.cline/data/settings/cline_mcp_settings.json` but the session factory never wired MCP servers into the ClineCore session.
|
||||
**Fix:** Added `getOrCreateMcpManager()` to `cline-session-factory.ts` that reads MCP server registrations via `resolveMcpServerRegistrations()`, creates an `InMemoryMcpManager` with a client factory using `@modelcontextprotocol/sdk` (supporting stdio, streamableHttp, and SSE transports), connects to all non-disabled servers, generates `Tool[]` via `createMcpTools()`, and passes them as `extraTools` in `coreConfig`. The MCP manager is cached across sessions (servers are long-lived processes). Connection has a 30s timeout to avoid blocking session start. Individual server connection failures are logged but don't prevent other servers or the session from starting.
|
||||
**Verified:** Debug harness confirmed agent lists `kamibiki__kb_search`, `kamibiki__kb_status`, `kamibiki__kb_index` from the kamibiki MCP server, and successfully invoked `kamibiki__kb_status` returning real indexing data (6 repos, 1M+ embeddings).
|
||||
|
||||
---
|
||||
|
||||
### 27. 🟢 Banners (e.g., "Try Claude Sonnet 4.6") can't be dismissed
|
||||
**Where:** Home screen → banner carousel
|
||||
**Symptom:** Clicking the X dismiss button on any banner does nothing — the banner remains visible and reappears on reload.
|
||||
**Root cause:** Two issues: (1) `state-builder.ts` hardcoded `dismissedBanners: undefined` instead of reading from globalState, so dismissed banners were never communicated to the webview. (2) `grpc-handler.ts` wrote dismissed banner IDs as plain strings instead of the `{ bannerId, dismissedAt }` objects the webview expects.
|
||||
**Fix:** State builder now reads `dismissedBanners` from globalState with `normalizeDismissedBanners()` that handles both legacy plain strings and new objects. Handler writes proper `{ bannerId, dismissedAt }` objects and normalizes legacy entries on read.
|
||||
**Verified:** Debug harness confirmed banners dismiss correctly — carousel shrinks as each banner is dismissed and stays dismissed across reloads.
|
||||
|
||||
### 28. 🟢 Can't mark chats as favorites in history
|
||||
**Where:** History tab → star button on task items
|
||||
**Symptom:** Clicking the star icon on a history item does nothing — the favorite state never changes.
|
||||
**Root cause:** Proto field name mismatch in `handleToggleTaskFavorite()`: handler read `request.params?.id` and `request.params?.isFavorite`, but the webview sends `taskId` and `isFavorited` (proto field names from `TaskFavoriteRequest`).
|
||||
**Fix:** Handler now reads both proto names (`taskId`/`isFavorited`) with fallback to legacy names (`id`/`isFavorite`).
|
||||
|
||||
### 29. 🟢 Copy button obscured by last code block in chat
|
||||
**Where:** Chat response text with code blocks
|
||||
**Symptom:** The response copy button overlaps with the last code block, making it hard to see and click.
|
||||
**Root cause:** `CopyButton.tsx` positioned the bottom-right copy button at `bottom-1` (4px from bottom edge), which overlapped with code block content. No padding existed between the markdown content and the button.
|
||||
**Fix:** Changed position to `bottom-2.5` (10px clearance) and added `pb-4` padding to the chat text content wrapper for code block clearance.
|
||||
|
||||
### 30. 🟢 Current balance shows "----" / reload button does nothing
|
||||
**Where:** Account pane → credit balance display
|
||||
**Symptom:** The current balance always shows "----" and the reload button has no effect.
|
||||
**Root cause:** `getUserCredits` handler returned `{ credits: undefined }` instead of calling the Cline API. The webview reads `response.balance.currentBalance` which was always undefined.
|
||||
**Fix:** `getUserCredits` and `getOrganizationCredits` now fetch real balance data from the Cline API using the stored auth token (`Bearer` header). Includes 10s timeout and error handling.
|
||||
**Tested:** Integration tests with mock HTTP server verify real balance data flows through correctly (7 tests).
|
||||
|
||||
### 31. 🟢 Logout button does nothing
|
||||
**Where:** Account pane → logout button
|
||||
**Symptom:** Clicking logout has no effect — the user remains logged in.
|
||||
**Root cause:** `accountLogoutClicked` was a STUB (silent no-op) in grpc-handler.
|
||||
**Fix:** Implemented `handleAccountLogout()` that calls `LegacyStateReader.clearClineAuthInfo()` to remove `cline:clineAccountId` from secrets.json, then pushes state update so the webview shows the sign-in view.
|
||||
**Tested:** Integration test verifies credentials are cleared from disk and auth status shows unauthenticated after logout.
|
||||
|
||||
### 32. 🟢 Low credit balance persists after account switching
|
||||
**Where:** Account pane after switching organizations
|
||||
**Symptom:** After switching from a low-balance org to a high-balance org, the "Insufficient balance" error persists.
|
||||
**Root cause:** `setUserOrganization` was a STUB. The active org was never updated on disk, so credit queries always returned the same org's data.
|
||||
**Fix:** Implemented `handleSetUserOrganization()` that calls `LegacyStateReader.setActiveOrganization()` to update the `active` flag on orgs in stored credentials. Each credit fetch is a fresh API call keyed by org ID, so switching orgs correctly fetches the new org's balance.
|
||||
**Tested:** Integration test with mock server verifies: switch from low-balance org → high-balance org returns correct (high) balance, not stale (low) balance.
|
||||
|
||||
## Open Issues
|
||||
|
||||
---
|
||||
|
||||
### 26. 🟢 Clicking history items does not open them
|
||||
**Where:** History tab → clicking any task item; also RECENT section on home screen
|
||||
**Symptom:** History items display correctly in the History view, but clicking on them does nothing — the view stays on the History tab instead of navigating to the chat view with the loaded task.
|
||||
**Root cause:** `handleShowTaskWithId()` in `grpc-handler.ts` called `this.delegate.showTaskWithId(id)` to load the task data and push state, but never fired `this.onNavigateCallback?.("chat")` to tell the webview to navigate from the History view to the Chat view.
|
||||
**Fix:** Added `this.onNavigateCallback?.("chat")` after `showTaskWithId()` completes in `grpc-handler.ts`. This sends a typed `navigate` message to the webview, which triggers `navigateToChat()` — hiding the History view and revealing the Chat view with the loaded task.
|
||||
**Verified:** Debug harness confirmed clicking items in both the History tab and RECENT section on the home screen now navigates to the chat view with full message history loaded.
|
||||
|
||||
---
|
||||
|
||||
## Observations (not bugs, just notes)
|
||||
|
||||
### UI Rendering — Task Completion View
|
||||
The completed task view renders correctly:
|
||||
- Task header with cost badge (e.g. "$0.0072")
|
||||
- Token usage bar (e.g. "1.4k / 200.0k")
|
||||
- Response text displayed properly
|
||||
- "Task Completed" card with green checkmark and the result
|
||||
- "Start New Task" button appears below the chat
|
||||
- Input area changes to "Type a message..." (follow-up mode)
|
||||
|
||||
### Feature Card Carousel
|
||||
The home screen shows a rotating feature card carousel (1/4 through 4/4) promoting:
|
||||
- Claude Sonnet 4.6
|
||||
- MiniMax M2.5
|
||||
- ChatGPT integration
|
||||
- Jupyter Notebooks
|
||||
Each with a dismiss (X) button per-card.
|
||||
|
||||
### Model Selector
|
||||
Bottom bar correctly shows `cline:anthropic/claud...` (truncated) with Plan/Act toggle. Act mode is the default.
|
||||
|
||||
### Debug Harness: `ui.send_message` and `ui.react_input(submit:true)` Don't Start Tasks
|
||||
Both `ui.send_message` (gRPC postMessage) and `ui.react_input` with `submit:true` report success but don't actually start a new task—the webview stays on the home screen. The workaround is to use `ui.react_input` (without submit) to set the text, then dispatch a KeyboardEvent via `web.evaluate`:
|
||||
```
|
||||
curl -s localhost:19229/api -d '{"method":"ui.react_input","params":{"text":"your message","clear":true}}'
|
||||
curl -s localhost:19229/api -d '{"method":"web.evaluate","params":{"expression":"(() => { const ta = document.querySelector(\"textarea\"); ta.focus(); ta.dispatchEvent(new KeyboardEvent(\"keydown\",{key:\"Enter\",code:\"Enter\",keyCode:13,which:13,bubbles:true})); return \"ok\"; })()"}}'
|
||||
```
|
||||
|
||||
### Debug Harness Limitations (Fixed)
|
||||
All three limitations below have been addressed:
|
||||
|
||||
- ~~Programmatic textarea input doesn't reliably trigger React state updates after the first task.~~ **Fixed**: Two new commands added:
|
||||
- `ui.react_input` — Uses `document.execCommand('insertText')` which fires real InputEvents that React's onChange handler processes correctly, even after multiple tasks.
|
||||
- `ui.send_message` — Bypasses the textarea entirely by sending gRPC requests via `postMessage` directly to the extension host.
|
||||
- ~~The `web.evaluate` context can't access the VS Code API.~~ **Fixed**: The webview now exposes the VS Code API as `window.__clineVsCodeApi`, and a new `web.post_message` command lets the harness send arbitrary messages to the extension host through it.
|
||||
- ~~The `ui.locator` Playwright actions don't reliably target elements inside the webview iframe.~~ **Fixed**: `findSidebar()` now validates cached frame references (checking for both detached and stale frames), `getTarget()` accepts a `forceRefresh` flag, and `ui.locator` automatically retries with frame re-discovery when targeting sidebar elements.
|
||||
@@ -1,20 +1,18 @@
|
||||
<p align="center">
|
||||
<img src="assets/icons/icon.png" width="80" alt="Cline" />
|
||||
</p>
|
||||
<div align="center"><sub>
|
||||
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
|
||||
</sub></div>
|
||||
|
||||
<h1 align="center">Cline</h1>
|
||||
# Cline
|
||||
|
||||
<p align="center">
|
||||
The open source coding agent in your IDE and terminal.
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tbody>
|
||||
<td align="center">
|
||||
<a href="https://docs.cline.bot" target="_blank"><strong>Docs</strong></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Download on VS Marketplace</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
|
||||
@@ -26,209 +24,127 @@ The open source coding agent in your IDE and terminal.
|
||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://cline.bot/join-us" target="_blank"><strong>Join us!</strong></a>
|
||||
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
|
||||
|
||||
<br>
|
||||
Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="50%">
|
||||
1. Enter your task and add images to convert mockups into functional apps or fix bugs with screenshots.
|
||||
2. Cline starts by analyzing your file structure & source code ASTs, running regex searches, and reading relevant files to get up to speed in existing projects. By carefully managing what information is added to context, Cline can provide valuable assistance even for large, complex projects without overwhelming the context window.
|
||||
3. Once Cline has the information he needs, he can:
|
||||
- Create and edit files + monitor linter/compiler errors along the way, letting him proactively fix issues like missing imports and syntax errors on his own.
|
||||
- Execute commands directly in your terminal and monitor their output as he works, letting him e.g., react to dev server issues after editing a file.
|
||||
- For web development tasks, Cline can launch the site in a headless browser, click, type, scroll, and capture screenshots + console logs, allowing him to fix runtime errors and visual bugs.
|
||||
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
|
||||
|
||||
### CLI
|
||||
|
||||
Run Cline in your terminal.
|
||||
Interactive chat or fully headless
|
||||
for CI/CD and scripting.
|
||||
|
||||
```
|
||||
npm i -g cline
|
||||
```
|
||||
|
||||
<a href="./sdk/apps/cli/README.md">Learn more</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
<td align="center" width="50%">
|
||||
|
||||
### Kanban
|
||||
|
||||
Run many agents in parallel from a
|
||||
web-based task board. Each card gets its own
|
||||
worktree, auto-commit, and dependency chains.
|
||||
|
||||
```
|
||||
npm i -g kanban
|
||||
```
|
||||
|
||||
<a href="https://github.com/cline/kanban">Learn more</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="50%">
|
||||
|
||||
### VS Code Extension
|
||||
|
||||
AI coding assistant in your editor.
|
||||
Create files, run commands, browse the web,
|
||||
and use tools with human-in-the-loop approval.
|
||||
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev">Install from VS Marketplace</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
<td align="center" width="50%">
|
||||
|
||||
### JetBrains Plugin
|
||||
|
||||
The same Cline experience in IntelliJ IDEA,
|
||||
PyCharm, WebStorm, GoLand, and the rest of
|
||||
the JetBrains family.
|
||||
|
||||
<a href="https://plugins.jetbrains.com/plugin/28247-cline">Install from JetBrains Marketplace</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
|
||||
### SDK
|
||||
|
||||
Build your own AI agents and integrations powered by the same engine that runs the CLI, Kanban, VS Code extension, and JetBrains plugin. Custom tools, multi-agent teams, connectors, scheduled automations, and more.
|
||||
|
||||
```
|
||||
npm install @cline/sdk
|
||||
```
|
||||
|
||||
<a href="https://docs.cline.bot/cline-sdk/overview">Documentation</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
> [!TIP]
|
||||
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
|
||||
|
||||
---
|
||||
|
||||
## Index
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
||||
|
||||
| Product | Description | Location | CHANGELOG |
|
||||
|---------|------------|--------------|--------------|
|
||||
| **SDK** | Node.js programmatic agent API and extension exports. | [`sdk/`](https://github.com/cline/cline/tree/main/sdk) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/sdk/CHANGELOG.md) |
|
||||
| **CLI** | Terminal UI, headless mode, shell commands, and CLI-specific flows. | [`sdk/apps/cli/`](https://github.com/cline/cline/tree/main/sdk/apps/cli) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/sdk/apps/cli/CHANGELOG.md) |
|
||||
| **VS Code Extension** | The Marketplace extension and extension host integration. | [`/`](https://github.com/cline/cline/tree/main) (WIP migrating) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/CHANGELOG.md) |
|
||||
| **JetBrains Plugin** | JetBrains-hosted client that talks to the shared agent core. | Currently we are not open-sourcing JetBrains plugins | - |
|
||||
| **Kanban** | Web-based multi-agent task board. | [`cline/kanban`](https://github.com/cline/kanban) | [CHANGELOG.md](https://github.com/cline/kanban/blob/main/CHANGELOG.md) |
|
||||
| **Docs site** | Public documentation pages. | [`docs/`](https://docs.cline.bot/) | - |
|
||||
### Use any API and Model
|
||||
|
||||
## Edits Code Across Your Project
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
|
||||
Cline reads your project structure, understands the relationships between files, and makes coordinated changes across your codebase. It monitors linter and compiler errors as it works, fixing issues like missing imports, type mismatches, and syntax errors before you even see them. In VS Code and JetBrains, every edit shows up as a diff you can review, modify, or revert. All changes are tracked with checkpoints, so you can easily undo the agent's work.
|
||||
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
||||
|
||||
## Runs Bash Commands
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
Cline executes commands directly in your terminal and watches the output in real time. Install packages, run build scripts, execute tests, deploy applications, manage databases. For long-running processes like dev servers, Cline continues working in the background and reacts to new output as it appears, catching compile errors, test failures, and server crashes as they happen.
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Plan and Act
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
|
||||
|
||||
Toggle between Plan mode and Act mode. In Plan mode, Cline explores your codebase, asks clarifying questions, and lays out a strategy. Once you're aligned, switch to Act mode and Cline executes the plan. Every file edit and terminal command requires your approval, so you stay in control of what actually changes. Or toggle auto-approve and let Cline run autonomously.
|
||||
### Run Commands in Terminal
|
||||
|
||||
## Rules and Skills
|
||||
Thanks to the new [shell integration updates in VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), Cline can execute commands directly in your terminal and receive the output. This allows him to perform a wide range of tasks, from installing packages and running build scripts to deploying applications, managing databases, and executing tests, all while adapting to your dev environment & toolchain to get the job done right.
|
||||
|
||||
Define project-specific rules in `.clinerules` files that guide how Cline works in your codebase: coding standards, architecture conventions, deployment procedures, testing requirements. Rules are picked up automatically by the CLI, VS Code extension, and JetBrains plugin. Use skills to let the model load specific rules when needed.
|
||||
For long running processes like dev servers, use the "Proceed While Running" button to let Cline continue in the task while the command runs in the background. As Cline works he’ll be notified of any new terminal output along the way, letting him react to issues that may come up, such as compile-time errors when editing files.
|
||||
|
||||
## Works With Every Model
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
Cline is not locked to a single AI provider. Use whichever model fits your workflow:
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
| Provider | Models |
|
||||
|----------|--------|
|
||||
| Anthropic | Claude Opus, Sonnet, Haiku |
|
||||
| OpenAI | GPT series model |
|
||||
| Google | Gemini series model |
|
||||
| OpenRouter | 200+ models from any provider |
|
||||
| Vercel AI Gateway | Models through Vercel AI Gateway |
|
||||
| AWS Bedrock | Claude, Llama, and more |
|
||||
| Azure / GCP Vertex | All hosted models |
|
||||
| Cerebras / Groq | Fast inference models |
|
||||
| Ollama / LM Studio | Run local models on your machine |
|
||||
| Any OpenAI-compatible API | Self-hosted or third-party endpoints |
|
||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
|
||||
|
||||
## Extend With Plugins or MCP Servers
|
||||
### Create and Edit Files
|
||||
|
||||
Extend Cline's capabilities with plugins. Using the SDK, register tools and lifecycle hooks programmatically through the plugin system for logging, auditing, policy enforcement, or adding domain-specific capabilities. Simple plugin example below.
|
||||
Cline can create and edit files directly in your editor, presenting you a diff view of the changes. You can edit or revert Cline's changes directly in the diff view editor, or provide feedback in chat until you're satisfied with the result. Cline also monitors linter/compiler errors (missing imports, syntax errors, etc.) so he can fix issues that come up along the way on his own.
|
||||
|
||||
```typescript
|
||||
import { Agent, createTool } from "@cline/sdk"
|
||||
All changes made by Cline are recorded in your file's Timeline, providing an easy way to track and revert modifications if needed.
|
||||
|
||||
const deployTool = createTool({
|
||||
name: "deploy",
|
||||
description: "Deploy the current branch to staging.",
|
||||
inputSchema: { type: "object", properties: { env: { type: "string" } }, required: ["env"] },
|
||||
execute: async (input) => {
|
||||
// your deployment logic
|
||||
},
|
||||
})
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
const agent = new Agent({ tools: [deployTool], /* ... */ })
|
||||
```
|
||||
...or use [MCP servers](https://github.com/modelcontextprotocol) to connect to databases, query APIs, manage cloud infrastructure, and interact with external systems. Use [community-built servers](https://github.com/modelcontextprotocol/servers) or ask Cline to create custom tools on the fly. In the CLI, manage servers with `cline mcp`.
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Multi-Agent Teams
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
|
||||
|
||||
Coordinate multiple agents working together on complex tasks. A coordinator agent breaks the work into subtasks and delegates to specialist agents, each with their own tools and context. Team state persists across sessions so you can pick up where you left off.
|
||||
### Use the Browser
|
||||
|
||||
```bash
|
||||
cline --team-name auth-sprint "Plan and implement user authentication with tests"
|
||||
```
|
||||
With Claude Sonnet's new [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) capability, Cline can launch a browser, click elements, type text, and scroll, capturing screenshots and console logs at each step. This allows for interactive debugging, end-to-end testing, and even general web use! This gives him autonomy to fixing visual bugs and runtime issues without you needing to handhold and copy-pasting error logs yourself.
|
||||
|
||||
## Scheduled Agents
|
||||
Try asking Cline to "test the app", and watch as he runs a command like `npm run dev`, launches your locally running dev server in a browser, and performs a series of tests to confirm that everything works. [See a demo here.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
Run agents on cron schedules for recurring automations. Daily PR summaries, weekly dependency checks, codebase health reports. Schedules persist across restarts and run independently of any terminal session.
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
```bash
|
||||
cline schedule create "PR summary" \
|
||||
--cron "0 9 * * MON-FRI" \
|
||||
--prompt "List all open PRs and their review status" \
|
||||
--workspace /path/to/repo
|
||||
```
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Connect to Slack, Telegram, Discord, and More
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
|
||||
|
||||
Chat with your agent from any messaging platform: Telegram, Slack, Discord, Google Chat, WhatsApp, and Linear. Each conversation thread maps to an agent session with full context. Set up access control to restrict who can interact with your agent.
|
||||
### "add a tool that..."
|
||||
|
||||
```bash
|
||||
cline connect telegram -k $BOT_TOKEN
|
||||
cline connect slack --token $SLACK_TOKEN --signing-secret $SECRET --base-url $URL
|
||||
```
|
||||
Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol), Cline can extend his capabilities through custom tools. While you can use [community-made servers](https://github.com/modelcontextprotocol/servers), Cline can instead create and install tools tailored to your specific workflow. Just ask Cline to "add a tool" and he will handle everything, from creating a new MCP server to installing it into the extension. These custom tools then become part of Cline's toolkit, ready to use in future tasks.
|
||||
|
||||
## Headless CLI for CI/CD
|
||||
- "add a tool that fetches Jira tickets": Retrieve ticket ACs and put Cline to work
|
||||
- "add a tool that manages AWS EC2s": Check server metrics and scale instances up or down
|
||||
- "add a tool that pulls the latest PagerDuty incidents": Fetch details and ask Cline to fix bugs
|
||||
|
||||
Run Cline with zero interaction for scripting and automation. Pipe input, get JSON output, chain commands, integrate into CI/CD pipelines.
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
```bash
|
||||
cline "Run tests and fix any failures"
|
||||
git diff origin/main | cline "Review these changes for issues"
|
||||
cline --json "List all TODO comments" | jq -r 'select(.type == "agent_event" and .event.text) | .event.text'
|
||||
```
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
|
||||
|
||||
### Add Context
|
||||
|
||||
**`@url`:** Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Cline the latest docs
|
||||
|
||||
**`@problems`:** Add workspace errors and warnings ('Problems' panel) for Cline to fix
|
||||
|
||||
**`@file`:** Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
|
||||
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
|
||||
|
||||
### Checkpoints: Compare and Restore
|
||||
|
||||
As Cline works through a task, the extension can take an internal snapshot of your workspace at each step. You can use the 'Compare' button to see a diff between the snapshot and your current workspace, and the 'Restore' button to roll back to that point.
|
||||
|
||||
For example, when working with a local web server, you can use 'Restore Workspace Only' to quickly test different versions of your app, then use 'Restore Task and Workspace' when you find the version you want to continue building from. This lets you safely explore different approaches without losing progress while the current checkpoint system is available.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Contributing
|
||||
|
||||
Start with the [Contributing Guide](CONTRIBUTING.md). Join our [Discord](https://discord.gg/cline) and head to the `#contributors` channel to connect with other contributors. Check our [careers page](https://cline.bot/join-us) for full-time roles.
|
||||
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
|
||||
|
||||
## Enterprise
|
||||
|
||||
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+436
@@ -0,0 +1,436 @@
|
||||
# Cline SDK — MCP Server Management Reference
|
||||
|
||||
This document describes how the Cline SDK handles MCP (Model Context Protocol) server lifecycle, configuration, and what is — and isn't — exposed for client integration. It is intended for client developers building UI around MCP server management.
|
||||
|
||||
---
|
||||
|
||||
## Summary: The Claim That "The SDK Lacks Hooks" Is Wrong
|
||||
|
||||
The SDK **does** provide a full MCP manager with lifecycle operations. The `McpManager` interface in `@clinebot/core` supports:
|
||||
|
||||
- `registerServer()` / `unregisterServer()` — add or remove servers
|
||||
- `connectServer()` / `disconnectServer()` — start or stop connections
|
||||
- `setServerDisabled()` — toggle enable/disable
|
||||
- `listServers()` — get snapshots of all servers with status
|
||||
- `refreshTools()` — force-refresh tool lists from a server
|
||||
- `dispose()` — shut down all servers
|
||||
|
||||
The **actual gap** is narrower: there is no built-in file-watcher that auto-reloads `cline_mcp_settings.json` when it changes, and the RPC layer (`@clinebot/rpc`) does not currently expose MCP management endpoints. This means clients that manage MCP settings through the settings file must bridge the gap between file edits and runtime state themselves.
|
||||
|
||||
---
|
||||
|
||||
## SDK Architecture for MCP
|
||||
|
||||
### Layer 1: Settings File (`cline_mcp_settings.json`)
|
||||
|
||||
The SDK reads MCP server configuration from a JSON settings file:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"docs": {
|
||||
"transport": { "type": "stdio", "command": "node", "args": ["./mcp.js"] }
|
||||
},
|
||||
"remote": {
|
||||
"transport": { "type": "streamableHttp", "url": "https://mcp.example.com" },
|
||||
"disabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Location resolution** (`resolveDefaultMcpSettingsPath()`):
|
||||
- `CLINE_MCP_SETTINGS_PATH` env var (if set)
|
||||
- Otherwise defaults to the platform-specific Cline data directory
|
||||
|
||||
**SDK utilities** for this file (all exported from `@clinebot/core`):
|
||||
- `resolveDefaultMcpSettingsPath()` — get the path
|
||||
- `hasMcpSettingsFile()` — check if it exists
|
||||
- `loadMcpSettingsFile()` — parse and validate with Zod
|
||||
- `resolveMcpServerRegistrations()` — parse file → `McpServerRegistration[]`
|
||||
- `registerMcpServersFromSettingsFile(manager)` — parse file and register all servers into a manager
|
||||
|
||||
### Layer 2: McpManager (`InMemoryMcpManager`)
|
||||
|
||||
`packages/core/src/extensions/mcp/manager.ts` — the runtime MCP lifecycle manager.
|
||||
|
||||
```typescript
|
||||
interface McpManager extends McpToolProvider {
|
||||
registerServer(registration: McpServerRegistration): Promise<void>;
|
||||
unregisterServer(serverName: string): Promise<void>;
|
||||
connectServer(serverName: string): Promise<void>;
|
||||
disconnectServer(serverName: string): Promise<void>;
|
||||
setServerDisabled(serverName: string, disabled: boolean): Promise<void>;
|
||||
listServers(): readonly McpServerSnapshot[];
|
||||
refreshTools(serverName: string): Promise<readonly McpToolDescriptor[]>;
|
||||
callTool(request: McpToolCallRequest): Promise<McpToolCallResult>;
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
Key behaviors:
|
||||
- **Lazy connection**: Servers are registered in disconnected state; connection happens on first `listTools()` or `callTool()`.
|
||||
- **Transport change detection**: If `registerServer()` is called with a changed transport config, the existing connection is torn down and the client is recreated.
|
||||
- **Exclusive locking**: Per-server operation locks prevent concurrent connect/disconnect races.
|
||||
- **Tool caching**: `listTools()` caches results for `toolsCacheTtlMs` (default 5 seconds). Use `refreshTools()` to force a refresh.
|
||||
- **Disable = disconnect**: Calling `setServerDisabled(name, true)` immediately disconnects the server.
|
||||
|
||||
### Layer 3: McpServerClient (Transport Layer)
|
||||
|
||||
`packages/core/src/extensions/mcp/client.ts` — the actual MCP protocol client.
|
||||
|
||||
The default factory (`createDefaultMcpServerClientFactory()`) creates `StdioMcpClient` instances:
|
||||
- Spawns child processes for `stdio` transport
|
||||
- Implements MCP JSON-RPC protocol (both newline-delimited and framed modes)
|
||||
- Auto-negotiates protocol mode by trying newline first, then framed
|
||||
- Protocol version: `2024-11-05`
|
||||
- Connect timeout: 1.5s, request timeout: 5s
|
||||
|
||||
```typescript
|
||||
interface McpServerClient {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
listTools(): Promise<readonly McpToolDescriptor[]>;
|
||||
callTool(request: { name: string; arguments?: Record<string, unknown> }): Promise<McpToolCallResult>;
|
||||
}
|
||||
|
||||
type McpServerClientFactory = (registration: McpServerRegistration) => Promise<McpServerClient> | McpServerClient;
|
||||
```
|
||||
|
||||
**Transport types supported**:
|
||||
| Type | Status |
|
||||
|---|---|
|
||||
| `stdio` | ✅ Fully implemented in `StdioMcpClient` |
|
||||
| `sse` | ⚠️ Type defined, but no built-in client (factory only creates `StdioMcpClient`) |
|
||||
| `streamableHttp` | ⚠️ Type defined, but no built-in client |
|
||||
|
||||
Clients needing SSE or StreamableHTTP support must provide a custom `McpServerClientFactory`.
|
||||
|
||||
### Layer 4: Tool Bridge
|
||||
|
||||
`packages/core/src/extensions/mcp/tools.ts` via `createMcpTools()` — converts MCP server tools into the SDK's `Tool` type for use in the agent loop.
|
||||
|
||||
```typescript
|
||||
interface CreateMcpToolsOptions {
|
||||
serverName: string;
|
||||
provider: McpToolProvider; // Usually the McpManager
|
||||
nameTransform?: McpToolNameTransform;
|
||||
timeoutMs?: number;
|
||||
retryable?: boolean;
|
||||
maxRetries?: number;
|
||||
}
|
||||
```
|
||||
|
||||
Default name transform: `{serverName}__{toolName}` (e.g. `docs__search`).
|
||||
|
||||
---
|
||||
|
||||
## How the Runtime Builder Uses MCP
|
||||
|
||||
`packages/core/src/runtime/runtime-builder.ts` → `loadConfiguredMcpTools()`:
|
||||
|
||||
1. Resolves the MCP settings file path
|
||||
2. Creates a fresh `InMemoryMcpManager`
|
||||
3. Calls `registerMcpServersFromSettingsFile()` to load all servers
|
||||
4. Creates `Tool[]` via `createMcpTools()` for each non-disabled server
|
||||
5. Returns the tools + a `shutdown()` callback that calls `manager.dispose()`
|
||||
|
||||
**Critical limitation**: This is done once at session build time. There is **no file watcher** that reloads MCP settings when they change during a session. If the settings file is edited mid-session, the running session won't see the changes.
|
||||
|
||||
---
|
||||
|
||||
## How Existing Apps Handle MCP Settings
|
||||
|
||||
### Tauri Apps (`apps/code`, `apps/desktop`)
|
||||
|
||||
Both Tauri apps implement MCP settings management **entirely in Rust** at the Tauri command level, bypassing the SDK's McpManager:
|
||||
|
||||
```rust
|
||||
// apps/code/src-tauri/src/main.rs (identical pattern in apps/desktop)
|
||||
fn list_mcp_servers() -> Result<McpServersResponse, String>
|
||||
fn set_mcp_server_disabled(name, disabled) -> Result<McpServersResponse, String>
|
||||
fn upsert_mcp_server(input) -> Result<McpServersResponse, String>
|
||||
fn delete_mcp_server(name) -> Result<McpServersResponse, String>
|
||||
```
|
||||
|
||||
These commands:
|
||||
- Read/write `cline_mcp_settings.json` directly
|
||||
- Return the full server list after each mutation
|
||||
- Do **not** interact with any running `McpManager` instance
|
||||
|
||||
The frontend (`apps/code/components/views/settings/mcp-view.tsx`) calls these Tauri commands:
|
||||
- `list_mcp_servers` — refresh the displayed server list
|
||||
- `set_mcp_server_disabled` — toggle enable/disable
|
||||
- `upsert_mcp_server` — add or edit a server
|
||||
- `delete_mcp_server` — remove a server
|
||||
|
||||
### Node.js Host (`apps/code/host/commands.ts`)
|
||||
|
||||
The Code app's Node.js host also implements MCP CRUD directly:
|
||||
```typescript
|
||||
// Direct file reads/writes, not using McpManager
|
||||
function readMcpServersResponse(): JsonRecord // reads cline_mcp_settings.json
|
||||
function writeMcpServersMap(servers: JsonRecord) // writes cline_mcp_settings.json
|
||||
function ensureMcpSettingsFile(): string // ensures file exists
|
||||
```
|
||||
|
||||
Commands: `list_mcp_servers`, `set_mcp_server_disabled`, `upsert_mcp_server`, `delete_mcp_server`, `ensure_mcp_settings_file`
|
||||
|
||||
### CLI (`apps/cli`)
|
||||
|
||||
The CLI has `clite config mcp` / `clite list mcp` for listing configured MCP servers. It uses the SDK's `resolveMcpServerRegistrations()` to read the settings file.
|
||||
|
||||
---
|
||||
|
||||
## What the SDK DOES Expose (Exported from `@clinebot/core`)
|
||||
|
||||
### Full Type & Implementation Exports
|
||||
|
||||
```typescript
|
||||
// Manager
|
||||
export { InMemoryMcpManager } from "./extensions/mcp";
|
||||
export type { McpManager, McpManagerOptions } from "./extensions/mcp";
|
||||
|
||||
// Client factory
|
||||
export { createDefaultMcpServerClientFactory } from "./extensions/mcp";
|
||||
export type { McpServerClient, McpServerClientFactory } from "./extensions/mcp";
|
||||
|
||||
// Config loading
|
||||
export { hasMcpSettingsFile, loadMcpSettingsFile, registerMcpServersFromSettingsFile,
|
||||
resolveDefaultMcpSettingsPath, resolveMcpServerRegistrations } from "./extensions/mcp";
|
||||
export type { LoadMcpSettingsOptions, McpSettingsFile, RegisterMcpServersFromSettingsOptions } from "./extensions/mcp";
|
||||
|
||||
// Types
|
||||
export type { McpServerRegistration, McpServerSnapshot, McpConnectionStatus,
|
||||
McpServerTransportConfig, McpStdioTransportConfig, McpSseTransportConfig,
|
||||
McpStreamableHttpTransportConfig } from "./extensions/mcp";
|
||||
|
||||
// Tool bridge
|
||||
export { createMcpTools } from "./extensions/mcp";
|
||||
export type { CreateMcpToolsOptions, McpToolCallRequest, McpToolCallResult,
|
||||
McpToolDescriptor, McpToolNameTransform, McpToolProvider } from "./extensions/mcp";
|
||||
|
||||
// Policies
|
||||
export { createDisabledMcpToolPolicies, createDisabledMcpToolPolicy } from "./extensions/mcp";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's Missing / The Actual Gaps
|
||||
|
||||
### 1. No RPC Endpoints for MCP Management
|
||||
|
||||
`packages/rpc/src` has **zero** MCP-related code. There are no gRPC/RPC methods for:
|
||||
- Listing MCP servers
|
||||
- Registering/unregistering servers
|
||||
- Connecting/disconnecting servers
|
||||
- Toggling server disabled state
|
||||
- Refreshing tools
|
||||
|
||||
This means RPC-backed clients cannot manage MCP through the RPC layer.
|
||||
|
||||
### 2. No Settings File Watcher
|
||||
|
||||
The SDK has a `UnifiedConfigFileWatcher` for agents, skills, rules, and workflows — but **not for MCP settings**. When another client edits `cline_mcp_settings.json`, running sessions don't see the change.
|
||||
|
||||
### 3. No Live Manager Exposure to Clients
|
||||
|
||||
The runtime builder creates an `InMemoryMcpManager` internally during `loadConfiguredMcpTools()`, but it's encapsulated — only the resulting `Tool[]` and a `shutdown()` callback are returned. The manager itself is not exposed to the caller, so clients can't call `connectServer()`, `disconnectServer()`, etc. on a running session's MCP manager.
|
||||
|
||||
### 4. SSE/StreamableHTTP Client Not Implemented
|
||||
|
||||
The transport types are defined, but the default client factory only produces `StdioMcpClient`. Clients needing SSE or StreamableHTTP must provide their own `McpServerClientFactory`.
|
||||
|
||||
---
|
||||
|
||||
## What a Client Needs to Do Today
|
||||
|
||||
To implement full MCP server management UI:
|
||||
|
||||
### Settings CRUD (Works Now)
|
||||
Read and write `cline_mcp_settings.json` directly. The SDK provides:
|
||||
- `resolveDefaultMcpSettingsPath()` — find the file
|
||||
- `loadMcpSettingsFile()` — parse it
|
||||
- Write it yourself (it's just JSON with `{ mcpServers: { ... } }`)
|
||||
|
||||
This is exactly what the Tauri apps and Node.js host do today.
|
||||
|
||||
### Runtime Lifecycle (Partial)
|
||||
For a new session, MCP tools are automatically loaded from the settings file by the runtime builder.
|
||||
|
||||
For mid-session changes (restart, delete, toggle), clients currently have two options:
|
||||
1. **Edit the settings file and restart the session** — the next session build will pick up the changes
|
||||
2. **Create and manage your own `InMemoryMcpManager`** — the SDK exports everything needed:
|
||||
```typescript
|
||||
const manager = new InMemoryMcpManager({
|
||||
clientFactory: createDefaultMcpServerClientFactory(),
|
||||
});
|
||||
await manager.registerServer({ name: "docs", transport: { type: "stdio", command: "node", args: ["./mcp.js"] } });
|
||||
await manager.connectServer("docs");
|
||||
const tools = await manager.listTools("docs");
|
||||
await manager.disconnectServer("docs");
|
||||
await manager.unregisterServer("docs");
|
||||
```
|
||||
|
||||
### Cross-Client Sync (Not Built)
|
||||
If multiple clients share the same settings file, there is no notification mechanism. Clients would need their own file watcher (e.g. `fs.watch()` / `chokidar`) on `cline_mcp_settings.json`.
|
||||
|
||||
---
|
||||
|
||||
## How MCP Tools Become Visible to the Agent (and the Client)
|
||||
|
||||
### MCP Tools Are Injected as Regular SDK Tools
|
||||
|
||||
The `createMcpTools()` function converts each MCP tool descriptor into a standard `Tool` object (from `@clinebot/shared`). These tools are **indistinguishable** from built-in tools once created — they have a `name`, `description`, `inputSchema`, and an `execute` function.
|
||||
|
||||
```typescript
|
||||
// packages/core/src/extensions/mcp/tools.ts
|
||||
export async function createMcpTools(options: CreateMcpToolsOptions): Promise<Tool[]> {
|
||||
const descriptors = await options.provider.listTools(options.serverName);
|
||||
return descriptors.map((descriptor) => createTool({
|
||||
name: nameTransform({ serverName, toolName: descriptor.name }), // e.g. "docs__search"
|
||||
description: descriptor.description || `Execute MCP tool "${descriptor.name}" from server "${serverName}".`,
|
||||
inputSchema: descriptor.inputSchema,
|
||||
execute: async (input, context) => options.provider.callTool({
|
||||
serverName, toolName: descriptor.name, arguments: input, context,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### The Runtime Builder Merges MCP Tools with Built-in Tools
|
||||
|
||||
In `DefaultRuntimeBuilder.build()` (line ~460-476):
|
||||
```typescript
|
||||
if (normalized.enableTools) {
|
||||
tools.push(...createBuiltinToolsList(...)); // SDK built-in tools
|
||||
const mcpRuntime = await loadConfiguredMcpTools(); // MCP tools
|
||||
tools.push(...mcpRuntime.tools);
|
||||
mcpShutdown = mcpRuntime.shutdown;
|
||||
}
|
||||
```
|
||||
|
||||
The resulting `tools: Tool[]` array — containing **both** built-in and MCP tools — is returned in the `BuiltRuntime`:
|
||||
```typescript
|
||||
interface BuiltRuntime {
|
||||
tools: Tool[]; // ← includes MCP tools
|
||||
hooks?: AgentHooks;
|
||||
shutdown: (reason: string) => Promise<void> | void;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### The Agent Receives All Tools (Including MCP) Uniformly
|
||||
|
||||
The `Agent` (from `@clinebot/agents`) receives the merged `tools` array. It doesn't know or care which tools came from MCP vs built-in. The LLM sees all tools in its tool definitions and can call any of them.
|
||||
|
||||
### Client Control Over MCP Tools
|
||||
|
||||
Clients **can** control MCP tools through the same mechanisms they use for any tool:
|
||||
|
||||
1. **Tool Policies** — Enable/disable or require approval per tool name:
|
||||
```typescript
|
||||
toolPolicies: {
|
||||
"docs__search": { enabled: true, autoApprove: true },
|
||||
"docs__write": { enabled: true, autoApprove: false }, // requires approval
|
||||
"risky__delete": { enabled: false }, // completely disabled
|
||||
}
|
||||
```
|
||||
|
||||
2. **MCP-specific disable policies** — The SDK provides helpers to disable all tools from a specific MCP server:
|
||||
```typescript
|
||||
import { createDisabledMcpToolPolicies } from "@clinebot/core";
|
||||
const policies = createDisabledMcpToolPolicies({
|
||||
serverName: "risky-server",
|
||||
toolNames: ["delete", "modify", "drop"],
|
||||
});
|
||||
// → { "risky-server__delete": { enabled: false }, ... }
|
||||
```
|
||||
|
||||
3. **CLI flags** — `--tool-enable <name>` and `--tool-disable <name>` work for MCP tools too (they operate on the transformed name like `docs__search`).
|
||||
|
||||
4. **Tool approval callback** — When `autoApprove: false`, the agent calls `requestToolApproval()` before executing the tool, giving the client a chance to approve/reject each call.
|
||||
|
||||
5. **`enableTools: false`** — Disables ALL tools including MCP.
|
||||
|
||||
### What Clients Can See
|
||||
|
||||
The `BuiltRuntime.tools` array is visible to the caller of `runtimeBuilder.build()`. The session manager and host apps can inspect it to know exactly which tools (including MCP tools) are available.
|
||||
|
||||
MCP tools follow the naming convention `{serverName}__{toolName}` by default, so clients can identify which tools came from which MCP server by parsing the name prefix.
|
||||
|
||||
---
|
||||
|
||||
## Remote MCP Servers (StreamableHTTP / SSE)
|
||||
|
||||
### The SDK's Design Intent
|
||||
|
||||
The SDK clearly **intends** to support remote MCP servers. The evidence:
|
||||
|
||||
1. **Transport types are fully defined and validated**:
|
||||
```typescript
|
||||
interface McpStreamableHttpTransportConfig {
|
||||
type: "streamableHttp";
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
interface McpSseTransportConfig {
|
||||
type: "sse";
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Config loader validates all three transports** — The Zod schemas accept `stdio`, `sse`, and `streamableHttp` equally. Legacy formats (`url` without explicit type) default to `sse`; `transportType: "http"` maps to `streamableHttp`.
|
||||
|
||||
3. **Manager is transport-agnostic** — The `McpManager` uses `McpServerClientFactory` to create clients. It doesn't care about transport type; that's the factory's job.
|
||||
|
||||
4. **Tests use `streamableHttp` registrations** — The manager test suite registers servers with `transport: { type: "streamableHttp", url: "https://mcp.example.test" }` and they work fine (with a mock client factory).
|
||||
|
||||
5. **Settings file and UI accept all transports** — Both Tauri apps and the Code app UI offer `stdio`, `sse`, and `streamableHttp` as choices.
|
||||
|
||||
### What's Actually Implemented vs Not
|
||||
|
||||
| Concern | Status |
|
||||
|---|---|
|
||||
| Transport type definitions | ✅ Complete |
|
||||
| Settings file parsing for all transports | ✅ Complete |
|
||||
| Settings file CRUD (UI/Tauri/CLI) for all transports | ✅ Complete |
|
||||
| `McpManager` lifecycle for all transports | ✅ Complete (transport-agnostic) |
|
||||
| `StdioMcpClient` (spawns child process) | ✅ Complete |
|
||||
| HTTP/SSE client (connects to remote URL) | ❌ Not in default factory |
|
||||
|
||||
### The Gap for Remote Servers
|
||||
|
||||
The **only** thing missing is that `createDefaultMcpServerClientFactory()` returns a `StdioMcpClient` unconditionally — it doesn't check `registration.transport.type` and will fail for `sse` or `streamableHttp` transports.
|
||||
|
||||
A client can fix this by providing a custom factory:
|
||||
```typescript
|
||||
const manager = new InMemoryMcpManager({
|
||||
clientFactory: async (registration) => {
|
||||
if (registration.transport.type === "stdio") {
|
||||
return createDefaultMcpServerClientFactory()(registration);
|
||||
}
|
||||
if (registration.transport.type === "streamableHttp") {
|
||||
return new MyStreamableHttpMcpClient(registration);
|
||||
}
|
||||
if (registration.transport.type === "sse") {
|
||||
return new MySseMcpClient(registration);
|
||||
}
|
||||
throw new Error(`Unsupported transport: ${registration.transport.type}`);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The client just needs to implement the `McpServerClient` interface (4 methods: `connect`, `disconnect`, `listTools`, `callTool`).
|
||||
|
||||
---
|
||||
|
||||
## Recommendation for Improvement
|
||||
|
||||
To close the gap, the SDK could:
|
||||
|
||||
1. **Add MCP settings to the config watcher system** — create an `McpConfigDefinition` for `UnifiedConfigFileWatcher` to auto-detect changes to `cline_mcp_settings.json`
|
||||
2. **Expose the McpManager from the runtime builder** — return it alongside the tool list so clients can call lifecycle methods
|
||||
3. **Add MCP management to the RPC layer** — implement gRPC methods that proxy to `McpManager`
|
||||
4. **Implement SSE/StreamableHTTP clients** — extend the default client factory
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
# Cline SDK — Provider Credentials & OAuth Reference
|
||||
|
||||
This document describes how the Cline SDK publishes inference provider metadata, handles credential resolution, and orchestrates OAuth authentication flows. It is intended for client developers integrating with `@clinebot/llms` and `@clinebot/core`.
|
||||
|
||||
---
|
||||
|
||||
## Provider Catalog
|
||||
|
||||
The SDK owns the canonical list of inference providers. It is **not** produced by clients.
|
||||
|
||||
### Where Providers Are Defined
|
||||
|
||||
| Layer | Location | What It Owns |
|
||||
|---|---|---|
|
||||
| `@clinebot/llms` | `packages/llms/src/gateway/builtins.ts` | `BUILTIN_SPECS` array — every built-in provider's `id`, `name`, `description`, `family`, `capabilities`, `apiKeyEnv`, `defaultModelId`, default `baseUrl` |
|
||||
| `@clinebot/llms` | `packages/llms/src/provider/ids.ts` | `BUILT_IN_PROVIDER` enum and `BUILT_IN_PROVIDER_IDS` array |
|
||||
| `@clinebot/shared` | `packages/shared/src/llms/gateway.ts` | `GatewayProviderManifest` type — the runtime shape clients receive |
|
||||
| `@clinebot/llms` | `packages/llms/src/gateway/provider-keys.ts` | Mapping from external `modelsDevKey` identifiers to runtime/generated provider IDs |
|
||||
|
||||
### BuiltinSpec Shape
|
||||
|
||||
Each built-in provider is declared as a `BuiltinSpec`:
|
||||
|
||||
```typescript
|
||||
interface BuiltinSpec {
|
||||
id: string; // e.g. "anthropic", "openai-native", "cline"
|
||||
name: string; // Human-readable name
|
||||
description: string;
|
||||
family: ProviderFamily; // Protocol family: "openai-compatible", "anthropic", "google", etc.
|
||||
protocol?: ProviderProtocol;
|
||||
client?: ProviderClient;
|
||||
capabilities?: ProviderCapability[]; // "reasoning" | "prompt-cache" | "tools" | "oauth" | "temperature" | "files"
|
||||
modelsProviderId?: string;
|
||||
defaultModelId?: string;
|
||||
modelsFactory?: () => Record<string, ModelInfo>;
|
||||
env?: readonly ("browser" | "node")[];
|
||||
apiKeyEnv?: readonly string[]; // Environment variable names for API key resolution
|
||||
docsUrl?: string;
|
||||
defaults?: GatewayProviderSettings; // Includes default baseUrl
|
||||
}
|
||||
```
|
||||
|
||||
### GatewayProviderManifest (Runtime Shape)
|
||||
|
||||
`toManifest()` converts a `BuiltinSpec` into a `GatewayProviderManifest`, which is what clients interact with at runtime:
|
||||
|
||||
```typescript
|
||||
interface GatewayProviderManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
defaultModelId: string;
|
||||
models: readonly GatewayModelDefinition[];
|
||||
env?: readonly ("browser" | "node")[];
|
||||
api?: string; // Default base URL
|
||||
apiKeyEnv?: readonly string[]; // Env var names for credential resolution
|
||||
docsUrl?: string;
|
||||
metadata?: Record<string, JsonValue | undefined>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Credential Resolution
|
||||
|
||||
### API Key Resolution Order
|
||||
|
||||
The SDK resolves credentials in `packages/llms/src/gateway/http.ts` via `resolveApiKey()`:
|
||||
|
||||
1. **Explicit `apiKey`** — passed directly in provider config
|
||||
2. **`apiKeyResolver()`** — async callback (e.g. fetch from keychain)
|
||||
3. **`apiKeyEnv`** — iterate environment variable names from the provider manifest; first non-empty value wins
|
||||
|
||||
If all fail, `getMissingApiKeyError()` produces a message naming the expected env vars:
|
||||
> `Missing API key for provider "anthropic". Set apiKey explicitly or one of: ANTHROPIC_API_KEY.`
|
||||
|
||||
### Per-Provider Credential Metadata Examples
|
||||
|
||||
| Provider | `apiKeyEnv` |
|
||||
|---|---|
|
||||
| `anthropic` | `["ANTHROPIC_API_KEY"]` |
|
||||
| `openai-native` | `["OPENAI_API_KEY"]` |
|
||||
| `gemini` | `["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"]` |
|
||||
| `vertex` | `["GCP_PROJECT_ID", "GOOGLE_CLOUD_PROJECT", "GOOGLE_APPLICATION_CREDENTIALS", "GEMINI_API_KEY", "GOOGLE_API_KEY"]` |
|
||||
| `bedrock` | `["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]` |
|
||||
| `sapaicore` | `["AICORE_SERVICE_KEY", "VCAP_SERVICES"]` |
|
||||
| `cline` | `["CLINE_API_KEY"]` |
|
||||
| `openrouter` | `["OPENROUTER_API_KEY"]` |
|
||||
| `deepseek` | `["DEEPSEEK_API_KEY"]` |
|
||||
|
||||
Most OpenAI-compatible providers follow the pattern `["{PROVIDER}_API_KEY"]`.
|
||||
|
||||
### Provider-Specific Settings
|
||||
|
||||
Some providers need additional configuration beyond an API key:
|
||||
|
||||
- **Vertex/GCP**: `gcpProjectId`, `gcpRegion`
|
||||
- **Bedrock/AWS**: `awsAuthentication` (`"iam" | "api-key" | "profile"`), `awsRegion`, `awsAccessKey`, `awsSecretKey`, `awsSessionToken`, `awsProfile`
|
||||
|
||||
These are passed through `ProviderSelectionConfig.settings` rather than `apiKeyEnv`.
|
||||
|
||||
---
|
||||
|
||||
## OAuth Authentication
|
||||
|
||||
### Which Providers Support OAuth
|
||||
|
||||
Only providers with `"oauth"` in their `capabilities` array support OAuth:
|
||||
|
||||
| Provider | OAuth Implementation |
|
||||
|---|---|
|
||||
| `cline` | `packages/core/src/auth/cline.ts` — Cline API OAuth |
|
||||
| `openai-codex` | `packages/core/src/auth/codex.ts` — ChatGPT/OpenAI Codex OAuth with PKCE |
|
||||
| `oca` | `packages/core/src/auth/oca.ts` — Oracle Code Assist OAuth with PKCE |
|
||||
|
||||
The CLI confirms this in `apps/cli/src/commands/auth.ts`:
|
||||
```typescript
|
||||
// Only these three providers support CLI OAuth flow
|
||||
if (providerId === "cline") return oauthApi.loginClineOAuth(...)
|
||||
if (providerId === "oca") return oauthApi.loginOcaOAuth(...)
|
||||
if (providerId === "openai-codex") return oauthApi.loginOpenAICodex(...)
|
||||
throw new Error(`Provider "${providerId}" does not support CLI OAuth flow`)
|
||||
```
|
||||
|
||||
### Responsibility Split: SDK vs Client
|
||||
|
||||
| Concern | Owner | Details |
|
||||
|---|---|---|
|
||||
| Spawn local callback server | **SDK** | `startLocalOAuthServer()` in `packages/core/src/auth/server.ts` |
|
||||
| Build authorization URL | **SDK** | Each auth module constructs the URL with redirect_uri, state, etc. |
|
||||
| Open browser / present URL | **Client** | SDK calls `callbacks.onAuth({ url, instructions })` — client decides how to handle |
|
||||
| Collect redirect code | **SDK** | Local HTTP server parses `?code=&state=` from redirect |
|
||||
| Exchange code for tokens | **SDK** | Each auth module handles the token exchange |
|
||||
| Prompt for manual code input | **Client** | SDK calls `callbacks.onPrompt()` or `callbacks.onManualCodeInput()` as fallback |
|
||||
|
||||
### The SDK Does NOT Open Browsers
|
||||
|
||||
The SDK never calls `open()` or launches a browser. It uses a callback-based interface:
|
||||
|
||||
```typescript
|
||||
// packages/core/src/auth/types.ts
|
||||
interface OAuthLoginCallbacks {
|
||||
onAuth: (info: { url: string; instructions?: string }) => void; // SDK emits URL here
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>; // SDK asks for input here
|
||||
onProgress?: (message: string) => void;
|
||||
onManualCodeInput?: () => Promise<string>; // Fallback if redirect fails
|
||||
}
|
||||
```
|
||||
|
||||
### The SDK DOES Spawn the Local Callback Server
|
||||
|
||||
`packages/core/src/auth/server.ts` exports `startLocalOAuthServer()`:
|
||||
|
||||
- Creates a `node:http` server on `127.0.0.1`
|
||||
- Tries a list of candidate ports in order, skipping `EADDRINUSE`
|
||||
- Listens on a configured callback path (e.g. `/callback`)
|
||||
- Extracts `code`, `state`, `provider`, `error` from the redirect URL query params
|
||||
- Returns a success HTML page to the browser ("Authentication Successful — You can close this window")
|
||||
- Auto-closes after 3 seconds via embedded `<script>`
|
||||
- Times out after 5 minutes by default
|
||||
|
||||
```typescript
|
||||
interface LocalOAuthServer {
|
||||
callbackUrl: string; // e.g. "http://127.0.0.1:54321/callback"
|
||||
waitForCallback: () => Promise<OAuthCallbackPayload>; // Resolves when redirect arrives
|
||||
cancelWait: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth Redirect URLs Are NOT in Provider Metadata
|
||||
|
||||
Redirect/callback URLs are **dynamically constructed at runtime**, not published in the provider manifest:
|
||||
|
||||
- **Cline**: Dynamic port → `http://localhost:{port}/callback`
|
||||
- **OpenAI Codex**: Hardcoded `http://localhost:1455/auth/callback` (fixed port, fixed client ID `app_EMoamEEZ73f0CkXaXp7hrann`)
|
||||
- **OCA**: Dynamic port with configurable path, default `/oauth/callback`
|
||||
|
||||
### Client Integration Helper
|
||||
|
||||
`packages/core/src/auth/client.ts` provides a convenience adapter:
|
||||
|
||||
```typescript
|
||||
interface OAuthClientCallbacksOptions {
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
||||
onOutput?: (message: string) => void;
|
||||
openUrl?: (url: string) => void | Promise<void>; // Client provides browser-open function
|
||||
onOpenUrlError?: (context: { url: string; error: unknown }) => void;
|
||||
}
|
||||
|
||||
function createOAuthClientCallbacks(options): OAuthLoginCallbacks
|
||||
```
|
||||
|
||||
The `openUrl` field is where a CLI passes its `open` implementation, a Tauri app passes shell open, etc.
|
||||
|
||||
### End-to-End OAuth Flow
|
||||
|
||||
```
|
||||
1. Client calls SDK login function (e.g. loginClineOAuth)
|
||||
2. SDK → startLocalOAuthServer() → binds to 127.0.0.1:{port}
|
||||
3. SDK → builds authorization URL with redirect_uri = callback server URL
|
||||
4. SDK → callbacks.onAuth({ url, instructions })
|
||||
5. Client → opens browser (or displays URL to user)
|
||||
6. User → authenticates in browser
|
||||
7. Provider → redirects to http://127.0.0.1:{port}/callback?code=...&state=...
|
||||
8. SDK's local server → captures code and state, renders success page
|
||||
9. SDK → exchanges authorization code for tokens (provider-specific)
|
||||
10. SDK → returns OAuthCredentials { access, refresh, expires, accountId?, email? }
|
||||
```
|
||||
|
||||
If the local server redirect times out or fails, the SDK falls back to:
|
||||
- `onManualCodeInput()` — ask user to paste code
|
||||
- `onPrompt({ message: "Paste the authorization code (or full redirect URL):" })` — final fallback
|
||||
- `parseAuthorizationInput()` can handle both raw codes and full URLs with query params
|
||||
|
||||
### OAuthCredentials Shape
|
||||
|
||||
```typescript
|
||||
interface OAuthCredentials {
|
||||
access: string; // Access token
|
||||
refresh: string; // Refresh token
|
||||
expires: number; // Expiration timestamp (ms since epoch)
|
||||
accountId?: string; // Provider-specific account ID
|
||||
email?: string; // For display/telemetry
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
### Provider-Specific OAuth Details
|
||||
|
||||
**Cline OAuth** (`packages/core/src/auth/cline.ts`):
|
||||
- Authorization URL: `{apiBaseUrl}/auth/authorize?client_type=extension&callback_url=...&state=...`
|
||||
- Token endpoint: `{apiBaseUrl}/auth/token`
|
||||
- Default API base: `https://api.cline.bot`
|
||||
- Supports provider passthrough (callback can include `?provider=google` etc.)
|
||||
|
||||
**OpenAI Codex OAuth** (`packages/core/src/auth/codex.ts`):
|
||||
- Uses PKCE (code challenge + verifier)
|
||||
- Authorization: `https://auth.openai.com/oauth/authorize`
|
||||
- Token: `https://auth.openai.com/oauth/token`
|
||||
- Client ID: `app_EMoamEEZ73f0CkXaXp7hrann`
|
||||
- Fixed redirect: `http://localhost:1455/auth/callback`
|
||||
- Scopes: `openid profile email offline_access`
|
||||
- JWT claim path: `https://api.openai.com/auth`
|
||||
|
||||
**OCA OAuth** (`packages/core/src/auth/oca.ts`):
|
||||
- Uses PKCE (S256 code challenge)
|
||||
- Supports `internal` and `external` mode with separate IDCS URLs and client IDs
|
||||
- Authorization: `{idcsUrl}/oauth2/v1/authorize`
|
||||
- Token: `{idcsUrl}/oauth2/v1/token`
|
||||
- Configurable callback ports and path
|
||||
+5
-3
@@ -8,7 +8,9 @@ We actively patch only the most recent minor release of Cline. Older versions re
|
||||
|
||||
We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions.
|
||||
|
||||
To report a security issue, please submit your report through our [Bugcrowd Vulnerability Disclosure Program](https://bugcrowd.com/engagements/clinebot-vdp-ess). Bugcrowd will manage communication and triage on our behalf.
|
||||
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/cline/cline/security/advisories/new) tab.
|
||||
|
||||
The team will send a response indicating the next steps in handling your report. After the initial reply, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
||||
|
||||
When reporting, please include:
|
||||
|
||||
@@ -16,10 +18,10 @@ When reporting, please include:
|
||||
- Steps to reproduce or a proof of concept
|
||||
- Any logs, stack traces, or screenshots that might help us understand the problem
|
||||
|
||||
Please keep the details private until a resolution has been reached.
|
||||
We acknowledge reports within 48 hours and aim to release a fix or mitigation within 30 days. While we work on a resolution, please keep the details private.
|
||||
|
||||
## Escalation
|
||||
|
||||
If you are unable to submit through Bugcrowd, you may send an email to security@cline.bot.
|
||||
If you do not receive an acknowledgement of your report within 5 business days, you may send an email to security@cline.bot.
|
||||
|
||||
Thank you for helping us keep Cline users safe.
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
<div align="center"><sub>
|
||||
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
|
||||
</sub></div>
|
||||
|
||||
# Cline
|
||||
<div align="center">
|
||||
<table>
|
||||
<tbody>
|
||||
<td align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Download on VS Marketplace</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
|
||||
|
||||
Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
|
||||
|
||||
1. Enter your task and add images to convert mockups into functional apps or fix bugs with screenshots.
|
||||
2. Cline starts by analyzing your file structure & source code ASTs, running regex searches, and reading relevant files to get up to speed in existing projects. By carefully managing what information is added to context, Cline can provide valuable assistance even for large, complex projects without overwhelming the context window.
|
||||
3. Once Cline has the information he needs, he can:
|
||||
- Create and edit files + monitor linter/compiler errors along the way, letting him proactively fix issues like missing imports and syntax errors on his own.
|
||||
- Execute commands directly in your terminal and monitor their output as he works, letting him e.g., react to dev server issues after editing a file.
|
||||
- For web development tasks, Cline can launch the site in a headless browser, click, type, scroll, and capture screenshots + console logs, allowing him to fix runtime errors and visual bugs.
|
||||
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
|
||||
|
||||
> [!TIP]
|
||||
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
|
||||
|
||||
---
|
||||
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
||||
|
||||
### Use any API and Model
|
||||
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
|
||||
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
|
||||
|
||||
### Run Commands in Terminal
|
||||
|
||||
Thanks to the new [shell integration updates in VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), Cline can execute commands directly in your terminal and receive the output. This allows him to perform a wide range of tasks, from installing packages and running build scripts to deploying applications, managing databases, and executing tests, all while adapting to your dev environment & toolchain to get the job done right.
|
||||
|
||||
For long running processes like dev servers, use the "Proceed While Running" button to let Cline continue in the task while the command runs in the background. As Cline works he’ll be notified of any new terminal output along the way, letting him react to issues that may come up, such as compile-time errors when editing files.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
|
||||
|
||||
### Create and Edit Files
|
||||
|
||||
Cline can create and edit files directly in your editor, presenting you a diff view of the changes. You can edit or revert Cline's changes directly in the diff view editor, or provide feedback in chat until you're satisfied with the result. Cline also monitors linter/compiler errors (missing imports, syntax errors, etc.) so he can fix issues that come up along the way on his own.
|
||||
|
||||
All changes made by Cline are recorded in your file's Timeline, providing an easy way to track and revert modifications if needed.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
|
||||
|
||||
### Use the Browser
|
||||
|
||||
With Claude Sonnet's new [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) capability, Cline can launch a browser, click elements, type text, and scroll, capturing screenshots and console logs at each step. This allows for interactive debugging, end-to-end testing, and even general web use! This gives him autonomy to fixing visual bugs and runtime issues without you needing to handhold and copy-pasting error logs yourself.
|
||||
|
||||
Try asking Cline to "test the app", and watch as he runs a command like `npm run dev`, launches your locally running dev server in a browser, and performs a series of tests to confirm that everything works. [See a demo here.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
|
||||
|
||||
### "add a tool that..."
|
||||
|
||||
Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol), Cline can extend his capabilities through custom tools. While you can use [community-made servers](https://github.com/modelcontextprotocol/servers), Cline can instead create and install tools tailored to your specific workflow. Just ask Cline to "add a tool" and he will handle everything, from creating a new MCP server to installing it into the extension. These custom tools then become part of Cline's toolkit, ready to use in future tasks.
|
||||
|
||||
- "add a tool that fetches Jira tickets": Retrieve ticket ACs and put Cline to work
|
||||
- "add a tool that manages AWS EC2s": Check server metrics and scale instances up or down
|
||||
- "add a tool that pulls the latest PagerDuty incidents": Fetch details and ask Cline to fix bugs
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
|
||||
|
||||
### Add Context
|
||||
|
||||
**`@url`:** Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Cline the latest docs
|
||||
|
||||
**`@problems`:** Add workspace errors and warnings ('Problems' panel) for Cline to fix
|
||||
|
||||
**`@file`:** Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
|
||||
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
|
||||
|
||||
### Checkpoints: Compare and Restore
|
||||
|
||||
As Cline works through a task, the extension takes a snapshot of your workspace at each step. You can use the 'Compare' button to see a diff between the snapshot and your current workspace, and the 'Restore' button to roll back to that point.
|
||||
|
||||
For example, when working with a local web server, you can use 'Restore Workspace Only' to quickly test different versions of your app, then use 'Restore Task and Workspace' when you find the version you want to continue building from. This lets you safely explore different approaches without losing progress.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
|
||||
|
||||
## Enterprise
|
||||
|
||||
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.9 KiB |
@@ -1,90 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Swap README.md with README.marketplace.md so the VS Code Marketplace listing
|
||||
// (which is generated from the README baked into the .vsix at package time)
|
||||
// keeps the extension-focused content even after the repo's README.md is
|
||||
// repurposed as a multi-product landing page.
|
||||
//
|
||||
// The README files diverge in two directions:
|
||||
// - README.md is what GitHub renders on the repo home page. We want this to
|
||||
// cover the SDK, JetBrains plugin, CLI, and VS Code extension together.
|
||||
// - README.marketplace.md is what users see on the VS Code Marketplace and
|
||||
// inside the extension after install. It stays focused on the VS Code UX.
|
||||
//
|
||||
// vsce reads README.md from the extension root at `vsce package` / `vsce publish`
|
||||
// time and has no flag to point it elsewhere, so we copy README.marketplace.md
|
||||
// over README.md just before packaging and put the original back afterwards.
|
||||
//
|
||||
// swapIn is idempotent: if README.md already matches README.marketplace.md
|
||||
// (e.g., an outer wrapper has already swapped), it no-ops instead of erroring
|
||||
// on the backup file. This lets nested callers (ext-vscode-publish-stable.yml wrapping the whole
|
||||
// step, plus the individual npm scripts swapping internally) coexist safely.
|
||||
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const projectRoot = path.join(__dirname, "..")
|
||||
|
||||
const README_PATH = path.join(projectRoot, "README.md")
|
||||
const MARKETPLACE_PATH = path.join(projectRoot, "README.marketplace.md")
|
||||
const BACKUP_PATH = path.join(projectRoot, ".README.github.bak")
|
||||
|
||||
function readFile(p) {
|
||||
return fs.readFileSync(p, "utf-8")
|
||||
}
|
||||
|
||||
export function swapIn() {
|
||||
if (!fs.existsSync(MARKETPLACE_PATH)) {
|
||||
throw new Error(`Missing ${MARKETPLACE_PATH}. The marketplace README must exist before publishing.`)
|
||||
}
|
||||
if (!fs.existsSync(README_PATH)) {
|
||||
throw new Error(`Missing ${README_PATH}. Cannot swap in marketplace README.`)
|
||||
}
|
||||
|
||||
if (readFile(README_PATH) === readFile(MARKETPLACE_PATH)) {
|
||||
return { skipped: true }
|
||||
}
|
||||
|
||||
if (fs.existsSync(BACKUP_PATH)) {
|
||||
throw new Error(
|
||||
`Stale backup at ${BACKUP_PATH}. A previous publish may have aborted before restoring README.md. ` +
|
||||
`Move it back to README.md manually before retrying.`,
|
||||
)
|
||||
}
|
||||
|
||||
fs.copyFileSync(README_PATH, BACKUP_PATH)
|
||||
fs.copyFileSync(MARKETPLACE_PATH, README_PATH)
|
||||
return { skipped: false }
|
||||
}
|
||||
|
||||
export function restore() {
|
||||
if (!fs.existsSync(BACKUP_PATH)) {
|
||||
return { skipped: true }
|
||||
}
|
||||
fs.copyFileSync(BACKUP_PATH, README_PATH)
|
||||
fs.unlinkSync(BACKUP_PATH)
|
||||
return { skipped: false }
|
||||
}
|
||||
|
||||
const invokedAsCli = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)
|
||||
if (invokedAsCli) {
|
||||
const cmd = process.argv[2]
|
||||
try {
|
||||
if (cmd === "swap-in") {
|
||||
const result = swapIn()
|
||||
console.log(result.skipped ? "marketplace-readme: already swapped, skipping" : "marketplace-readme: swapped in")
|
||||
} else if (cmd === "restore") {
|
||||
const result = restore()
|
||||
console.log(result.skipped ? "marketplace-readme: no backup, skipping" : "marketplace-readme: restored")
|
||||
} else {
|
||||
console.error("Usage: marketplace-readme.mjs <swap-in|restore>")
|
||||
process.exit(2)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`marketplace-readme: ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Wraps the marketplace publish flow (vsce + ovsx) so the .vsix gets packaged
|
||||
// with the marketplace-flavored README instead of the GitHub-flavored README.
|
||||
//
|
||||
// vsce reads README.md from the extension root at publish time and there's no
|
||||
// flag to point it elsewhere, so we swap README.marketplace.md into place
|
||||
// first and restore the original on the way out. The swap helper is
|
||||
// idempotent, so this is safe to run nested under another wrapper (e.g., the
|
||||
// CI step in .github/workflows/ext-vscode-publish-stable.yml that also packages a .vsix for the
|
||||
// GitHub release artifact before invoking this script).
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/publish-marketplace.mjs # release channel
|
||||
// node scripts/publish-marketplace.mjs --pre-release # pre-release channel
|
||||
|
||||
import { execFileSync } from "node:child_process"
|
||||
import { restore, swapIn } from "./marketplace-readme.mjs"
|
||||
|
||||
const isPrerelease = process.argv.includes("--pre-release")
|
||||
|
||||
const result = swapIn()
|
||||
|
||||
let interrupted = false
|
||||
const cleanupOnSignal = (exitCode) => () => {
|
||||
interrupted = true
|
||||
try {
|
||||
if (!result.skipped) {
|
||||
restore()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`marketplace-readme: failed to restore on signal: ${err.message}`)
|
||||
}
|
||||
process.exit(exitCode)
|
||||
}
|
||||
process.on("SIGINT", cleanupOnSignal(130))
|
||||
process.on("SIGTERM", cleanupOnSignal(143))
|
||||
|
||||
try {
|
||||
const vsceArgs = ["publish", "--allow-package-secrets", "sendgrid"]
|
||||
if (isPrerelease) {
|
||||
vsceArgs.push("--pre-release")
|
||||
}
|
||||
execFileSync("vsce", vsceArgs, { stdio: "inherit" })
|
||||
|
||||
const ovsxArgs = ["ovsx", "publish"]
|
||||
if (isPrerelease) {
|
||||
ovsxArgs.push("--pre-release")
|
||||
}
|
||||
execFileSync("npx", ovsxArgs, { stdio: "inherit" })
|
||||
} finally {
|
||||
if (!interrupted && !result.skipped) {
|
||||
restore()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"cline-sdk": {
|
||||
"source": "cline/sdk-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skill/cline-sdk/SKILL.md",
|
||||
"computedHash": "ce565d78d5bd1c40075248a3a925b2059c5c024bfcdbbf1f55bf3440cae34144"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import { afterEach, describe, it } from "mocha"
|
||||
import sinon from "sinon"
|
||||
import "should"
|
||||
import { anthropicModels } from "@shared/api"
|
||||
import { ANTHROPIC_FAST_MODE_BETA, AnthropicHandler } from "../anthropic"
|
||||
|
||||
describe("AnthropicHandler", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const createAsyncIterable = (data: readonly unknown[] = []) => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield* data
|
||||
},
|
||||
})
|
||||
|
||||
describe("getModel", () => {
|
||||
it("should return the fast mode model when configured", () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-6:fast",
|
||||
})
|
||||
|
||||
const result = handler.getModel()
|
||||
|
||||
result.id.should.equal("claude-opus-4-6:fast")
|
||||
result.info.should.deepEqual(anthropicModels["claude-opus-4-6:fast"])
|
||||
})
|
||||
|
||||
it("should return the 1m fast mode model when configured", () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-6:1m:fast",
|
||||
})
|
||||
|
||||
const result = handler.getModel()
|
||||
|
||||
result.id.should.equal("claude-opus-4-6:1m:fast")
|
||||
result.info.should.deepEqual(anthropicModels["claude-opus-4-6:1m:fast"])
|
||||
})
|
||||
|
||||
it("should return the 4.7 model when configured", () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-7",
|
||||
})
|
||||
|
||||
const result = handler.getModel()
|
||||
|
||||
result.id.should.equal("claude-opus-4-7")
|
||||
result.info.should.deepEqual(anthropicModels["claude-opus-4-7"])
|
||||
})
|
||||
|
||||
it("should return the 4.7 1m model when configured", () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-7:1m",
|
||||
})
|
||||
|
||||
const result = handler.getModel()
|
||||
|
||||
result.id.should.equal("claude-opus-4-7:1m")
|
||||
result.info.should.deepEqual(anthropicModels["claude-opus-4-7:1m"])
|
||||
})
|
||||
|
||||
it("should return the 4.8 model when configured", () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-8",
|
||||
})
|
||||
|
||||
const result = handler.getModel()
|
||||
|
||||
result.id.should.equal("claude-opus-4-8")
|
||||
result.info.should.deepEqual(anthropicModels["claude-opus-4-8"])
|
||||
})
|
||||
|
||||
it("should return the 4.8 1m model when configured", () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-8:1m",
|
||||
})
|
||||
|
||||
const result = handler.getModel()
|
||||
|
||||
result.id.should.equal("claude-opus-4-8:1m")
|
||||
result.info.should.deepEqual(anthropicModels["claude-opus-4-8:1m"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("createMessage", () => {
|
||||
it("should route fast mode requests through the beta messages API", async () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-6:fast",
|
||||
})
|
||||
|
||||
const standardCreate = sinon.stub().resolves(createAsyncIterable())
|
||||
const betaCreate = sinon.stub().callsFake(function (this: { _client?: object }, _params: unknown) {
|
||||
should.exist(this._client)
|
||||
return Promise.resolve(createAsyncIterable())
|
||||
})
|
||||
|
||||
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
|
||||
messages: {
|
||||
create: standardCreate,
|
||||
},
|
||||
beta: {
|
||||
messages: {
|
||||
_client: {},
|
||||
create: betaCreate,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
for await (const _chunk of handler.createMessage("system prompt", [{ role: "user", content: "Hello" }])) {
|
||||
}
|
||||
|
||||
sinon.assert.notCalled(standardCreate)
|
||||
sinon.assert.calledOnce(betaCreate)
|
||||
sinon.assert.calledWithMatch(betaCreate, {
|
||||
model: "claude-opus-4-6",
|
||||
betas: [ANTHROPIC_FAST_MODE_BETA],
|
||||
speed: "fast",
|
||||
stream: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should include the 1m beta when routing 1m fast mode requests through the beta messages API", async () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-6:1m:fast",
|
||||
})
|
||||
|
||||
const standardCreate = sinon.stub().resolves(createAsyncIterable())
|
||||
const betaCreate = sinon.stub().callsFake(function (this: { _client?: object }, _params: unknown) {
|
||||
should.exist(this._client)
|
||||
return Promise.resolve(createAsyncIterable())
|
||||
})
|
||||
|
||||
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
|
||||
messages: {
|
||||
create: standardCreate,
|
||||
},
|
||||
beta: {
|
||||
messages: {
|
||||
_client: {},
|
||||
create: betaCreate,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
for await (const _chunk of handler.createMessage("system prompt", [{ role: "user", content: "Hello" }])) {
|
||||
}
|
||||
|
||||
sinon.assert.notCalled(standardCreate)
|
||||
sinon.assert.calledOnce(betaCreate)
|
||||
sinon.assert.calledWithMatch(betaCreate, {
|
||||
model: "claude-opus-4-6",
|
||||
betas: [ANTHROPIC_FAST_MODE_BETA, "context-1m-2025-08-07"],
|
||||
speed: "fast",
|
||||
stream: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should include the 1m beta header for Claude Opus 4.7 1m requests", async () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-7:1m",
|
||||
reasoningEffort: "high",
|
||||
})
|
||||
|
||||
const standardCreate = sinon.stub().resolves(createAsyncIterable())
|
||||
|
||||
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
|
||||
messages: {
|
||||
create: standardCreate,
|
||||
},
|
||||
beta: {
|
||||
messages: {
|
||||
_client: {},
|
||||
create: sinon.stub().resolves(createAsyncIterable()),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
for await (const _chunk of handler.createMessage("system prompt", [{ role: "user", content: "Hello" }])) {
|
||||
}
|
||||
|
||||
sinon.assert.calledOnce(standardCreate)
|
||||
const requestBody = standardCreate.firstCall.args[0] as Record<string, any>
|
||||
const requestOptions = standardCreate.firstCall.args[1] as Record<string, any>
|
||||
requestBody.model.should.equal("claude-opus-4-7")
|
||||
requestBody.thinking.should.deepEqual({ type: "adaptive" })
|
||||
requestOptions.should.deepEqual({
|
||||
headers: {
|
||||
"anthropic-beta": "context-1m-2025-08-07",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should use adaptive thinking and output_config for Claude Opus adaptive models", async () => {
|
||||
const handler = new AnthropicHandler({
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-opus-4-7",
|
||||
reasoningEffort: "xhigh",
|
||||
})
|
||||
|
||||
const standardCreate = sinon.stub().resolves(createAsyncIterable())
|
||||
|
||||
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
|
||||
messages: {
|
||||
create: standardCreate,
|
||||
},
|
||||
beta: {
|
||||
messages: {
|
||||
_client: {},
|
||||
create: sinon.stub().resolves(createAsyncIterable()),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
for await (const _chunk of handler.createMessage("system prompt", [{ role: "user", content: "Hello" }])) {
|
||||
}
|
||||
|
||||
sinon.assert.calledOnce(standardCreate)
|
||||
const requestBody = standardCreate.firstCall.args[0] as Record<string, any>
|
||||
requestBody.should.have.property("thinking")
|
||||
requestBody.thinking.should.deepEqual({ type: "adaptive" })
|
||||
requestBody.should.have.property("output_config")
|
||||
requestBody.output_config.should.deepEqual({ effort: "xhigh" })
|
||||
should(requestBody.temperature).equal(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
import "should"
|
||||
import { moonshotModels } from "@shared/api"
|
||||
import type { ClineStorageMessage } from "@shared/messages/content"
|
||||
import sinon from "sinon"
|
||||
import { MoonshotHandler } from "../moonshot"
|
||||
|
||||
interface MoonshotRequestPayload {
|
||||
model: string
|
||||
temperature: number
|
||||
max_tokens: number
|
||||
}
|
||||
|
||||
describe("MoonshotHandler", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const createAsyncIterable = (data: unknown[] = []): AsyncIterable<unknown> => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield* data
|
||||
},
|
||||
})
|
||||
|
||||
it("supports kimi-k2.6 model metadata", async () => {
|
||||
const handler = new MoonshotHandler({
|
||||
moonshotApiKey: "test-api-key",
|
||||
apiModelId: "kimi-k2.6",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("kimi-k2.6")
|
||||
model.info.should.deepEqual(moonshotModels["kimi-k2.6"])
|
||||
|
||||
const createStub = sinon.stub().resolves(createAsyncIterable([]))
|
||||
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
|
||||
chat: {
|
||||
completions: {
|
||||
create: createStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const messages: ClineStorageMessage[] = [{ role: "user", content: "hi" }]
|
||||
for await (const _chunk of handler.createMessage("system", messages)) {
|
||||
// Consume stream to trigger request execution.
|
||||
}
|
||||
|
||||
const payload = createStub.firstCall.args[0] as MoonshotRequestPayload
|
||||
payload.model.should.equal("kimi-k2.6")
|
||||
payload.temperature.should.equal(moonshotModels["kimi-k2.6"].temperature)
|
||||
payload.max_tokens.should.equal(moonshotModels["kimi-k2.6"].maxTokens)
|
||||
})
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import "should"
|
||||
import { vertexGlobalModels } from "@shared/api"
|
||||
import { VertexHandler } from "../vertex"
|
||||
|
||||
describe("VertexHandler", () => {
|
||||
it("supports Gemini 3.5 Flash model metadata", () => {
|
||||
const handler = new VertexHandler({
|
||||
vertexProjectId: "test-project",
|
||||
vertexRegion: "global",
|
||||
apiModelId: "gemini-3.5-flash",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("gemini-3.5-flash")
|
||||
model.info.contextWindow!.should.equal(1_048_576)
|
||||
model.info.inputPrice!.should.equal(1.5)
|
||||
model.info.outputPrice!.should.equal(9)
|
||||
model.info.cacheReadsPrice!.should.equal(0.15)
|
||||
model.info.supportsGlobalEndpoint!.should.equal(true)
|
||||
model.info.supportsReasoning!.should.equal(true)
|
||||
vertexGlobalModels.should.have.property("gemini-3.5-flash")
|
||||
})
|
||||
})
|
||||
@@ -1,274 +0,0 @@
|
||||
import { getSkillsDirectoriesForScan } from "@core/storage/disk"
|
||||
import type { GlobalInstructionsFile } from "@shared/remote-config/schema"
|
||||
import type { SkillContent, SkillMetadata } from "@shared/skills"
|
||||
import { fileExistsAtPath, isDirectory } from "@utils/fs"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { parseYamlFrontmatter } from "./frontmatter"
|
||||
|
||||
/**
|
||||
* A remote skill entry after frontmatter validation.
|
||||
* name is always frontmatter.name (canonical). A warning is logged if entry.name drifts.
|
||||
*/
|
||||
export interface ValidatedRemoteSkill {
|
||||
name: string
|
||||
description: string
|
||||
alwaysEnabled: boolean
|
||||
contents: string
|
||||
}
|
||||
|
||||
export interface SkillToggleState {
|
||||
globalSkillsToggles?: Record<string, boolean>
|
||||
localSkillsToggles?: Record<string, boolean>
|
||||
remoteSkillsToggles?: Record<string, boolean>
|
||||
remoteSkillEntries?: GlobalInstructionsFile[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate remote skill entries from GlobalInstructionsFile[].
|
||||
*
|
||||
* Validates:
|
||||
* - frontmatter.name and frontmatter.description are present strings
|
||||
* - Warns if entry.name does not match frontmatter.name (drift)
|
||||
*
|
||||
* Returns only valid entries. Callers share this single validation point
|
||||
* instead of duplicating frontmatter parsing.
|
||||
*/
|
||||
export function parseRemoteSkillEntries(entries: GlobalInstructionsFile[]): ValidatedRemoteSkill[] {
|
||||
return entries
|
||||
.map((entry) => {
|
||||
const { data: frontmatter } = parseYamlFrontmatter(entry.contents)
|
||||
if (!frontmatter.name || typeof frontmatter.name !== "string") return null
|
||||
if (!frontmatter.description || typeof frontmatter.description !== "string") return null
|
||||
// Warn on drift but use frontmatter.name as the canonical identity.
|
||||
// The dashboard should keep entry.name in sync, but we don't reject on mismatch
|
||||
// since that would silently hide org-configured skills from users.
|
||||
if (entry.name !== frontmatter.name) {
|
||||
Logger.warn(`Remote skill entry.name "${entry.name}" does not match frontmatter.name "${frontmatter.name}"`)
|
||||
}
|
||||
return {
|
||||
name: frontmatter.name,
|
||||
description: frontmatter.description as string,
|
||||
alwaysEnabled: entry.alwaysEnabled,
|
||||
contents: entry.contents,
|
||||
}
|
||||
})
|
||||
.filter((e): e is NonNullable<typeof e> => e !== null)
|
||||
}
|
||||
|
||||
/** Parse YAML frontmatter from markdown content (shared helper). */
|
||||
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
|
||||
const result = parseYamlFrontmatter(fileContent)
|
||||
if (result.parseError) {
|
||||
Logger.warn("Failed to parse YAML frontmatter:", result.parseError)
|
||||
}
|
||||
return { data: result.data, content: result.body }
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a directory for skill subdirectories containing SKILL.md files.
|
||||
*/
|
||||
async function scanSkillsDirectory(dirPath: string, source: "global" | "project"): Promise<SkillMetadata[]> {
|
||||
const skills: SkillMetadata[] = []
|
||||
|
||||
if (!(await fileExistsAtPath(dirPath)) || !(await isDirectory(dirPath))) {
|
||||
return skills
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath)
|
||||
|
||||
for (const entryName of entries) {
|
||||
const entryPath = path.join(dirPath, entryName)
|
||||
const stats = await fs.stat(entryPath).catch(() => null)
|
||||
if (!stats?.isDirectory()) continue
|
||||
|
||||
const skill = await loadSkillMetadata(entryPath, source, entryName)
|
||||
if (skill) {
|
||||
skills.push(skill)
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EACCES") {
|
||||
Logger.warn(`Permission denied reading skills directory: ${dirPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
return skills
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skill metadata from a skill directory.
|
||||
*/
|
||||
async function loadSkillMetadata(
|
||||
skillDir: string,
|
||||
source: "global" | "project",
|
||||
skillName: string,
|
||||
): Promise<SkillMetadata | null> {
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
if (!(await fileExistsAtPath(skillMdPath))) return null
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(skillMdPath, "utf-8")
|
||||
const { data: frontmatter } = parseFrontmatter(fileContent)
|
||||
|
||||
// Validate required fields
|
||||
if (!frontmatter.name || typeof frontmatter.name !== "string") {
|
||||
Logger.warn(`Skill at ${skillDir} missing required 'name' field`)
|
||||
return null
|
||||
}
|
||||
if (!frontmatter.description || typeof frontmatter.description !== "string") {
|
||||
Logger.warn(`Skill at ${skillDir} missing required 'description' field`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Name must match directory name per spec
|
||||
if (frontmatter.name !== skillName) {
|
||||
Logger.warn(`Skill name "${frontmatter.name}" doesn't match directory "${skillName}"`)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
name: skillName,
|
||||
description: frontmatter.description,
|
||||
path: skillMdPath,
|
||||
source,
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.warn(`Failed to load skill at ${skillDir}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover all skills from global (~/.cline/skills), remote config, and project directories.
|
||||
*
|
||||
* Precedence (highest wins on name collision via getAvailableSkills):
|
||||
* remote (enterprise) > disk-global (user personal) > project (workspace)
|
||||
*
|
||||
* This is achieved by the array order + getAvailableSkills iterating in reverse (last wins):
|
||||
* [project..., disk-global..., remote...]
|
||||
*/
|
||||
export async function discoverSkills(cwd: string, remoteSkillEntries?: GlobalInstructionsFile[]): Promise<SkillMetadata[]> {
|
||||
const skills: SkillMetadata[] = []
|
||||
|
||||
const scanDirs = getSkillsDirectoriesForScan(cwd)
|
||||
|
||||
// Collect project and disk-global skills separately so we can insert remote between them
|
||||
const projectSkills: SkillMetadata[] = []
|
||||
const diskGlobalSkills: SkillMetadata[] = []
|
||||
|
||||
for (const dir of scanDirs) {
|
||||
const dirSkills = await scanSkillsDirectory(dir.path, dir.source)
|
||||
if (dir.source === "project") {
|
||||
projectSkills.push(...dirSkills)
|
||||
} else {
|
||||
diskGlobalSkills.push(...dirSkills)
|
||||
}
|
||||
}
|
||||
|
||||
// Remote skills: validated via parseRemoteSkillEntries and keyed by frontmatter.name.
|
||||
const remoteSkills: SkillMetadata[] = parseRemoteSkillEntries(remoteSkillEntries || []).map((entry) => ({
|
||||
name: entry.name,
|
||||
description: entry.description,
|
||||
path: `remote:${entry.name}`,
|
||||
source: "global" as const,
|
||||
}))
|
||||
|
||||
// Insert in order: project → disk-global → remote
|
||||
// getAvailableSkills iterates backwards so remote (last) wins, then disk-global, then project
|
||||
skills.push(...projectSkills, ...diskGlobalSkills, ...remoteSkills)
|
||||
|
||||
return skills
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available skills with override resolution (global > project).
|
||||
*/
|
||||
export function getAvailableSkills(skills: SkillMetadata[]): SkillMetadata[] {
|
||||
const seen = new Set<string>()
|
||||
const result: SkillMetadata[] = []
|
||||
|
||||
// Iterate backwards: global skills (added last) are seen first and take precedence
|
||||
for (let i = skills.length - 1; i >= 0; i--) {
|
||||
const skill = skills[i]
|
||||
if (!seen.has(skill.name)) {
|
||||
seen.add(skill.name)
|
||||
result.unshift(skill)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function filterEnabledSkills(skills: SkillMetadata[], toggleState: SkillToggleState = {}): SkillMetadata[] {
|
||||
const globalSkillsToggles = toggleState.globalSkillsToggles ?? {}
|
||||
const localSkillsToggles = toggleState.localSkillsToggles ?? {}
|
||||
const remoteSkillsToggles = toggleState.remoteSkillsToggles ?? {}
|
||||
const remoteSkillMap = new Map(
|
||||
parseRemoteSkillEntries(toggleState.remoteSkillEntries || []).map((entry) => [entry.name, entry]),
|
||||
)
|
||||
|
||||
return skills.filter((skill) => {
|
||||
if (skill.path.startsWith("remote:")) {
|
||||
const name = skill.path.replace("remote:", "")
|
||||
const entry = remoteSkillMap.get(name)
|
||||
if (entry?.alwaysEnabled) {
|
||||
return true
|
||||
}
|
||||
return remoteSkillsToggles[name] !== false
|
||||
}
|
||||
|
||||
const toggles = skill.source === "global" ? globalSkillsToggles : localSkillsToggles
|
||||
return toggles[skill.path] !== false
|
||||
})
|
||||
}
|
||||
|
||||
export async function discoverAvailableSkills(cwd: string, toggleState: SkillToggleState = {}): Promise<SkillMetadata[]> {
|
||||
const allSkills = await discoverSkills(cwd, toggleState.remoteSkillEntries)
|
||||
return filterEnabledSkills(getAvailableSkills(allSkills), toggleState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full skill content including instructions.
|
||||
* For remote skills, pass remoteSkillEntries so content can be loaded without disk I/O.
|
||||
*/
|
||||
export async function getSkillContent(
|
||||
skillName: string,
|
||||
availableSkills: SkillMetadata[],
|
||||
remoteSkillEntries?: GlobalInstructionsFile[],
|
||||
): Promise<SkillContent | null> {
|
||||
const skill = availableSkills.find((s) => s.name === skillName)
|
||||
if (!skill) return null
|
||||
|
||||
// Remote skills have no file on disk — retrieve content from the provided entries.
|
||||
// Try entry.name first (fast path when dashboard is in sync), fall back to frontmatter match.
|
||||
if (skill.path.startsWith("remote:")) {
|
||||
let entry = (remoteSkillEntries || []).find((e) => e.name === skillName)
|
||||
if (!entry) {
|
||||
entry = (remoteSkillEntries || []).find((e) => {
|
||||
const { data } = parseYamlFrontmatter(e.contents)
|
||||
return typeof data.name === "string" && data.name === skillName
|
||||
})
|
||||
}
|
||||
if (!entry) return null
|
||||
const { body } = parseYamlFrontmatter(entry.contents)
|
||||
return {
|
||||
...skill,
|
||||
instructions: body.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(skill.path, "utf-8")
|
||||
const { content: body } = parseFrontmatter(fileContent)
|
||||
|
||||
return {
|
||||
...skill,
|
||||
instructions: body.trim(),
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { ensureWorkflowsDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import path from "path"
|
||||
import { Controller } from "@/core/controller"
|
||||
|
||||
/**
|
||||
* Refresh the workflow toggles
|
||||
*/
|
||||
export async function refreshWorkflowToggles(
|
||||
controller: Controller,
|
||||
workingDirectory: string,
|
||||
): Promise<{
|
||||
globalWorkflowToggles: ClineRulesToggles
|
||||
localWorkflowToggles: ClineRulesToggles
|
||||
}> {
|
||||
// Global workflows
|
||||
const globalWorkflowToggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
const globalClineWorkflowsFilePath = await ensureWorkflowsDirectoryExists()
|
||||
const updatedGlobalWorkflowToggles = await synchronizeRuleToggles(globalClineWorkflowsFilePath, globalWorkflowToggles)
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", updatedGlobalWorkflowToggles)
|
||||
|
||||
const workflowRulesToggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
|
||||
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
|
||||
controller.stateManager.setWorkspaceState("workflowToggles", updatedWorkflowToggles)
|
||||
|
||||
return {
|
||||
globalWorkflowToggles: updatedGlobalWorkflowToggles,
|
||||
localWorkflowToggles: updatedWorkflowToggles,
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { discoverChromeInstances } from "@services/browser/BrowserDiscovery"
|
||||
import { BrowserSession } from "@services/browser/BrowserSession"
|
||||
import { BrowserConnection } from "@shared/proto/cline/browser"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Discover Chrome instances
|
||||
* @param controller The controller instance
|
||||
* @param request The request message
|
||||
* @returns The browser connection result
|
||||
*/
|
||||
export async function discoverBrowser(controller: Controller, _request: EmptyRequest): Promise<BrowserConnection> {
|
||||
try {
|
||||
const discoveredHost = await discoverChromeInstances()
|
||||
|
||||
if (discoveredHost) {
|
||||
// Don't update the remoteBrowserHost state when auto-discovering
|
||||
// This way we don't override the user's preference
|
||||
|
||||
// Test the connection to get the endpoint
|
||||
const browserSession = new BrowserSession(controller.stateManager)
|
||||
const result = await browserSession.testConnection(discoveredHost)
|
||||
|
||||
return BrowserConnection.create({
|
||||
success: true,
|
||||
message: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
|
||||
endpoint: result.endpoint || "",
|
||||
})
|
||||
} else {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message:
|
||||
"No Chrome instances found. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
|
||||
endpoint: "",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message: `Error discovering browser: ${error instanceof Error ? error.message : String(error)}`,
|
||||
endpoint: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { BrowserConnectionInfo } from "@shared/proto/cline/browser"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Get information about the current browser connection
|
||||
* @param controller The controller instance
|
||||
* @param request The request message
|
||||
* @returns The browser connection info
|
||||
*/
|
||||
export async function getBrowserConnectionInfo(controller: Controller, _: EmptyRequest): Promise<BrowserConnectionInfo> {
|
||||
try {
|
||||
// Get browser settings from extension state
|
||||
const browserSettings = controller.stateManager.getGlobalSettingsKey("browserSettings")
|
||||
|
||||
// Check if there's an active browser session by using the controller's handleWebviewMessage approach
|
||||
// This is similar to what's done in controller/index.ts for the "getBrowserConnectionInfo" message
|
||||
if (controller.task?.browserSession) {
|
||||
// Access the browser session through the controller's task property
|
||||
// Using indexer notation to access private property
|
||||
const browserSession = controller.task.browserSession
|
||||
const connectionInfo = browserSession.getConnectionInfo()
|
||||
|
||||
// Convert from BrowserSession.BrowserConnectionInfo to proto.BrowserConnectionInfo
|
||||
return BrowserConnectionInfo.create({
|
||||
isConnected: connectionInfo.isConnected,
|
||||
isRemote: connectionInfo.isRemote,
|
||||
host: connectionInfo.host || "", // Ensure host is never undefined
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback to browser settings if no active browser session
|
||||
return BrowserConnectionInfo.create({
|
||||
isConnected: false,
|
||||
isRemote: !!browserSettings.remoteBrowserEnabled,
|
||||
host: browserSettings.remoteBrowserHost || "",
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
Logger.error("Error getting browser connection info:", error)
|
||||
return BrowserConnectionInfo.create({
|
||||
isConnected: false,
|
||||
isRemote: false,
|
||||
host: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { ChromePath } from "@shared/proto/cline/browser"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { BrowserSession } from "../../../services/browser/BrowserSession"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Get the detected Chrome executable path
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request message
|
||||
* @returns The detected Chrome path and whether it's bundled
|
||||
*/
|
||||
export async function getDetectedChromePath(controller: Controller, _: EmptyRequest): Promise<ChromePath> {
|
||||
try {
|
||||
const browserSession = new BrowserSession(controller.stateManager)
|
||||
const result = await browserSession.getDetectedChromePath()
|
||||
|
||||
return ChromePath.create({
|
||||
path: result.path,
|
||||
isBundled: result.isBundled,
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error("Error getting detected Chrome path:", error)
|
||||
return ChromePath.create({
|
||||
path: "",
|
||||
isBundled: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { EmptyRequest, String as StringMessage } from "@shared/proto/cline/common"
|
||||
import { BrowserSession } from "../../../services/browser/BrowserSession"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Relaunch Chrome in debug mode
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request message
|
||||
* @returns The browser relaunch result as a string message
|
||||
*/
|
||||
export async function relaunchChromeDebugMode(controller: Controller, _: EmptyRequest): Promise<StringMessage> {
|
||||
try {
|
||||
const browserSession = new BrowserSession(controller.stateManager)
|
||||
|
||||
// Relaunch Chrome in debug mode
|
||||
await browserSession.relaunchChromeDebugMode(controller)
|
||||
|
||||
// The actual result will be sent via the ProtoBus in the BrowserSession.relaunchChromeDebugMode method
|
||||
// Here we just return a message as a placeholder
|
||||
return { value: "Chrome relaunch initiated" }
|
||||
} catch (error) {
|
||||
throw new Error(`Error relaunching Chrome: ${error instanceof Error ? error.message : globalThis.String(error)}`)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { discoverChromeInstances } from "@services/browser/BrowserDiscovery"
|
||||
import { BrowserSession } from "@services/browser/BrowserSession"
|
||||
import { BrowserConnection } from "@shared/proto/cline/browser"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Test connection to a browser instance
|
||||
* @param controller The controller instance
|
||||
* @param request The request message
|
||||
* @returns The browser connection result
|
||||
*/
|
||||
export async function testBrowserConnection(controller: Controller, request: StringRequest): Promise<BrowserConnection> {
|
||||
try {
|
||||
const browserSession = new BrowserSession(controller.stateManager)
|
||||
const text = request.value || ""
|
||||
|
||||
// If no text is provided, try auto-discovery
|
||||
if (!text) {
|
||||
try {
|
||||
const discoveredHost = await discoverChromeInstances()
|
||||
if (discoveredHost) {
|
||||
// Test the connection to the discovered host
|
||||
const result = await browserSession.testConnection(discoveredHost)
|
||||
return BrowserConnection.create({
|
||||
success: result.success,
|
||||
message: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
|
||||
endpoint: result.endpoint || "",
|
||||
})
|
||||
} else {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message:
|
||||
"No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
|
||||
endpoint: "",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
|
||||
endpoint: "",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Test the provided URL
|
||||
const result = await browserSession.testConnection(text)
|
||||
return BrowserConnection.create({
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
endpoint: result.endpoint || "",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
|
||||
endpoint: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
|
||||
import { telemetryService } from "../../../services/telemetry"
|
||||
import { Empty, StringRequest } from "../../../shared/proto/cline/common"
|
||||
import { ensureFocusChainFile, extractFocusChainListFromText } from "../../task/focus-chain/file-utils"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Opens or creates a focus chain checklist markdown file for editing
|
||||
* The file is stored at <globalStorage>/tasks/<taskId>/focus_chain_taskid_<taskId>.md
|
||||
*/
|
||||
export async function openFocusChainFile(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
if (!request.value) {
|
||||
throw new Error("Task ID is required")
|
||||
}
|
||||
|
||||
const taskId = request.value
|
||||
|
||||
// Get the current focus chain list from the task's most recent task_progress message
|
||||
let initialFocusChainContent: string | undefined
|
||||
const currentTask = controller.task
|
||||
if (currentTask) {
|
||||
// Get the task's message history and find the most recent task_progress message
|
||||
// TODO - can we decouple this from ClineMessages?
|
||||
const clineMessages = currentTask.messageStateHandler.getClineMessages()
|
||||
const lastProgressMessage = clineMessages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((m) => m.say === "task_progress")
|
||||
|
||||
if (lastProgressMessage && lastProgressMessage.text) {
|
||||
initialFocusChainContent = extractFocusChainListFromText(lastProgressMessage.text) || undefined
|
||||
}
|
||||
}
|
||||
|
||||
const focusChainFilePath = await ensureFocusChainFile(taskId, initialFocusChainContent)
|
||||
telemetryService.captureFocusChainListOpened(taskId)
|
||||
await openFileIntegration(focusChainFilePath)
|
||||
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import {
|
||||
type FileSearchSource,
|
||||
RipgrepError,
|
||||
type SearchWorkspaceFilesResult,
|
||||
searchWorkspaceFiles,
|
||||
searchWorkspaceFilesMultiroot,
|
||||
} from "@services/search/file-search"
|
||||
|
||||
import { telemetryService } from "@services/telemetry"
|
||||
import { FileSearchRequest, FileSearchResults, FileSearchType } from "@shared/proto/cline/file"
|
||||
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
|
||||
import { type FsInfo, getFsInfo } from "@utils/fs-info"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
// error_reason values surfaced on FileSearchResults; see proto/cline/file.proto.
|
||||
const ERROR_REASON_WORKSPACE_UNAVAILABLE = "workspace_unavailable"
|
||||
const ERROR_REASON_RIPGREP_SPAWN_FAILED = "ripgrep_spawn_failed"
|
||||
const ERROR_REASON_UNKNOWN = "unknown"
|
||||
|
||||
function classifyError(error: unknown): { errorReason: string; errorMessage: string } {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (error instanceof RipgrepError) {
|
||||
const firstStderrLine = error.stderr ? error.stderr.trim().split("\n", 1)[0] : ""
|
||||
return {
|
||||
errorReason: ERROR_REASON_RIPGREP_SPAWN_FAILED,
|
||||
errorMessage: firstStderrLine || errorMessage,
|
||||
}
|
||||
}
|
||||
return { errorReason: ERROR_REASON_UNKNOWN, errorMessage }
|
||||
}
|
||||
|
||||
// Fire-and-forget the FS-class lookup + telemetry capture. The picker awaits
|
||||
// the searchFiles response, so we must not block it on a slow/hung mount —
|
||||
// `getFsInfo` does a `realpath` and a `mount`/`stat -f` that, even with the
|
||||
// outer timeout in fs-info, can still cost seconds on a stale network FS.
|
||||
function captureWithFsContext(fsContextPath: string | undefined, capture: (fsContext: FsInfo) => void | Promise<void>): void {
|
||||
getFsInfo(fsContextPath)
|
||||
.then(capture)
|
||||
.catch((err) => Logger.warn(`searchFiles: telemetry capture failed: ${err}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for files in the workspace with fuzzy matching
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing search query, and optionally a mentionsRequestId and workspace_hint
|
||||
* @returns Results containing matching files/folders
|
||||
*/
|
||||
export async function searchFiles(controller: Controller, request: FileSearchRequest): Promise<FileSearchResults> {
|
||||
// Best-effort path used for FS-class telemetry. Declared in the function
|
||||
// scope so the catch block can also reference it. When the request carries
|
||||
// a workspaceHint we tag against the matched root; for cross-root searches
|
||||
// (no hint) we fall back to the primary root, since attributing one event
|
||||
// to "the root that mattered" is impossible without per-root events.
|
||||
let fsContextPath: string | undefined
|
||||
|
||||
try {
|
||||
// Map enum to string for the search service
|
||||
let selectedTypeString: "file" | "folder" | undefined
|
||||
if (request.selectedType === FileSearchType.FILE) {
|
||||
selectedTypeString = "file"
|
||||
} else if (request.selectedType === FileSearchType.FOLDER) {
|
||||
selectedTypeString = "folder"
|
||||
}
|
||||
|
||||
// Extract hint, ensure workspaceManager is ready, check for multiroot
|
||||
const workspaceHint = request.workspaceHint
|
||||
const workspaceManager = await controller.ensureWorkspaceManager()
|
||||
const hasMultirootSupport = workspaceManager && workspaceManager.getRoots()?.length > 0
|
||||
|
||||
let searchResult: SearchWorkspaceFilesResult
|
||||
|
||||
if (hasMultirootSupport) {
|
||||
// Tag the actually-searched root, not always the primary —
|
||||
// otherwise an SSHFS secondary root looks like a fast primary
|
||||
// in dashboards. searchWorkspaceFilesMultiroot resolves the hint
|
||||
// the same way (by name).
|
||||
const hintedRoot = workspaceHint
|
||||
? (workspaceManager.getRootByName(workspaceHint) ??
|
||||
workspaceManager.getRoots().find((r) => r.path === workspaceHint))
|
||||
: undefined
|
||||
fsContextPath = hintedRoot?.path ?? workspaceManager.getRoots()[0]?.path
|
||||
searchResult = await searchWorkspaceFilesMultiroot(
|
||||
request.query || "",
|
||||
workspaceManager,
|
||||
request.limit || 20,
|
||||
selectedTypeString,
|
||||
workspaceHint,
|
||||
)
|
||||
} else {
|
||||
// Legacy single workspace search
|
||||
const workspacePath = await getWorkspacePath()
|
||||
|
||||
if (!workspacePath) {
|
||||
Logger.error("Error in searchFiles: No workspace path available")
|
||||
telemetryService.captureMentionFailed("folder", "workspace_unavailable", "No workspace path available")
|
||||
return {
|
||||
results: [],
|
||||
mentionsRequestId: request.mentionsRequestId,
|
||||
errorReason: ERROR_REASON_WORKSPACE_UNAVAILABLE,
|
||||
errorMessage: "No workspace path available",
|
||||
}
|
||||
}
|
||||
|
||||
fsContextPath = workspacePath
|
||||
// Call file search service with query from request
|
||||
searchResult = await searchWorkspaceFiles(
|
||||
request.query || "",
|
||||
workspacePath,
|
||||
request.limit || 20, // Use default limit of 20 if not specified
|
||||
selectedTypeString,
|
||||
)
|
||||
}
|
||||
|
||||
const searchSource: FileSearchSource = searchResult.source
|
||||
|
||||
// Convert search results to proto FileInfo objects using the conversion function
|
||||
const protoResults = convertSearchResultsToProtoFileInfos(searchResult.items)
|
||||
|
||||
// Track search results telemetry
|
||||
// Determine search type for telemetry
|
||||
let searchType: "file" | "folder" | "all" = "all"
|
||||
if (request.selectedType === FileSearchType.FILE) {
|
||||
searchType = "file"
|
||||
} else if (request.selectedType === FileSearchType.FOLDER) {
|
||||
searchType = "folder"
|
||||
}
|
||||
|
||||
captureWithFsContext(fsContextPath, (fsContext) =>
|
||||
telemetryService.captureMentionSearchResults(
|
||||
request.query || "",
|
||||
protoResults.length,
|
||||
searchType,
|
||||
protoResults.length === 0,
|
||||
fsContext,
|
||||
searchSource,
|
||||
),
|
||||
)
|
||||
|
||||
// Return successful results
|
||||
return { results: protoResults, mentionsRequestId: request.mentionsRequestId }
|
||||
} catch (error) {
|
||||
const { errorReason, errorMessage } = classifyError(error)
|
||||
Logger.error(`Error in searchFiles (errorReason=${errorReason}):`, error)
|
||||
|
||||
const mentionType =
|
||||
request.selectedType === FileSearchType.FILE
|
||||
? "file"
|
||||
: request.selectedType === FileSearchType.FOLDER
|
||||
? "folder"
|
||||
: "folder" // Default to folder for "all" searches
|
||||
|
||||
const errorType: "ripgrep_spawn_failed" | "permission_denied" | "unknown" =
|
||||
errorReason === ERROR_REASON_RIPGREP_SPAWN_FAILED
|
||||
? "ripgrep_spawn_failed"
|
||||
: error instanceof Error && error.message.includes("permission")
|
||||
? "permission_denied"
|
||||
: "unknown"
|
||||
|
||||
// fsContextPath may be unset if we threw before resolving the workspace;
|
||||
// getFsInfo handles undefined and returns the unknown sentinel.
|
||||
captureWithFsContext(fsContextPath, (fsContext) =>
|
||||
telemetryService.captureMentionFailed(mentionType, errorType, errorMessage, fsContext),
|
||||
)
|
||||
|
||||
return {
|
||||
results: [],
|
||||
mentionsRequestId: request.mentionsRequestId,
|
||||
errorReason,
|
||||
errorMessage,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ClineRulesToggles, RuleScope, ToggleWorkflowRequest } from "@shared/proto/cline/file"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Toggles a workflow on or off
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the workflow path and enabled state
|
||||
* @returns The updated workflow toggles
|
||||
*/
|
||||
export async function toggleWorkflow(controller: Controller, request: ToggleWorkflowRequest): Promise<ClineRulesToggles> {
|
||||
const { workflowPath, enabled, scope } = request
|
||||
|
||||
if (!workflowPath || typeof enabled !== "boolean" || scope === undefined) {
|
||||
Logger.error("toggleWorkflow: Missing or invalid parameters", {
|
||||
workflowPath,
|
||||
scope,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters for toggleWorkflow")
|
||||
}
|
||||
|
||||
// Handle the three different scopes
|
||||
let toggles: Record<string, boolean>
|
||||
|
||||
switch (scope) {
|
||||
case RuleScope.GLOBAL: {
|
||||
toggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
toggles[workflowPath] = enabled
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
|
||||
break
|
||||
}
|
||||
case RuleScope.LOCAL: {
|
||||
toggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
toggles[workflowPath] = enabled
|
||||
controller.stateManager.setWorkspaceState("workflowToggles", toggles)
|
||||
break
|
||||
}
|
||||
case RuleScope.REMOTE: {
|
||||
toggles = controller.stateManager.getGlobalStateKey("remoteWorkflowToggles")
|
||||
toggles[workflowPath] = enabled
|
||||
controller.stateManager.setGlobalState("remoteWorkflowToggles", toggles)
|
||||
break
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid scope: ${scope}`)
|
||||
}
|
||||
|
||||
await controller.postStateToWebview()
|
||||
|
||||
// Return the updated toggles
|
||||
return ClineRulesToggles.create({ toggles: toggles })
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import * as disk from "@core/storage/disk"
|
||||
import axios from "axios"
|
||||
import { expect } from "chai"
|
||||
import fs from "fs/promises"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import sinon from "sinon"
|
||||
import { ClineEnv, Environment } from "@/config"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { getFeatureFlagsService } from "@/services/feature-flags"
|
||||
import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { refreshClineModels } from "../refreshClineModels"
|
||||
|
||||
describe("refreshClineModels", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
sandbox.stub(Logger, "log")
|
||||
sandbox.stub(Logger, "error")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it("marks Qwen 3.7 Max as prompt-cache capable when Cline model pricing includes cache reads", async () => {
|
||||
sandbox.stub(getFeatureFlagsService(), "getBooleanFlagEnabled").callsFake((flag) => {
|
||||
return flag === FeatureFlag.EXTENSION_CLINE_MODELS_ENDPOINT
|
||||
})
|
||||
sandbox.stub(ClineEnv, "config").returns({
|
||||
environment: Environment.production,
|
||||
appBaseUrl: "https://app.cline-mock.bot",
|
||||
apiBaseUrl: "https://api.cline-mock.bot",
|
||||
mcpBaseUrl: "https://api.cline-mock.bot/v1/mcp",
|
||||
})
|
||||
sandbox.stub(StateManager, "get").returns({
|
||||
getModelsCache: () => null,
|
||||
setModelsCache: () => {},
|
||||
} as any)
|
||||
sandbox.stub(disk, "ensureCacheDirectoryExists").resolves("/tmp")
|
||||
sandbox.stub(fs, "writeFile").resolves()
|
||||
sandbox.stub(axios, "get").resolves({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "qwen/qwen3.7-max",
|
||||
name: "Qwen: Qwen3.7 Max",
|
||||
description: null,
|
||||
context_length: 1_000_000,
|
||||
top_provider: {
|
||||
max_completion_tokens: 65_536,
|
||||
context_length: 1_000_000,
|
||||
is_moderated: false,
|
||||
},
|
||||
architecture: {
|
||||
modality: "text->text",
|
||||
},
|
||||
pricing: {
|
||||
prompt: "0.00000125",
|
||||
completion: "0.00000375",
|
||||
input_cache_read: "0.00000025",
|
||||
},
|
||||
supported_parameters: ["include_reasoning", "reasoning"],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const models = await refreshClineModels({} as any)
|
||||
const qwen37 = models["qwen/qwen3.7-max"]
|
||||
|
||||
expect(qwen37.supportsPromptCache).to.equal(true)
|
||||
expect(qwen37.cacheReadsPrice).to.equal(0.25)
|
||||
expect(qwen37.cacheWritesPrice).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Report bug slash command logic
|
||||
*/
|
||||
export async function reportBug(controller: Controller, _request: StringRequest): Promise<Empty> {
|
||||
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { ApiProviderInfo } from "@/core/api"
|
||||
import type { SystemPromptContext } from "@/core/prompts/system-prompt/types"
|
||||
import { getDeepPlanningRegistry } from "./registry"
|
||||
import { generateGemini3Template } from "./variants/gemini3"
|
||||
import { generateGPT51Template } from "./variants/gpt51"
|
||||
|
||||
const focusChainIntro: string = `**Task Progress Parameter:**
|
||||
When creating the new task, you must include a task_progress parameter that breaks down the implementation into trackable steps. This parameter should be included inside the tool call, but not located inside of other content/argument blocks. This should follow the standard Markdown checklist format with "- [ ]" for incomplete items.`
|
||||
|
||||
/**
|
||||
* Generates the deep-planning slash command response with model-family-aware variant selection
|
||||
* @param focusChainSettings Optional focus chain settings to include in the prompt
|
||||
* @param providerInfo Optional API provider info for model family detection
|
||||
* @param enableNativeToolCalls Optional flag to determine if native tool calling is enabled
|
||||
* @returns The deep-planning prompt string with appropriate variant and focus chain settings applied
|
||||
*/
|
||||
export function getDeepPlanningPrompt(
|
||||
focusChainSettings?: { enabled: boolean },
|
||||
providerInfo?: ApiProviderInfo,
|
||||
enableNativeToolCalls?: boolean,
|
||||
): string {
|
||||
// Create context for variant selection
|
||||
const context: SystemPromptContext = {
|
||||
providerInfo: providerInfo || ({} as ApiProviderInfo),
|
||||
ide: "vscode",
|
||||
}
|
||||
|
||||
// Get the appropriate variant from registry
|
||||
const registry = getDeepPlanningRegistry()
|
||||
const variant = registry.get(context)
|
||||
const newTaskInstructions = generateNewTaskInstructions(enableNativeToolCalls ?? false)
|
||||
const focusChainParam = focusChainSettings?.enabled ? focusChainIntro : ""
|
||||
|
||||
// For variants with extensive focus chain prompting, generate template with focus chain flag
|
||||
let template: string
|
||||
if (variant.id === "gpt-51") {
|
||||
template = generateGPT51Template(focusChainSettings?.enabled ?? false, enableNativeToolCalls ?? false)
|
||||
} else if (variant.id === "gemini-3") {
|
||||
template = generateGemini3Template(focusChainSettings?.enabled ?? false, enableNativeToolCalls ?? false)
|
||||
} else {
|
||||
template = variant.template
|
||||
template = template.replace("{{FOCUS_CHAIN_PARAM}}", focusChainParam)
|
||||
template = template.replace("{{NEW_TASK_INSTRUCTIONS}}", newTaskInstructions)
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the new_task tool instructions based on whether native tool calling is enabled
|
||||
* @param enableNativeToolCalls Whether native tool calling is enabled
|
||||
* @returns The new_task tool instructions string
|
||||
*/
|
||||
function generateNewTaskInstructions(enableNativeToolCalls: boolean): string {
|
||||
if (enableNativeToolCalls) {
|
||||
return `
|
||||
**new_task Tool Definition:**
|
||||
|
||||
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"name": "new_task",
|
||||
"arguments": {
|
||||
"context": "Your detailed context here following the 5-point structure..."
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
The context parameter should include all five sections as described above.`
|
||||
} else {
|
||||
return `
|
||||
**new_task Tool Definition:**
|
||||
|
||||
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
|
||||
|
||||
\`\`\`xml
|
||||
<new_task>
|
||||
<context>Your detailed context here following the 5-point structure...</context>
|
||||
</new_task>
|
||||
\`\`\`
|
||||
|
||||
The context parameter should include all five sections as described above.`
|
||||
}
|
||||
}
|
||||
|
||||
// Export types for external use
|
||||
export type { DeepPlanningRegistry, DeepPlanningVariant } from "./types"
|
||||
@@ -1,100 +0,0 @@
|
||||
import type { SystemPromptContext } from "@/core/prompts/system-prompt/types"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { DeepPlanningVariant, DeepPlanningRegistry as IDeepPlanningRegistry } from "./types"
|
||||
import {
|
||||
createAnthropicVariant,
|
||||
createGemini3Variant,
|
||||
createGeminiVariant,
|
||||
createGenericVariant,
|
||||
createGPT51Variant,
|
||||
} from "./variants"
|
||||
|
||||
/**
|
||||
* Singleton registry for managing deep-planning prompt variants
|
||||
* Selects appropriate variant based on model family detection
|
||||
*/
|
||||
class DeepPlanningRegistry implements IDeepPlanningRegistry {
|
||||
private static instance: DeepPlanningRegistry | null = null
|
||||
private variants: Map<string, DeepPlanningVariant> = new Map()
|
||||
private genericVariant: DeepPlanningVariant
|
||||
|
||||
private constructor() {
|
||||
// Initialize all variants
|
||||
this.registerVariant(createAnthropicVariant())
|
||||
this.registerVariant(createGeminiVariant())
|
||||
this.registerVariant(createGemini3Variant())
|
||||
this.registerVariant(createGPT51Variant())
|
||||
|
||||
// Generic variant must be registered last as fallback
|
||||
const genericVariant = createGenericVariant()
|
||||
this.registerVariant(genericVariant)
|
||||
this.genericVariant = genericVariant
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance of the registry
|
||||
*/
|
||||
public static getInstance(): DeepPlanningRegistry {
|
||||
if (!DeepPlanningRegistry.instance) {
|
||||
DeepPlanningRegistry.instance = new DeepPlanningRegistry()
|
||||
}
|
||||
return DeepPlanningRegistry.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new variant in the registry
|
||||
*/
|
||||
public register(variant: DeepPlanningVariant): void {
|
||||
this.registerVariant(variant)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to register a variant
|
||||
*/
|
||||
private registerVariant(variant: DeepPlanningVariant): void {
|
||||
this.variants.set(variant.id, variant)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate variant based on the system prompt context
|
||||
* Uses matcher functions to determine which variant to use
|
||||
* Falls back to generic variant if no match or on error
|
||||
*/
|
||||
public get(context: SystemPromptContext): DeepPlanningVariant {
|
||||
try {
|
||||
// Try each variant's matcher function (except generic which is last)
|
||||
for (const variant of this.variants.values()) {
|
||||
// Skip generic variant in iteration (it's the fallback)
|
||||
if (variant.id === "generic") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Test if this variant matches the context
|
||||
if (variant.matcher(context)) {
|
||||
return variant
|
||||
}
|
||||
}
|
||||
|
||||
// No match found, return generic variant
|
||||
return this.genericVariant
|
||||
} catch (error) {
|
||||
// On any error, safely fall back to generic variant
|
||||
Logger.warn("Error selecting deep-planning variant, falling back to generic:", error)
|
||||
return this.genericVariant
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered variants
|
||||
*/
|
||||
public getAll(): DeepPlanningVariant[] {
|
||||
return Array.from(this.variants.values())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export singleton instance getter
|
||||
*/
|
||||
export function getDeepPlanningRegistry(): DeepPlanningRegistry {
|
||||
return DeepPlanningRegistry.getInstance()
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { SystemPromptContext } from "@/core/prompts/system-prompt/types"
|
||||
|
||||
/**
|
||||
* Configuration for a deep-planning prompt variant
|
||||
*/
|
||||
export interface DeepPlanningVariant {
|
||||
/** Unique identifier for this variant (e.g., "anthropic", "gemini", "gpt-5", "generic") */
|
||||
id: string
|
||||
|
||||
/** Human-readable description of this variant */
|
||||
description: string
|
||||
|
||||
/** The model family this variant is designed for */
|
||||
family: string
|
||||
|
||||
/** Version number for this variant */
|
||||
version: number
|
||||
|
||||
/** Matcher function to determine if this variant should be used */
|
||||
matcher: (context: SystemPromptContext) => boolean
|
||||
|
||||
/** The complete prompt template string */
|
||||
template: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for deep-planning prompt variants
|
||||
*/
|
||||
export interface DeepPlanningRegistry {
|
||||
/** Get the appropriate variant based on context */
|
||||
get(context: SystemPromptContext): DeepPlanningVariant
|
||||
|
||||
/** Register a new variant */
|
||||
register(variant: DeepPlanningVariant): void
|
||||
|
||||
/** Get all registered variants */
|
||||
getAll(): DeepPlanningVariant[]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user