Compare commits

...

43 Commits

Author SHA1 Message Date
NightTrek f463197a2b feat(cli): Add hooks_enabled support to CLI settings
- Add hooks_enabled field to Settings proto message (field 134)
- Add hooks_enabled parsing to CLI settings parser
- Enables users to toggle hooks via -s hooks_enabled=true/false flag

Fixes missing CLI support for hooks that was available in the VSCode extension
2025-11-29 01:13:14 -08:00
DL Techy e62fbf6b0c Add shell option for cmd.exe to prevent double quote escaping (#7630)
* fix(terminal): Add shell option for cmd.exe to prevent double quote escaping

Added shell: true option specifically for cmd.exe to prevent double quotes
from being over escaped during command execution. This resolves Windows-specific
issues with terminal command handling while maintaining compatibility with
other shells.

* chore: Add changeset for terminal command execution fix
2025-11-28 13:02:47 -08:00
Saoud Rizwan 64254fc97a Fix API request badge causing text to wrap when hidden (#7739)
The cost badge was using opacity:0 to hide itself when there's no cost,
but still rendered "$0.0000" which took up horizontal space. This caused
the "API Request..." label to wrap to a second line unnecessarily.

Now the badge renders empty content when hidden, taking up no width
while still maintaining its height contribution to the row layout.
2025-11-28 08:05:09 -08:00
Luna c312c4aef6 Asksage usage fetch models (#7329)
* Add flagship models

* Add model fetching

* Add usage handling, tool result handling

* Update AskSageProvider.tsx

* Create eight-pants-explode.md

---------

Co-authored-by: alex-mcgraw-askSage <alex.mcgraw@asksage.ai>
2025-11-27 12:35:00 -06:00
celestial-vault fab49e810b Add fixed header to ClineRulesToggleModal (#7729)
- Add flex-shrink-0 to header section containing tabs and description text
- Keep tabs and description visible when content area scrolls
2025-11-27 12:32:17 -06:00
celestial-vault 2a20523e16 View remote rules and workflows in the editor (#7702)
* allow the user to view remote rules and workflows in the editor by creating a temp file

* add await
2025-11-27 11:37:47 -06:00
celestial-vault 06585821d1 conditionally fetch litellm models based on presence of api key and baseUrl (#7713) 2025-11-27 11:37:13 -06:00
Saoud Rizwan 9e802b11da Revert "Add Claude Code GitHub Workflow (#7717)"
This reverts commit afb77c5a8d.
2025-11-27 01:45:05 -08:00
Saoud Rizwan afb77c5a8d Add Claude Code GitHub Workflow (#7717)
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"
2025-11-26 20:01:50 -08:00
Saoud Rizwan 0a4811222f fix: unblock opening a task when using cline account (#7715) 2025-11-26 18:40:51 -08:00
canvrno b4ce378e4b v3.38.3 Release Notes (#7711)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-11-26 15:59:16 -08:00
CandiedUniverse 2f60a898af fix(hooks): Fix issue identified by linter in proto file (#7707) 2025-11-26 14:50:12 -08:00
Walter Korman 8ffd82eda3 feat(context): add context window error detection for vercel ai gateway (#7623)
feat(context): add context window error detection for vercel ai gateway #7623
2025-11-26 22:58:57 +01:00
Andrei Eternal 81276fdf85 Add os/cline ver/host info to task metadata & change Task History -> EXPORT to just open the task directory (#7706)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-11-26 13:40:42 -08:00
celestial-vault 164e11aae1 Refresh models in the LiteLLM provider component when the base URL changes (#7705) 2025-11-26 13:03:16 -08:00
Enrico Carlesso 9792f174b1 Adding Grok 4.1 and Grok Code to Cline (#7632) 2025-11-26 12:56:25 -08:00
canvrno a590200c64 Remove native tool calling feature flag (#7704) 2025-11-26 12:41:20 -08:00
Bee 297a45d73a feat(providers): add thinking level config to Vertex and Anthropic model support (#7701)
* feat(providers): add thinking level config to Vertex and Anthropic model support

- Pass thinking level configuration (plan/act mode) to Vertex provider through VertexHandlerOptions interface
- Add support for @-versioned Anthropic model IDs (e.g., claude-haiku-4-5@20251001) in cache control logic

This enables mode-specific thinking level configuration for the Vertex provider by propagating geminiPlanModeThinkingLevel and geminiActModeThinkingLevel settings based on the current mode. Also extends Anthropic model compatibility with newer versioning format.

* reasoning

* sonnet

* yield signature delta
2025-11-26 12:14:19 -08:00
Seb Duerr e84de0ab3c feat: update Cerebras models and speed (#7631) 2025-11-26 11:08:24 -08:00
Ara 515cb81439 fix(terminal): simplify cmd.exe command arguments (#7695)
Remove /s flag and extra quoting from cmd.exe shell arguments.
The previous approach with /s /c and quoted command was causing
issues with proper command execution in Windows cmd.exe.
2025-11-26 11:05:10 -08:00
celestial-vault 550428eabd LiteLLM provider dynamic model fetching (#7679)
* add dynamic model fetching for litellm provider and get rid of manual model config; also implement dynamic modelinfo lookup

* don't clear the models list when a fetch fails
2025-11-26 10:54:51 -08:00
schardosin 22c22a1cfc Fixed SAP AI Core Deployments Mode (#7675)
* fixed sap ai core deployments not working

* isolated chunk to string in a function

* added changeset
2025-11-26 09:26:06 -06:00
Dominic Cooney 8202479cec fix: Add proxy rules, proxy support for McpHub & others (#7659) 2025-11-26 02:06:56 -08:00
Dominic Cooney bcbaa4518d docs: Document proxy settings. (#7637)
* docs: Document proxy settings.

* Update docs/troubleshooting/networking-and-proxies.mdx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-11-26 01:30:28 -08:00
canvrno 9d799643ba npm audit fix docs + webview (#7687) 2025-11-26 00:49:40 -08:00
canvrno 6271c5da37 Remind models of new_task tool parameters when deep_planning is invoked (#7685)
* Remind models of new_task tool parameters when deep_planning is invoked

* cleanup
2025-11-25 21:59:53 -08:00
Auroter 89aeb3db3d feat(telemetry): Add OpenTelemetry metrics infrastructure (#7211)
* feat(telemetry): Add OpenTelemetry metrics infrastructure

Implement OpenTelemetry metrics support (counters, histograms, gauges) while maintaining backward compatibility with PostHog dashboards.

Changes:
- Updated ITelemetryProvider interface with recordCounter, recordHistogram, and recordGauge methods
- Implemented full OpenTelemetry metrics in OpenTelemetryTelemetryProvider with lazy instrument creation
- Added stub implementations in PostHogTelemetryProvider for backward compatibility
- Updated NoOpTelemetryProvider with metric method stubs
- Added comprehensive documentation in METRICS_IMPLEMENTATION_SUMMARY.md

Architecture:
- Dual instrumentation: existing PostHog events remain unchanged
- OpenTelemetry gets proper metrics for quantitative analysis
- Each provider handles metrics appropriately for its platform

Next steps:
- Add helper methods to TelemetryService for recording metrics with standard attributes
- Update high-priority capture methods (tokens, API performance) to call metric recording
- Validate with OpenTelemetry collector setup

* refactor(telemetry): add structured metrics and improve error handling

- Add userId and userEmail tracking to TelemetryService
- Implement helper methods (recordCounter, recordHistogram, recordGauge) with standardized attributes
- Add structured metrics for turns, tokens, costs, cache usage, and API performance
- Remove default case from TelemetryProviderFactory switch to enable exhaustive type checking
- Improve error handling by moving unsupported provider type logging outside switch
- Ensure all metric recordings include standard attributes (userId, email, metadata)

This refactoring enables better observability by recording key metrics (counters, histograms, gauges) across all telemetry providers while maintaining consistent attribute propagation and error isolation.

* fix: add logs back in to no-op provider

* fix: use Logger instead of console

* fix: remove unreachable code

* fix: satisfy compiler for config.type

* fix: remove metrics implementation summary

* chore: update telemetry to include mode in conversation turn events

- Added mode parameter to captureConversationTurnEvent in TelemetryService.
- Updated related telemetry metrics to include mode for better tracking.
- Adjusted tests to verify mode is correctly captured in telemetry events.

* fix: call signatures from merge detritus

* feat(telemetry): add optional description parameter to metric recording methods

Add optional `description` parameter to `recordCounter`, `recordHistogram`,
and `recordGauge` methods across the telemetry service layer. This enables
providers to include descriptive metadata when recording metrics.

Changes:
- Updated ITelemetryProvider interface methods to accept description parameter
- Modified TelemetryService private methods to pass description to providers
- Updated NoOpTelemetryProvider stub implementation
- Enhanced FakeProvider test implementation to capture descriptions
- Updated test assertions to verify description parameter handling

This change maintains backward compatibility as the description parameter
is optional.

* feat(telemetry): update recordGauge method to handle null values for metric retirement

- Modified the `recordGauge` method in `ITelemetryProvider` and its implementations to accept `null` as a valid value, allowing for the retirement of gauge series.
- Updated the `TelemetryService` to ensure proper cleanup of gauge entries when the series ends.
- Enhanced the `FakeProvider` test to validate the new behavior of gauge recording and retirement.
- Adjusted related tests to confirm that previous series are retired correctly when new values are recorded.

This change improves the management of gauge metrics, preventing stale entries and ensuring accurate telemetry data.

* refactor(telemetry): remove user email from telemetry service and related tests

- Removed the user email property from the TelemetryService and its associated methods, streamlining user attribute handling.
- Updated tests to reflect the removal of email, ensuring that metrics and events no longer rely on this attribute.
- Adjusted documentation in ITelemetryProvider to clarify the attributes used in metric recording.

This change enhances data privacy and simplifies the telemetry data model.

* feat(telemetry): enhance task metrics tracking with new counters and histograms

- Introduced new maps to track task turn counts, tool call counts, and error counts.
- Added methods to increment task counters and reset aggregates for better metric management.
- Updated existing telemetry capture methods to utilize the new counters and record histograms for task-related metrics.
- Enhanced tests to validate the new histogram entries for task turns, tool calls, and errors.

This change improves the granularity of telemetry data, allowing for more detailed analysis of task performance and error rates.

* refactor(telemetry): improve token usage handling in TelemetryService

- Updated conditions for recording cache write/read tokens and total cost to check for finite values, ensuring proper handling of undefined or null values.
- Introduced default values for token counts and total cost to prevent potential errors in metric recording.
- Enhanced readability by using descriptive variable names for token values.

This change enhances the robustness of telemetry data collection by ensuring that only valid numeric values are recorded.

* feat(telemetry): centralize metric definitions in TelemetryService

- Introduced a static METRICS object in TelemetryService to define all metric names, improving maintainability and readability.
- Updated existing telemetry recording methods to utilize the new METRICS constants, ensuring consistency across metric names.
- Enhanced tests to validate the use of METRICS constants in assertions for counters and histograms.

This change streamlines metric management and reduces the risk of errors due to hardcoded strings.

* refactor(telemetry): improve gauge observation handling in OpenTelemetryTelemetryProvider

- Replaced direct access to gauge values with a snapshot method to enhance data integrity during observable gauge callbacks.
- Introduced a new `snapshotGaugeSeries` method to encapsulate the logic for retrieving gauge data, improving code readability and maintainability.
- Updated the observable gauge callback to utilize the new snapshot method, ensuring that the latest values are accurately observed.

This change streamlines the process of observing gauge metrics, reducing potential errors and improving the overall telemetry data collection.

* refactor(telemetry): add required parameter to metric recording methods

- Updated `recordCounter`, `recordHistogram`, and `recordGauge` methods across the telemetry service and providers to include an optional `required` parameter, allowing for more flexible metric recording.
- Adjusted implementations in `NoOpTelemetryProvider`, `OpenTelemetryTelemetryProvider`, `PostHogTelemetryProvider`, and `FakeProvider` to handle the new parameter.
- Enhanced tests to validate the behavior of the `required` parameter in metric recording.

This change improves the control over metric recording conditions, enhancing the telemetry data collection process.

* fixed testing system

---------

Co-authored-by: NightTrek <Daniels@dual4t.com>
Co-authored-by: Daniel Steigman <35793213+NightTrek@users.noreply.github.com>
2025-11-25 18:56:30 -08:00
Saoud Rizwan 0caeea1b37 Enhance text overflow handling in TaskHeader component (#7674) 2025-11-25 14:23:39 -08:00
Saoud Rizwan 852a7c9198 Remove TaskTimeline from TaskHeader (#7670) 2025-11-25 13:09:15 -08:00
canvrno c4ef472aeb npm audit fix for glob package vulnerability (#7661) 2025-11-25 10:59:26 -08:00
Bee e22c457d19 fix: improve error property extraction from nested response objects (#7669)
- Remove intermediate response extraction to preserve full error structure
- Add fallback to error.response.message and error.response.status
- Stringify error object in logException for better console output
2025-11-25 10:58:55 -08:00
Juan Pablo Flores c15287ace0 Creates and Refactor Enterprise Docs (#7365)
* Refactor enterprise documentation: reorganize member management and roles, add AWS Bedrock configuration guides, and remove outdated security concerns section.

* Update docs/enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration.mdx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: reneehuang1 <100229782+reneehuang1@users.noreply.github.com>
2025-11-24 16:49:36 -08:00
Bee d30f54a89c fix: add a refresh guard flag for auth (#7654)
* add a refresh guard flag (

* Implemented atomic refresh handling

Replaced the boolean flag with a Promise

- When a refresh is needed, the code first checks if _refreshPromise exists
- If it exists, concurrent calls wait for the same Promise to complete and then return the refreshed token
- If it doesn't exist, a new Promise is created and stored in _refreshPromise
- The Promise is cleared in the finally block after completion
2025-11-24 15:35:56 -08:00
Bee 56b913d951 fix: feature-flags cache persistence during auth transitions (#7652)
* fix: feature-flags cache persistence during auth transitions

Restructured feature flags polling and cache management to prevent empty cache states during authentication transitions:

- Move cache timestamp update to after successful population in poll() method to ensure cache validity reflects actual data availability
- Remove cache.clear() from reset() method to preserve existing flag values until new data is fetched
- Split polling logic in AuthService to explicitly handle authenticated vs unauthenticated states
- Poll feature flags immediately after reset for authenticated users to ensure cache is populated

This prevents temporary cache misses when users log in/out while maintaining cache freshness guarantees.

* remove reset method and usage in auth flow

Remove the FeatureFlagsService.reset() method and its call during user
authentication. The feature flags polling mechanism is sufficient to
keep flags up-to-date for authenticated users without requiring an
explicit cache reset on auth state changes.

Changes:
- Remove reset() method from FeatureFlagsService
- Remove featureFlagsService.reset() call from AuthService after user identification
- Rely solely on poll() to manage feature flags cache updates
2025-11-24 15:35:45 -08:00
Saoud Rizwan 55a30e0ffa Add support for opus 4.5 global endpoint in bedrock (#7653) 2025-11-24 14:49:23 -08:00
Saoud Rizwan a017f3dfd3 Add Claude Opus 4.5 (#7648) 2025-11-24 13:33:31 -08:00
Saoud Rizwan 41ebe7c9d1 Make npm installation less strict about package-lock needing to be in sync 2025-11-24 12:26:44 -08:00
Bee 4d11f0d2fa feat: implement edit tools conversion adapter [CLIENTS-23] (#7601)
* feat: implement edit tools conversion adapter

Add logic to transform `apply_patch` tool calls into specific `write_to_file` and `replace_in_file` operations. This adapter bridges the gap between patch-based model outputs and atomic file system tools.

- Implement `transformToolCallMessages` to parse patch content:
  - Converts "Add File" patches to `write_to_file`.
  - Converts "Update File" patches to `replace_in_file` with search/replace blocks.
- Add logic to reconstruct tool result messages to match the expected V4A patch format (including `<final_file_content>`).
- Add comprehensive unit tests in `src/core/api/adapters/__tests__/adapters.test.ts` covering add/update operations, multiple tool blocks, and result reconstruction.

* fix typos

* typo
2025-11-23 13:41:18 -05:00
Bee 4baa2474eb fix: ensure reasoning signature is accessible at top level (#7615)
* fix: ensure reasoning signature is accessible at top level

Extract signature from nested summary object and promote it to the top-level
reasoning structure when not already present. This ensures consistent access
to the signature field across all providers, regardless of where it's initially
provided in the reasoning details. The fomatter that each provider runs would then reconstruct the messages in the format they need.

* clean up
2025-11-21 16:36:39 -08:00
Juan Pablo Flores c94e2cf913 Upgrade/workflows docs (#7593)
* feat(workflows): restructure and enhance workflows documentation with best practices and quick start guide

* feat(workflows): enhance documentation with modular workflow practices and new PR review workflow example

* feat(workflows): improve clarity in workflow creation instructions

* Update docs/features/slash-commands/workflows/best-practices.mdx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-21 15:45:37 -08:00
Bee dbedc6cfaa chore: clean up prompts and fix model family identifier (#7614)
- Simplified plan_mode_respond instruction text by removing redundant explanations and usage field
- Fixed incorrect MODEL_FAMILY references in native-gpt-5-1 config (was using NATIVE_GPT_5 instead of NATIVE_GPT_5_1)
- Added call_id to reasoning handler output for tracking and OpenAI Response API (unreleased)

The prompt simplification makes the response parameter instruction more concise while maintaining clarity. The model family correction ensures the GPT-5-1 variant uses the correct identifier throughout.
2025-11-21 14:25:18 -08:00
Sarah Fortune 2ac568e649 Add a setting to disable the Add Remote Servers feature in the extension. (#7612)
* Add a setting to disable the `Add Remote Servers` feature in the extension.

* Add setting to unit test

* Rename setting
2025-11-21 12:36:47 -08:00
tjandy98 b13d0e75ea Add support for Perplexity sonar and sonar-pro models to SAP AI Core Provider (#7605)
* Add perplexity models

* Add perplexity models to sap aicore

* Update api.ts

* Update sapaicore.ts

* add changeset

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

* update maxTokens and contextWindow

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2025-11-21 09:42:39 -08:00
120 changed files with 5450 additions and 1862 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix: do not retry request automatically on auth failure.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
This minor change adds new models and image support for rleated models, adds fetching of model info from API, updates tool handling, and adds retrieval usage stats for individual messages and a user's monthly token usage.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix task timeline display height.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Ensure tool arguments are streamed during file operations when native tool calling is enabled.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added thinking level setting for Gemini 3.0 Pro
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Feat: add thought signature support for Gemini SDK
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Feat: Enable native tool calling for Baseten and Kimi K2 models
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed a bug where terminal commands with double quotes are broken when "Terminal Execution Mode" is set to "Background Exec"
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Replaces generic robot icon with Cline logo across VS Code UI
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add Kimi K2 Thinking to Baseten Provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Skip MCP tool with invalid name (e.g. name too long) when native tool calling is enabled.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix Anthropic provider missing signature param when thinking is enabled.
+90
View File
@@ -0,0 +1,90 @@
# Networking & Proxy Support
To ensure Cline works correctly in all environments (VSCode, JetBrains, CLI) and with various network configurations (especially corporate proxies), strictly follow these guidelines for all network activity.
In extension code, do NOT use the global `fetch` or a default `axios` instance. (Note, `shared/net.ts` is exempt from these rules because it sets up the fetch wrappers.) In Webview code, you SHOULD use global `fetch`.
Global `fetch` and default `axios` do not automatically pick up proxy configurations in all environments (specifically JetBrains and CLI). You MUST use the provided utilities in `@/shared/net` which handle proxy agent configuration. In the webview, the browser/embedder handles proxies.
## Guidelines
### 1. Using `fetch`
Instead of `fetch(...)`, import the proxy-aware wrapper:
```typescript
import { fetch } from '@/shared/net'
// Usage is identical to global fetch
const response = await fetch('https://api.example.com/data')
```
### 2. Using `axios`
When using `axios`, you must apply the settings from `getAxiosSettings()`:
```typescript
import axios from 'axios'
import { getAxiosSettings } from '@/shared/net'
const response = await axios.get('https://api.example.com/data', {
headers: { 'Authorization': '...' },
...getAxiosSettings() // <--- CRITICAL: Injects the proxy agent if needed
})
```
### 3. Third-Party Clients (OpenAI, Ollama, etc.)
Most API client libraries allow you to customize the `fetch` implementation. You **MUST** pass the proxy-aware `fetch` to these clients.
**Example (OpenAI):**
```typescript
import OpenAI from "openai"
import { fetch } from "@/shared/net"
this.client = new OpenAI({
apiKey: '...',
fetch, // <--- CRITICAL: Pass our fetch wrapper
})
```
### 4. Tests
Use `mockFetchForTesting` to mock the underlying fetch implementation.
**Example (callback):**
```
import { mockFetchForTesting } from "@/shared/net"
...
let mockFetch = ...
mockFetchForTesting(mockFetch, () => {
// This calls mockFetch
fetch('https://foo.example').then(...)
})
// Original fetch is restored immediately when the call returns.
```
**Example (Promise):**
```
import { mockFetchForTesting } from "@/shared/net"
...
let mockFetch = ...
await mockFetchForTesting(mockFetch, async () => {
await ...
// This calls mockFetch
await fetch('https://foo.example')
...
})
// Original fetch is restored when the Promise from the callback settles
```
## Verification
If you are adding a new network call or integration:
1. Check `@/shared/net.ts` is imported.
2. Ensure `fetch` or `getAxiosSettings` is being used.
3. Verify that third-party clients are configured to use the custom fetch.
+2 -2
View File
@@ -60,11 +60,11 @@ jobs:
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
run: npm install --include=optional
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
run: cd webview-ui && npm install --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
+17
View File
@@ -1,5 +1,22 @@
# Changelog
## [3.38.3]
- Task export feature now opens the task directory, allowing easy access to the full task files
- Added Grok 4.1 and Grok Code to XAI provider
- Enabled native tool calling for Baseten and Kimi K2 models
- Added thinking level to Gemini 3.0 Pro preview
- Expanded Hooks functionality
- Removed Task Timeline from Task Header
- Bug fix for slash commands
- Bug fixes for Vertex provider
- Bug fixes for thinking/reasoning issues across multiple providers when using native tool calling
- Bug fixes for terminal usage on Windows devices
## [3.38.2]
- Add Claude Opus 4.5
## [3.38.1]
### Fixed
+6
View File
@@ -290,6 +290,12 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
return err
}
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
case "hooks_enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.HooksEnabled = boolPtr(val)
// Integer fields
case "request_timeout_ms":
+51 -13
View File
@@ -151,7 +151,14 @@
"features/slash-commands/deep-planning"
]
},
"features/slash-commands/workflows",
{
"group": "Workflows",
"pages": [
"features/slash-commands/workflows/index",
"features/slash-commands/workflows/quickstart",
"features/slash-commands/workflows/best-practices"
]
},
{
"group": "Task Management",
"pages": [
@@ -241,16 +248,10 @@
"exploring-clines-tools/remote-browser-support"
]
},
{
"group": "Enterprise",
"pages": [
"enterprise-solutions/overview",
"enterprise-solutions/security-concerns"
]
},
{
"group": "Reference",
"pages": [
"troubleshooting/networking-and-proxies",
"troubleshooting/terminal-quick-fixes",
"troubleshooting/terminal-integration-guide",
"more-info/telemetry"
@@ -258,15 +259,36 @@
}
]
},
{
"tab": "Enterprise",
"icon": "building",
"groups": [
{
"group": "Enterprise Solutions",
"pages": [
"enterprise-solutions/overview",
"enterprise-solutions/onboarding",
"enterprise-solutions/members/roles-and-permissions",
{
"group": "Provider Remote Configuration",
"pages": [
{
"group": "AWS Bedrock",
"pages": [
"enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration",
"enterprise-solutions/provider-remote-config/aws-bedrock/member-configuration"
]
}
]
}
]
}
]
},
{
"tab": "Learn",
"icon": "graduation-cap",
"href": "https://cline.bot/learn"
},
{
"tab": "Blog",
"icon": "newspaper",
"href": "https://cline.bot/blog"
}
]
},
@@ -328,6 +350,22 @@
{
"source": "/cline-cli/samples",
"destination": "/cline-cli/samples/overview"
},
{
"source": "/enterprise-solutions/configure-AWS-Bedrock-Admin",
"destination": "/enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration"
},
{
"source": "/enterprise-solutions/configure-AWS-Bedrock-Member",
"destination": "/enterprise-solutions/provider-remote-config/aws-bedrock/member-configuration"
},
{
"source": "/enterprise-solutions/configure-workOS-authkit",
"destination": "/enterprise-solutions/onboarding"
},
{
"source": "/enterprise-solutions/Onboarding your Organization",
"destination": "/enterprise-solutions/onboarding"
}
],
"search": {
@@ -0,0 +1,63 @@
---
title: "Managing Members"
sidebarTitle: "Managing Members"
description: "A guide to adding, removing, and editing members in your enterprise organization."
---
This guide covers the practical steps for adding, editing, and removing members from your enterprise dashboard. For a conceptual overview of roles and permissions, see the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions).
<Frame caption="The Members Dashboard provides a central place to manage your team.">
<img src="https://storage.googleapis.com/cline_public_images/members-dash.png" alt="Members Dashboard" />
</Frame>
## Adding Members
To invite someone to your organization, you must have an open seat available on your organization.
1. Navigate to the **Members** tab in your dashboard.
2. Click the **Add Members** button.
3. Enter one or more email addresses, separated by commas.
4. Select a role for the new member(s). It's best practice to start with the "Member" role unless you know they need admin privileges.
5. Click **Send Invitation**.
Invited users will receive an email with a link to join. You can cancel a pending invitation at any time by clicking the trash icon next to the user's email in the 'Pending Invites' section.
<Tip>
**Managing Users at Scale**
When inviting a large number of users, you can paste a comma-separated list of emails directly into the invitation field. While role changes and removals are performed individually, this bulk invitation feature helps streamline the onboarding process for entire teams.
</Tip>
<Frame caption="Adding members to your organization">
<img src="https://storage.googleapis.com/cline_public_images/adding-members.png" alt="Confirm Member Removal" />
</Frame>
## Editing Member Roles
As your team's needs change, you can adjust member roles directly from the dashboard.
- Find the member in your list.
- Under the "Role" column, click the dropdown menu.
- Select their new role. The change takes effect immediately.
Refer to the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions) for a detailed breakdown of what each role can do.
## Removing Members
Removing a member immediately revokes their access to all organization-specific resources, including shared API keys and configurations.
1. Go to the **Members Dashboard**.
2. Find the member in the list and click the red trash icon (<Icon icon="trash" iconType="solid" />).
3. Confirm the removal when prompted.
<Frame caption="You will be asked to confirm before a member is permanently removed.">
<img src="https://storage.googleapis.com/cline_public_images/remove-user.png" alt="Confirm Member Removal" />
</Frame>
## Troubleshooting Invitations
If an invited user is having trouble joining, check these common issues:
- **Invitation Not Received**: Ask the user to check their spam or junk mail folder. If it's not there, cancel the pending invitation and try sending it again, verifying the email address is correct.
- **"Invalid Domain" Error**: The user's email address must belong to a domain that has been verified for your organization. Work with your IT administrator to ensure the necessary domains are configured.
@@ -0,0 +1,63 @@
---
title: "Managing Members"
sidebarTitle: "Managing Members"
description: "A guide to adding, removing, and editing members in your enterprise organization."
---
This guide covers the practical steps for adding, editing, and removing members from your enterprise dashboard. For a conceptual overview of roles and permissions, see the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions).
<Frame caption="The Members Dashboard provides a central place to manage your team.">
<img src="https://storage.googleapis.com/cline_public_images/members-dash.png" alt="Members Dashboard" />
</Frame>
## Adding Members
To invite someone to your organization, you must have an open seat available on your organization.
1. Navigate to the **Members** tab in your dashboard.
2. Click the **Add Members** button.
3. Enter one or more email addresses, separated by commas.
4. Select a role for the new member(s). It's best practice to start with the "Member" role unless you know they need admin privileges.
5. Click **Send Invitation**.
Invited users will receive an email with a link to join. You can cancel a pending invitation at any time by clicking the trash icon next to the user's email in the 'Pending Invites' section.
<Tip>
**Managing Users at Scale**
When inviting a large number of users, you can paste a comma-separated list of emails directly into the invitation field. While role changes and removals are performed individually, this bulk invitation feature helps streamline the onboarding process for entire teams.
</Tip>
<Frame caption="Adding members to your organization">
<img src="https://storage.googleapis.com/cline_public_images/adding-members.png" alt="Confirm Member Removal" />
</Frame>
## Editing Member Roles
As your team's needs change, you can adjust member roles directly from the dashboard.
- Find the member in your list.
- Under the "Role" column, click the dropdown menu.
- Select their new role. The change takes effect immediately.
Refer to the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions) for a detailed breakdown of what each role can do.
## Removing Members
Removing a member immediately revokes their access to all organization-specific resources, including shared API keys and configurations.
1. Go to the **Members Dashboard**.
2. Find the member in the list and click the red trash icon (<Icon icon="trash" iconType="solid" />).
3. Confirm the removal when prompted.
<Frame caption="You will be asked to confirm before a member is permanently removed.">
<img src="https://storage.googleapis.com/cline_public_images/remove-user.png" alt="Confirm Member Removal" />
</Frame>
## Troubleshooting Invitations
If an invited user is having trouble joining, check these common issues:
- **Invitation Not Received**: Ask the user to check their spam or junk mail folder. If it's not there, cancel the pending invitation and try sending it again, verifying the email address is correct.
- **"Invalid Domain" Error**: The user's email address must belong to a domain that has been verified for your organization. Work with your IT administrator to ensure the necessary domains are configured.
@@ -0,0 +1,27 @@
---
title: "Members Overview"
sidebarTitle: "Overview"
description: "An overview of member management in your enterprise organization."
---
This section provides a comprehensive guide to managing members in your enterprise organization. Here, you'll find everything you need to know about roles, permissions, and the practical steps for adding, editing, and removing members from your dashboard.
## Key Topics
<CardGroup cols={2}>
<Card
title="Roles and Permissions"
icon="user-shield"
href="/enterprise-solutions/members/roles-and-permissions"
>
A detailed breakdown of the available roles and their specific permissions.
</Card>
<Card
title="Managing Members"
icon="users-gear"
href="/enterprise-solutions/members/managing-members"
>
A practical guide to adding, editing, and removing members from your
dashboard.
</Card>
</CardGroup>
@@ -0,0 +1,83 @@
---
title: "Roles and Permissions"
sidebarTitle: "Roles and Permissions"
description: "An overview of member roles, permissions, and best practices for your enterprise organization."
---
Choosing the right role for each member is crucial for maintaining security and ensuring your team can work effectively. This guide provides a detailed breakdown of the available roles, their specific permissions, and best practices for managing your organization.
## Role Definitions
Heres a summary of the available roles and their intended use cases.
<CardGroup cols={1}>
<Card title="Owner" icon="user-crown">
**Best for:** The primary account holder or a small number of designated leaders.
Owners have unrestricted access to all settings, including billing, member management, and security configurations. To maintain tight control over the organization, the number of Owners should be kept to a minimum.
</Card>
<Card title="Admin" icon="user-gear">
**Best for:** Team leads or IT administrators who need to manage users and configurations.
Admins can invite, edit, and remove members, as well as manage provider configurations. They have broad access but cannot manage billing or change the Owner. This is a suitable role for trusted team managers.
</Card>
<Card title="Member" icon="user">
**Best for:** Most developers and individual contributors.
Members can use Cline with the organization's shared resources but cannot change any settings or view other users' activity. This is the safest default role for new users.
</Card>
</CardGroup>
## Permissions Matrix
For a detailed comparison, this matrix outlines the specific capabilities of each role.
| Permission | Member | Admin | Owner |
| --------------------------- | :----: | :----: | :----: |
| **General Usage** | | | |
| Use Cline | ✅ | ✅ | ✅ |
| Access Shared API Providers | ✅ | ✅ | ✅ |
| | | | |
| **Member Management** | | | |
| View Members | ❌ | ✅ | ✅ |
| Invite New Members | ❌ | ✅ | ✅ |
| Edit Member Roles | ❌ | ✅ | ✅ |
| Remove Members | ❌ | ✅ | ✅ |
| Remove Admins | ❌ | ❌ | ✅ |
| | | | |
| **Configuration** | | | |
| Configure API Providers | ❌ | ✅ | ✅ |
| Manage Security Settings | ❌ | ❌ | ✅ |
| | | | |
| **Billing & Ownership** | | | |
| View Billing Information | ❌ | ❌ | ✅ |
| Manage Subscription | ❌ | ❌ | ✅ |
| Transfer Ownership | ❌ | ❌ | ✅ |
## Role Management Best Practices
Effective role management is fundamental to securing your organization.
- **Apply the Principle of Least Privilege**: Always assign the role with the minimum necessary permissions. Most users should be **Members**. Grant **Admin** rights only to those who are responsible for user management or technical configuration.
- **Limit the Number of Owners**: The **Owner** role should be reserved for one or two key individuals who control the account and billing. This centralization of power prevents accidental or malicious changes to critical settings.
- **Regularly Audit Roles**: Periodically review the list of Admins and Owners to ensure the assigned roles are still appropriate. When a team member's responsibilities change, adjust their role accordingly.
## Identity Providers and Domain Verification
For a user to successfully join and sign in to your organization, two conditions must be met:
1. Their email must be managed by your organization's verified **Identity Provider (IDP)**, such as Microsoft Entra ID, Okta, or AWS.
2. Your organization must have a **verified domain** with a provider like Google or Microsoft.
This ensures that only authenticated users from your company can access your Cline organization.
## Seat Management and Invitations
Each user in your organization, regardless of role, consumes one seat from your license.
- When an invitation is sent, a seat is considered "pending."
- If an invited user does not accept, the invitation can be revoked to free up the seat.
- Removing a member from the organization immediately frees up a seat.
Now that you understand the different roles and how to manage them, you can proceed to [configuring provider remote access](/enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration) for your organization.
+128
View File
@@ -0,0 +1,128 @@
---
title: "Onboarding"
description: "This guide explains how administrators configure SSO provisioning and user management in Cline Enterprise."
---
## Overview
Cline Enterprise integrates with your existing identity provider (IdP) via WorkOS to deliver secure SSO and zero-touch user lifecycle management. In this guide, you'll connect your IdP (Okta, Azure AD, Google Workspace, or any SAML/OIDC provider), enable just-in-time (JIT) provisioning so new users are created automatically on first sign-in, and configure role mapping so permissions stay aligned with your directory—no manual invites or seat reconciliations required.
## Prerequisites
- [Cline Enterprise License](https://cline.bot/enterprise)
- Access to your identity provider (IdP) configuration (e.g., Okta, Azure AD, Google Workspace)
- Knowledge of your organization's SSO requirements
## Configuration Steps
### Step 1: Onboard to Cline Enterprise license
Your IdP administrator will receive an email with a link to register their organization with WorkOS during onboarding.
### Step 2: Configure Your Identity Provider
Connect your identity provider (IdP) to WorkOS:
1. In the WorkOS dashboard, go to **AuthKit → Connections**
2. Click **Add Connection**
3. Select your identity provider (e.g., Okta, Azure AD, Google Workspace, Generic SAML/OIDC)
4. Follow the provider-specific setup instructions
Each identity provider (IdP) will have its own setup process and required fields. Be sure to follow the specific instructions in the WorkOS dashboard for your chosen provider.
For more explicit instruction on connecting your IdP, refer to the [WorkOS SSO documentation](https://workos.com/docs/authkit/sso)
### Step 3: Configure User Provisioning
Cline Enterprise uses **just-in-time provisioning** that works automatically:
- **Organizations are created automatically**
- **Users gain access automatically** on their first SSO sign-in, once their credentials have been configured by the IdP administrator.
- **Roles sync automatically** from your IdP (Admin/Owner → Admin, Member → Member)
- **No manual user invites or seat management** required
No additional configuration is needed. Users are provisioned automatically when they sign in through SSO.
### Step 4: Configure User Attributes Mapping
User roles are mapped automatically from your IdP:
- **Admin** in IdP → **Admin** role in Cline (Note: The first Owner of the org is created manually during onboarding)
- **Member** in IdP → **Member** role in Cline
<Info>
For what each role can access, see the [Roles and Permissions](./members/roles-and-permissions) page.
</Info>
If needed, you can configure additional user attributes in the Cline Admin console:
1. Go to **Settings → Authentication → User Attributes**
2. Map attributes such as email and name based on your IdP configuration
For information about available user attributes, see the [WorkOS User Object Documentation](https://workos.com/docs/authkit/user-management).
### Step 5: Test SSO Connection
Before allowing users to sign in, test the SSO flow to ensure everything is configured correctly.
**To test the connection:**
1. In the WorkOS dashboard (or Cline Admin console if available), locate and click **Test SSO Connection**
2. You'll be redirected to your IdP's login page
3. Enter valid credentials for a test user
4. After successful authentication, you should be redirected back
5. Confirm that the user's information (name, email, role) displays correctly
**Expected outcome:** The test user is authenticated, their account details are visible, and their role matches what's configured in your IdP.
**If the test fails:** Double-check your IdP configuration (redirect URIs, SAML certificates, attribute mappings). See the [WorkOS SSO documentation](https://workos.com/docs/authkit/sso) for troubleshooting guidance.
### User Access
Once SSO is configured, users in your IdP can access Cline automatically without manual invites or account setup.
**First-time sign-in flow:**
1. User navigates to Cline and clicks **Sign in with SSO**
2. User authenticates via your organization's IdP
3. Cline automatically creates their account in your Organization
4. Role is assigned based on their IdP role (see [Step 4](#step-4-configure-user-attributes-mapping))
5. User is redirected to Cline and can begin working
**What happens automatically:**
- Account creation with correct organization assignment
- Role and permission assignment
- Basic profile information (name, email) populated from IdP
**No action required:** Users don't need to request access or wait for approval. Access is granted immediately upon successful IdP authentication.
### Managing Access
All access management and revocation of users is currently handled by your IdP:
- Add users → access granted automatically on first login
- Change roles → updated on next login
- Remove users → access revoked automatically
<Info>
Role changes sync automatically on the user's next sign-in.
</Info>
### Changing your IdP
In order to change to a different IdP, please contact support and we will guide you through this process.
---
## Verification
Steps to verify successful configuration:
1. **Test User Sign-In**: Have a test user sign in through the SSO flow (access is granted automatically on first login)
2. **Verify User Provisioning**: Confirm that the user is automatically created and has appropriate role permissions
3. **Check User Attributes**: Verify that user information (name, email, organization) is correctly populated
4. **Test Role Changes**: Update a user's role in your IdP and verify it syncs on their next login
5. **Test User Deprovisioning**: Remove a user from your IdP and verify they lose access to Cline on their next login attempt
6. **Review Audit Logs**: Check WorkOS audit logs to ensure authentication events are being recorded
---
@@ -0,0 +1,127 @@
---
title: "Configure AWS Bedrock Provider (Admin)"
sidebarTitle: "Configure AWS Bedrock (Admin)"
description: "This guide explains how administrators configure AWS Bedrock as the organization-wide LLM provider for Cline."
---
As an administrator, you can add AWS Bedrock as the organization-wide LLM provider for all Cline users. This centralized approach ensures consistent access to Amazon's AI models while maintaining your organization's security and compliance requirements through VPC endpoints, region controls, and prompt caching optimizations.
## Before You Begin
To get started with setting up AWS Bedrock as your organization's LLM provider, you'll need a few items in place.
**Administrator access to the Cline Admin console**
You need admin privileges to enforce provider settings across your organization. If you can navigate to **Settings → Cline Settings** in the admin console at [app.cline.bot](https://app.cline.bot), you have the right access level.
<Info>
**Quick Check**: Try accessing the settings page now. If you can see the provider configuration options, you're good to go.
</Info>
**AWS Bedrock account with the right permissions**
Your AWS account needs specific Bedrock permissions to work with Cline.
<Note>
If you don't have direct AWS access, coordinate with your cloud team to get these permissions set up before proceeding.
</Note>
**Your preferred AWS region**
Choose your primary AWS region carefully since this will be enforced for all users.
<Tip>
Check which models are available in your region first. Some newer models might not be available in all regions yet.
</Tip>
<Frame>
<img
src="https://storage.googleapis.com/cline-static-assets-prod/assets/AWS%20Remote%20Config.gif"
/>
</Frame>
## Configuration Steps
<Steps>
<Step title="Access Cline Settings">
Navigate to [app.cline.bot](https://app.cline.bot) and sign in with your administrator account. Go to **Settings → Cline Settings**.
<Info>
You should see the provider configuration options if you have the correct admin access level.
</Info>
</Step>
<Step title="Enable Remote Provider Configuration">
Toggle on **Enable settings** to reveal the remote provider configuration options. This allows you to enforce provider settings across your organization.
</Step>
<Step title="Select AWS Bedrock as the API Provider">
Open the **API Provider** dropdown menu and select **Amazon Bedrock**. This will open the Bedrock configuration panel where you'll configure all your organization-wide settings.
</Step>
<Step title="Configure Bedrock Settings">
The configuration panel includes several settings that control how Bedrock works for your organization. Configure what you need:
<AccordionGroup>
<Accordion title="Region (required)">
Enter your preferred AWS region like `us-west-2` or `us-east-1`. This region will be enforced for all organization members.
[View AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
<Tip>
For most organizations, `us-east-1` or `us-west-2` are recommended as they have the best model availability.
</Tip>
</Accordion>
<Accordion title="Custom VPC Endpoint (optional)">
If your organization uses a private VPC endpoint for Bedrock, specify it here to ensure all API calls go through your network infrastructure.
[Learn more about AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-overview.html)
</Accordion>
<Accordion title="Cross-region Inference (optional)">
Enable this to let Bedrock automatically route requests to other regions when your primary region has capacity constraints. Useful for maintaining availability during high-demand periods.
[Learn more about Inference Profiles](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html)
</Accordion>
<Accordion title="Global Inference Profile (optional)">
Turn this on to use AWS's global inference routing, which automatically directs requests to the optimal region based on availability and latency.
</Accordion>
<Accordion title="Prompt Caching (optional)">
Enable prompt caching to reduce costs and latency. Bedrock caches portions of prompts that remain consistent across requests, making repeated interactions faster and cheaper.
[Learn more about Prompt Caching](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html)
</Accordion>
</AccordionGroup>
</Step>
<Step title="Save Configuration">
After configuring your settings, close the provider configuration panel and click **Save** on the settings page to persist your changes.
Once saved, all organization members signed into the Cline extension will automatically use AWS Bedrock with your configured settings. They won't be able to select other providers or switch to their personal Cline accounts.
<Warning>
Members can't switch to personal Cline accounts or join other organizations once remote configuration is enabled. This ensures consistent provider usage across your team.
</Warning>
</Step>
</Steps>
## Verification
To verify the configuration:
1. Check that the provider shows as "Amazon Bedrock" in the Enabled provider field
2. Confirm the settings persist after refreshing the page
3. Test with a member account to ensure they see only Bedrock as a provider
## Troubleshooting
**Members don't see the configured provider**
Ensure you clicked Save after closing the configuration panel. Verify the member account belongs to the correct organization.
**Configuration changes don't persist**
Make sure to click the Save button on the main settings page, not just close the configuration panel.
**Need to change regions later**
You can update the region at any time. Members will need to ensure their local AWS credentials have access to the new region. For more information, refer to the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
For further details, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your internal cloud team.
@@ -0,0 +1,131 @@
---
title: "Configure AWS Bedrock in VS Code (Members)"
sidebarTitle: "Configure AWS Bedrock (Member)"
description: "Guide for engineers configuring AWS Bedrock credentials in VS Code after admin setup"
---
As a team member, you can connect your local development environment to your organization's AWS Bedrock setup. This guide walks you through configuring your AWS credentials in VS Code so you can start using models through your organization's Bedrock infrastructure. Your administrator has already configured the provider settings—you just need to add your credentials to get started.
## Before You Begin
To successfully connect to your organization's AWS Bedrock setup, you'll need a few things ready.
**Cline extension installed and configured**
The Cline extension must be installed in VS Code and you need to be signed into your organization account. If you haven't installed Cline yet, follow our [installation guide](/getting-started/installing-cline).
<Info>
**Quick Check**: Open the Cline panel in VS Code. If you see your organization name in the bottom left, you're signed in correctly.
</Info>
**AWS credentials with Bedrock access**
You need AWS credentials that have permission to access Bedrock in your organization's configured region.
<Note>
If you don't have AWS credentials yet, reach out to your IT or cloud team to get access keys or AWS CLI profiles configured with the necessary Bedrock permissions.
</Note>
<Frame>
<img
src="https://storage.googleapis.com/cline-static-assets-prod/assets/VS%20Code%20Bedrock%20API%20Key.gif"
/>
</Frame>
## Configuration Steps
<Steps>
<Step title="Open Cline Settings">
Open VS Code and access the Cline settings panel using either of these methods:
- Click the settings icon (⚙️) in the Cline panel
- Click on the API Provider dropdown located directly below the chat area (it will display as `bedrock.anthropic.claude-sonnet-4-20250514-v1:0` or similar)
</Step>
<Step title="Select Your Authentication Method">
Choose one of the following credential methods to authenticate with AWS Bedrock:
<AccordionGroup>
<Accordion title="AWS Bedrock API Key">
Use dedicated AWS access keys specifically for Bedrock access.
[Learn more about AWS Bedrock API Keys](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html)
1. Select the **API Key** radio button
2. Enter your AWS Access Key ID and Secret Access Key
3. These credentials are stored locally and used only by the VS Code extension
</Accordion>
<Accordion title="AWS Profile">
Use an existing AWS CLI profile configured on your machine.
[Learn more about AWS CLI Profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html)
1. Select the **AWS Profile** radio button
2. Choose or enter the profile name from your `~/.aws/credentials` file
3. Cline will use the credentials associated with that profile
</Accordion>
<Accordion title="AWS Credentials">
Use your default AWS credential chain (environment variables, EC2 instance roles, etc.).
1. Select the **AWS Credentials** radio button
2. Cline will automatically detect credentials from your environment using the standard AWS credential provider chain
</Accordion>
</AccordionGroup>
<Note>
The AWS Region is preconfigured by your administrator and does not need to be set in the extension.
</Note>
</Step>
<Step title="Verify Configuration">
After selecting your authentication method, the extension will display checkmarks for enabled features:
- ✓ Supports images
- ✓ Supports browser use
- ✓ Supports prompt caching
Additional settings like cross-region inference and global inference profile will be locked (shown with a lock icon 🔒) as they're controlled by your administrator.
</Step>
<Step title="Test the Connection">
Send a test message in Cline to verify your credentials work correctly with the configured Bedrock region.
<Tip>
**Testing Recommendation**
It is recommended to test the connection in plan mode to verify everything works correctly before using it for actual tasks.
</Tip>
</Step>
</Steps>
## Troubleshooting
**Authentication errors ("Access Denied" or "Invalid Credentials")**
Verify your chosen credential method has the necessary IAM permissions to call Bedrock in the configured region. Required permissions include `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream`. For more information, refer to [AWS Bedrock IAM Permissions](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html).
**Region-related errors or "model not available"**
Ask your administrator to confirm which region is configured for your organization. Ensure your AWS credentials have access to Bedrock in that specific region. [View AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
**Don't see AWS Bedrock as an option**
Confirm you're signed into the correct Cline organization. Verify your administrator has saved the Bedrock configuration. Try signing out and back into the extension.
**AWS Credentials option not finding credentials**
Verify AWS CLI is installed and configured with `aws configure` ([AWS CLI Installation Guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). Check that credentials are present in `~/.aws/credentials`. For EC2/ECS environments, ensure IAM roles are properly attached. If using environment variables, set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
## Security Best Practices
When configuring your AWS credentials, follow these security guidelines:
- Use IAM roles with minimum required permissions ([AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html))
- Rotate access keys regularly if using the API Key method
- Never store credentials in code or version control
- Prefer AWS Profile method for better credential management
- Consider using AWS SSO/federated roles for enhanced security
Your organization administrator controls which models are available. The extension will automatically display available models based on your region's Bedrock configuration. For more information about available models, refer to the [AWS Bedrock Model Access documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html).
For further assistance, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your organization's cloud administrator.
@@ -1,61 +0,0 @@
---
title: "Security Concerns"
---
## Enterprise Security with Cline
Cline addresses enterprise security concerns through its unique client-side architecture that prioritizes data privacy, secure cloud integration, and transparent operations. Below is a comprehensive overview of how Cline maintains robust security measures for enterprise environments.
### Client-Side Architecture
Cline operates exclusively as a client-side VSCode extension with zero server-side components. This fundamental design choice ensures that your code and data remain within your secure environment at all times. Unlike traditional AI assistants that send data to external servers for processing, Cline connects directly to your chosen cloud provider's AI endpoints, keeping all sensitive information within your infrastructure boundaries.
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-arch.png"
alt="Cline's relationship to local and remote assets"
/>
</Frame>
### Data Privacy Commitment
Cline implements a strict zero data retention policy, meaning your intellectual property never leaves your secure environment. The extension does not collect, store, or transmit your code to any central servers. This approach significantly reduces potential attack vectors that might otherwise be introduced through data transmission to third-party systems. Telemetry collection is optional and requires explicit consent.
### Cloud Provider Integration
Enterprise teams can access cutting-edge AI models through their existing cloud deployments. Cline supports seamless integration with:
- AWS Bedrock
- Google Cloud Vertex AI
- Microsoft Azure
These integrations utilize your organization's existing security credentials, including native IAM role assumption for AWS. This ensures that all AI processing occurs within your corporate cloud environment, maintaining compliance with your established security protocols.
### Open-Source Transparency
Cline's codebase is completely open-source, allowing for comprehensive security auditing by your internal teams. This transparency enables security professionals to verify exactly how the extension functions and confirm that it adheres to your organization's security requirements. Organizations can review the code to ensure it aligns with their security policies before deployment.
### Controlled Modifications
The extension implements safeguards against unauthorized changes to your codebase. Cline requires explicit user approval for all file modifications and terminal commands, preventing accidental or unwanted alterations. This approval-based workflow maintains the integrity of your projects while still providing AI assistance.
### Enterprise Deployment Support
For organizations with strict security review processes, Cline provides comprehensive documentation including detailed deployment diagrams, sequence diagrams illustrating all data flows, and complete security posture documentation. These materials facilitate thorough security reviews and help demonstrate compliance with enterprise data handling standards and regulations.
### Access Control
Enterprise editions of Cline (planned for Q2 2025) will include centralized administration features that allow organizations to:
- Manage user access with customizable permission levels
- Provision accounts with corporate credentials
- Immediately revoke access when needed
- Control which AI providers and LLM endpoints can be used
- Deploy standardized settings across the organization
- Prevent unauthorized use of personal API keys
### Compliance and Governance
Cline's architecture supports compliance with data sovereignty requirements and enterprise data handling regulations. The planned Enterprise Complete edition will further enhance governance with detailed audit logging, compliance reporting, and automated policy enforcement mechanisms.
By combining client-side processing, direct cloud provider integration, and transparent operations, Cline offers enterprise teams a secure way to leverage AI assistance while maintaining strict control over their sensitive code and data.
-445
View File
@@ -1,445 +0,0 @@
---
title: "Workflows"
sidebarTitle: "Workflows"
---
Workflows allow you to define a series of steps to guide Cline through a repetitive set of tasks, such as deploying a service or submitting a PR.
To invoke a workflow, type `/[workflow-name.md]` in the chat.
## How to Create and Use Workflows
Workflows live alongside [Cline Rules](/features/cline-rules). Creating one is straightforward:
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/workflows.png" alt="Workflows tab in Cline" />
</Frame>
1. Create a markdown file with clear instructions for the steps Cline should take
2. Save it with a `.md` extension in your workflows directory
3. To trigger a workflow, just type `/` followed by the workflow filename
4. Provide any required parameters when prompted
The real power comes from how you structure your workflow files. You can:
- Leverage Cline's [built-in tools](/exploring-clines-tools/cline-tools-guide) like `ask_followup_question`, `read_file`, `search_files`, and `new_task`
- Use command-line tools you already have installed like `gh` or `docker`
- Reference external [MCP tool calls](/mcp/mcp-overview) like Slack or Whatsapp
- Chain multiple actions together in a specific sequence
## Real-world Example
I created a PR Review workflow that's already saving me tons of time.
````md pr-review.md [expandable]
You have access to the `gh` terminal command. I already authenticated it for you. Please review it to use the PR that I asked you to review. You're already in the `cline` repo.
<detailed_sequence_of_steps>
# GitHub PR Review Process - Detailed Sequence of Steps
## 1. Gather PR Information
1. Get the PR title, description, and comments:
```bash
gh pr view <PR-number> --json title,body,comments
```
2. Get the full diff of the PR:
```bash
gh pr diff <PR-number>
```
## 2. Understand the Context
1. Identify which files were modified in the PR:
```bash
gh pr view <PR-number> --json files
```
2. Examine the original files in the main branch to understand the context:
```xml
<read_file>
<path>path/to/file</path>
</read_file>
```
3. For specific sections of a file, you can use search_files:
```xml
<search_files>
<path>path/to/directory</path>
<regex>search term</regex>
<file_pattern>*.ts</file_pattern>
</search_files>
```
## 3. Analyze the Changes
1. For each modified file, understand:
- What was changed
- Why it was changed (based on PR description)
- How it affects the codebase
- Potential side effects
2. Look for:
- Code quality issues
- Potential bugs
- Performance implications
- Security concerns
- Test coverage
## 4. Ask for User Confirmation
1. Before making a decision, ask the user if you should approve the PR, providing your assessment and justification:
```xml
<ask_followup_question>
<question>Based on my review of PR #<PR-number>, I recommend [approving/requesting changes]. Here's my justification:
[Detailed justification with key points about the PR quality, implementation, and any concerns]
Would you like me to proceed with this recommendation?</question>
<options>["Yes, approve the PR", "Yes, request changes", "No, I'd like to discuss further"]</options>
</ask_followup_question>
```
## 5. Ask if User Wants a Comment Drafted
1. After the user decides on approval/rejection, ask if they would like a comment drafted:
```xml
<ask_followup_question>
<question>Would you like me to draft a comment for this PR that you can copy and paste?</question>
<options>["Yes, please draft a comment", "No, I'll handle the comment myself"]</options>
</ask_followup_question>
```
2. If the user wants a comment drafted, provide a well-structured comment they can copy:
```
Thank you for this PR! Here's my assessment:
[Detailed assessment with key points about the PR quality, implementation, and any suggestions]
[Include specific feedback on code quality, functionality, and testing]
```
## 6. Make a Decision
1. Approve the PR if it meets quality standards:
```bash
# For single-line comments:
gh pr review <PR-number> --approve --body "Your approval message"
# For multi-line comments with proper whitespace formatting:
cat << EOF | gh pr review <PR-number> --approve --body-file -
Thanks @username for this PR! The implementation looks good.
I particularly like how you've handled X and Y.
Great work!
EOF
```
2. Request changes if improvements are needed:
```bash
# For single-line comments:
gh pr review <PR-number> --request-changes --body "Your feedback message"
# For multi-line comments with proper whitespace formatting:
cat << EOF | gh pr review <PR-number> --request-changes --body-file -
Thanks @username for this PR!
The implementation looks promising, but there are a few things to address:
1. Issue one
2. Issue two
Please make these changes and we can merge this.
EOF
```
Note: The `cat << EOF | ... --body-file -` approach preserves all whitespace and formatting without requiring temporary files. The `-` parameter tells the command to read from standard input.
</detailed_sequence_of_steps>
<example_review_process>
# Example PR Review Process
Let's walk through a real example of reviewing PR #3627 which fixes the thinking mode calculation for Claude 3.7 models.
## Step 1: Gather PR Information
```bash
# Get PR details
gh pr view 3627 --json title,body,comments
# Get the full diff
gh pr diff 3627
```
## Step 2: Understand the Context
```xml
# Examine the original files to understand what's being changed
<read_file>
<path>src/shared/api.ts</path>
</read_file>
# Look at the ThinkingBudgetSlider component implementation
<read_file>
<path>webview-ui/src/components/settings/ThinkingBudgetSlider.tsx</path>
</read_file>
# Check how thinking mode is currently handled in API providers
<search_files>
<path>src/api/providers</path>
<regex>reasoningOn</regex>
<file_pattern>*.ts</file_pattern>
</search_files>
```
## Step 3: Analyze the Changes
From the PR information and code examination, we can see that:
1. The PR fixes an issue with the thinking mode budget calculation for Claude 3.7 models
2. Currently, the thinking budget is incorrectly calculated as 80% of maxTokens (8192), resulting in 6553 tokens
3. Claude 3.7 actually supports a much larger thinking budget (up to 64000 tokens)
4. The PR adds a `thinkingConfig` property with `maxBudget: 64000` to model definitions
5. It updates API handlers to use this value when reasoning mode is enabled
6. It enhances the slider component to calculate max values based on model-specific percentages
7. Comprehensive tests were added to verify the calculations
## Step 4: Ask for User Confirmation
```xml
<ask_followup_question>
<question>Based on my review of PR #3627, I recommend approving this PR. Here's my justification:
1. The PR correctly fixes the thinking mode budget calculation for Claude 3.7 models
2. It properly implements the thinkingConfig.maxBudget property (64000) for Claude 3.7 models
3. It applies the appropriate percentage (50%) for the slider calculation
4. The changes are well-tested with comprehensive unit tests
5. The implementation is clean and follows the project's coding standards
6. The PR includes helpful screenshots demonstrating the fix
Would you like me to proceed with approving this PR?</question>
<options>["Yes, approve the PR", "No, I'd like to discuss further", "Let me review it myself first"]</options>
</ask_followup_question>
```
## Step 5: Ask if User Wants a Comment Drafted
```xml
<ask_followup_question>
<question>Would you like me to draft a comment for this PR that you can copy and paste?</question>
<options>["Yes, please draft a comment", "No, I'll handle the comment myself"]</options>
</ask_followup_question>
```
## Step 6: Make a Decision
```bash
# Option 1: Simple one-line comment
gh pr review 3627 --approve --body "This PR looks good! It correctly fixes the thinking mode budget calculation for Claude 3.7 models."
# Option 2: Multi-line comment with proper whitespace formatting
cat << EOF | gh pr review 3627 --approve --body-file -
This PR looks good! It correctly fixes the thinking mode budget calculation for Claude 3.7 models.
I particularly like:
1. The proper implementation of thinkingConfig.maxBudget property (64000)
2. The appropriate percentage (50%) for the slider calculation
3. The comprehensive unit tests
4. The clean implementation that follows project coding standards
Great work!
EOF
```
</example_review_process>
<common_gh_commands>
# Common GitHub CLI Commands for PR Review
## Basic PR Commands
```bash
# List open PRs
gh pr list
# View a specific PR
gh pr view <PR-number>
# View PR with specific fields
gh pr view <PR-number> --json title,body,comments,files,commits
# Check PR status
gh pr status
```
## Diff and File Commands
```bash
# Get the full diff of a PR
gh pr diff <PR-number>
# List files changed in a PR
gh pr view <PR-number> --json files
# Check out a PR locally
gh pr checkout <PR-number>
```
## Review Commands
```bash
# Approve a PR (single-line comment)
gh pr review <PR-number> --approve --body "Your approval message"
# Approve a PR (multi-line comment with proper whitespace)
cat << EOF | gh pr review <PR-number> --approve --body-file -
Your multi-line
approval message with
proper whitespace formatting
EOF
# Request changes on a PR (single-line comment)
gh pr review <PR-number> --request-changes --body "Your feedback message"
# Request changes on a PR (multi-line comment with proper whitespace)
cat << EOF | gh pr review <PR-number> --request-changes --body-file -
Your multi-line
change request with
proper whitespace formatting
EOF
# Add a comment review (without approval/rejection)
gh pr review <PR-number> --comment --body "Your comment message"
# Add a comment review with proper whitespace
cat << EOF | gh pr review <PR-number> --comment --body-file -
Your multi-line
comment with
proper whitespace formatting
EOF
```
## Additional Commands
```bash
# View PR checks status
gh pr checks <PR-number>
# View PR commits
gh pr view <PR-number> --json commits
# Merge a PR (if you have permission)
gh pr merge <PR-number> --merge
```
</common_gh_commands>
<general_guidelines_for_commenting>
When reviewing a PR, please talk normally and like a friendly reviwer. You should keep it short, and start out by thanking the author of the pr and @ mentioning them.
Whether or not you approve the PR, you should then give a quick summary of the changes without being too verbose or definitive, staying humble like that this is your understanding of the changes. Kind of how I'm talking to you right now.
If you have any suggestions, or things that need to be changed, request changes instead of approving the PR.
Leaving inline comments in code is good, but only do so if you have something specific to say about the code. And make sure you leave those comments first, and then request changes in the PR with a short comment explaining the overall theme of what you're asking them to change.
</general_guidelines_for_commenting>
<example_comments_that_i_have_written_before>
<brief_approve_comment>
Looks good, though we should make this generic for all providers & models at some point
</brief_approve_comment>
<brief_approve_comment>
Will this work for models that may not match across OR/Gemini? Like the thinking models?
</brief_approve_comment>
<approve_comment>
This looks great! I like how you've handled the global endpoint support - adding it to the ModelInfo interface makes total sense since it's just another capability flag, similar to how we handle other model features.
The filtered model list approach is clean and will be easier to maintain than hardcoding which models work with global endpoints. And bumping the genai library was obviously needed for this to work.
Thanks for adding the docs about the limitations too - good for users to know they can't use context caches with global endpoints but might get fewer 429 errors.
</approve_comment>
<requesst_changes_comment>
This is awesome. Thanks @scottsus.
My main concern though - does this work for all the possible VS Code themes? We struggled with this initially which is why it's not super styled currently. Please test and share screenshots with the different themes to make sure before we can merge
</request_changes_comment>
<request_changes_comment>
Hey, the PR looks good overall but I'm concerned about removing those timeouts. Those were probably there for a reason - VSCode's UI can be finicky with timing.
Could you add back the timeouts after focusing the sidebar? Something like:
```typescript
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
await setTimeoutPromise(100) // Give UI time to update
visibleWebview = WebviewProvider.getSidebarInstance()
```
</request_changes_comment>
<request_changes_comment>
Heya @alejandropta thanks for working on this!
A few notes:
1 - Adding additional info to the environment variables is fairly problematic because env variables get appended to **every single message**. I don't think this is justifiable for a somewhat niche use case.
2 - Adding this option to settings to include that could be an option, but we want our options to be simple and straightforward for new users
3 - We're working on revisualizing the way our settings page is displayed/organized, and this could potentially be reconciled once that is in and our settings page is more clearly delineated.
So until the settings page is update, and this is added to settings in a way that's clean and doesn't confuse new users, I don't think we can merge this. Please bear with us.
</request_changes_comment>
<request_changes_comment>
Also, don't forget to add a changeset since this fixes a user-facing bug.
The architectural change is solid - moving the focus logic to the command handlers makes sense. Just don't want to introduce subtle timing issues by removing those timeouts.
</request_changes_comment>
</example_comments_that_i_have_written_before>
````
When I get a new PR to review, I used to manually gather context: checking the PR description, examining the diff, looking at surrounding files, and finally forming an opinion. Now I just:
1. Type `/pr-review.md` in chat
2. Paste in the PR number
3. Let Cline handle everything else
My workflow uses the `gh` command-line tool and Cline's built in `ask_followup_question` to:
- Pull the PR description and comments
- Examine the diff
- Check surrounding files for context
- Analyze potential issues
- Asks me if it's cool approve it if everything looks good, with justification for why it should be approved
- If I say "yes," Cline automatically approves the PR with the `gh` command
This has taken my PR review process from a manual, multi-step operation to a single command that gives me everything I need to make an informed decision.
> This is just one example of a workflow file. You can find more in our [prompts repository](https://github.com/cline/prompts) for inspiration.
## Building Your Own Workflows
The beauty of workflows is they're completely customizable to your needs. You might create workflows for all kinds of repetitive tasks:
- For releases, you could have a workflow that grabs all merged PRs, builds a changelog, and handles version bumps.
- Setting up new projects is perfect for workflows. Just run one command to create your folder structure, install dependencies, and set up configs.
- Need to create a report? Create a workflow that grabs stats from different sources and formats them exactly how you like. You can even visualize them with a charting library and then make a presentation out of it with a library like [slidev](https://sli.dev/).
- You can even use workflows to draft messages to your team using an MCP server like Slack or Whatsapp after you submit a PR.
With Workflows, your imagination is the limit. The true potential comes from spotting those annoying repetitive tasks you do all the time.
If you can describe something as "first I do X, then Y, then Z" - that's a perfect workflow candidate.
Start with something small that bugs you, turn it into a workflow, and keep refining it. You'll be shocked how much of your day can be automated this way.
@@ -0,0 +1,135 @@
---
title: "Workflows Best Practices"
sidebarTitle: "Best Practices"
description: "Tips and strategies for creating effective and reliable Cline workflows."
---
Creating effective workflows requires a balance of clear instructions, modular design, and intelligent tool usage. Follow these best practices to get the most out of Cline's automation capabilities.
## Use Cline to Build Workflows
We highly recommend using Cline to help you build your workflows. Since Cline understands your project's context and structure, it can be an invaluable partner in designing automation that fits your specific needs.
### Building your own workflows
Creating a workflow is simpler than you might think. There's actually a workflow for building workflows!
First, **save the [create-new-workflow.md](https://github.com/cline/prompts/blob/main/workflows/create-new-workflow.md) file to your workspace** (e.g., in `.clinerules/workflows/`).
Then, type `/create-new-workflow.md` and Cline guides you through it:
1. It asks for the purpose and a concise name.
2. You describe the objective and expected outputs.
3. You list the major steps (Cline can help determine details).
4. It generates the properly structured workflow file.
<Tip>
**Automate Your History:** The best workflows come from tasks you've already done. After completing something you'll need to repeat, tell Cline: "Create a workflow for the process I just completed." It analyzes the conversation, identifies the steps, and generates the workflow file. Your accumulated context becomes reusable automation.
</Tip>
Workflows live in `.clinerules/workflows/` for project-specific ones or `~/Documents/Cline/Workflows/` for global ones you use across projects. Project workflows take precedence when names match.
## Workflow Design
<Tip>
**Start Simple:** Begin with small, single-task workflows. As you get comfortable, you can combine them or create more complex sequences.
</Tip>
### Be Modular
Instead of creating one massive workflow file, break complex tasks into smaller, reusable workflows. This makes them easier to maintain and debug.
### Use Clear Comments
Just like with code, commenting your workflow steps is crucial. Explain *why* a step is happening, not just *what* is happening. This helps both you (the future maintainer) and Cline understand the intent.
### Version Control
Treat your workflows as part of your codebase. Store them in your Git repository (in `.clinerules/workflows/`) so they are versioned, reviewed, and shared with your team.
## Prompt Engineering for Cline
### Be Specific with Tool Use
Don't just say "find the file." Be explicit about which tool Cline should use.
* **Bad:** "Find the user controller."
* **Good:** "Use `search_files` to look for `UserController` in the `src/controllers` directory."
## Advanced Techniques
### Available Tools
Cline has a powerful set of tools you can use within your workflows. Here are the most common ones:
#### execute_command
Executes a CLI command on your system. Use this for running tests, builds, git commands, or any other terminal operation.
```xml
<execute_command>
<command>npm run test</command>
<requires_approval>false</requires_approval>
</execute_command>
```
#### read_file
Reads the contents of a file. Essential for analyzing code or configuration.
```xml
<read_file>
<path>src/config.json</path>
</read_file>
```
#### write_to_file
Creates or overwrites a file. Use this to generate boilerplate, config files, or documentation.
```xml
<write_to_file>
<path>src/components/Button.tsx</path>
<content>
// File content goes here...
</content>
</write_to_file>
```
#### search_files
Searches for a regex pattern across files in a directory. Great for finding TODOs, usage examples, or specific code patterns.
```xml
<search_files>
<path>src</path>
<regex>TODO</regex>
<file_pattern>*.ts</file_pattern>
</search_files>
```
#### ask_followup_question
Asks the user for input or confirmation. This makes your workflow interactive and allows for human-in-the-loop decision making.
```xml
<ask_followup_question>
<question>Do you want to deploy to production?</question>
<options>["Yes", "No"]</options>
</ask_followup_question>
```
#### browser_action
Controls a built-in browser to interact with websites or local servers. Useful for testing web UIs or scraping data.
```xml
<browser_action>
<action>launch</action>
<url>http://localhost:3000</url>
</browser_action>
```
### Leverage MCP Tools
You can use Model Context Protocol (MCP) tools within your workflows to interact with external services like GitHub, Slack, or databases. This allows you to create powerful end-to-end automations.
### Manage Context Window
Be mindful of Cline's context window. If a workflow is too long or processes too much data, it might exceed the token limit.
* **Break it down:** Split long workflows into smaller parts.
* **Be concise:** Keep instructions clear and to the point.
## Learn More
<Card title="Cline Learn" icon="lightbulb" href="https://cline.bot/learn">
Dive deeper into general prompt engineering strategies to write even better instructions for Cline.
</Card>
@@ -0,0 +1,139 @@
---
title: "Workflows Overview"
sidebarTitle: "Overview"
description: "Learn what Cline workflows are, why they are useful, and how to structure them."
---
Workflows in Cline are Markdown files that define a series of steps to guide Cline through repetitive or complex tasks. They are a powerful way to automate your development processes directly within your editor.
To invoke a workflow, you simply type `/` followed by the workflow's filename in the chat (e.g., `/deploy.md`).
## Why Use Cline Workflows?
* **Automation:** Automate repetitive tasks like setting up a new project, deploying a service, or running a specific test suite.
* **Consistency:** Ensure that tasks are performed the same way every time, reducing errors.
* **Reduced Cognitive Load:** Don't waste mental energy remembering complex sequences of commands or steps.
* **Contextual:** Workflows run within your project's context, so Cline has access to your files and can use its tools to interact with them.
## How They Work
A workflow file is a standard Markdown file with a `.md` extension. Cline reads this file and interprets the instructions step-by-step. The real power comes from Cline's ability to use its built-in tools and other capabilities within these instructions:
* **Cline Tools:** Use tools like `read_file`, `write_to_file`, `execute_command`, and `ask_followup_question`.
* **Command-Line Tools:** Instruct Cline to use any CLI tool installed on your machine (e.g., `git`, `gh`, `npm`, `docker`).
* **MCP Tools:** Reference tools from connected Model Context Protocol (MCP) servers.
## Workflows vs. Rules
It's important to understand the difference between Cline Workflows and Cline Rules, as they serve different purposes:
| Feature | Purpose | When to Use |
| :--- | :--- | :--- |
| **Cline Rules** | Define *how* Cline should behave generally. They are always active (or contextually triggered) and set the "ground rules" for your project. | Enforcing coding standards, tech stack preferences, or project-specific constraints (e.g., "Always use TypeScript", "Never edit the `db` folder"). |
| **Cline Workflows** | Define *what* specific task Cline should perform. They are sequences of steps invoked on-demand to automate a process. | Automating repetitive tasks like creating a component, running a release process, or generating a daily report. |
Think of **Rules** as the *environment* Cline works in, and **Workflows** as the *scripts* you give Cline to execute.
### Example: Automating a Release
Imagine you need to prepare a new release for your library.
**Without a workflow**, you might have to manually:
1. Open `package.json` and bump the version number.
2. Run your test suite to make sure everything is green.
3. Update `CHANGELOG.md` with the latest commits.
4. Run `git commit -am "v1.0.1"`.
5. Run `git tag v1.0.1`.
6. Run `git push origin main --tags`.
This is tedious and easy to mess up. You might forget to run the tests or format the changelog correctly.
**With a Cline workflow**, you define these steps once in a `release.md` file. Then, you just type:
```bash
/release.md
```
Cline will then meticulously follow your instructions: updating files, running tests, and executing git commands—pausing only if it encounters an error or needs your input.
## Where are Workflows Stored?
You can store workflows in two locations, depending on whether they are specific to a project or meant to be global.
<Tabs>
<Tab title="Project-Specific Workflows">
Store workflows that are specific to a single project in a `.clinerules/workflows/` directory in your project's root.
1. Create a `.clinerules` folder in your project's root directory (if it doesn't already exist).
<Note>
The `.clinerules` directory may be hidden by default on some systems. You might need to enable **Show Hidden Files** to see it.
</Note>
2. Inside `.clinerules`, create a `workflows` folder.
3. Create your Markdown workflow files (e.g., `deploy.md`) in this folder.
These workflows will only be available when you have this specific project open.
</Tab>
<Tab title="Global Workflows">
Store workflows that you want to use across all your projects in a global directory.
* **macOS/Linux:** `~/Documents/Cline/Workflows/`
* **Windows:** `C:\Users\USERNAME\Documents\Cline\Workflows\`
Create your Markdown workflow files directly in this directory. They will be available in any project you open with Cline.
</Tab>
</Tabs>
## Manage Workflows
You can easily manage your workflows directly within the extension. This feature provides a unified interface to handle all your automation needs without leaving your editor or hunting through file directories. It consolidates both project-specific rules and global workflows into one view, giving you full control over your automation environment.
1. Click the **Manage Cline Rules and Workflows** button (<Icon icon="scale-balanced" />) at the bottom of the extension.
2. This opens an interface where you can:
* **View all available workflows:** See a comprehensive list of both project-specific and global workflows.
* **Control automation:** Toggle individual workflows on and off as needed for your current task.
* **Create and Edit:** Add new workflows or modify existing ones directly within the interface.
* **Clean up:** Delete workflows you no longer need.
<Frame caption="Manage Workflows">
<img src="https://storage.googleapis.com/cline_public_images/workflow-menu.gif" alt="Manage Cline Rules and Workflows Interface" />
</Frame>
## Workflow Structure Example
Here is a simple example of a workflow file (`daily-changelog.md`) that helps you create a daily changelog.
````markdown daily-changelog.md
# Daily Changelog Generator
This workflow helps you create a changelog for your daily work.
1. **Check your recent git commits:**
I will run the following command to see your commits from today.
```bash
git log --author="$(git config user.name)" --since="yesterday" --oneline
```
2. **Summarize your work:**
I will present the commits to you and ask for a summary of your changes to be added to the `changelog.md` file.
3. **Create/Append to daily changelog:**
I will append to the `changelog.md` file. The content will include a header with the current date, the list of commits, and your summary.
````
### Breakdown of the Workflow
This workflow demonstrates that you don't always need to provide specific tool calls (like XML blocks). Cline is smart enough to interpret your high-level instructions.
1. **Step 1: Check recent git commits**
* We give Cline a specific command to run. This ensures it gets exactly the data we want (today's commits).
<Tip>
After Cline shows the git commit history, you may need to click the **Proceed While Running** button to allow the workflow to continue.
</Tip>
2. **Step 2: Summarize your work**
* Instead of forcing a specific tool, we simply tell Cline what to do: "ask for a summary".
* Cline knows it needs to use its capabilities to ask you a question.
3. **Step 3: Create/Append to daily changelog**
* We describe the desired outcome: "append to the `changelog.md` file" with specific content.
* Cline figures out how to format the file and use its file-writing tools to accomplish the task.
@@ -0,0 +1,112 @@
---
title: "Workflows Quick Start"
sidebarTitle: "Quick Start"
description: "A step-by-step guide to creating your first Cline workflow."
---
In this tutorial, you will create a powerful workflow that automates the process of reviewing a GitHub Pull Request. This example demonstrates how to combine CLI tools, file analysis, and user interaction into a seamless process.
### Prerequisites
* You have Cline installed.
* You have the [GitHub CLI (`gh`)](https://cli.github.com/) installed and authenticated.
* You have a Git repository open with a Pull Request you want to test this on.
## Creating a Pull Request Review Workflow
This workflow will automate the process of fetching PR details, analyzing the code changes for issues, and drafting a review comment.
<Steps>
<Step title="Create the Workflow File">
First, create the directory structure for your project-specific workflows.
1. In the root of your project, create a new folder named `.clinerules`.
2. Inside `.clinerules`, create another folder named `workflows`.
3. Finally, create a new file named `pr-review.md` inside the `workflows` folder.
</Step>
<Step title="Write the Workflow Content">
Open the `pr-review.md` file and add the following content. This workflow will gather PR details, analyze the changes, and help you submit a review.
````markdown pr-review.md
# Pull Request Reviewer
This workflow helps me review a pull request by analyzing the changes and drafting a review.
## 1. Gather PR Information
First, I need to understand what this PR is about. I'll fetch the title, description, and list of changed files.
```bash
gh pr view PR_NUMBER --json title,body,files
```
## 2. Examine Modified Files
Now I will examine the diff to understand the specific code changes.
```bash
gh pr diff PR_NUMBER
```
## 3. Analyze Changes
I will analyze the code changes for:
* **Bugs:** Logic errors or edge cases.
* **Performance:** Inefficient loops or operations.
* **Security:** Vulnerabilities or unsafe practices.
## 4. Confirm Assessment
Based on my analysis, I will present my findings and ask how you want to proceed.
```xml
<ask_followup_question>
<question>I've reviewed PR #PR_NUMBER. Here is my assessment:
[Insert Analysis Here]
Do you want me to approve this PR, request changes, or just leave a comment?</question>
<options>["Approve", "Request Changes", "Comment", "Do nothing"]</options>
</ask_followup_question>
```
## 5. Execute Review
Finally, I will execute the review command based on your decision.
```bash
# If approving:
gh pr review PR_NUMBER --approve --body "Looks good to me! [Summary of analysis]"
# If requesting changes:
gh pr review PR_NUMBER --request-changes --body "Please address the following: [Issues list]"
# If commenting:
gh pr review PR_NUMBER --comment --body "[Comments]"
```
````
<Note>
When you run this workflow, you will replace `PR_NUMBER` with the actual number of the pull request you want to review (e.g., `/pr-review.md 123`).
</Note>
</Step>
<Step title="Run the Workflow">
Now you're ready to run your new workflow.
1. Open the Cline chat panel.
2. Type `/pr-review.md` followed by the PR number (e.g., `/pr-review.md 42`) and press Enter.
3. Cline will fetch the PR details, analyze the code, and present you with its findings before submitting the review.
<Tip>
As Cline executes commands (like `gh pr view`), it may show you the output and pause. You will need to click the **Proceed While Running** button to allow Cline to analyze the content and continue with the workflow.
</Tip>
</Step>
</Steps>
### Other Common Use Cases
This is just one example. You can create workflows for a wide variety of tasks, such as:
* **Creating Components:** Automate the boilerplate for new files (like React components or API endpoints).
* **Running Tests:** Create a workflow that runs your test suite and summarizes the results.
* **Deploying Your Application:** Automate your deployment pipeline using tools like `docker` and `kubectl`.
* **Refactoring Code:** Guide Cline through a complex refactoring process step-by-step.
Explore Cline's capabilities and your own development processes to find repetitive tasks that can be turned into efficient workflows.
+3 -3
View File
@@ -5017,9 +5017,9 @@
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
+1
View File
@@ -17,6 +17,7 @@ description: "Learn how to configure and use Anthropic Claude models with Cline.
Cline supports the following Anthropic Claude models:
- `claude-haiku-4-5-20251001`
- `claude-opus-4-5-20251101`
- `claude-opus-4-1-20250805`
- `claude-opus-4-20250514`
- `anthropic/claude-sonnet-4.5` (Recommended)
@@ -0,0 +1,229 @@
---
title: "Networking and Proxies"
sidebarTitle: "Networking & Proxies"
description: "Configure Cline to work behind firewalls and proxies"
---
If you're working behind a corporate proxy or firewall, you'll need to configure
proxy settings for Cline to connect to AI providers. The configuration varies
depending on which version of Cline you're using.
## VSCode Extension
The VSCode extension automatically uses VSCode's built-in proxy settings. See
[Network Connections in Visual Studio Code, Proxy server support](https://code.visualstudio.com/docs/setup/network#_proxy-server-support)
for instructions on how to set up proxies in VSCode. No additional configuration
is needed for Cline itself.
## CLI
The Cline CLI uses standard HTTP proxy environment variables. Configure these before running `cline` commands.
### Basic Configuration
**Windows (Command Prompt)**
```cmd
set https_proxy=http://proxy.company.com:8080
set http_proxy=http://proxy.company.com:8080
cline start
```
**Windows (PowerShell)**
```powershell
$env:https_proxy="http://proxy.company.com:8080"
$env:http_proxy="http://proxy.company.com:8080"
cline start
```
**macOS/Linux**
```bash
export https_proxy=http://proxy.company.com:8080
export http_proxy=http://proxy.company.com:8080
cline start
```
### Proxy with Authentication
If your proxy requires authentication, include credentials in the URL:
```bash
export https_proxy=http://username:password@proxy.company.com:8080
export http_proxy=http://username:password@proxy.company.com:8080
```
<Warning>
Storing credentials in environment variables can be a security risk.
</Warning>
### Bypass Proxy for Localhost
To prevent localhost traffic from going through the proxy, set the `no_proxy` environment variable:
**Windows**
```cmd
set no_proxy=localhost,127.0.0.1,.local
```
**macOS/Linux**
```bash
export no_proxy=localhost,127.0.0.1,.local
```
### Custom Certificate Authority
If your proxy uses a custom CA certificate:
**Windows**
```cmd
set NODE_EXTRA_CA_CERTS=C:\path\to\ca-certificate.crt
cline start
```
**macOS/Linux**
```bash
export NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem
cline start
```
### Permanent Configuration
To avoid setting these variables every time, add them to your shell profile or system environment variables.
**macOS/Linux** (add to `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
```bash
# Proxy configuration
export https_proxy=http://proxy.company.com:8080
export http_proxy=http://proxy.company.com:8080
export no_proxy=localhost,127.0.0.1,.local
export NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem
```
**Windows** (System Environment Variables):
1. Search for "Environment Variables" in Windows Settings
2. Add the variables under "User variables" or "System variables"
3. Restart your terminal or IDE
### Known Limitations
Cline CLI only supports HTTP proxies. It does not support SOCKS proxies,
proxy autoconfiguration (PAC) scripts, or HTTP proxies which require
authentication beyond a basic username and password.
## JetBrains IDEs
The JetBrains plugin uses the IDE's HTTP proxy settings.
### Configure JetBrains Proxy
1. Open Settings/Preferences:
- **Windows/Linux**: File > Settings
- **macOS**: IntelliJ IDEA > Preferences
- Or press `Ctrl+Alt+S` (Windows/Linux) or `Cmd+,` (macOS)
2. Navigate to:
```
Appearance & Behavior > System Settings > HTTP Proxy
```
3. Select "Manual proxy configuration"
4. Configure your proxy:
- **Host name**: `proxy.company.com`
- **Port number**: `8080`
- **No proxy for**: `localhost,127.0.0.1`
- Check "Proxy authentication" if required
- Enter your username and password
5. Click "Check connection" to verify the settings
6. Click "OK" to apply
7. Restart the IDE
### Test Connection
After configuring the proxy, test that Cline can connect to your AI provider:
1. Open the Cline panel
2. Try sending a simple message
3. If connection fails, check the IDE's Event Log for error messages
### Custom Certificate Authority
If your proxy uses a custom CA:
1. Add the certificate to your system's trust store, or
2. Import it into the JetBrains IDE:
- Settings > Tools > Server Certificates
- Click "+" to add your certificate
### Known Limitations
Cline in JetBrains only supports HTTP proxies. It does not support SOCKS
proxies, proxy autoconfiguration (PAC) scripts, or HTTP proxies which require
authentication beyond a basic username and password.
Cline does not pick up changed proxy settings dynamically. After changing proxy
settings, restart the IDE for Cline to use the new settings.
## Troubleshooting
### Connection Timeouts
If you're experiencing connection timeouts:
1. Verify your proxy address and port are correct
2. Check if the proxy requires authentication
3. Ensure the AI provider's API endpoints aren't blocked by your firewall
### SSL/TLS Certificate Errors
If you see certificate-related errors:
1. Check that `NODE_EXTRA_CA_CERTS` points to the correct certificate file
2. Ensure the certificate file is in PEM format
3. Use curl to verify the certificate works, for example, `curl -x proxy.corp.example:8080 --cacert /path/to/ca-cert.pem -o - -vv https://api.cline.bot/`
4. Consider disabling `http.proxyStrictSSL` in VSCode (not recommended for production)
### Testing Proxy Configuration
If you encounter problems with Cline networking, first verify your proxy
configuration works using curl:
```bash
# Linux/macOS
export https_proxy=http://proxy.company.com:8080
curl -vv https://api.anthropic.com
# Windows PowerShell
$env:https_proxy="http://proxy.company.com:8080"
curl.exe -vv https://api.anthropic.com
```
Use `--cacert $NODE_EXTRA_CA_CERTS` to specify a certificate if necessary.
Next, check ~/.cline/cline-core-service.log (CLI, JetBrains) for log messages
confirming your proxy configuration and any network-related errors.
## Common Proxy Patterns
### Authenticated HTTPS Proxy
```bash
export https_proxy=http://username:password@proxy.company.com:8080
export NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem
```
### Proxy with No Authentication
```bash
export https_proxy=http://proxy.company.com:8080
export http_proxy=http://proxy.company.com:8080
```
### Proxy with Bypass Rules
```bash
export https_proxy=http://proxy.company.com:8080
export no_proxy=localhost,127.0.0.1,.company.local,192.168.0.0/16
```
+27 -16
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.38.1",
"version": "3.38.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.38.1",
"version": "3.38.3",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -2222,6 +2222,8 @@
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2230,6 +2232,8 @@
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6348,13 +6352,15 @@
}
},
"node_modules/@vscode/vsce/node_modules/glob": {
"version": "11.0.3",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
@@ -6370,9 +6376,11 @@
}
},
"node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": {
"version": "10.0.3",
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
@@ -10241,7 +10249,9 @@
"license": "MIT"
},
"node_modules/glob": {
"version": "10.4.3",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
@@ -10254,9 +10264,6 @@
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
@@ -15195,13 +15202,15 @@
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "11.0.3",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
@@ -15239,9 +15248,11 @@
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "10.0.3",
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.38.1",
"version": "3.38.3",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+1 -1
View File
@@ -225,7 +225,7 @@ message ToggleWorkflowRequest {
message HookInfo {
string name = 1;
bool enabled = 2;
string absolutePath = 3;
string absolute_path = 3;
}
message WorkspaceHooks {
+5
View File
@@ -27,8 +27,12 @@ service ModelsService {
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Hicap models
rpc refreshHicapModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns LiteLLM models
rpc refreshLiteLlmModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Subscribe to OpenRouter models updates
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
// Subscribe to LiteLLM models updates
rpc subscribeToLiteLlmModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
// Updates API configuration (legacy - uses combined configuration)
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
// Updates API configuration (new - uses separate options and secrets)
@@ -98,6 +102,7 @@ message OpenRouterModelInfo {
repeated ModelTier tiers = 12;
optional string name = 13;
optional double temperature = 14;
optional bool supports_reasoning = 15;
}
// Shared response message for model information
+1
View File
@@ -225,6 +225,7 @@ message Settings {
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
optional string act_mode_aihubmix_model_id = 132;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
optional bool hooks_enabled = 134;
}
message DictationSettings {
File diff suppressed because it is too large Load Diff
+606
View File
@@ -0,0 +1,606 @@
import { ClineStorageMessage } from "@/shared/messages/content"
const APPLY_PATCH_PATCH_REGEX = /\*\*\* Begin Patch\s+([\s\S]*?)\s+\*\*\* End Patch/m
/**
* Convert apply_patch tool calls to write_to_file and replace_in_file format
*/
export function convertApplyPatchToolCalls(messages: Array<ClineStorageMessage>): Array<ClineStorageMessage> {
// Map to track tool_use_id to converted tool info and original input
const toolUseIdMap = new Map<string, { name: string; input: any; originalInput: any }>()
return messages.map((message) => {
if (!Array.isArray(message.content)) {
return message
}
const convertedContent = message.content.map((block) => {
// Handle tool_use blocks
if (block.type === "tool_use" && block.name === "apply_patch") {
const converted = convertApplyPatchToToolCalls(block.input)
// Store the conversion with original input for matching tool_result
toolUseIdMap.set(block.id, { ...converted, originalInput: block.input })
return {
...block,
name: converted.name,
input: converted.input,
}
}
// Handle tool_result blocks
if (block.type === "tool_result") {
const conversion = toolUseIdMap.get(block.tool_use_id)
if (conversion) {
// Reconstruct the tool_result content to match apply_patch format
const reconstructedContent = reconstructApplyPatchResult(
block,
conversion.name,
conversion.input,
conversion.originalInput,
)
return {
...block,
content: reconstructedContent,
}
}
}
return block
})
return {
...message,
content: convertedContent,
}
})
}
interface ConvertedTool {
name: string
input: any
}
/**
* Parse apply_patch input and convert to write_to_file or replace_in_file format
*/
function convertApplyPatchToToolCalls(input: any): ConvertedTool {
const patchInput = typeof input === "string" ? input : input?.input || ""
// Parse the patch format
const patchMatch = patchInput.match(APPLY_PATCH_PATCH_REGEX)
if (!patchMatch) {
// If we can't parse it, return as-is with write_to_file
return {
name: "write_to_file",
input: input,
}
}
const patchContent = patchMatch[1]
// Extract file operation (Add, Update, or Delete)
const fileMatch = patchContent.match(/\*\*\* (Add|Update|Delete) File: (.+?)(?:\n|$)/m)
if (!fileMatch) {
return {
name: "write_to_file",
input: input,
}
}
const action = fileMatch[1]
const filePath = fileMatch[2].trim()
// If it's an Add operation, convert to write_to_file
if (action === "Add") {
// Extract the content after the file line
const contentAfterFile = patchContent.substring(fileMatch.index! + fileMatch[0].length)
return {
name: "write_to_file",
input: {
absolutePath: filePath,
content: extractNewContentFromPatch(contentAfterFile),
},
}
}
// If it's Update or Delete, convert to replace_in_file
if (action === "Update" || action === "Delete") {
const diff = convertPatchToDiff(patchContent.substring(fileMatch.index! + fileMatch[0].length))
return {
name: "replace_in_file",
input: {
absolutePath: filePath,
diff: diff,
},
}
}
// Fallback
return {
name: "write_to_file",
input: input,
}
}
/**
* Extract new content from add operation patch
*/
function extractNewContentFromPatch(patchContent: string): string {
// For Add operations, the patch should contain lines starting with +
const lines = patchContent.split("\n")
const contentLines: string[] = []
for (const line of lines) {
if (line.startsWith("+")) {
// Remove the + prefix and exactly ONE space if present (but not if it's a tab)
let content = line.substring(1)
if (content.startsWith(" ") && !content.startsWith("\t")) {
content = content.substring(1)
}
contentLines.push(content)
}
}
return contentLines.join("\n")
}
/**
* Convert V4A patch format to SEARCH/REPLACE format
*/
function convertPatchToDiff(patchContent: string): string {
const diffBlocks: string[] = []
const lines = patchContent.split("\n")
let i = 0
while (i < lines.length) {
const line = lines[i]
// Skip empty lines at the start
if (!line.trim() && i === 0) {
i++
continue
}
// Check if this is the start of a hunk (@@) or a direct change line
if (line.trim().startsWith("@@") || line.startsWith("-") || line.startsWith("+")) {
const currentSearch: string[] = []
const currentReplace: string[] = []
// Collect @@ context marker lines
// @@ prefix marks context lines. If @@something, then "something" is context.
// If just @@, then it's an empty context line.
while (i < lines.length && lines[i].trim().startsWith("@@")) {
const trimmedLine = lines[i].trim()
// Extract the actual context content after @@
const contextLine = trimmedLine.substring(2)
// Always add the context line (even if empty)
currentSearch.push(contextLine)
currentReplace.push(contextLine)
i++
}
if (i >= lines.length) {
break
}
// Collect all remaining lines in this hunk until we hit end of content or next @@
const hunkLines: string[] = []
while (i < lines.length) {
// Check if this is a new hunk (starts with @@)
if (lines[i].trim().startsWith("@@")) {
break
}
hunkLines.push(lines[i])
i++
}
// Now process the hunk to build SEARCH/REPLACE
let hasChanges = false
for (let j = 0; j < hunkLines.length; j++) {
const hunkLine = hunkLines[j]
if (hunkLine.startsWith("-")) {
hasChanges = true
// Strip the - prefix and exactly ONE space if present (but not if it's a tab)
let content = hunkLine.substring(1)
if (content.startsWith(" ") && !content.startsWith(" \t")) {
content = content.substring(1)
}
currentSearch.push(content)
} else if (hunkLine.startsWith("+")) {
hasChanges = true
// Strip the + prefix and exactly ONE space if present (but not if it's a tab)
let content = hunkLine.substring(1)
if (content.startsWith(" ") && !content.startsWith(" \t")) {
content = content.substring(1)
}
currentReplace.push(content)
} else {
// Context line without @@ prefix - add to both sides
currentSearch.push(hunkLine)
currentReplace.push(hunkLine)
}
}
// Create the diff block if we have changes
if (hasChanges && (currentSearch.length > 0 || currentReplace.length > 0)) {
diffBlocks.push(
"------- SEARCH\n" +
currentSearch.join("\n") +
"\n=======\n" +
currentReplace.join("\n") +
"\n+++++++ REPLACE",
)
}
} else {
i++
}
}
return diffBlocks.join("\n")
}
/**
* Reconstruct tool_result content to match apply_patch format by extracting
* the final file content and converting it back to V4A patch format
*/
function reconstructApplyPatchResult(
block: any,
convertedToolName: string,
_convertedInput: any,
originalInput: any,
): string | any[] {
// Extract the content from the tool_result
const content = typeof block.content === "string" ? block.content : ""
// Try to extract the final_file_content
const finalContentMatch = content.match(/<final_file_content path="([^"]+)">\s*([\s\S]*?)\s*<\/final_file_content>/)
if (!finalContentMatch) {
// If no final_file_content found, return original content
return block.content
}
const filePath = finalContentMatch[1]
const finalContent = finalContentMatch[2]
// Reconstruct the result message based on the converted tool type
if (convertedToolName === "write_to_file") {
// For write_to_file, we just need to confirm the file was created/written
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully saved to ${filePath}.\n\nThe file has been created/updated with the new content.`
}
if (convertedToolName === "replace_in_file") {
// For replace_in_file, we need to reconstruct the V4A patch format result
// Try to parse the original patch to get the action and build context
const patchInput = typeof originalInput === "string" ? originalInput : originalInput?.input || ""
const patchMatch = patchInput.match(APPLY_PATCH_PATCH_REGEX)
if (patchMatch) {
const patchContent = patchMatch[1]
const fileMatch = patchContent.match(/\*\*\* (Add|Update|Delete) File: (.+?)(?:\n|$)/m)
if (fileMatch) {
const action = fileMatch[1]
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully updated in ${filePath}.\n\nThe file has been modified using ${action} operation.\n\n<final_file_content path="${filePath}">\n${finalContent}\n</final_file_content>\n\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.`
}
}
// Fallback for replace_in_file
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully updated in ${filePath}.\n\n<final_file_content path="${filePath}">\n${finalContent}\n</final_file_content>\n\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.`
}
// Default fallback
return block.content
}
/**
* Convert write_to_file and replace_in_file tool calls to apply_patch format
*/
export function convertWriteToFileToolCalls(messages: Array<ClineStorageMessage>): Array<ClineStorageMessage> {
// Map to track tool_use_id to converted tool info and original input
const toolUseIdMap = new Map<string, { originalName: string; originalInput: any; patchInput?: string }>()
// First pass: collect tool_use blocks
for (const message of messages) {
if (!Array.isArray(message.content)) {
continue
}
for (const block of message.content) {
if (block.type === "tool_use" && (block.name === "write_to_file" || block.name === "replace_in_file")) {
toolUseIdMap.set(block.id, {
originalName: block.name,
originalInput: block.input,
})
}
}
}
// Second pass: find tool_results and extract final content to build proper patches
const finalContentMap = new Map<string, string>()
for (const message of messages) {
if (!Array.isArray(message.content)) {
continue
}
for (const block of message.content) {
if (block.type === "tool_result" && toolUseIdMap.has(block.tool_use_id)) {
const content = typeof block.content === "string" ? block.content : ""
const finalContentMatch = content.match(
/<final_file_content path="([^"]+)">\s*([\s\S]*?)\s*<\/final_file_content>/,
)
if (finalContentMatch) {
finalContentMap.set(block.tool_use_id, finalContentMatch[2])
}
}
}
}
// Third pass: convert messages
return messages.map((message) => {
if (!Array.isArray(message.content)) {
return message
}
const convertedContent = message.content.map((block) => {
// Handle tool_use blocks for write_to_file and replace_in_file
if (block.type === "tool_use" && (block.name === "write_to_file" || block.name === "replace_in_file")) {
const finalContent = finalContentMap.get(block.id)
const patchInput = convertToPatchFormat(block.name, block.input, finalContent)
// Update the map with the generated patch
const existingEntry = toolUseIdMap.get(block.id)
if (existingEntry) {
existingEntry.patchInput = patchInput
}
return {
...block,
name: "apply_patch",
input: {
input: patchInput,
},
}
}
// Handle tool_result blocks
if (block.type === "tool_result") {
const conversion = toolUseIdMap.get(block.tool_use_id)
if (conversion) {
// Reconstruct the tool_result content to match apply_patch format
const reconstructedContent = reconstructWriteToFileResult(
block,
conversion.originalName,
conversion.originalInput,
)
return {
...block,
content: reconstructedContent,
}
}
}
return block
})
return {
...message,
content: convertedContent,
}
})
}
/**
* Convert write_to_file or replace_in_file input to apply_patch format
*/
function convertToPatchFormat(toolName: string, input: any, finalContent?: string): string {
const filePath = input.absolutePath || input.path || ""
if (toolName === "write_to_file") {
// Convert write_to_file to Add operation
const content = input.content || ""
const lines = content.split("\n")
const patchLines = ["@@"]
patchLines.push(...lines.map((line: string) => `+ ${line}`))
return `apply_patch <<"EOF"
*** Begin Patch
*** Add File: ${filePath}
${patchLines.join("\n")}
*** End Patch
EOF`
}
if (toolName === "replace_in_file") {
// Convert replace_in_file to Update operation
const diff = input.diff || ""
// Parse SEARCH/REPLACE blocks and convert to V4A format with context
const patchContent = convertDiffToPatchWithContext(diff, finalContent)
return `apply_patch <<"EOF"
*** Begin Patch
*** Update File: ${filePath}
${patchContent}
*** End Patch
EOF`
}
return ""
}
/**
* Convert SEARCH/REPLACE diff format to V4A patch format with additional context from final content
*/
function convertDiffToPatchWithContext(diff: string, finalContent?: string): string {
const patchLines: string[] = []
// Match all SEARCH/REPLACE blocks
const blockRegex = /------- SEARCH\s*\n([\s\S]*?)\n=======\s*\n([\s\S]*?)\n\+{7} REPLACE/g
let match
while ((match = blockRegex.exec(diff)) !== null) {
const searchContent = match[1]
const replaceContent = match[2]
const searchLines = searchContent.split("\n")
const replaceLines = replaceContent.split("\n")
// Find common prefix and suffix between search and replace
let prefixEnd = 0
while (
prefixEnd < searchLines.length &&
prefixEnd < replaceLines.length &&
searchLines[prefixEnd] === replaceLines[prefixEnd]
) {
prefixEnd++
}
let suffixStart = searchLines.length
let replaceSuffixStart = replaceLines.length
while (
suffixStart > prefixEnd &&
replaceSuffixStart > prefixEnd &&
searchLines[suffixStart - 1] === replaceLines[replaceSuffixStart - 1]
) {
suffixStart--
replaceSuffixStart--
}
// If we have finalContent, extract additional context from it
if (finalContent) {
const finalLines = finalContent.split("\n")
// Find where the replaced content appears in the final file
let matchIndex = -1
for (let i = 0; i < finalLines.length; i++) {
// Try to match the first replace line
if (replaceLines.length > 0 && finalLines[i] === replaceLines[0]) {
// Check if subsequent lines also match
let allMatch = true
for (let j = 1; j < replaceLines.length && i + j < finalLines.length; j++) {
if (finalLines[i + j] !== replaceLines[j]) {
allMatch = false
break
}
}
if (allMatch) {
matchIndex = i
break
}
}
}
if (matchIndex >= 0) {
// Extract up to 3 lines before as context
const contextStart = Math.max(0, matchIndex - 3)
const contextLines: string[] = []
for (let i = contextStart; i < matchIndex; i++) {
contextLines.push(finalLines[i])
}
// Pad to 3 lines if needed (with empty strings)
while (contextLines.length < 3) {
contextLines.unshift("")
}
// Add @@ marker with the first context line
if (contextLines[0] === "") {
patchLines.push("@@")
} else {
patchLines.push(`@@${contextLines[0]}`)
}
// Add remaining context lines (without @@ marker)
for (let i = 1; i < contextLines.length; i++) {
patchLines.push(contextLines[i])
}
// Add common prefix lines (without +/- markers)
for (let i = 0; i < prefixEnd; i++) {
patchLines.push(searchLines[i])
}
// Add the actual changes (lines that differ)
for (let i = prefixEnd; i < suffixStart; i++) {
patchLines.push(`- ${searchLines[i]}`)
}
for (let i = prefixEnd; i < replaceSuffixStart; i++) {
patchLines.push(`+ ${replaceLines[i]}`)
}
// Add common suffix lines (without +/- markers)
for (let i = suffixStart; i < searchLines.length; i++) {
patchLines.push(searchLines[i])
}
// Extract up to 3 lines after as trailing context (without @@ markers)
const contextEnd = Math.min(finalLines.length, matchIndex + replaceLines.length + 3)
for (let i = matchIndex + replaceLines.length; i < contextEnd; i++) {
patchLines.push(finalLines[i])
}
continue
}
}
// Fallback: if no finalContent or couldn't find match, use the prefix/suffix from SEARCH/REPLACE
patchLines.push("@@")
// Add common prefix lines (without +/- markers)
for (let i = 0; i < prefixEnd; i++) {
patchLines.push(searchLines[i])
}
// Add the actual changes (lines that differ)
for (let i = prefixEnd; i < suffixStart; i++) {
patchLines.push(`- ${searchLines[i]}`)
}
for (let i = prefixEnd; i < replaceSuffixStart; i++) {
patchLines.push(`+ ${replaceLines[i]}`)
}
// Add common suffix lines (without +/- markers)
for (let i = suffixStart; i < searchLines.length; i++) {
patchLines.push(searchLines[i])
}
}
return patchLines.join("\n")
}
/**
* Reconstruct tool_result content to match apply_patch result format
*/
function reconstructWriteToFileResult(block: any, originalToolName: string, originalInput: any): string | any[] {
// Extract the content from the tool_result
const content = typeof block.content === "string" ? block.content : ""
// Try to extract the final_file_content
const finalContentMatch = content.match(/<final_file_content path="([^"]+)">\s*([\s\S]*?)\s*<\/final_file_content>/)
const filePath = originalInput.absolutePath || originalInput.path || ""
if (!finalContentMatch) {
// If no final_file_content found, create a simple success message
if (originalToolName === "write_to_file") {
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully saved to ${filePath}.\n\nThe file has been created/updated with the new content.`
} else {
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully updated in ${filePath}.\n\nThe file has been modified.`
}
}
const finalContent = finalContentMatch[2]
// Reconstruct the result message based on the original tool type
if (originalToolName === "write_to_file") {
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully saved to ${filePath}.\n\nThe file has been created/updated with the new content.`
}
if (originalToolName === "replace_in_file") {
return `[apply_patch for '${filePath}'] Result:\nThe content was successfully updated in ${filePath}.\n\nThe file has been modified using Update operation.\n\n<final_file_content path="${filePath}">\n${finalContent}\n</final_file_content>\n\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.`
}
// Default fallback
return block.content
}
+60
View File
@@ -0,0 +1,60 @@
import { ClineStorageMessage } from "@/shared/messages/content"
import { ClineDefaultTool } from "@/shared/tools"
import { convertApplyPatchToolCalls, convertWriteToFileToolCalls } from "./diff-editors"
/**
* Transforms tool call messages between different tool formats based on native tool support.
* Converts between apply_patch and write_to_file/replace_in_file formats as needed.
*
* @param clineMessages - Array of messages containing tool calls to transform
* @param nativeTools - Array of tools natively supported by the current provider
* @returns Transformed messages array, or original if no transformation needed
*/
export function transformToolCallMessages(
clineMessages: ClineStorageMessage[],
nativeTools?: ClineDefaultTool[],
): ClineStorageMessage[] {
// Early return if no messages or native tools provided
if (!clineMessages?.length || !nativeTools?.length) {
return clineMessages
}
// Create Sets for O(1) lookup performance
const nativeToolSet = new Set(nativeTools)
const usedToolSet = new Set<string>()
// Single pass: collect all tools used in assistant messages
for (const msg of clineMessages) {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "tool_use" && block.name) {
usedToolSet.add(block.name)
}
}
}
}
// Early return if no tools were used
if (usedToolSet.size === 0) {
return clineMessages
}
// Determine which conversion to apply
const hasApplyPatchNative = nativeToolSet.has(ClineDefaultTool.APPLY_PATCH)
const hasFileEditNative = nativeToolSet.has(ClineDefaultTool.FILE_EDIT) || nativeToolSet.has(ClineDefaultTool.FILE_NEW)
const hasApplyPatchUsed = usedToolSet.has(ClineDefaultTool.APPLY_PATCH)
const hasFileEditUsed = usedToolSet.has(ClineDefaultTool.FILE_EDIT) || usedToolSet.has(ClineDefaultTool.FILE_NEW)
// Convert write_to_file/replace_in_file → apply_patch
if (hasApplyPatchNative && hasFileEditUsed) {
return convertWriteToFileToolCalls(clineMessages)
}
// Convert apply_patch → write_to_file/replace_in_file
if (hasFileEditNative && hasApplyPatchUsed) {
return convertApplyPatchToolCalls(clineMessages)
}
return clineMessages
}
+1
View File
@@ -129,6 +129,7 @@ function createHandlerForProvider(
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
thinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
ulid: options.ulid,
})
case "openai":
+7
View File
@@ -63,6 +63,12 @@ export class AnthropicHandler implements ApiHandler {
switch (modelId) {
// 'latest' alias does not support cache_control
case "claude-haiku-4-5@20251001":
case "claude-sonnet-4-5@20250929":
case "claude-sonnet-4@20250514":
case "claude-opus-4-5@20251101":
case "claude-opus-4-1@20250805":
case "claude-opus-4@20250514":
case "claude-haiku-4-5-20251001":
case "claude-sonnet-4-5-20250929:1m":
case "claude-sonnet-4-5-20250929":
@@ -70,6 +76,7 @@ export class AnthropicHandler implements ApiHandler {
case "claude-3-7-sonnet-20250219":
case "claude-3-5-sonnet-20241022":
case "claude-3-5-haiku-20241022":
case "claude-opus-4-5-20251101":
case "claude-opus-4-20250514":
case "claude-opus-4-1-20250805":
case "claude-3-opus-20240229":
+81 -2
View File
@@ -19,6 +19,16 @@ type AskSageRequest = {
}[]
model: string
dataset: "none"
usage: boolean
}
type AskSageUsage = {
model_tokens: {
completion_tokens: number
prompt_tokens: number
total_tokens: number
}
asksage_tokens: number
}
type AskSageResponse = {
@@ -28,6 +38,18 @@ type AskSageResponse = {
response: string
// Generated response message
message: string
// whether embedding & vector systems are down
embedding_down: boolean
vectors_down: boolean
// references if dataset is not none
references: string
type: string
added_obj: any
tool_calls: any
// usage metrics
usage: AskSageUsage | null
tool_responses: any[]
tool_calls_unified: any[]
}
export class AskSageHandler implements ApiHandler {
@@ -50,7 +72,6 @@ export class AskSageHandler implements ApiHandler {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
try {
const model = this.getModel()
// Transform messages into AskSageRequest format
const formattedMessages = messages.map((msg) => {
const content = Array.isArray(msg.content)
@@ -68,6 +89,7 @@ export class AskSageHandler implements ApiHandler {
message: formattedMessages,
model: model.id,
dataset: "none",
usage: true,
}
// Make request to AskSage API
@@ -91,15 +113,72 @@ export class AskSageHandler implements ApiHandler {
throw new Error("No content in AskSage response")
}
// Return entire response as a single chunk since streaming is not supported
// Yield tool responses if they exist
if (result.tool_responses && result.tool_responses.length > 0) {
for (const toolResponse of result.tool_responses) {
yield {
type: "text",
text: `[Tool Response: ${JSON.stringify(toolResponse)}]\n`,
}
}
}
// Yield the main response text
yield {
type: "text",
text: result.message,
}
// Yield usage information if available
if (result.usage) {
yield {
type: "usage",
inputTokens: result.usage.model_tokens.prompt_tokens,
outputTokens: result.usage.model_tokens.completion_tokens,
cacheReadTokens: 0,
cacheWriteTokens: 0,
totalCost: result.usage.asksage_tokens, // Cost = Consumed AskSage tokens
}
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`AskSage request failed: ${error.message}`)
}
throw error
}
}
async getApiStreamUsage() {
if (!this.apiKey) {
return undefined
}
try {
const response = await fetch(`${this.apiUrl}/count-monthly-tokens`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-access-tokens": this.apiKey,
},
body: JSON.stringify({ app_name: "asksage" }),
})
if (!response.ok) {
console.error("Failed to fetch AskSage usage", await response.text())
return undefined
}
const data = await response.json()
const usedTokens = data.response as number
return {
type: "usage" as const,
inputTokens: usedTokens,
outputTokens: 0,
}
} catch (error) {
console.error("Error fetching AskSage usage:", error)
return undefined
}
}
-2
View File
@@ -195,8 +195,6 @@ export class ClineHandler implements ApiHandler {
}
}
console.log("didOutputUsage", didOutputUsage, chunk.usage)
if (!didOutputUsage && chunk.usage) {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
+81 -49
View File
@@ -1,6 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import OpenAI from "openai"
import { StateManager } from "@/core/storage/StateManager"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { isAnthropicModelId } from "@/utils/model-utils"
@@ -19,6 +20,14 @@ interface LiteLlmHandlerOptions extends CommonApiHandlerOptions {
ulid?: string
}
/**
* Extended chat completion parameters that include LiteLLM-specific options
* not present in the standard OpenAI SDK types
*/
interface LiteLlmChatCompletionCreateParams extends OpenAI.Chat.ChatCompletionCreateParamsStreaming {
drop_params?: boolean
}
export interface LiteLlmModelInfoResponse {
data: Array<{
model_name: string
@@ -37,6 +46,54 @@ export interface LiteLlmModelInfoResponse {
}>
}
/**
* Exported utility function to fetch LiteLLM model info
* @param baseUrl The base URL for the LiteLLM API
* @param apiKey The API key for authentication
* @returns The model info response or undefined if fetch fails
*/
export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): Promise<LiteLlmModelInfoResponse | undefined> {
// Handle base URLs that already include /v1 to avoid double /v1/v1/
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`
const url = `${normalizedBaseUrl}/model/info`
try {
const response = await fetch(url, {
method: "GET",
headers: {
accept: "application/json",
"x-litellm-api-key": apiKey,
},
})
if (response.ok) {
const data: LiteLlmModelInfoResponse = await response.json()
return data
} else {
console.error("Failed to fetch LiteLLM model info:", response.statusText)
// Try with Authorization header instead
const retryResponse = await fetch(url, {
method: "GET",
headers: {
accept: "application/json",
Authorization: `Bearer ${apiKey}`,
},
})
if (retryResponse.ok) {
const data: LiteLlmModelInfoResponse = await retryResponse.json()
return data
} else {
console.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
}
}
} catch (error) {
console.error("Error fetching LiteLLM model info:", error)
throw error
}
}
export class LiteLlmHandler implements ApiHandler {
private options: LiteLlmHandlerOptions
private client: OpenAI | undefined
@@ -84,49 +141,14 @@ export class LiteLlmHandler implements ApiHandler {
}
const client = this.ensureClient()
// Handle base URLs that already include /v1 to avoid double /v1/v1/
const baseUrl = client.baseURL.endsWith("/v1") ? client.baseURL : `${client.baseURL}/v1`
const url = `${baseUrl}/model/info`
const data = await fetchLiteLlmModelsInfo(client.baseURL, this.options.liteLlmApiKey || "")
try {
const response = await fetch(url, {
method: "GET",
headers: {
accept: "application/json",
"x-litellm-api-key": this.options.liteLlmApiKey || "",
},
})
if (response.ok) {
const data: LiteLlmModelInfoResponse = await response.json()
this.modelInfoCache = data
this.modelInfoCacheTimestamp = now
return data
} else {
console.warn("Failed to fetch LiteLLM model info:", response.statusText)
// Try with Authorization header instead
const retryResponse = await fetch(url, {
method: "GET",
headers: {
accept: "application/json",
Authorization: `Bearer ${this.options.liteLlmApiKey || ""}`,
},
})
if (retryResponse.ok) {
const data: LiteLlmModelInfoResponse = await retryResponse.json()
this.modelInfoCache = data
this.modelInfoCacheTimestamp = now
return data
} else {
console.warn("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
return undefined
}
}
} catch (error) {
console.warn("Error fetching LiteLLM model info:", error)
return undefined
if (data) {
this.modelInfoCache = data
this.modelInfoCacheTimestamp = now
}
return data
}
private async getModelCostInfo(publicModelName: string): Promise<{
@@ -186,6 +208,7 @@ export class LiteLlmHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam | Anthropic.Messages.TextBlockParam = {
role: "system",
@@ -193,23 +216,23 @@ export class LiteLlmHandler implements ApiHandler {
}
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini") || modelId.includes("o4-mini")
const isCodexModel = modelId.toLowerCase().includes("codex")
// Configuration for extended thinking
const budgetTokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = budgetTokens !== 0
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 0
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 1
if ((isOminiModel || isAnthropicModelId(modelId)) && reasoningOn) {
temperature = undefined // OAI omni and Anthropic extended thinking mode doesn't support temperature
}
const modelInfo = await this.modelInfo(modelId)
// Automatically enable caching if the model supports it
const cacheControl =
this.options.liteLlmUsePromptCache && Boolean(modelInfo?.model_info.supports_prompt_caching)
? { cache_control: { type: "ephemeral" } }
: undefined
(modelInfo?.model_info.supports_prompt_caching ?? false) ? { cache_control: { type: "ephemeral" } } : undefined
if (cacheControl) {
// Add cache_control to system message if enabled
@@ -277,10 +300,11 @@ export class LiteLlmHandler implements ApiHandler {
messages: [systemMessage, ...enhancedMessages],
temperature,
stream: true,
stream_options: { include_usage: true },
drop_params: true,
...(!isCodexModel && { stream_options: { include_usage: true } }), // Codex models are only on the responses api, which doesn't take the stream_options parameter. we will need to migrate to the responses api for this to work
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
...(this.options.ulid && { litellm_session_id: `cline-${this.options.ulid}` }), // Add session ID for LiteLLM tracking
})
} as LiteLlmChatCompletionCreateParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
@@ -344,9 +368,17 @@ export class LiteLlmHandler implements ApiHandler {
}
getModel() {
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
// Try to get model info from StateManager cache first
const cachedModelInfo = StateManager.get().getModelInfo("liteLlm", modelId)
// Fall back to provided model info or defaults if not in cache
const modelInfo = cachedModelInfo || liteLlmModelInfoSaneDefaults
return {
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
info: this.options.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
id: modelId,
info: modelInfo,
}
}
}
+2
View File
@@ -1,6 +1,7 @@
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { type Config, type Message, Ollama } from "ollama"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOllamaMessages } from "../transform/ollama-format"
@@ -30,6 +31,7 @@ export class OllamaHandler implements ApiHandler {
try {
const clientOptions: Partial<Config> = {
host: this.options.ollamaBaseUrl,
fetch,
}
// Add API key if provided (for Ollama cloud or authenticated instances)
+100 -30
View File
@@ -312,13 +312,12 @@ namespace Gemini {
}
/**
* Prepare Gemini request payload with thinking configuration and implicit caching support
* Prepare Gemini request payload with implicit caching support
*/
export function prepareRequestPayload(
systemPrompt: string,
messages: ClineStorageMessage[],
model: { id: SapAiCoreModelId; info: ModelInfo },
thinkingBudgetTokens?: number,
): any {
const contents = messages.map(convertAnthropicMessageToGemini)
@@ -337,17 +336,15 @@ namespace Gemini {
},
}
// Add thinking config if the model supports it and budget is provided
const thinkingBudget = thinkingBudgetTokens ?? 0
const _maxBudget = model.info.thinkingConfig?.maxBudget ?? 0
if (thinkingBudget > 0 && model.info.thinkingConfig) {
// Add thinking configuration to the payload
;(payload as any).thinkingConfig = {
thinkingBudget: thinkingBudget,
includeThoughts: true,
}
}
// Note: SAP AI Core's Gemini deployment doesn't support thinkingConfig yet
// Commenting out until support is added
// const thinkingBudget = thinkingBudgetTokens ?? 0
// if (thinkingBudget > 0 && model.info.thinkingConfig) {
// ;(payload as any).thinkingConfig = {
// thinkingBudget: thinkingBudget,
// includeThoughts: true,
// }
// }
return payload
}
@@ -363,6 +360,21 @@ export class SapAiCoreHandler implements ApiHandler {
this.options = options
}
/**
* Converts a chunk from the stream to a UTF-8 string
* Handles Buffer, string, and byte array formats
*/
private chunkToString(chunk: any): string {
if (Buffer.isBuffer(chunk)) {
return chunk.toString("utf-8")
} else if (typeof chunk === "string") {
return chunk
} else {
// Handle comma-separated byte values or other array-like formats
return Buffer.from(chunk).toString("utf-8")
}
}
private validateCredentials(): void {
if (
!this.options.sapAiCoreClientId ||
@@ -585,6 +597,7 @@ export class SapAiCoreHandler implements ApiHandler {
"o4-mini",
]
const perplexityModels = ["sonar-pro", "sonar"]
const geminiModels = ["gemini-2.5-flash", "gemini-2.5-pro"]
let url: string
@@ -597,10 +610,12 @@ export class SapAiCoreHandler implements ApiHandler {
const formattedMessages = Bedrock.formatMessagesForConverseAPI(messages)
// Get message indices for caching
const userMsgIndices = messages.reduce(
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
[] as number[],
)
const userMsgIndices = messages.reduce((acc, msg, index) => {
if (msg.role === "user") {
acc.push(index)
}
return acc
}, [] as number[])
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
@@ -673,9 +688,26 @@ export class SapAiCoreHandler implements ApiHandler {
delete payload.stream
delete payload.stream_options
}
} else if (perplexityModels.includes(model.id)) {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/chat/completions`
payload = {
stream: true,
messages: openAiMessages,
temperature: 0.0,
frequency_penalty: 0,
presence_penalty: 0,
stop: null,
model: model.id,
stream_options: { include_usage: true },
}
} else if (geminiModels.includes(model.id)) {
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/models/${model.id}:streamGenerateContent`
payload = Gemini.prepareRequestPayload(systemPrompt, messages, model, this.options.thinkingBudgetTokens)
payload = Gemini.prepareRequestPayload(systemPrompt, messages, model)
} else {
throw new Error(`Unsupported model: ${model.id}`)
}
@@ -715,7 +747,7 @@ export class SapAiCoreHandler implements ApiHandler {
outputTokens: response.data.usage.completion_tokens,
}
}
} else if (openAIModels.includes(model.id)) {
} else if (openAIModels.includes(model.id) || perplexityModels.includes(model.id)) {
yield* this.streamCompletionGPT(response.data, model)
} else if (
model.id === "anthropic--claude-4.5-sonnet" ||
@@ -729,18 +761,54 @@ export class SapAiCoreHandler implements ApiHandler {
} else {
yield* this.streamCompletion(response.data, model)
}
} catch (error) {
} catch (error: any) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error("Error status:", error.response.status)
console.error("Error data:", error.response.data)
console.error("Error headers:", error.response.headers)
if (error.response.status === 404) {
console.error("404 Error reason:", error.response.data)
throw new Error(`404 Not Found: ${error.response.data}`)
// Handle error data - need to read stream if responseType was 'stream'
let errorMessage = "Unknown error"
if (error.response.data) {
try {
// If it's a stream, read it
if (
typeof error.response.data.on === "function" ||
typeof error.response.data[Symbol.asyncIterator] === "function"
) {
const chunks: Buffer[] = []
for await (const chunk of error.response.data) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
const fullData = Buffer.concat(chunks).toString("utf-8")
errorMessage = fullData
try {
// Try to parse as JSON for better formatting
const jsonError = JSON.parse(fullData)
errorMessage = JSON.stringify(jsonError, null, 2)
} catch {
// Keep as plain text if not JSON
}
} else if (typeof error.response.data === "string") {
errorMessage = error.response.data
} else if (typeof error.response.data === "object") {
errorMessage = JSON.stringify(error.response.data, null, 2)
}
console.error("Error data:", errorMessage)
} catch (e) {
console.error("Failed to read error data:", e)
console.error("Raw error data:", error.response.data)
}
}
if (error.response.status === 404) {
throw new Error(`404 Not Found: ${errorMessage}`)
} else if (error.response.status === 400) {
throw new Error(`400 Bad Request: ${errorMessage}`)
}
throw new Error(`HTTP ${error.response.status}: ${errorMessage}`)
} else if (error.request) {
// The request was made but no response was received
console.error("Error request:", error.request)
@@ -750,8 +818,6 @@ export class SapAiCoreHandler implements ApiHandler {
console.error("Error message:", error.message)
throw new Error(`Error setting up request: ${error.message}`)
}
throw new Error("Failed to create message")
}
}
@@ -763,7 +829,8 @@ export class SapAiCoreHandler implements ApiHandler {
try {
for await (const chunk of stream) {
const lines = chunk.toString().split("\n").filter(Boolean)
const chunkStr = this.chunkToString(chunk)
const lines = chunkStr.split("\n").filter(Boolean)
for (const line of lines) {
if (line.startsWith("data: ")) {
const jsonData = line.slice(6)
@@ -822,7 +889,8 @@ export class SapAiCoreHandler implements ApiHandler {
try {
// Iterate over the stream and process each chunk
for await (const chunk of stream) {
const lines = chunk.toString().split("\n").filter(Boolean)
const chunkStr = this.chunkToString(chunk)
const lines = chunkStr.split("\n").filter(Boolean)
for (const line of lines) {
if (line.startsWith("data: ")) {
@@ -895,7 +963,8 @@ export class SapAiCoreHandler implements ApiHandler {
try {
for await (const chunk of stream) {
const lines = chunk.toString().split("\n").filter(Boolean)
const chunkStr = this.chunkToString(chunk)
const lines = chunkStr.split("\n").filter(Boolean)
for (const line of lines) {
if (line.trim() === "data: [DONE]") {
// End of stream, yield final usage
@@ -967,7 +1036,8 @@ export class SapAiCoreHandler implements ApiHandler {
try {
for await (const chunk of stream) {
const lines = chunk.toString().split("\n").filter(Boolean)
const chunkStr = this.chunkToString(chunk)
const lines = chunkStr.split("\n").filter(Boolean)
for (const line of lines) {
if (line.startsWith("data: ")) {
const jsonData = line.slice(6)
+17 -3
View File
@@ -18,6 +18,7 @@ interface VertexHandlerOptions extends CommonApiHandlerOptions {
geminiApiKey?: string
geminiBaseUrl?: string
ulid?: string
thinkingLevel?: string
}
export class VertexHandler implements ApiHandler {
@@ -89,12 +90,16 @@ export class VertexHandler implements ApiHandler {
modelId.includes("haiku-4-5")) &&
budget_tokens !== 0
)
// Tools are available only when native tools are enabled.
const nativeToolsOn = tools?.length ? tools?.length > 0 : false
let stream
switch (modelId) {
case "claude-haiku-4-5@20251001":
case "claude-sonnet-4-5@20250929":
case "claude-sonnet-4@20250514":
case "claude-opus-4-5@20251101":
case "claude-opus-4-1@20250805":
case "claude-opus-4@20250514":
case "claude-3-7-sonnet@20250219":
@@ -103,6 +108,7 @@ export class VertexHandler implements ApiHandler {
case "claude-3-5-haiku@20241022":
case "claude-3-opus@20240229":
case "claude-3-haiku@20240307": {
const anthropicMessages = sanitizeAnthropicMessages(messages, true)
stream = await clientAnthropic.beta.messages.create(
{
model: modelId,
@@ -116,14 +122,15 @@ export class VertexHandler implements ApiHandler {
cache_control: { type: "ephemeral" },
},
],
messages: sanitizeAnthropicMessages(messages, true),
messages: anthropicMessages,
stream: true,
tools: tools?.length ? (tools as AnthropicTool[]) : undefined,
tools: nativeToolsOn ? (tools as AnthropicTool[]) : undefined,
// tool_choice options:
// - none: disables tool use, even if tools are provided. Claude will not call any tools.
// - auto: allows Claude to decide whether to call any provided tools or not. This is the default value when tools are provided.
// - any: tells Claude that it must use one of the provided tools, but doesnt force a particular tool.
tool_choice: tools ? { type: "any" } : undefined,
// NOTE: Forcing tool use when tools are provided will result in error when thinking is also enabled.
tool_choice: nativeToolsOn && !reasoningOn ? { type: "any" } : undefined,
},
{
headers: {},
@@ -219,6 +226,13 @@ export class VertexHandler implements ApiHandler {
break
case "content_block_delta":
switch (chunk.delta.type) {
case "signature_delta":
yield {
type: "reasoning",
reasoning: "",
signature: chunk.delta.signature,
}
break
case "thinking_delta":
yield {
type: "reasoning",
@@ -44,6 +44,7 @@ export async function createOpenRouterStream(
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-4.5-sonnet": // OpenRouter accidentally included this in model list for a brief moment, and users may be using this model id. And to support prompt caching, we need to add it here.
case "anthropic/claude-sonnet-4":
case "anthropic/claude-opus-4.5":
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
case "anthropic/claude-3.7-sonnet":
@@ -107,6 +108,7 @@ export async function createOpenRouterStream(
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-4.5-sonnet":
case "anthropic/claude-sonnet-4":
case "anthropic/claude-opus-4.5":
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
case "anthropic/claude-3.7-sonnet":
@@ -151,6 +153,7 @@ export async function createOpenRouterStream(
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-4.5-sonnet":
case "anthropic/claude-sonnet-4":
case "anthropic/claude-opus-4.5":
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
case "anthropic/claude-3.7-sonnet":
@@ -6,7 +6,8 @@ export function checkContextWindowExceededError(error: unknown): boolean {
checkIsOpenRouterContextWindowError(error) ||
checkIsAnthropicContextWindowError(error) ||
checkIsCerebrasContextWindowError(error) ||
checkIsBedrockContextWindowError(error)
checkIsBedrockContextWindowError(error) ||
checkIsVercelContextWindowError(error)
)
}
@@ -113,3 +114,58 @@ function checkIsBedrockContextWindowError(error: any): boolean {
return false
}
}
export function checkIsVercelContextWindowError(error: any): boolean {
try {
const status = error?.status ?? error?.error?.param?.statusCode ?? error?.statusCode
// Check for explicit context_length_exceeded code (OpenAI streaming errors)
const errorCode = error?.error?.error?.code
if (errorCode === "context_length_exceeded") {
return true
}
const messages: string[] = [
error?.message,
error?.error?.message,
error?.error?.param?.message,
error?.error?.param?.error,
error?.error?.error?.message,
error?.error?.value?.error_message, // Alibaba Qwen validation errors
].filter((msg) => msg != null)
if (messages.length === 0) {
return false
}
// Must be a 400 error OR have 400 embedded in error_message (Alibaba Qwen case)
const hasValidStatus = String(status) === "400"
const errorMessage = error?.error?.value?.error_message
const has400InMessage =
errorMessage &&
typeof errorMessage === "string" &&
(errorMessage.includes('"code":400') || errorMessage.includes('"code": 400'))
if (!hasValidStatus && !has400InMessage) {
return false
}
const CONTEXT_ERROR_PATTERNS = [
/input is too long/i,
/input token count exceeds.*maximum.*tokens? allowed/i,
/input exceeds.*context window/i,
/requested input length.*exceeds.*maximum input length/i,
/prompt is too long.*tokens?\s*>\s*\d+\s*maximum/i,
/\bcontext\s*(?:length|window)\b.*exceed/i,
/\bmaximum\s*context\b/i,
/\b(?:input\s*)?tokens?\s*exceed/i,
/too\s*many\s*tokens/i,
] as const
return messages
.map((msg) => String(msg).toLowerCase())
.some((message) => CONTEXT_ERROR_PATTERNS.some((pattern) => pattern.test(message)))
} catch {
return false
}
}
@@ -15,7 +15,18 @@ export interface ModelMetadataEntry {
mode: string
}
export interface EnvironmentMetadataEntry {
ts: number
os_name: string
os_version: string
os_arch: string
host_name: string
host_version: string
cline_version: string
}
export interface TaskMetadata {
files_in_context: FileMetadataEntry[]
model_usage: ModelMetadataEntry[]
environment_history: EnvironmentMetadataEntry[]
}
@@ -0,0 +1,43 @@
import { collectEnvironmentMetadata, getTaskMetadata, saveTaskMetadata } from "@core/storage/disk"
import type { EnvironmentMetadataEntry } from "./ContextTrackerTypes"
export class EnvironmentContextTracker {
readonly taskId: string
constructor(taskId: string) {
this.taskId = taskId
}
async recordEnvironment() {
const metadata = await getTaskMetadata(this.taskId)
if (!metadata.environment_history) {
metadata.environment_history = []
}
const currentEnv = await collectEnvironmentMetadata()
const currentEnvWithTs: EnvironmentMetadataEntry = {
ts: Date.now(),
...currentEnv,
}
const lastEntry = metadata.environment_history[metadata.environment_history.length - 1]
if (lastEntry && this.isSameEnvironment(lastEntry, currentEnvWithTs)) {
return // No change, don't add duplicate
}
metadata.environment_history.push(currentEnvWithTs)
await saveTaskMetadata(this.taskId, metadata)
}
private isSameEnvironment(a: EnvironmentMetadataEntry, b: EnvironmentMetadataEntry): boolean {
return (
a.os_name === b.os_name &&
a.os_version === b.os_version &&
a.os_arch === b.os_arch &&
a.host_name === b.host_name &&
a.host_version === b.host_version &&
a.cline_version === b.cline_version
)
}
}
@@ -47,7 +47,7 @@ describe("FileContextTracker", () => {
chokidarWatchStub = sandbox.stub(chokidar, "watch").returns(mockFileSystemWatcher as any)
// Mock disk module functions
mockTaskMetadata = { files_in_context: [], model_usage: [] }
mockTaskMetadata = { files_in_context: [], model_usage: [], environment_history: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
@@ -17,7 +17,7 @@ describe("ModelContextTracker", () => {
sandbox = sinon.createSandbox()
// Mock disk module functions
mockTaskMetadata = { files_in_context: [], model_usage: [] }
mockTaskMetadata = { files_in_context: [], model_usage: [], environment_history: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
+50 -2
View File
@@ -1,16 +1,64 @@
import { StateManager } from "@core/storage/StateManager"
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { REMOTE_URI_SCHEME } from "@shared/remote-config/constants"
import { writeFile } from "@utils/fs"
import * as os from "os"
import * as path from "path"
import { Controller } from ".."
/**
* Opens a file in the editor
* @param controller The controller instance
* @param request The request message containing the file path in the 'value' field
* @param request The request message containing the file path in the 'value' field.
* Supports special URI format for remote rules/workflows:
* - remote://rule/{ruleName}
* - remote://workflow/{workflowName}
* @returns Empty response
*/
export async function openFile(_controller: Controller, request: StringRequest): Promise<Empty> {
if (request.value) {
openFileIntegration(request.value)
// Check for remote:// prefix for remote rules/workflows
if (request.value.startsWith(REMOTE_URI_SCHEME)) {
await openRemoteFile(request.value)
} else {
await openFileIntegration(request.value)
}
}
return Empty.create()
}
/**
* Opens a remote rule or workflow file by creating a temp file with its contents
* @param uri The remote URI in format: remote://rule/{name} or remote://workflow/{name}
*/
async function openRemoteFile(uri: string): Promise<void> {
// Parse: remote://rule/{name} or remote://workflow/{name}
const match = uri.match(/^remote:\/\/(rule|workflow)\/(.+)$/)
if (!match) {
throw new Error(`Invalid remote file URI: ${uri}`)
}
const [, type, name] = match
const remoteConfig = StateManager.get().getRemoteConfigSettings()
// Look up content based on type
const items = type === "rule" ? remoteConfig.remoteGlobalRules : remoteConfig.remoteGlobalWorkflows
const item = items?.find((r) => r.name === name)
if (!item?.contents) {
throw new Error(`Remote ${type} not found: ${name}`)
}
// Create temp file with read-only header comment
const typeLabel = type === "rule" ? "rule" : "workflow"
const header = `# ⚠️ READ-ONLY: This ${typeLabel} is managed by your organization.\n# Changes made here will not be saved.\n\n`
const content = header + item.contents
// Sanitize the name for use in filename (replace invalid characters)
const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "_")
const tempPath = path.join(os.tmpdir(), `cline-remote-${type}-${sanitizedName}.md`)
await writeFile(tempPath, content)
await openFileIntegration(tempPath)
}
+13 -8
View File
@@ -5,7 +5,6 @@ import { detectWorkspaceRoots } from "@core/workspace/detection"
import { setupWorkspaceManager } from "@core/workspace/setup"
import type { WorkspaceRootManager } from "@core/workspace/WorkspaceRootManager"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import type { ApiProvider, ModelInfo } from "@shared/api"
@@ -20,6 +19,7 @@ import type { UserInfo } from "@shared/UserInfo"
import { fileExistsAtPath } from "@utils/fs"
import axios from "axios"
import fs from "fs/promises"
import open from "open"
import pWaitFor from "p-wait-for"
import * as path from "path"
import type { FolderLockWithRetryResult } from "src/core/locks/types"
@@ -251,7 +251,14 @@ export class Controller {
historyItem?: HistoryItem,
taskSettings?: Partial<Settings>,
) {
await fetchRemoteConfig(this)
// Fire-and-forget: We intentionally don't await fetchRemoteConfig here.
// Remote config is already fetched in startRemoteConfigTimer() which runs in the constructor,
// so enterprise policies (yoloModeAllowed, allowedMCPServers, etc.) are already applied.
// This call just ensures we have the latest state, but we shouldn't block the UI for it.
// getGlobalSettingsKey() reads from remoteConfigCache on each call, so any updates
// will apply as soon as this fetch completes. The function also calls postStateToWebview()
// when done and catches all errors internally.
fetchRemoteConfig(this)
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
@@ -824,8 +831,9 @@ export class Controller {
}
async exportTaskWithId(id: string) {
const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
await downloadTask(historyItem.ts, apiConversationHistory)
const { taskDirPath } = await this.getTaskWithId(id)
console.log(`[EXPORT] Opening task directory: ${taskDirPath}`)
await open(taskDirPath)
}
async deleteTaskFromState(id: string) {
@@ -989,10 +997,7 @@ export class Controller {
remoteConfigSettings: this.stateManager.getRemoteConfigSettings(),
lastDismissedCliBannerVersion,
subagentsEnabled,
nativeToolCallSetting: {
user: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
featureFlag: featureFlagsService.getNativeToolCallEnabled(),
},
nativeToolCallSetting: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
}
}
@@ -0,0 +1,78 @@
import type { ModelInfo } from "@shared/api"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
import { fetchLiteLlmModelsInfo } from "@/core/api/providers/litellm"
import { StateManager } from "@/core/storage/StateManager"
import { toProtobufModels } from "@/shared/proto-conversions/models/typeConversion"
import { sendLiteLlmModelsEvent } from "./subscribeToLiteLlmModels"
/**
* Core function: Refreshes the LiteLLM models and returns application types
* @param controller The controller instance
* @returns Record of model ID to ModelInfo (application types)
*/
export async function refreshLiteLlmModels(): Promise<Record<string, ModelInfo>> {
const models: Record<string, ModelInfo> = {}
const stateManager = StateManager.get()
try {
// Get the LiteLLM configuration
const apiConfiguration = stateManager.getApiConfiguration()
const baseUrl = apiConfiguration.liteLlmBaseUrl || ""
const apiKey = apiConfiguration.liteLlmApiKey
if (!apiKey) {
throw new Error("LiteLLM API key is not configured or is invalid")
}
// Use the shared utility function to fetch model info
const data = await fetchLiteLlmModelsInfo(baseUrl, apiKey)
if (data?.data) {
for (const rawModel of data.data) {
const modelInfo: ModelInfo = {
name: rawModel.model_name,
maxTokens: rawModel.model_info?.max_output_tokens ?? rawModel.model_info?.max_tokens ?? 4096,
contextWindow: rawModel.model_info?.max_input_tokens ?? rawModel.model_info?.max_tokens ?? 8192,
supportsImages: rawModel.model_info?.supports_vision ?? false,
supportsPromptCache: rawModel.model_info?.supports_prompt_caching ?? false,
supportsReasoning: rawModel.model_info?.supports_reasoning ?? false,
inputPrice: rawModel.model_info?.input_cost_per_token
? rawModel.model_info.input_cost_per_token * 1_000_000
: 0,
outputPrice: rawModel.model_info?.output_cost_per_token
? rawModel.model_info.output_cost_per_token * 1_000_000
: 0,
cacheWritesPrice: rawModel.model_info?.cache_creation_input_token_cost
? rawModel.model_info.cache_creation_input_token_cost * 1_000_000
: undefined,
cacheReadsPrice: rawModel.model_info?.cache_read_input_token_cost
? rawModel.model_info.cache_read_input_token_cost * 1_000_000
: undefined,
description: undefined,
}
models[rawModel.model_name] = modelInfo
}
}
} catch (error) {
console.error("Error fetching LiteLLM models:", error)
throw error
}
// Store in StateManager's in-memory cache
StateManager.get().setModelsCache("liteLlm", models)
// Send event to subscribers
try {
await sendLiteLlmModelsEvent(
OpenRouterCompatibleModelInfo.create({
models: toProtobufModels(models),
}),
)
} catch (error) {
console.error("Error sending LiteLLM models event:", error)
}
return models
}
@@ -0,0 +1,21 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
import { toProtobufModels } from "../../../shared/proto-conversions/models/typeConversion"
import type { Controller } from "../index"
import { refreshLiteLlmModels } from "./refreshLiteLlmModels"
/**
* Refreshes LiteLLM models and returns protobuf types for gRPC
* @param controller The controller instance
* @param request Empty request (unused but required for gRPC signature)
* @returns OpenRouterCompatibleModelInfo with protobuf types
*/
export async function refreshLiteLlmModelsRpc(
_controller: Controller,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const models = await refreshLiteLlmModels()
return OpenRouterCompatibleModelInfo.create({
models: toProtobufModels(models),
})
}
@@ -134,6 +134,11 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
break
case "anthropic/claude-opus-4.5":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 6.25
modelInfo.cacheReadsPrice = 0.5
break
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
modelInfo.supportsPromptCache = true
@@ -0,0 +1,56 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
import { Controller } from "../index"
// Keep track of active LiteLLM models subscriptions
const activeLiteLlmModelsSubscriptions = new Set<StreamingResponseHandler<OpenRouterCompatibleModelInfo>>()
/**
* Subscribe to LiteLLM models events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToLiteLlmModels(
_controller: Controller,
_request: EmptyRequest,
responseStream: StreamingResponseHandler<OpenRouterCompatibleModelInfo>,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeLiteLlmModelsSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeLiteLlmModelsSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "liteLlmModels_subscription" }, responseStream)
}
}
/**
* Send a LiteLLM models event to all active subscribers
* @param models The LiteLLM models to send
*/
export async function sendLiteLlmModelsEvent(models: OpenRouterCompatibleModelInfo): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeLiteLlmModelsSubscriptions).map(async (responseStream) => {
try {
await responseStream(
models,
false, // Not the last message
)
} catch (error) {
console.error("Error sending LiteLLM models event:", error)
// Remove the subscription if there was an error
activeLiteLlmModelsSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -8,6 +8,7 @@ import { sendMcpMarketplaceCatalogEvent } from "../mcp/subscribeToMcpMarketplace
import { refreshBasetenModels } from "../models/refreshBasetenModels"
import { refreshGroqModels } from "../models/refreshGroqModels"
import { refreshHicapModels } from "../models/refreshHicapModels"
import { refreshLiteLlmModels } from "../models/refreshLiteLlmModels"
import { refreshOpenRouterModels } from "../models/refreshOpenRouterModels"
import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels"
@@ -194,6 +195,12 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
}
})
const liteLlmBaseUrl = controller.stateManager.getGlobalSettingsKey("liteLlmBaseUrl")
const liteLlmApiKey = controller.stateManager.getSecretKey("liteLlmApiKey")
if (liteLlmBaseUrl && liteLlmApiKey) {
await refreshLiteLlmModels()
}
// GUI relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// We do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
+7 -2
View File
@@ -241,8 +241,13 @@ cline "<prompt>"
* 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 const deepPlanningToolResponse = (focusChainSettings?: { enabled: boolean }, providerInfo?: ApiProviderInfo) => {
return getDeepPlanningPrompt(focusChainSettings, providerInfo)
export const deepPlanningToolResponse = (
focusChainSettings?: { enabled: boolean },
providerInfo?: ApiProviderInfo,
enableNativeToolCalls?: boolean,
) => {
return getDeepPlanningPrompt(focusChainSettings, providerInfo, enableNativeToolCalls)
}
@@ -2,15 +2,23 @@ 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/gpt5"
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): string {
export function getDeepPlanningPrompt(
focusChainSettings?: { enabled: boolean },
providerInfo?: ApiProviderInfo,
enableNativeToolCalls?: boolean,
): string {
// Create context for variant selection
const context: SystemPromptContext = {
providerInfo: providerInfo || ({} as ApiProviderInfo),
@@ -20,27 +28,61 @@ export function getDeepPlanningPrompt(focusChainSettings?: { enabled: boolean },
// 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-5") {
template = generateGPT51Template(focusChainSettings?.enabled ?? false)
if (variant.id === "gpt-51") {
template = generateGPT51Template(focusChainSettings?.enabled ?? false, enableNativeToolCalls ?? false)
} else if (variant.id === "gemini-3") {
template = generateGemini3Template(focusChainSettings?.enabled ?? false)
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)
}
// For variants with simpler focus chain prompting, Replace the FOCUS_CHAIN_PARAM placeholder with actual content
const focusChainParam = focusChainSettings?.enabled
? `**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.`
: ""
template = template.replace("{{FOCUS_CHAIN_PARAM}}", focusChainParam)
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"
@@ -254,7 +254,7 @@ Refer to @path/to/file/markdown.md for a complete breakdown of the task requirem
{{FOCUS_CHAIN_PARAM}}
{{NEW_TASK_INSTRUCTIONS}}
### Mode Switching
@@ -262,7 +262,7 @@ Refer to @path/to/file/markdown.md for a complete breakdown of the task requirem
{{FOCUS_CHAIN_PARAM}}
{{NEW_TASK_INSTRUCTIONS}}
### Mode Switching
@@ -26,8 +26,9 @@ export function createGemini3Variant(): DeepPlanningVariant {
/**
* Generates the deep-planning template with shell-specific commands
* @param focusChainEnabled Whether focus chain (task_progress) is enabled for this task
* @param enableNativeToolCalls Whether native tool calling is enabled
*/
export function generateGemini3Template(focusChainEnabled: boolean): string {
export function generateGemini3Template(focusChainEnabled: boolean, enableNativeToolCalls: boolean): string {
const detectedShell = getShell()
let isPowerShell = false
@@ -209,7 +210,34 @@ You also MUST include the path to the markdown file you have created in your new
}
</IMPORTANT>
${
enableNativeToolCalls
? `**new_task Tool Definition:**
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
{
"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.
`
: `**new_task Tool Definition:**
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
<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.
`
}
### Mode Switching
<IMPORTANT>
@@ -245,7 +245,7 @@ Refer to @path/to/file/markdown.md for a complete breakdown of the task requirem
{{FOCUS_CHAIN_PARAM}}
{{NEW_TASK_INSTRUCTIONS}}
### Mode Switching
@@ -26,8 +26,9 @@ export function createGPT51Variant(): DeepPlanningVariant {
/**
* Generates the deep-planning template with shell-specific commands
* @param focusChainEnabled Whether focus chain (task_progress) is enabled for this task
* @param enableNativeToolCalls Whether native tool calling is enabled
*/
export function generateGPT51Template(focusChainEnabled: boolean): string {
export function generateGPT51Template(focusChainEnabled: boolean, enableNativeToolCalls: boolean): string {
const detectedShell = getShell()
let isPowerShell = false
@@ -209,6 +210,34 @@ You also MUST include the path to the markdown file you have created in your new
}
</IMPORTANT>
${
enableNativeToolCalls
? `**new_task Tool Definition:**
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
{
"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.
`
: `**new_task Tool Definition:**
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
<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.
`
}
### Mode Switching
@@ -6,4 +6,4 @@ export { createAnthropicVariant } from "./anthropic"
export { createGeminiVariant } from "./gemini"
export { createGemini3Variant } from "./gemini3"
export { createGenericVariant } from "./generic"
export { createGPT51Variant } from "./gpt5"
export { createGPT51Variant } from "./gpt51"
@@ -65,8 +65,7 @@ However, if while writing your response you realize you actually need to do more
{
name: "response",
required: true,
instruction: `The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response. (You MUST use the response parameter, do not simply place the response text directly within <plan_mode_respond> tags.)`,
usage: "Your response here",
instruction: `The response to provide to the user.`,
},
{
name: "task_progress",
@@ -65,7 +65,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
ClineDefaultTool.TODO,
)
.placeholders({
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5,
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5_1,
})
.config({})
// Override components with custom templates from overrides.ts
@@ -78,7 +78,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
.build()
// Compile-time validation
const validationResult = validateVariant({ ...config, id: ModelFamily.NATIVE_GPT_5 }, { strict: true })
const validationResult = validateVariant({ ...config, id: ModelFamily.NATIVE_GPT_5_1 }, { strict: true })
if (!validationResult.isValid) {
console.error("GPT-5-1 variant configuration validation failed:", validationResult.errors)
throw new Error(`Invalid GPT-5-1 variant configuration: ${validationResult.errors.join(", ")}`)
+1 -1
View File
@@ -52,7 +52,7 @@ export async function parseSlashCommands(
compact: condenseToolResponse(focusChainSettings),
newrule: newRuleToolResponse(),
reportbug: reportBugToolResponse(),
"deep-planning": deepPlanningToolResponse(focusChainSettings, providerInfo),
"deep-planning": deepPlanningToolResponse(focusChainSettings, providerInfo, willUseNativeTools),
subagent: subagentToolResponse(),
}
+22 -3
View File
@@ -55,6 +55,7 @@ export class StateManager {
huaweiCloudMaasModels: Record<string, ModelInfo> | null
hicapModels: Record<string, ModelInfo> | null
aihubmixModels: Record<string, ModelInfo> | null
liteLlmModels: Record<string, ModelInfo> | null
} = {
openRouterModels: null,
groqModels: null,
@@ -64,6 +65,7 @@ export class StateManager {
huaweiCloudMaasModels: null,
hicapModels: null,
aihubmixModels: null,
liteLlmModels: null,
}
// Debounced persistence state
@@ -374,7 +376,16 @@ export class StateManager {
* Set models cache for a specific provider (in-memory only, not persisted)
*/
setModelsCache(
provider: "openRouter" | "groq" | "baseten" | "huggingFace" | "requesty" | "huaweiCloudMaas" | "hicap" | "aihubmix",
provider:
| "openRouter"
| "groq"
| "baseten"
| "huggingFace"
| "requesty"
| "huaweiCloudMaas"
| "hicap"
| "aihubmix"
| "liteLlm",
models: Record<string, ModelInfo>,
): void {
const cacheKey = `${provider}Models` as keyof typeof this.modelInfoCache
@@ -385,11 +396,19 @@ export class StateManager {
* Get model info by provider and model ID (from in-memory cache)
*/
getModelInfo(
provider: "openRouter" | "groq" | "baseten" | "huggingFace" | "requesty" | "huaweiCloudMaas" | "hicap" | "aihubmix",
provider:
| "openRouter"
| "groq"
| "baseten"
| "huggingFace"
| "requesty"
| "huaweiCloudMaas"
| "hicap"
| "aihubmix"
| "liteLlm",
modelId: string,
): ModelInfo | undefined {
const cacheKey = `${provider}Models` as keyof typeof this.modelInfoCache
console.log("[OpenRouter] modelInfoCache:", this.modelInfoCache)
return this.modelInfoCache[cacheKey]?.[modelId]
}
+34 -2
View File
@@ -1,5 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
import { EnvironmentMetadataEntry, TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
import { execa } from "@packages/execa"
import { ClineMessage } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
@@ -10,6 +10,7 @@ import fs from "fs/promises"
import os from "os"
import * as path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { ExtensionRegistryInfo } from "@/registry"
import { McpMarketplaceCatalog } from "@/shared/mcp"
import { StateManager } from "./StateManager"
@@ -168,6 +169,37 @@ export async function saveClineMessages(taskId: string, uiMessages: ClineMessage
}
}
/**
* Collects environment metadata for the current system and host.
* This information is used for debugging and task portability.
* Returns metadata without timestamp - timestamp is added by EnvironmentContextTracker.
*/
export async function collectEnvironmentMetadata(): Promise<Omit<EnvironmentMetadataEntry, "ts">> {
try {
const hostVersion = await HostProvider.env.getHostVersion({})
return {
os_name: os.platform(),
os_version: os.release(),
os_arch: os.arch(),
host_name: hostVersion.platform || "Unknown",
host_version: hostVersion.version || "Unknown",
cline_version: ExtensionRegistryInfo.version,
}
} catch (error) {
console.error("Failed to collect environment metadata:", error)
// Return fallback values if collection fails
return {
os_name: os.platform(),
os_version: os.release(),
os_arch: os.arch(),
host_name: "Unknown",
host_version: "Unknown",
cline_version: "Unknown",
}
}
}
export async function getTaskMetadata(taskId: string): Promise<TaskMetadata> {
const filePath = path.join(await ensureTaskDirectoryExists(taskId), GlobalFileNames.taskMetadata)
try {
@@ -177,7 +209,7 @@ export async function getTaskMetadata(taskId: string): Promise<TaskMetadata> {
} catch (error) {
console.error("Failed to read task metadata:", error)
}
return { files_in_context: [], model_usage: [] }
return { files_in_context: [], model_usage: [], environment_history: [] }
}
export async function saveTaskMetadata(taskId: string, metadata: TaskMetadata) {
+12
View File
@@ -311,11 +311,23 @@ class ReasoningHandler {
return null
}
// Ensure signature is set if it's hidden in the summary / reasoning details
// to ensure it's always accessible at the top level by each provider.
if (!this.pendingReasoning.signature && this.pendingReasoning.summary.length) {
const lastSummary = this.pendingReasoning.summary.at(-1)
if (lastSummary && typeof lastSummary === "object" && "signature" in lastSummary) {
if (typeof lastSummary.signature === "string") {
this.pendingReasoning.signature = lastSummary.signature
}
}
}
return {
type: "thinking",
thinking: this.pendingReasoning.content,
signature: this.pendingReasoning.signature,
summary: this.pendingReasoning.summary,
call_id: this.pendingReasoning.id,
}
}
+24 -6
View File
@@ -5,6 +5,7 @@ import { AssistantMessageContent, parseAssistantMessageV2, ToolUse } from "@core
import { ContextManager } from "@core/context/context-management/ContextManager"
import { checkContextWindowExceededError } from "@core/context/context-management/context-error-handling"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { EnvironmentContextTracker } from "@core/context/context-tracking/EnvironmentContextTracker"
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
import { ModelContextTracker } from "@core/context/context-tracking/ModelContextTracker"
import {
@@ -65,6 +66,7 @@ import { HistoryItem } from "@shared/HistoryItem"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
import { CLINE_MCP_TOOL_IDENTIFIER } from "@shared/mcp"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import type { Mode } from "@shared/storage/types"
import { ClineDefaultTool } from "@shared/tools"
import { ClineAskResponse } from "@shared/WebviewMessage"
import { isClaude4PlusModelFamily, isGPT5ModelFamily, isLocalModel, isNextGenModelFamily } from "@utils/model-utils"
@@ -226,6 +228,7 @@ export class Task {
// Metadata tracking
private fileContextTracker: FileContextTracker
private modelContextTracker: ModelContextTracker
private environmentContextTracker: EnvironmentContextTracker
// Focus Chain
private FocusChainManager?: FocusChainManager
@@ -362,9 +365,10 @@ export class Task {
updateTaskHistory: this.updateTaskHistory,
})
// Initialize file context tracker
// Initialize context trackers
this.fileContextTracker = new FileContextTracker(controller, this.taskId)
this.modelContextTracker = new ModelContextTracker(this.taskId)
this.environmentContextTracker = new EnvironmentContextTracker(this.taskId)
// Initialize focus chain manager only if enabled
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings")
@@ -1017,6 +1021,13 @@ export class Task {
})
}
// Record environment metadata for new task
try {
await this.environmentContextTracker.recordEnvironment()
} catch (error) {
console.error("Failed to record environment metadata:", error)
}
await this.initiateTaskLoop(userContent)
}
@@ -1279,6 +1290,13 @@ export class Task {
})
}
// Record environment metadata when resuming task (tracks cross-platform migrations)
try {
await this.environmentContextTracker.recordEnvironment()
} catch (error) {
console.error("Failed to record environment metadata on resume:", error)
}
await this.messageStateHandler.overwriteApiConversationHistory(modifiedApiConversationHistory)
await this.initiateTaskLoop(newUserContent)
}
@@ -2100,8 +2118,7 @@ export class Task {
workspaceRoots,
isSubagentsEnabledAndCliInstalled,
isCliSubagent,
enableNativeToolCalls:
featureFlagsService.getNativeToolCallEnabled() && this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
enableNativeToolCalls: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
}
const { systemPrompt, tools } = await getSystemPrompt(promptContext)
@@ -2670,7 +2687,9 @@ export class Task {
content: userContent,
})
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
const modeSetting = this.stateManager.getGlobalSettingsKey("mode")
const currentMode: Mode = modeSetting === "act" ? "act" : "plan"
telemetryService.captureConversationTurnEvent(this.ulid, providerId, model.id, "user", currentMode)
// Capture task initialization timing telemetry for the first API request
@@ -2747,7 +2766,6 @@ export class Task {
})
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
telemetryService.captureConversationTurnEvent(
this.ulid,
providerId,
@@ -3210,7 +3228,7 @@ export class Task {
// Pre-fetch necessary data to avoid redundant calls within loops
const ulid = this.ulid
const focusChainSettings = this.stateManager.getGlobalSettingsKey("focusChainSettings")
const useNativeToolCalls = this.useNativeToolCalls
const useNativeToolCalls = this.stateManager.getGlobalStateKey("nativeToolCallEnabled")
const providerInfo = this.getCurrentProviderInfo()
const cwd = this.cwd
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(this.controller, cwd)
+4 -53
View File
@@ -1,58 +1,9 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { writeFile } from "@utils/fs"
import os from "os"
import * as path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/host/window"
import { openFile } from "./open-file"
export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) {
// File name
const date = new Date(dateTs)
const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase()
const day = date.getDate()
const year = date.getFullYear()
let hours = date.getHours()
const minutes = date.getMinutes().toString().padStart(2, "0")
const seconds = date.getSeconds().toString().padStart(2, "0")
const ampm = hours >= 12 ? "pm" : "am"
hours = hours % 12
hours = hours ? hours : 12 // the hour '0' should be '12'
const fileName = `cline_task_${month}-${day}-${year}_${hours}-${minutes}-${seconds}-${ampm}.md`
// Generate markdown
const markdownContent = conversationHistory
.map((message) => {
const role = message.role === "user" ? "**User:**" : "**Assistant:**"
const content = Array.isArray(message.content)
? message.content.map((block) => formatContentBlockToMarkdown(block)).join("\n")
: message.content
return `${role}\n\n${content}\n\n`
})
.join("---\n\n")
// Prompt user for save location
const saveResponse = await HostProvider.window.showSaveDialog({
options: {
filters: { Markdown: { extensions: ["md"] } },
defaultPath: path.join(os.homedir(), "Downloads", fileName),
},
})
if (saveResponse.selectedPath) {
try {
// Write content to the selected location
await writeFile(saveResponse.selectedPath, markdownContent)
await openFile(saveResponse.selectedPath, false, true)
} catch (error) {
await HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
})
}
}
}
/**
* Formats a content block to markdown for display in API request messages.
* Used by Task class to format user content for the api_req_started message.
*/
export function formatContentBlockToMarkdown(block: Anthropic.ContentBlockParam): string {
switch (block.type) {
case "text":
+2 -1
View File
@@ -1,6 +1,6 @@
import axios from "axios"
import ogs from "open-graph-scraper"
import { getAxiosSettings } from "@/shared/net"
import { fetch, getAxiosSettings } from "@/shared/net"
export interface OpenGraphData {
title?: string
@@ -28,6 +28,7 @@ export async function fetchOpenGraphData(url: string): Promise<OpenGraphData> {
fetchOptions: {
redirect: "follow", // Follow redirects
} as any,
fetch, // Use configured fetch with proxy support
}
const { result } = await ogs(options)
+55 -25
View File
@@ -69,6 +69,7 @@ export class AuthService {
protected _activeAuthStatusUpdateHandlers = new Set<StreamingResponseHandler<AuthState>>()
protected _handlerToController = new Map<StreamingResponseHandler<AuthState>, Controller>()
protected _controller: Controller
protected _refreshPromise: Promise<void> | null = null
/**
* Creates an instance of AuthService.
@@ -152,28 +153,56 @@ export class AuthService {
// Check if token has expired
if (await provider.shouldRefreshIdToken(clineAccountAuthToken, this._clineAuthInfo.expiresAt)) {
try {
const updatedAuthInfo = await provider.retrieveClineAuthInfo(this._controller)
if (updatedAuthInfo) {
this._clineAuthInfo = updatedAuthInfo
this._authenticated = true
clineAccountAuthToken = updatedAuthInfo.idToken
}
} catch (error) {
// Only log out for permanent auth failures, not network issues
if (error instanceof AuthInvalidTokenError) {
Logger.error("Token is invalid or expired:", error)
this._clineAuthInfo = null
this._authenticated = false
telemetryService.captureAuthLoggedOut(this._provider?.name, LogoutReason.ERROR_RECOVERY)
} else if (error instanceof AuthNetworkError) {
Logger.error("Network error refreshing token", error)
// Keep existing auth info, will retry on next getAuthToken() call
} else {
throw error // Re-throw unexpected errors
}
// If a refresh is already in progress, wait for it to complete
if (this._refreshPromise) {
Logger.info("Token refresh already in progress, waiting for completion")
await this._refreshPromise
// After waiting, return the updated token
clineAccountAuthToken = this._clineAuthInfo?.idToken
return clineAccountAuthToken ? `workos:${clineAccountAuthToken}` : null
}
await this.sendAuthStatusUpdate()
// Start a new refresh operation
this._refreshPromise = (async () => {
let authStatusChanged = false
try {
const updatedAuthInfo = await provider.retrieveClineAuthInfo(this._controller)
if (updatedAuthInfo) {
this._clineAuthInfo = updatedAuthInfo
this._authenticated = true
clineAccountAuthToken = updatedAuthInfo.idToken
authStatusChanged = true
}
} catch (error) {
// Only log out for permanent auth failures, not network issues
if (error instanceof AuthInvalidTokenError) {
Logger.error("Token is invalid or expired:", error)
this._clineAuthInfo = null
this._authenticated = false
telemetryService.captureAuthLoggedOut(this._provider?.name, LogoutReason.ERROR_RECOVERY)
authStatusChanged = true
} else if (error instanceof AuthNetworkError) {
Logger.error("Network error refreshing token", error)
// Keep existing auth info, will retry on next getAuthToken() call
} else {
throw error // Re-throw unexpected errors
}
} finally {
this._refreshPromise = null
}
// Defer auth status update to avoid infinite loop
if (authStatusChanged) {
setImmediate(() => {
this.sendAuthStatusUpdate().catch((error) => {
Logger.error("Error sending auth status update after token refresh:", error)
})
})
}
})()
await this._refreshPromise
}
return clineAccountAuthToken ? `workos:${clineAccountAuthToken}` : null
@@ -393,11 +422,12 @@ export class AuthService {
// Identify the user in telemetry if available
if (this._clineAuthInfo?.userInfo?.id) {
telemetryService.identifyAccount(this._clineAuthInfo.userInfo)
// Reset feature flags to ensure they are fetched for the new/logged in user
featureFlagsService.reset(this._clineAuthInfo?.userInfo?.id)
// Poll feature flags immediately for authenticated users to ensure cache is populated
await featureFlagsService.poll(this._clineAuthInfo?.userInfo?.id)
} else {
// Poll feature flags for unauthenticated state
await featureFlagsService.poll(undefined)
}
// Poll feature flags to ensure they are up to date for all users
await featureFlagsService.poll(this._clineAuthInfo?.userInfo?.id)
// Update state in webviews once per unique controller
await Promise.all(Array.from(uniqueControllers).map((c) => c.postStateToWebview()))
+3 -4
View File
@@ -56,14 +56,13 @@ export class ClineError extends Error {
public readonly modelId?: string,
public readonly providerId?: string,
) {
const serialized = serializeError(raw)
const error = serialized?.response || serialized
const error = serializeError(raw)
const message = error.message || String(error) || error?.cause?.means
const message = error.message || error?.response?.message || String(error) || error?.cause?.means
super(message)
// Extract status from multiple possible locations
const status = error.status || error.statusCode
const status = error.status || error.statusCode || error.response?.status
this.modelId = modelId || error.modelId
this.providerId = providerId || error.providerId
+1 -1
View File
@@ -41,7 +41,7 @@ export class ErrorService {
public logException(error: Error | ClineError, properties?: Record<string, unknown>): void {
this.provider.logException(error, properties)
console.error("[ErrorService] Logging exception", error)
console.error("[ErrorService] Logging exception", JSON.stringify(error))
}
public logMessage(
@@ -38,13 +38,15 @@ export class FeatureFlagsService {
return
}
}
this.cacheInfo = { updateTime: timesNow, userId: userId || null }
for (const flag of FEATURE_FLAGS) {
const payload = await this.getFeatureFlag(flag).catch(() => false)
this.cache.set(flag, payload ?? false)
}
// Only update timestamp after successfully populating cache
this.cacheInfo = { updateTime: timesNow, userId: userId || null }
getClineOnboardingModels() // Refresh onboarding models cache if relevant flag changed
}
@@ -93,10 +95,6 @@ export class FeatureFlagsService {
return this.getBooleanFlagEnabled(FeatureFlag.HOOKS)
}
public getNativeToolCallEnabled(): boolean {
return this.getBooleanFlagEnabled(FeatureFlag.NATIVE_TOOL_CALLS_NEXT_GEN_MODELS)
}
public isResponseApiEnabled(): boolean {
return this.getBooleanFlagEnabled(FeatureFlag.OPENAI_NATIVE_RESPONSE_API)
}
@@ -149,19 +147,6 @@ export class FeatureFlagsService {
return this.provider.getSettings()
}
/**
* Reset the feature flags cache
* Should run on user auth state changes to ensure flags are up-to-date
*/
public reset(userId?: string): void {
// Skip known user ID to avoid redundant resets
if (userId && this.cacheInfo.userId === userId) {
return
}
this.cacheInfo = { updateTime: 0, userId: userId || null }
this.cache.clear()
}
/**
* For testing: directly set a feature flag in the cache
*/
+1
View File
@@ -34,6 +34,7 @@ import * as path from "path"
import ReconnectingEventSource from "reconnecting-eventsource"
import { z } from "zod"
import { HostProvider } from "@/hosts/host-provider"
import { fetch } from "@/shared/net"
import { ShowMessageType } from "@/shared/proto/host/window"
import { getServerAuthHash } from "@/utils/mcpAuth"
import { TelemetryService } from "../telemetry/TelemetryService"
@@ -1,7 +1,7 @@
import { getValidOpenTelemetryConfig } from "@/shared/services/config/otel-config"
import { isPostHogConfigValid, posthogConfig } from "@/shared/services/config/posthog-config"
import { Logger } from "../logging/Logger"
import type { ITelemetryProvider } from "./providers/ITelemetryProvider"
import type { ITelemetryProvider, TelemetryProperties, TelemetrySettings } from "./providers/ITelemetryProvider"
import { OpenTelemetryClientProvider } from "./providers/opentelemetry/OpenTelemetryClientProvider"
import { OpenTelemetryTelemetryProvider } from "./providers/opentelemetry/OpenTelemetryTelemetryProvider"
import { PostHogClientProvider } from "./providers/posthog/PostHogClientProvider"
@@ -15,9 +15,10 @@ export type TelemetryProviderType = "posthog" | "no-op" | "opentelemetry"
/**
* Configuration for telemetry providers
*/
export interface TelemetryProviderConfig {
type: TelemetryProviderType
}
export type TelemetryProviderConfig =
| { type: "posthog"; apiKey?: string; host?: string }
| { type: "opentelemetry"; enabled?: boolean }
| { type: "no-op" }
/**
* Factory class for creating telemetry providers
@@ -27,17 +28,25 @@ export class TelemetryProviderFactory {
/**
* Creates multiple telemetry providers based on configuration
* Supports dual tracking during transition period
* @returns Array of ITelemetryProvider instances
*/
public static async createProviders(): Promise<ITelemetryProvider[]> {
const configs = TelemetryProviderFactory.getDefaultConfigs()
const providers: ITelemetryProvider[] = await Promise.all(configs.map((c) => TelemetryProviderFactory.createProvider(c)))
const providers: ITelemetryProvider[] = []
// Fallback to no-op if no providers available
for (const config of configs) {
try {
const provider = await TelemetryProviderFactory.createProvider(config)
providers.push(provider)
} catch (error) {
Logger.error(`Failed to create telemetry provider: ${config.type}`, error)
}
}
// Always have at least a no-op provider
if (providers.length === 0) {
providers.push(new NoOpTelemetryProvider())
Logger.info("TelemetryProviderFactory: Using NoOp provider (no valid configs)")
}
Logger.info("TelemetryProviderFactory: Created providers - " + providers.map((p) => p.constructor.name).join(", "))
return providers
}
@@ -46,8 +55,6 @@ export class TelemetryProviderFactory {
* Creates a single telemetry provider based on the provided configuration
* @param config Configuration for the telemetry provider
* @returns ITelemetryProvider instance
* @deprecated Use createProviders() for multi-provider support
* @deprecated Use createProviders() for multi-provider support
*/
private static async createProvider(config: TelemetryProviderConfig): Promise<ITelemetryProvider> {
switch (config.type) {
@@ -68,11 +75,9 @@ export class TelemetryProviderFactory {
return new NoOpTelemetryProvider()
}
case "no-op":
return new NoOpTelemetryProvider()
default:
// Always fallback to NoOp provider. Only log error for unsupported types
if (config.type !== "no-op") {
console.error(`Unsupported telemetry provider type: ${config.type}`)
}
Logger.error(`Unsupported telemetry provider type: ${(config as { type?: string }).type ?? "unknown"}`)
return new NoOpTelemetryProvider()
}
}
@@ -80,17 +85,19 @@ export class TelemetryProviderFactory {
/**
* Gets the default telemetry provider configuration
* @returns Default configuration using available providers
* @returns Default configuration using available providers
*/
public static getDefaultConfigs(): TelemetryProviderConfig[] {
const configs: TelemetryProviderConfig[] = []
if (isPostHogConfigValid(posthogConfig)) {
configs.push({ type: "posthog", ...posthogConfig })
}
const otelConfig = getValidOpenTelemetryConfig()
if (otelConfig) {
configs.push({ type: "opentelemetry", ...otelConfig })
}
return configs.length > 0 ? configs : [{ type: "no-op" }]
}
}
@@ -100,38 +107,59 @@ export class TelemetryProviderFactory {
* or for testing purposes
*/
export class NoOpTelemetryProvider implements ITelemetryProvider {
public isOptIn = true
private isOptIn = true
public log(event: string, properties?: Record<string, unknown>): void {
Logger.log(`[NoOpTelemetryProvider] ${event}: ${JSON.stringify(properties)}`)
log(_event: string, _properties?: TelemetryProperties): void {
Logger.log(`[NoOpTelemetryProvider] ${_event}: ${JSON.stringify(_properties)}`)
}
public logRequired(event: string, properties?: Record<string, unknown>): void {
Logger.log(`[NoOpTelemetryProvider] REQUIRED ${event}: ${JSON.stringify(properties)}`)
logRequired(_event: string, _properties?: TelemetryProperties): void {
Logger.log(`[NoOpTelemetryProvider] REQUIRED ${_event}: ${JSON.stringify(_properties)}`)
}
public identifyUser(userInfo: any, properties?: Record<string, unknown>): void {
Logger.info(`[NoOpTelemetryProvider] identifyUser - ${JSON.stringify(userInfo)} - ${JSON.stringify(properties)}`)
identifyUser(_userInfo: any, _properties?: TelemetryProperties): void {
Logger.info(`[NoOpTelemetryProvider] identifyUser - ${JSON.stringify(_userInfo)} - ${JSON.stringify(_properties)}`)
}
public setOptIn(optIn: boolean): void {
Logger.info(`[NoOpTelemetryProvider] setOptIn(${optIn})`)
this.isOptIn = optIn
setOptIn(_optIn: boolean): void {
Logger.info(`[NoOpTelemetryProvider] setOptIn(${_optIn})`)
this.isOptIn = _optIn
}
public isEnabled(): boolean {
isEnabled(): boolean {
return false
}
public getSettings() {
getSettings(): TelemetrySettings {
return {
extensionEnabled: false,
hostEnabled: false,
level: "off" as const,
level: "off",
}
}
public async dispose(): Promise<void> {
Logger.info("[NoOpTelemetryProvider] Disposing")
recordCounter(
_name: string,
_value: number,
_attributes?: TelemetryProperties,
_description?: string,
_required = false,
): void {
// no-op
}
recordHistogram(
_name: string,
_value: number,
_attributes?: TelemetryProperties,
_description?: string,
_required = false,
): void {
// no-op
}
recordGauge(
_name: string,
_value: number | null,
_attributes?: TelemetryProperties,
_description?: string,
_required = false,
): void {
// no-op
}
async dispose(): Promise<void> {
Logger.info(`[NoOpTelemetryProvider] Disposing (optIn=${this.isOptIn})`)
}
}
@@ -27,8 +27,8 @@ describe("Telemetry system is abstracted and can easily switch between providers
})
const MOCK_USER_INFO = {
id: "test-user-123",
email: "test@example.com",
displayName: "Test User",
email: "test@example.com",
createdAt: new Date().toISOString(),
organizations: [],
}
+236
View File
@@ -81,6 +81,42 @@ export class TelemetryService {
["subagents", true], // CLI Subagents telemetry enabled
])
private userId?: string
private taskTurnCounts = new Map<string, number>()
private taskToolCallCounts = new Map<string, number>()
private taskErrorCounts = new Map<string, number>()
public static readonly METRICS = {
TASK: {
TURNS_TOTAL: "cline.turns.total",
TURNS_PER_TASK: "cline.turns.per_task",
TOKENS_INPUT_TOTAL: "cline.tokens.input.total",
TOKENS_INPUT_PER_RESPONSE: "cline.tokens.input.per_response",
TOKENS_OUTPUT_TOTAL: "cline.tokens.output.total",
TOKENS_OUTPUT_PER_RESPONSE: "cline.tokens.output.per_response",
COST_TOTAL: "cline.cost.total",
COST_PER_EVENT: "cline.cost.per_event",
},
CACHE: {
WRITE_TOTAL: "cline.cache.write.tokens.total",
WRITE_PER_EVENT: "cline.cache.write.tokens.per_event",
READ_TOTAL: "cline.cache.read.tokens.total",
READ_PER_EVENT: "cline.cache.read.tokens.per_event",
HITS_TOTAL: "cline.cache.hits.total",
},
TOOLS: {
CALLS_TOTAL: "cline.tool.calls.total",
CALLS_PER_TASK: "cline.tool.calls.per_task",
},
ERRORS: {
TOTAL: "cline.errors.total",
PER_TASK: "cline.errors.per_task",
},
API: {
TTFT_SECONDS: "cline.api.ttft.seconds",
DURATION_SECONDS: "cline.api.duration.seconds",
THROUGHPUT_TOKENS_PER_SECOND: "cline.api.throughput.tokens_per_second",
},
}
// Event constants for tracking user interactions and system events
private static readonly EVENTS = {
// Task-related events for tracking conversation and execution flow
@@ -329,6 +365,81 @@ export class TelemetryService {
})
}
private getStandardAttributes(extra?: TelemetryProperties): TelemetryProperties {
return {
...this.telemetryMetadata,
...(this.userId ? { userId: this.userId } : {}),
...(extra ?? {}),
}
}
private recordCounter(
name: string,
value: number,
attributes?: TelemetryProperties,
description?: string,
required = false,
): void {
const attrs = this.getStandardAttributes(attributes)
this.providers.forEach((provider) => {
try {
provider.recordCounter(name, value, attrs, description, required)
} catch (error) {
console.error(`[TelemetryService] recordCounter failed: ${name}`, error)
}
})
}
private recordHistogram(
name: string,
value: number,
attributes?: TelemetryProperties,
description?: string,
required = false,
): void {
const attrs = this.getStandardAttributes(attributes)
this.providers.forEach((provider) => {
try {
provider.recordHistogram(name, value, attrs, description, required)
} catch (error) {
console.error(`[TelemetryService] recordHistogram failed: ${name}`, error)
}
})
}
/**
* Gauge values require explicit cleanup: callers must pass null with the same attribute set
* when the series identified by name+attributes ends to prevent stale metric entries.
*/
private recordGauge(
name: string,
value: number | null,
attributes?: TelemetryProperties,
description?: string,
required = false,
): void {
const attrs = this.getStandardAttributes(attributes)
this.providers.forEach((provider) => {
try {
provider.recordGauge(name, value, attrs, description, required)
} catch (error) {
console.error(`[TelemetryService] recordGauge failed: ${name}`, error)
}
})
}
private incrementTaskCounter(store: Map<string, number>, ulid: string): number {
const nextValue = (store.get(ulid) ?? 0) + 1
store.set(ulid, nextValue)
return nextValue
}
private resetTaskAggregates(ulid: string): void {
this.taskTurnCounts.delete(ulid)
this.taskToolCallCounts.delete(ulid)
this.taskErrorCounts.delete(ulid)
}
public captureExtensionActivated() {
this.captureToProviders(TelemetryService.EVENTS.USER.EXTENSION_ACTIVATED, {}, false)
}
@@ -396,6 +507,7 @@ export class TelemetryService {
...this.telemetryMetadata,
}
this.userId = userInfo.id
// Update all providers with error isolation
this.providers.forEach((provider) => {
try {
@@ -539,6 +651,7 @@ export class TelemetryService {
* @param openAiCompatibleDomain Optional domain for OpenAI Compatible providers (e.g., "api.example.com")
*/
public captureTaskCreated(ulid: string, apiProvider?: string, openAiCompatibleDomain?: string) {
this.resetTaskAggregates(ulid)
this.capture({
event: TelemetryService.EVENTS.TASK.CREATED,
properties: { ulid, apiProvider, openAiCompatibleDomain },
@@ -552,6 +665,7 @@ export class TelemetryService {
* @param openAiCompatibleDomain Optional domain for OpenAI Compatible providers (e.g., "api.example.com")
*/
public captureTaskRestarted(ulid: string, apiProvider?: string, openAiCompatibleDomain?: string) {
this.resetTaskAggregates(ulid)
this.capture({
event: TelemetryService.EVENTS.TASK.RESTARTED,
properties: { ulid, apiProvider, openAiCompatibleDomain },
@@ -567,6 +681,7 @@ export class TelemetryService {
event: TelemetryService.EVENTS.TASK.COMPLETED,
properties: { ulid },
})
this.resetTaskAggregates(ulid)
}
/**
@@ -612,6 +727,51 @@ export class TelemetryService {
isNativeToolCall,
},
})
const turnCount = this.incrementTaskCounter(this.taskTurnCounts, ulid)
const turnAttributes = { ulid, provider, model, source, mode }
this.recordCounter(TelemetryService.METRICS.TASK.TURNS_TOTAL, 1, turnAttributes)
this.recordHistogram(TelemetryService.METRICS.TASK.TURNS_PER_TASK, turnCount, turnAttributes)
if (Number.isFinite(tokenUsage.cacheWriteTokens)) {
const cacheWriteTokens = tokenUsage.cacheWriteTokens ?? 0
this.recordCounter(TelemetryService.METRICS.CACHE.WRITE_TOTAL, cacheWriteTokens, {
ulid,
provider,
model,
mode,
})
this.recordHistogram(TelemetryService.METRICS.CACHE.WRITE_PER_EVENT, cacheWriteTokens, {
ulid,
provider,
model,
mode,
})
}
if (Number.isFinite(tokenUsage.cacheReadTokens)) {
const cacheReadTokens = tokenUsage.cacheReadTokens ?? 0
this.recordCounter(TelemetryService.METRICS.CACHE.READ_TOTAL, cacheReadTokens, {
ulid,
provider,
model,
mode,
})
this.recordHistogram(TelemetryService.METRICS.CACHE.READ_PER_EVENT, cacheReadTokens, {
ulid,
provider,
model,
mode,
})
}
if (Number.isFinite(tokenUsage.totalCost)) {
const totalCost = tokenUsage.totalCost ?? 0
const costAttributes = { ulid, provider, model, mode, currency: "USD" }
this.recordCounter(TelemetryService.METRICS.TASK.COST_TOTAL, totalCost, costAttributes)
this.recordHistogram(TelemetryService.METRICS.TASK.COST_PER_EVENT, totalCost, costAttributes)
}
}
/**
@@ -631,6 +791,18 @@ export class TelemetryService {
model,
},
})
if (Number.isFinite(tokensIn)) {
const value = tokensIn ?? 0
this.recordCounter(TelemetryService.METRICS.TASK.TOKENS_INPUT_TOTAL, value, { ulid, model })
this.recordHistogram(TelemetryService.METRICS.TASK.TOKENS_INPUT_PER_RESPONSE, value, { ulid, model })
}
if (Number.isFinite(tokensOut)) {
const value = tokensOut ?? 0
this.recordCounter(TelemetryService.METRICS.TASK.TOKENS_OUTPUT_TOTAL, value, { ulid, model })
this.recordHistogram(TelemetryService.METRICS.TASK.TOKENS_OUTPUT_PER_RESPONSE, value, { ulid, model })
}
}
/**
@@ -692,6 +864,7 @@ export class TelemetryService {
feedbackType,
},
})
this.resetTaskAggregates(ulid)
}
// Tool events
@@ -739,6 +912,17 @@ export class TelemetryService {
isNativeToolCall,
},
})
const toolAttributes = {
ulid,
tool,
model: modelId,
success,
autoApproved,
}
const toolCallCount = this.incrementTaskCounter(this.taskToolCallCounts, ulid)
this.recordCounter(TelemetryService.METRICS.TOOLS.CALLS_TOTAL, 1, toolAttributes)
this.recordHistogram(TelemetryService.METRICS.TOOLS.CALLS_PER_TASK, toolCallCount, toolAttributes)
}
/**
@@ -990,6 +1174,34 @@ export class TelemetryService {
...data,
},
})
if (typeof data.ttftSec === "number") {
this.recordHistogram(TelemetryService.METRICS.API.TTFT_SECONDS, data.ttftSec, {
ulid,
model: modelId,
provider: "gemini",
})
}
if (typeof data.totalDurationSec === "number") {
this.recordHistogram(TelemetryService.METRICS.API.DURATION_SECONDS, data.totalDurationSec, {
ulid,
model: modelId,
provider: "gemini",
})
}
if (typeof data.throughputTokensPerSec === "number") {
this.recordHistogram(TelemetryService.METRICS.API.THROUGHPUT_TOKENS_PER_SECOND, data.throughputTokensPerSec, {
ulid,
model: modelId,
provider: "gemini",
})
}
if (data.cacheHit) {
this.recordCounter(TelemetryService.METRICS.CACHE.HITS_TOTAL, 1, { ulid, model: modelId, provider: "gemini" })
}
}
/**
@@ -1043,6 +1255,21 @@ export class TelemetryService {
timestamp: new Date().toISOString(),
},
})
this.recordCounter(TelemetryService.METRICS.ERRORS.TOTAL, 1, {
ulid: args.ulid,
model: args.model,
provider: args.provider,
error_status: args.errorStatus,
})
const errorAttributes = {
ulid: args.ulid,
model: args.model,
provider: args.provider,
error_status: args.errorStatus,
}
const errorCount = this.incrementTaskCounter(this.taskErrorCounts, args.ulid)
this.recordHistogram(TelemetryService.METRICS.ERRORS.PER_TASK, errorCount, errorAttributes)
}
/**
@@ -1374,6 +1601,15 @@ export class TelemetryService {
feature_flag_enabled: featureFlagEnabled,
},
})
const isMultiRoot = rootCount > 1
this.recordGauge("cline.workspace.active_roots", rootCount, {
is_multi_root: isMultiRoot,
})
// Retire the previous series to avoid leaking gauge entries when the flag flips.
this.recordGauge("cline.workspace.active_roots", null, {
is_multi_root: !isMultiRoot,
})
}
/**
@@ -0,0 +1,192 @@
import * as assert from "assert"
import type { ITelemetryProvider, TelemetryProperties, TelemetrySettings } from "../providers/ITelemetryProvider"
import { TelemetryService } from "../TelemetryService"
class FakeProvider implements ITelemetryProvider {
public counters: Array<{ name: string; value: number; attributes: TelemetryProperties; description?: string }> = []
public histograms: Array<{ name: string; value: number; attributes: TelemetryProperties; description?: string }> = []
public gauges = new Map<string, Map<string, { value: number; attributes: TelemetryProperties; description?: string }>>()
log(): void {}
logRequired(): void {}
identifyUser(): void {}
setOptIn(): void {}
isEnabled(): boolean {
return true
}
getSettings(): TelemetrySettings {
return { extensionEnabled: true, hostEnabled: true, level: "all" }
}
recordCounter(name: string, value: number, attributes?: TelemetryProperties, description?: string, _required = false): void {
this.counters.push({ name, value, attributes: attributes ?? {}, description })
}
recordHistogram(
name: string,
value: number,
attributes?: TelemetryProperties,
description?: string,
_required = false,
): void {
this.histograms.push({ name, value, attributes: attributes ?? {}, description })
}
recordGauge(
name: string,
value: number | null,
attributes?: TelemetryProperties,
description?: string,
_required = false,
): void {
const attrKey = JSON.stringify(attributes ?? {})
const series = this.gauges.get(name)
if (value === null) {
series?.delete(attrKey)
if (series && series.size === 0) {
this.gauges.delete(name)
}
return
}
let nextSeries = series
if (!nextSeries) {
nextSeries = new Map()
this.gauges.set(name, nextSeries)
}
nextSeries.set(attrKey, { value, attributes: attributes ?? {}, description })
}
async dispose(): Promise<void> {}
}
function createTelemetryService(provider: FakeProvider): TelemetryService {
return new TelemetryService([provider], {
extension_version: "test",
platform: "test-platform",
platform_version: "1.0.0",
os_type: "darwin",
os_version: "24",
is_dev: "true",
})
}
describe("TelemetryService metrics", () => {
it("captureTokenUsage emits token counters and histograms", () => {
const provider = new FakeProvider()
const service = createTelemetryService(provider)
service.captureTokenUsage("task-1", 120, 80, "model-a")
assert.deepStrictEqual(
provider.counters.map((entry) => entry.name),
[TelemetryService.METRICS.TASK.TOKENS_INPUT_TOTAL, TelemetryService.METRICS.TASK.TOKENS_OUTPUT_TOTAL],
)
assert.deepStrictEqual(
provider.histograms.map((entry) => entry.name),
[TelemetryService.METRICS.TASK.TOKENS_INPUT_PER_RESPONSE, TelemetryService.METRICS.TASK.TOKENS_OUTPUT_PER_RESPONSE],
)
provider.counters.forEach((entry) => {
assert.strictEqual(entry.attributes.ulid, "task-1")
assert.strictEqual(entry.attributes.model, "model-a")
assert.strictEqual(entry.attributes.extension_version, "test")
})
})
it("captureConversationTurnEvent emits counters with cache and cost", () => {
const provider = new FakeProvider()
const service = createTelemetryService(provider)
service.identifyAccount({ id: "user-1" } as any)
service.captureConversationTurnEvent("task-2", "openai", "gpt-4", "assistant", "plan", {
tokensIn: 150,
tokensOut: 200,
cacheWriteTokens: 40,
cacheReadTokens: 20,
totalCost: 1.23,
})
assert.deepStrictEqual(
provider.counters.map((entry) => entry.name),
[
TelemetryService.METRICS.TASK.TURNS_TOTAL,
TelemetryService.METRICS.CACHE.WRITE_TOTAL,
TelemetryService.METRICS.CACHE.READ_TOTAL,
TelemetryService.METRICS.TASK.COST_TOTAL,
],
)
const costEntry = provider.counters.find((entry) => entry.name === "cline.cost.total")
assert.ok(costEntry)
assert.strictEqual(costEntry?.attributes.ulid, "task-2")
assert.strictEqual(costEntry?.attributes.provider, "openai")
assert.strictEqual(costEntry?.attributes.model, "gpt-4")
assert.strictEqual(costEntry?.attributes.mode, "plan")
assert.strictEqual(costEntry?.attributes.currency, "USD")
assert.deepStrictEqual(
provider.histograms.map((entry) => entry.name),
[
TelemetryService.METRICS.TASK.TURNS_PER_TASK,
TelemetryService.METRICS.CACHE.WRITE_PER_EVENT,
TelemetryService.METRICS.CACHE.READ_PER_EVENT,
TelemetryService.METRICS.TASK.COST_PER_EVENT,
],
)
const turnEntry = provider.histograms.find((entry) => entry.name === TelemetryService.METRICS.TASK.TURNS_PER_TASK)
assert.ok(turnEntry)
assert.strictEqual(turnEntry?.value, 1)
assert.strictEqual(turnEntry?.attributes.ulid, "task-2")
assert.strictEqual(turnEntry?.attributes.provider, "openai")
assert.strictEqual(turnEntry?.attributes.model, "gpt-4")
assert.strictEqual(turnEntry?.attributes.source, "assistant")
assert.strictEqual(turnEntry?.attributes.mode, "plan")
})
it("captureWorkspaceInitialized emits gauge and retires previous series", () => {
const provider = new FakeProvider()
const service = createTelemetryService(provider)
service.captureWorkspaceInitialized(3, ["Git"], 500)
const initialSeries = provider.gauges.get("cline.workspace.active_roots")
assert.ok(initialSeries)
assert.strictEqual(initialSeries.size, 1)
const [initialEntry] = Array.from(initialSeries.values())
assert.strictEqual(initialEntry.value, 3)
assert.strictEqual(initialEntry.attributes.is_multi_root, true)
assert.strictEqual(initialEntry.attributes.extension_version, "test")
service.captureWorkspaceInitialized(1, ["Git"], 200)
const updatedSeries = provider.gauges.get("cline.workspace.active_roots")
assert.ok(updatedSeries)
assert.strictEqual(updatedSeries.size, 1)
const [updatedEntry] = Array.from(updatedSeries.values())
assert.strictEqual(updatedEntry.value, 1)
assert.strictEqual(updatedEntry.attributes.is_multi_root, false)
})
it("captureProviderApiError increments error counter", () => {
const provider = new FakeProvider()
const service = createTelemetryService(provider)
service.captureProviderApiError({
ulid: "task-3",
model: "claude",
errorMessage: "boom",
provider: "anthropic",
errorStatus: 500,
})
assert.strictEqual(provider.counters.length, 1)
const entry = provider.counters[0]
assert.strictEqual(entry.name, TelemetryService.METRICS.ERRORS.TOTAL)
assert.strictEqual(entry.value, 1)
assert.strictEqual(entry.attributes.ulid, "task-3")
assert.strictEqual(entry.attributes.provider, "anthropic")
assert.strictEqual(entry.attributes.model, "claude")
assert.strictEqual(entry.attributes.error_status, 500)
assert.strictEqual(provider.histograms.length, 1)
const errorHistogram = provider.histograms[0]
assert.strictEqual(errorHistogram.name, TelemetryService.METRICS.ERRORS.PER_TASK)
assert.strictEqual(errorHistogram.value, 1)
assert.strictEqual(errorHistogram.attributes.ulid, "task-3")
assert.strictEqual(errorHistogram.attributes.provider, "anthropic")
assert.strictEqual(errorHistogram.attributes.model, "claude")
assert.strictEqual(errorHistogram.attributes.error_status, 500)
})
})
@@ -87,22 +87,37 @@ export interface ITelemetryProvider {
getSettings(): TelemetrySettings
/**
* (Optional) Increment a counter metric.
* Record a counter metric (cumulative value that only increases)
* Providers that don't support metrics may implement this as a no-op.
* @param name Metric name
* @param name Metric name (e.g., "cline.tokens.input")
* @param value Amount to increment by (default 1)
* @param attributes Optional metric attributes (JSON-serializable)
* @param attributes Optional metric attributes including userId, ulid (JSON-serializable)
*/
incrementCounter?(name: string, value?: number, attributes?: TelemetryProperties): void
recordCounter(name: string, value: number, attributes?: TelemetryProperties, description?: string, required?: boolean): void
/**
* (Optional) Record a value in a histogram metric.
* Record a histogram metric (distribution of values for percentile analysis)
* Providers that don't support metrics may implement this as a no-op.
* @param name Metric name
* @param name Metric name (e.g., "cline.api.duration_seconds")
* @param value Value to record
* @param attributes Optional metric attributes (JSON-serializable)
* @param attributes Optional metric attributes including userId, ulid (JSON-serializable)
*/
recordHistogram?(name: string, value: number, attributes?: TelemetryProperties): void
recordHistogram(name: string, value: number, attributes?: TelemetryProperties, description?: string, required?: boolean): void
/**
* Record a gauge metric (point-in-time value that can go up or down)
* Providers that don't support metrics may implement this as a no-op.
* @param name Metric name (e.g., "cline.workspace.active_roots")
* @param value Current value, or null to retire the series identified by name + attributes
* @param attributes Optional metric attributes including userId, ulid (JSON-serializable). When retiring a series pass the same attribute set that was used when recording it.
*/
recordGauge(
name: string,
value: number | null,
attributes?: TelemetryProperties,
description?: string,
required?: boolean,
): void
/**
* Clean up resources when the provider is disposed
@@ -20,6 +20,8 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
// Lazy instrument caches for metrics
private counters = new Map<string, ReturnType<Meter["createCounter"]>>()
private histograms = new Map<string, ReturnType<Meter["createHistogram"]>>()
private gauges = new Map<string, ReturnType<Meter["createObservableGauge"]>>()
private gaugeValues = new Map<string, Map<string, { value: number; attributes?: TelemetryProperties }>>()
constructor() {
// Initialize telemetry settings
@@ -120,7 +122,6 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
// Store user attributes for future events
this.userAttributes = {
user_id: userInfo.id,
user_email: userInfo.email || "",
user_name: userInfo.displayName || "",
...this.flattenProperties(properties),
}
@@ -156,17 +157,24 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
}
/**
* Increment a counter metric (lazy creation).
* Only creates the counter on first use if meter is available.
* Record a counter metric (cumulative value that only increases)
* Lazy creation - only creates the counter on first use if meter is available.
*/
public incrementCounter(name: string, value: number = 1, attributes?: TelemetryProperties): void {
if (!this.meter) {
public recordCounter(
name: string,
value: number,
attributes?: TelemetryProperties,
description?: string,
required = false,
): void {
if (!this.meter || (!required && !this.isEnabled())) {
return
}
let counter = this.counters.get(name)
if (!counter) {
counter = this.meter.createCounter(name)
const options = description ? { description } : undefined
counter = this.meter.createCounter(name, options)
this.counters.set(name, counter)
console.log(`[OTEL] Created counter: ${name}`)
}
@@ -175,17 +183,24 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
}
/**
* Record a histogram metric (lazy creation).
* Only creates the histogram on first use if meter is available.
* Record a histogram metric (distribution of values for percentile analysis)
* Lazy creation - only creates the histogram on first use if meter is available.
*/
public recordHistogram(name: string, value: number, attributes?: TelemetryProperties): void {
if (!this.meter) {
public recordHistogram(
name: string,
value: number,
attributes?: TelemetryProperties,
description?: string,
required = false,
): void {
if (!this.meter || (!required && !this.isEnabled())) {
return
}
let histogram = this.histograms.get(name)
if (!histogram) {
histogram = this.meter.createHistogram(name)
const options = description ? { description } : undefined
histogram = this.meter.createHistogram(name, options)
this.histograms.set(name, histogram)
console.log(`[OTEL] Created histogram: ${name}`)
}
@@ -193,6 +208,78 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
histogram.record(value, this.flattenProperties(attributes))
}
/**
* Record a gauge metric (point-in-time value that can go up or down)
* Lazy creation - creates an observable gauge that reads from stored values
*/
public recordGauge(
name: string,
value: number | null,
attributes?: TelemetryProperties,
description?: string,
required = false,
): void {
if (!this.meter || (!required && !this.isEnabled())) {
return
}
const attrKey = attributes ? JSON.stringify(attributes) : ""
const existingSeries = this.gaugeValues.get(name)
if (value === null) {
if (existingSeries) {
existingSeries.delete(attrKey)
if (existingSeries.size === 0) {
this.gaugeValues.delete(name)
this.gauges.delete(name)
}
}
return
}
let series = existingSeries
if (!series) {
series = new Map()
this.gaugeValues.set(name, series)
}
if (!this.gauges.has(name)) {
const options = description ? { description } : undefined
const gauge = this.meter.createObservableGauge(name, options)
gauge.addCallback((observableResult) => {
const snapshot = this.snapshotGaugeSeries(name)
if (snapshot.length === 0) {
return
}
for (const data of snapshot) {
observableResult.observe(data.value, this.flattenProperties(data.attributes))
}
})
this.gauges.set(name, gauge)
console.log(`[OTEL] Created gauge: ${name}`)
}
series.set(attrKey, { value, attributes })
}
private snapshotGaugeSeries(name: string): Array<{ value: number; attributes?: TelemetryProperties }> {
const series = this.gaugeValues.get(name)
if (!series) {
return []
}
const snapshot: Array<{ value: number; attributes?: TelemetryProperties }> = []
for (const data of series.values()) {
snapshot.push({
value: data.value,
attributes: data.attributes ? { ...data.attributes } : undefined,
})
}
return snapshot
}
public async dispose(): Promise<void> {
// OpenTelemetry client provider handles shutdown
// Individual providers don't need to do anything
@@ -98,7 +98,6 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
distinctId: userInfo.id,
properties: {
uuid: userInfo.id,
email: userInfo.email,
name: userInfo.displayName,
...properties,
alias: distinctId,
@@ -129,14 +128,63 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
}
/**
* Metrics are not supported in PostHog provider. These are intentional no-ops.
* Record a counter metric by converting to equivalent PostHog event
* This maintains backward compatibility with existing dashboards
*/
public incrementCounter(name: string, value: number = 1, attributes?: TelemetryProperties): void {
// no-op
public recordCounter(
name: string,
value: number,
attributes?: TelemetryProperties,
_description?: string,
required = false,
): void {
if (!this.isEnabled() && !required) return
// Convert metric to event format for PostHog
// Most counters don't need individual events - they're aggregated in OpenTelemetry
// Only log significant counter events that have dashboard equivalents
if (name === "cline.tokens.input.total" || name === "cline.tokens.output.total") {
// These will be batched and emitted as a single "task.tokens" event
// Implementation will be added when we update captureTokenUsage
}
}
public recordHistogram(name: string, value: number, attributes?: TelemetryProperties): void {
// no-op
/**
* Record a histogram metric by converting to equivalent PostHog event
* Histograms track distributions, but PostHog events capture individual values
*/
public recordHistogram(
_name: string,
_value: number,
_attributes?: TelemetryProperties,
_description?: string,
_required = false,
): void {
// Histograms are for distribution analysis in OpenTelemetry
// PostHog gets the raw values through existing event capture methods
// No action needed here - events already capture these values
}
/**
* Record a gauge metric by converting to equivalent PostHog event
* Gauges track current state, which we can log as state change events
*/
public recordGauge(
name: string,
value: number | null,
attributes?: TelemetryProperties,
_description?: string,
required = false,
): void {
if ((!this.isEnabled() && !required) || value === null) return
// Convert gauge updates to state change events
if (name === "cline.workspace.active_roots") {
this.log("workspace.roots_changed", {
count: value,
...attributes,
})
}
}
public async dispose(): Promise<void> {
+1 -1
View File
@@ -103,7 +103,7 @@ export interface ExtensionState {
hooksEnabled?: ClineFeatureSetting
remoteConfigSettings?: Partial<RemoteConfigFields>
subagentsEnabled?: boolean
nativeToolCallSetting?: ClineFeatureSetting
nativeToolCallSetting?: boolean
}
export interface ClineMessage {
+119 -10
View File
@@ -236,6 +236,7 @@ export interface ModelInfo {
contextWindow?: number
supportsImages?: boolean
supportsPromptCache: boolean // this value is hardcoded for now
supportsReasoning?: boolean // Whether the model supports reasoning/thinking mode
inputPrice?: number // Keep for non-tiered input models
outputPrice?: number // Keep for non-tiered output models
thinkingConfig?: {
@@ -362,6 +363,16 @@ export const anthropicModels = {
cacheReadsPrice: 0.3,
tiers: CLAUDE_SONNET_1M_TIERS,
},
"claude-opus-4-5-20251101": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 5.0,
outputPrice: 25.0,
cacheWritesPrice: 6.25,
cacheReadsPrice: 0.5,
},
"claude-opus-4-1-20250805": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -465,6 +476,11 @@ export const claudeCodeModels = {
supportsImages: false,
supportsPromptCache: false,
},
"claude-opus-4-5-20251101": {
...anthropicModels["claude-opus-4-5-20251101"],
supportsImages: false,
supportsPromptCache: false,
},
"claude-opus-4-1-20250805": {
...anthropicModels["claude-opus-4-1-20250805"],
supportsImages: false,
@@ -548,6 +564,17 @@ export const bedrockModels = {
cacheReadsPrice: 0.3,
tiers: CLAUDE_SONNET_1M_TIERS,
},
"anthropic.claude-opus-4-5-20251101-v1:0": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 5.0,
outputPrice: 25.0,
cacheWritesPrice: 6.25,
cacheReadsPrice: 0.5,
},
"anthropic.claude-opus-4-20250514-v1:0": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -878,6 +905,16 @@ export const vertexModels = {
cacheWritesPrice: 1.25,
cacheReadsPrice: 0.1,
},
"claude-opus-4-5@20251101": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 5.0,
outputPrice: 25.0,
cacheWritesPrice: 6.25,
cacheReadsPrice: 0.5,
},
"claude-opus-4-1@20250805": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -2627,6 +2664,46 @@ export const askSageModels = {
"google-gemini-2.5-pro": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"google-claude-45-sonnet": {
maxTokens: 64000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"google-claude-4-opus": {
maxTokens: 32000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gpt-5": {
maxTokens: 65536,
contextWindow: 2_097_152,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gpt-5-mini": {
maxTokens: 32768,
contextWindow: 1_048_576,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gpt-5-nano": {
maxTokens: 16384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
@@ -2822,6 +2899,33 @@ export const nebiusDefaultModelId = "Qwen/Qwen2.5-32B-Instruct-fast" satisfies N
export type XAIModelId = keyof typeof xaiModels
export const xaiDefaultModelId: XAIModelId = "grok-4"
export const xaiModels = {
"grok-4-1-fast-reasoning": {
contextWindow: 2_000_000,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0.2,
cacheReadsPrice: 0.05,
outputPrice: 0.5,
description: "xAI's Grok 4.1 Reasoning Fast - multimodal model with 2M context.",
},
"grok-4-1-fast-non-reasoning": {
contextWindow: 2_000_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.2,
cacheReadsPrice: 0.05,
outputPrice: 0.5,
description: "xAI's Grok 4.1 Non-Reasoning Fast - multimodal model with 2M context.",
},
"grok-code-fast-1": {
contextWindow: 256_000,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0.2,
cacheReadsPrice: 0.02,
outputPrice: 1.5,
description: "xAI's Grok Coding model.",
},
"grok-4-fast-reasoning": {
maxTokens: 30000,
contextWindow: 2000000,
@@ -3110,7 +3214,7 @@ export const cerebrasModels = {
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Intelligent general purpose model with 2,000 tokens/s",
description: "Intelligent general purpose model with 1,000 tokens/s",
},
"gpt-oss-120b": {
maxTokens: 65536,
@@ -3148,15 +3252,6 @@ export const cerebrasModels = {
outputPrice: 0,
description: "SOTA coding performance with ~2500 tokens/s",
},
"qwen-3-235b-a22b-thinking-2507": {
maxTokens: 32000,
contextWindow: 65000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "SOTA performance with ~1500 tokens/s",
},
} as const satisfies Record<string, ModelInfo>
// Groq
@@ -3462,6 +3557,20 @@ export const sapAiCoreModels = {
supportsPromptCache: true,
description: sapAiCoreModelDescription,
},
sonar: {
maxTokens: 128_000,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
description: sapAiCoreModelDescription,
},
"sonar-pro": {
maxTokens: 128_000,
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: false,
description: sapAiCoreModelDescription,
},
} as const satisfies Record<string, ModelInfo>
// Moonshot AI Studio
+21 -6
View File
@@ -125,12 +125,27 @@ export function convertClineStorageToAnthropicMessage(
* Clean a content block by removing Cline-specific fields and returning only Anthropic-compatible fields
*/
export function cleanContentBlock(block: ClineContent): Anthropic.ContentBlock {
// Remove Cline-specific fields: reasoning_details, call_id, summary
if ("reasoning_details" in block || "call_id" in block || "summary" in block) {
// biome-ignore lint/correctness/noUnusedVariables: intentional destructuring to remove properties
const { reasoning_details, call_id, summary, ...cleanBlock } = block as any
return cleanBlock as Anthropic.ContentBlock
// Fast path: if no Cline-specific fields exist, return as-is
const hasClineFields =
"reasoning_details" in block ||
"call_id" in block ||
"summary" in block ||
(block.type === "tool_use" && "signature" in block)
if (!hasClineFields) {
return block as Anthropic.ContentBlock
}
return block as Anthropic.ContentBlock
// Remove Cline-specific fields (signature only for tool_use blocks)
// biome-ignore lint/correctness/noUnusedVariables: intentional destructuring to remove properties
const { reasoning_details, call_id, summary, ...rest } = block as any
// Remove signature only from tool_use blocks (used by Gemini)
if (rest.type === "tool_use" && "signature" in rest) {
// biome-ignore lint/correctness/noUnusedVariables: intentional destructuring to remove properties
const { signature, ...cleanBlock } = rest
return cleanBlock satisfies Anthropic.ContentBlock
}
return rest satisfies Anthropic.ContentBlock
}
@@ -48,6 +48,7 @@ export function fromProtobufModelInfo(protoInfo: OpenRouterModelInfo): ModelInfo
contextWindow: protoInfo.contextWindow,
supportsImages: protoInfo.supportsImages,
supportsPromptCache: protoInfo.supportsPromptCache,
supportsReasoning: protoInfo.supportsReasoning,
inputPrice: protoInfo.inputPrice,
outputPrice: protoInfo.outputPrice,
cacheWritesPrice: protoInfo.cacheWritesPrice,
@@ -68,6 +69,7 @@ export function toProtobufModelInfo(modelInfo: ModelInfo): OpenRouterModelInfo {
contextWindow: modelInfo.contextWindow,
supportsImages: modelInfo.supportsImages,
supportsPromptCache: modelInfo.supportsPromptCache,
supportsReasoning: modelInfo.supportsReasoning,
inputPrice: modelInfo.inputPrice,
outputPrice: modelInfo.outputPrice,
cacheWritesPrice: modelInfo.cacheWritesPrice,
@@ -314,6 +314,7 @@ describe("Remote Config Schema", () => {
version: "v1",
telemetryEnabled: true,
mcpMarketplaceEnabled: false,
blockPersonalRemoteMCPServers: true,
allowedMCPServers: [{ id: "https://github.com/mcp/filesystem" }, { id: "https://github.com/mcp/github" }],
yoloModeAllowed: true,
openTelemetryEnabled: true,
+5
View File
@@ -0,0 +1,5 @@
/**
* URI scheme for opening remote rules/workflows in the editor.
* Used to construct URIs like: remote://rule/{name} or remote://workflow/{name}
*/
export const REMOTE_URI_SCHEME = "remote://"
-1
View File
@@ -1 +0,0 @@
export * from "./schema"
+7
View File
@@ -142,9 +142,16 @@ export const RemoteConfigSchema = z.object({
telemetryEnabled: z.boolean().optional(),
// MCP settings
// If this is false, the MCP marketplace is disabled in the extension
mcpMarketplaceEnabled: z.boolean().optional(),
// If this is configured, the users only have access to these allowlisted MCP servers in the marketplace.
allowedMCPServers: z.array(AllowedMCPServerSchema).optional(),
// A list of pre-configured remote MCP servers.
remoteMCPServers: z.array(RemoteMCPServerSchema).optional(),
// If this is true, users cannot use or configure MCP servers that are not remotely configured.
blockPersonalRemoteMCPServers: z.boolean().optional(),
// If the user is allowed to enable YOLO mode. Note this is different from the extension setting
// yoloModeEnabled, because we do not want to force YOLO enabled for the user.
@@ -6,8 +6,6 @@ export enum FeatureFlag {
FOCUS_CHAIN_CHECKLIST = "focus_chain_checklist",
DO_NOTHING = "do_nothing",
HOOKS = "hooks",
// Feature flag for enabling native tool calls for next-gen models
NATIVE_TOOL_CALLS_NEXT_GEN_MODELS = "native_tool_calls_next_gen",
// Feature flag for showing the new onboarding flow or old welcome view.
ONBOARDING_MODELS = "onboarding_models",
OPENAI_NATIVE_RESPONSE_API = "openai_native_response_api",
@@ -16,7 +14,6 @@ export enum FeatureFlag {
export const FeatureFlagDefaultValue: Partial<Record<FeatureFlag, FeatureFlagPayload>> = {
[FeatureFlag.DO_NOTHING]: false,
[FeatureFlag.HOOKS]: false,
[FeatureFlag.NATIVE_TOOL_CALLS_NEXT_GEN_MODELS]: process.env.IS_DEV === "true",
[FeatureFlag.ONBOARDING_MODELS]: process.env.E2E_TEST === "true" ? { models: {} } : undefined,
[FeatureFlag.OPENAI_NATIVE_RESPONSE_API]: process.env.IS_DEV === "true",
}
@@ -32,8 +32,8 @@ class StandaloneTerminalProcess extends EventEmitter {
const shellArgs = this.getShellArgs(shell, command)
try {
// Spawn the process
this.childProcess = spawn(shell, shellArgs, {
// Create shell options
const shellOptions = {
cwd: cwd,
stdio: ["ignore", "pipe", "pipe"], // Disable STDIN to prevent interactivity
env: {
@@ -45,7 +45,18 @@ class StandaloneTerminalProcess extends EventEmitter {
SYSTEMD_PAGER: "", // Disable systemd pager
MANPAGER: "cat", // Disable man pager
},
})
}
// Enable the shell option for "cmd.exe" to prevent double quotes from being over escaped
if (shell.toLowerCase().includes("cmd")) {
shellOptions.shell = true
// Spawn the process with special handling for "cmd.exe"
this.childProcess = spawn("cmd.exe", shellArgs, shellOptions)
} else {
// Spawn the process
this.childProcess = spawn(shell, shellArgs, shellOptions)
}
// Track process state
let didEmitEmptyLine = false
@@ -200,8 +211,7 @@ class StandaloneTerminalProcess extends EventEmitter {
if (shell.toLowerCase().includes("powershell") || shell.toLowerCase().includes("pwsh")) {
return ["-Command", command]
} else {
// Use /s /c with quoted command for proper quote handling in cmd.exe
return ["/s", "/c", `"${command}"`]
return ["/c", command]
}
} else {
// Use -l for login shell, -c for command

Some files were not shown because too many files have changed in this diff Show More