mirror of
https://github.com/cline/cline.git
synced 2026-09-07 04:44:58 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e261f2f47 | |||
| 5f050c5353 | |||
| 2516424020 | |||
| 0d43d014eb | |||
| df7d5062bc | |||
| 98aec6c9d3 | |||
| 5a65b12a43 | |||
| 741bea5af8 | |||
| ad1e35a425 | |||
| 2e2239b138 | |||
| 03aac0cd80 | |||
| 22cc73f614 | |||
| 06b05ddfe9 | |||
| 2670a4a171 | |||
| 1699c9a63a | |||
| 808dd42ae9 | |||
| 71af56f493 | |||
| 1167b4f3a6 | |||
| e8d6370b0c | |||
| e243376a39 | |||
| 4f1be9d512 | |||
| 94d36ce719 | |||
| 5df7498f03 | |||
| c2b87252ac | |||
| be353bb3da | |||
| 0a7791de1f | |||
| 7cca102e14 | |||
| 4f591de6a9 | |||
| 60436b3ddf | |||
| df5954052d | |||
| a66a2784c3 | |||
| 47031cea25 | |||
| e29740479e |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
added social icons and appropriate links to the new version modal
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Lock the LiteLLM Api Key input when it's remotely configured
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
updated welcome card content and added ability to close each card
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add endpoint configuration file support for on-premise deployments
|
||||
|
||||
Enterprise customers can now configure custom API endpoints by creating a `~/.cline/endpoints.json` file with custom URLs for `appBaseUrl`, `apiBaseUrl`, and `mcpBaseUrl`. When this file is present, Cline runs in on-premise mode with the custom endpoints.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: prevent infinite retry loops when replace_in_file fails repeatedly
|
||||
|
||||
Add safeguards to prevent the LLM from getting stuck in infinite retry loops when `replace_in_file` operations fail repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix(extract-text): strip notebook outputs to reduce context size
|
||||
|
||||
Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing the amount of context sent to the LLM while preserving the essential code and markdown content.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: throttle diff view updates during streaming
|
||||
|
||||
Add throttling to diff view updates during content streaming to reduce UI flickering and improve performance. Updates are now batched at reasonable intervals instead of firing on every token received.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove deprecated zai-glm-4.6 model from Cerebras provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Make Sonnet 4.5 the default Amazon Bedrock model id
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(hooks): Run hooks from cwd of the workspace repo root.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Disable PostHog and build-time OpenTelemetry telemetry in self-hosted/on-premise mode. Enterprise customers running self-hosted deployments will no longer send any telemetry to Cline's collectors. Runtime environment OTEL and remote config OTEL remain available for enterprises to configure their own telemetry collection.
|
||||
@@ -16,13 +16,6 @@
|
||||
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
|
||||
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
|
||||
|
||||
# ============================================================================
|
||||
# TELEMETRY PROVIDER CONTROL
|
||||
# ============================================================================
|
||||
# Control which telemetry providers are active
|
||||
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
|
||||
# Set to false to disable Telemetry completely
|
||||
|
||||
# ============================================================================
|
||||
# OPENTELEMETRY (Optional - for advanced telemetry)
|
||||
# ============================================================================
|
||||
|
||||
@@ -97,7 +97,6 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
|
||||
@@ -138,7 +138,6 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
|
||||
@@ -38,6 +38,7 @@ coverage-unit
|
||||
src/generated/
|
||||
src/shared/proto/
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
*.tsbuildinfo
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
|
||||
+29
-2
@@ -1,14 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## [3.55.0]
|
||||
|
||||
- Add new model: Arcee Trinity Large Preview
|
||||
- Add new model: Moonshot Kimi K2.5
|
||||
- Add MCP prompts support - prompts from connected MCP servers now appear in slash command autocomplete as `/mcp:<server>:<prompt>`
|
||||
|
||||
## [3.54.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Native tool calls support for Ollama provider
|
||||
- Sonnet 4.5 is now the default Amazon Bedrock model id
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
|
||||
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
|
||||
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
|
||||
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed Mistral's Devstral-2512 free from the free models list
|
||||
- Removed deprecated zai-glm-4.6 model from Cerebras provider
|
||||
|
||||
## [3.53.1]
|
||||
|
||||
### Fixed
|
||||
- Bug in responses API
|
||||
|
||||
- Bug in responses API
|
||||
|
||||
## [3.53.0]
|
||||
|
||||
### Fixed
|
||||
- Removed grok model from free tier
|
||||
|
||||
- Removed grok model from free tier
|
||||
|
||||
## [3.52.0]
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.317 4.15557C18.7873 3.45369 17.147 2.93658 15.4319 2.6404C15.4007 2.63469 15.3695 2.64897 15.3534 2.67754C15.1424 3.05276 14.9087 3.54225 14.7451 3.927C12.9004 3.65083 11.0652 3.65083 9.25832 3.927C9.09465 3.5337 8.85248 3.05276 8.64057 2.67754C8.62449 2.64992 8.59328 2.63564 8.56205 2.6404C6.84791 2.93563 5.20756 3.45275 3.67693 4.15557C3.66368 4.16129 3.65233 4.17082 3.64479 4.18319C0.533392 8.83155 -0.31895 13.3657 0.0991801 17.8436C0.101072 17.8655 0.11337 17.8864 0.130398 17.8997C2.18321 19.4073 4.17171 20.3225 6.12328 20.9291C6.15451 20.9386 6.18761 20.9272 6.20748 20.9015C6.66913 20.2711 7.08064 19.6063 7.43348 18.9073C7.4543 18.8664 7.43442 18.8178 7.39186 18.8016C6.73913 18.554 6.1176 18.2521 5.51973 17.9093C5.47244 17.8816 5.46865 17.814 5.51216 17.7816C5.63797 17.6873 5.76382 17.5893 5.88396 17.4902C5.90569 17.4721 5.93598 17.4683 5.96153 17.4797C9.88928 19.273 14.1415 19.273 18.023 17.4797C18.0485 17.4674 18.0788 17.4712 18.1015 17.4893C18.2216 17.5883 18.3475 17.6873 18.4742 17.7816C18.5177 17.814 18.5149 17.8816 18.4676 17.9093C17.8697 18.2588 17.2482 18.554 16.5945 18.8006C16.552 18.8168 16.533 18.8664 16.5538 18.9073C16.9143 19.6054 17.3258 20.2701 17.7789 20.9005C17.7978 20.9272 17.8319 20.9386 17.8631 20.9291C19.8241 20.3225 21.8126 19.4073 23.8654 17.8997C23.8834 17.8864 23.8948 17.8664 23.8967 17.8445C24.3971 12.6676 23.0585 8.17064 20.3482 4.18414C20.3416 4.17082 20.3303 4.16129 20.317 4.15557ZM8.02002 15.117C6.8375 15.117 5.86313 14.0313 5.86313 12.6981C5.86313 11.3648 6.8186 10.2791 8.02002 10.2791C9.23087 10.2791 10.1958 11.3743 10.1769 12.6981C10.1769 14.0313 9.22141 15.117 8.02002 15.117ZM15.9947 15.117C14.8123 15.117 13.8379 14.0313 13.8379 12.6981C13.8379 11.3648 14.7933 10.2791 15.9947 10.2791C17.2056 10.2791 18.1705 11.3743 18.1516 12.6981C18.1516 14.0313 17.2056 15.117 15.9947 15.117Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12C2 16.418 4.865 20.166 8.84 21.49C9.34 21.58 9.52 21.27 9.52 21C9.52 20.77 9.51 20.14 9.51 19.31C6.73 19.91 6.14 17.97 6.14 17.97C5.68 16.81 5.03 16.5 5.03 16.5C4.12 15.88 5.1 15.9 5.1 15.9C6.1 15.97 6.63 16.93 6.63 16.93C7.5 18.45 8.97 18 9.54 17.76C9.63 17.11 9.89 16.67 10.17 16.42C7.95 16.17 5.62 15.31 5.62 11.5C5.62 10.39 6 9.5 6.65 8.79C6.55 8.54 6.2 7.5 6.75 6.15C6.75 6.15 7.59 5.88 9.5 7.17C10.29 6.95 11.15 6.84 12 6.84C12.85 6.84 13.71 6.95 14.5 7.17C16.41 5.88 17.25 6.15 17.25 6.15C17.8 7.5 17.45 8.54 17.35 8.79C18 9.5 18.38 10.39 18.38 11.5C18.38 15.32 16.04 16.16 13.81 16.41C14.17 16.72 14.5 17.33 14.5 18.26C14.5 19.6 14.49 20.68 14.49 21C14.49 21.27 14.67 21.59 15.17 21.49C19.14 20.16 22 16.42 22 12C22 6.477 17.523 2 12 2Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 902 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2001_1428)">
|
||||
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5562 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2937 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516Z" fill="#FAFAFA"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2001_1428">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 989 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.0512 4.07466C15.3113 5.17727 16.301 5.99866 17.4829 5.99866C18.8627 5.99866 19.9813 4.87965 19.9813 3.49933C19.9813 2.11902 18.8627 1 17.4829 1C16.2764 1 15.2703 1.85537 15.036 2.99314C13.0155 3.20991 11.4378 4.92417 11.4378 7.00167C11.4378 7.00636 11.4378 7.00988 11.4378 7.01456C9.24041 7.10713 7.23397 7.73284 5.641 8.72062C5.04949 8.26247 4.30688 7.98945 3.50102 7.98945C1.5672 7.98945 0 9.55725 0 11.4918C0 12.8955 0.824597 14.1048 2.01581 14.6637C2.13177 18.7297 6.56047 22 12.0082 22C17.4559 22 21.8905 18.7261 22.0006 14.6567C23.1824 14.0942 24 12.8885 24 11.493C24 9.55842 22.4328 7.99063 20.499 7.99063C19.6966 7.99063 18.9575 8.2613 18.3672 8.71594C16.7602 7.72113 14.7315 7.09541 12.5119 7.01222C12.5119 7.0087 12.5119 7.00636 12.5119 7.00285C12.5119 5.51473 13.6176 4.27971 15.0512 4.077V4.07466ZM5.50044 13.7146C5.559 12.4444 6.40234 11.4695 7.38272 11.4695C8.3631 11.4695 9.11274 12.4995 9.05417 13.7697C8.99561 15.0398 8.26354 15.5015 7.28199 15.5015C6.30044 15.5015 5.44187 14.9848 5.50044 13.7146ZM16.6348 11.4695C17.6164 11.4695 18.4597 12.4444 18.5171 13.7146C18.5757 14.9848 17.716 15.5015 16.7356 15.5015C15.7552 15.5015 15.022 15.041 14.9634 13.7697C14.9048 12.4995 15.6533 11.4695 16.6348 11.4695ZM15.4682 16.6533C15.6521 16.6721 15.7693 16.8631 15.6978 17.0341C15.0946 18.4766 13.6703 19.4901 12.0082 19.4901C10.3461 19.4901 8.92299 18.4766 8.31859 17.0341C8.24714 16.8631 8.36427 16.6721 8.54817 16.6533C9.62577 16.5444 10.7912 16.4846 12.0082 16.4846C13.2252 16.4846 14.3895 16.5444 15.4682 16.6533Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3263 1.90393H21.6998L14.3297 10.3274L23 21.7899H16.2112L10.894 14.838L4.80995 21.7899H1.43443L9.31743 12.78L1 1.90393H7.96111L12.7674 8.25826L18.3263 1.90393ZM17.1423 19.7707H19.0116L6.94539 3.81706H4.93946L17.1423 19.7707Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
@@ -104,14 +104,18 @@ func (s *EnvService) Shutdown(ctx context.Context, req *cline.EmptyRequest) (*cl
|
||||
return &cline.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *EnvService) isTelemetryEnabled() bool {
|
||||
// In CLI mode, check the CLINE_TELEMETRY_DISABLED environment variable
|
||||
return os.Getenv("CLINE_TELEMETRY_DISABLED") != "true"
|
||||
}
|
||||
|
||||
// GetTelemetrySettings returns the telemetry settings for CLI mode
|
||||
func (s *EnvService) GetTelemetrySettings(ctx context.Context, req *cline.EmptyRequest) (*host.GetTelemetrySettingsResponse, error) {
|
||||
if s.verbose {
|
||||
log.Printf("GetTelemetrySettings called")
|
||||
}
|
||||
|
||||
// In CLI mode, check the POSTHOG_TELEMETRY_ENABLED environment variable
|
||||
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
|
||||
telemetryEnabled := s.isTelemetryEnabled()
|
||||
|
||||
var setting host.Setting
|
||||
if telemetryEnabled {
|
||||
@@ -133,8 +137,7 @@ func (s *EnvService) SubscribeToTelemetrySettings(req *cline.EmptyRequest, strea
|
||||
log.Printf("SubscribeToTelemetrySettings called")
|
||||
}
|
||||
|
||||
// Send initial telemetry state
|
||||
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
|
||||
telemetryEnabled := s.isTelemetryEnabled()
|
||||
|
||||
var setting host.Setting
|
||||
if telemetryEnabled {
|
||||
|
||||
+15
-1
@@ -117,7 +117,13 @@
|
||||
"features/auto-compact",
|
||||
"features/background-edit",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
{
|
||||
"group": "Cline Rules",
|
||||
"pages": [
|
||||
"features/cline-rules/overview",
|
||||
"features/cline-rules/conditional-rules"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Commands & Shortcuts",
|
||||
"pages": [
|
||||
@@ -424,6 +430,14 @@
|
||||
{
|
||||
"source": "/enterprise-solutions/team-management/roles-and-permissions",
|
||||
"destination": "/enterprise-solutions/team-management/managing-members"
|
||||
},
|
||||
{
|
||||
"source": "/features/cline-rules",
|
||||
"destination": "/features/cline-rules/overview"
|
||||
},
|
||||
{
|
||||
"source": "/features/conditional-rules",
|
||||
"destination": "/features/cline-rules/conditional-rules"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
Cline Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to include context and preferences for your projects or globally for every conversation.
|
||||
|
||||
## Creating a Rule
|
||||
|
||||
You can create a rule by clicking the `+` button in the Rules tab. This will open a new file in your IDE which you can use to write your rule.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
|
||||
</Frame>
|
||||
|
||||
Once you save the file:
|
||||
|
||||
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
|
||||
- Or in the Global Rules directory (if it's a Global Rule):
|
||||
|
||||
### Global Rules Directory Location
|
||||
|
||||
The location of your Global Rules directory depends on your operating system:
|
||||
|
||||
| Operating System | Default Location | Notes |
|
||||
|------------------|------------------|-------|
|
||||
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
|
||||
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
|
||||
|
||||
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
|
||||
|
||||
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
|
||||
|
||||
```markdown Example Cline Rule Structure [expandable]
|
||||
# Project Guidelines
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
- Update relevant documentation in /docs when modifying features
|
||||
- Keep README.md in sync with new capabilities
|
||||
- Maintain changelog entries in CHANGELOG.md
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
Create ADRs in /docs/adr for:
|
||||
|
||||
- Major dependency changes
|
||||
- Architectural pattern changes
|
||||
- New integration patterns
|
||||
- Database schema changes
|
||||
Follow template in /docs/adr/template.md
|
||||
|
||||
## Code Style & Patterns
|
||||
|
||||
- Generate API clients using OpenAPI Generator
|
||||
- Use TypeScript axios template
|
||||
- Place generated code in /src/generated
|
||||
- Prefer composition over inheritance
|
||||
- Use repository pattern for data access
|
||||
- Follow error handling pattern in /src/utils/errors.ts
|
||||
|
||||
## Testing Standards
|
||||
|
||||
- Unit tests required for business logic
|
||||
- Integration tests for API endpoints
|
||||
- E2E tests for critical user flows
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
|
||||
2. **Team Consistency**: Ensures consistent behavior across all team members
|
||||
3. **Project-Specific**: Rules and standards tailored to each project's needs
|
||||
4. **Institutional Knowledge**: Maintains project standards and practices in code
|
||||
|
||||
Place the `.clinerules` file in your project's root directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules
|
||||
├── src/
|
||||
├── docs/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
|
||||
|
||||
### AGENTS.md Standard Support
|
||||
|
||||
Cline also supports the [AGENTS.md](https://agents.md/) standard as a fallback
|
||||
(in addition to Cline Rules) by automatically detecting `AGENTS.md` files in
|
||||
your workspace root. This allows you to use the same rules file across different AI
|
||||
coding tools.
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── AGENTS.md
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Tips for Writing Effective Cline Rules
|
||||
|
||||
- Be Clear and Concise: Use simple language and avoid ambiguity.
|
||||
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
|
||||
- Test and Iterate: Experiment to find what works best for your workflow.
|
||||
|
||||
### .clinerules/ Folder System
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Folder containing active rules
|
||||
│ ├── 01-coding.md # Core coding standards
|
||||
│ ├── 02-documentation.md # Documentation requirements
|
||||
│ └── current-sprint.md # Rules specific to current work
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
|
||||
|
||||
#### Using a Rules Bank
|
||||
|
||||
For projects with multiple contexts or teams, maintain a rules bank directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules - automatically applied
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Repository of available but inactive rules
|
||||
│ ├── clients/ # Client-specific rule sets
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ ├── frameworks/ # Framework-specific rules
|
||||
│ │ ├── react.md
|
||||
│ │ └── vue.md
|
||||
│ └── project-types/ # Project type standards
|
||||
│ ├── api-service.md
|
||||
│ └── frontend-app.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### Benefits of the Folder Approach
|
||||
|
||||
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
|
||||
2. **Easier Maintenance**: Update individual rule files without affecting others
|
||||
3. **Team Flexibility**: Different team members can activate rules specific to their current task
|
||||
4. **Reduced Noise**: Keep the active ruleset focused and relevant
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
Switch between client projects:
|
||||
|
||||
```bash
|
||||
# Switch to Client B project
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
Adapt to different tech stacks:
|
||||
|
||||
```bash
|
||||
# Frontend React project
|
||||
cp clinerules-bank/frameworks/react.md .clinerules/
|
||||
```
|
||||
|
||||
#### Implementation Tips
|
||||
|
||||
- Keep individual rule files focused on specific concerns
|
||||
- Use descriptive filenames that clearly indicate the rule's purpose
|
||||
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
|
||||
- Create team scripts to quickly activate common rule combinations
|
||||
|
||||
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
|
||||
|
||||
### Managing Rules with the Toggleable Popover
|
||||
|
||||
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
|
||||
|
||||
Located conveniently under the chat input field, this popover allows you to:
|
||||
|
||||
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
|
||||
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
|
||||
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
|
||||
|
||||
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
title: "Conditional Rules"
|
||||
sidebarTitle: "Conditional Rules"
|
||||
description: "Activate rules automatically based on which files you're working with"
|
||||
---
|
||||
|
||||
Conditional rules let you scope rules to specific parts of your codebase. Rules activate only when you're working with matching files, keeping your context focused and relevant.
|
||||
|
||||
For an introduction to Cline Rules, see the [Overview](/features/cline-rules/overview).
|
||||
|
||||
- **Without conditionals**: every rule loads for every request.
|
||||
- **With conditionals**, rules activate only when your current files match their defined scope.
|
||||
|
||||
For example, React component rules should appear when you're working with React components, not when you're editing Python or documentation.
|
||||
|
||||
## How It Works
|
||||
|
||||
Conditional rules use YAML frontmatter at the top of your rule files. When Cline processes a request, it gathers context from your current work (open files, visible tabs, mentioned paths, edited files), evaluates each rule's conditions, and activates matching rules.
|
||||
|
||||
<Note>
|
||||
When a conditional rule activates, you'll see a notification: **"Conditional rules applied: workspace:frontend-rules.md"**
|
||||
</Note>
|
||||
|
||||
## Writing Conditional Rules
|
||||
|
||||
Add YAML frontmatter to the top of any rule file in your `.clinerules/` directory:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# React Component Guidelines
|
||||
|
||||
When creating or modifying React components:
|
||||
- Use functional components with React hooks
|
||||
- Extract reusable logic into custom React hooks
|
||||
- Keep components focused on a single responsibility
|
||||
```
|
||||
|
||||
The `---` markers delimit the frontmatter. Everything after the closing `---` is your rule content.
|
||||
|
||||
### The `paths` Conditional
|
||||
|
||||
Currently, `paths` is the supported conditional. It takes an array of glob patterns:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/**" # All files under src/
|
||||
- "*.config.js" # Config files in root
|
||||
- "packages/*/src/" # Monorepo package sources
|
||||
---
|
||||
```
|
||||
|
||||
**Glob pattern syntax:**
|
||||
- `*` matches any characters except `/`
|
||||
- `**` matches any characters including `/` (recursive)
|
||||
- `?` matches a single character
|
||||
- `[abc]` matches any character in the brackets
|
||||
- `{a,b}` matches either pattern
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Pattern | Matches |
|
||||
|---------|---------|
|
||||
| `src/**/*.ts` | All TypeScript files under `src/` |
|
||||
| `*.md` | Markdown files in root only |
|
||||
| `**/*.test.ts` | Test files anywhere in the project |
|
||||
| `packages/{web,api}/**` | Files in web or api packages |
|
||||
| `src/components/*.tsx` | TSX files directly in components (not nested) |
|
||||
|
||||
### Behavior Details
|
||||
|
||||
**Multiple patterns**: A rule activates if any pattern matches any file in your context.
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "mobile/**"
|
||||
---
|
||||
# Activates when working in frontend OR mobile
|
||||
```
|
||||
|
||||
**No frontmatter**: Rules without frontmatter are always active.
|
||||
|
||||
**Empty paths array**: `paths: []` means the rule never activates. Use this to temporarily disable a rule.
|
||||
|
||||
**Invalid YAML**: If frontmatter can't be parsed, Cline fails open: the rule activates with raw content visible to help debugging.
|
||||
|
||||
## What Counts as "Current Context"
|
||||
|
||||
Cline evaluates rules based on:
|
||||
|
||||
1. **Your message**: File paths mentioned in your prompt (e.g., "update `src/App.tsx`")
|
||||
2. **Open tabs**: Files currently open in your editor
|
||||
3. **Visible files**: Files visible in your active editor panes
|
||||
4. **Edited files**: Files Cline has created, modified, or deleted during the task
|
||||
5. **Pending operations**: Files Cline is about to edit
|
||||
|
||||
Conditional rules can activate on your first message, when relevant files are open, or mid-task when Cline starts working with matching files.
|
||||
|
||||
<Tip>
|
||||
Be explicit about file paths in your prompts. "Update `src/services/user.ts`" reliably triggers path-based rules; "update the user service" may not.
|
||||
</Tip>
|
||||
|
||||
## Practical Examples
|
||||
|
||||
Copy these patterns and adapt them to your project structure.
|
||||
|
||||
### Frontend vs Backend Rules
|
||||
|
||||
Keep frontend and backend rules separate to avoid noise. Frontend rules only load when working with UI code, backend rules only load when working with API or service code.
|
||||
|
||||
```yaml
|
||||
# .clinerules/frontend.md
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/pages/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# Frontend Guidelines
|
||||
|
||||
- Use Tailwind CSS for styling
|
||||
- Prefer server components where possible
|
||||
- Keep client components small and focused
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .clinerules/backend.md
|
||||
---
|
||||
paths:
|
||||
- "src/api/**"
|
||||
- "src/services/**"
|
||||
- "src/db/**"
|
||||
---
|
||||
|
||||
# Backend Guidelines
|
||||
|
||||
- Use dependency injection for services
|
||||
- All database queries go through repositories
|
||||
- Return typed errors, not thrown exceptions
|
||||
```
|
||||
|
||||
### Test File Rules
|
||||
|
||||
Enforce testing standards automatically. This rule activates only when you're writing or modifying tests, so testing guidance appears exactly when you need it.
|
||||
|
||||
```yaml
|
||||
# .clinerules/testing.md
|
||||
---
|
||||
paths:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
- "**/__tests__/**"
|
||||
---
|
||||
|
||||
# Testing Standards
|
||||
|
||||
- Use descriptive test names: "should [expected behavior] when [condition]"
|
||||
- One assertion per test when possible
|
||||
- Mock external dependencies, not internal modules
|
||||
- Use factories for test data, not fixtures
|
||||
```
|
||||
|
||||
### Documentation Rules
|
||||
|
||||
Apply documentation standards only when editing docs. Prevents style rules from cluttering your context when you're writing code.
|
||||
|
||||
```yaml
|
||||
# .clinerules/docs.md
|
||||
---
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "**/*.md"
|
||||
- "**/*.mdx"
|
||||
---
|
||||
|
||||
# Documentation Guidelines
|
||||
|
||||
- Use sentence case for headings
|
||||
- Include code examples for all features
|
||||
- Keep paragraphs short (3-4 sentences max)
|
||||
- Link to related documentation
|
||||
```
|
||||
|
||||
## Combining with Rule Toggles
|
||||
|
||||
Conditional rules work alongside the rule toggle UI. Toggle off a conditional rule to disable it entirely (it won't activate even if paths match). Toggle on to let it activate when conditions are met.
|
||||
|
||||
This provides two levels of control: manual toggles and automatic condition-based activation.
|
||||
|
||||
## Tips for Effective Conditional Rules
|
||||
|
||||
### Start Broad, Then Narrow
|
||||
|
||||
Begin with broader patterns and refine as you learn what works:
|
||||
|
||||
```yaml
|
||||
# Start here
|
||||
paths:
|
||||
- "src/**"
|
||||
|
||||
# Then narrow down
|
||||
paths:
|
||||
- "src/features/auth/**"
|
||||
```
|
||||
|
||||
### Use Descriptive Filenames
|
||||
|
||||
Name your rule files to indicate their scope:
|
||||
|
||||
```
|
||||
.clinerules/
|
||||
├── api-endpoints.md # Rules for API code
|
||||
├── database-models.md # Rules for DB layer
|
||||
├── react-components.md # Rules for React
|
||||
└── universal.md # No frontmatter = always active
|
||||
```
|
||||
|
||||
### Keep Universal Rules Separate
|
||||
|
||||
Put always-on rules (coding standards, project conventions) in files without frontmatter. Reserve conditional rules for context-specific guidance.
|
||||
|
||||
### Test Your Patterns
|
||||
|
||||
Not sure if a pattern matches? Create a simple test rule:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "your/pattern/here/**"
|
||||
---
|
||||
|
||||
TEST: This rule should activate for your/pattern/here files.
|
||||
```
|
||||
|
||||
Then work with a file in that path and check if you see the activation notification.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Rule not activating:**
|
||||
- Check that file paths in your context match the glob pattern
|
||||
- Verify the rule is toggled on in the rules panel
|
||||
- Ensure YAML frontmatter has proper `---` delimiters
|
||||
|
||||
**Rule activating unexpectedly:**
|
||||
- Review glob patterns: `**` is recursive and may match more than intended
|
||||
- Check for open files that match the pattern
|
||||
- File paths mentioned in your message also count as context
|
||||
|
||||
**Frontmatter showing in output:**
|
||||
- YAML couldn't be parsed
|
||||
- Check for syntax errors (unquoted special characters, improper indentation)
|
||||
|
||||
## Related
|
||||
|
||||
- [Cline Rules Overview](/features/cline-rules/overview) - Complete rules system guide
|
||||
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
|
||||
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
|
||||
- [@ Mentions](/features/at-mentions/overview) - Add files to context explicitly
|
||||
- [Understanding Context Management](/prompting/understanding-context-management) - How Cline manages context window
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: "Cline Rules"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Add persistent instructions and context to guide Cline's behavior"
|
||||
---
|
||||
|
||||
Cline Rules provide system-level guidance for your projects. Rules persist across conversations, ensuring consistent behavior without repeating instructions in every chat.
|
||||
|
||||
## How It Works
|
||||
|
||||
Rules are loaded when Cline starts a task. Here's what happens:
|
||||
|
||||
**Loading order**: Cline checks for rules in this sequence:
|
||||
1. `.clinerules/` folder (all `.md` files inside)
|
||||
2. Single `.clinerules` file
|
||||
3. `AGENTS.md` file
|
||||
|
||||
**Scope precedence**: Workspace rules override global rules when both define the same guidance.
|
||||
|
||||
**Multiple files**: When using a `.clinerules/` folder, all Markdown files are combined into one ruleset. Numeric prefixes (like `01-`, `02-`) control the order.
|
||||
|
||||
**Conditional activation**: Rules with YAML frontmatter activate only when you're working with matching files. See [Conditional Rules](/features/cline-rules/conditional-rules) for details.
|
||||
|
||||
## Supported Rule Files
|
||||
|
||||
Cline reads rules from multiple file formats in your workspace root, letting you share rules across different AI coding tools:
|
||||
|
||||
| File/Folder | Source | Notes |
|
||||
|-------------|--------|-------|
|
||||
| `.clinerules/` | Cline | Folder with `.md` files (recommended) |
|
||||
| `.cursor/rules/` | Cursor | Folder with `.mdc` files |
|
||||
| `.windsurf/rules` | Windsurf | Folder with multiple `md` files |
|
||||
| `AGENTS.md` | Universal | Follows [agents.md](https://agents.md/) standard, searched recursively |
|
||||
|
||||
Cline prioritizes `.clinerules` when present. Other formats load only if no `.clinerules` exists (except `AGENTS.md`, which always searches subdirectories). All rules appear in the Rules popover where you can toggle them.
|
||||
|
||||
## Creating Rules
|
||||
|
||||
Click the `+` button in the Rules tab to create a new rule. This opens a file in your editor where you write your guidance.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
|
||||
</Frame>
|
||||
|
||||
When you save the file, it's stored in:
|
||||
- **Workspace rules**: `.clinerules/` in your project root
|
||||
- **Global rules**: Platform-specific location (see table below)
|
||||
|
||||
You can also use the [`/newrule` slash command](/features/slash-commands/new-rule) to have Cline generate a rule based on your description.
|
||||
|
||||
### Global Rules Location
|
||||
|
||||
| Operating System | Default Location |
|
||||
|------------------|------------------|
|
||||
| **Windows** | `Documents\Cline\Rules` |
|
||||
| **macOS** | `~/Documents/Cline/Rules` |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` or `~/Cline/Rules` |
|
||||
|
||||
<Note>
|
||||
Linux/WSL users: Check both locations if you don't find global rules in `~/Documents/Cline/Rules`.
|
||||
</Note>
|
||||
|
||||
## Managing Rules
|
||||
|
||||
The Rules popover (below the chat input) shows active rules and lets you toggle them on or off.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Rules Popover" />
|
||||
</Frame>
|
||||
|
||||
The popover displays:
|
||||
- **Global rules**: From your user-level Rules directory
|
||||
- **Workspace rules**: From `.clinerules/` in your project
|
||||
|
||||
Toggle any rule to enable or disable it. Disabled rules won't load, even if they match conditions.
|
||||
|
||||
## When to Use Rules
|
||||
|
||||
Rules work best for persistent project context:
|
||||
|
||||
- **Code standards**: Formatting preferences, naming conventions, project-specific patterns
|
||||
- **Documentation requirements**: Where to add docs, what format to follow
|
||||
- **Architecture decisions**: Design patterns, dependency rules, module boundaries
|
||||
- **Team conventions**: PR processes, branch naming, commit message format
|
||||
- **Technology constraints**: Required libraries, banned APIs, version requirements
|
||||
|
||||
Rules are less effective for:
|
||||
- One-time instructions (just say it in the chat)
|
||||
- Complex multi-step workflows (use [Workflows](/features/slash-commands/workflows/index) instead)
|
||||
- Dynamic decisions that depend on runtime context
|
||||
|
||||
## Example Rule
|
||||
|
||||
```markdown
|
||||
# Backend API Guidelines
|
||||
|
||||
## Route Handlers
|
||||
|
||||
- Use async/await, not callbacks
|
||||
- Validate request bodies with Zod schemas
|
||||
- Return typed errors from `src/errors.ts`
|
||||
- All routes require authentication unless in `publicRoutes` array
|
||||
|
||||
## Database Access
|
||||
|
||||
- All queries go through repository classes in `src/repositories/`
|
||||
- Use transactions for multi-table updates
|
||||
- Never expose raw database errors to clients
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit tests for business logic in `src/services/`
|
||||
- Integration tests for route handlers in `src/routes/`
|
||||
- Mock external APIs, not internal modules
|
||||
```
|
||||
|
||||
This rule provides clear, actionable guidance without explaining obvious concepts or using vague language.
|
||||
|
||||
## Using a Folder Structure
|
||||
|
||||
For projects with many rules, organize them in a `.clinerules/` folder:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/
|
||||
│ ├── 01-coding-standards.md
|
||||
│ ├── 02-documentation.md
|
||||
│ └── 03-testing.md
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline loads all Markdown files in `.clinerules/` automatically. The numeric prefixes help you control ordering, but they're optional.
|
||||
|
||||
### Organizing a Rules Bank
|
||||
|
||||
Maintain a separate folder for rules you might need but don't always want active:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Available but inactive
|
||||
│ ├── clients/
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ └── frameworks/
|
||||
│ ├── react.md
|
||||
│ └── vue.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
Copy files from the bank to `.clinerules/` when you need them. This keeps your active context lean while maintaining a library of reusable guidance.
|
||||
|
||||
Switch contexts with simple file operations:
|
||||
|
||||
```bash
|
||||
# Switch to Client B
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Consider git-ignoring `.clinerules/` while tracking `clinerules-bank/` so team members can activate the rules relevant to their current work.
|
||||
</Tip>
|
||||
|
||||
## Conditional Rules
|
||||
|
||||
Scope rules to specific file patterns using YAML frontmatter. This keeps React guidance out of Python code and backend rules away from frontend work.
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# React Guidelines
|
||||
|
||||
Use functional components with hooks. Extract reusable logic into custom hooks.
|
||||
```
|
||||
|
||||
This rule activates only when working with files matching those patterns. Read the [Conditional Rules guide](/features/cline-rules/conditional-rules) for pattern syntax, behavior details, and more examples.
|
||||
|
||||
## Tips for Effective Rules
|
||||
|
||||
**Be specific**: "Use async/await for all database calls" beats "write good async code."
|
||||
|
||||
**Show patterns**: Include file paths and real examples. "Follow the error handling in `src/utils/errors.ts`" gives Cline a concrete reference.
|
||||
|
||||
**Focus on outcomes**: Describe what you want, not step-by-step instructions. Let Cline figure out how.
|
||||
|
||||
**Test and refine**: Start with core standards. Add rules when you find yourself repeating the same feedback.
|
||||
|
||||
**Use conditional rules**: Load guidance only when relevant. This keeps context efficient and reduces noise.
|
||||
|
||||
## Related
|
||||
|
||||
- [Conditional Rules](/features/cline-rules/conditional-rules) - Activate rules based on file patterns
|
||||
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
|
||||
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
|
||||
- [New Rule Slash Command](/features/slash-commands/new-rule) - Generate rules with AI assistance
|
||||
- [Plan and Act Mode](/features/plan-and-act) - Use different rules for planning vs execution
|
||||
@@ -144,10 +144,6 @@ if (process.env.ERROR_SERVICE_API_KEY) {
|
||||
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
|
||||
}
|
||||
|
||||
if (process.env.POSTHOG_TELEMETRY_ENABLED) {
|
||||
buildEnvVars["process.env.POSTHOG_TELEMETRY_ENABLED"] = JSON.stringify(process.env.POSTHOG_TELEMETRY_ENABLED)
|
||||
}
|
||||
|
||||
// OpenTelemetry configuration (injected at build time from GitHub secrets)
|
||||
// These provide production defaults that can be overridden at runtime via environment variables
|
||||
if (process.env.OTEL_TELEMETRY_ENABLED) {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.53.1",
|
||||
"version": "3.55.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.53.1",
|
||||
"version": "3.55.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
|
||||
+1
-1
@@ -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.53.1",
|
||||
"version": "3.55.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -75,6 +75,19 @@ message McpResourceTemplate {
|
||||
optional string description = 4;
|
||||
}
|
||||
|
||||
message McpPromptArgument {
|
||||
string name = 1;
|
||||
optional string description = 2;
|
||||
optional bool required = 3;
|
||||
}
|
||||
|
||||
message McpPrompt {
|
||||
string name = 1;
|
||||
optional string title = 2;
|
||||
optional string description = 3;
|
||||
repeated McpPromptArgument arguments = 4;
|
||||
}
|
||||
|
||||
enum McpServerStatus {
|
||||
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
|
||||
// To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
|
||||
@@ -95,6 +108,7 @@ message McpServer {
|
||||
optional int32 timeout = 9;
|
||||
optional bool oauth_required = 10;
|
||||
optional string oauth_auth_status = 11;
|
||||
repeated McpPrompt prompts = 12;
|
||||
}
|
||||
|
||||
message McpServers {
|
||||
|
||||
@@ -28,6 +28,8 @@ service TaskService {
|
||||
rpc exportTaskWithId(StringRequest) returns (Empty);
|
||||
// Toggles the favorite status of a task
|
||||
rpc toggleTaskFavorite(TaskFavoriteRequest) returns (Empty);
|
||||
// Toggles the pin status of a task
|
||||
rpc toggleTaskPin(TaskPinRequest) returns (Empty);
|
||||
// Gets filtered task history
|
||||
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
|
||||
// Sends a response to a previous ask operation
|
||||
@@ -42,6 +44,8 @@ service TaskService {
|
||||
rpc deleteAllTaskHistory(EmptyRequest) returns (DeleteAllTaskHistoryCount);
|
||||
// Explains changes with AI and adds inline comments to the diff view
|
||||
rpc explainChanges(ExplainChangesRequest) returns (Empty);
|
||||
// Updates the custom name for a task
|
||||
rpc updateTaskName(UpdateTaskNameRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
@@ -60,6 +64,21 @@ message TaskFavoriteRequest {
|
||||
bool is_favorited = 3;
|
||||
}
|
||||
|
||||
// Request message for toggling task pin status
|
||||
message TaskPinRequest {
|
||||
Metadata metadata = 1;
|
||||
string task_id = 2;
|
||||
bool is_pinned = 3;
|
||||
}
|
||||
|
||||
// Request message for updating task custom name
|
||||
message UpdateTaskNameRequest {
|
||||
Metadata metadata = 1;
|
||||
string task_id = 2;
|
||||
string custom_name = 3;
|
||||
string custom_name_color = 4;
|
||||
}
|
||||
|
||||
// Response for task details
|
||||
message TaskResponse {
|
||||
string id = 1;
|
||||
@@ -73,6 +92,7 @@ message TaskResponse {
|
||||
int32 cache_writes = 9;
|
||||
int32 cache_reads = 10;
|
||||
string model_id = 11;
|
||||
bool is_pinned = 12;
|
||||
}
|
||||
|
||||
// Request for getting task history with filtering
|
||||
@@ -103,6 +123,9 @@ message TaskItem {
|
||||
int32 cache_writes = 9;
|
||||
int32 cache_reads = 10;
|
||||
string model_id = 11;
|
||||
bool is_pinned = 12;
|
||||
string custom_name = 13;
|
||||
string custom_name_color = 14;
|
||||
}
|
||||
|
||||
// Request for ask response operation
|
||||
|
||||
@@ -33,6 +33,9 @@ service EnvService {
|
||||
|
||||
// Initiates a graceful shutdown of the host bridge service.
|
||||
rpc shutdown(cline.EmptyRequest) returns (cline.Empty);
|
||||
|
||||
// Logs a debug message to the host environment's log/output console.
|
||||
rpc debugLog(cline.StringRequest) returns (cline.Empty);
|
||||
}
|
||||
|
||||
message GetHostVersionResponse {
|
||||
|
||||
+7
-4
@@ -36,8 +36,9 @@ import { arePathsEqual } from "./utils/path"
|
||||
* @throws ClineConfigurationError if endpoints.json exists but is invalid
|
||||
*/
|
||||
export async function initialize(context: vscode.ExtensionContext): Promise<WebviewProvider> {
|
||||
// Configure the shared Logging class to use HostProvider's output channel
|
||||
Logger.setOutput((msg: string) => HostProvider.get().logToChannel(msg))
|
||||
// Configure the shared Logging class to use HostProvider's output channels and debug logger
|
||||
Logger.subscribe((msg: string) => HostProvider.get().logToChannel(msg)) // File system logging
|
||||
Logger.subscribe((msg: string) => HostProvider.env.debugLog({ value: msg })) // Host debug logging
|
||||
|
||||
// Initialize ClineEndpoint configuration first (reads ~/.cline/endpoints.json if present)
|
||||
// This must be done before any other code that calls ClineEnv.config()
|
||||
@@ -61,8 +62,10 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
// Set the distinct ID for logging and telemetry
|
||||
await initializeDistinctId(context)
|
||||
|
||||
// Initialize PostHog client provider
|
||||
PostHogClientProvider.getInstance()
|
||||
// Initialize PostHog client provider (skip in self-hosted mode)
|
||||
if (!ClineEndpoint.isSelfHosted()) {
|
||||
PostHogClientProvider.getInstance()
|
||||
}
|
||||
|
||||
// Setup the external services
|
||||
await ErrorService.initialize()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { GenerateContentConfig, GoogleGenAI } from "@google/genai"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
|
||||
@@ -44,6 +45,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
baseURL: this.options.baseURL,
|
||||
defaultHeaders: {
|
||||
"APP-Code": this.options.appCode,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -64,6 +66,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
baseURL: `${this.options.baseURL}/v1`,
|
||||
defaultHeaders: {
|
||||
"APP-Code": this.options.appCode,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -87,6 +90,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
// @ts-expect-error
|
||||
"APP-Code": this.options.appCode,
|
||||
Authorization: `Bearer ${this.options.apiKey ?? ""}`,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
@@ -33,6 +34,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.apiKey,
|
||||
baseURL: this.options.anthropicBaseUrl || undefined,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -96,10 +97,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
// Make request to AskSage API
|
||||
const response = await fetch(`${this.apiUrl}/query`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
},
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
|
||||
@@ -157,10 +155,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}/count-monthly-tokens`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
},
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ app_name: "asksage" }),
|
||||
})
|
||||
|
||||
@@ -194,4 +189,12 @@ export class AskSageHandler implements ApiHandler {
|
||||
info: askSageModels[askSageDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
private headers() {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
...buildExternalBasicHeaders(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BasetenModelId, basetenDefaultModelId, basetenModels, ModelInfo } from
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -34,6 +35,7 @@ export class BasetenHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://inference.baseten.co/v1",
|
||||
apiKey: this.options.basetenApiKey,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Cerebras from "@cerebras/cerebras_cloud_sdk"
|
||||
import { CerebrasModelId, cerebrasDefaultModelId, cerebrasModels, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
@@ -35,11 +36,13 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
this.client = new Cerebras({
|
||||
apiKey: cleanApiKey,
|
||||
timeout: 30000, // 30 second timeout
|
||||
fetch, // Use configured fetch with proxy support
|
||||
defaultHeaders: {
|
||||
...externalHeaders,
|
||||
"X-Cerebras-3rd-Party-Integration": "cline",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,12 +2,13 @@ import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } fr
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { addReasoningContent } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
@@ -33,6 +34,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
apiKey: this.options.deepSeekApiKey,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -81,14 +83,10 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
const convertedMessages = convertToOpenAiMessages(messages)
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = isDeepseekReasoner
|
||||
? [{ role: "system", content: systemPrompt }, ...addReasoningContent(convertedMessages, messages)]
|
||||
: [{ role: "system", content: systemPrompt }, ...convertedMessages]
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -123,10 +124,7 @@ export class DifyHandler implements ApiHandler {
|
||||
try {
|
||||
response = await fetch(fullUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
} catch (error: any) {
|
||||
@@ -438,9 +436,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/files/upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
headers: this.headers(),
|
||||
body: formData,
|
||||
})
|
||||
|
||||
@@ -461,10 +457,7 @@ export class DifyHandler implements ApiHandler {
|
||||
async stopGeneration(taskId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/chat-messages/${taskId}/stop`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify({ user }),
|
||||
})
|
||||
|
||||
@@ -494,9 +487,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
headers: this.headers(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -531,9 +522,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
headers: this.headers(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -553,10 +542,7 @@ export class DifyHandler implements ApiHandler {
|
||||
async deleteConversation(conversationId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify({ user }),
|
||||
})
|
||||
|
||||
@@ -587,10 +573,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/name`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
@@ -623,10 +606,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/messages/${messageId}/feedbacks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
@@ -659,4 +639,19 @@ export class DifyHandler implements ApiHandler {
|
||||
this.conversationId = null
|
||||
this.currentTaskId = null
|
||||
}
|
||||
|
||||
private jsonHeaders() {
|
||||
return {
|
||||
...this.headers(),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}
|
||||
|
||||
private headers() {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
return {
|
||||
...externalHeaders,
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DoubaoModelId, doubaoDefaultModelId, doubaoModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -25,10 +25,9 @@ export class DoubaoHandler implements ApiHandler {
|
||||
throw new Error("Doubao API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
|
||||
apiKey: this.options.doubaoApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Doubao client: ${error.message}`)
|
||||
@@ -80,9 +79,9 @@ export class DoubaoHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FireworksModelId, fireworksDefaultModelId, fireworksModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -28,10 +28,9 @@ export class FireworksHandler implements ApiHandler {
|
||||
throw new Error("Fireworks API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.fireworks.ai/inference/v1",
|
||||
apiKey: this.options.fireworksApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Fireworks client: ${error.message}`)
|
||||
@@ -88,9 +87,9 @@ export class FireworksHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ThinkingLevel,
|
||||
} from "@google/genai"
|
||||
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -63,6 +64,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
private ensureClient(): GoogleGenAI {
|
||||
if (!this.client) {
|
||||
const options = this.options as GeminiHandlerOptions
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
|
||||
if (options.isVertex) {
|
||||
// Initialize with Vertex AI configuration
|
||||
@@ -74,6 +76,9 @@ export class GeminiHandler implements ApiHandler {
|
||||
vertexai: true,
|
||||
project,
|
||||
location,
|
||||
httpOptions: {
|
||||
headers: externalHeaders,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
|
||||
@@ -85,7 +90,12 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
|
||||
this.client = new GoogleGenAI({
|
||||
apiKey: options.geminiApiKey,
|
||||
httpOptions: {
|
||||
headers: externalHeaders,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Gemini client: ${error.message}`)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { GroqModelId, groqDefaultModelId, groqModels, ModelInfo } from "@shared/
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -103,6 +104,7 @@ export class GroqHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
apiKey: this.options.groqApiKey,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { hicapModelInfoSaneDefaults, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -29,7 +30,7 @@ export class HicapHandler implements ApiHandler {
|
||||
throw new Error("Model ID is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.hicap.ai/v2/openai",
|
||||
apiKey: this.options.hicapApiKey,
|
||||
defaultHeaders: {
|
||||
@@ -86,9 +87,8 @@ export class HicapHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HuaweiCloudMaasModelId, huaweiCloudMaasDefaultModelId, huaweiCloudMaasM
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -28,10 +28,9 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
throw new Error("Huawei Cloud MaaS API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.modelarts-maas.com/v1/",
|
||||
apiKey: this.options.huaweiCloudMaasApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -32,13 +32,9 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://router.huggingface.co/v1",
|
||||
apiKey: this.options.huggingFaceApiKey,
|
||||
defaultHeaders: {
|
||||
"User-Agent": "Cline/1.0",
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Hugging Face client: ${error.message}`)
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient, fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isAnthropicModelId } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
@@ -64,6 +65,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"x-litellm-api-key": apiKey,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -78,6 +80,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -112,10 +115,9 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
throw new Error("LiteLLM API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
|
||||
apiKey: this.options.liteLlmApiKey || "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LiteLLM client: ${error.message}`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -26,11 +26,10 @@ export class LmStudioHandler implements ApiHandler {
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
// Docs on the new v0 api endpoint: https://lmstudio.ai/docs/app/api/endpoints/rest
|
||||
baseURL: new URL("api/v0", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
|
||||
apiKey: "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LM Studio client: ${error.message}`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { MinimaxModelId, ModelInfo, minimaxDefaultModelId, minimaxModels } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
@@ -30,12 +31,14 @@ export class MinimaxHandler implements ApiHandler {
|
||||
throw new Error("MiniMax API key is required")
|
||||
}
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.minimaxApiKey,
|
||||
baseURL:
|
||||
this.options.minimaxApiLine === "china"
|
||||
? "https://api.minimaxi.com/anthropic"
|
||||
: "https://api.minimax.io/anthropic",
|
||||
defaultHeaders: externalHeaders,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { HTTPClient } from "@mistralai/mistralai/lib/http"
|
||||
import { Tool as MistralTool } from "@mistralai/mistralai/models/components/tool"
|
||||
import { MistralModelId, ModelInfo, mistralDefaultModelId, mistralModels } from "@shared/api"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -29,6 +30,7 @@ export class MistralHandler implements ApiHandler {
|
||||
throw new Error("Mistral API key is required")
|
||||
}
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
// Create HTTP client with custom fetch for proxy support
|
||||
// The Mistral SDK's HTTPClient passes a Request object to the fetcher,
|
||||
// but we need to extract the URL and init options to pass to our fetch wrapper
|
||||
@@ -37,6 +39,11 @@ export class MistralHandler implements ApiHandler {
|
||||
fetcher: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
// Handle both string/URL and Request object inputs
|
||||
if (input instanceof Request) {
|
||||
Object.keys(externalHeaders).forEach((key) => {
|
||||
if (!input.headers.has(key)) {
|
||||
input.headers.set(key, externalHeaders[key])
|
||||
}
|
||||
})
|
||||
return fetch(input.url, {
|
||||
method: input.method,
|
||||
headers: input.headers,
|
||||
@@ -48,7 +55,16 @@ export class MistralHandler implements ApiHandler {
|
||||
...init,
|
||||
} as RequestInit)
|
||||
}
|
||||
return fetch(input, init)
|
||||
|
||||
// Merge external headers with existing headers
|
||||
const mergedInit = {
|
||||
...init,
|
||||
headers: {
|
||||
...externalHeaders,
|
||||
...(init?.headers || {}),
|
||||
},
|
||||
}
|
||||
return fetch(input, mergedInit)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ModelInfo, MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -26,11 +26,10 @@ export class MoonshotHandler implements ApiHandler {
|
||||
throw new Error("Moonshot API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL:
|
||||
this.options.moonshotApiLine === "china" ? "https://api.moonshot.cn/v1" : "https://api.moonshot.ai/v1",
|
||||
apiKey: this.options.moonshotApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Moonshot client: ${error.message}`)
|
||||
@@ -52,7 +51,7 @@ export class MoonshotHandler implements ApiHandler {
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
temperature: model.info.temperature,
|
||||
max_tokens: model.info.maxTokens,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ModelInfo, type NebiusModelId, nebiusDefaultModelId, nebiusModels
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -26,10 +26,9 @@ export class NebiusHandler implements ApiHandler {
|
||||
throw new Error("Nebius API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.studio.nebius.ai/v1",
|
||||
apiKey: this.options.nebiusApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Nebius client: ${error.message}`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ModelInfo, NousResearchModelId, nousResearchDefaultModelId, nousResearchModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -25,7 +26,7 @@ export class NousResearchHandler implements ApiHandler {
|
||||
throw new Error("NousResearch API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://inference-api.nousResearch.com/v1",
|
||||
apiKey: this.options.nousResearchApiKey,
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
OCI_HEADER_OPC_REQUEST_ID,
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { OcaModelInfo } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
@@ -35,12 +36,14 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions {
|
||||
export class OcaHandler implements ApiHandler {
|
||||
protected options: OcaHandlerOptions
|
||||
protected client: OpenAI | undefined
|
||||
protected externalHeaders: Record<string, string> = {}
|
||||
|
||||
constructor(options: OcaHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
protected initializeClient(options: OcaHandlerOptions) {
|
||||
protected initializeClient(options: OcaHandlerOptions): OpenAI {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
return new (class OCIOpenAI extends OpenAI {
|
||||
protected override async prepareOptions(opts: any): Promise<void> {
|
||||
const token = await OcaAuthService.getInstance().getAuthToken()
|
||||
@@ -50,7 +53,7 @@ export class OcaHandler implements ApiHandler {
|
||||
opts.headers ??= {}
|
||||
// OCA Headers
|
||||
const ociHeaders = await createOcaHeaders(token, options.taskId!)
|
||||
opts.headers = { ...opts.headers, ...ociHeaders }
|
||||
opts.headers = { ...opts.headers, ...externalHeaders, ...ociHeaders }
|
||||
Logger.log(`Making request with customer opc-request-id: ${opts.headers?.["opc-request-id"]}`)
|
||||
return super.prepareOptions(opts)
|
||||
}
|
||||
@@ -113,12 +116,13 @@ export class OcaHandler implements ApiHandler {
|
||||
if (!token) {
|
||||
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
|
||||
}
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
const ociHeaders = await createOcaHeaders(token, this.options.taskId!)
|
||||
Logger.log(`Making calculate cost request with customer opc-request-id: ${ociHeaders["opc-request-id"]}`)
|
||||
try {
|
||||
const response = await fetch(`${client.baseURL}/spend/calculate`, {
|
||||
method: "POST",
|
||||
headers: ociHeaders,
|
||||
headers: { ...externalHeaders, ...ociHeaders },
|
||||
body: JSON.stringify({
|
||||
completion_response: {
|
||||
model: modelId,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { type Config, type Message, Ollama } from "ollama"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -7,6 +9,7 @@ import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
import type { ApiStream } from "../transform/stream"
|
||||
import { ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface OllamaHandlerOptions extends CommonApiHandlerOptions {
|
||||
ollamaBaseUrl?: string
|
||||
@@ -30,14 +33,17 @@ export class OllamaHandler implements ApiHandler {
|
||||
private ensureClient(): Ollama {
|
||||
if (!this.client) {
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
const clientOptions: Partial<Config> = {
|
||||
host: this.options.ollamaBaseUrl,
|
||||
fetch,
|
||||
headers: externalHeaders,
|
||||
}
|
||||
|
||||
// Add API key if provided (for Ollama cloud or authenticated instances)
|
||||
if (this.options.ollamaApiKey) {
|
||||
clientOptions.headers = {
|
||||
...clientOptions.headers,
|
||||
Authorization: `Bearer ${this.options.ollamaApiKey}`,
|
||||
}
|
||||
}
|
||||
@@ -51,7 +57,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry({ retryAllErrors: true })
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
|
||||
|
||||
@@ -70,20 +76,44 @@ export class OllamaHandler implements ApiHandler {
|
||||
options: {
|
||||
num_ctx: Number(this.options.ollamaApiOptionsCtxNum),
|
||||
},
|
||||
tools: tools as any,
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
// Race the API request against the timeout
|
||||
const stream = (await Promise.race([apiPromise, timeoutPromise])) as Awaited<typeof apiPromise>
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
if (typeof chunk.message.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.message.content,
|
||||
}
|
||||
Logger.debug("[OllamaHandler] Message Chunk" + JSON.stringify(chunk))
|
||||
|
||||
const delta = chunk.message
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
Logger.debug(`[OllamaHandler] Tool Calls Detected: ${JSON.stringify(delta.tool_calls)}`)
|
||||
yield* toolCallProcessor.processToolCallDeltas(
|
||||
delta.tool_calls?.map((tc, inx) => ({
|
||||
index: inx,
|
||||
id: `ollama-tool-${inx}`,
|
||||
function: {
|
||||
name: tc.function.name,
|
||||
arguments:
|
||||
typeof tc.function.arguments === "string"
|
||||
? tc.function.arguments
|
||||
: JSON.stringify(tc.function.arguments),
|
||||
},
|
||||
type: "function",
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof delta.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
// Handle token usage if available
|
||||
if (chunk.eval_count !== undefined || chunk.prompt_eval_count !== undefined) {
|
||||
yield {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -184,6 +185,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
session_id: this.sessionId,
|
||||
"User-Agent": `cline/${process.env.npm_package_version || "1.0.0"} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`,
|
||||
...(accountId ? { "ChatGPT-Account-Id": accountId } : {}),
|
||||
...buildExternalBasicHeaders(),
|
||||
}
|
||||
|
||||
// Try using OpenAI SDK first
|
||||
|
||||
@@ -9,7 +9,7 @@ import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/cline/models"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isGPT5ModelFamily } from "@/utils/model-utils"
|
||||
@@ -41,9 +41,8 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
throw new Error("OpenAI API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
apiKey: this.options.openAiNativeApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating OpenAI client: ${error.message}`)
|
||||
|
||||
@@ -2,8 +2,9 @@ import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity"
|
||||
import { azureOpenAiDefaultApiVersion, ModelInfo, OpenAiCompatibleModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI, { AzureOpenAI } from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient, fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -45,6 +46,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
try {
|
||||
const baseUrl = this.options.openAiBaseUrl?.toLowerCase() ?? ""
|
||||
const isAzureDomain = baseUrl.includes("azure.com") || baseUrl.includes("azure.us")
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
// Azure API shape slightly differs from the core API shape...
|
||||
if (
|
||||
this.options.azureApiVersion ||
|
||||
@@ -58,7 +60,10 @@ export class OpenAiHandler implements ApiHandler {
|
||||
this.getAzureAudienceScope(this.options.openAiBaseUrl),
|
||||
),
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
defaultHeaders: {
|
||||
...externalHeaders,
|
||||
...this.options.openAiHeaders,
|
||||
},
|
||||
fetch,
|
||||
})
|
||||
} else {
|
||||
@@ -66,16 +71,18 @@ export class OpenAiHandler implements ApiHandler {
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
defaultHeaders: {
|
||||
...externalHeaders,
|
||||
...this.options.openAiHeaders,
|
||||
},
|
||||
fetch,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
fetch,
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -163,7 +170,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import axios from "axios"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { createOpenAIClient, getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
@@ -40,14 +40,13 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
throw new Error("OpenRouter API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://openrouter.ai/api/v1",
|
||||
apiKey: this.options.openRouterApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
|
||||
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating OpenRouter client: ${error.message}`)
|
||||
@@ -140,7 +139,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
if (
|
||||
"reasoning_details" in delta &&
|
||||
delta.reasoning_details &&
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
delta.reasoning_details.length && // exists and non-0
|
||||
!shouldSkipReasoningForModel(this.options.openRouterModelId)
|
||||
) {
|
||||
@@ -158,7 +157,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
|
||||
}
|
||||
didOutputUsage = true
|
||||
|
||||
@@ -4,6 +4,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -63,9 +64,12 @@ export class QwenCodeHandler implements ApiHandler {
|
||||
if (!this.client) {
|
||||
// Create the client instance with dummy key initially
|
||||
// The API key will be updated dynamically via ensureAuthenticated
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
this.client = new OpenAI({
|
||||
apiKey: "dummy-key-will-be-replaced",
|
||||
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
defaultHeaders: externalHeaders,
|
||||
fetch,
|
||||
})
|
||||
}
|
||||
return this.client
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
@@ -49,12 +49,11 @@ export class QwenHandler implements ApiHandler {
|
||||
throw new Error("Alibaba API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: this.useChinaApi()
|
||||
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: this.options.qwenApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Alibaba client: ${error.message}`)
|
||||
@@ -151,9 +150,9 @@ export class QwenHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import { toRequestyServiceStringUrl } from "@/shared/clients/requesty"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -42,14 +42,13 @@ export class RequestyHandler implements ApiHandler {
|
||||
throw new Error("Requesty API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: toRequestyServiceStringUrl(this.options.requestyBaseUrl),
|
||||
apiKey: this.options.requestyApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Requesty client: ${error.message}`)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels }
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -29,10 +29,9 @@ export class SambanovaHandler implements ApiHandler {
|
||||
throw new Error("SambaNova API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.sambanova.ai/v1",
|
||||
apiKey: this.options.sambanovaApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating SambaNova client: ${error.message}`)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels }
|
||||
import axios from "axios"
|
||||
import JSON5 from "json5"
|
||||
import OpenAI from "openai"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -427,9 +428,10 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
client_secret: this.options.sapAiCoreClientSecret,
|
||||
}
|
||||
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
const tokenUrl = this.options.sapAiCoreTokenUrl!.replace(/\/+$/, "") + "/oauth/token"
|
||||
const response = await axios.post(tokenUrl, payload, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
headers: { ...externalHeaders, "Content-Type": "application/x-www-form-urlencoded" },
|
||||
...getAxiosSettings(),
|
||||
})
|
||||
const token = response.data as Token
|
||||
@@ -447,7 +449,9 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
// TODO: these fallback fetching deployment id methods can be removed in future version if decided that users migration to fetching deployment id in design-time (open SAP AI Core provider UI) considered as completed.
|
||||
private async getAiCoreDeployments(): Promise<Deployment[]> {
|
||||
const token = await this.getToken()
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
const headers = {
|
||||
...externalHeaders,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"AI-Resource-Group": this.options.sapAiResourceGroup || "default",
|
||||
"Content-Type": "application/json",
|
||||
@@ -585,7 +589,9 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
private async *createMessageWithDeployments(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
const token = await this.getToken()
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
const headers = {
|
||||
...externalHeaders,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"AI-Resource-Group": this.options.sapAiResourceGroup || "default",
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -29,10 +29,9 @@ export class TogetherHandler implements ApiHandler {
|
||||
throw new Error("Together API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.together.xyz/v1",
|
||||
apiKey: this.options.togetherApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Together client: ${error.message}`)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
@@ -34,14 +34,13 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
throw new Error("Vercel AI Gateway API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://ai-gateway.vercel.sh/v1",
|
||||
apiKey: this.options.vercelAiGatewayApiKey,
|
||||
defaultHeaders: {
|
||||
"http-referer": "https://cline.bot",
|
||||
"x-title": "Cline",
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Vercel AI Gateway client: ${error.message}`)
|
||||
@@ -98,7 +97,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
if (
|
||||
"reasoning_details" in delta &&
|
||||
delta.reasoning_details &&
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
delta.reasoning_details.length && // exists and non-0
|
||||
!shouldSkipReasoningForModel(this.options.openRouterModelId)
|
||||
) {
|
||||
@@ -110,7 +109,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
// @ts-ignore - Vercel AI Gateway extends OpenAI types
|
||||
// @ts-expect-error - Vercel AI Gateway extends OpenAI types
|
||||
const totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
|
||||
yield {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
|
||||
import { FunctionDeclaration as GoogleTool } from "@google/genai"
|
||||
import { ModelInfo, VertexModelId, vertexDefaultModelId, vertexModels } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -54,11 +55,13 @@ export class VertexHandler implements ApiHandler {
|
||||
throw new Error("Vertex AI region is required")
|
||||
}
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
// Initialize Anthropic client for Claude models
|
||||
this.clientAnthropic = new AnthropicVertex({
|
||||
projectId: this.options.vertexProjectId,
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
|
||||
region: this.options.vertexRegion,
|
||||
defaultHeaders: externalHeaders,
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Vertex AI Anthropic client: ${error.message}`)
|
||||
|
||||
@@ -4,7 +4,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -31,10 +31,9 @@ export class XAIHandler implements ApiHandler {
|
||||
throw new Error("xAI API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.x.ai/v1",
|
||||
apiKey: this.options.xaiApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating xAI client: ${error.message}`)
|
||||
@@ -86,7 +85,7 @@ export class XAIHandler implements ApiHandler {
|
||||
if (!shouldSkipReasoningForModel(modelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
reasoning: delta.reasoning_content,
|
||||
}
|
||||
}
|
||||
@@ -97,9 +96,8 @@ export class XAIHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { version as extensionVersion } from "../../../../package.json"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
@@ -41,7 +41,7 @@ export class ZAiHandler implements ApiHandler {
|
||||
throw new Error("Z AI API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: this.useChinaApi() ? "https://open.bigmodel.cn/api/paas/v4" : "https://api.z.ai/api/paas/v4",
|
||||
apiKey: this.options.zaiApiKey,
|
||||
defaultHeaders: {
|
||||
@@ -49,7 +49,6 @@ export class ZAiHandler implements ApiHandler {
|
||||
"X-Title": "Cline",
|
||||
"X-Cline-Version": extensionVersion,
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Z AI client: ${error.message}`)
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { ClineAssistantThinkingBlock, ClineStorageMessage } from "@/shared/messages/content"
|
||||
|
||||
/**
|
||||
* DeepSeek Reasoner message format with reasoning_content support.
|
||||
*/
|
||||
export type DeepSeekReasonerMessage = OpenAI.Chat.ChatCompletionMessageParam & {
|
||||
reasoning_content?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds reasoning_content to OpenAI messages for DeepSeek Reasoner.
|
||||
* Per DeepSeek API: reasoning_content should be passed back during tool calling in the same turn,
|
||||
* and omitted when starting a new turn.
|
||||
*/
|
||||
export function addReasoningContent(
|
||||
openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[],
|
||||
originalMessages: ClineStorageMessage[],
|
||||
): DeepSeekReasonerMessage[] {
|
||||
// Find last user message index (start of current turn)
|
||||
// If no user message exists (lastUserIndex = -1), all messages are in the "current turn",
|
||||
// so reasoning_content will be added to all assistant messages. This is intentional.
|
||||
let lastUserIndex = -1
|
||||
for (let i = openAiMessages.length - 1; i >= 0; i--) {
|
||||
if (openAiMessages[i].role === "user") {
|
||||
lastUserIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Extract thinking content from original messages, keyed by assistant index
|
||||
const thinkingByIndex = new Map<number, string>()
|
||||
let assistantIdx = 0
|
||||
for (const msg of originalMessages) {
|
||||
if (msg.role === "assistant") {
|
||||
if (Array.isArray(msg.content)) {
|
||||
const thinking = msg.content
|
||||
.filter((p): p is ClineAssistantThinkingBlock => p.type === "thinking")
|
||||
.map((p) => p.thinking)
|
||||
.join("\n")
|
||||
if (thinking) {
|
||||
thinkingByIndex.set(assistantIdx, thinking)
|
||||
}
|
||||
}
|
||||
assistantIdx++
|
||||
}
|
||||
}
|
||||
|
||||
// Add reasoning_content only to assistant messages in current turn
|
||||
let aiIdx = 0
|
||||
return openAiMessages.map((msg, i): DeepSeekReasonerMessage => {
|
||||
if (msg.role === "assistant") {
|
||||
const thinking = thinkingByIndex.get(aiIdx++)
|
||||
if (thinking && i >= lastUserIndex) {
|
||||
return { ...msg, reasoning_content: thinking }
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Anthropic messages to OpenAI format and merges consecutive messages with the same role.
|
||||
|
||||
@@ -849,6 +849,7 @@ export class Controller {
|
||||
const lastDismissedInfoBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedInfoBannerVersion") || 0
|
||||
const lastDismissedModelBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedModelBannerVersion") || 0
|
||||
const lastDismissedCliBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedCliBannerVersion") || 0
|
||||
const dismissedBanners = this.stateManager.getGlobalStateKey("dismissedBanners")
|
||||
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
const skillsEnabled = this.stateManager.getGlobalSettingsKey("skillsEnabled")
|
||||
|
||||
@@ -963,6 +964,7 @@ export class Controller {
|
||||
lastDismissedModelBannerVersion,
|
||||
remoteConfigSettings: this.stateManager.getRemoteConfigSettings(),
|
||||
lastDismissedCliBannerVersion,
|
||||
dismissedBanners,
|
||||
subagentsEnabled,
|
||||
nativeToolCallSetting: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
|
||||
@@ -1005,11 +1007,23 @@ export class Controller {
|
||||
const history = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const existingItemIndex = history.findIndex((h) => h.id === item.id)
|
||||
if (existingItemIndex !== -1) {
|
||||
history[existingItemIndex] = item
|
||||
// Preserve custom fields from existing item when updating
|
||||
const existingItem = history[existingItemIndex]
|
||||
history[existingItemIndex] = {
|
||||
...item,
|
||||
// Only preserve these fields if they exist in the existing item and are not explicitly set in the new item
|
||||
customName: item.customName ?? existingItem.customName,
|
||||
customNameColor: item.customNameColor ?? existingItem.customNameColor,
|
||||
isPinned: item.isPinned ?? existingItem.isPinned,
|
||||
}
|
||||
} else {
|
||||
history.push(item)
|
||||
}
|
||||
this.stateManager.setGlobalState("taskHistory", history)
|
||||
|
||||
// Force immediate write to disk to survive hot reloads
|
||||
await this.stateManager.flushPendingState()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
|
||||
@@ -66,9 +66,15 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
|
||||
// Calculate total count before sorting
|
||||
const totalCount = filteredTasks.length
|
||||
|
||||
// Apply sorting
|
||||
// Apply sorting - pinned tasks always come first
|
||||
if (sortBy) {
|
||||
filteredTasks.sort((a, b) => {
|
||||
// First, sort by pinned status
|
||||
if (a.isPinned !== b.isPinned) {
|
||||
return a.isPinned ? -1 : 1
|
||||
}
|
||||
|
||||
// Then apply the selected sort
|
||||
switch (sortBy) {
|
||||
case "oldest":
|
||||
return a.ts - b.ts
|
||||
@@ -88,8 +94,13 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Default sort by newest
|
||||
filteredTasks.sort((a, b) => b.ts - a.ts)
|
||||
// Default sort by newest, but pinned first
|
||||
filteredTasks.sort((a, b) => {
|
||||
if (a.isPinned !== b.isPinned) {
|
||||
return a.isPinned ? -1 : 1
|
||||
}
|
||||
return b.ts - a.ts
|
||||
})
|
||||
}
|
||||
|
||||
// Map to response format
|
||||
@@ -98,6 +109,7 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
|
||||
task: item.task,
|
||||
ts: item.ts,
|
||||
isFavorited: item.isFavorited || false,
|
||||
isPinned: item.isPinned || false,
|
||||
size: item.size || 0,
|
||||
totalCost: item.totalCost || 0,
|
||||
tokensIn: item.tokensIn || 0,
|
||||
@@ -105,6 +117,8 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
|
||||
cacheWrites: item.cacheWrites || 0,
|
||||
cacheReads: item.cacheReads || 0,
|
||||
modelId: item.modelId || "",
|
||||
customName: item.customName || "",
|
||||
customNameColor: item.customNameColor || "",
|
||||
}))
|
||||
|
||||
return TaskHistoryArray.create({
|
||||
|
||||
@@ -30,6 +30,9 @@ export async function toggleTaskFavorite(controller: Controller, request: TaskFa
|
||||
// Update global state and wait for it to complete
|
||||
try {
|
||||
controller.stateManager.setGlobalState("taskHistory", updatedHistory)
|
||||
|
||||
// Force immediate write to disk to survive hot reloads
|
||||
await controller.stateManager.flushPendingState()
|
||||
} catch (stateErr) {
|
||||
Logger.error("Error updating global state:", stateErr)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { TaskPinRequest } from "@shared/proto/cline/task"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../"
|
||||
|
||||
export async function toggleTaskPin(controller: Controller, request: TaskPinRequest): Promise<Empty> {
|
||||
if (!request.taskId || request.isPinned === undefined) {
|
||||
const errorMsg = `[toggleTaskPin] Invalid request: taskId or isPinned missing`
|
||||
Logger.error(errorMsg)
|
||||
return Empty.create({})
|
||||
}
|
||||
|
||||
try {
|
||||
// Update in-memory state only
|
||||
try {
|
||||
const history = controller.stateManager.getGlobalStateKey("taskHistory")
|
||||
|
||||
const taskIndex = history.findIndex((item) => item.id === request.taskId)
|
||||
|
||||
if (taskIndex === -1) {
|
||||
Logger.log(`[toggleTaskPin] Task not found in history array!`)
|
||||
} else {
|
||||
// Create a new array instead of modifying in place to ensure state change
|
||||
const updatedHistory = [...history]
|
||||
updatedHistory[taskIndex] = {
|
||||
...updatedHistory[taskIndex],
|
||||
isPinned: request.isPinned,
|
||||
}
|
||||
|
||||
// Update global state and wait for it to complete
|
||||
try {
|
||||
controller.stateManager.setGlobalState("taskHistory", updatedHistory)
|
||||
|
||||
// Force immediate write to disk to survive hot reloads
|
||||
await controller.stateManager.flushPendingState()
|
||||
} catch (stateErr) {
|
||||
Logger.error("Error updating global state:", stateErr)
|
||||
}
|
||||
}
|
||||
} catch (historyErr) {
|
||||
Logger.error("Error processing task history:", historyErr)
|
||||
}
|
||||
|
||||
// Post to webview
|
||||
try {
|
||||
await controller.postStateToWebview()
|
||||
} catch (webviewErr) {
|
||||
Logger.error("Error posting to webview:", webviewErr)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Error in toggleTaskPin:", error)
|
||||
}
|
||||
|
||||
return Empty.create({})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { UpdateTaskNameRequest } from "@shared/proto/cline/task"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../"
|
||||
|
||||
export async function updateTaskName(controller: Controller, request: UpdateTaskNameRequest): Promise<Empty> {
|
||||
if (!request.taskId) {
|
||||
const errorMsg = `[updateTaskName] Invalid request: taskId missing`
|
||||
Logger.error(errorMsg)
|
||||
return Empty.create({})
|
||||
}
|
||||
|
||||
Logger.log(`[updateTaskName] Received request for task ${request.taskId}:`, {
|
||||
customName: request.customName,
|
||||
customNameColor: request.customNameColor,
|
||||
})
|
||||
|
||||
try {
|
||||
// Update in-memory state only
|
||||
try {
|
||||
const history = controller.stateManager.getGlobalStateKey("taskHistory")
|
||||
|
||||
const taskIndex = history.findIndex((item) => item.id === request.taskId)
|
||||
|
||||
if (taskIndex === -1) {
|
||||
Logger.log(`[updateTaskName] Task not found in history array!`)
|
||||
} else {
|
||||
const oldTask = history[taskIndex]
|
||||
Logger.log(`[updateTaskName] Current task state:`, {
|
||||
customName: oldTask.customName,
|
||||
customNameColor: oldTask.customNameColor,
|
||||
})
|
||||
|
||||
// Create a new array instead of modifying in place to ensure state change
|
||||
const updatedHistory = [...history]
|
||||
updatedHistory[taskIndex] = {
|
||||
...updatedHistory[taskIndex],
|
||||
customName: request.customName || undefined,
|
||||
customNameColor: request.customNameColor || undefined,
|
||||
}
|
||||
|
||||
Logger.log(`[updateTaskName] Updated task state:`, {
|
||||
customName: updatedHistory[taskIndex].customName,
|
||||
customNameColor: updatedHistory[taskIndex].customNameColor,
|
||||
})
|
||||
|
||||
// Update global state and wait for it to complete
|
||||
try {
|
||||
controller.stateManager.setGlobalState("taskHistory", updatedHistory)
|
||||
Logger.log(`[updateTaskName] Successfully saved to global state`)
|
||||
|
||||
// Force immediate write to disk to survive hot reloads
|
||||
await controller.stateManager.flushPendingState()
|
||||
Logger.log(`[updateTaskName] Successfully flushed to disk`)
|
||||
} catch (stateErr) {
|
||||
Logger.error("Error updating global state:", stateErr)
|
||||
}
|
||||
}
|
||||
} catch (historyErr) {
|
||||
Logger.error("Error processing task history:", historyErr)
|
||||
}
|
||||
|
||||
// Post to webview
|
||||
try {
|
||||
await controller.postStateToWebview()
|
||||
} catch (webviewErr) {
|
||||
Logger.error("Error posting to webview:", webviewErr)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Error in updateTaskName:", error)
|
||||
}
|
||||
|
||||
return Empty.create({})
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export class HookProcess extends EventEmitter {
|
||||
private readonly scriptPath: string,
|
||||
private readonly timeoutMs: number = 30000,
|
||||
private readonly abortSignal?: AbortSignal,
|
||||
private readonly cwd?: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -108,6 +109,7 @@ export class HookProcess extends EventEmitter {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
shell: true, // Use shell on all platforms for shebang interpretation
|
||||
detached: process.platform !== "win32", // Create process group on Unix
|
||||
cwd: this.cwd, // Execute from the determined workspace root
|
||||
})
|
||||
|
||||
let didEmitEmptyLine = false
|
||||
|
||||
@@ -38,7 +38,15 @@ describe("Hook System", () => {
|
||||
|
||||
// Mock StateManager to return our temp directory
|
||||
sandbox.stub(StateManager, "get").returns({
|
||||
getGlobalStateKey: () => [{ path: tempDir }],
|
||||
getGlobalStateKey: (key: string) => {
|
||||
if (key === "workspaceRoots") {
|
||||
return [{ path: tempDir }]
|
||||
}
|
||||
if (key === "primaryRootIndex") {
|
||||
return 0
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
} as any)
|
||||
|
||||
// Reset hook discovery cache for clean test state
|
||||
@@ -79,6 +87,40 @@ describe("Hook System", () => {
|
||||
})
|
||||
|
||||
describe("StdioHookRunner", () => {
|
||||
it("should execute workspace hook from its respective workspace root directory", async () => {
|
||||
// Create a test hook script that outputs the current working directory
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = require('fs').readFileSync(0, 'utf-8');
|
||||
// Output the current working directory
|
||||
console.log(JSON.stringify({
|
||||
cancel: false,
|
||||
contextModification: "CWD: " + process.cwd()
|
||||
}))`
|
||||
|
||||
await writeHookScript(hookPath, hookScript)
|
||||
|
||||
// Test execution
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("PreToolUse")
|
||||
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
preToolUse: {
|
||||
toolName: "test_tool",
|
||||
parameters: {},
|
||||
},
|
||||
})
|
||||
|
||||
result.cancel.should.be.false()
|
||||
// The hook should execute from its workspace root (tempDir)
|
||||
// Use fs.realpath to normalize paths (handles macOS /private prefix)
|
||||
const cwdFromHook = result.contextModification!.replace("CWD: ", "")
|
||||
const normalizedCwd = await fs.realpath(cwdFromHook)
|
||||
const normalizedTempDir = await fs.realpath(tempDir)
|
||||
normalizedCwd.should.equal(normalizedTempDir)
|
||||
})
|
||||
|
||||
it("should execute hook script and parse output", async () => {
|
||||
// Create a test hook script
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
|
||||
@@ -545,6 +587,39 @@ console.log(JSON.stringify({
|
||||
result.errorMessage!.should.match(/Workspace error/)
|
||||
})
|
||||
|
||||
it("should execute global hook from primary workspace root directory", async () => {
|
||||
// Create a global hook script that outputs the current working directory
|
||||
const globalHookPath = path.join(globalHooksDir, "PreToolUse")
|
||||
const globalHookScript = `#!/usr/bin/env node
|
||||
const input = require('fs').readFileSync(0, 'utf-8');
|
||||
// Output the current working directory
|
||||
console.log(JSON.stringify({
|
||||
cancel: false,
|
||||
contextModification: "CWD: " + process.cwd()
|
||||
}))`
|
||||
await writeHookScript(globalHookPath, globalHookScript)
|
||||
|
||||
// Test execution
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("PreToolUse")
|
||||
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
preToolUse: {
|
||||
toolName: "test_tool",
|
||||
parameters: {},
|
||||
},
|
||||
})
|
||||
|
||||
result.cancel.should.be.false()
|
||||
// Global hooks should execute from the primary workspace root (tempDir)
|
||||
// Use fs.realpath to normalize paths (handles macOS /private prefix)
|
||||
const cwdFromHook = result.contextModification!.replace("CWD: ", "")
|
||||
const normalizedCwd = await fs.realpath(cwdFromHook)
|
||||
const normalizedTempDir = await fs.realpath(tempDir)
|
||||
normalizedCwd.should.equal(normalizedTempDir)
|
||||
})
|
||||
|
||||
it("should work with global PostToolUse hooks", async () => {
|
||||
// Create global PostToolUse hook
|
||||
const globalHookPath = path.join(globalHooksDir, "PostToolUse")
|
||||
|
||||
@@ -263,6 +263,7 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
private readonly abortSignal?: AbortSignal,
|
||||
private readonly taskId?: string,
|
||||
private readonly toolName?: string,
|
||||
private readonly cwd?: string,
|
||||
) {
|
||||
super(hookName)
|
||||
}
|
||||
@@ -302,7 +303,7 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
const inputJson = JSON.stringify(jsonObj)
|
||||
|
||||
// Create HookProcess for execution with streaming
|
||||
const hookProcess = new HookProcess(this.scriptPath, HOOK_EXECUTION_TIMEOUT_MS, this.abortSignal)
|
||||
const hookProcess = new HookProcess(this.scriptPath, HOOK_EXECUTION_TIMEOUT_MS, this.abortSignal, this.cwd)
|
||||
|
||||
// Set up streaming if callback is provided
|
||||
if (this.streamCallback) {
|
||||
@@ -775,10 +776,19 @@ export class HookFactory {
|
||||
)
|
||||
}
|
||||
|
||||
// Create runners with source determination for each script
|
||||
// Get workspace roots for cwd determination
|
||||
const stateManager = StateManager.get()
|
||||
const workspaceRoots = stateManager.getGlobalStateKey("workspaceRoots")
|
||||
const primaryRootIndex = stateManager.getGlobalStateKey("primaryRootIndex") ?? 0
|
||||
const primaryCwd = workspaceRoots?.[primaryRootIndex]?.path
|
||||
|
||||
// Create runners with source and cwd determination for each script
|
||||
// Global hooks run from primary workspace root
|
||||
// Workspace-specific hooks run from their respective workspace root
|
||||
const runners = scripts.map((script) => {
|
||||
const source = this.determineScriptSource(script, hooksDirs)
|
||||
return new StdioHookRunner(hookName, script, source, streamCallback, abortSignal, taskId, toolName)
|
||||
const cwd = this.determineHookCwd(script, hooksDirs, workspaceRoots, primaryCwd)
|
||||
return new StdioHookRunner(hookName, script, source, streamCallback, abortSignal, taskId, toolName, cwd)
|
||||
})
|
||||
|
||||
if (runners.length === 0) {
|
||||
@@ -806,6 +816,48 @@ export class HookFactory {
|
||||
return "workspace" // Default to workspace if uncertain
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the working directory for a hook script based on its location.
|
||||
*
|
||||
* - Global hooks (from ~/Documents/Cline/Hooks/): run from the primary workspace root
|
||||
* - Workspace hooks (from workspaceRoot/.clinerules/hooks/): run from that specific workspace root
|
||||
*
|
||||
* This ensures workspace-specific hooks can use relative paths that are meaningful
|
||||
* within their own workspace context.
|
||||
*
|
||||
* @param scriptPath The full path to the hook script
|
||||
* @param hooksDirs Array of all hooks directories
|
||||
* @param workspaceRoots Array of workspace root objects
|
||||
* @param primaryCwd The primary workspace root path (fallback)
|
||||
* @returns The working directory to use for this hook
|
||||
*/
|
||||
private determineHookCwd(
|
||||
scriptPath: string,
|
||||
hooksDirs: string[],
|
||||
workspaceRoots: Array<{ path: string }> | undefined,
|
||||
primaryCwd: string | undefined,
|
||||
): string | undefined {
|
||||
const containingDir = hooksDirs.find((dir) => scriptPath.startsWith(dir))
|
||||
|
||||
// If global hook, use primary workspace root
|
||||
if (containingDir && HookFactory.isGlobalHooksDir(containingDir)) {
|
||||
return primaryCwd
|
||||
}
|
||||
|
||||
// If workspace hook, find which workspace root it belongs to
|
||||
// Workspace hooks are at: workspaceRoot/.clinerules/hooks/
|
||||
// So find the workspace root whose path is a prefix of the containing hooks dir
|
||||
if (containingDir && workspaceRoots) {
|
||||
const workspaceRoot = workspaceRoots.find((root) => containingDir.startsWith(root.path))
|
||||
if (workspaceRoot) {
|
||||
return workspaceRoot.path
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to primary cwd
|
||||
return primaryCwd
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorizes hook scripts by their location (global vs workspace).
|
||||
* Global hooks are located in ~/Documents/Cline/Hooks/
|
||||
|
||||
+3
-1
@@ -464,12 +464,14 @@ Example:
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
+3
-1
@@ -430,12 +430,14 @@ Example:
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
+3
-1
@@ -392,12 +392,14 @@ By waiting for and carefully considering the user's response after each tool use
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
@@ -495,12 +495,14 @@ Example:
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
+3
-1
@@ -461,12 +461,14 @@ Example:
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
+3
-1
@@ -419,12 +419,14 @@ By waiting for and carefully considering the user's response after each tool use
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
@@ -464,12 +464,14 @@ Example:
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
@@ -430,12 +430,14 @@ Example:
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
+3
-1
@@ -392,12 +392,14 @@ By waiting for and carefully considering the user's response after each tool use
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
@@ -464,12 +464,14 @@ Example:
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
@@ -430,12 +430,14 @@ Example:
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
+3
-1
@@ -392,12 +392,14 @@ By waiting for and carefully considering the user's response after each tool use
|
||||
|
||||
MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
## test-server (`test`)
|
||||
|
||||
### Available Tools
|
||||
|
||||
@@ -22,12 +22,14 @@ export function hasEnabledMcpServers(context: SystemPromptContext): boolean {
|
||||
|
||||
const MCP_TEMPLATE_TEXT = `MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the \`use_mcp_tool\` tool, and access the server's resources via the \`access_mcp_resource\` tool.
|
||||
|
||||
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
|
||||
|
||||
{{MCP_SERVERS_LIST}}`
|
||||
|
||||
export async function getMcp(variant: PromptVariant, context: SystemPromptContext): Promise<string | undefined> {
|
||||
@@ -71,6 +73,18 @@ function formatMcpServersList(servers: McpServer[]): string {
|
||||
?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
|
||||
.join("\n")
|
||||
|
||||
const prompts = server.prompts
|
||||
?.map((prompt) => {
|
||||
const argsStr = prompt.arguments?.length
|
||||
? `\n Arguments: ${prompt.arguments
|
||||
.map((arg) => `${arg.name}${arg.required ? " (required)" : ""}${arg.description ? `: ${arg.description}` : ""}`)
|
||||
.join(", ")}`
|
||||
: ""
|
||||
const title = prompt.title ? ` (${prompt.title})` : ""
|
||||
return `- ${prompt.name}${title}: ${prompt.description || "No description"}${argsStr}`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
const config = JSON.parse(server.config)
|
||||
|
||||
return (
|
||||
@@ -80,7 +94,8 @@ function formatMcpServersList(servers: McpServer[]): string {
|
||||
: "") +
|
||||
(tools ? `\n\n### Available Tools\n${tools}` : "") +
|
||||
(templates ? `\n\n### Resource Templates\n${templates}` : "") +
|
||||
(resources ? `\n\n### Direct Resources\n${resources}` : "")
|
||||
(resources ? `\n\n### Direct Resources\n${resources}` : "") +
|
||||
(prompts ? `\n\n### Available Prompts\n${prompts}` : "")
|
||||
)
|
||||
})
|
||||
.join("\n\n")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ModelFamily } from "@/shared/prompts"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import { isGPT5ModelFamily } from "@/utils/model-utils"
|
||||
import type { ClineToolSpec } from "../spec"
|
||||
import { TASK_PROGRESS_PARAMETER } from "../types"
|
||||
|
||||
@@ -80,7 +81,7 @@ const NATIVE_GPT_5: ClineToolSpec = {
|
||||
id: ClineDefaultTool.APPLY_PATCH,
|
||||
name: "apply_patch",
|
||||
description: APPLY_PATCH_TOOL_DESC,
|
||||
contextRequirements: (context) => context.providerInfo.model.id.includes("gpt-5"),
|
||||
contextRequirements: (context) => isGPT5ModelFamily(context.providerInfo.model.id),
|
||||
parameters: [
|
||||
{
|
||||
name: "input",
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { McpPromptResponse } from "@shared/mcp"
|
||||
import { expect } from "chai"
|
||||
import { formatMcpPromptResponse, McpPromptFetcher, parseSlashCommands } from "../index"
|
||||
|
||||
describe("slash-commands", () => {
|
||||
describe("formatMcpPromptResponse", () => {
|
||||
it("should format text message", () => {
|
||||
const response: McpPromptResponse = {
|
||||
messages: [{ role: "user", content: { type: "text", text: "Hello world" } }],
|
||||
}
|
||||
const result = formatMcpPromptResponse(response)
|
||||
expect(result).to.equal("[User]\nHello world")
|
||||
})
|
||||
|
||||
it("should format assistant message", () => {
|
||||
const response: McpPromptResponse = {
|
||||
messages: [{ role: "assistant", content: { type: "text", text: "I can help" } }],
|
||||
}
|
||||
const result = formatMcpPromptResponse(response)
|
||||
expect(result).to.equal("[Assistant]\nI can help")
|
||||
})
|
||||
|
||||
it("should include description when provided", () => {
|
||||
const response: McpPromptResponse = {
|
||||
description: "Test description",
|
||||
messages: [{ role: "user", content: { type: "text", text: "Hello" } }],
|
||||
}
|
||||
const result = formatMcpPromptResponse(response)
|
||||
expect(result).to.include("Description: Test description")
|
||||
expect(result).to.include("[User]\nHello")
|
||||
})
|
||||
|
||||
it("should format multiple messages", () => {
|
||||
const response: McpPromptResponse = {
|
||||
messages: [
|
||||
{ role: "user", content: { type: "text", text: "Question" } },
|
||||
{ role: "assistant", content: { type: "text", text: "Answer" } },
|
||||
],
|
||||
}
|
||||
const result = formatMcpPromptResponse(response)
|
||||
expect(result).to.include("[User]\nQuestion")
|
||||
expect(result).to.include("[Assistant]\nAnswer")
|
||||
})
|
||||
|
||||
it("should format image content", () => {
|
||||
const response: McpPromptResponse = {
|
||||
messages: [{ role: "user", content: { type: "image", data: "base64data", mimeType: "image/png" } }],
|
||||
}
|
||||
const result = formatMcpPromptResponse(response)
|
||||
expect(result).to.equal("[User]\n[Image: image/png]")
|
||||
})
|
||||
|
||||
it("should format audio content", () => {
|
||||
const response: McpPromptResponse = {
|
||||
messages: [{ role: "user", content: { type: "audio", data: "base64data", mimeType: "audio/mp3" } }],
|
||||
}
|
||||
const result = formatMcpPromptResponse(response)
|
||||
expect(result).to.equal("[User]\n[Audio: audio/mp3]")
|
||||
})
|
||||
|
||||
it("should format resource with text", () => {
|
||||
const response: McpPromptResponse = {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "resource",
|
||||
resource: { uri: "file:///test.txt", text: "File content" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
const result = formatMcpPromptResponse(response)
|
||||
expect(result).to.include("[Resource: file:///test.txt]")
|
||||
expect(result).to.include("File content")
|
||||
})
|
||||
|
||||
it("should format resource without text", () => {
|
||||
const response: McpPromptResponse = {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: {
|
||||
type: "resource",
|
||||
resource: { uri: "file:///binary.bin" },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
const result = formatMcpPromptResponse(response)
|
||||
expect(result).to.equal("[User]\n[Resource: file:///binary.bin]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseSlashCommands MCP handling", () => {
|
||||
const mockMcpPromptFetcher: McpPromptFetcher = async (serverName, promptName) => {
|
||||
if (serverName === "test-server" && promptName === "greet") {
|
||||
return {
|
||||
description: "A greeting prompt",
|
||||
messages: [{ role: "user", content: { type: "text", text: "Hello from MCP!" } }],
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
it("should process MCP prompt command in task tag", async () => {
|
||||
const text = "<task>/mcp:test-server:greet</task>"
|
||||
const result = await parseSlashCommands(text, {}, {}, "test-ulid", undefined, false, undefined, mockMcpPromptFetcher)
|
||||
|
||||
expect(result.processedText).to.include('<mcp_prompt server="test-server" prompt="greet">')
|
||||
expect(result.processedText).to.include("Hello from MCP!")
|
||||
expect(result.needsClinerulesFileCheck).to.equal(false)
|
||||
})
|
||||
|
||||
it("should process MCP prompt with additional text", async () => {
|
||||
const text = "<task>/mcp:test-server:greet Please expand on this</task>"
|
||||
const result = await parseSlashCommands(text, {}, {}, "test-ulid", undefined, false, undefined, mockMcpPromptFetcher)
|
||||
|
||||
expect(result.processedText).to.include('<mcp_prompt server="test-server" prompt="greet">')
|
||||
expect(result.processedText).to.include("Please expand on this")
|
||||
})
|
||||
|
||||
it("should handle MCP prompt with colons in prompt name", async () => {
|
||||
const fetcherWithColons: McpPromptFetcher = async (serverName, promptName) => {
|
||||
if (serverName === "server" && promptName === "prompt:with:colons") {
|
||||
return {
|
||||
messages: [{ role: "user", content: { type: "text", text: "Colon prompt" } }],
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const text = "<task>/mcp:server:prompt:with:colons</task>"
|
||||
const result = await parseSlashCommands(text, {}, {}, "test-ulid", undefined, false, undefined, fetcherWithColons)
|
||||
|
||||
expect(result.processedText).to.include('prompt="prompt:with:colons"')
|
||||
expect(result.processedText).to.include("Colon prompt")
|
||||
})
|
||||
|
||||
// Note: Tests for "unknown MCP server", "no fetcher", and "fetcher errors"
|
||||
// are skipped because they require StateManager initialization when falling
|
||||
// through to workflow checking. The core MCP functionality is covered above.
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ApiProviderInfo } from "@core/api"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { McpPromptResponse } from "@shared/mcp"
|
||||
import fs from "fs/promises"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -15,6 +16,11 @@ import {
|
||||
} from "../prompts/commands"
|
||||
import { StateManager } from "../storage/StateManager"
|
||||
|
||||
/**
|
||||
* Callback type for fetching MCP prompts
|
||||
*/
|
||||
export type McpPromptFetcher = (serverName: string, promptName: string) => Promise<McpPromptResponse | null>
|
||||
|
||||
type FileBasedWorkflow = {
|
||||
fullPath: string
|
||||
fileName: string
|
||||
@@ -42,6 +48,7 @@ export async function parseSlashCommands(
|
||||
focusChainSettings?: { enabled: boolean },
|
||||
enableNativeToolCalls?: boolean,
|
||||
providerInfo?: ApiProviderInfo,
|
||||
mcpPromptFetcher?: McpPromptFetcher,
|
||||
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
|
||||
const SUPPORTED_DEFAULT_COMMANDS = [
|
||||
"newtask",
|
||||
@@ -79,10 +86,10 @@ export async function parseSlashCommands(
|
||||
// Regex to find slash commands anywhere in text (not just at the beginning).
|
||||
// This mirrors how @ mentions work - they can appear anywhere in a message.
|
||||
//
|
||||
// Pattern breakdown: /(^|\s)\/([a-zA-Z0-9_.-]+)(?=\s|$)/
|
||||
// Pattern breakdown: /(^|\s)\/([a-zA-Z0-9_.:@-]+)(?=\s|$)/
|
||||
// - (^|\s) : Must be at start of string OR preceded by whitespace
|
||||
// - \/ : The literal slash character
|
||||
// - ([a-zA-Z0-9_.-]+) : The command name (letters, numbers, underscore, dot, hyphen)
|
||||
// - ([a-zA-Z0-9_.:@-]+) : The command name (letters, numbers, underscore, dot, hyphen, colon, @)
|
||||
// - (?=\s|$): Must be followed by whitespace or end of string (lookahead)
|
||||
//
|
||||
// This safely avoids false matches in:
|
||||
@@ -91,7 +98,8 @@ export async function parseSlashCommands(
|
||||
// - Partial words: "foo/bar" - same reason
|
||||
//
|
||||
// Only ONE slash command per message is processed (first match found).
|
||||
const slashCommandInTextRegex = /(^|\s)\/([a-zA-Z0-9_.-]+)(?=\s|$)/
|
||||
// Note: Colons are allowed to support MCP prompt commands like /mcp:server:prompt
|
||||
const slashCommandInTextRegex = /(^|\s)\/([a-zA-Z0-9_.:@-]+)(?=\s|$)/
|
||||
|
||||
// Helper function to calculate positions and remove slash command from text
|
||||
const removeSlashCommand = (
|
||||
@@ -144,6 +152,39 @@ export async function parseSlashCommands(
|
||||
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" }
|
||||
}
|
||||
|
||||
// Check for MCP prompt commands (format: mcp:<server>:<prompt>)
|
||||
if (commandName.startsWith("mcp:") && mcpPromptFetcher) {
|
||||
const parts = commandName.split(":")
|
||||
if (parts.length >= 3) {
|
||||
const serverName = parts[1]
|
||||
const promptName = parts.slice(2).join(":") // Allow colons in prompt name
|
||||
|
||||
try {
|
||||
const promptResponse = await mcpPromptFetcher(serverName, promptName)
|
||||
if (promptResponse) {
|
||||
// Format the prompt messages as text
|
||||
const promptContent = formatMcpPromptResponse(promptResponse)
|
||||
|
||||
// Remove the slash command and add the prompt content
|
||||
const textWithoutSlashCommand = removeSlashCommand(text, tagContent, contentStartIndex, slashMatch)
|
||||
const processedText =
|
||||
`<mcp_prompt server="${serverName}" prompt="${promptName}">\n${promptContent}\n</mcp_prompt>\n` +
|
||||
textWithoutSlashCommand
|
||||
|
||||
// Track telemetry for MCP prompt usage
|
||||
telemetryService.captureSlashCommandUsed(ulid, commandName, "mcp_prompt")
|
||||
|
||||
return { processedText, needsClinerulesFileCheck: false }
|
||||
} else {
|
||||
// Prompt not found - log for debugging and fall through to workflow checking
|
||||
Logger.debug(`MCP prompt not found: ${commandName} (server: ${serverName}, prompt: ${promptName})`)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`Error fetching MCP prompt ${commandName}: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const globalWorkflows: Workflow[] = Object.entries(globalWorkflowToggles)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([filePath, _]) => ({
|
||||
@@ -214,3 +255,35 @@ export async function parseSlashCommands(
|
||||
// if no supported commands are found, return the original text
|
||||
return { processedText: text, needsClinerulesFileCheck: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats MCP prompt response messages into a text format for injection
|
||||
*/
|
||||
export function formatMcpPromptResponse(response: McpPromptResponse): string {
|
||||
const parts: string[] = []
|
||||
|
||||
if (response.description) {
|
||||
parts.push(`Description: ${response.description}`)
|
||||
}
|
||||
|
||||
for (const message of response.messages) {
|
||||
const roleLabel = message.role === "user" ? "User" : "Assistant"
|
||||
|
||||
if (message.content.type === "text") {
|
||||
parts.push(`[${roleLabel}]\n${message.content.text}`)
|
||||
} else if (message.content.type === "image") {
|
||||
parts.push(`[${roleLabel}]\n[Image: ${message.content.mimeType}]`)
|
||||
} else if (message.content.type === "audio") {
|
||||
parts.push(`[${roleLabel}]\n[Audio: ${message.content.mimeType}]`)
|
||||
} else if (message.content.type === "resource") {
|
||||
const resource = message.content.resource
|
||||
if (resource.text) {
|
||||
parts.push(`[${roleLabel}]\n[Resource: ${resource.uri}]\n${resource.text}`)
|
||||
} else {
|
||||
parts.push(`[${roleLabel}]\n[Resource: ${resource.uri}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Controller } from "@/core/controller"
|
||||
import { buildBasicClineHeaders } from "@/services/EnvUtils"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ConfiguredAPIKeys } from "@/shared/storage/state-keys"
|
||||
import { ClineEnv } from "../../../config"
|
||||
import { AuthService } from "../../../services/auth/AuthService"
|
||||
import { CLINE_API_ENDPOINT } from "../../../shared/cline/api"
|
||||
@@ -217,12 +218,14 @@ async function ensureUserInOrgWithRemoteConfig(controller: Controller): Promise<
|
||||
await controller.accountService.switchAccount(organizationId)
|
||||
}
|
||||
|
||||
const configuredApiKeys: ConfiguredAPIKeys = {}
|
||||
// Fetch and store API keys for configured providers
|
||||
const hasConfiguredProviders = config.providerSettings && Object.keys(config.providerSettings).length > 0
|
||||
if (hasConfiguredProviders) {
|
||||
const apiKeys = await fetchApiKeysForOrganization(organizationId)
|
||||
if (config.providerSettings?.LiteLLM) {
|
||||
if (apiKeys.litellm) {
|
||||
configuredApiKeys["litellm"] = true
|
||||
controller.stateManager.setSecret("remoteLiteLlmApiKey", apiKeys.litellm)
|
||||
} else {
|
||||
controller.stateManager.setSecret("remoteLiteLlmApiKey", undefined)
|
||||
@@ -237,7 +240,7 @@ async function ensureUserInOrgWithRemoteConfig(controller: Controller): Promise<
|
||||
// Cache and apply the remote config
|
||||
await writeRemoteConfigToCache(organizationId, config)
|
||||
if (isRemoteConfigEnabled(organizationId)) {
|
||||
await applyRemoteConfig(config, undefined, controller.mcpHub)
|
||||
await applyRemoteConfig(config, configuredApiKeys, controller.mcpHub)
|
||||
} else {
|
||||
clearRemoteConfig()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { synchronizeRemoteRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { RemoteConfig } from "@shared/remote-config/schema"
|
||||
import { GlobalStateAndSettings, RemoteConfigFields } from "@shared/storage/state-keys"
|
||||
import { ConfiguredAPIKeys, GlobalStateAndSettings, RemoteConfigFields } from "@shared/storage/state-keys"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { getDistinctId } from "@/services/logging/distinctId"
|
||||
import { getTelemetryService, telemetryService } from "@/services/telemetry"
|
||||
import { type McpHub } from "@/services/mcp/McpHub"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { OpenTelemetryClientProvider } from "@/services/telemetry/providers/opentelemetry/OpenTelemetryClientProvider"
|
||||
import { OpenTelemetryTelemetryProvider } from "@/services/telemetry/providers/opentelemetry/OpenTelemetryTelemetryProvider"
|
||||
import { type TelemetryService } from "@/services/telemetry/TelemetryService"
|
||||
@@ -266,17 +267,14 @@ export function clearRemoteConfig() {
|
||||
/**
|
||||
* Applies remote config to the StateManager's remote config cache
|
||||
* @param remoteConfig The remote configuration object to apply
|
||||
* @param settingsDirectoryPath Path to the settings directory
|
||||
* @param mcpHub Optional McpHub instance to prevent watcher triggers during sync
|
||||
* @param mcpHub McpHub instance to prevent watcher triggers during sync
|
||||
*/
|
||||
export async function applyRemoteConfig(
|
||||
remoteConfig?: RemoteConfig,
|
||||
settingsDirectoryPath?: string,
|
||||
mcpHub?: any,
|
||||
remoteConfig: RemoteConfig,
|
||||
configuredKeys: ConfiguredAPIKeys,
|
||||
mcpHub: McpHub,
|
||||
): Promise<void> {
|
||||
const stateManager = StateManager.get()
|
||||
const telemetryService = await getTelemetryService()
|
||||
|
||||
// If no remote config provided, clear the cache and relevant state
|
||||
if (!remoteConfig) {
|
||||
clearRemoteConfig()
|
||||
@@ -318,6 +316,7 @@ export async function applyRemoteConfig(
|
||||
for (const [key, value] of Object.entries(transformed)) {
|
||||
stateManager.setRemoteConfigField(key as keyof RemoteConfigFields, value)
|
||||
}
|
||||
stateManager.setRemoteConfigField("configuredApiKeys", configuredKeys)
|
||||
|
||||
// Restore previousRemoteMCPServers across cache clears
|
||||
if (previousRemoteMCPServers !== undefined) {
|
||||
@@ -330,7 +329,7 @@ export async function applyRemoteConfig(
|
||||
if (remoteConfig.remoteMCPServers !== undefined) {
|
||||
try {
|
||||
// Get settings directory path - use provided path or get it from disk helper
|
||||
const settingsPath = settingsDirectoryPath || (await ensureSettingsDirectoryExists())
|
||||
const settingsPath = await ensureSettingsDirectoryExists()
|
||||
await syncRemoteMcpServersToSettings(remoteConfig.remoteMCPServers, settingsPath, mcpHub)
|
||||
// Store current remote servers list for next sync to detect removals
|
||||
stateManager.setRemoteConfigField("previousRemoteMCPServers", remoteConfig.remoteMCPServers)
|
||||
|
||||
@@ -16,8 +16,6 @@ export class TaskState {
|
||||
userMessageContentReady = false
|
||||
// Map of tool names to their tool_use_id for creating proper ToolResultBlockParam
|
||||
toolUseIdMap: Map<string, string> = new Map()
|
||||
// Track tool calls that have already had errors pushed (prevents duplicates during streaming)
|
||||
errorPushedForCallIds: Set<string> = new Set()
|
||||
|
||||
// Presentation locks
|
||||
presentAssistantMessageLocked = false
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
import { beforeEach, describe, it } from "mocha"
|
||||
import "should"
|
||||
import { TaskState } from "../TaskState"
|
||||
|
||||
describe("TaskState", () => {
|
||||
describe("errorPushedForCallIds", () => {
|
||||
let taskState: TaskState
|
||||
|
||||
beforeEach(() => {
|
||||
taskState = new TaskState()
|
||||
})
|
||||
|
||||
it("should initialize as empty Set", () => {
|
||||
taskState.errorPushedForCallIds.should.be.instanceOf(Set)
|
||||
taskState.errorPushedForCallIds.size.should.equal(0)
|
||||
})
|
||||
|
||||
it("should track call_ids that have had errors pushed", () => {
|
||||
const callId = "call_abc123"
|
||||
|
||||
// Initially not present
|
||||
taskState.errorPushedForCallIds.has(callId).should.be.false()
|
||||
|
||||
// Add call_id
|
||||
taskState.errorPushedForCallIds.add(callId)
|
||||
|
||||
// Now present
|
||||
taskState.errorPushedForCallIds.has(callId).should.be.true()
|
||||
})
|
||||
|
||||
it("should track multiple call_ids independently", () => {
|
||||
const callId1 = "call_abc123"
|
||||
const callId2 = "call_def456"
|
||||
const callId3 = "call_ghi789"
|
||||
|
||||
// Add first two
|
||||
taskState.errorPushedForCallIds.add(callId1)
|
||||
taskState.errorPushedForCallIds.add(callId2)
|
||||
|
||||
// Check tracking
|
||||
taskState.errorPushedForCallIds.has(callId1).should.be.true()
|
||||
taskState.errorPushedForCallIds.has(callId2).should.be.true()
|
||||
taskState.errorPushedForCallIds.has(callId3).should.be.false()
|
||||
})
|
||||
|
||||
it("should clear all tracked call_ids", () => {
|
||||
// Add multiple call_ids
|
||||
taskState.errorPushedForCallIds.add("call_1")
|
||||
taskState.errorPushedForCallIds.add("call_2")
|
||||
taskState.errorPushedForCallIds.add("call_3")
|
||||
taskState.errorPushedForCallIds.size.should.equal(3)
|
||||
|
||||
// Clear
|
||||
taskState.errorPushedForCallIds.clear()
|
||||
|
||||
// Should be empty
|
||||
taskState.errorPushedForCallIds.size.should.equal(0)
|
||||
taskState.errorPushedForCallIds.has("call_1").should.be.false()
|
||||
})
|
||||
|
||||
it("should not add duplicate call_ids (Set behavior)", () => {
|
||||
const callId = "call_abc123"
|
||||
|
||||
taskState.errorPushedForCallIds.add(callId)
|
||||
taskState.errorPushedForCallIds.add(callId)
|
||||
taskState.errorPushedForCallIds.add(callId)
|
||||
|
||||
// Still only one entry
|
||||
taskState.errorPushedForCallIds.size.should.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("duplicate diff error prevention logic", () => {
|
||||
let taskState: TaskState
|
||||
|
||||
beforeEach(() => {
|
||||
taskState = new TaskState()
|
||||
})
|
||||
|
||||
/**
|
||||
* Simulates the duplicate check logic from WriteToFileToolHandler.
|
||||
* This tests the core logic that prevents duplicate error messages
|
||||
* when parallel tool calling is enabled.
|
||||
*/
|
||||
function shouldSkipDuplicateError(callId: string | undefined): boolean {
|
||||
const id = callId || ""
|
||||
if (id && taskState.errorPushedForCallIds.has(id)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function markErrorPushed(callId: string | undefined): void {
|
||||
const id = callId || ""
|
||||
if (id) {
|
||||
taskState.errorPushedForCallIds.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
it("should not skip first error for a call_id", () => {
|
||||
const callId = "call_abc123"
|
||||
|
||||
shouldSkipDuplicateError(callId).should.be.false()
|
||||
})
|
||||
|
||||
it("should skip subsequent errors for same call_id", () => {
|
||||
const callId = "call_abc123"
|
||||
|
||||
// First call - not skipped
|
||||
shouldSkipDuplicateError(callId).should.be.false()
|
||||
markErrorPushed(callId)
|
||||
|
||||
// Second call - should skip
|
||||
shouldSkipDuplicateError(callId).should.be.true()
|
||||
|
||||
// Third call - still skipped
|
||||
shouldSkipDuplicateError(callId).should.be.true()
|
||||
})
|
||||
|
||||
it("should allow different call_ids to each have their error", () => {
|
||||
const callId1 = "call_tool1"
|
||||
const callId2 = "call_tool2"
|
||||
|
||||
// First tool error
|
||||
shouldSkipDuplicateError(callId1).should.be.false()
|
||||
markErrorPushed(callId1)
|
||||
|
||||
// Second tool error (different call_id) - should NOT skip
|
||||
shouldSkipDuplicateError(callId2).should.be.false()
|
||||
markErrorPushed(callId2)
|
||||
|
||||
// But both should now skip on retry
|
||||
shouldSkipDuplicateError(callId1).should.be.true()
|
||||
shouldSkipDuplicateError(callId2).should.be.true()
|
||||
})
|
||||
|
||||
it("should not skip when call_id is empty (XML tools fallback)", () => {
|
||||
// Empty call_id (typical for XML-based tools)
|
||||
shouldSkipDuplicateError("").should.be.false()
|
||||
markErrorPushed("")
|
||||
|
||||
// Still doesn't skip because empty string is falsy
|
||||
shouldSkipDuplicateError("").should.be.false()
|
||||
})
|
||||
|
||||
it("should not skip when call_id is undefined", () => {
|
||||
shouldSkipDuplicateError(undefined).should.be.false()
|
||||
markErrorPushed(undefined)
|
||||
|
||||
// Still doesn't skip
|
||||
shouldSkipDuplicateError(undefined).should.be.false()
|
||||
})
|
||||
|
||||
it("should reset tracking between API requests (clear)", () => {
|
||||
const callId = "call_abc123"
|
||||
|
||||
// Mark error pushed
|
||||
markErrorPushed(callId)
|
||||
shouldSkipDuplicateError(callId).should.be.true()
|
||||
|
||||
// Simulate reset between API requests
|
||||
taskState.errorPushedForCallIds.clear()
|
||||
|
||||
// Same call_id should not skip after reset
|
||||
shouldSkipDuplicateError(callId).should.be.false()
|
||||
})
|
||||
|
||||
it("should handle rapid streaming chunks (same call_id repeated)", () => {
|
||||
const callId = "call_streaming"
|
||||
|
||||
// Simulate 238 streaming chunks (as seen in the bug)
|
||||
const results: boolean[] = []
|
||||
|
||||
for (let i = 0; i < 238; i++) {
|
||||
const shouldSkip = shouldSkipDuplicateError(callId)
|
||||
results.push(shouldSkip)
|
||||
|
||||
if (!shouldSkip) {
|
||||
markErrorPushed(callId)
|
||||
}
|
||||
}
|
||||
|
||||
// First should not skip, rest should skip
|
||||
results[0].should.be.false()
|
||||
results
|
||||
.slice(1)
|
||||
.every((r) => r)
|
||||
.should.be.true()
|
||||
|
||||
// Should have only added one entry
|
||||
taskState.errorPushedForCallIds.size.should.equal(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
+10
-1
@@ -2571,7 +2571,6 @@ export class Task {
|
||||
await this.diffViewProvider.reset()
|
||||
this.streamHandler.reset()
|
||||
this.taskState.toolUseIdMap.clear()
|
||||
this.taskState.errorPushedForCallIds.clear()
|
||||
|
||||
const { toolUseHandler, reasonsHandler } = this.streamHandler.getHandlers()
|
||||
const stream = this.attemptApiRequest(previousApiReqIndex) // yields only if the first chunk is successful, otherwise will allow the user to retry the request (most likely due to rate limit error, which gets thrown on the first chunk)
|
||||
@@ -3045,6 +3044,15 @@ export class Task {
|
||||
this.workspaceManager,
|
||||
)
|
||||
|
||||
// Create MCP prompt fetcher callback that wraps mcpHub.getPrompt
|
||||
const mcpPromptFetcher = async (serverName: string, promptName: string) => {
|
||||
try {
|
||||
return await this.mcpHub.getPrompt(serverName, promptName)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
|
||||
parsedText,
|
||||
localWorkflowToggles,
|
||||
@@ -3053,6 +3061,7 @@ export class Task {
|
||||
focusChainSettings,
|
||||
useNativeToolCalls,
|
||||
providerInfo,
|
||||
mcpPromptFetcher,
|
||||
)
|
||||
|
||||
if (needsCheck) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user