Compare commits

...

23 Commits

Author SHA1 Message Date
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
Seb Duerr dc85299a73 Remove deprecated zai-glm-4.6 model from Cerebras provider (#8717) 2026-01-22 12:58:07 -08:00
ClineXDiego 4533ed3ea8 fix: support drag & drop files from SSH remote workspaces (#8804)
Add vscode-remote: scheme to the valid URI filter for drag & drop operations.
This allows files from SSH Remote workspaces to be dropped into the chat.

Fixes #7606
2026-01-22 12:37:11 -08:00
Max 66d7664d1b improve cline command permission parsing logic (#8544)
- cline command permission flag can now parse subshells correctly and
validate that subshells don't contain disallowed commands.

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-22 11:11:53 -08:00
dependabot[bot] 50aeb7098f chore(deps): bump undici (#8783)
Bumps [undici](https://github.com/nodejs/undici) to 6.23.0 and updates ancestor dependency . These dependencies need to be updated together.


Updates `undici` from 6.22.0 to 6.23.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.23.0)

Updates `undici` from 7.16.0 to 7.19.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.23.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.23.0
  dependency-type: indirect
- dependency-name: undici
  dependency-version: 7.19.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ClineXDiego <diego@cline.bot>
2026-01-22 10:50:49 -08:00
github-actions[bot] c2e6eafb45 v3.52.0 Release Notes (#8633)
- 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.

- Bugs in DiffViewProvider for file editing
- Ollama's recommended models to use correct identifiers

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-22 10:48:32 -08:00
Juan Pablo Flores 0220628c39 Adds Open AI Codex docs (#8791)
* feat(docs): add OpenAI Codex provider setup instructions and update model selection guidance

* fix(docs): improve clarity and formatting in OpenAI Codex documentation

* Update docs/provider-config/openai-codex.mdx

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

* Update docs/provider-config/openai-codex.mdx

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tony Loehr <turingxo@gmail.com>
2026-01-22 09:30:12 -08:00
CandiedUniverse 5052220195 feat(hooks): Make hooks always enabled and remove its feature setting. [CLINE-1179] (#8777)
* feat(hooks): Standardize on calling getHooksEnabledSafe().

* feat(hooks): Hard-code getHookEnabledSafe() to return true unless on Windows.

* feat(hooks): Remove hooks setting from the CLI.

* feat(hooks): Remove hooks toggle from the Feature Settings UI.

* feat(hooks): Remove hooksEnabled toggles from settings/task APIs.

* feat(hooks): Stop using hooksEnabled setting.

* feat(hooks): npm run changeset

* feat(hooks): Simplify getHooksEnabledSafe() function signature.

* feat(hooks): Remove hooks setting migration.

feat(hooks): Remove hooksEnabled from updateSettingsCli() conversion.

feat(hooks): Use 'reserved' for removed fields in UpdateSettingsRequest protobuf.
2026-01-22 09:26:16 -08:00
CandiedUniverse abf3081e56 Rules: Wire up conditional rules functionality [ENG-1470] (#8669)
* feat(rules): Write technical design / implementation plan doc.

* update frontmatter plan

* feat(rules): Initial implementation based on plan doc.

* feat(rules): Add tool-call path harvesting for path-scoped Cline Rules.

* chore(rules): exclude internal paths-frontmatter plan doc from PR

* fix(rules): use latest user message for paths frontmatter context

* feat(rules): Implement conditional_rules_applied say type.

* feat(rules): changes as per Cline's code review feedback

* feat(rules): npm run changeset

* feat(rules): Changes as per ellipsis-dev feedback.

* feat(rules): Changes as per code review feedback (i.e. don't bloat the task context).

* feat(rules): Fix failing unit tests.
2026-01-22 09:25:59 -08:00
Michael Gutin 125cb78a31 Switch background element now has a 3:1 contrast ration with thumb and rule row background in vs code light and dark themes (#8747) 2026-01-22 12:16:51 -05:00
CandiedUniverse 17539228ea feat(cli): Add note about ctrl+c to exit the CLI. [CLINE-1162] (#8796)
* feat(cli): Add note about ctrl+c to exit the CLI.

* feat(cli): npm run changeset
2026-01-22 09:01:57 -08:00
Ara bdb7cc36fa feat(DiffEditRow): add button to open file in editor (#8564)
* refactor(diff): return result object with line tracking metadata

Change constructNewFileContent to return an object containing newContent
and line number information instead of just the string content. Add
charIndexToLineNumber helper function to support tracking where changes
occur in the file.

Update all callers and tests to access the newContent property from
the result object.

* minor fix

* fix: hide line numbers when not available from backend

* chore: add changeset

* feat: add startLineNumbers support to ApplyPatchHandler

* fix: split V4A @@ chunks into separate Patch objects for proper line numbers

* fix(DiffEditRow): preserve +/- prefix in diff line display for backwards compatibility

* fix line numbers
2026-01-21 21:38:45 -08:00
Tony Loehr d3e1065707 added jupyter docs (#8742)
* added jupyter docs

* fix jupyter docs and add gifs

* fix jupyter docs 2

* fix jupyter gif placement
2026-01-21 16:15:26 -08:00
387 changed files with 3463 additions and 2411 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add claude 4.5 haiku
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: finalize document content during approval flow
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Expose --version in cline cli command
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: prevent duplicate diff errors when parallel tool calling is enabled
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Log Persistence errors to PostHog
-13
View File
@@ -1,13 +0,0 @@
---
"claude-dev": patch
---
feat: add OpenAI Codex (ChatGPT Plus/Pro) provider
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.
Models available:
- gpt-5.2-codex (default)
- gpt-5.1-codex-max
- gpt-5.1-codex-mini
- gpt-5.2
@@ -1,9 +0,0 @@
---
"claude-dev": patch
---
Fix two bugs in DiffViewProvider file editing:
1. **Line boundary validation**: Add `safelyTruncateDocument()` to prevent out-of-bounds line errors on JetBrains hosts (fixes #8423, #8429). The gRPC protocol strictly validates line numbers, causing "truncateDocument INTERNAL: Wrong line" errors when `truncateDocument()` was called with a line number >= document line count.
2. **Content concatenation on final update**: When replacing content without a trailing newline, the old content at line N+1 was concatenated to the new content. Fixed by extending the replacement range to cover the entire document on final update.
-9
View File
@@ -1,9 +0,0 @@
---
"claude-dev": patch
---
docs: fix outdated Ollama model names in documentation
Updated recommended Ollama models to use correct identifiers:
- Changed qwen3-coder-30b to qwen2.5-coder:32b
- Changed devstral-small to codellama:34b-code
+7
View File
@@ -0,0 +1,7 @@
---
"claude-dev": patch
---
fix(extract-text): strip notebook outputs to reduce context size
Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing the amount of context sent to the LLM while preserving the essential code and markdown content.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
OpenAI GPT-5 Codex models are now using Apply Patch tool for diff edits.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix issue where tool call ids are invalid when switching between models using the chat completion format and the responses api format.
-10
View File
@@ -1,10 +0,0 @@
---
"claude-dev": patch
---
fix: improve Jupyter notebook diff view and reduce LLM context for notebook edits
- Restore switchToSpecializedEditor() for Jupyter notebook diff views that was accidentally removed during rebase
- Open .ipynb files in Jupyter notebook editor after save instead of leaving stale diff view
- Strip notebook outputs from content sent to LLM, reducing context by 95% (196KB → 9KB)
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: add chat output on skill use
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Adding telemetry for background exec terminal
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
This pull request introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness. The feature allows users to seamlessly work with Jupyter notebooks using Cline's AI capabilities while preserving the notebook's JSON structure.
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Remove deprecated zai-glm-4.6 model from Cerebras provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Make Sonnet 4.5 the default Amazon Bedrock model id
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: removes retry message from UI after retry succeeds
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Support native tool calling for LM Studio and Ollama provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fixing integration tests from testing framework
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
add claude 4.5 opus into sap provider.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Limite Vertex and LiteLLM options when they're remote configured
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix crash when the Context Menu has a type but no options
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Throttle the remote config fetch
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Improve history view filter menu
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add git worktree management UI for running parallel Cline sessions
+20 -2
View File
@@ -1,10 +1,28 @@
# Changelog
## [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.
### Fixed
- Bugs in DiffViewProvider for file editing
- Ollama's recommended models to use correct identifiers
## [3.51.0]
### Added
- Adding OpenAI gpt-5.2-codex model to the model picker
- Adding OpenAI gpt-5.2-codex model to the model picker
## [3.50.0]
@@ -1737,4 +1755,4 @@ Add Opus 4.1 through Claude Code
## [0.0.6]
- Initial release
- Initial release
+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": [
"**",
+1 -1
View File
@@ -231,7 +231,7 @@ func promptForInitialTask(ctx context.Context, modeFlag string, workspaces []str
prompt, err := output.PromptForInitialTask(
"Start a new Cline task",
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
modeFlag,
slash.NewRegistry(ctx),
)
+58
View File
@@ -298,6 +298,64 @@ cline task view
\f[I]# Start interactive chat with this task\f[R]
cline task chat
.EE
.SH ENVIRONMENT
.TP
\f[B]CLINE_COMMAND_PERMISSIONS\f[R]
JSON configuration for restricting which shell commands Cline can
execute.
When set, commands are validated against allow/deny patterns before
execution.
When not set, all commands are allowed (backward compatibility).
.RS
.PP
Format:
\f[CR]{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}\f[R]
.PP
\f[B]Fields:\f[R]
.IP \(bu 2
\f[B]allow\f[R] (array of strings): Glob patterns for allowed commands.
If specified, only matching commands are permitted.
Uses \f[CR]*\f[R] to match any characters and \f[CR]?\f[R] to match a
single character.
.IP \(bu 2
\f[B]deny\f[R] (array of strings): Glob patterns for denied commands.
Deny rules take precedence over allow rules.
.IP \(bu 2
\f[B]allowRedirects\f[R] (boolean): Whether to allow shell redirects
(\f[CR]>\f[R], \f[CR]>>\f[R], \f[CR]<\f[R], etc.).
Defaults to false.
.PP
\f[B]Rule evaluation:\f[R]
.IP "1." 3
Check for dangerous characters (backticks outside single quotes,
unquoted newlines)
.IP "2." 3
Parse command into segments split by operators (\f[CR]&&\f[R],
\f[CR]||\f[R], \f[CR]|\f[R], \f[CR];\f[R])
.IP "3." 3
If redirects detected and \f[CR]allowRedirects\f[R] is not true, command
is denied
.IP "4." 3
Each segment is validated against deny rules first, then allow rules
.IP "5." 3
Subshell contents (\f[CR]$(...)\f[R] and \f[CR](...)\f[R]) are
recursively validated
.IP "6." 3
All segments must pass for the command to be allowed
.PP
\f[B]Examples:\f[R]
.IP
.EX
\f[I]# Allow only npm and git commands\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *"]}\(aq
\f[I]# Allow development commands but deny dangerous ones\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *", "node *"], "deny": ["rm \-rf *", "sudo *"]}\(aq
\f[I]# Allow file operations with redirects\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["cat *", "echo *"], "allowRedirects": true}\(aq
.EE
.RE
.SH ARCHITECTURE
Cline operates on a three\-layer architecture:
.TP
+36
View File
@@ -323,6 +323,42 @@ cline task view
cline task chat
```
# ENVIRONMENT
**CLINE_COMMAND_PERMISSIONS**
: JSON configuration for restricting which shell commands Cline can execute. When set, commands are validated against allow/deny patterns before execution. When not set, all commands are allowed.
Format: `{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}`
**Fields:**
- **allow** (array of strings): Glob patterns for allowed commands. If specified, only matching commands are permitted. Uses `*` to match any characters and `?` to match a single character. Setting allow on anything will deny all others.
- **deny** (array of strings): Glob patterns for denied commands. Deny rules take precedence over allow rules.
- **allowRedirects** (boolean): Whether to allow shell redirects (`>`, `>>`, `<`, etc.). Defaults to false.
**Rule evaluation:**
1. Check for dangerous characters (backticks outside single quotes, unquoted newlines)
2. Parse command into segments split by operators (`&&`, `||`, `|`, `;`)
3. If redirects detected and `allowRedirects` is not true, command is denied
4. Each segment is validated against deny rules first, then allow rules
5. Subshell contents (`$(...)` and `(...)`) are recursively validated
6. All segments must pass for the command to be allowed
**Examples:**
```bash
# Allow only npm and git commands.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
# Allow file operations with redirects
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
```
# ARCHITECTURE
Cline operates on a three-layer architecture:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "1.0.9",
"version": "1.0.10",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "cline-core.js",
"bin": {
-1
View File
@@ -110,7 +110,6 @@ func (m *Manager) ListSettings(ctx context.Context) error {
"dictationSettings",
"autoCondenseThreshold",
"autoApprovalSettings",
"hooksEnabled",
}
// Render each field using the renderer
+1 -1
View File
@@ -79,7 +79,7 @@ func RenderField(key string, value interface{}, censor bool) error {
"planActSeparateModelsSetting", "enableCheckpointsSetting",
"terminalReuseEnabled", "mcpResponsesCollapsed", "strictPlanModeEnabled",
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
"terminalOutputLineLimit", "autoCondenseThreshold", "hooksEnabled":
"terminalOutputLineLimit", "autoCondenseThreshold":
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
return nil
+1 -1
View File
@@ -348,7 +348,7 @@ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
return ChangeInputTypeMsg{
InputType: InputTypeFeedback,
Title: "Your feedback",
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor",
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor\nctrl+c to exit",
}
}
}
+1 -1
View File
@@ -287,7 +287,7 @@ func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error
model := output.NewInputModelWithRegistry(
output.InputTypeMessage,
"Cline is ready for your message...",
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
currentMode,
ih.slashCommandRegistry,
)
-6
View File
@@ -290,12 +290,6 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
return err
}
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
case "hooks_enabled":
val, err := parseBool(value)
if err != nil {
return err
}
settings.HooksEnabled = boolPtr(val)
case "azure_identity":
val, err := parseBool(value)
if err != nil {
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

+2
View File
@@ -148,6 +148,7 @@
"features/hooks/samples"
]
},
"features/jupyter-notebooks",
"features/multiroot-workspace",
"features/plan-and-act",
"features/skills",
@@ -198,6 +199,7 @@
"provider-config/anthropic",
"provider-config/claude-code",
"provider-config/openai",
"provider-config/openai-codex",
"provider-config/openrouter",
"provider-config/cerebras",
"provider-config/deepseek",
+112
View File
@@ -0,0 +1,112 @@
---
title: "Jupyter Notebooks"
description: "AI-assisted editing of Jupyter notebooks with cell-level context awareness"
---
# Jupyter Notebooks
Cline provides comprehensive support for Jupyter notebooks (`.ipynb` files), enabling AI-assisted editing with full cell-level context awareness. This feature was developed in collaboration with Amazon to bring AI coding assistance to data science workflows.
## Getting started
Jupyter notebook support is a built-in feature of Cline. To use it, you just need to have the Jupyter notebook extension enabled in VS Code. Once you open any `.ipynb` file, you'll see AI-assisted buttons in your notebook interface.
## How to use
### Generate Cell
Click the sparkle icon (✨) in the notebook toolbar to generate new cells with AI assistance.
![Generate Cell demo](../assets/jupyter-generate-cell.gif)
**How it works:** The AI receives context from surrounding cells, so it understands the variables and imports already in scope. This means you can reference existing DataFrames, functions, and other objects without re-explaining them.
**Example prompt:** "Create a visualization showing the correlation matrix of numeric columns with a heatmap"
The cell is inserted with proper notebook JSON structure, preserving metadata and ready to execute.
### Explain Cell
Click the Explain button in any cell's title bar to get a detailed explanation of what the cell does.
This is useful for:
- Revisiting old notebooks
- Onboarding to a teammate's analysis
- Understanding complex transformations
**How it works:** Cline extracts the full cell context, including outputs, so explanations can reference actual results like column names, row counts, and computed values.
### Improve Cell
Click the Improve button in any cell's title bar to enhance existing cells with AI suggestions.
![Explain and Improve Cell demo](../assets/jupyter-explain-improve-cell.gif)
Use this to:
- Optimize slow pandas operations
- Add error handling
- Refactor for clarity
- Convert loops to vectorized operations
**How it works:** Cline suggests improvements while preserving the cell's position and metadata in the notebook structure. The AI explains what was changed and why.
## How cell context works
Unlike traditional file editing, Jupyter notebooks are JSON documents containing arrays of cells. Each cell has its own type, source content, metadata, execution count, and outputs.
When you use a Jupyter command, Cline extracts structured context that includes:
- **Cell type** (code, markdown, or raw)
- **Source content** as an array of lines
- **Cell metadata** and configuration
- **Execution count** for code cells
- **Outputs** including data, text, and error traces
This structured representation allows the AI to understand not just the code, but its context within the notebook and its actual output.
### JSON structure preservation
Cline is designed to work carefully with the cell JSON structure, aiming to:
- Keep cell boundaries intact
- Preserve execution counts
- Maintain cell metadata
- Keep outputs associated with their source cells
The AI is specifically prompted to preserve notebook structure, though you should always review changes to ensure your notebook format remains correct.
## Keyboard shortcuts
You can bind any of these commands to keyboard shortcuts for faster access:
1. Open VS Code keyboard shortcuts (Cmd/Ctrl + K, Cmd/Ctrl + S)
2. Search for `cline.jupyterGenerateCell`, `cline.jupyterExplainCell`, or `cline.jupyterImproveCell`
3. Assign your preferred key combinations
## Tips for best results
**For Generate Cell:**
- Be specific about what you want the cell to do
- Reference existing variables by name (the AI can see them)
- Mention preferred libraries if you have a preference (e.g., "use seaborn" or "use plotly")
**For Explain Cell:**
- Works best on cells that have been executed (outputs provide additional context)
- Good for complex chained operations like pandas groupby/merge sequences
**For Improve Cell:**
- Mention what aspect you want to improve (performance, readability, error handling)
- The AI will explain the changes it suggests
## Limitations
- Notebook support requires the Jupyter notebook extension to be enabled in VS Code
- Cell context extraction depends on VS Code's notebook API
- Very large notebooks may require more context than some models can handle efficiently
## Related
- [Cline Tools Guide](/exploring-clines-tools/cline-tools-guide) -- Overview of all Cline commands
- [Model Selection Guide](/core-features/model-selection-guide) -- Choosing the right model for your workflow
+19 -2
View File
@@ -25,11 +25,28 @@ The easiest way to get started is with **Cline** as your provider:
## Alternative: Use Another Provider
If you prefer to use your own API keys, you can select from providers like:
If you prefer to use your own accounts or API keys, you have several options:
### Sign in with OpenAI (Recommended)
The easiest way to use OpenAI models is with **OpenAI Codex**—no API keys needed:
1. Select **"OpenAI Codex"** from the API Provider dropdown
2. Click **"Sign in with OpenAI"**
3. Authorize Cline in your browser
4. Choose your model
<Card title="OpenAI Codex Setup Guide" icon="key" href="/provider-config/openai-codex">
See the full setup guide with screenshots and troubleshooting tips.
</Card>
### Other Providers
You can also use API keys with these providers:
- **OpenRouter** - Great value, multiple models
- **Anthropic** - Direct access to Claude models
- **OpenAI** - Access to GPT models
- **OpenAI** - Access to GPT models via API key
- **Google Gemini** - Google's AI models
- **Ollama** - Run models locally on your computer
-1
View File
@@ -18,7 +18,6 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
Cline supports the following Cerebras models:
- `zai-glm-4.6` - Fast general-purpose model on Cerebras (up to 1,000 tokens/s). To be deprecated soon.
- `zai-glm-4.7` - Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
+105
View File
@@ -0,0 +1,105 @@
---
title: "OpenAI Codex"
sidebarTitle: "OpenAI Codex"
description: "Connect your OpenAI account to Cline via OAuth for seamless access to OpenAI models."
---
OpenAI Codex lets you connect your OpenAI account directly to Cline using OAuth, meaning you don't need to manage API keys. Simply sign in with your OpenAI account through a one-click browser authentication, and you'll automatically have access to all the models available on your OpenAI plan. No additional configuration required.
<Tip>
If you prefer to use API keys instead, see the [OpenAI (API Key)](/provider-config/openai) provider configuration.
</Tip>
## Prerequisites
- An [OpenAI account](https://platform.openai.com/signup)
- An active OpenAI subscription or API access plan
<Note>
The models available to you depend on your OpenAI account's subscription tier. See [OpenAI's pricing page](https://openai.com/pricing) for details on what's included in each plan.
</Note>
## Connecting Your OpenAI Account
<Steps>
<Step title="Open Cline Settings">
Click the settings gear icon in the top-right corner of the Cline extension.
</Step>
<Step title="Select OpenAI Codex Provider">
Choose **"OpenAI Codex"** from the "API Provider" dropdown menu.
</Step>
<Step title="Sign In with OpenAI">
Click the **"Sign in with OpenAI"** button. This will open a browser window for authentication.
</Step>
<Step title="Authorize Cline">
In the browser window that opens:
1. Sign in to your OpenAI account (if not already signed in)
2. Review the permissions Cline is requesting
3. Click **"Authorize"** to grant access
</Step>
<Step title="Select Your Model">
Once authorized, you'll be returned to Cline. Choose your desired model from the **"Model"** dropdown.
</Step>
</Steps>
<Note>
If a model doesn't appear in your dropdown, it may not be available on your OpenAI subscription tier.
</Note>
For the most current list of available models and their capabilities, see the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
## Managing Your Connection
### Disconnecting Your Account
To disconnect your OpenAI account from Cline:
1. Open Cline Settings
2. With OpenAI Codex selected as the provider, click **"Sign out"**
3. Confirm the disconnection
This removes the OAuth connection. You can reconnect at any time by signing in again.
### Token Refresh
OAuth tokens are automatically refreshed by Cline. If you encounter authentication errors:
1. Try disconnecting and reconnecting your account
2. Ensure your OpenAI account is in good standing
3. Check that your subscription is active
## Troubleshooting
### "Authentication failed" error
- Ensure you're signing in with the correct OpenAI account
- Check that your OpenAI subscription is active
- Try clearing your browser cache and signing in again
### Models not appearing
- The available models depend on your OpenAI subscription tier
- Some models may require specific plan upgrades
- Check [OpenAI's pricing page](https://openai.com/pricing) for model availability by plan
### OAuth window doesn't open
- Check if pop-ups are blocked in your browser
- Try using a different browser
- Ensure you have a stable internet connection
### Connection keeps expiring
- This is rare but can happen if your OpenAI account session expired
- Disconnect and reconnect to refresh your authentication
## Good to Know
Cline only requests the permissions necessary to make API calls on your behalf. Your OpenAI credentials are never stored by Cline.
When using OpenAI Codex, Cline accesses models through your OpenAI/ChatGPT subscription. There is no separate per-token API billing or consumption of OpenAI API credits for this provider; usage and limits are governed by your ChatGPT (or OpenAI account) subscription plan, not by Cline.
## Related Documentation
- [OpenAI (API Key)](/provider-config/openai): Alternative configuration using API keys
- [Model Selection Guide](/core-features/model-selection-guide): Help choosing the right model
+48 -65
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.51.0",
"version": "3.53.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.51.0",
"version": "3.53.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -1184,6 +1184,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2646,6 +2647,7 @@
"node_modules/@grpc/grpc-js": {
"version": "1.9.15",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.8",
"@types/node": ">=12.12.47"
@@ -3229,6 +3231,7 @@
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@hono/node-server": "^1.19.7",
"ajv": "^8.17.1",
@@ -3297,6 +3300,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -4912,8 +4916,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4",
@@ -4926,8 +4929,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4",
@@ -4940,8 +4942,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4",
@@ -4954,8 +4955,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4",
@@ -4968,8 +4968,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4",
@@ -4982,8 +4981,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4",
@@ -4996,8 +4994,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4",
@@ -5010,8 +5007,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4",
@@ -5024,8 +5020,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4",
@@ -5038,8 +5033,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4",
@@ -5052,8 +5046,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4",
@@ -5066,8 +5059,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4",
@@ -5080,8 +5072,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4",
@@ -5094,8 +5085,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4",
@@ -5108,8 +5098,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4",
@@ -5122,8 +5111,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.4",
@@ -5136,8 +5124,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4",
@@ -5150,8 +5137,7 @@
"optional": true,
"os": [
"openharmony"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4",
@@ -5164,8 +5150,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4",
@@ -5178,8 +5163,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4",
@@ -5192,8 +5176,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4",
@@ -5206,8 +5189,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@sap-ai-sdk/ai-api": {
"version": "2.1.0",
@@ -6770,8 +6752,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/get-folder-size": {
"version": "3.0.4",
@@ -6803,6 +6784,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -7519,6 +7501,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8320,6 +8303,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -8654,9 +8638,9 @@
}
},
"node_modules/cheerio/node_modules/undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@@ -9525,7 +9509,8 @@
},
"node_modules/devtools-protocol": {
"version": "0.0.1342118",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/diff": {
"version": "5.2.0",
@@ -12476,6 +12461,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -12709,6 +12695,7 @@
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"license": "MPL-2.0",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -14860,9 +14847,9 @@
}
},
"node_modules/open-graph-scraper/node_modules/undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@@ -15430,6 +15417,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15611,7 +15599,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -15632,7 +15619,6 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -16322,7 +16308,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -17800,7 +17785,6 @@
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
@@ -17817,7 +17801,6 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -18178,6 +18161,7 @@
"version": "5.5.3",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -18250,9 +18234,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.19.0.tgz",
"integrity": "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -18430,7 +18414,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -18505,7 +18488,6 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -19193,6 +19175,7 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+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.51.0",
"version": "3.53.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+3 -3
View File
@@ -108,7 +108,6 @@ message Secrets {
message Settings {
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;
@@ -257,7 +256,6 @@ message Settings {
optional FocusChainSettings focus_chain_settings = 149;
optional string custom_prompt = 150;
optional double auto_condense_threshold = 151;
optional bool hooks_enabled = 152;
optional bool subagents_enabled = 153;
optional bool enable_parallel_tool_calling = 154;
optional bool background_edit_enabled = 155;
@@ -278,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 {
@@ -384,6 +383,8 @@ message UpdateTaskSettingsRequest {
// Message for updating settings
message UpdateSettingsRequest {
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
Metadata metadata = 1;
optional ModelsApiConfiguration api_configuration = 2;
optional string telemetry_setting = 3;
@@ -408,7 +409,6 @@ message UpdateSettingsRequest {
optional DictationSettings dictation_settings = 23;
optional double auto_condense_threshold = 24;
optional bool multi_root_enabled = 25;
optional bool hooks_enabled = 26;
optional string vscode_terminal_execution_mode = 27;
optional int32 max_consecutive_mistakes = 28;
optional bool subagents_enabled = 29;
+1
View File
@@ -70,6 +70,7 @@ enum ClineSay {
HOOK_STATUS = 30;
HOOK_OUTPUT_STREAM = 31;
COMMAND_PERMISSION_DENIED = 32;
CONDITIONAL_RULES_APPLIED = 33;
}
// Enum for ClineSayTool tool types
+7 -8
View File
@@ -2,18 +2,18 @@ import * as vscode from "vscode"
import {
cleanupMcpMarketplaceCatalogFromGlobalState,
migrateCustomInstructionsToGlobalRules,
migrateHooksEnabledToBoolean,
migrateTaskHistoryToFile,
migrateWelcomeViewCompleted,
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"
import { ExtensionRegistryInfo } from "./registry"
import { BannerService } from "./services/banner/BannerService"
import { audioRecordingService } from "./services/dictation/AudioRecordingService"
@@ -26,7 +26,6 @@ import { ShowMessageType } from "./shared/proto/host/window"
import { syncWorker } from "./shared/services/worker/sync"
import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
import { getLatestAnnouncementId } from "./utils/announcements"
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
import { arePathsEqual } from "./utils/path"
/**
* Performs intialization for Cline that is common to all platforms.
@@ -35,10 +34,13 @@ import { arePathsEqual } from "./utils/path"
* @returns The webview provider
*/
export async function initialize(context: vscode.ExtensionContext): Promise<WebviewProvider> {
// Configure the shared Logging class to use HostProvider's output channel
Logger.setOutput((msg: string) => HostProvider.get().logToChannel(msg))
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.",
@@ -70,9 +72,6 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
// Ensure taskHistory.json exists and migrate legacy state (runs once)
await migrateTaskHistoryToFile(context)
// Migrate hooksEnabled from ClineFeatureSetting to boolean (one-time cleanup)
await migrateHooksEnabledToBoolean(context)
// Clean up MCP marketplace catalog from global state (moved to disk cache)
await cleanupMcpMarketplaceCatalogFromGlobalState(context)
@@ -127,7 +126,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}`)
}
}
-1
View File
@@ -44,7 +44,6 @@ class ClineEndpoint {
this.environment = Environment.production
break
}
console.info("Cline environment updated: ", this.environment)
}
public getEnvironment(): EnvironmentConfig {
+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,
})
+7 -7
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"
@@ -126,7 +126,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 +143,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 +199,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 +217,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 +260,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
+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
}
}
+3 -3
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
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { type Config, type Message, Ollama } from "ollama"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOllamaMessages } from "../transform/ollama-format"
@@ -93,7 +94,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 +108,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
}
}
+17 -41
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
@@ -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]
+5 -5
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)
}
}
@@ -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
@@ -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")
}
}
+12 -11
View File
@@ -3,7 +3,8 @@ import { describe, it } from "mocha"
import { constructNewFileContent as cnfc } from "./diff"
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
return cnfc(diffContent, originalContent, isFinal, "v2")
const result = await cnfc(diffContent, originalContent, isFinal, "v2")
return result.newContent
}
describe("constructNewFileContent", () => {
@@ -217,13 +218,13 @@ replaced
} else {
const result1 = await cnfc(diff, original, isFinal ?? true)
const result2 = await cnfc2(diff, original, isFinal ?? true)
const _equal = result1 === result2
const _equal2 = result1 === expected
const _equal = result1.newContent === result2
const _equal2 = result1.newContent === expected
// Verify both implementations produce same result
expect(result1).to.equal(result2)
expect(result1.newContent).to.equal(result2)
// Verify result matches expected
expect(result1).to.equal(expected)
expect(result1.newContent).to.equal(expected)
}
})
})
@@ -264,7 +265,7 @@ replaced`
// Should still work and replace line2 with "replaced"
const expected = "line1\nreplaced\nline3"
expect(result1).to.equal(expected)
expect(result1.newContent).to.equal(expected)
})
it("should handle missing final REPLACE marker with multiple lines of replacement", async () => {
@@ -281,7 +282,7 @@ replaced`
const result1 = await cnfc(diff, original, true) // isFinal = true
const expected = "function test() {\n\tconst a = 42;\n\tconsole.log('updated');\n\treturn a;\n}"
expect(result1).to.equal(expected)
expect(result1.newContent).to.equal(expected)
})
// it("should NOT process incomplete replacement when isFinal is false", async () => {
@@ -319,7 +320,7 @@ new second
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "first\nnew second\nthird\nnew fourth\n"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
})
it("should handle multiple out-of-order replacements", async () => {
@@ -342,7 +343,7 @@ fifth
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "one\nsecond\nthree\nfourth\nfifth\n"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
})
it("should handle out-of-order replacements with indentation", async () => {
@@ -360,7 +361,7 @@ fifth
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "function test() {\n\tconst a = 10;\n\tconst b = 2;\n\tconst c = 30;\n\n}"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
})
it("should handle out-of-order replacements with empty lines", async () => {
@@ -380,6 +381,6 @@ new body content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "header\nnew body content\nnew footer\n"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
})
})
+31 -7
View File
@@ -2,6 +2,17 @@ const SEARCH_BLOCK_START = "------- SEARCH"
const SEARCH_BLOCK_END = "======="
const REPLACE_BLOCK_END = "+++++++ REPLACE"
/**
* Converts a character index in a string to a 1-based line number.
* @param content - The full content string
* @param charIndex - The character index in the content
* @returns The 1-based line number where charIndex falls
*/
export function getLineNumberFromCharIndex(content: string, charIndex: number): number {
if (charIndex <= 0) return 1
return content.substring(0, charIndex).split("\n").length
}
const SEARCH_BLOCK_CHAR = "-"
const REPLACE_BLOCK_CHAR = "+"
const LEGACY_SEARCH_BLOCK_CHAR = "<"
@@ -236,7 +247,7 @@ export async function constructNewFileContent(
originalContent: string,
isFinal: boolean,
version: "v1" | "v2" = "v1",
): Promise<string> {
): Promise<{ newContent: string; matchIndices: number[] }> {
const constructor = constructNewFileContentVersionMapping[version]
if (!constructor) {
throw new Error(`Invalid version '${version}' for file content constructor`)
@@ -246,13 +257,17 @@ export async function constructNewFileContent(
const constructNewFileContentVersionMapping: Record<
string,
(diffContent: string, originalContent: string, isFinal: boolean) => Promise<string>
(diffContent: string, originalContent: string, isFinal: boolean) => Promise<{ newContent: string; matchIndices: number[] }>
> = {
v1: constructNewFileContentV1,
v2: constructNewFileContentV2,
} as const
async function constructNewFileContentV1(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
async function constructNewFileContentV1(
diffContent: string,
originalContent: string,
isFinal: boolean,
): Promise<{ newContent: string; matchIndices: number[] }> {
let result = ""
let lastProcessedIndex = 0
@@ -470,7 +485,9 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
result += originalContent.slice(currentPos)
}
return result
// Return all match indices from the replacements
// This is used to determine the line numbers for each SEARCH/REPLACE block in the UI
return { newContent: result, matchIndices: replacements.map((r) => r.start) }
}
enum ProcessingState {
@@ -566,7 +583,7 @@ class NewFileContentConstructor {
this.internalProcessLine(line, true, this.pendingNonStandardLines.length)
}
public getResult() {
public getResult(): { newContent: string; matchIndices: number[] } {
// If this is the final chunk, append any remaining original content
if (this.isFinal && this.lastProcessedIndex < this.originalContent.length) {
this.result += this.originalContent.slice(this.lastProcessedIndex)
@@ -574,7 +591,10 @@ class NewFileContentConstructor {
if (this.isFinal && this.state !== ProcessingState.Idle) {
throw new Error("File processing incomplete - SEARCH/REPLACE operations still active during finalization")
}
return this.result
// Note: V2 implementation doesn't currently track match indices
// For now, return empty array. If V2 becomes the default and we need line numbers,
// we should add state to track all match indices.
return { newContent: this.result, matchIndices: [] }
}
private internalProcessLine(
@@ -800,7 +820,11 @@ class NewFileContentConstructor {
}
}
export async function constructNewFileContentV2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
export async function constructNewFileContentV2(
diffContent: string,
originalContent: string,
isFinal: boolean,
): Promise<{ newContent: string; matchIndices: number[] }> {
const newFileContentConstructor = new NewFileContentConstructor(originalContent, isFinal)
const lines = diffContent.split("\n")
@@ -3,7 +3,8 @@ import { describe, it } from "mocha"
import { constructNewFileContent as cnfc } from "./diff"
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
return cnfc(diffContent, originalContent, isFinal, "v2")
const result = await cnfc(diffContent, originalContent, isFinal, "v2")
return result.newContent
}
describe("Diff Format Edge Cases", () => {
@@ -18,7 +19,7 @@ new content
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
})
@@ -33,7 +34,7 @@ new content
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
})
@@ -48,7 +49,7 @@ new content
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
})
@@ -62,7 +63,7 @@ new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("before\nnew content\nafter")
expect(result1.newContent).to.equal("before\nnew content\nafter")
expect(result2).to.equal("before\nnew content\nafter")
})
@@ -77,7 +78,7 @@ new content
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
})
@@ -92,7 +93,7 @@ new content
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
})
@@ -112,7 +113,7 @@ second new content
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nfirst new content\nafter\nsecond new content\nend"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
})
@@ -132,7 +133,7 @@ second new content
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nfirst new content\nafter\nsecond new content\nend"
expect(result1).to.equal(expectedResult)
expect(result1.newContent).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
})
})
+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)
}
}
}
@@ -0,0 +1,155 @@
import { HostProvider } from "@/hosts/host-provider"
import { extractPathLikeStrings, RuleEvaluationContext, toWorkspaceRelativePosixPath } from "./rule-conditionals"
type WorkspaceRoot = { path: string }
type WorkspaceManagerLike = { getRoots(): WorkspaceRoot[] }
type ClineMessageLike = {
type: string
ask?: string
say?: string
text?: string
}
type MessageStateHandlerLike = {
getClineMessages(): ClineMessageLike[]
}
export type RuleContextBuilderDeps = {
cwd: string
messageStateHandler: MessageStateHandlerLike
workspaceManager?: WorkspaceManagerLike
}
/**
* Builds the evaluation context used for conditional Cline Rules (e.g. YAML frontmatter `paths:`).
*
* Kept in the user-instructions domain so Task remains orchestration-focused.
*
* Path context is gathered from multiple sources in clineMessages:
* - User messages (task, user_feedback)
* - Visible/open tabs
* - Tool results (say="tool") - completed operations
* - Tool requests (ask="tool") - pending operations (captures intent before execution)
*/
export class RuleContextBuilder {
/**
* Maximum number of path candidates to consider for rule activation.
* This cap prevents performance degradation in long-running tasks with many file operations.
*/
static readonly MAX_RULE_PATH_CANDIDATES = 100
static async buildEvaluationContext(deps: RuleContextBuilderDeps): Promise<RuleEvaluationContext> {
return {
paths: await RuleContextBuilder.getRulePathContext(deps),
}
}
/**
* Parse apply_patch input to extract target file paths from patch headers.
* Matches lines like: *** Add File: path/to/file.ts
*/
private static extractPathsFromApplyPatch(input: string): string[] {
if (typeof input !== "string" || !input) return []
const paths: string[] = []
const fileHeaderRegex = /^\*\*\* (?:Add|Update|Delete) File: (.+?)(?:\n|$)/gm
let m: RegExpExecArray | null
while ((m = fileHeaderRegex.exec(input))) {
const filePath = (m[1] || "").trim()
if (filePath) {
paths.push(filePath)
}
}
return paths
}
private static async getRulePathContext(deps: RuleContextBuilderDeps): Promise<string[]> {
const candidates: string[] = []
const clineMessages = deps.messageStateHandler.getClineMessages()
// (1) Current-turn user message evidence:
// Use the most recent user-authored text (initial task or subsequent feedback).
// NOTE: We intentionally prefer the latest user_feedback over the original task to
// support first-turn activation on later turns.
const lastUserMsg = [...clineMessages]
.reverse()
.find((m) => m.type === "say" && (m.say === "user_feedback" || m.say === "task") && typeof m.text === "string")
if (lastUserMsg?.text) {
candidates.push(...extractPathLikeStrings(lastUserMsg.text))
}
// (2) Visible + open tabs
const roots = deps.workspaceManager?.getRoots().map((r) => r.path) ?? [deps.cwd]
const rawVisiblePaths = (await HostProvider.window.getVisibleTabs({}))?.paths ?? []
const rawOpenTabPaths = (await HostProvider.window.getOpenTabs({}))?.paths ?? []
for (const abs of [...rawVisiblePaths, ...rawOpenTabPaths]) {
for (const root of roots) {
const rel = toWorkspaceRelativePosixPath(abs, root)
if (rel) {
candidates.push(rel)
break
}
}
}
// (3) Files edited by Cline during this task (completed operations):
// Parse say="tool" messages for tool results indicating file operations.
for (const msg of clineMessages) {
if (msg.type !== "say" || msg.say !== "tool" || !msg.text) continue
try {
const tool = JSON.parse(msg.text) as { tool?: string; path?: string }
if (
(tool.tool === "editedExistingFile" || tool.tool === "newFileCreated" || tool.tool === "fileDeleted") &&
tool.path
) {
candidates.push(tool.path)
}
} catch {
// ignore parse errors
}
}
// (4) Tool requests (pending operations):
// Parse ask="tool" messages to capture the assistant's intent BEFORE tool execution.
// This enables rule activation even when:
// - The tool hasn't completed yet
// - The tool fails (intent was still expressed)
// - Files don't exist yet (new file creation)
for (const msg of clineMessages) {
if (msg.type !== "ask" || msg.ask !== "tool" || !msg.text) continue
try {
const tool = JSON.parse(msg.text) as {
tool?: string
path?: string
content?: string // apply_patch stores patch content here
}
// Extract path from standard file tools
if (tool.path) {
candidates.push(tool.path)
}
// Handle apply_patch specially: parse patch headers for file paths
if (tool.tool === "applyPatch" && tool.content) {
candidates.push(...RuleContextBuilder.extractPathsFromApplyPatch(tool.content))
}
} catch {
// ignore parse errors
}
}
// Normalize/dedupe/cap
const seen = new Set<string>()
const normalized: string[] = []
for (const c of candidates) {
const posix = c.replace(/\\/g, "/").replace(/^\//, "")
if (!posix || posix === "/") continue
if (seen.has(posix)) continue
seen.add(posix)
normalized.push(posix)
if (normalized.length >= RuleContextBuilder.MAX_RULE_PATH_CANDIDATES) break
}
return normalized.sort()
}
}
@@ -0,0 +1,284 @@
import { expect } from "chai"
import sinon from "sinon"
import { RuleContextBuilder, RuleContextBuilderDeps } from "../RuleContextBuilder"
// Mock HostProvider to avoid actual VSCode API calls
const mockHostProvider = {
window: {
getVisibleTabs: sinon.stub().resolves({ paths: [] }),
getOpenTabs: sinon.stub().resolves({ paths: [] }),
},
}
describe("RuleContextBuilder", () => {
let hostProviderStub: sinon.SinonStub
beforeEach(() => {
// Stub HostProvider to use mock
hostProviderStub = sinon.stub(require("@/hosts/host-provider"), "HostProvider").value(mockHostProvider)
})
afterEach(() => {
sinon.restore()
})
describe("getRulePathContext from ask='tool' messages", () => {
it("extracts path from ask='tool' message with write_to_file", async () => {
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => [
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "write_to_file",
path: "src/components/Button.tsx",
content: "// new file",
}),
},
],
},
}
const context = await RuleContextBuilder.buildEvaluationContext(deps)
expect(context.paths).to.include("src/components/Button.tsx")
})
it("extracts paths from multiple sequential tool requests", async () => {
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => [
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "write_to_file",
path: "src/utils/helper.ts",
}),
},
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "replace_in_file",
path: "src/index.ts",
}),
},
],
},
}
const context = await RuleContextBuilder.buildEvaluationContext(deps)
expect(context.paths).to.include("src/utils/helper.ts")
expect(context.paths).to.include("src/index.ts")
})
it("handles malformed JSON gracefully", async () => {
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => [
{
type: "ask",
ask: "tool",
text: "not valid json {{{",
},
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "write_to_file",
path: "valid/path.ts",
}),
},
],
},
}
// Should not throw and should extract the valid path
const context = await RuleContextBuilder.buildEvaluationContext(deps)
expect(context.paths).to.include("valid/path.ts")
})
it("extracts paths from apply_patch tool request", async () => {
const patchContent = `*** Add File: src/new-feature.ts
+const x = 1
*** Update File: src/existing.ts
---
+++
@@ 1,1 @@
-const y = 2
+const y = 3`
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => [
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "applyPatch",
content: patchContent,
}),
},
],
},
}
const context = await RuleContextBuilder.buildEvaluationContext(deps)
expect(context.paths).to.include("src/new-feature.ts")
expect(context.paths).to.include("src/existing.ts")
})
it("deduplicates paths from multiple sources", async () => {
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => [
{
type: "say",
say: "task",
text: "Update src/index.ts",
},
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "write_to_file",
path: "src/index.ts",
}),
},
{
type: "say",
say: "tool",
text: JSON.stringify({
tool: "editedExistingFile",
path: "src/index.ts",
}),
},
],
},
}
const context = await RuleContextBuilder.buildEvaluationContext(deps)
// Should only appear once despite being in 3 messages
const indexCount = (context.paths ?? []).filter((p) => p === "src/index.ts").length
expect(indexCount).to.equal(1)
})
it("normalizes Windows-style paths to POSIX", async () => {
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => [
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "write_to_file",
path: "src\\components\\Button.tsx",
}),
},
],
},
}
const context = await RuleContextBuilder.buildEvaluationContext(deps)
expect(context.paths).to.include("src/components/Button.tsx")
})
it("respects MAX_RULE_PATH_CANDIDATES limit", async () => {
// Create more messages than the limit
const messages: Array<{ type: string; ask: string; text: string }> = []
for (let i = 0; i < RuleContextBuilder.MAX_RULE_PATH_CANDIDATES + 50; i++) {
messages.push({
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "write_to_file",
path: `src/file${i}.ts`,
}),
})
}
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => messages,
},
}
const context = await RuleContextBuilder.buildEvaluationContext(deps)
expect((context.paths ?? []).length).to.be.at.most(RuleContextBuilder.MAX_RULE_PATH_CANDIDATES)
})
})
describe("extractPathsFromApplyPatch", () => {
it("extracts paths from Add File headers", async () => {
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => [
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "applyPatch",
content: "*** Add File: src/new.ts\n+content",
}),
},
],
},
}
const context = await RuleContextBuilder.buildEvaluationContext(deps)
expect(context.paths).to.include("src/new.ts")
})
it("extracts paths from Update File headers", async () => {
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => [
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "applyPatch",
content: "*** Update File: src/existing.ts\n--- \n+++ \n@@ 1,1 @@\n-old\n+new",
}),
},
],
},
}
const context = await RuleContextBuilder.buildEvaluationContext(deps)
expect(context.paths).to.include("src/existing.ts")
})
it("extracts paths from Delete File headers", async () => {
const deps: RuleContextBuilderDeps = {
cwd: "/workspace",
messageStateHandler: {
getClineMessages: () => [
{
type: "ask",
ask: "tool",
text: JSON.stringify({
tool: "applyPatch",
content: "*** Delete File: src/old.ts",
}),
},
],
},
}
const context = await RuleContextBuilder.buildEvaluationContext(deps)
expect(context.paths).to.include("src/old.ts")
})
})
})
@@ -50,5 +50,24 @@ describe("rule-conditionals", () => {
const res = extractPathLikeStrings("see https://example.com/a/b and edit src/index.ts")
expect(res).to.deep.equal(["src/index.ts"])
})
it("ignores fenced code blocks", () => {
const text =
"Please update src/index.ts\n\n```ts\n// example code\nconst p = 'apps/web/src/App.tsx'\n// also: packages/foo/src\n```\n\nThanks!"
const res = extractPathLikeStrings(text)
expect(res).to.deep.equal(["src/index.ts"])
})
it("does not extract URLs inside code fences", () => {
const text = "```\nSee https://example.com/a/b and src/index.ts\n```\nBut edit docs/readme.md"
const res = extractPathLikeStrings(text)
expect(res).to.deep.equal(["docs/readme.md"])
})
it("extracts paths from stack traces (outside code fences)", () => {
const text = "Error: boom\n at foo (src/index.ts:12:3)\n at bar (apps/web/src/App.tsx:5:1)"
const res = extractPathLikeStrings(text)
expect(res).to.deep.equal(["src/index.ts", "apps/web/src/App.tsx"])
})
})
})
@@ -25,7 +25,7 @@ describe("rule loading with paths frontmatter", () => {
expect(res1.content).to.contain("universal.md")
expect(res1.content).to.contain("scoped.md")
expect(res1.content).to.not.contain("paths:")
expect(res1.activatedConditionalRules.map((r) => r.name)).to.include("scoped.md")
expect(res1.activatedConditionalRules.map((r) => r.name)).to.include("global:scoped.md")
const res2 = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
evaluationContext: { paths: ["docs/readme.md"] },
@@ -109,7 +109,7 @@ describe("rule loading with paths frontmatter", () => {
evaluationContext: { paths: ["src/index.ts"] },
})
expect(res.activatedConditionalRules.map((r) => r.name)).to.deep.equal(files)
expect(res.activatedConditionalRules.map((r) => r.name)).to.deep.equal(files.map((f) => `global:${f}`))
} finally {
await fs.rm(tmp, { recursive: true, force: true })
}
@@ -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 () => {
@@ -1,4 +1,11 @@
import { getRuleFilesTotalContent, synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
import {
ActivatedConditionalRule,
getRemoteRulesTotalContentWithMetadata,
getRuleFilesTotalContentWithMetadata,
RULE_SOURCE_PREFIX,
RuleLoadResultWithInstructions,
synchronizeRuleToggles,
} from "@core/context/instructions/user-instructions/rule-helpers"
import { formatResponse } from "@core/prompts/responses"
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { StateManager } from "@core/storage/StateManager"
@@ -7,24 +14,42 @@ 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"
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
export const getGlobalClineRules = async (
globalClineRulesFilePath: string,
toggles: ClineRulesToggles,
opts?: { evaluationContext?: RuleEvaluationContext },
): Promise<RuleLoadResultWithInstructions> => {
let combinedContent = ""
const activatedConditionalRules: ActivatedConditionalRule[] = []
// 1. Get file-based rules
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, globalClineRulesFilePath, toggles)
if (rulesFilesTotalContent) {
combinedContent = rulesFilesTotalContent
// Note: ruleNamePrefix explicitly set to "global" for clarity (matches the default)
const rulesFilesTotal = await getRuleFilesTotalContentWithMetadata(
rulesFilePaths,
globalClineRulesFilePath,
toggles,
{
evaluationContext: opts?.evaluationContext,
ruleNamePrefix: "global",
},
)
if (rulesFilesTotal.content) {
combinedContent = rulesFilesTotal.content
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`)
}
}
@@ -33,31 +58,35 @@ export const getGlobalClineRules = async (globalClineRulesFilePath: string, togg
const remoteConfigSettings = stateManager.getRemoteConfigSettings()
const remoteRules = remoteConfigSettings.remoteGlobalRules || []
const remoteToggles = stateManager.getGlobalStateKey("remoteRulesToggles") || {}
for (const rule of remoteRules) {
// If alwaysEnabled, always include; otherwise check toggle
const isEnabled = rule.alwaysEnabled || remoteToggles[rule.name] !== false
if (isEnabled) {
if (combinedContent) {
combinedContent += "\n\n"
}
combinedContent += `${rule.name}\n${rule.contents}`
}
const remoteResult = getRemoteRulesTotalContentWithMetadata(remoteRules, remoteToggles, {
evaluationContext: opts?.evaluationContext,
})
if (remoteResult.content) {
if (combinedContent) combinedContent += "\n\n"
combinedContent += remoteResult.content
activatedConditionalRules.push(...remoteResult.activatedConditionalRules)
}
// 3. Return formatted instructions
if (combinedContent) {
return formatResponse.clineRulesGlobalDirectoryInstructions(globalClineRulesFilePath, combinedContent)
if (!combinedContent) {
return { instructions: undefined, activatedConditionalRules: [] }
}
return undefined
return {
instructions: formatResponse.clineRulesGlobalDirectoryInstructions(globalClineRulesFilePath, combinedContent),
activatedConditionalRules,
}
}
export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles) => {
export const getLocalClineRules = async (
cwd: string,
toggles: ClineRulesToggles,
opts?: { evaluationContext?: RuleEvaluationContext },
): Promise<RuleLoadResultWithInstructions> => {
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let clineRulesFileInstructions: string | undefined
let instructions: string | undefined
const activatedConditionalRules: ActivatedConditionalRule[] = []
if (await fileExistsAtPath(clineRulesFilePath)) {
if (await isDirectory(clineRulesFilePath)) {
@@ -68,28 +97,53 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
[".clinerules", "skills"],
])
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
const rulesFilesTotal = await getRuleFilesTotalContentWithMetadata(rulesFilePaths, cwd, toggles, {
evaluationContext: opts?.evaluationContext,
ruleNamePrefix: "workspace",
})
if (rulesFilesTotal.content) {
instructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotal.content)
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 {
if (clineRulesFilePath in toggles && toggles[clineRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalFileInstructions(cwd, ruleFileContent)
const raw = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (raw) {
// Keep single-file .clinerules behavior consistent with directory/remote rules:
// - Parse YAML frontmatter (fail-open on parse errors)
// - Evaluate conditionals against the request's evaluation context
const parsed = parseYamlFrontmatter(raw)
if (parsed.hadFrontmatter && parsed.parseError) {
// Fail-open: preserve the raw contents so the LLM can still see the author's intent.
instructions = formatResponse.clineRulesLocalFileInstructions(cwd, raw)
} else {
const { passed, matchedConditions } = evaluateRuleConditionals(
parsed.data,
opts?.evaluationContext ?? {},
)
if (passed) {
instructions = formatResponse.clineRulesLocalFileInstructions(cwd, parsed.body.trim())
if (parsed.hadFrontmatter && Object.keys(matchedConditions).length > 0) {
activatedConditionalRules.push({
name: `${RULE_SOURCE_PREFIX.workspace}:${GlobalFileNames.clineRules}`,
matchedConditions,
})
}
}
}
}
}
} catch {
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
Logger.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
}
}
}
return clineRulesFileInstructions
return { instructions, activatedConditionalRules }
}
export async function refreshClineRulesToggles(
@@ -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
@@ -46,6 +46,7 @@ const evaluatePathsConditional: ConditionalEvaluatorWithMatch = (frontmatterValu
}
const patterns = frontmatterValue.map((p) => p.trim()).filter(Boolean)
// Policy:
// - `paths` omitted => universal (because this evaluator is never invoked)
// - `paths: []` (or `paths` that trims to no usable patterns) => match nothing (fail-closed)
@@ -111,8 +112,13 @@ export function evaluateRuleConditionals(
export function extractPathLikeStrings(text: string): string[] {
if (!text) return []
// 0) Strip fenced code blocks to avoid extracting paths from pasted code.
// This dramatically reduces false positives from snippets containing `foo/bar` or `a.b.c`.
// Note: We intentionally keep this simple and fail-open (if fences are unbalanced, we do nothing special).
const withoutCodeFences = text.replace(/```[\s\S]*?```/g, " ")
// 1) Remove URLs to avoid false positives.
const withoutUrls = text.replace(/\b\w+:\/\/[^\s]+/g, " ")
const withoutUrls = withoutCodeFences.replace(/\b\w+:\/\/[^\s]+/g, " ")
// 2) Match tokens that look like paths.
// - Either contain at least one slash (e.g. src/index.ts)
@@ -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
@@ -153,25 +154,43 @@ export type ActivatedConditionalRule = {
matchedConditions: Record<string, string[]>
}
// Prefixes used to make activated conditional rule identifiers self-explanatory in the UI.
// NOTE: These are display identifiers (not toggle keys).
export const RULE_SOURCE_PREFIX = {
workspace: "workspace",
global: "global",
remote: "remote",
} as const
export type RuleLoadResult = {
content: string
activatedConditionalRules: ActivatedConditionalRule[]
}
/**
* Result type for rule loading functions that return formatted instructions.
* Used by getGlobalClineRules and getLocalClineRules.
*/
export type RuleLoadResultWithInstructions = {
instructions?: string
activatedConditionalRules: ActivatedConditionalRule[]
}
export const getRuleFilesTotalContentWithMetadata = async (
rulesFilePaths: string[],
basePath: string,
toggles: ClineRulesToggles,
opts?: { evaluationContext?: RuleEvaluationContext },
opts?: { evaluationContext?: RuleEvaluationContext; ruleNamePrefix?: keyof typeof RULE_SOURCE_PREFIX },
): Promise<RuleLoadResult> => {
const evaluationContext = opts?.evaluationContext ?? {}
const prefix = RULE_SOURCE_PREFIX[opts?.ruleNamePrefix ?? "global"]
type RuleLoadPart = {
contentPart: string | null
activatedRule: ActivatedConditionalRule | null
}
const parts: RuleLoadPart[] = await Promise.all(
const parts = await Promise.all(
rulesFilePaths.map(async (filePath) => {
const ruleFilePath = path.resolve(basePath, filePath)
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
@@ -199,7 +218,7 @@ export const getRuleFilesTotalContentWithMetadata = async (
}
const activatedRule =
hadFrontmatter && Object.keys(matchedConditions).length > 0
? { name: ruleFilePathRelative, matchedConditions }
? { name: `${prefix}:${ruleFilePathRelative}`, matchedConditions }
: null
return { contentPart: `${ruleFilePathRelative}\n${body.trim()}`, activatedRule }
@@ -245,7 +264,7 @@ export function getRemoteRulesTotalContentWithMetadata(
if (!passed) continue
if (hadFrontmatter && Object.keys(matchedConditions).length > 0) {
activatedConditionalRules.push({ name: rule.name, matchedConditions })
activatedConditionalRules.push({ name: `${RULE_SOURCE_PREFIX.remote}:${rule.name}`, matchedConditions })
}
if (combinedContent) combinedContent += "\n\n"
@@ -413,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 {}

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