Compare commits

...

40 Commits

Author SHA1 Message Date
Arafatkatze 5d77a9a787 Send to cline provider 2026-01-27 18:28:13 -08:00
cline-test 92ebfce0cb feat(settings): Support linking to recommended or free model picker. 2026-01-27 18:01:44 -08:00
cline-test 8a5042a8bc release(3.55.0): Version bump and update WhatsNewModal 2026-01-27 17:31:11 -08:00
github-actions 726853d679 Updating CHANGELOG.md format 2026-01-27 23:52:40 +00:00
github-actions[bot] 0f52d80e55 changeset version bump 2026-01-27 23:52:38 +00:00
Ara 71af56f493 feat: add Arcee AI Trinity Large Preview to free models (#8897)
Add arcee-ai/trinity-large-preview:free as a new free model option:
- Add to onboarding models with 131k context window and score of 88
- Include in OpenRouterModelPicker free models list
- Update filter to preserve Trinity Large models like Minimax models
2026-01-27 15:51:39 -08:00
Juan Pablo Flores 1167b4f3a6 feat(rules): Conditional rules docs (#8874)
* docs(rules): Initial thoughts on docs for conditional rules.

* docs: restructure Cline Rules documentation into nested structure

Reorganize Cline Rules documentation by:
- Creating a "Cline Rules" group with overview and conditional-rules pages
- Moving conditional-rules.mdx into features/cline-rules/ subdirectory
- Adding URL redirects for backward compatibility
- Streamlining conditional-rules content for clarity and conciseness
- Adding cross-reference link to the overview page

This improves documentation navigation by grouping related rule concepts together and makes the content more accessible with clearer, more concise explanations.

* docs(cline-rules): consolidate rule file format documentation

Reorganize and expand the documentation for supported rule file formats:

- Add new "Supported Rule Files" section with comprehensive table
- Document cross-tool compatibility (Cursor, Windsurf, AGENTS.md)
- Clarify file priority and loading behavior
- Remove separate AGENTS.md section and integrate into unified table

This improves discoverability by showing all supported formats in one
place and makes it clearer how Cline works with rules from different AI
coding tools.

* docs(rules): remove context management note from overview

---------

Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
2026-01-27 14:17:29 -08:00
WangXiaolong e8d6370b0c feat(deepseek): add native tool calling support and reasoning_content handling (#7888)
* feat(deepseek): add native tool calling support and reasoning_content passback

- Add DeepSeek to isNextGenModelProvider list to enable native tool calling
- Add isDeepSeekModelFamily function for model identification
- Add addReasoningContent function for DeepSeek Reasoner's reasoning_content field
  - Pass back reasoning_content during tool calling within the same turn
  - Clear reasoning_content when starting a new conversation turn
- Compliant with DeepSeek API documentation for thinking mode with tool calling

* Update src/core/api/transform/r1-format.ts

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

* Update comments for user message handling logic

Clarify reasoning for handling user messages in comments.

* Update src/core/api/transform/r1-format.ts

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

* fix: format code for consistency in isNextGenModelFamily function

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
2026-01-27 13:24:20 -08:00
Toby White e243376a39 feat: add MCP prompts support (#8066)
* feat: add MCP prompts support

Implement support for MCP prompts as defined in the MCP spec (2025-06-18):
- Add McpPrompt and McpPromptArgument types to shared types
- Update proto definitions with prompt messages
- Update McpHub to fetch prompts list and get individual prompts
- Add prompts to system prompt component for AI awareness
- Add McpPromptRow UI component for displaying prompts
- Update ServerRow with Prompts tab showing available prompts
- Add slash command integration (/mcp:<server>:<prompt>)
- Update regex patterns to support colons in command names

MCP prompts are user-controlled templates that can be invoked via
slash commands to inject contextual messages into the conversation.

* style: alphabetize imports in mcp-server-conversion.ts

Reorder imports to follow project convention of alphabetical ordering.

* feat: add MCP prompts to slash command autocomplete

Wire up mcpServers to SlashCommandMenu so MCP prompt commands appear
in the autocomplete dropdown with their own "MCP Prompts" section.

* test: add unit tests for MCP prompt slash commands

- Add webview slash-commands.test.ts testing getMcpPromptCommands,
  getMatchingSlashCommands, and validateSlashCommand with MCP servers
- Add backend slash-commands tests for formatMcpPromptResponse and
  parseSlashCommands MCP handling
- Export formatMcpPromptResponse for testability
- Add "mcp_prompt" to telemetry captureSlashCommandUsed types

* test: update snapshots and fix backend tests for MCP prompts

- Update system prompt snapshots to include MCP prompts section
- Remove backend tests requiring StateManager initialization
  (tests for unknown server, no fetcher, fetcher errors)
- Core MCP prompt functionality is covered by remaining tests

* fix: change test status to valid 'connecting' value

* chore: remove commented debug line from prompts fetching

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use Logger instead of console.error for lint compliance

* fix: wire up mcpPromptFetcher callback to parseSlashCommands

The MCP prompt slash commands were not working because the
mcpPromptFetcher callback was never passed to parseSlashCommands.
This adds the callback that wraps mcpHub.getPrompt() to actually
fetch and inject prompt content when using /mcp:server:prompt.

* fix: resolve MCP prompts keyboard navigation and edge cases

- Add mcpServers param to keyboard handler's getMatchingSlashCommands calls
  to fix arrow key navigation and Enter/Tab selection for MCP prompts
- Add null check for connection.client in McpHub.getPrompt()
- Add debug logging when MCP prompt fetch returns null
- Fix regex in shouldShowSlashCommandsMenu to include colons for MCP format

* chore: add changeset for MCP prompts feature

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robin Newhouse <robin@cline.bot>
2026-01-27 13:12:02 -08:00
Tomás Barreiro 4f1be9d512 Replace POSTHOG_TELEMETRY_ENABLED with CLINE_TELEMETRY_DISABLED (#8818)
* Replace `POSTHOG_TELEMETRY_ENABLED` with `CLINE_TELEMETRY_DISABLED`

* Update cli/pkg/hostbridge/env.go

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-27 18:17:06 +01:00
Ara 94d36ce719 feat(ui): reduce font size for thinking content (#8892)
Add text-xs class to reasoning content in ThinkingRow component
for improved visual hierarchy and readability.
2026-01-27 08:28:39 -08:00
Bee 5df7498f03 refactor: simplify ThinkingRow expansion state management (#8735)
* refactor: simplify ThinkingRow expansion state management

Remove the responseStarted prop and complex logic that conditionally controlled ThinkingRow visibility during streaming. Simplify to allow ThinkingRow to remain expandable throughout the entire streaming lifecycle instead of forcing it expanded during reasoning and then collapsing after response starts.

Changes:
- Remove ApiReqState type and responseStarted tracking
- Eliminate showStreamingThinking and showCollapsedThinking logic
- Use consistent isExpanded state based only on user toggle
- Always show ThinkingRow title

* remove unused responseStarted

* feat(ui): update thinking UI with improved expand/collapse controls

Changes:
- Replace "Thinking..." with "Working..." status text in non-plan mode
- Switch from ChevronRight to ChevronUp/Down icons for better UX
- Redesign thinking section header with cleaner layout
- Remove preview text when collapsed, show only "Thinking" label
- Add consistent border styling to thinking content
- Implement per-tool thinking expand/collapse state management
- Update icon sizing and styling for better visual consistency

This improves the user experience by making the thinking/reasoning sections more intuitive to expand and collapse, with clearer visual indicators and a more polished appearance.

* add blur

* feat: chevron fix, reasoning change, slight style change

* feat: spacing issues

* keep thinking row expanded during stream

* Reasoning -> Thoughts

* feat: Inline reading of files vs having reading then read list items seperately

* feat: remove extra reading state

* feat: removed reasoning from file expandable file state

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2026-01-27 07:53:03 -08:00
Juan Pablo Flores c2b87252ac Fixes Cannot restore checkpoint more than once #8866 (#8873) 2026-01-27 07:46:43 -08:00
github-actions[bot] be353bb3da v3.54.0 Release Notes (#8840)
- Native tool calls support for Ollama provider
- Sonnet 4.5 is now the default Amazon Bedrock model id

- 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.

- Removed Devstral-2512 free from the free models list
- Removed deprecated zai-glm-4.6 model from Cerebras provider

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-27 07:44:49 -08:00
Ara 0a7791de1f feat: remove Mistral Devstral 2512 from free models list (#8889)
Remove mistralai/devstral-2512:free from:
- Onboarding models configuration
- Free models picker in settings
- OpenRouter model filter exception list

The Devstral model is no longer included as a free tier option.
2026-01-27 07:16:55 -08:00
Robin Newhouse 7cca102e14 fix: apply_patch tool now works with OCA provider's gpt5 model ID (#8875) 2026-01-26 22:54:21 -08:00
Bee 4f591de6a9 feat: Adds support for native tool calls to Ollama provider (#8871)
* feat: add support for tool calls in Ollama API

Enhanced OllamaHandler to support tool calls by adding a 'tools' parameter to createMessage. Implements processing of tool call deltas using ToolCallProcessor, enabling handling of function calls made by the model. Added necessary imports for ChatCompletionTool and ToolCallProcessor types.

* add changeset
2026-01-26 17:50:52 -08:00
Tomás Barreiro 60436b3ddf Do not call feature_flag_called event if the value hasn't changed (#8867)
* Do not call feature_flag_called event if the value hasn't changed

* Send the feature flag called on startup
2026-01-26 10:51:33 -08:00
Saoud Rizwan df5954052d feat: disable extended thinking by default (#8863) 2026-01-26 10:21:21 -08:00
Igor Tceglevskii a66a2784c3 feat: disable PostHog services in self-hosted mode (#8842)
- Skip PostHog client initialization when running in self-hosted mode
- Return no-op config from ErrorProviderFactory and FeatureFlagsProviderFactory
- Add comprehensive tests for self-hosted mode PostHog disabling behavior

This ensures no telemetry or analytics data is sent when users run
the extension in a self-hosted environment.
2026-01-26 07:51:02 -08:00
Bee 47031cea25 feat: add debugLog RPC for host bridge logging (#8841)
* feat: add appendOutputLog RPC for host bridge logging

Add new appendOutputLog RPC endpoint to EnvService proto definition
and refactor VSCode output channel creation to use a dedicated factory
function. This enables structured logging through the host bridge
service instead of direct Logger calls.

* rename appendOutputLog to debugLog and add subscriber pattern

- Rename `appendOutputLog` RPC to `debugLog` with documentation
- Refactor Logger to use subscriber pattern instead of single output
- Update HostProvider to use env.debugLog directly for logging
- Remove redundant logger callback from setupHostProvider

* feat: add multi-subscriber support for Logger output

- Rename Logger.setOutput to Logger.subscribe to better reflect behavior
- Subscribe both output channel and debug logger to receive log messages
- Enable logging to multiple destinations simultaneously

* update mock
2026-01-23 18:18:24 -08:00
Robin Newhouse e29740479e fix: skip diff error UI handling during streaming to prevent flickering (#8788)
* fix: skip diff error UI handling during streaming to prevent flickering

During streaming, handlePartialBlock is called repeatedly, and if the diff
application fails (e.g., search string not found), all the error handling code
was running on every chunk. This caused:
- consecutiveMistakeCount to rapidly increment
- diff_error messages to be added/removed repeatedly
- revertChanges/reset to be called repeatedly
- rapid flickering of the diff viewer

Now we return early from the catch block when block.partial is true, skipping
all error UI handling. The error is only processed once on the final block.

* chore: add changeset for diff error suppression

* test: add unit tests for partial block streaming behavior

Adds tests verifying that error handling is skipped during streaming
(block.partial=true) to prevent counter rapid increment and UI flickering.

* chore: remove unused errorPushedForCallIds tracking

This mechanism was replaced by the simpler block.partial check for
skipping error handling during streaming. Remove the dead code.
2026-01-23 16:55:32 -08:00
Ara 74f607ff8e chore(release): bump version to 3.53.1 (#8839)
- Fix bug in responses API
- Update changeset package name from "cline" to "claude-dev"
- Update version in package.json and package-lock.json
2026-01-23 15:46:59 -08:00
Robin Newhouse 0edf6d777b fix: prevent infinite retry loops when replace_in_file fails repeatedly (#8787)
* fix: prevent infinite retry loops when replace_in_file fails repeatedly

The consecutiveMistakeCount was being reset to 0 at the START of each
WriteToFileToolHandler execution, before the tooManyMistakes check could
see accumulated failures. This allowed the model to retry failing
replace_in_file operations indefinitely, causing context explosion.

Changes:
- Move counter reset from before operation to after successful saveChanges()
- Add consecutiveMistakeCount++ in the diff error catch block
- Fix typo: "his thought process" → "Cline's thought process"

* chore: add changeset for retry loop prevention

* test: add unit tests for consecutiveMistakeCount behavior

Verify the fix for infinite retry loops by testing that:
- Counter is NOT reset at the start of operations
- Counter IS reset only after successful saveChanges()
- Counter IS incremented on diff errors
- Repeated failures accumulate so tooManyMistakes can trigger
2026-01-23 15:37:01 -08:00
Robin Newhouse de630c64d4 fix: throttle diff view updates during streaming (#8785)
* fix: throttle diff view updates during streaming

Skip redundant rapid updates to reduce performance issues in large
streams (e.g., notebooks) and reset throttle state on cleanup.

* chore: add changeset for diff throttling fix

* test: add unit tests for diff view update throttling

Add comprehensive tests for the throttling behavior introduced in the
streaming diff updates fix. Tests cover empty content, unchanged content,
time-based throttling, final update bypass, and state reset.
2026-01-23 15:36:47 -08:00
Bee 3ff63562c8 chore: migrate host logging to shared Logger service (#8820)
* chore: migrate host logging to shared Logger service

- Replace HostProvider.logToChannel usage with Logger.log/error
  in controller, webview, and checkpoint migration code
- Remove redundant, low-value log statements from Cline API
  methods to reduce noise
- Centralize logging through shared Logger service for more
  consistent, structured logging and easier maintenance
- Remove redundant , low-value log statements from StateManager where
  we logged error that would be throw and get logged again

* Update src/integrations/checkpoints/CheckpointMigration.ts

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

* fix

* update tests

* update tests

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-23 15:27:11 -08:00
Saoud Rizwan 4d6f908fbd fix: add null check when filtering tools by type in Responses API providers (#8837)
Users reported seeing this error with the OpenAI Codex provider:
{"message":"Cannot read properties of undefined (reading 'type')","modelId":"gpt-5.2-codex"}

The issue occurs when filtering tools before sending to the Responses API.
The filter accessed .type without checking if the tool element was defined:

  tools.filter((tool) => tool.type === "function")

If the tools array contains any undefined elements, this throws. Fixed by
adding optional chaining:

  tools.filter((tool) => tool?.type === "function")

Applied the same fix to all three providers using the Responses API:
- openai-codex.ts (ChatGPT Plus/Pro subscriptions)
- openai-native.ts (OpenAI API with Responses format)
- oca.ts (OpenAI-compatible API with Responses format)
2026-01-23 14:48:51 -08:00
Igor Tceglevskii 6521fdcc94 disable telemetry for self-hosted environments (#8790) 2026-01-23 14:16:50 -08:00
Igor Tceglevskii 1393eace27 Endpoint configuration file (#8645) 2026-01-23 13:40:31 -08:00
github-actions[bot] eebb99c1e3 Changeset version bump (#8800)
* changeset version bump

* Updating CHANGELOG.md format

* update changelog and banner for release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-23 13:12:29 -08:00
Ara 9ed44f7a83 feat(cerebras): use model-specific temperature configuration (#8833)
- Extract model retrieval to avoid multiple function calls
- Use temperature from model.info with fallback to 0 instead of hardcoded value
- Allows temperature to be configured per model rather than using a fixed value

This change enables more flexible temperature configuration for different Cerebras models while maintaining backward compatibility with a default value of 0.
2026-01-23 12:50:57 -08:00
Ara 6a95cc5f19 feat: add default temperature to Cerebras model configuration (#8832)
Set default temperature value of 0.9 for Cerebras model in the model
configuration. This establishes a consistent default sampling temperature
for the model's response generation behavior.
2026-01-23 12:42:26 -08:00
er-ri 1d0637f39c fix: add support for haiku 4.5 in JP_SUPPORTED_CRIS_MODELS and enable global endpoint support (#8298) 2026-01-23 11:46:02 -08:00
AJ Juaire f2c16bae7a Make the default bedrock model Sonnet 4.5 (#8830) 2026-01-23 11:45:49 -08:00
Ara 204f15ce1c Remove free period on grok (#8831) 2026-01-23 11:27:42 -08:00
Robin Newhouse 8118e11596 fix(extract-text): strip notebook outputs to reduce context size (#8784)
* fix(extract-text): strip notebook outputs to reduce context size

* chore: add changeset for notebook outputs fix
2026-01-22 17:50:31 -08:00
Bee f7b593df35 chore: remove noisy log when checking file outside workspace (#8814)
* chore: remove noisy log when checking file outside workspace

Removes a `Logger.error` call in `ifFileExistsRelativePath` that triggered whenever a file path was checked without an active workspace. This log was creating excessive noise during long conversations where many files were mentioned but no workspace was open.

* update test
2026-01-22 17:01:22 -08:00
Saoud Rizwan 2e0358a7a1 fix: disable browser tool by default (#8815)
The browser tool conflicts with the new websearch tool. Disabling it by
default provides a better out-of-box experience.
2026-01-22 16:58:29 -08:00
Bee 0fbc10f807 chore: remove unhelpful and noisy log statements - part 1 (#8813)
* chore: remove unhelpful and noisy log statements - part 1

Removes excessive debug and info logs across several services to reduce console noise, specifically:
- Deletes `[DEBUG]` logs for request registration, subscription setup/cleanup, and event dispatching in the gRPC controller and UI handlers.
- Removes verbose file cleanup logs in `ClineTempManager` and process termination logs in `AudioRecordingService`.
- Simplifies the success log in `refreshOpenRouterModels` by removing the large JSON payload dump.
- Upgrades the log level from `debug` to `error` for request cleanup failures in `GrpcRequestRegistry` to ensure exceptions are properly highlighted.

* removes subscription logs
2026-01-22 16:44:05 -08:00
Bee c093ca1760 refactor: replace console with Logger service (#8741)
* chore: add grit rule to enforce Logger service over console calls

Add a new Grit linting rule that detects direct console method usage
(log, debug, error, warn, info) and prompts developers to use the
Logger service instead for consistent logging practices.

The rule is configured in biome.jsonc to apply to most source files
while excluding test files, webview-ui, evals, standalone, e2e tests,
and scripts where direct console usage may be acceptable.

* support variadic args

* wip: migrate console to Logger

* migrate rest of console logger

* Switch to Logger

* Migrations

* shared

* use shared

* revert format change

* Update tests to stub Logger instead of console

* verbose in dev mode
2026-01-22 13:16:37 -08:00
404 changed files with 5690 additions and 2012 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
improve cline command permission validation logic. add cline command permission man page documentation
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Remove deprecated zai-glm-4.6 model from Cerebras provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Wiring-up "conditionals" for Cline Rules files.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix drag & drop files from SSH remote workspaces into chat
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Updated switch component to adhere to 3:1 contrast accessibility standard for both light and dark vscode themes
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Remove hooks feature setting and make it always enabled (except on Windows).
-7
View File
@@ -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)
# ============================================================================
-1
View File
@@ -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)
-1
View File
@@ -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)
+39 -2
View File
@@ -1,18 +1,55 @@
# 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
## [3.53.0]
### Fixed
- Removed grok model from free tier
## [3.52.0]
### Added
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
- Grok models are now moving out of free tier and into paid plans.
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
### Fixed
- Bugs in DiffViewProvider for file editing
- Ollama's recommended models to use correct identifiers
## [3.51.0]
### Added
+23
View File
@@ -147,6 +147,29 @@
"src/dev/grit/vscode-api.grit"
]
},
{
// Do not use console logging directly, use the Logger service instead.
"plugins": [
"src/dev/grit/console-log.grit"
],
"includes": [
"**",
"!**/webview-ui/**",
"!**/evals/**",
"!**/standalone/**",
"!**/e2e/**",
"!**/test/**",
"!**/__tests__/**",
"!**/*.test.ts",
"!**/*.stories.ts",
"!src/dev/**",
"!**/*.mjs",
"!**/*.js",
"!**/scripts/**",
"!**/*.tsx",
"!**/testing-platform/**"
]
},
{
"includes": [
"**",
+7 -4
View File
@@ -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
View File
@@ -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": {
-188
View File
@@ -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
+205
View File
@@ -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
-4
View File
@@ -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) {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.52.0",
"version": "3.55.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.52.0",
"version": "3.55.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.52.0",
"version": "3.55.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+14
View File
@@ -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 {
+2 -4
View File
@@ -106,11 +106,8 @@ message Secrets {
// in src/shared/storage/state-keys.ts and use the scripts/generate-state-proto.mjs
// script to regenerate this list.
message Settings {
reserved 152; // was hooks_enabled (removed - now always enabled on macOS/Linux)
optional string lite_llm_base_url = 1;
optional bool lite_llm_use_prompt_cache = 2;
map<string, string> open_ai_headers = 3;
optional string anthropic_base_url = 4;
optional string open_router_provider_sorting = 5;
optional string aws_region = 6;
@@ -279,6 +276,7 @@ message Settings {
optional int32 open_telemetry_log_batch_timeout = 170;
optional int32 open_telemetry_log_max_queue_size = 171;
optional bool worktrees_enabled = 172;
map<string, string> open_ai_headers = 173;
}
message DictationSettings {
@@ -385,7 +383,7 @@ message UpdateTaskSettingsRequest {
// Message for updating settings
message UpdateSettingsRequest {
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
Metadata metadata = 1;
optional ModelsApiConfiguration api_configuration = 2;
+3
View File
@@ -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 {
+542
View File
@@ -0,0 +1,542 @@
import { afterEach, beforeEach, describe, it } from "mocha"
import "should"
import fs from "fs/promises"
import os from "os"
import path from "path"
import sinon from "sinon"
import { ClineConfigurationError, ClineEndpoint, ClineEnv, Environment } from "../config"
describe("ClineEndpoint configuration", () => {
let sandbox: sinon.SinonSandbox
let tempDir: string
let originalHomedir: typeof os.homedir
beforeEach(async () => {
sandbox = sinon.createSandbox()
tempDir = path.join(os.tmpdir(), `config-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
await fs.mkdir(tempDir, { recursive: true })
// Create .cline directory
await fs.mkdir(path.join(tempDir, ".cline"), { recursive: true })
// Stub os.homedir to return our temp directory
originalHomedir = os.homedir
sandbox
.stub(os, "homedir")
.returns(tempDir)
// Reset the singleton state using internal method
;(ClineEndpoint as any)._instance = null
;(ClineEndpoint as any)._initialized = false
})
afterEach(async () => {
sandbox.restore()
// Reset singleton state
;(ClineEndpoint as any)._instance = null
;(ClineEndpoint as any)._initialized = false
try {
await fs.rm(tempDir, { recursive: true, force: true })
} catch {
// Ignore cleanup errors
}
})
describe("valid config parsing", () => {
it("should parse valid endpoints.json with all required fields", async () => {
const validConfig = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.appBaseUrl.should.equal("https://app.enterprise.com")
config.apiBaseUrl.should.equal("https://api.enterprise.com")
config.mcpBaseUrl.should.equal("https://mcp.enterprise.com")
config.environment.should.equal(Environment.selfHosted)
})
it("should work without endpoints.json (standard mode)", async () => {
// No endpoints.json file exists
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.environment.should.not.equal(Environment.selfHosted)
// Should use production defaults
config.appBaseUrl.should.equal("https://app.cline.bot")
config.apiBaseUrl.should.equal("https://api.cline.bot")
})
it("should accept URLs with ports", async () => {
const validConfig = {
appBaseUrl: "http://localhost:3000",
apiBaseUrl: "http://localhost:7777",
mcpBaseUrl: "http://localhost:8080/mcp",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.appBaseUrl.should.equal("http://localhost:3000")
config.apiBaseUrl.should.equal("http://localhost:7777")
config.mcpBaseUrl.should.equal("http://localhost:8080/mcp")
})
it("should accept URLs with paths", async () => {
const validConfig = {
appBaseUrl: "https://proxy.enterprise.com/cline/app",
apiBaseUrl: "https://proxy.enterprise.com/cline/api",
mcpBaseUrl: "https://proxy.enterprise.com/cline/mcp",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.appBaseUrl.should.equal("https://proxy.enterprise.com/cline/app")
})
})
describe("invalid JSON handling", () => {
it("should throw ClineConfigurationError for invalid JSON syntax", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{ invalid json }", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("Invalid JSON")
}
})
it("should throw ClineConfigurationError for truncated JSON", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '{"appBaseUrl": "https://test.com"', "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("Invalid JSON")
}
})
it("should throw ClineConfigurationError for empty file", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
}
})
it("should throw ClineConfigurationError for non-object JSON", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '"just a string"', "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must contain a JSON object")
}
})
it("should throw ClineConfigurationError for array JSON", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "[]", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
// Arrays pass the object check but fail on required fields
error.message.should.containEql("Missing required field")
}
})
it("should throw ClineConfigurationError for null JSON", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "null", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must contain a JSON object")
}
})
})
describe("missing required fields", () => {
it("should throw ClineConfigurationError when appBaseUrl is missing", async () => {
const config = {
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql('Missing required field "appBaseUrl"')
}
})
it("should throw ClineConfigurationError when apiBaseUrl is missing", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql('Missing required field "apiBaseUrl"')
}
})
it("should throw ClineConfigurationError when mcpBaseUrl is missing", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql('Missing required field "mcpBaseUrl"')
}
})
it("should throw ClineConfigurationError when all fields are missing", async () => {
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{}", "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("Missing required field")
}
})
it("should throw ClineConfigurationError when field is null", async () => {
const config = {
appBaseUrl: null,
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql('Missing required field "appBaseUrl"')
}
})
it("should throw ClineConfigurationError when field is empty string", async () => {
const config = {
appBaseUrl: "",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("cannot be empty")
}
})
it("should throw ClineConfigurationError when field is whitespace only", async () => {
const config = {
appBaseUrl: " ",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("cannot be empty")
}
})
it("should throw ClineConfigurationError when field is non-string", async () => {
const config = {
appBaseUrl: 12345,
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must be a string")
}
})
})
describe("invalid URL detection", () => {
it("should throw ClineConfigurationError for invalid URL format", async () => {
const config = {
appBaseUrl: "not-a-valid-url",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must be a valid URL")
}
})
it("should throw ClineConfigurationError for URL without protocol", async () => {
const config = {
appBaseUrl: "app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must be a valid URL")
}
})
it("should throw ClineConfigurationError for malformed URL", async () => {
const config = {
appBaseUrl: "https://",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql("must be a valid URL")
}
})
it("should include the invalid URL value in error message", async () => {
const invalidUrl = "definitely-not-a-url"
const config = {
appBaseUrl: invalidUrl,
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
try {
await ClineEndpoint.initialize()
throw new Error("Should have thrown")
} catch (error: any) {
error.should.be.instanceof(ClineConfigurationError)
error.message.should.containEql(invalidUrl)
}
})
})
describe("environment switching blocked in self-hosted mode", () => {
it("should throw error when trying to change environment in self-hosted mode", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
await ClineEndpoint.initialize()
// Verify we're in self-hosted mode
ClineEndpoint.config.environment.should.equal(Environment.selfHosted)
// Try to change environment - should throw
try {
ClineEnv.setEnvironment("staging")
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.containEql("Cannot change environment in on-premise mode")
}
})
it("should throw error for all environment values in self-hosted mode", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
await ClineEndpoint.initialize()
const environments = ["staging", "local", "production", "anything"]
for (const env of environments) {
try {
ClineEnv.setEnvironment(env)
throw new Error(`Should have thrown for environment: ${env}`)
} catch (error: any) {
error.message.should.containEql("Cannot change environment in on-premise mode")
}
}
})
it("should allow environment switching in standard mode", async () => {
// No endpoints.json file - standard mode
await ClineEndpoint.initialize()
// Verify we're NOT in self-hosted mode
ClineEndpoint.config.environment.should.not.equal(Environment.selfHosted)
// Should be able to change environment
ClineEnv.setEnvironment("staging")
ClineEnv.getEnvironment().environment.should.equal("staging")
ClineEnv.setEnvironment("local")
ClineEnv.getEnvironment().environment.should.equal("local")
ClineEnv.setEnvironment("production")
ClineEnv.getEnvironment().environment.should.equal("production")
})
})
describe("self-hosted mode behavior", () => {
it("should report selfHosted environment in self-hosted mode", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
await ClineEndpoint.initialize()
const envConfig = ClineEndpoint.config
envConfig.environment.should.equal(Environment.selfHosted)
})
it("should use custom endpoints from file", async () => {
const customConfig = {
appBaseUrl: "https://custom-app.internal",
apiBaseUrl: "https://custom-api.internal",
mcpBaseUrl: "https://custom-mcp.internal/v1",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(customConfig), "utf8")
await ClineEndpoint.initialize()
const config = ClineEndpoint.config
config.appBaseUrl.should.equal("https://custom-app.internal")
config.apiBaseUrl.should.equal("https://custom-api.internal")
config.mcpBaseUrl.should.equal("https://custom-mcp.internal/v1")
})
})
describe("initialization behavior", () => {
it("should only initialize once", async () => {
await ClineEndpoint.initialize()
ClineEndpoint.isInitialized().should.be.true()
// Second initialize should be a no-op
await ClineEndpoint.initialize()
ClineEndpoint.isInitialized().should.be.true()
})
it("should throw error when accessing config before initialization", async () => {
// Already reset in beforeEach, so accessing should throw
try {
const _ = ClineEndpoint.config
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.containEql("not initialized")
}
})
})
describe("isSelfHosted() method", () => {
it("should return true when not initialized (safety fallback)", async () => {
// Reset singleton state - already done in beforeEach, not initialized
ClineEndpoint.isInitialized().should.be.false()
ClineEndpoint.isSelfHosted().should.be.true()
})
it("should return true when in self-hosted mode", async () => {
const config = {
appBaseUrl: "https://app.enterprise.com",
apiBaseUrl: "https://api.enterprise.com",
mcpBaseUrl: "https://mcp.enterprise.com",
}
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
await ClineEndpoint.initialize()
ClineEndpoint.isSelfHosted().should.be.true()
})
it("should return false when in normal mode (no endpoints.json)", async () => {
// No endpoints.json file exists
await ClineEndpoint.initialize()
ClineEndpoint.isSelfHosted().should.be.false()
})
})
})
+19 -5
View File
@@ -7,10 +7,10 @@ import {
migrateWorkspaceToGlobalStorage,
} from "./core/storage/state-migrations"
import { WebviewProvider } from "./core/webview"
import { Logger } from "./services/logging/Logger"
import "./utils/path" // necessary to have access to String.prototype.toPosix
import { HostProvider } from "@/hosts/host-provider"
import { Logger } from "@/shared/services/Logger"
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
import { StateManager } from "./core/storage/StateManager"
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
@@ -27,17 +27,29 @@ import { syncWorker } from "./shared/services/worker/sync"
import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
import { getLatestAnnouncementId } from "./utils/announcements"
import { arePathsEqual } from "./utils/path"
/**
* Performs intialization for Cline that is common to all platforms.
*
* @param context
* @returns The webview provider
* @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 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()
// Throws ClineConfigurationError if config file exists but is invalid
const { ClineEndpoint } = await import("./config")
await ClineEndpoint.initialize()
try {
await StateManager.initialize(context)
} catch (error) {
console.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
Logger.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to initialize Cline's application state. Please restart the extension.",
@@ -50,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()
@@ -123,7 +137,7 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
Logger.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
}
}
+227 -14
View File
@@ -1,22 +1,38 @@
export enum Environment {
production = "production",
staging = "staging",
local = "local",
}
import * as fs from "fs/promises"
import * as os from "os"
import * as path from "path"
import { Environment, type EnvironmentConfig } from "./shared/config-types"
import { Logger } from "./shared/services/Logger"
export interface EnvironmentConfig {
environment: Environment
export { Environment, type EnvironmentConfig }
/**
* Schema for the endpoints.json configuration file used in on-premise deployments.
* All fields are required and must be valid URLs.
*/
interface EndpointsFileSchema {
appBaseUrl: string
apiBaseUrl: string
mcpBaseUrl: string
}
class ClineEndpoint {
public static instance = new ClineEndpoint()
public static get config() {
return ClineEndpoint.instance.config()
/**
* Error thrown when the Cline configuration file exists but is invalid.
* This error prevents Cline from starting to avoid misconfiguration in enterprise environments.
*/
export class ClineConfigurationError extends Error {
constructor(message: string) {
super(message)
this.name = "ClineConfigurationError"
}
}
class ClineEndpoint {
private static _instance: ClineEndpoint | null = null
private static _initialized = false
// On-premise config loaded from file (null if not on-premise)
private onPremiseConfig: EndpointsFileSchema | null = null
private environment: Environment = Environment.production
private constructor() {
@@ -24,15 +40,189 @@ class ClineEndpoint {
const _env = process?.env?.CLINE_ENVIRONMENT_OVERRIDE || process?.env?.CLINE_ENVIRONMENT
if (_env && Object.values(Environment).includes(_env as Environment)) {
this.environment = _env as Environment
return
}
}
/**
* Initializes the ClineEndpoint singleton.
* Must be called before any other methods.
* Reads the endpoints.json file if it exists and validates its schema.
*
* @throws ClineConfigurationError if the endpoints.json file exists but is invalid
*/
public static async initialize(): Promise<void> {
if (ClineEndpoint._initialized) {
return
}
ClineEndpoint._instance = new ClineEndpoint()
// Try to load on-premise config from file
const endpointsConfig = await ClineEndpoint.loadEndpointsFile()
if (endpointsConfig) {
ClineEndpoint._instance.onPremiseConfig = endpointsConfig
Logger.log("Cline running in self-hosted mode with custom endpoints")
}
ClineEndpoint._initialized = true
}
/**
* Returns true if the ClineEndpoint has been initialized.
*/
public static isInitialized(): boolean {
return ClineEndpoint._initialized
}
/**
* Checks if Cline is running in self-hosted/on-premise mode.
* @returns true if in selfHosted mode, or true if not initialized (safety fallback to prevent accidental external calls)
*/
public static isSelfHosted(): boolean {
// Safety fallback: if not initialized, treat as selfHosted
// to prevent accidental external service calls before configuration is loaded
if (!ClineEndpoint._initialized) {
return true
}
return ClineEndpoint.config.environment === Environment.selfHosted
}
/**
* Returns the singleton instance.
* @throws Error if not initialized
*/
public static get instance(): ClineEndpoint {
if (!ClineEndpoint._initialized || !ClineEndpoint._instance) {
throw new Error("ClineEndpoint not initialized. Call ClineEndpoint.initialize() first.")
}
return ClineEndpoint._instance
}
/**
* Static getter for convenient access to the current configuration.
* @throws Error if not initialized
*/
public static get config(): EnvironmentConfig {
return ClineEndpoint.instance.config()
}
/**
* Returns the path to the endpoints.json configuration file.
* Located at ~/.cline/endpoints.json
*/
private static getEndpointsFilePath(): string {
return path.join(os.homedir(), ".cline", "endpoints.json")
}
/**
* Loads and validates the endpoints.json file.
* @returns The validated endpoints config, or null if the file doesn't exist
* @throws ClineConfigurationError if the file exists but is invalid
*/
private static async loadEndpointsFile(): Promise<EndpointsFileSchema | null> {
const filePath = ClineEndpoint.getEndpointsFilePath()
try {
await fs.access(filePath)
} catch {
// File doesn't exist - not on-premise mode
return null
}
// File exists, must be valid or we fail
try {
const fileContent = await fs.readFile(filePath, "utf8")
let data: unknown
try {
data = JSON.parse(fileContent)
} catch (parseError) {
throw new ClineConfigurationError(
`Invalid JSON in endpoints configuration file (${filePath}): ${parseError instanceof Error ? parseError.message : String(parseError)}`,
)
}
return ClineEndpoint.validateEndpointsSchema(data, filePath)
} catch (error) {
if (error instanceof ClineConfigurationError) {
throw error
}
throw new ClineConfigurationError(
`Failed to read endpoints configuration file (${filePath}): ${error instanceof Error ? error.message : String(error)}`,
)
}
}
/**
* Validates that the provided data matches the EndpointsFileSchema.
* All fields must be present and be valid URLs.
*
* @param data The parsed JSON data to validate
* @param filePath The path to the file (for error messages)
* @returns The validated EndpointsFileSchema
* @throws ClineConfigurationError if validation fails
*/
private static validateEndpointsSchema(data: unknown, filePath: string): EndpointsFileSchema {
if (typeof data !== "object" || data === null) {
throw new ClineConfigurationError(`Endpoints configuration file (${filePath}) must contain a JSON object`)
}
const obj = data as Record<string, unknown>
const requiredFields = ["appBaseUrl", "apiBaseUrl", "mcpBaseUrl"] as const
const result: Partial<EndpointsFileSchema> = {}
for (const field of requiredFields) {
const value = obj[field]
if (value === undefined || value === null) {
throw new ClineConfigurationError(
`Missing required field "${field}" in endpoints configuration file (${filePath})`,
)
}
if (typeof value !== "string") {
throw new ClineConfigurationError(
`Field "${field}" in endpoints configuration file (${filePath}) must be a string`,
)
}
if (!value.trim()) {
throw new ClineConfigurationError(
`Field "${field}" in endpoints configuration file (${filePath}) cannot be empty`,
)
}
// Validate URL format
try {
new URL(value)
} catch {
throw new ClineConfigurationError(
`Field "${field}" in endpoints configuration file (${filePath}) must be a valid URL. Got: "${value}"`,
)
}
result[field] = value
}
return result as EndpointsFileSchema
}
/**
* Returns the current environment configuration.
*/
public config(): EnvironmentConfig {
return this.getEnvironment()
}
/**
* Sets the current environment.
* @throws Error if in on-premise mode (environment switching is disabled)
*/
public setEnvironment(env: string) {
if (this.onPremiseConfig) {
throw new Error("Cannot change environment in on-premise mode. Endpoints are configured via ~/.cline/endpoints.json")
}
switch (env.toLowerCase()) {
case "staging":
this.environment = Environment.staging
@@ -44,10 +234,24 @@ class ClineEndpoint {
this.environment = Environment.production
break
}
console.info("Cline environment updated: ", this.environment)
}
/**
* Returns the current environment configuration.
* If running in on-premise mode, returns the custom endpoints.
*/
public getEnvironment(): EnvironmentConfig {
// On-premise mode: use custom endpoints from file
if (this.onPremiseConfig) {
return {
environment: Environment.selfHosted,
appBaseUrl: this.onPremiseConfig.appBaseUrl,
apiBaseUrl: this.onPremiseConfig.apiBaseUrl,
mcpBaseUrl: this.onPremiseConfig.mcpBaseUrl,
}
}
// Standard mode: use built-in environment URLs
switch (this.environment) {
case Environment.staging:
return {
@@ -79,5 +283,14 @@ class ClineEndpoint {
* Usage:
* - ClineEnv.config() to get the current config.
* - ClineEnv.setEnvironment(Environment.local) to change the environment.
*
* IMPORTANT: ClineEndpoint.initialize() must be called before using ClineEnv.
*/
export const ClineEnv = ClineEndpoint.instance
export const ClineEnv = {
config: () => ClineEndpoint.config,
setEnvironment: (env: string) => ClineEndpoint.instance.setEnvironment(env),
getEnvironment: () => ClineEndpoint.instance.getEnvironment(),
}
// Export the class for initialization
export { ClineEndpoint }
+2 -1
View File
@@ -1,6 +1,7 @@
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { ClineStorageMessage } from "@/shared/messages/content"
import { Logger } from "@/shared/services/Logger"
import { ClineTool } from "@/shared/tools"
import { AIhubmixHandler } from "./providers/aihubmix"
import { AnthropicHandler } from "./providers/anthropic"
@@ -480,7 +481,7 @@ export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): Ap
}
}
} catch (error) {
console.error("buildApiHandler error:", error)
Logger.error("buildApiHandler error:", error)
}
return createHandlerForProvider(apiProvider, options, mode)
+4 -3
View File
@@ -1,6 +1,7 @@
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
@@ -58,7 +59,7 @@ export class AskSageHandler implements ApiHandler {
private apiKey: string
constructor(options: AskSageHandlerOptions) {
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
Logger.log("init api url", options.asksageApiUrl, askSageDefaultURL)
this.options = options
this.apiKey = options.asksageApiKey || ""
this.apiUrl = options.asksageApiUrl || askSageDefaultURL
@@ -164,7 +165,7 @@ export class AskSageHandler implements ApiHandler {
})
if (!response.ok) {
console.error("Failed to fetch AskSage usage", await response.text())
Logger.error("Failed to fetch AskSage usage", await response.text())
return undefined
}
@@ -177,7 +178,7 @@ export class AskSageHandler implements ApiHandler {
outputTokens: 0,
}
} catch (error) {
console.error("Error fetching AskSage usage:", error)
Logger.error("Error fetching AskSage usage:", error)
return undefined
}
}
+13 -8
View File
@@ -12,6 +12,7 @@ import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_
import { calculateApiCostOpenAI, calculateApiCostQwen } from "@utils/cost"
import { ExtensionRegistryInfo } from "@/registry"
import { ClineStorageMessage } from "@/shared/messages/content"
import { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToR1Format } from "../transform/r1-format"
@@ -109,9 +110,13 @@ interface ProviderChainOptions {
profile?: string
}
// a special jp inference profile was created for sonnet 4.5
// a special jp inference profile was created for sonnet 4.5 & haiku 4.5
// https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html
const JP_SUPPORTED_CRIS_MODELS = ["anthropic.claude-sonnet-4-5-20250929-v1:0", "anthropic.claude-sonnet-4-5-20250929-v1:0:1m"]
const JP_SUPPORTED_CRIS_MODELS = [
"anthropic.claude-sonnet-4-5-20250929-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0:1m",
"anthropic.claude-haiku-4-5-20251001-v1:0",
]
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
@@ -449,7 +454,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Error parsing Deepseek response chunk:", error)
Logger.error("Error parsing Deepseek response chunk:", error)
// Propagate the error by yielding a text response with error information
yield {
type: "text",
@@ -550,7 +555,7 @@ export class AwsBedrockHandler implements ApiHandler {
for await (const chunk of response.stream) {
// Debug logging to see actual response structure
// console.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
// Handle thinking response in additionalModelResponseFields (LangChain format)
const metadata = chunk.metadata as ExtendedMetadata | undefined
@@ -854,7 +859,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
// Log unsupported content types for debugging
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
return null
})
.filter((item): item is ContentBlock => item !== null)
@@ -912,7 +917,7 @@ export class AwsBedrockHandler implements ApiHandler {
},
}
} catch (error) {
console.error("Failed to process image content:", error)
Logger.error("Failed to process image content:", error)
// Return a text content indicating the error instead of null
// This ensures users are aware of the issue
return {
@@ -1111,7 +1116,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Error with OpenAI model via Converse API:", error)
Logger.error("Error with OpenAI model via Converse API:", error)
// Try to extract more detailed error information
let errorMessage = "Failed to process OpenAI model request"
@@ -1246,7 +1251,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Error with Qwen model via Converse API:", error)
Logger.error("Error with Qwen model via Converse API:", error)
// Try to extract more detailed error information
let errorMessage = "Failed to process Qwen model request"
+3 -2
View File
@@ -112,10 +112,11 @@ export class CerebrasHandler implements ApiHandler {
}
try {
const model = this.getModel()
const stream = await client.chat.completions.create({
model: this.getModel().id,
model: model.id,
messages: cerebrasMessages,
temperature: 0,
temperature: model.info.temperature ?? 0,
stream: true,
max_tokens: CEREBRAS_DEFAULT_MAX_TOKENS,
})
+11 -8
View File
@@ -7,10 +7,10 @@ import { ClineEnv } from "@/config"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService } from "@/services/auth/AuthService"
import { buildClineExtraHeaders } from "@/services/EnvUtils"
import { Logger } from "@/services/logging/Logger"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch, getAxiosSettings } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
@@ -35,10 +35,13 @@ export class ClineHandler implements ApiHandler {
private clineAccountService = ClineAccountService.getInstance()
private _authService: AuthService
private client: OpenAI | undefined
private readonly _baseUrl = ClineEnv.config().apiBaseUrl
lastGenerationId?: string
private lastRequestId?: string
private get _baseUrl(): string {
return ClineEnv.config().apiBaseUrl
}
constructor(options: ClineHandlerOptions) {
this.options = options
this._authService = AuthService.getInstance()
@@ -126,7 +129,7 @@ export class ClineHandler implements ApiHandler {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
Logger.error(`Cline API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
@@ -143,7 +146,7 @@ export class ClineHandler implements ApiHandler {
const choiceWithError = choice as any
if (choiceWithError.error) {
const error = choiceWithError.error
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
Logger.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
} else {
throw new Error(
@@ -199,7 +202,7 @@ export class ClineHandler implements ApiHandler {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
if (["x-ai/grok-code-fast-1", "kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
if (["kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
totalCost = 0
}
@@ -217,14 +220,14 @@ export class ClineHandler implements ApiHandler {
// Fallback to generation endpoint if usage chunk not returned
if (!didOutputUsage) {
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
Logger.warn("Cline API did not return usage chunk, fetching from generation endpoint")
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
}
}
} catch (error) {
console.error("Cline API Error:", error)
Logger.error("Cline API Error:", error)
throw error
}
}
@@ -260,7 +263,7 @@ export class ClineHandler implements ApiHandler {
}
} catch (error) {
// ignore if fails
console.error("Error fetching cline generation details:", error)
Logger.error("Error fetching cline generation details:", error)
}
}
return undefined
+5 -9
View File
@@ -7,7 +7,7 @@ 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"
@@ -81,14 +81,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,
+38 -37
View File
@@ -1,5 +1,6 @@
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
import { ModelInfo } from "../../../shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
@@ -84,7 +85,7 @@ export class DifyHandler implements ApiHandler {
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
console.log("[DIFY DEBUG] Constructor called with:", {
Logger.log("[DIFY DEBUG] Constructor called with:", {
hasApiKey: !!this.apiKey,
baseUrl: this.baseUrl,
})
@@ -98,7 +99,7 @@ export class DifyHandler implements ApiHandler {
}
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
console.log("[DIFY DEBUG] createMessage called with:", {
Logger.log("[DIFY DEBUG] createMessage called with:", {
systemPromptLength: systemPrompt?.length || 0,
messagesCount: messages?.length || 0,
})
@@ -115,8 +116,8 @@ export class DifyHandler implements ApiHandler {
}
const fullUrl = `${this.baseUrl}/chat-messages`
console.log("[DIFY DEBUG] Making request to:", fullUrl)
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
Logger.log("[DIFY DEBUG] Making request to:", fullUrl)
Logger.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
let response: Response
try {
@@ -129,21 +130,21 @@ export class DifyHandler implements ApiHandler {
body: JSON.stringify(requestBody),
})
} catch (error: any) {
console.error("[DIFY DEBUG] Network error during fetch:", error)
Logger.error("[DIFY DEBUG] Network error during fetch:", error)
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
throw new Error(`Dify API network error: ${error.message}${cause}`)
}
console.log("[DIFY DEBUG] Response status:", response.status)
Logger.log("[DIFY DEBUG] Response status:", response.status)
const headersObj: Record<string, string> = {}
response.headers.forEach((value, key) => {
headersObj[key] = value
})
console.log("[DIFY DEBUG] Response headers:", headersObj)
Logger.log("[DIFY DEBUG] Response headers:", headersObj)
if (!response.ok) {
const errorText = await response.text()
console.error("[DIFY DEBUG] Error response:", errorText)
Logger.error("[DIFY DEBUG] Error response:", errorText)
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
}
@@ -159,14 +160,14 @@ export class DifyHandler implements ApiHandler {
const processedEvents: string[] = []
let lastEventTime = Date.now()
console.log("[DIFY DEBUG] Starting to read streaming response...")
Logger.log("[DIFY DEBUG] Starting to read streaming response...")
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
console.log("[DIFY DEBUG] Stream ended naturally")
console.log(
Logger.log("[DIFY DEBUG] Stream ended naturally")
Logger.log(
"[DIFY DEBUG] Final state - hasYieldedContent:",
hasYieldedContent,
"fullText length:",
@@ -178,7 +179,7 @@ export class DifyHandler implements ApiHandler {
}
const chunk = decoder.decode(value, { stream: true })
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
Logger.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
buffer += chunk
const lines = buffer.split("\n")
@@ -187,41 +188,41 @@ export class DifyHandler implements ApiHandler {
buffer = lines.pop() || ""
for (const line of lines) {
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
Logger.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
if (line.startsWith("data: ")) {
const data = line.slice(6).trim()
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
Logger.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
if (data === "[DONE]") {
console.log("[DIFY DEBUG] Received [DONE] signal")
Logger.log("[DIFY DEBUG] Received [DONE] signal")
break
}
if (data === "") {
console.log("[DIFY DEBUG] Empty data line, skipping")
Logger.log("[DIFY DEBUG] Empty data line, skipping")
continue
}
try {
const parsed = JSON.parse(data)
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
Logger.log("[DIFY DEBUG] Parsed JSON:", parsed)
processedEvents.push(parsed.event || "unknown")
lastEventTime = Date.now()
// Capture conversation_id as soon as it's available
if (parsed.conversation_id && !this.conversationId) {
this.conversationId = parsed.conversation_id
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
Logger.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
}
// Handle different Dify event types based on actual Dify API
if (parsed.event === "message") {
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
Logger.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
// Dify sends the full text in each "answer" chunk, so we replace.
if (typeof parsed.answer === "string") {
fullText = parsed.answer
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
Logger.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
@@ -229,10 +230,10 @@ export class DifyHandler implements ApiHandler {
hasYieldedContent = true
}
} else if (parsed.event === "message_replace") {
console.log("[DIFY DEBUG] Replace message event:", parsed)
Logger.log("[DIFY DEBUG] Replace message event:", parsed)
if (parsed.answer) {
fullText = parsed.answer // Replace instead of append
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
Logger.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
yield {
type: "text",
text: fullText,
@@ -240,7 +241,7 @@ export class DifyHandler implements ApiHandler {
hasYieldedContent = true
}
} else if (parsed.event === "message_end") {
console.log("[DIFY DEBUG] Message end event", parsed)
Logger.log("[DIFY DEBUG] Message end event", parsed)
// Message completed. Yield final text if we have any.
if (fullText) {
yield {
@@ -260,19 +261,19 @@ export class DifyHandler implements ApiHandler {
}
return // End of stream
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Error event:", parsed)
Logger.error("[DIFY DEBUG] Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
Logger.log("[DIFY DEBUG] Workflow event:", parsed.event)
// These are informational events, continue processing
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
Logger.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
// These are informational events, continue processing
} else if (parsed.event === "ping") {
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
Logger.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
// Ping event, do nothing
} else {
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
Logger.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
// Try to extract text from other possible fields
if (parsed.text) {
fullText += parsed.text
@@ -299,17 +300,17 @@ export class DifyHandler implements ApiHandler {
}
}
} catch (e) {
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
Logger.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
}
} else if (line.trim() !== "") {
console.log(
Logger.log(
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
JSON.stringify(line),
)
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
try {
const parsed = JSON.parse(line.trim())
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
Logger.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
processedEvents.push(parsed.event || "direct-json")
// Handle the same event types as above
@@ -330,7 +331,7 @@ export class DifyHandler implements ApiHandler {
}
return
} else if (parsed.event === "error") {
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
Logger.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
} else if (parsed.answer || parsed.text || parsed.content) {
// Fallback for any content in direct JSON
@@ -344,7 +345,7 @@ export class DifyHandler implements ApiHandler {
}
} catch (e) {
// Not JSON, continue
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
}
}
@@ -359,11 +360,11 @@ export class DifyHandler implements ApiHandler {
streamDuration: Date.now() - lastEventTime,
conversationId: this.conversationId,
}
console.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
Logger.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
// If we have any accumulated text at all, yield it as a fallback
if (fullText.trim()) {
console.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
Logger.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
yield {
type: "text",
text: fullText,
@@ -380,7 +381,7 @@ export class DifyHandler implements ApiHandler {
}
} finally {
reader.releaseLock()
console.log("[DIFY DEBUG] Stream reader released")
Logger.log("[DIFY DEBUG] Stream reader released")
}
}
@@ -399,7 +400,7 @@ export class DifyHandler implements ApiHandler {
// Only prepend the system prompt if it's the very first message of a new conversation.
if (!this.conversationId && systemPrompt) {
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
Logger.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
return `${systemPrompt}\n\n---\n\n${userQuery}`
}
+6 -5
View File
@@ -11,6 +11,7 @@ import {
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
import { telemetryService } from "@/services/telemetry"
import { ClineStorageMessage } from "@/shared/messages/content"
import { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { RetriableError, withRetry } from "../retry"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
@@ -332,7 +333,7 @@ export class GeminiHandler implements ApiHandler {
throughputTokensPerSec: throughputTokensPerSecSdk,
})
} else {
console.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
Logger.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
}
}
}
@@ -408,7 +409,7 @@ export class GeminiHandler implements ApiHandler {
trace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens ?? 0, cost: cacheReadCost }
}
// console.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
// Logger.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
return totalCost
}
@@ -450,13 +451,13 @@ export class GeminiHandler implements ApiHandler {
})
if (response.totalTokens === undefined) {
console.warn("Gemini token counting returned undefined, using fallback")
Logger.warn("Gemini token counting returned undefined, using fallback")
return this.estimateTokens(content)
}
return response.totalTokens
} catch (error) {
console.warn("Gemini token counting failed, using fallback", error)
Logger.warn("Gemini token counting failed, using fallback", error)
return this.estimateTokens(content)
}
}
@@ -475,7 +476,7 @@ export class GeminiHandler implements ApiHandler {
const jsonStr = JSON.stringify(block)
return total + jsonStr.length
} catch (e) {
console.warn("Failed to stringify block for token estimation", e)
Logger.warn("Failed to stringify block for token estimation", e)
return total
}
}
+6 -5
View File
@@ -4,6 +4,7 @@ import OpenAI from "openai"
import { StateManager } from "@/core/storage/StateManager"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
import { isAnthropicModelId } from "@/utils/model-utils"
import { ApiHandler, CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
@@ -70,7 +71,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
const data: LiteLlmModelInfoResponse = await response.json()
return data
} else {
console.error("Failed to fetch LiteLLM model info:", response.statusText)
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
// Try with Authorization header instead
const retryResponse = await fetch(url, {
method: "GET",
@@ -84,12 +85,12 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
const data: LiteLlmModelInfoResponse = await retryResponse.json()
return data
} else {
console.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
}
}
} catch (error) {
console.error("Error fetching LiteLLM model info:", error)
Logger.error("Error fetching LiteLLM model info:", error)
throw error
}
}
@@ -169,7 +170,7 @@ export class LiteLlmHandler implements ApiHandler {
}
}
} catch (error) {
console.warn("Error getting LiteLLM model cost info:", error)
Logger.warn("Error getting LiteLLM model cost info:", error)
}
// Fallback to zero costs if we can't get the information
@@ -200,7 +201,7 @@ export class LiteLlmHandler implements ApiHandler {
return totalCost
} catch (error) {
console.error("Error calculating spend:", error)
Logger.error("Error calculating spend:", error)
return undefined
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ import {
OCI_HEADER_OPC_REQUEST_ID,
} from "@/services/auth/oca/utils/constants"
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
import { Logger } from "@/services/logging/Logger"
import { OcaModelInfo } from "@/shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiFormat } from "@/shared/proto/index.cline"
import { Logger } from "@/shared/services/Logger"
import { ApiHandler, type CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -134,11 +134,11 @@ export class OcaHandler implements ApiHandler {
const data: { cost: number } = await response.json()
return data.cost
} else {
console.error("Error calculating spend:", response.statusText)
Logger.error("Error calculating spend:", response.statusText)
return undefined
}
} catch (error) {
console.error("Error calculating spend:", error)
Logger.error("Error calculating spend:", error)
return undefined
}
}
@@ -313,7 +313,7 @@ export class OcaHandler implements ApiHandler {
// Convert ChatCompletion tools to Responses API format if provided
const responseTools = tools
?.filter((tool) => tool.type === "function")
?.filter((tool) => tool?.type === "function")
.map((tool: any) => ({
type: "function" as const,
name: tool.function.name,
+35 -8
View File
@@ -1,11 +1,14 @@
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { type Config, type Message, Ollama } from "ollama"
import type { ChatCompletionTool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
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
@@ -50,7 +53,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)]
@@ -69,20 +72,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 {
@@ -93,7 +120,7 @@ export class OllamaHandler implements ApiHandler {
}
}
} catch (streamError: any) {
console.error("Error processing Ollama stream:", streamError)
Logger.error("Error processing Ollama stream:", streamError)
throw new Error(`Ollama stream processing error: ${streamError.message || "Unknown error"}`)
}
} catch (error) {
@@ -107,7 +134,7 @@ export class OllamaHandler implements ApiHandler {
const statusCode = error.status || error.statusCode
const errorMessage = error.message || "Unknown error"
console.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
Logger.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
throw error
}
}
+18 -42
View File
@@ -1,16 +1,11 @@
import * as os from "os"
import { v7 as uuidv7 } from "uuid"
import { ModelInfo, OpenAiCodexModelId, openAiCodexDefaultModelId, openAiCodexModels } from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool } from "openai/resources/chat/completions"
import {
ModelInfo,
OpenAiCodexModelId,
openAiCodexDefaultModelId,
openAiCodexModels,
} from "@shared/api"
import * as os from "os"
import { v7 as uuidv7 } from "uuid"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
@@ -53,7 +48,9 @@ export class OpenAiCodexHandler implements ApiHandler {
}
private normalizeUsage(usage: any, model: { id: string; info: ModelInfo }): ApiStreamUsageChunk | undefined {
if (!usage) return undefined
if (!usage) {
return undefined
}
const inputDetails = usage.input_tokens_details ?? usage.prompt_tokens_details
@@ -90,11 +87,7 @@ export class OpenAiCodexHandler implements ApiHandler {
return out
}
async *createMessage(
systemPrompt: string,
messages: ClineStorageMessage[],
tools?: ChatCompletionTool[],
): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
const model = this.getModel()
// Reset state for this request
@@ -104,9 +97,7 @@ export class OpenAiCodexHandler implements ApiHandler {
// Get access token from OAuth manager
let accessToken = await openAiCodexOAuthManager.getAccessToken()
if (!accessToken) {
throw new Error(
"Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.",
)
throw new Error("Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.")
}
// Format conversation for Responses API
@@ -166,7 +157,7 @@ export class OpenAiCodexHandler implements ApiHandler {
// Pass through strict value from tool (MCP/custom tools have strict: false, built-in tools default to true)
if (tools && tools.length > 0) {
body.tools = tools
.filter((tool: any) => tool.type === "function")
.filter((tool: any) => tool?.type === "function")
.map((tool: any) => ({
type: "function",
name: tool.function.name,
@@ -179,11 +170,7 @@ export class OpenAiCodexHandler implements ApiHandler {
return body
}
private async *executeRequest(
requestBody: any,
model: { id: string; info: ModelInfo },
accessToken: string,
): ApiStream {
private async *executeRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
// Create AbortController for cancellation
this.abortController = new AbortController()
@@ -237,11 +224,7 @@ export class OpenAiCodexHandler implements ApiHandler {
}
}
private async *makeCodexRequest(
requestBody: any,
model: { id: string; info: ModelInfo },
accessToken: string,
): ApiStream {
private async *makeCodexRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
const url = `${CODEX_API_BASE_URL}/responses`
// Get ChatGPT account ID for organization subscriptions
@@ -302,10 +285,7 @@ export class OpenAiCodexHandler implements ApiHandler {
}
}
private async *handleStreamResponse(
body: ReadableStream<Uint8Array>,
model: { id: string; info: ModelInfo },
): ApiStream {
private async *handleStreamResponse(body: ReadableStream<Uint8Array>, model: { id: string; info: ModelInfo }): ApiStream {
const reader = body.getReader()
const decoder = new TextDecoder()
let buffer = ""
@@ -317,7 +297,9 @@ export class OpenAiCodexHandler implements ApiHandler {
}
const { done, value } = await reader.read()
if (done) break
if (done) {
break
}
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")
@@ -380,10 +362,7 @@ export class OpenAiCodexHandler implements ApiHandler {
}
// Handle tool/function call deltas
if (
event?.type === "response.tool_call_arguments.delta" ||
event?.type === "response.function_call_arguments.delta"
) {
if (event?.type === "response.tool_call_arguments.delta" || event?.type === "response.function_call_arguments.delta") {
const callId = event.call_id || event.tool_call_id || event.id || this.pendingToolCallId
const name = event.name || event.function_name || this.pendingToolCallName
const args = event.delta || event.arguments
@@ -484,10 +463,7 @@ export class OpenAiCodexHandler implements ApiHandler {
getModel(): { id: OpenAiCodexModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
const id =
modelId && modelId in openAiCodexModels
? (modelId as OpenAiCodexModelId)
: openAiCodexDefaultModelId
const id = modelId && modelId in openAiCodexModels ? (modelId as OpenAiCodexModelId) : openAiCodexDefaultModelId
const info: ModelInfo = openAiCodexModels[id]
+6 -6
View File
@@ -8,10 +8,10 @@ import {
import { calculateApiCostOpenAI } from "@utils/cost"
import OpenAI from "openai"
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
import { Logger } from "@/services/logging/Logger"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiFormat } from "@/shared/proto/cline/models"
import { Logger } from "@/shared/services/Logger"
import { isGPT5ModelFamily } from "@/utils/model-utils"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
@@ -135,7 +135,7 @@ export class OpenAiNativeHandler implements ApiHandler {
try {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
} catch (error) {
console.error("Error processing tool call delta:", error, delta.tool_calls)
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
}
}
@@ -159,7 +159,7 @@ export class OpenAiNativeHandler implements ApiHandler {
// Convert ChatCompletion tools to Responses API format if provided
const responseTools = tools
?.filter((tool) => tool.type === "function")
?.filter((tool) => tool?.type === "function")
.map((tool: any) => ({
type: "function" as const,
name: tool.function.name,
@@ -316,11 +316,11 @@ export class OpenAiNativeHandler implements ApiHandler {
chunk.response?.status === "incomplete" &&
chunk.response?.incomplete_details?.reason === "max_output_tokens"
) {
console.log("Ran out of tokens")
Logger.log("Ran out of tokens")
if (chunk.response?.output_text?.length > 0) {
console.log("Partial output:", chunk.response.output_text)
Logger.log("Partial output:", chunk.response.output_text)
} else {
console.log("Ran out of tokens during reasoning")
Logger.log("Ran out of tokens during reasoning")
}
}
+7 -6
View File
@@ -7,6 +7,7 @@ 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 { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
@@ -80,7 +81,7 @@ export class OpenRouterHandler implements ApiHandler {
// Check for error field directly on chunk
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
Logger.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
@@ -95,7 +96,7 @@ export class OpenRouterHandler implements ApiHandler {
const choiceWithError = choice as any
if (choiceWithError.error) {
const error = choiceWithError.error
console.error(
Logger.error(
`OpenRouter Mid-Stream Error: ${error?.code || "Unknown"} - ${error?.message || "Unknown error"}`,
)
// Format error details
@@ -179,7 +180,7 @@ export class OpenRouterHandler implements ApiHandler {
try {
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
const generation = (await generationIterator.next()).value
// console.log("OpenRouter generation details:", generation)
// Logger.log("OpenRouter generation details:", generation)
return {
type: "usage",
cacheWriteTokens: 0,
@@ -191,7 +192,7 @@ export class OpenRouterHandler implements ApiHandler {
}
} catch (error) {
// ignore if fails
console.error("Error fetching OpenRouter generation details:", error)
Logger.error("Error fetching OpenRouter generation details:", error)
}
}
return undefined
@@ -199,7 +200,7 @@ export class OpenRouterHandler implements ApiHandler {
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
async *fetchGenerationDetails(genId: string) {
// console.log("Fetching generation details for:", genId)
// Logger.log("Fetching generation details for:", genId)
try {
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
headers: {
@@ -211,7 +212,7 @@ export class OpenRouterHandler implements ApiHandler {
yield response.data?.data
} catch (error) {
// ignore if fails
console.error("Error fetching OpenRouter generation details:", error)
Logger.error("Error fetching OpenRouter generation details:", error)
throw error
}
}
+2 -1
View File
@@ -6,6 +6,7 @@ import * as os from "os"
import * as path from "path"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -76,7 +77,7 @@ export class QwenCodeHandler implements ApiHandler {
const credsStr = await fs.readFile(keyFile, "utf-8")
return JSON.parse(credsStr)
} catch (error) {
console.error(
Logger.error(
`Error reading or parsing credentials file at ${getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)}`,
)
throw new Error(`Failed to load Qwen OAuth credentials: ${error}`)
+2 -1
View File
@@ -12,6 +12,7 @@ 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 { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -134,7 +135,7 @@ export class QwenHandler implements ApiHandler {
try {
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
} catch (error) {
console.error("Error processing tool call delta:", error, delta.tool_calls)
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
}
}
+22 -21
View File
@@ -12,6 +12,7 @@ import JSON5 from "json5"
import OpenAI from "openai"
import { ClineStorageMessage } from "@/shared/messages/content"
import { getAxiosSettings } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
@@ -144,7 +145,7 @@ namespace Bedrock {
}
// Log unsupported content types for debugging
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
return null
})
.filter((item): item is BedrockContentBlock => item !== null)
@@ -213,7 +214,7 @@ namespace Bedrock {
},
}
} catch (error) {
console.error("Failed to process image content:", error)
Logger.error("Failed to process image content:", error)
// Return a text content indicating the error instead of null
// This ensures users are aware of the issue
return {
@@ -412,7 +413,7 @@ export class SapAiCoreHandler implements ApiHandler {
url: destination.url || this.options.sapAiCoreBaseUrl!,
}
} catch (error) {
console.error("Failed to create AI Core destination:", error)
Logger.error("Failed to create AI Core destination:", error)
throw new Error(`Unable to create AI Core destination: ${error instanceof Error ? error.message : String(error)}`)
}
}
@@ -473,7 +474,7 @@ export class SapAiCoreHandler implements ApiHandler {
})
.filter((deployment: any) => deployment !== null)
} catch (error) {
console.error("Error fetching deployments:", error)
Logger.error("Error fetching deployments:", error)
throw new Error("Failed to fetch deployments")
}
}
@@ -577,7 +578,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Error in SAP orchestration mode:", error)
Logger.error("Error in SAP orchestration mode:", error)
throw error
}
}
@@ -596,7 +597,7 @@ export class SapAiCoreHandler implements ApiHandler {
if (!deploymentId) {
// Fallback to runtime deployment id fetching for users who haven't opened the SAP provider UI
console.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
Logger.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
deploymentId = await this.getDeploymentForModel(model.id)
}
@@ -800,8 +801,8 @@ export class SapAiCoreHandler implements ApiHandler {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error("Error status:", error.response.status)
console.error("Error headers:", error.response.headers)
Logger.error("Error status:", error.response.status)
Logger.error("Error headers:", error.response.headers)
// Handle error data - need to read stream if responseType was 'stream'
let errorMessage = "Unknown error"
@@ -830,10 +831,10 @@ export class SapAiCoreHandler implements ApiHandler {
} else if (typeof error.response.data === "object") {
errorMessage = JSON.stringify(error.response.data, null, 2)
}
console.error("Error data:", errorMessage)
Logger.error("Error data:", errorMessage)
} catch (e) {
console.error("Failed to read error data:", e)
console.error("Raw error data:", error.response.data)
Logger.error("Failed to read error data:", e)
Logger.error("Raw error data:", error.response.data)
}
}
@@ -846,11 +847,11 @@ export class SapAiCoreHandler implements ApiHandler {
throw new Error(`HTTP ${error.response.status}: ${errorMessage}`)
} else if (error.request) {
// The request was made but no response was received
console.error("Error request:", error.request)
Logger.error("Error request:", error.request)
throw new Error("No response received from server")
} else {
// Something happened in setting up the request that triggered an Error
console.error("Error message:", error.message)
Logger.error("Error message:", error.message)
throw new Error(`Error setting up request: ${error.message}`)
}
}
@@ -898,13 +899,13 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Failed to parse JSON data:", error)
Logger.error("Failed to parse JSON data:", error)
}
}
}
}
} catch (error) {
console.error("Error streaming completion:", error)
Logger.error("Error streaming completion:", error)
throw error
}
}
@@ -965,7 +966,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Failed to parse JSON data:", error)
Logger.error("Failed to parse JSON data:", error)
yield {
type: "text",
text: `[ERROR] Failed to parse response data: ${error instanceof Error ? error.message : String(error)}`,
@@ -975,7 +976,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Error streaming completion:", error)
Logger.error("Error streaming completion:", error)
yield {
type: "text",
text: `[ERROR] Failed to process stream: ${error instanceof Error ? error.message : String(error)}`,
@@ -1044,13 +1045,13 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Failed to parse GPT JSON data:", error)
Logger.error("Failed to parse GPT JSON data:", error)
}
}
}
}
} catch (error) {
console.error("Error streaming GPT completion:", error)
Logger.error("Error streaming GPT completion:", error)
throw error
}
}
@@ -1109,13 +1110,13 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
} catch (error) {
console.error("Failed to parse Gemini JSON data:", error)
Logger.error("Failed to parse Gemini JSON data:", error)
}
}
}
}
} catch (error) {
console.error("Error streaming Gemini completion:", error)
Logger.error("Error streaming Gemini completion:", error)
throw error
}
}
+4 -3
View File
@@ -4,6 +4,7 @@ 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 { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
@@ -125,11 +126,11 @@ export class VercelAIGatewayHandler implements ApiHandler {
}
if (!didOutputUsage) {
console.warn("Vercel AI Gateway did not provide usage information in stream")
Logger.warn("Vercel AI Gateway did not provide usage information in stream")
}
} catch (error: any) {
console.error("Vercel AI Gateway error details:", error)
console.error("Error stack:", error.stack)
Logger.error("Vercel AI Gateway error details:", error)
Logger.error("Error stack:", error.stack)
throw new Error(`Vercel AI Gateway error: ${error.message}`)
}
}
+18 -17
View File
@@ -3,6 +3,7 @@ import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCo
import { calculateApiCostAnthropic } from "@utils/cost"
import * as vscode from "vscode"
import { ClineStorageMessage } from "@/shared/messages/content"
import { Logger } from "@/shared/services/Logger"
import { ApiHandler, CommonApiHandlerOptions, SingleCompletionHandler } from "../"
import { withRetry } from "../retry"
import { ApiStream } from "../transform/stream"
@@ -123,7 +124,7 @@ declare module "vscode" {
* const systemPrompt = "You are a helpful assistant";
* const messages = [{ role: "user", content: "Hello!" }];
* for await (const chunk of handler.createMessage(systemPrompt, messages)) {
* console.log(chunk);
* Logger.log(chunk);
* }
* ```
*/
@@ -147,7 +148,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
this.client = null
this.ensureCleanState()
} catch (error) {
console.error("Error during configuration change cleanup:", error)
Logger.error("Error during configuration change cleanup:", error)
}
}
})
@@ -279,7 +280,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
private async getClient(): Promise<vscode.LanguageModelChat> {
if (!this.client) {
console.debug("Cline <Language Model API>: Getting client with options:", {
Logger.debug("Cline <Language Model API>: Getting client with options:", {
vsCodeLmModelSelector: this.options.vsCodeLmModelSelector,
hasOptions: !!this.options,
selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [],
@@ -288,11 +289,11 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
try {
// Use default empty selector if none provided to get all available models
const selector = this.options?.vsCodeLmModelSelector || {}
console.debug("Cline <Language Model API>: Creating client with selector:", selector)
Logger.debug("Cline <Language Model API>: Creating client with selector:", selector)
this.client = await this.createClient(selector)
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"
console.error("Cline <Language Model API>: Client creation failed:", message)
Logger.error("Cline <Language Model API>: Client creation failed:", message)
throw new Error(`Cline <Language Model API>: Failed to create client: ${message}`)
}
}
@@ -413,7 +414,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
if (chunk instanceof vscode.LanguageModelTextPart) {
// Validate text part value
if (typeof chunk.value !== "string") {
console.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
Logger.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
continue
}
@@ -426,18 +427,18 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
try {
// Validate tool call parameters
if (!chunk.name || typeof chunk.name !== "string") {
console.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
Logger.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
continue
}
if (!chunk.callId || typeof chunk.callId !== "string") {
console.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
Logger.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
continue
}
// Ensure input is a valid object
if (!chunk.input || typeof chunk.input !== "object") {
console.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
Logger.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
continue
}
@@ -453,7 +454,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
accumulatedText += toolCallText
// Log tool call for debugging
console.debug("Cline <Language Model API>: Processing tool call:", {
Logger.debug("Cline <Language Model API>: Processing tool call:", {
name: chunk.name,
callId: chunk.callId,
inputSize: JSON.stringify(chunk.input).length,
@@ -464,10 +465,10 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
text: toolCallText,
}
} catch (error) {
console.error("Cline <Language Model API>: Failed to process tool call:", error)
Logger.error("Cline <Language Model API>: Failed to process tool call:", error)
}
} else {
console.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
Logger.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
}
}
@@ -489,7 +490,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
}
if (error instanceof Error) {
console.error("Cline <Language Model API>: Stream error details:", {
Logger.error("Cline <Language Model API>: Stream error details:", {
message: error.message,
stack: error.stack,
name: error.name,
@@ -500,12 +501,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
} else if (typeof error === "object" && error !== null) {
// Handle error-like objects
const errorDetails = JSON.stringify(error, null, 2)
console.error("Cline <Language Model API>: Stream error object:", errorDetails)
Logger.error("Cline <Language Model API>: Stream error object:", errorDetails)
throw new Error(`Cline <Language Model API>: Response stream error: ${errorDetails}`)
} else {
// Fallback for unknown error types
const errorMessage = String(error)
console.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
Logger.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
throw new Error(`Cline <Language Model API>: Response stream error: ${errorMessage}`)
}
}
@@ -526,7 +527,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
// Log any missing properties for debugging
for (const [prop, value] of Object.entries(requiredProps)) {
if (!value && value !== 0) {
console.warn(`Cline <Language Model API>: Client missing ${prop} property`)
Logger.warn(`Cline <Language Model API>: Client missing ${prop} property`)
}
}
@@ -557,7 +558,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector)
: "vscode-lm"
console.debug("Cline <Language Model API>: No client available, using fallback model info")
Logger.debug("Cline <Language Model API>: No client available, using fallback model info")
return {
id: fallbackId,
+3 -1
View File
@@ -1,3 +1,5 @@
import { Logger } from "@/shared/services/Logger"
interface RetryOptions {
maxRetries?: number
baseDelay?: number
@@ -72,7 +74,7 @@ export function withRetry(options: RetryOptions = {}) {
try {
await handlerInstance.options.onRetryAttempt(attempt + 1, maxRetries, delay, error)
} catch (e) {
console.error("Error in onRetryAttempt callback:", e)
Logger.error("Error in onRetryAttempt callback:", e)
}
}
+5 -4
View File
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { Logger } from "@/shared/services/Logger"
const o1SystemPrompt = (systemPrompt: string) => `
# System Prompt
@@ -317,7 +318,7 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
// Validate required parameters
if (!validateToolInput(toolName, tool_input)) {
console.error(`Invalid tool call for ${toolName}:`, content)
Logger.error(`Invalid tool call for ${toolName}:`, content)
return null
}
@@ -358,8 +359,8 @@ function validateToolInput(toolName: string, tool_input: Record<string, string>)
// </write_to_file>`;
//
// const { normalText, toolCalls } = parseAIResponse(aiResponse);
// console.log(normalText);
// console.log(toolCalls);
// Logger.log(normalText);
// Logger.log(toolCalls);
// Convert OpenAI response to Anthropic format
export function convertO1ResponseToAnthropicMessage(
@@ -432,4 +433,4 @@ export function convertO1ResponseToAnthropicMessage(
// usage: { prompt_tokens: 50, completion_tokens: 100 }
// };
// const anthropicMessage = convertO1ResponseToAnthropicMessage(openAICompletion);
// console.log(anthropicMessage);
// Logger.log(anthropicMessage);
+3 -2
View File
@@ -9,6 +9,7 @@ import {
ClineTextContentBlock,
ClineUserToolResultContentBlock,
} from "@/shared/messages/content"
import { Logger } from "@/shared/services/Logger"
// OpenAI API has a maximum tool call ID length of 40 characters
const MAX_TOOL_CALL_ID_LENGTH = 40
@@ -424,7 +425,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
try {
parsedInput = JSON.parse(toolCall.function?.arguments || "{}")
} catch (error) {
console.error("Failed to parse tool arguments:", error)
Logger.error("Failed to parse tool arguments:", error)
}
return {
type: "tool_use",
@@ -439,7 +440,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
return anthropicMessage
}
} catch (error) {
console.error("Error converting OpenAI message to Anthropic format:", error)
Logger.error("Error converting OpenAI message to Anthropic format:", error)
}
return anthropicMessage
+59
View File
@@ -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.
@@ -3,7 +3,7 @@ import type {
ChatCompletionToolChoiceOption,
ChatCompletionTool as OpenAITool,
} from "openai/resources/chat/completions"
import { Logger } from "@/services/logging/Logger"
import { Logger } from "@/shared/services/Logger"
import type { ApiStreamToolCallsChunk } from "./stream"
/**
+2 -1
View File
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { Logger } from "@/shared/services/Logger"
/**
* Safely converts a value into a plain object.
@@ -23,7 +24,7 @@ export function asObjectSafe(value: any): object {
return {}
} catch (error) {
console.warn("Cline <Language Model API>: Failed to parse object:", error)
Logger.warn("Cline <Language Model API>: Failed to parse object:", error)
return {}
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
import OpenAI from "openai"
import { Logger } from "@/services/logging/Logger"
import { ModelInfo } from "@/shared/api"
import { Logger } from "@/shared/services/Logger"
// Type that represents the OpenAI ResponseStream with its private properties
// The #private property issue can be resolved by using the AsyncIterable interface
@@ -143,11 +143,11 @@ export async function* handleResponsesApiStreamResponse(
chunk.response?.status === "incomplete" &&
chunk.response?.incomplete_details?.reason === "max_output_tokens"
) {
console.log("Ran out of tokens")
Logger.log("Ran out of tokens")
if (chunk.response?.output_text?.length > 0) {
console.log("Partial output:", chunk.response.output_text)
Logger.log("Partial output:", chunk.response.output_text)
} else {
console.log("Ran out of tokens during reasoning")
Logger.log("Ran out of tokens during reasoning")
}
}
+2 -1
View File
@@ -6,6 +6,7 @@ import { ShowMessageType } from "@shared/proto/host/window"
import { fileExistsAtPath } from "@utils/fs"
import * as path from "path"
import { ulid } from "ulid"
import { Logger } from "@/shared/services/Logger"
interface TaskReconstructionResult {
totalTasks: number
@@ -143,7 +144,7 @@ async function backupExistingTaskHistory(): Promise<void> {
}
} catch (error) {
// Non-fatal error, just log it
console.warn("Failed to backup existing task history:", error)
Logger.warn("Failed to backup existing task history:", error)
}
}
@@ -7,6 +7,7 @@ import { fileExistsAtPath } from "@utils/fs"
import cloneDeep from "clone-deep"
import fs from "fs/promises"
import * as path from "path"
import { Logger } from "@/shared/services/Logger"
import { getContextWindowInfo } from "./context-window-utils"
enum EditType {
@@ -119,7 +120,7 @@ export class ContextManager {
)
}
} catch (error) {
console.error("Failed to load context history:", error)
Logger.error("Failed to load context history:", error)
}
return new Map()
}
@@ -139,7 +140,7 @@ export class ContextManager {
"utf8",
)
} catch (error) {
console.error("Failed to save context history:", error)
Logger.error("Failed to save context history:", error)
}
}
@@ -207,7 +208,7 @@ export class ContextManager {
maxContextWindow: contextWindow,
}
} catch (error) {
console.error("Error parsing API request info for context telemetry:", error)
Logger.error("Error parsing API request info for context telemetry:", error)
}
}
}
@@ -357,7 +358,7 @@ export class ContextManager {
// Validate and fix tool_use/tool_result pairing
this.ensureToolResultsFollowToolUse(updatedMessages)
// OLD NOTE: if you try to console log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
// OLD NOTE: if you try to Logger log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
return updatedMessages
}
@@ -743,7 +744,7 @@ export class ContextManager {
return true
}
} catch (error) {
console.error("applyFirstUserMessageReplacement:", error)
Logger.error("applyFirstUserMessageReplacement:", error)
}
}
return false
@@ -4,6 +4,7 @@ import chokidar, { FSWatcher } from "chokidar"
import * as path from "path"
import * as vscode from "vscode"
import { Controller } from "@/core/controller"
import { Logger } from "@/shared/services/Logger"
import { getCwd } from "@/utils/path"
import type { FileMetadataEntry } from "./ContextTrackerTypes"
@@ -46,7 +47,7 @@ export class FileContextTracker {
const cwd = await getCwd()
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
Logger.info("No workspace folder available - cannot determine current working directory")
return
}
@@ -85,7 +86,7 @@ export class FileContextTracker {
try {
const cwd = await getCwd()
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
Logger.info("No workspace folder available - cannot determine current working directory")
return
}
@@ -95,7 +96,7 @@ export class FileContextTracker {
// Set up file watcher for this file
await this.setupFileWatcher(filePath)
} catch (error) {
console.error("Failed to track file operation:", error)
Logger.error("Failed to track file operation:", error)
}
}
@@ -157,7 +158,7 @@ export class FileContextTracker {
metadata.files_in_context.push(newEntry)
await saveTaskMetadata(taskId, metadata)
} catch (error) {
console.error("Failed to add file to metadata:", error)
Logger.error("Failed to add file to metadata:", error)
}
}
@@ -208,7 +209,7 @@ export class FileContextTracker {
}
}
} catch (error) {
console.error("Error checking file context metadata:", error)
Logger.error("Error checking file context metadata:", error)
}
// Also check deleted task messages for file operations
@@ -222,7 +223,7 @@ export class FileContextTracker {
}
}
} catch (error) {
console.error("Error checking task messages:", error)
Logger.error("Error checking task messages:", error)
}
}
}
@@ -239,7 +240,7 @@ export class FileContextTracker {
// are legitimate workspace state keys but don't fit the strict LocalStateKey type system
this.controller.stateManager.setWorkspaceState(key as any, files)
} catch (error) {
console.error("Error storing pending file context warning:", error)
Logger.error("Error storing pending file context warning:", error)
}
}
@@ -252,7 +253,7 @@ export class FileContextTracker {
const files = this.controller.stateManager.getWorkspaceStateKey(key as any) as string[]
return files
} catch (error) {
console.error("Error retrieving pending file context warning:", error)
Logger.error("Error retrieving pending file context warning:", error)
}
return undefined
}
@@ -268,7 +269,7 @@ export class FileContextTracker {
return files
}
} catch (error) {
console.error("Error retrieving pending file context warning:", error)
Logger.error("Error retrieving pending file context warning:", error)
}
return undefined
}
@@ -301,11 +302,11 @@ export class FileContextTracker {
}
const duration = Date.now() - startTime
console.log(
Logger.log(
`FileContextTracker: Processed ${existingTaskIds.size} tasks, found ${pendingWarningKeys.length} pending warnings, ${orphanedPendingContextTasks.length} orphaned, deleted ${orphanedPendingContextTasks.length}, took ${duration}ms`,
)
} catch (error) {
console.error("[FileContextTracker] Error cleaning up orphaned file context warnings:", error)
Logger.error("[FileContextTracker] Error cleaning up orphaned file context warnings:", error)
}
}
}
@@ -10,6 +10,7 @@ import * as path from "path"
import * as sinon from "sinon"
import * as disk from "@/core/storage/disk"
import { Logger } from "@/shared/services/Logger"
import * as fsUtils from "@/utils/fs"
import { discoverSkills, getAvailableSkills, getSkillContent } from "../skills"
@@ -29,8 +30,8 @@ describe("Skills Utility Functions", () => {
beforeEach(() => {
sandbox = sinon.createSandbox()
// Stub console.warn to avoid noise in test output
sandbox.stub(console, "warn")
// Stub Logger.warn to avoid noise in test output
sandbox.stub(Logger, "warn")
// Stub filesystem utilities
fileExistsStub = sandbox.stub(fsUtils, "fileExistsAtPath")
@@ -295,7 +296,7 @@ Content`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /missing required 'name' field/)
sinon.assert.calledWithMatch(Logger.warn as sinon.SinonStub, /missing required 'name' field/)
})
it("should reject skill with missing description field", async () => {
@@ -315,7 +316,7 @@ Content`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /missing required 'description' field/)
sinon.assert.calledWithMatch(Logger.warn as sinon.SinonStub, /missing required 'description' field/)
})
it("should reject skill when name doesn't match directory name", async () => {
@@ -336,7 +337,7 @@ Content`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /doesn't match directory/)
sinon.assert.calledWithMatch(Logger.warn as sinon.SinonStub, /doesn't match directory/)
})
it("should handle malformed YAML frontmatter gracefully", async () => {
@@ -14,6 +14,7 @@ import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
import fs from "fs/promises"
import path from "path"
import { Controller } from "@/core/controller"
import { Logger } from "@/shared/services/Logger"
import { parseYamlFrontmatter } from "./frontmatter"
import { evaluateRuleConditionals, type RuleEvaluationContext } from "./rule-conditionals"
@@ -45,10 +46,10 @@ export const getGlobalClineRules = async (
activatedConditionalRules.push(...rulesFilesTotal.activatedConditionalRules)
}
} catch {
console.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
Logger.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
}
} else {
console.error(`${globalClineRulesFilePath} is not a directory`)
Logger.error(`${globalClineRulesFilePath} is not a directory`)
}
}
@@ -105,7 +106,7 @@ export const getLocalClineRules = async (
activatedConditionalRules.push(...rulesFilesTotal.activatedConditionalRules)
}
} catch {
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
Logger.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
}
} else {
try {
@@ -137,7 +138,7 @@ export const getLocalClineRules = async (
}
}
} catch {
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
Logger.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
}
}
}
@@ -12,6 +12,7 @@ import { fileExistsAtPath, isDirectory } from "@utils/fs"
import fs from "fs/promises"
import path from "path"
import { Controller } from "@/core/controller"
import { Logger } from "@/shared/services/Logger"
/**
* Refreshes the toggles for windsurf, cursor, and agents rules
@@ -75,7 +76,7 @@ export const getLocalWindsurfRules = async (cwd: string, toggles: ClineRulesTogg
}
}
} catch {
console.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
Logger.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
}
}
}
@@ -101,7 +102,7 @@ export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggle
}
}
} catch {
console.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
Logger.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
}
}
}
@@ -119,7 +120,7 @@ export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggle
cursorRulesDirInstructions = formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
}
} catch {
console.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
Logger.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
}
}
}
@@ -149,7 +150,7 @@ async function findAgentsMdFiles(cwd: string): Promise<string[]> {
return basename === GlobalFileNames.agentsRulesFile.toLowerCase()
})
} catch (error) {
console.error(`Failed to find agents.md files in ${cwd}:`, error)
Logger.error(`Failed to find agents.md files in ${cwd}:`, error)
return []
}
}
@@ -184,7 +185,7 @@ export const getLocalAgentsRules = async (cwd: string, toggles: ClineRulesToggle
}
return null
} catch (error) {
console.error(`Failed to read agents.md file at ${filePath}:`, error)
Logger.error(`Failed to read agents.md file at ${filePath}:`, error)
return null
}
}),
@@ -194,7 +195,7 @@ export const getLocalAgentsRules = async (cwd: string, toggles: ClineRulesToggle
return formatResponse.agentsRulesLocalFileInstructions(cwd, combinedContent)
}
} catch (error) {
console.error("Failed to read agents.md files:", error)
Logger.error("Failed to read agents.md files:", error)
}
return undefined
@@ -5,6 +5,7 @@ import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
import fs from "fs/promises"
import * as path from "path"
import { Controller } from "@/core/controller"
import { Logger } from "@/shared/services/Logger"
import { parseYamlFrontmatter } from "./frontmatter"
import { evaluateRuleConditionals, RuleEvaluationContext } from "./rule-conditionals"
@@ -30,7 +31,7 @@ export async function readDirectoryRecursive(
}
return results
} catch (error) {
console.error(`Error reading directory ${directoryPath}: ${error}`)
Logger.error(`Error reading directory ${directoryPath}: ${error}`)
return []
}
}
@@ -98,7 +99,7 @@ export async function synchronizeRuleToggles(
}
}
} catch (error) {
console.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
Logger.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
}
return updatedToggles
@@ -431,7 +432,7 @@ export async function deleteRuleFile(
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`Error deleting file: ${errorMessage}`, error)
Logger.error(`Error deleting file: ${errorMessage}`, error)
return {
success: false,
message: `Failed to delete file.`,
@@ -3,13 +3,14 @@ import type { SkillContent, SkillMetadata } from "@shared/skills"
import { fileExistsAtPath, isDirectory } from "@utils/fs"
import * as fs from "fs/promises"
import * as path from "path"
import { Logger } from "@/shared/services/Logger"
import { parseYamlFrontmatter } from "./frontmatter"
/** Parse YAML frontmatter from markdown content (shared helper). */
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
const result = parseYamlFrontmatter(fileContent)
if (result.parseError) {
console.warn("Failed to parse YAML frontmatter:", result.parseError)
Logger.warn("Failed to parse YAML frontmatter:", result.parseError)
}
return { data: result.data, content: result.body }
}
@@ -39,7 +40,7 @@ async function scanSkillsDirectory(dirPath: string, source: "global" | "project"
}
} catch (error: unknown) {
if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EACCES") {
console.warn(`Permission denied reading skills directory: ${dirPath}`)
Logger.warn(`Permission denied reading skills directory: ${dirPath}`)
}
}
@@ -63,17 +64,17 @@ async function loadSkillMetadata(
// Validate required fields
if (!frontmatter.name || typeof frontmatter.name !== "string") {
console.warn(`Skill at ${skillDir} missing required 'name' field`)
Logger.warn(`Skill at ${skillDir} missing required 'name' field`)
return null
}
if (!frontmatter.description || typeof frontmatter.description !== "string") {
console.warn(`Skill at ${skillDir} missing required 'description' field`)
Logger.warn(`Skill at ${skillDir} missing required 'description' field`)
return null
}
// Name must match directory name per spec
if (frontmatter.name !== skillName) {
console.warn(`Skill name "${frontmatter.name}" doesn't match directory "${skillName}"`)
Logger.warn(`Skill name "${frontmatter.name}" doesn't match directory "${skillName}"`)
return null
}
@@ -84,7 +85,7 @@ async function loadSkillMetadata(
source,
}
} catch (error) {
console.warn(`Failed to load skill at ${skillDir}:`, error)
Logger.warn(`Failed to load skill at ${skillDir}:`, error)
return null
}
}
@@ -1,4 +1,5 @@
import { AuthState, AuthStateChangedRequest } from "@shared/proto/cline/account"
import { Logger } from "@/shared/services/Logger"
import type { Controller } from "../index"
/**
@@ -16,7 +17,7 @@ export async function authStateChanged(controller: Controller, request: AuthStat
// Return the same user info
return AuthState.create({ user: request.user })
} catch (error) {
console.error(`Failed to update auth state: ${error}`)
Logger.error(`Failed to update auth state: ${error}`)
throw error
}
}
@@ -1,4 +1,5 @@
import { GetOrganizationCreditsRequest, OrganizationCreditsData, OrganizationUsageTransaction } from "@shared/proto/cline/account"
import { Logger } from "@/shared/services/Logger"
import type { Controller } from "../index"
/**
@@ -50,7 +51,7 @@ export async function getOrganizationCredits(
) || [],
})
} catch (error) {
console.error(`Failed to fetch organization credits data: ${error}`)
Logger.error(`Failed to fetch organization credits data: ${error}`)
throw error
}
}
@@ -1,5 +1,6 @@
import { UserCreditsData } from "@shared/proto/cline/account"
import type { EmptyRequest } from "@shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
import type { Controller } from "../index"
/**
@@ -32,7 +33,7 @@ export async function getUserCredits(controller: Controller, _request: EmptyRequ
paymentTransactions: paymentTransactions,
})
} catch (error) {
console.error(`Failed to fetch user credits data: ${error}`)
Logger.error(`Failed to fetch user credits data: ${error}`)
throw error
}
}
@@ -1,8 +1,9 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { HostProvider } from "@/hosts/host-provider"
import { openExternal } from "@/utils/env"
import { ShowMessageType } from "@shared/proto/host/window"
import { HostProvider } from "@/hosts/host-provider"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { Logger } from "@/shared/services/Logger"
import { openExternal } from "@/utils/env"
import { Controller } from ".."
/**
@@ -29,7 +30,7 @@ export async function openAiCodexSignIn(controller: Controller, _: EmptyRequest)
await controller.postStateToWebview()
})
.catch((error) => {
console.error("[openAiCodexSignIn] OAuth callback failed:", error)
Logger.error("[openAiCodexSignIn] OAuth callback failed:", error)
openAiCodexOAuthManager.cancelAuthorizationFlow()
// Don't show notification for timeouts (user likely just abandoned)
const errorMessage = error instanceof Error ? error.message : String(error)
@@ -41,7 +42,7 @@ export async function openAiCodexSignIn(controller: Controller, _: EmptyRequest)
}
})
} catch (error) {
console.error("[openAiCodexSignIn] Failed to start OAuth flow:", error)
Logger.error("[openAiCodexSignIn] Failed to start OAuth flow:", error)
openAiCodexOAuthManager.cancelAuthorizationFlow()
throw error
}
@@ -1,5 +1,6 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -16,7 +17,7 @@ export async function openAiCodexSignOut(controller: Controller, _: EmptyRequest
// Update the state to reflect sign out
await controller.postStateToWebview()
} catch (error) {
console.error("[openAiCodexSignOut] Failed to sign out:", error)
Logger.error("[openAiCodexSignOut] Failed to sign out:", error)
throw error
}
@@ -1,5 +1,6 @@
import { BrowserConnectionInfo } from "@shared/proto/cline/browser"
import { EmptyRequest } from "@shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
import { Controller } from "../index"
/**
@@ -36,7 +37,7 @@ export async function getBrowserConnectionInfo(controller: Controller, _: EmptyR
host: browserSettings.remoteBrowserHost || "",
})
} catch (error: unknown) {
console.error("Error getting browser connection info:", error)
Logger.error("Error getting browser connection info:", error)
return BrowserConnectionInfo.create({
isConnected: false,
isRemote: false,
@@ -1,5 +1,6 @@
import { ChromePath } from "@shared/proto/cline/browser"
import { EmptyRequest } from "@shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
import { BrowserSession } from "../../../services/browser/BrowserSession"
import { Controller } from "../index"
@@ -19,7 +20,7 @@ export async function getDetectedChromePath(controller: Controller, _: EmptyRequ
isBundled: result.isBundled,
})
} catch (error) {
console.error("Error getting detected Chrome path:", error)
Logger.error("Error getting detected Chrome path:", error)
return ChromePath.create({
path: "",
isBundled: false,
@@ -3,6 +3,7 @@ import { Empty } from "@shared/proto/cline/common"
import pWaitFor from "p-wait-for"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/index.host"
import { Logger } from "@/shared/services/Logger"
import { ClineCheckpointRestore } from "../../../shared/WebviewMessage"
import { Controller } from ".."
@@ -14,7 +15,7 @@ export async function checkpointRestore(controller: Controller, request: Checkpo
await pWaitFor(() => controller.task?.taskState.isInitialized === true, {
timeout: 3_000,
}).catch((error) => {
console.log("Failed to init new Cline instance to restore checkpoint", error)
Logger.log("Failed to init new Cline instance to restore checkpoint", error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to restore checkpoint",
@@ -1,5 +1,6 @@
import { CheckpointEvent, CheckpointEvent_OperationType, CheckpointSubscriptionRequest } from "@shared/proto/cline/checkpoints"
import { Timestamp } from "@shared/proto/google/protobuf/timestamp"
import { Logger } from "@/shared/services/Logger"
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
import { Controller } from "../index"
@@ -105,7 +106,7 @@ export async function sendCheckpointEvent(eventData: CheckpointEventData): Promi
try {
await responseStream(event, false) // Not the last message
} catch (error) {
console.error("Error sending checkpoint event:", error)
Logger.error("Error sending checkpoint event:", error)
subscriptions.delete(responseStream)
if (subscriptions.size === 0) {
activeCheckpointSubscriptions.delete(cwdHash)
+2 -2
View File
@@ -1,8 +1,8 @@
import { getFileMentionFromPath } from "@/core/mentions"
import { singleFileDiagnosticsToProblemsString } from "@/integrations/diagnostics"
import { Logger } from "@/services/logging/Logger"
import { telemetryService } from "@/services/telemetry"
import { CommandContext, Empty } from "@/shared/proto/index.cline"
import { Logger } from "@/shared/services/Logger"
import { Controller } from "../index"
import { sendAddToInputEvent } from "../ui/subscribeToAddToInput"
@@ -39,7 +39,7 @@ export async function addToCline(controller: Controller, request: CommandContext
await sendAddToInputEvent(input)
}
console.log("addToCline", request.selectedText, filePath, request.language, notebookContext ? "with notebook context" : "")
Logger.log("addToCline", request.selectedText, filePath, request.language)
telemetryService.captureButtonClick("codeAction_addToChat", controller.task?.ulid)
return {}
@@ -1,9 +1,9 @@
import { getFileMentionFromPath } from "@/core/mentions"
import { HostProvider } from "@/hosts/host-provider"
import { Logger } from "@/services/logging/Logger"
import { telemetryService } from "@/services/telemetry"
import { CommandContext, Empty } from "@/shared/proto/index.cline"
import { ShowMessageType } from "@/shared/proto/index.host"
import { Logger } from "@/shared/services/Logger"
import { Controller } from "../index"
export async function explainWithCline(
+6 -5
View File
@@ -2,6 +2,7 @@ import { getFileMentionFromPath } from "@/core/mentions"
import { singleFileDiagnosticsToProblemsString } from "@/integrations/diagnostics"
import { telemetryService } from "@/services/telemetry"
import { CommandContext, Empty } from "@/shared/proto/index.cline"
import { Logger } from "@/shared/services/Logger"
import { Controller } from "../index"
export async function fixWithCline(controller: Controller, request: CommandContext): Promise<Empty> {
@@ -9,11 +10,11 @@ export async function fixWithCline(controller: Controller, request: CommandConte
const fileMention = await getFileMentionFromPath(filePath)
const problemsString = await singleFileDiagnosticsToProblemsString(filePath, request.diagnostics)
const taskMessage = `Fix the following code in ${fileMention}
\`\`\`\n${request.selectedText}\n\`\`\`\n\nProblems:\n${problemsString}`
await controller.initTask(taskMessage)
console.log("fixWithCline", request.selectedText, request.filePath, request.language, problemsString)
await controller.initTask(
`Fix the following code in ${fileMention}
\`\`\`\n${request.selectedText}\n\`\`\`\n\nProblems:\n${problemsString}`,
)
Logger.log("fixWithCline", request.selectedText, request.filePath, request.language, problemsString)
telemetryService.captureButtonClick("codeAction_fixWithCline", controller.task?.ulid)
return {}
@@ -1,9 +1,9 @@
import { getFileMentionFromPath } from "@/core/mentions"
import { HostProvider } from "@/hosts/host-provider"
import { Logger } from "@/services/logging/Logger"
import { telemetryService } from "@/services/telemetry"
import { CommandContext, Empty } from "@/shared/proto/index.cline"
import { ShowMessageType } from "@/shared/proto/index.host"
import { Logger } from "@/shared/services/Logger"
import { Controller } from "../index"
export async function improveWithCline(
@@ -1,6 +1,7 @@
import { RecordingResult } from "@shared/proto/cline/dictation"
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
import { telemetryService } from "@/services/telemetry"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -19,7 +20,7 @@ export const cancelRecording = async (controller: Controller): Promise<Recording
isSuccess = !!result?.success
errorMessage = result?.error ?? ""
} catch (error) {
console.error("Error canceling recording:", error)
Logger.error("Error canceling recording:", error)
isSuccess = false
errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
}
@@ -1,5 +1,6 @@
import { RecordingStatus } from "@shared/proto/cline/dictation"
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
import { Logger } from "@/shared/services/Logger"
/**
* Gets the current recording status
@@ -15,7 +16,7 @@ export const getRecordingStatus = async (): Promise<RecordingStatus> => {
error: status.error ?? "",
})
} catch (error) {
console.error("Error getting recording status:", error)
Logger.error("Error getting recording status:", error)
return RecordingStatus.create({
isRecording: false,
durationSeconds: 0,
@@ -5,6 +5,7 @@ import { audioRecordingService } from "@/services/dictation/AudioRecordingServic
import { telemetryService } from "@/services/telemetry"
import { AUDIO_PROGRAM_CONFIG } from "@/shared/audioProgramConstants"
import { ShowMessageType } from "@/shared/proto/host/window"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -24,7 +25,7 @@ async function handleInstallWithCline(
await controller.postStateToWebview()
await controller.initTask(installTask)
HostProvider.get().logToChannel(`Started task to install ${dependencyName}`)
Logger.log(`[handleInstallWithCline] Started task to install ${dependencyName}`)
}
/**
@@ -143,7 +144,7 @@ export const startRecording = async (controller: Controller): Promise<RecordingR
error: result.error || "",
})
} catch (error) {
console.error("Error starting recording:", error)
Logger.error("Error starting recording:", error)
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
// Handle different error types
@@ -1,6 +1,7 @@
import { RecordedAudio } from "@shared/proto/cline/dictation"
import { audioRecordingService } from "@/services/dictation/AudioRecordingService"
import { telemetryService } from "@/services/telemetry"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -24,7 +25,7 @@ export const stopRecording = async (controller: Controller): Promise<RecordedAud
error: result.error ?? "",
})
} catch (error) {
console.error("Error stopping recording:", error)
Logger.error("Error stopping recording:", error)
telemetryService.captureVoiceRecordingStopped(taskId, recordingDuration, false, process.platform)
@@ -3,6 +3,7 @@ import { HostProvider } from "@/hosts/host-provider"
import { getVoiceTranscriptionService } from "@/services/dictation/VoiceTranscriptionService"
import { telemetryService } from "@/services/telemetry"
import { ShowMessageType } from "@/shared/proto/host/window"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -59,7 +60,7 @@ export const transcribeAudio = async (controller: Controller, request: Transcrib
error: result.error ?? "",
})
} catch (error) {
console.error("Error transcribing audio:", error)
Logger.error("Error transcribing audio:", error)
const durationMs = Date.now() - startTime
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
@@ -10,7 +10,6 @@ describe("ifFileExistsRelativePath", () => {
let sandbox: sinon.SinonSandbox
let mockController: Controller
let getWorkspacePathStub: sinon.SinonStub
let consoleErrorStub: sinon.SinonStub
beforeEach(() => {
sandbox = sinon.createSandbox()
@@ -20,9 +19,6 @@ describe("ifFileExistsRelativePath", () => {
// Stub getWorkspacePath utility
getWorkspacePathStub = sandbox.stub(pathUtils, "getWorkspacePath")
// Stub console.error to prevent test output pollution
consoleErrorStub = sandbox.stub(console, "error")
})
afterEach(() => {
@@ -43,12 +39,11 @@ describe("ifFileExistsRelativePath", () => {
expect(typeof result.value).to.equal("boolean")
})
it("should return false and log error when no workspace path is available", async () => {
it("should return false when no workspace path is available", async () => {
const noWorkspaceScenarios = [null, undefined]
for (const workspaceValue of noWorkspaceScenarios) {
getWorkspacePathStub.resolves(workspaceValue)
consoleErrorStub.resetHistory()
const request = StringRequest.create({
value: "src/test.ts",
@@ -57,7 +52,6 @@ describe("ifFileExistsRelativePath", () => {
const result = await ifFileExistsRelativePath(mockController, request)
expect(result).to.deep.equal(BooleanResponse.create({ value: false }))
expect(consoleErrorStub.called).to.be.true
}
})
@@ -6,6 +6,7 @@ import { expect } from "chai"
import { afterEach, beforeEach, describe, it } from "mocha"
import * as path from "path"
import * as sinon from "sinon"
import { Logger } from "@/shared/services/Logger"
import { openFileRelativePath } from "../openFileRelativePath"
describe("openFileRelativePath", () => {
@@ -28,7 +29,7 @@ describe("openFileRelativePath", () => {
getWorkspacePathStub = sandbox.stub(pathUtils, "getWorkspacePath")
// Stub console.error to prevent test output pollution
consoleErrorStub = sandbox.stub(console, "error")
consoleErrorStub = sandbox.stub(Logger, "error")
})
afterEach(() => {
+2 -1
View File
@@ -1,4 +1,5 @@
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
import { writeTextToClipboard } from "@/utils/env"
import { Controller } from ".."
@@ -14,7 +15,7 @@ export async function copyToClipboard(_controller: Controller, request: StringRe
await writeTextToClipboard(request.value)
}
} catch (error) {
console.error("Error copying to clipboard:", error)
Logger.error("Error copying to clipboard:", error)
}
return Empty.create()
}
+2 -1
View File
@@ -5,6 +5,7 @@ import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/host/window"
import { Logger } from "@/shared/services/Logger"
import { getCwd, getDesktopDir } from "@/utils/path"
import { Controller } from ".."
import { openFile } from "./openFile"
@@ -24,7 +25,7 @@ export async function createRuleFile(controller: Controller, request: RuleFileRe
!request.type ||
typeof request.type !== "string"
) {
console.error("createRuleFile: Missing or invalid parameters", {
Logger.error("createRuleFile: Missing or invalid parameters", {
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
filename: typeof request.filename === "string" ? request.filename : `Invalid: ${typeof request.filename}`,
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
+2 -1
View File
@@ -4,6 +4,7 @@ import path from "path"
import { ensureSkillsDirectoryExists } from "@/core/storage/disk"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/host/window"
import { Logger } from "@/shared/services/Logger"
import { fileExistsAtPath } from "@/utils/fs"
import { Controller } from ".."
import { openFile } from "./openFile"
@@ -38,7 +39,7 @@ export async function createSkillFile(controller: Controller, request: CreateSki
const { skillName, isGlobal } = request
if (!skillName || typeof skillName !== "string" || typeof isGlobal !== "boolean") {
console.error("createSkillFile: Missing or invalid parameters", {
Logger.error("createSkillFile: Missing or invalid parameters", {
skillName: typeof skillName === "string" ? skillName : `Invalid: ${typeof skillName}`,
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
})
+2 -1
View File
@@ -3,6 +3,7 @@ import { getWorkspaceBasename } from "@core/workspace"
import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/host/window"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -20,7 +21,7 @@ export async function deleteRuleFile(controller: Controller, request: RuleFileRe
!request.type ||
typeof request.type !== "string"
) {
console.error("deleteRuleFile: Missing or invalid parameters", {
Logger.error("deleteRuleFile: Missing or invalid parameters", {
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
rulePath: typeof request.rulePath === "string" ? request.rulePath : `Invalid: ${typeof request.rulePath}`,
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
+3 -2
View File
@@ -1,6 +1,7 @@
import { DeleteSkillRequest, SkillsToggles } from "@shared/proto/cline/file"
import fs from "fs/promises"
import path from "path"
import { Logger } from "@/shared/services/Logger"
import { fileExistsAtPath } from "@/utils/fs"
import { Controller } from ".."
@@ -14,7 +15,7 @@ export async function deleteSkillFile(controller: Controller, request: DeleteSki
const { skillPath, isGlobal } = request
if (!skillPath || typeof skillPath !== "string" || typeof isGlobal !== "boolean") {
console.error("deleteSkillFile: Missing or invalid parameters", {
Logger.error("deleteSkillFile: Missing or invalid parameters", {
skillPath: typeof skillPath === "string" ? skillPath : `Invalid: ${typeof skillPath}`,
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
})
@@ -26,7 +27,7 @@ export async function deleteSkillFile(controller: Controller, request: DeleteSki
// Verify the path exists
if (!(await fileExistsAtPath(skillDir))) {
console.warn(`deleteSkillFile: Skill directory not found: ${skillDir}`)
Logger.warn(`deleteSkillFile: Skill directory not found: ${skillDir}`)
// Return current toggles anyway
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
+2 -1
View File
@@ -1,6 +1,7 @@
import { RelativePaths, RelativePathsRequest } from "@shared/proto/cline/file"
import * as path from "path"
import { URI } from "vscode-uri"
import { Logger } from "@/shared/services/Logger"
import { isDirectory } from "@/utils/fs"
import { asRelativePath } from "@/utils/path"
import { Controller } from ".."
@@ -17,7 +18,7 @@ export async function getRelativePaths(_controller: Controller, request: Relativ
try {
result.push(await getRelativePath(uriString))
} catch (error) {
console.error(`Error calculating relative path for ${uriString}:`, error)
Logger.error(`Error calculating relative path for ${uriString}:`, error)
}
}
return RelativePaths.create({ paths: result })
@@ -15,7 +15,6 @@ export async function ifFileExistsRelativePath(_controller: Controller, request:
if (!workspacePath) {
// If no workspace is open, return false
console.error("Error in ifFileExistsRelativePath: No workspace path available") // TODO
return BooleanResponse.create({ value: false })
}
@@ -2,6 +2,7 @@ import { workspaceResolver } from "@core/workspace"
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { getWorkspacePath } from "@utils/path"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -14,7 +15,7 @@ export async function openFileRelativePath(_controller: Controller, request: Str
const workspacePath = await getWorkspacePath()
if (!workspacePath) {
console.error("Error in openFileRelativePath: No workspace path available")
Logger.error("Error in openFileRelativePath: No workspace path available")
return Empty.create()
}
+2 -1
View File
@@ -3,6 +3,7 @@ import { refreshExternalRulesToggles } from "@core/context/instructions/user-ins
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
import { EmptyRequest } from "@shared/proto/cline/common"
import { RefreshedRules } from "@shared/proto/cline/file"
import { Logger } from "@/shared/services/Logger"
import { getCwd, getDesktopDir } from "@/utils/path"
import type { Controller } from "../index"
@@ -32,7 +33,7 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
globalWorkflowToggles: { toggles: globalWorkflowToggles },
})
} catch (error) {
console.error("Failed to refresh rules:", error)
Logger.error("Failed to refresh rules:", error)
throw error
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { StringRequest } from "@shared/proto/cline/common"
import { GitCommits } from "@shared/proto/cline/file"
import { searchCommits as searchCommitsUtil } from "@utils/git"
import { getWorkspacePath } from "@utils/path"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -21,7 +22,7 @@ export async function searchCommits(_controller: Controller, request: StringRequ
return GitCommits.create({ commits })
} catch (error) {
console.error(`Error searching commits: ${JSON.stringify(error)}`)
Logger.error(`Error searching commits: ${JSON.stringify(error)}`)
return GitCommits.create({ commits: [] })
}
}
+4 -3
View File
@@ -3,6 +3,7 @@ import { telemetryService } from "@services/telemetry"
import { FileSearchRequest, FileSearchResults, FileSearchType } from "@shared/proto/cline/file"
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
import { getWorkspacePath } from "@utils/path"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -41,8 +42,8 @@ export async function searchFiles(controller: Controller, request: FileSearchReq
const workspacePath = await getWorkspacePath()
if (!workspacePath) {
console.error("Error in searchFiles: No workspace path available")
await telemetryService.captureMentionFailed("folder", "not_found", "No workspace path available")
Logger.error("Error in searchFiles: No workspace path available")
telemetryService.captureMentionFailed("folder", "not_found", "No workspace path available")
return { results: [], mentionsRequestId: request.mentionsRequestId }
}
@@ -78,7 +79,7 @@ export async function searchFiles(controller: Controller, request: FileSearchReq
return { results: protoResults, mentionsRequestId: request.mentionsRequestId }
} catch (error) {
// Log the error but don't include it in the response, following the pattern in searchCommits
console.error("Error in searchFiles:", error)
Logger.error("Error in searchFiles:", error)
// Track as a search execution error with appropriate error type
const errorMessage = error instanceof Error ? error.message : String(error)
+2 -1
View File
@@ -1,5 +1,6 @@
import { selectFiles as selectFilesIntegration } from "@integrations/misc/process-files"
import { BooleanRequest, StringArrays } from "@shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -13,7 +14,7 @@ export async function selectFiles(_controller: Controller, request: BooleanReque
const { images, files } = await selectFilesIntegration(request.value)
return StringArrays.create({ values1: images, values2: files })
} catch (error) {
console.error("Error selecting images & files:", error)
Logger.error("Error selecting images & files:", error)
// Return empty array on error
return StringArrays.create({ values1: [], values2: [] })
}
+2 -1
View File
@@ -1,5 +1,6 @@
import type { ToggleAgentsRuleRequest } from "@shared/proto/cline/file"
import { ClineRulesToggles } from "@shared/proto/cline/file"
import { Logger } from "@/shared/services/Logger"
import type { Controller } from "../index"
/**
@@ -12,7 +13,7 @@ export async function toggleAgentsRule(controller: Controller, request: ToggleAg
const { rulePath, enabled } = request
if (!rulePath || typeof enabled !== "boolean") {
console.error("toggleAgentsRule: Missing or invalid parameters", {
Logger.error("toggleAgentsRule: Missing or invalid parameters", {
rulePath,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
})
+2 -1
View File
@@ -2,6 +2,7 @@ import { getWorkspaceBasename } from "@core/workspace"
import type { ToggleClineRuleRequest } from "@shared/proto/cline/file"
import { RuleScope, ToggleClineRules } from "@shared/proto/cline/file"
import { telemetryService } from "@/services/telemetry"
import { Logger } from "@/shared/services/Logger"
import type { Controller } from "../index"
/**
@@ -14,7 +15,7 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
const { scope, rulePath, enabled } = request
if (!rulePath || typeof enabled !== "boolean" || scope === undefined) {
console.error("toggleClineRule: Missing or invalid parameters", {
Logger.error("toggleClineRule: Missing or invalid parameters", {
rulePath,
scope,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
+2 -1
View File
@@ -1,5 +1,6 @@
import type { ToggleCursorRuleRequest } from "@shared/proto/cline/file"
import { ClineRulesToggles } from "@shared/proto/cline/file"
import { Logger } from "@/shared/services/Logger"
import type { Controller } from "../index"
/**
@@ -12,7 +13,7 @@ export async function toggleCursorRule(controller: Controller, request: ToggleCu
const { rulePath, enabled } = request
if (!rulePath || typeof enabled !== "boolean") {
console.error("toggleCursorRule: Missing or invalid parameters", {
Logger.error("toggleCursorRule: Missing or invalid parameters", {
rulePath,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
})
+2 -1
View File
@@ -1,4 +1,5 @@
import { SkillsToggles, ToggleSkillRequest } from "@shared/proto/cline/file"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -11,7 +12,7 @@ export async function toggleSkill(controller: Controller, request: ToggleSkillRe
const { skillPath, isGlobal, enabled } = request
if (!skillPath || typeof enabled !== "boolean" || typeof isGlobal !== "boolean") {
console.error("toggleSkill: Missing or invalid parameters", {
Logger.error("toggleSkill: Missing or invalid parameters", {
skillPath,
isGlobal,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
@@ -1,5 +1,6 @@
import type { ToggleWindsurfRuleRequest } from "@shared/proto/cline/file"
import { ClineRulesToggles } from "@shared/proto/cline/file"
import { Logger } from "@/shared/services/Logger"
import type { Controller } from "../index"
/**
@@ -12,7 +13,7 @@ export async function toggleWindsurfRule(controller: Controller, request: Toggle
const { rulePath, enabled } = request
if (!rulePath || typeof enabled !== "boolean") {
console.error("toggleWindsurfRule: Missing or invalid parameters", {
Logger.error("toggleWindsurfRule: Missing or invalid parameters", {
rulePath,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
})
+2 -1
View File
@@ -1,4 +1,5 @@
import { ClineRulesToggles, RuleScope, ToggleWorkflowRequest } from "@shared/proto/cline/file"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
@@ -11,7 +12,7 @@ export async function toggleWorkflow(controller: Controller, request: ToggleWork
const { workflowPath, enabled, scope } = request
if (!workflowPath || typeof enabled !== "boolean" || scope === undefined) {
console.error("toggleWorkflow: Missing or invalid parameters", {
Logger.error("toggleWorkflow: Missing or invalid parameters", {
workflowPath,
scope,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
+6 -5
View File
@@ -3,6 +3,7 @@ import { serviceHandlers } from "@generated/hosts/vscode/protobus-services"
import { GrpcRecorderBuilder } from "@/core/controller/grpc-recorder/grpc-recorder.builder"
import { GrpcRequestRegistry } from "@/core/controller/grpc-request-registry"
import { ExtensionMessage } from "@/shared/ExtensionMessage"
import { Logger } from "@/shared/services/Logger"
import { GrpcCancel, GrpcRequest } from "@/shared/WebviewMessage"
/**
@@ -28,7 +29,7 @@ function withRecordingMiddleware(postMessage: PostMessageToWebview, controller:
response.grpc_response,
)
} catch (e) {
console.warn("Failed to record gRPC response:", e)
Logger.warn("Failed to record gRPC response:", e)
}
}
return postMessage(response)
@@ -42,7 +43,7 @@ function recordRequest(request: GrpcRequest, controller: Controller): void {
try {
GrpcRecorderBuilder.getRecorder(controller).recordRequest(request)
} catch (e) {
console.warn("Failed to record gRPC request:", e)
Logger.warn("Failed to record gRPC request:", e)
}
}
@@ -91,7 +92,7 @@ async function handleUnaryRequest(
})
} catch (error) {
// Send error response
console.log("Protobus error:", error)
Logger.log("Protobus error:", error)
await postMessageToWebview({
type: "grpc_response",
grpc_response: {
@@ -142,7 +143,7 @@ async function handleStreamingRequest(
// The stream will be closed when the client disconnects or when the service explicitly ends it
} catch (error) {
// Send error response
console.log("Protobus error:", error)
Logger.log("Protobus error:", error)
await postMessageToWebview({
type: "grpc_response",
grpc_response: {
@@ -173,7 +174,7 @@ export async function handleGrpcRequestCancel(postMessageToWebview: PostMessageT
},
})
} else {
console.log(`[DEBUG] Request not found for cancellation: ${request.request_id}`)
Logger.log(`[DEBUG] Request not found for cancellation: ${request.request_id}`)
}
}
@@ -9,6 +9,7 @@ import {
GrpcSessionLog,
SessionStats,
} from "@/core/controller/grpc-recorder/types"
import { Logger } from "@/shared/services/Logger"
export class GrpcRecorderNoops implements IRecorder {
recordRequest(_request: GrpcRequest): void {}
@@ -55,7 +56,7 @@ export class GrpcRecorder implements IRecorder {
}
this.fileHandler.initialize(this.sessionLog).catch((error) => {
console.error("Failed to initialize gRPC log file:", error)
Logger.error("Failed to initialize gRPC log file:", error)
})
}
@@ -118,7 +119,7 @@ export class GrpcRecorder implements IRecorder {
const pendingRequest = this.pendingRequests.get(requestId)
if (!pendingRequest) {
console.warn(`No pending request found for response with ID: ${requestId}`)
Logger.warn(`No pending request found for response with ID: ${requestId}`)
return
}
@@ -142,7 +143,7 @@ export class GrpcRecorder implements IRecorder {
this.flushLogAsync()
this.runHooks(entry).catch((e) => console.error("Post-record hook failed:", e))
this.runHooks(entry).catch((e) => Logger.error("Post-record hook failed:", e))
}
private async runHooks(entry: GrpcLogEntry): Promise<void> {
@@ -181,7 +182,7 @@ export class GrpcRecorder implements IRecorder {
public recordError(requestId: string, error: string): void {
const pendingRequest = this.pendingRequests.get(requestId)
if (!pendingRequest) {
console.warn(`No pending request found for error with ID: ${requestId}`)
Logger.warn(`No pending request found for error with ID: ${requestId}`)
return
}
@@ -200,7 +201,7 @@ export class GrpcRecorder implements IRecorder {
private flushLogAsync(): void {
setImmediate(() => {
this.fileHandler.write(this.sessionLog).catch((error) => {
console.error("Failed to flush gRPC log:", error)
Logger.error("Failed to flush gRPC log:", error)
})
})
}
+2 -3
View File
@@ -1,3 +1,4 @@
import { Logger } from "@/shared/services/Logger"
import { StreamingResponseHandler } from "./grpc-handler"
/**
@@ -54,7 +55,6 @@ export class GrpcRequestRegistry {
timestamp: new Date(),
responseStream,
})
console.log(`[DEBUG] Registered request: ${requestId}`)
}
/**
@@ -69,9 +69,8 @@ export class GrpcRequestRegistry {
}
try {
requestInfo.cleanup()
console.log(`[DEBUG] Cleaned up request: ${requestId}`)
} catch (error) {
console.error(`Error cleaning up request ${requestId}:`, error)
Logger.error(`Error cleaning up request ${requestId}:`, error)
}
this.activeRequests.delete(requestId)
return true

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