Compare commits

...
Author SHA1 Message Date
Saoud Rizwan d58dc8f36a Create fair-glasses-lick.md 2025-07-14 20:53:45 -07:00
Saoud Rizwan 2a054ef6a5 Add groq to kimi providers 2025-07-14 20:50:02 -07:00
Saoud Rizwan 57c8b8120d Revert "pass linter errors with read_file (#4159)" (#4905)
This reverts commit 5e2b199377.
2025-07-14 20:18:19 -07:00
Saoud Rizwan 5243f0b9b1 Set default kimi provider to together (#4904) 2025-07-14 19:49:45 -07:00
celestial-vault c014060275 remove unused ExtensionStateContext setters (#4865) 2025-07-14 18:35:54 -07:00
celestial-vault d790ce86a0 Add markdown parsing to MCP responses (#4862)
* refactor out useEffect logic

* add markdown parsing to mcp response

* add display mode to global state; simplify state flow

* fix imports after merge conflicts
2025-07-14 18:17:14 -07:00
celestial-vault 5e2b199377 pass linter errors with read_file (#4159)
* pass linter errors with read_file

* changeset

* add code back after merge with main
2025-07-14 16:49:37 -07:00
celestial-vault 9234d0cdc4 [McpResponseDisplay] Refactor out useEffect logic to helper function (#4852)
* refactor out useEffect logic

* refactor: memoize renderSegment with useCallback
2025-07-14 15:29:50 -07:00
Sarah Fortune db1db8c95d Change the name of the cline core script from standalone.js to cline-core.js (#4894)
Update the name of the script so that you can tell from the process name what it is,
standalone.js is too vague to be able to associate it with cline.
2025-07-14 15:22:11 -07:00
Sarah Fortune f53af72643 Update the vscode usages script to separate out vscode.commands.executeCommand calls. (#4891)
vscode.commands.executeCommand runs other vscode commands, so we need the know which commands are being run.
2025-07-14 15:21:27 -07:00
Massimiliano Angelino 260e0d5f8e feat: adding Bedrock Api Keys support (#4728) 2025-07-15 00:19:58 +05:30
celestial-vault 5b68ee5523 Add kimi-k2 as trending model (#4889)
* add kimi k2 as trending model

* changeset

* adjust wording
2025-07-14 11:21:26 -07:00
Ara 3e5abd5e72 Removing reasoning UX for Grok 4 models and correct pricing (#4849)
* Removing reasoning UX for Grok 4 models and correct pricing

* More resiliency
2025-07-13 17:36:41 -07:00
Sarah Fortuneandellipsis-dev[bot] 1ba5873454 DiffViewProvider refactoring for the host bridge (#4877)
* DiffViewProvider refactoring.

Move the platform specific logic for updating the diff out of DiffViewProvider and into VscodeDiffViewProvider.replaceText().
Add a stub handler for replaceText in the ExternalDiffviewProvider.

* Add comment to replaceText()

* Update src/integrations/editor/DiffViewProvider.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-13 16:28:09 -07:00
Sarah Fortune 1bdaf8ef6f Implement openDiffEditor in the ExternalDiffViewProvider (#4875)
Use the host bridge to open the diff view.
2025-07-13 14:43:33 -07:00
pashpashpash 7f6038c74e better balance display (#4867) 2025-07-12 20:36:19 -07:00
Sarah Fortune 2fd9635b97 Add a vscode specific DiffViewProvider, and one for external platforms. (#4866)
* Add a vscode specific DiffViewProvider, and one for external platforms.

Make the DiffViewProvider class abstract, with two implementations the VscodeDiffViewProvider and the ExternalDiffViewProvider.
Move the vscode specific code to open the diff view into VscodeDiffViewProvider.openDiffEditor. The ExternalDiffViewProvider will use the host bridge to open the diff view.
Add a way to get the correct DiffViewProvider for the platform to the host provider.

Right now, there is only platform specific logic for `open`, the other functions like `scroll` and `replaceText` will be added in a follow up PR. The end-state will look like [this](https://github.com/cline/cline/compare/main...sjf-gg), but it's easier to test and review each part separately.

* In the VscodeDiffViewProvider, use the previous way of getting the open diff editor, before it was switched to the host bridge.

Using the host bridge doesn't help here, because we really need a reference to the actual vscode text editor document, which the host bridge can't return. So, it was doing openDocument, and then immediately using the vscode SDK to get the editor reference. When, really the diff editor was already open, and we only need the editor reference.
2025-07-12 12:45:57 -07:00
Sarah Fortune 568b834338 Add DiffService to the host bridge. (#4841)
* Add DiffService to the host bridge.

Add a new diff service with a method to open the diff view for a file.
On vscode, we will not use the hostbridge for opening the diff editor, so the vscode handler just throws an error.

* Update diff.proto

* Fix proto import
2025-07-12 12:43:55 -07:00
381e9b9d1f v3.18.14 Release Notes (#4857)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-12 02:47:50 -07:00
Saoud Rizwan d86861629d Fix re-sign in flow (#4856)
* Fix re-sign in flow

* Fix AuthState user representation to webview to fix issue where invalid auth was still showing as logged in

* Fix comments

* Create stale-peas-give.md
2025-07-12 02:45:18 -07:00
7fb10ba053 v3.18.13 Release Notes (#4846)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-12 02:03:43 -07:00
Saoud RizwanandCopilot b7ca95ed57 Replace firebase user object session logic with google endpoint to get id token and custom jwt validation (#4853)
* Replace firebase user object session logic with google endpoint to get id token and custom jwt validation

* Add comments

* Update src/services/account/ClineAccountService.ts

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

* Create green-drinks-rest.md

* Remove signOut

* Ensure that refresh token is passed properly in params

* Name function better and add comment; fix org switch logic

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-12 02:00:52 -07:00
Saoud Rizwan 6bd8726dd6 Saoudrizwan/show resignin button (#4855)
* Show sign in button when cline account shows auth error

* Add comment
2025-07-12 01:00:42 -07:00
Saoud Rizwan 347d4f48da Remove cancelReason when retrying request to show the proper animation in ChatRow (#4854) 2025-07-12 00:59:44 -07:00
canvrno baa5aaa0a7 git branch analysis workflow (#4717) 2025-07-11 17:46:46 -07:00
16f066dcbf Host bridge migration: showErrorMessage, showInformationMessage, showWarningMessage (#4745)
* host bridge migration: showErrorMessage & showInformationMessage & showWarningMessage

Introduces the `showMessage` host API to the VS Code extension, allowing the host to display informational, warning, and error messages to the user.

The changes include:

- A new `ShowMessageRequest` and `SelectedResponse` message definition in `proto/host/window.proto` to define the request and response structure for showing messages.
- A new file `src/hosts/vscode/window/showMessage.ts` that implements the `showErrorMessage`, `showInformationMessage`, and `showWarningMessage` functions. These functions use the VS Code API to display messages based on the `ShowMessageRequest`.
- Replace all current call sites with the new implementations

* define the request structure

This commit introduces the `showErrorMessage`, `showInformationMessage`, and `showWarningMessage` host APIs to the VS Code extension. These APIs allow the host to display informational, warning, and error messages to the user.

The changes include:

- Added `ShowErrorMessageRequest`, `ShowInformationMessageRequest`, and `ShowWarningMessageRequest` message definitions in `proto/host/window.proto` to define the request structure for showing messages.
- Added new files `src/hosts/vscode/window/showErrorMessage.ts`, `src/hosts/vscode/window/showInformationMessage.ts`, and `src/hosts/vscode/window/showWarningMessage.ts` that implement the corresponding functions. These functions use the VS Code API to display messages based on the provided message and options.

* Simplify showMessage functions and use options array

The `showErrorMessage`, `showInformationMessage`, and `showWarningMessage` functions in `src/hosts/vscode/window/` have been refactored for simplification.

- The functions now directly destructure the `modal`, `detail`, and `items` properties from the input object.
- The VS Code API calls now directly pass the `modal` and `detail` options, and use the spread operator to pass the `items.options` array as additional arguments. This removes the need for conditional logic to construct the options object.

* wip: apply feedback

* Update src/integrations/git/commit-message-generator.ts

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

* merge conflict

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-11 17:29:01 -07:00
celestial-vault 59f42c7a81 [AccountView] - Default balance to dashes (#4844)
* default balance to dashes

* changeset

* add balance formatter
2025-07-12 05:55:47 +05:30
celestial-vault 3a86938a56 pass extension version in headers (#4843) 2025-07-11 16:16:23 -07:00
Sarah Fortune 042bf359a9 DiffViewProvider refactoring for the host bridge (#4836)
* DiffViewProvider refactoring

Move the vscode specific code to setup and open the diff view editor into `openDiffEditor`.

* Change openDiffEditor to return void instead of returning a vscode specific editor type
2025-07-11 14:18:26 -07:00
Beeandabeatrix 17200740a8 Trigger auth status update on secret storage change (#4837)
* Trigger auth status update on secret storage change

The auth status should be updated when the clineAccountId secret changes. This commit adds a listener to the secrets.onDidChange event and calls sendAuthStatusUpdate when the clineAccountId secret changes. This ensures that the auth status is always up-to-date.

* restore

* afgain

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-11 12:53:24 -07:00
Sarah Fortune c38f443ec4 DiffViewProvider clean-up (#4835)
* In the DiffViewProvider, store the absolutePath instead of the cwd.

Remove unused var `scrollListener`.

* Remove cwd param

* Don't call getCwd in a loop
2025-07-11 10:49:13 -07:00
13f1f0d44b v3.18.12 Release Notes (#4819)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.12

---------

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: pashpashpash <nik@cline.bot>
2025-07-10 19:50:20 -07:00
pashpashpash 30a169e0c3 supporting buy_credits_url response from backend (#4823) 2025-07-10 19:33:25 -07:00
Ara 77ab7f8ef9 Fix the flaky Cline provider switching toggle (#4791)
* Fix Flaky Cline provider toggle

* More resiliency
2025-07-10 19:32:19 -07:00
akfosterandellipsis-dev[bot] 6d5ea98026 fix: insufficient credits display (#4821)
* fix: insufficient credits display

* add changeset

* Update src/api/providers/cline.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-10 19:21:59 -07:00
Sarah Fortune 1879798b68 Remove unused vars from the DiffViewProvider (#4818)
Remove `lastFirstVisibleLine`, this value is set, but never read.
Remove `shouldAutoScroll`, this is always true.
2025-07-10 16:10:13 -07:00
3ed47fba17 Request validation and remove credit balance for cline team (#4817)
* Fix: Ensure Cline client is initialized with the latest auth token

The Cline client was not being re-initialized with the latest authentication token after the user signs in. This resulted in the client using an outdated or non-existent token, leading to authentication errors when making API requests.

This commit ensures that the Cline client is initialized with the most recent authentication token by setting the `apiKey` property of the `OpenAI` client instance to the current auth token retrieved from `AuthService` before every request. This guarantees that the client always uses the valid and up-to-date token for authentication.

* changeset

* move this._authService.getAuthToken() to ensureClient

* Request validation and remove credit balance for cline team

Add request validation for Cline API requests and fix the user interface for displaying credit-related information in UI.

The changes include:

- **Credit Balance Validation:** Implemented `validateRequest` method in `ClineAccountService` to check user's credit balance before making API requests.  Requests from active organizations are skipped. An error is thrown if the balance is insufficient.
- **Error Handling:** Improved error handling in `ClineHandler` to provide more informative error messages to the user.
- **UI Enhancements:**
    - Updated `CreditLimitError` component to display the current balance that matches the account view balance format (4 decimal places).
    - Modified `ChatRow` to parse error messages and display the `CreditLimitError` component when applicable.
    - Updated `AccountView` to only display credit balance for user accounts, not organization accounts.
    - Removed unused props from `CreditLimitError` component. Context: https://cline-space.slack.com/archives/C08KYBFL9DJ/p1752182278852399?thread_ts=1752164726.247429&cid=C08KYBFL9DJ
- **Dependencies:** Updated dependencies in `webview-ui` to include `tailwindcss` and configured `tailwind.config.js` to support VSCode theme variables.

* changeset

* Update src/api/providers/cline.ts

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

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-10 15:58:13 -07:00
Sarah Fortune 439e99d8d1 Fix script that packages the standalone zip. (#4816)
The vscode packager VCE has different logic that the npm module `ignore`, so it was not including the same files as VCE.

Just copy how VCE uses the .vscodeignore file in the packaging script.

Fix the ignore for demo.gif, it was still getting included because it was only matching demo.gif at the top level.
Also ignore .github and .husky.
2025-07-10 15:57:22 -07:00
github-actions[bot] 8de99c90a6 v3.18.11 Release Notes (#4814) 2025-07-10 13:49:09 -07:00
Beeandabeatrix 5b475fe88c Fix: Ensure Cline client is initialized with the latest auth token (#4813)
* Fix: Ensure Cline client is initialized with the latest auth token

The Cline client was not being re-initialized with the latest authentication token after the user signs in. This resulted in the client using an outdated or non-existent token, leading to authentication errors when making API requests.

This commit ensures that the Cline client is initialized with the most recent authentication token by setting the `apiKey` property of the `OpenAI` client instance to the current auth token retrieved from `AuthService` before every request. This guarantees that the client always uses the valid and up-to-date token for authentication.

* changeset

* move this._authService.getAuthToken() to ensureClient

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-10 13:13:30 -07:00
Beeandabeatrix 190d3bd2dc Fix: Improve auth flow and state validation (#4786)
Improves the authentication flow and state validation process.  We no longer reset the auth nounce after each sign in as AuthService is a singleton and there is no risk of nonce collision between different users as only one user can be signed in at a time.

Changes:
    - The `authNonce` is now generated once during `AuthService` instantiation and stored as a read-only property. This ensures that the nonce remains consistent throughout the authentication process.
    - The `resetAuthNonce` method has been removed, as the nonce is no longer meant to be reset.
    - The `createAuthRequest` method now uses the URL object for more graceful query construction.
- **Controller:**
    - The `validateAuthState` method has been simplified to directly compare the provided state with the stored `authNonce`.
- **Extension:**
    - The extension now prompts the user for confirmation if the state parameter in the auth callback does not match the stored `authNonce`. This allows sign-ins initiated from outside the extension (e.g., Cline web) to be handled correctly.

Issue: The issue is that the authNonce is being reset in the validateAuthState method in the Controller, but the extension.ts is directly accessing authService.authNonce without going through the validation method. This creates a race condition where:

User initiates auth, nonce is generated
Auth callback comes back with the state
If there are multiple auth attempts or the callback is processed multiple times, the nonce might be reset before the validation in extension.ts happens
User gets "Invalid auth state" error

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-10 21:53:36 +05:30
d0069eb7cb Changeset version bump (#4785)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.9 with improved descriptions

* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and package.json for version 3.18.10

* ahugosaia'ohs'gasgh

---------

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: pashpashpash <nik@cline.bot>
2025-07-10 00:05:06 -07:00
pashpashpash 268bbec7f1 recommend grok 4 (#4798)
* recommend grok 4

* changeset
2025-07-09 23:42:04 -07:00
pashpashpash 192cd2602e Pashpashpash/grok 4 (#4793)
* grok 4

* numbers
2025-07-09 23:36:06 -07:00
Andrei EternalandAndrei Edell c724edd118 Replace use of vscode.fs.writeFile with node equivalent (#4790)
* Replace use of vscode.fs.writeFile with node equivalent

* format fix

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-07-09 18:11:47 -07:00
celestial-vault 0eaf350d87 Provider handler param types (#4729)
* add provider handler options types and only pass those fields when building

* field was removed in merge, remove it
2025-07-09 16:33:27 -07:00
Ara 398bc87a64 Adding Thinking Token Config to Gemini 2.5 pro model and adding gemini 2.5 flash preview (#4668)
* Adding Thinking Token Config to Gemini 2.5 pro model

* Adding Thinking Token Config to Gemini 2.5 pro model

* Adding console logs

* Adding console logs

* Adding console logs
2025-07-10 03:42:07 +05:30
0ec447c992 v3.18.9 Release Notes (#4783)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.9 with improved descriptions

---------

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: pashpashpash <nik@cline.bot>
2025-07-09 14:44:25 -07:00
pashpashpash ba41131b36 Pashpashpash/enabling streaming cline (#4784)
* grok 4

* fixing streaming in cline provider

* removing grok promo from cline provider

* changeset

* oopsie

* oopsie
2025-07-09 14:33:55 -07:00
Beeandabeatrix 0042230acd Handle authentication errors for Cline provider (#4781)
* Fix: Cline authentication errors

This commit improves error handling for Cline authentication. It adds checks for the presence of a Cline account authentication token before making API requests. If the token is missing, it throws an "Unauthorized" error, prompting the user to sign in. Additionally, it catches `ERR_BAD_REQUEST` or 401 errors from the Cline API and throws the same "Unauthorized" error, providing a more user-friendly experience when authentication fails.

* add changeset

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-10 02:15:27 +05:30
ncrypted | Oliver 9ff705ffc0 Remove: Pricing for SAP AI Core Provider (#4778)
* remove: pricing for sapaicore

* add: model description hint regarding capacity unit pricing
2025-07-09 10:38:16 -07:00
Dennise Bartlett dea016408c Update Changelog and Versions (#4751) 2025-07-08 20:13:37 -07:00
Frostbourne bf82444aec grok 3 cache pricing (#4750) 2025-07-08 19:59:17 -07:00
988b65f1ad v3.18.7 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.7

---------

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: pashpashpash <nik@cline.bot>
2025-07-08 18:41:16 -07:00
Dennise Bartlettandfrostbournesb d838bcdc34 Fix account buttons (#4742)
* Update links for account buttons

* add dashboard url

* setup links for future changes

---------

Co-authored-by: frostbournesb <frostbournesb@protonmail.com>
2025-07-08 17:56:38 -07:00
pashpashpash ff1e3297a8 ending grok promo in UI (#4740)
* ending grok promo in UI

* changeset
2025-07-08 17:30:06 -07:00
github-actions[bot] 2428389620 v3.18.6 Release Notes
v3.18.6 Release Notes
2025-07-08 16:16:52 -07:00
6f8627bb5f Organization Accounts added to Extension. Refactor Auth components for Accounts
* Working organization and personal inference, account switching, and usage/credit reporting.

* Organization dropdown tweaks (#4710)

* organization dropdown tweaks

* reverted AuthService

* Update Firebase Provider and Auth Service to support re-hydration of the user credentials

* Add Github Auth Flow

* Fix merge conflicts

* Fix some dumb typing

* recreate dropdown value when initialized (#4716)

* added changeset

* Update urls to production and handle some PR concerns

* Get user credits when signing in (#4736)

* get user balance when signing in instead of when there is no active org

* swapped order of getUserCredits, made calls async

* async changes continued, moved setIsLoading to finally

---------

Co-authored-by: canvrno <46584286+canvrno@users.noreply.github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-08 15:59:08 -07:00
Beeandabeatrix 1e81d98abf Fix fresh install mode launch config (#4732)
* Fix fresh install mode launch config

Updates the launch configuration in `.vscode/launch.json` to include a temporary profile and user data directory. This fixes the issue where the launch config does not start in fresh install mode for extension development. This change prevents  interference from existing settings and extensions. The `--user-data-dir=/tmp/cline/user` argument specifies a temporary directory for user data, while `--profile-temp` ensures a clean profile is used for each launch. Also, `--sync=off` is added to disable settings sync.

* update name

* tmp dir

* Implement in-memory storage for temporary profiles

Adds in-memory storage for global state, workspace state, and secrets when running in a temporary profile. This is determined by the `TEMP_PROFILE` environment variable being set to "true". When active, the `updateGlobalState`, `getGlobalState`, `updateGlobalStateBatch`, `updateSecretsBatch`, `storeSecret`, `getSecret`, `updateWorkspaceState`, and `getWorkspaceState` functions will use `Map` objects to store and retrieve data instead of VS Code's `globalState`, `secrets`, and `workspaceState` APIs. This ensures that no data is persisted to disk when using a temporary profile, providing a clean environment for testing and development.

* Refactor tmp user directory for dev launch config

This commit refactors the temporary user directory used in the development launch configuration.

- Updates `.vscode/launch.json` to use `${workspaceFolder}/dist/tmp/user` for the `--user-data-dir` argument, ensuring the temporary profile is located within the workspace.
- Adds `TEMP_PROFILE: "true"` to the environment variables in `.vscode/launch.json` to enable in-memory storage for temporary profiles.
- Renames the `clean-sandbox` task in `.vscode/tasks.json` to `clean-tmp-user` and modifies its command to remove and recreate the `${workspaceFolder}/dist/tmp/user` directory. This ensures a clean environment for each launch.

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-08 15:05:11 -07:00
Sarah Fortune 267170920a Update the packaging script for the standalone app to include the same files as the extension in the zip (#4708)
* Package the same files for the standalone app and the extension

When packaging the standalone app use the same .vscodeignore that the vscode packager uses to decide which files to include.
Exclude extra files from the vscode extension that aren't needed: dist-standalone, old_docs and eslint-rules.

* Ignore the whole directory, not just the contents.

* Don't package .DS_Store files

* Update comment
2025-07-08 11:16:49 -07:00
Sarah Fortune 386c78c114 Update the no-vscode-postmessage eslint rule to check for any of the vscode SDK calls that have been replaced (#4715)
* Update the no-vscode-postmessage eslint rule to check for all the vscode SDK calls that have been replaced.

Expand the rule to check for all of the vscode SDK calls that have been
switched to the host bridge or replaced with native functions.

* Remove redundant messages
2025-07-08 10:50:10 -07:00
Sarah Fortune 265a56391a Replace vscode.workspace.fs.stat with fs.stat (#4714)
Use the util function that already exists isDirectory from utils/fs.ts
Use the same function in getRelativePaths
2025-07-08 10:49:56 -07:00
canvrno ff4bab22fb host bridge migration - openExternal (#4502) 2025-07-08 10:47:12 -07:00
tjandy98 bc468707a6 Add header for SAP AI Core Tracking (#4696)
* Add support for Gemini 2.5 Pro and Flash models

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

* Create calm-ads-glow.md

* Update request header

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

Add ai-client-type header for tracking

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

Create little-lions-joke.md

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

update changeset

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

update

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

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2025-07-07 21:09:41 -07:00
Sarah Fortuneandellipsis-dev[bot] 0a6a565d41 Replace vscode.workspace.asRelativePath with the host bridge. (#4712)
* Replace vscode.workspace.asRelativePath with the host bridge.

Add a util function asRelativePath to path.ts that does the same thing as the vcode API (returns the path relative to the workspace directory).
In the getRelativePaths protobus handler, don't allow @mentions for files outside the workspace, they do not work in cline, so just prevent them from being added at all.
If the fs.stat fails for a file, don't @mention it either, if stat() fails it means the file doesn't exist or is unreadable.

* Update src/core/controller/file/getRelativePaths.ts

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

* Update src/core/controller/file/getRelativePaths.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-07 19:40:14 -07:00
d30e4d0194 v3.18.5 Release Notes (#4704)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.5

---------

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: Cline Evaluation <cline@example.com>
2025-07-07 19:12:41 -07:00
AraandCline Evaluation d453eed582 feat: Globally persist plan/act mode across sessions and ensure proper workspace level persistance of chatsettings(for language) (#4646)
* feat: Persist plan/act mode across sessions

- Add mode persistence to global state storage
- Load saved mode on controller initialization
- Update state keys to include 'mode' as a valid global state key
- Ensures user's selected mode (plan/act) is maintained between VS Code sessions

* Split chat settings storage between global and workspace state
- Move mode setting to global state for cross-workspace persistence
- Store other chat settings in workspace state for project-specific configuration
- Update state retrieval logic to merge global mode with workspace settings
- Add chatSettings to LocalStateKey type definitions

* Code Cleanup

* Code Cleanup

* Get the chatsettings back to global

* Get the chatsettings back to global

* Get the chatsettings back to global

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-07 18:23:41 -07:00
Ara 7e32314c0b Optimize provider switching performance with batched storage operations (#4676)
* Optimize provider switching performance with batched storage operations

* Optimize provider switching performance with batched storage operations
2025-07-07 18:20:55 -07:00
Ara cce8f09ae5 Fixing bug on accepting tool response (#4675) 2025-07-07 18:19:50 -07:00
BeeandBeatrix Woo 0262e13ac4 Capture token usage for conversation turns in telementry (#4703)
Adds capture of token usage data for conversation turns in the telemetry service. This includes tokensIn, tokensOut, cacheWriteTokens, cacheReadTokens, and totalCost.

The changes involve:

- Modifying the `captureConversationTurnEvent` method in `TelemetryService.ts` to accept and include token usage data in the captured event properties.
- Updating the `Task` class in `src/core/task/index.ts` to pass token usage information when capturing conversation turn events. This ensures that token usage is tracked for both regular and cached responses.
- Refactor capture event to use object destructuring for easier readability

Co-authored-by: Beatrix Woo <beatrix@cline.bot>
2025-07-08 04:54:06 +05:30
celestial-vault 6d2cf55fc5 migrate download MCP response to proto (#4682) 2025-07-07 14:58:49 -07:00
Sarah Fortune 3577c2efa9 Remove uri service from the host bridge. (#4660)
It is being replaced with the npm module vscode-uri.
2025-07-07 14:27:19 -07:00
Sarah Fortune f97ef745d9 Replace vscode.workspace.getWorkspaceFolder() with the host bridge (#4659)
* Replace vscode.workspace.getWorkspaceFolder() with the host bridge

Use the hostbridge getWorkspacePaths() and use the result to
check for the workspaceFolder of the current file open in the IDE.

* Organize imports

* Update isLocatedInWorkspace() to check all the workspace directories, not just the first.

Add utility function to check if a path is inside a directory instead of duplicating the logic.

* Remove stubs for workspaceFolders that are not needed anymore.
2025-07-07 14:23:26 -07:00
schardosin 4e27e06670 SAP AI Core small bug fix and reorder models (#4686)
* small fix to avoid exception and removed log of returned data

* reorganized sapaicore models to be grouped in logical groups

* updated changese with changes in SAP AI Core

* removed additional received data log sections
2025-07-07 15:44:27 -05:00
ef02d6b0b2 Changeset version bump (#4685)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update package.json

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-07 13:26:11 -07:00
celestial-vaultandSaoud Rizwan 7ab6189595 mark welcomeViewCompleted as true after auth redirect (#4699)
* mark welcomeViewCompleted as true after auth redirect

* Create real-ravens-nail.md

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-07 13:14:48 -07:00
celestial-vault 4beaa2a086 sync local search state after timeout (#4698) 2025-07-07 11:16:08 -07:00
Ara 5f90018ab5 Revert "remove blur (#4664)" (#4677) 2025-07-06 12:05:30 -06:00
celestial-vault 9e761cd1f0 lazily initialize provider sdks with error catching (#4661) 2025-07-06 11:41:16 -05:00
吴天一 e84f2ff962 remove blur (#4664) 2025-07-06 00:13:23 -05:00
tjandy98 1842254c57 Add support for Gemini 2.5 Pro and Flash models to SAP AI Core Provider (#4655)
* Add support for Gemini 2.5 Pro and Flash models

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

* Create calm-ads-glow.md

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2025-07-05 12:10:00 -05:00
pashpashpashandCline Evaluation 4a2dad4552 showOpenDialogue host bridge migration (#4651)
* showOpenDialogue host bridge migration

* addressing comments

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-03 22:55:19 -07:00
Sarah Fortune 3248c37358 Replace uses of vscode workspaceFolders with host bridge (#4649)
* Instead of getting the cwd from the workspaceFolders use the host bridge util getCwd()

Replace uses in integrations/claude-code/run.ts

* Organize imports
2025-07-03 15:53:53 -07:00
Sarah Fortune 172b46f1b0 Remove unused files *.bak (#4648)
* Remove unused file Checkpoint-test-utils.ts.bak

* Remove all .bak files from src/integrations
2025-07-03 15:36:21 -07:00
Sarah Fortune 9578d7cde1 Instead of getting the cwd from the workspaceFolders use the host bridge util getCwd() (#4647)
Remove top-level property cwd task, await can't be used at the top level.
Make cwd a class property, and pass the cwd into the constructor of task (await can't be used in the constructor either).
2025-07-03 15:36:08 -07:00
Sarah Fortune 2979d47e01 Replace cwd from the workspaceFolders with the host bridge. (#4645)
Don't export cwd from task/index.ts. This top-level property cwd will be removed in a following PR because await cannot be used at the top-level.
Use the hostbridge util getCwd() in createRuleFile.ts and refreshRules.ts instead of import the cwd from `task`.
Add a util function to get the desktop directory instead of constructing it multiple places, update uses with the new function getDesktopDir()
Replace function getCwd in FileContextTracker.ts with just getCwd from paths.ts.
2025-07-03 15:08:52 -07:00
4569300f00 v3.18.3 Release Notes (#4644)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.3

---------

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: Cline Evaluation <cline@example.com>
2025-07-03 15:06:57 -07:00
ToshiiandCline Evaluation 36e3f4cdd3 add log + run options (#4630)
* log + options

* changeset

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-03 14:54:10 -07:00
Tomás BarreiroandAra bc5225ce52 Improve Claude Code handling (#4619)
* Prevent filling the chat with error messages

* Improve env variables and remove the magic number

* Add changeset

* refactor

* Update run.ts

---------

Co-authored-by: Ara <arafat.da.khan@gmail.com>
2025-07-03 14:41:59 -07:00
Kevin TaylorandCline Evaluation 50dc89b551 Strip thinking tokens from Cerebras reasoning model inputs (#4635)
* Strip thinking tokens from Cerebras reasoning model inputs

Filter out thinking tokens in message history

* changeset

* changeset

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-03 14:40:58 -07:00
celestial-vault d576b68cca Move plan/act model settings to global storage (#4636)
* move model settings to global storage

* cleanup

* only run migration if globalState key is undefined
2025-07-03 16:24:14 -05:00
github-actions[bot] 6c2c0780ee v3.18.2 Release Notes (#4581) 2025-07-02 22:15:45 -07:00
pashpashpashandCline Evaluation 021a014012 setting claude 4 as best model (#4637)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-02 21:45:30 -07:00
Ara ef1a68b5e3 Adding a troubleshooting guide for terminal problems (#4592) 2025-07-02 19:08:42 -07:00
Tomás Barreiro 72029fe205 feat: Introduce Thinking Budget customization for Claude Code (#4618) 2025-07-03 06:37:32 +05:30
celestial-vault 2af151e736 [SettingsView] [ApiConfig Section] save on change (#4554)
* refactor out apiconfig section

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder

* refactor out debug section

* pull out about section

* implement save on change and remove confirmation modals for api config section

* add doc strings to new hook functions

* add debounced text field for smooth typing

* use context value directly for apiProvider dropdown value; remove unecessary memo; remove keys from ApiOptions

* add welcomeViewCompleted state boolean to control welcome view showing

* refactor other sections to save-on-change; remove form diff calculation logic

* cleanup

* remove memo

* make welcomeViewCompleted context value initial value false
2025-07-02 19:39:49 -05:00
canvrno 64963c4e9c confirmation popup when deleting tasks (#4627) 2025-07-01 22:41:58 -07:00
Toshii 52571ccee8 base (#4600) 2025-07-01 17:08:28 -07:00
87322feeb7 Fall back to getting current terminal content when shell integration API fails to return output (#4605)
* Revert to when terminal process worked more reliably

* Get last terminal output if no output is retrieved

* Fix getting terminal output for when shell integration unavailable

* Apply suggestions from code review

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

* Revert removing previous changes

* Update TerminalProcess to emit current terminal contents instead of a silent command completion message

* Revert when first chunk fails message

* Revert error title

* Fixing tests with fake timers

---------

Co-authored-by: Dennise Bartlett <bartlett.dc.1@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 16:57:36 -07:00
kevinneung 27f8372c5b add cline.walkthrough to command handler (#4621)
added cline.walkthrough to command handler, since it would throw an error otherwise
2025-07-02 05:20:02 +05:30
Sarah Fortune b3d3e9861f Use the host bridge to get the cwd in autoApprove.ts (#4601)
Update the ToolExecutor to use await because the approval is now async.
2025-07-01 16:01:33 -07:00
pashpashpashandCline Evaluation 01178909ee adding unique remote git urls to context on first message env variables (#4622)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 15:15:10 -07:00
Sarah Fortune c982216113 Cleanup: Remove unused property and param postMessage from ClineAccountService (#4620)
* Remove unused property and param postMessage

* Remove unused property and param postMessage

Remove unused import.
2025-07-01 14:44:41 -07:00
261 changed files with 8372 additions and 6370 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add groq to kimi providers
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add Kimi-K2 as the trending model in the Cline Provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Refactor chat view into multiple modular files
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Include litellm_session_id as part of chat completion requests
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add Claude Sonnet 4 and Opus 4 model in SAP AI Core provider.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Added API Key support for Bedrock integration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: Do not read auth variables from the user env when using Claude Code
@@ -0,0 +1,75 @@
# Git Diff Analysis Workflow
## Objective
Analyze the current branch's changes against main to provide informed insights and context for development decisions.
## Step 1: Gather Git Information
<important>Do not return any text or conversation other than what is necessary to run these commands</important>
**First, check the expected output size:**
```shell
(git branch --show-current && echo "=== STATUS ===" && git status --porcelain | cat && echo "=== COMMIT MESSAGES ===" && git log main..HEAD --oneline | cat && echo "=== CHANGED FILES ===" && git diff main --name-only | cat && echo "=== FULL DIFF ===" && git diff main | cat) | wc -l
```
**If the expected line count is greater than 500 lines, use the file-based approach:**
```shell
git branch --show-current > cline-git-analysis.temp && echo "=== STATUS ===" >> cline-git-analysis.temp && git status --porcelain >> cline-git-analysis.temp && echo "=== COMMIT MESSAGES ===" >> cline-git-analysis.temp && git log main..HEAD --oneline >> cline-git-analysis.temp && echo "=== CHANGED FILES ===" >> cline-git-analysis.temp && git diff main --name-only >> cline-git-analysis.temp && echo "=== FULL DIFF ===" >> cline-git-analysis.temp && git diff main >> cline-git-analysis.temp
```
Then, read the file using the read_file tool. After you have read the file but before you proceed with subsequent steps, delete it:
```shell
rm cline-git-analysis.temp
```
**If the expected line count is 500 lines or fewer, use the direct approach:**
```shell
git branch --show-current && echo "=== STATUS ===" && git status --porcelain | cat && echo "=== COMMIT MESSAGES ===" && git log main..HEAD --oneline | cat && echo "=== CHANGED FILES ===" && git diff main --name-only | cat && echo "=== FULL DIFF ===" && git diff main | cat
```
<important>If using the direct approach, pipe outputs through `cat` to avoid interactive terminals. If the user's shell is not bash/zsh, adjust the command and chaining
syntax accordingly.</important>
## Step 2: Silent, Structured Analysis Phase
- Analyze all git output without providing commentary or narration
- Read the full diff to understand the scope and nature of changes
- Identify patterns, architectural modifications, or potential impacts
- Use `read_file` to examine any related files providing additional context on the changes you have observed
## Step 3: Context Gathering
- Analyze related code without providing commentary or narration
- Read relevant related source files if needed for complete understanding
- Check dependencies, imports, or cross-references spanning the changes
- Understand the broader codebase context around modifications
- This additional context gathering should include related backend code, as well as related ui/frontend code
- You will typically need to analyze at least several files, potentially many, in order to fully complete this step
- You should not continue reading additional context if you have exhausted more than 60% of your available context window
- If you have exhausted less than 40% of your context window, you should continue reviewing additional context
## Step 4: Ready for User Interaction
**Only after completing the full analysis:**
- Engage with the user based on comprehensive understanding
- Provide insights about specific modifications and their impacts
- If you are certain they exist, note potential breaking changes or compatibility issues
- Answer questions with informed context from the complete change set and context gathering
- If the user has not provided a question, or the question is insufficient to provide a quality response, ask brief (one sentence) clarifying questions.
- Only offer recommendations if they are applicable to the user's request and relevant to the changes that you have observed
## Key Rules
- **No prose or conversation during git research phase**
- **No prose or conversation during context gathering phase**
- **Complete all analysis before any user interaction**
- **Use gathered information for all subsequent questions and insights**
- **Focus on understanding the complete picture before discussing**
## Optional: Additional Analysis Commands
For deeper investigation when needed:
```shell
# Detailed commit history with author info
git log main..HEAD --format="%h %s (%an)" | cat
# Change statistics
git diff main --stat | cat
# Specific file type changes
git diff main --name-only | grep -E '\.(ts|js|tsx|jsx|py|md)$' | cat
+1
View File
@@ -22,6 +22,7 @@
"react-hooks/exhaustive-deps": "off",
"eslint-rules/no-protobuf-object-literals": "error",
"eslint-rules/no-grpc-client-object-literals": "error",
"eslint-rules/no-direct-vscode-api": "warn",
"no-restricted-syntax": [
"error",
{
+4 -3
View File
@@ -23,19 +23,20 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--user-data-dir=${workspaceFolder}/dist/tmp/user",
"--profile-temp",
"--sync",
"off",
"--sync=off",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}"
],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "clean-sandbox",
"preLaunchTask": "clean-tmp-user",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"env": {
"IS_DEV": "true",
"TEMP_PROFILE": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
+2 -2
View File
@@ -233,10 +233,10 @@
"type": "shell"
},
{
"label": "clean-sandbox",
"label": "clean-tmp-user",
"type": "shell",
"dependsOn": ["watch"],
"command": "rm -rf .vscode-dev"
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
}
],
"inputs": [
+7 -2
View File
@@ -2,20 +2,24 @@
.vscode/**
.vscode-test/**
out/**
dist-standalone/**
node_modules/**
src/**
standalone/**
.gitignore
.yarnrc
esbuild.js
vsc-extension-quickstart.md
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
**/.vscode-test.*
eslint-rules/**
.github/**
.husky/**
# Custom
demo.gif
**/demo.gif
.nvmrc
.gitattributes
.prettierignore
@@ -32,6 +36,7 @@ webview-ui/node_modules/**
# Ignore docs
docs/**
old_docs/**
# Fix issue where codicons don't get packaged (https://github.com/microsoft/vscode-extension-samples/issues/692)
!node_modules/@vscode/codicons/dist/codicon.css
+67
View File
@@ -1,5 +1,72 @@
# Changelog
## [3.18.14]
- Fix bug where Cline account users logged in with invalid token would not be shown as logged out in webview presentation layer
## [3.18.13]
- Fix authentication issue where Cline accounts users would keep getting logged out or seeing 'Unexpected API response' errors
## [3.18.12]
- Fix flaky organization switching behavior in Cline provider that caused UI inconsistencies and double loading
- Fix insufficient credits error display to properly show error messages when account balance is too low
- Improve credit balance validation and error handling for Cline provider requests
## [3.18.11]
- Fix authentication issues with Cline provider by ensuring the client always uses the latest auth token
## [3.18.10]
- Update recommended fast & cheap model to Grok 4 in OpenRouter model picker
- Fix Gemini 2.5 Pro thinking budget slider and add support for Gemini 2.5 Flash Lite Preview model (Thanks @arafatkatze!)
## [3.18.9]
- Fix streaming reliability issues with Cline provider that could cause connection problems during long conversations
- Fix authentication error handling for Cline provider to show clearer error messages when not signed in and prevent recursive failed requests
- Remove incorrect pricing display for SAP AI Core provider since it uses non-USD "Capacity Units" that cannot be directly converted (Thanks @ncryptedV1!)
## [3.18.8]
- Update pricing for Grok 3 model because the promotion ended
## [3.18.7]
- Remove promotional "free" messaging for Grok 3 model in UI
## [3.18.6]
- Update request header to include `"ai-client-type": "Cline"` to SAP Api Provider
- Add organization organization accounts
## [3.18.5]
- Fix Plan/Act mode persistence across sessions and multi-workspace conflicts
- Improve provider switching performance by 18x (from 550ms to 30ms) with batched storage operations
- Improve SAP AI Core provider model organization and fix exception handling (Thanks @schardosin!)
## [3.18.4]
- Add support for Gemini 2.5 Pro and Flash to SAP AI Core Provider
- Fix logging in with Cline account not getting past welcome screen
## [3.18.3]
- Improve Cerebras Qwen model performance by removing thinking tokens from model input (Thanks @kevint-cerebras!)
- Improve Claude Code provider with better error handling and performance optimizations (Thanks @BarreiroT!)
## [3.18.2]
- Fix issue where terminal output would not be captured if shell integration fails by falling back to capturing the terminal content.
- Add confirmation popup when deleting tasks
- Add support for Claude Sonnet 4 and Opus 4 model in SAP AI Core provider (Thanks @lizzzcai!)
- Add support for `litellm_session_id` to group requests in a single session (Thanks @jorgegarciarey!)
- Add "Thinking Budget" customization for Claude Code (Thanks @BarreiroT!)
- Fix issue where the extension would use the user's environment variables for authentication when using Claude Code (Thanks @BarreiroT!)
## [3.18.1]
- Add support for Claude 4 Sonnet in SAP AI Core provider (Thanks @GTxx!)
+5
View File
@@ -147,6 +147,7 @@
"pages": [
"provider-config/anthropic",
"provider-config/claude-code",
"provider-config/aws-bedrock-with-apikey-authentication",
"provider-config/aws-bedrock-with-credentials-authentication",
"provider-config/aws-bedrock-with-profile-authentication",
"provider-config/gcp-vertex-ai",
@@ -170,6 +171,10 @@
"running-models-locally/ollama"
]
},
{
"group": "Troubleshooting",
"pages": ["troubleshooting/terminal-quick-fixes", "troubleshooting/terminal-integration-guide"]
},
{
"group": "More Info",
"pages": ["more-info/telemetry"]
@@ -14,6 +14,8 @@ Certain scenarios may warrant using local models, including handling highly sens
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
#### [AWS Bedrock setup for API Keys](/provider-config/aws-bedrock-with-apikey-authentication)
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/provider-config/aws-bedrock-with-credentials-authentication)
#### [AWS Bedrock setup for SSO token (AWS Profile)](/provider-config/aws-bedrock-with-profile-authentication)
@@ -58,3 +58,16 @@ When you use the terminal mention in your message, here's what happens behind th
6. The AI can now "see" the complete terminal output with all formatting preserved
This process happens automatically whenever you use the terminal mention, giving the AI access to your command results, error messages, and other terminal output without you having to copy it manually.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal mentions or terminal integration in general (such as "Shell Integration Unavailable" or commands not showing output), please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
Common issues include:
- Terminal mentions not capturing output
- "Shell Integration Unavailable" messages in Cline chat
- Commands executing but output not visible to Cline
- Terminal integration working inconsistently
The troubleshooting guide provides platform-specific solutions and detailed configuration steps to resolve these issues.
@@ -74,8 +74,25 @@ This approach ensures that all terminal output, including colors and formatting,
- **Select specific output when needed**: By default, the integration captures all terminal content, but you can also select specific lines before right-clicking to focus on just the relevant output.
- **Combine with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Combine terminal outputs with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Use for build and test output**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
- **Contextualize build & test outputs with the terminal**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
Next time you're staring at a cryptic error message in your terminal, try using Cline's terminal integration instead of copying and pasting. You'll get more accurate help because Cline can see the complete terminal context with proper formatting.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal integration, such as "Shell Integration Unavailable" or commands not showing output, please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
The troubleshooting guide covers:
- Common terminal integration issues and quick fixes
- Platform-specific solutions for Windows, macOS, and Linux
- Shell-specific configurations for zsh, bash, PowerShell, and more
- Advanced debugging techniques
- Terminal settings optimization
<Tip>
**Quick Fix**: Most terminal issues can be resolved by switching to bash in the Cline settings and increasing the shell
integration timeout to 10 seconds.
</Tip>
@@ -0,0 +1,135 @@
---
title: "AWS Bedrock"
description: "Learn how to set up AWS Bedrock with Cline using credentials authentication. This guide covers AWS environment setup, regional access verification, and secure integration with the Cline VS Code extension."
---
### Overview
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
- **Developer Focus:** This guide is tailored for individual developers that want to enable access to frontier models via AWS Bedrock with a simplified setup using API Keys.
---
### Step 1: Prepare Your AWS Environment
#### 1.1 Individual user setup - Create a Bedrock API Key
For more detailed instructions check the [documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html).
1. **Sign in to the AWS Management Console:**\
[AWS Console](https://aws.amazon.com/console/)
2. **Access Bedrock Console:**
- [Bedrock Console](https://console.aws.amazon.com/bedrock)
- Create a new Long Lived API Key. This API Key will have by default the `AmazonBedrockLimitedAccess` IAM policy
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
#### 1.2 Create or Modify the Policy
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
- `bedrock:InvokeModel`
- `bedrock:InvokeModelWithResponseStream`
- `bedrock:CallWithBearerToken`
You can create a custom IAM policy with these permissions and attach it to your IAM user or role.
1. In the AWS IAM console, create a new policy.
2. Use the JSON editor to add the following policy document:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:CallWithBearerToken"],
"Resource": "*" // For enhanced security, scope this to specific model ARNs if possible.
}
]
}
```
3. Name the policy (e.g., `ClineBedrockInvokeAccess`) and attach it to the IAM user associated with the key you created. The IAM user and the API key have the same prefix.
**Important Considerations:**
- **Model Listing in Cline:** The minimal permissions (`bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`) are sufficient for Cline to _use_ a model if you specify the model ID directly in Cline's settings. If you rely on Cline to dynamically list available Bedrock models, you might need additional permissions like `bedrock:ListFoundationModels`.
- **AWS Marketplace Subscriptions:** For third-party models (e.g., Anthropic Claude), the **`AmazonBedrockLimitedAccess`** policy grants you the necessary permissions to subscribe via the AWS Marketplace. There is no explicit access to be enabled. For Anthropic models you are still required to submit a First Time Use (FTU) form via the Console. If you get the following message in the Cline chat `[ERROR] Failed to process response: Model use case details have not been submitted for this account. Fill out the Anthropic use case details form before using the model.` then open the [Playground in the AWS Bedrock Console](https://console.aws.amazon.com/bedrock/home?#/text-generation-playground), select any Anthropic model and fill in the form (you might need to send a prompt first)
---
### Step 2: Verify Regional and Model Access
#### 2.1 Choose and Confirm a Region
1. **Select a Region:**\
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
2. **Verify Model Access:**
- **Note:** Some models are only accessible via an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html). In such case check the box "Cross Region Inference".
---
### Step 3: Configure the Cline VS Code Extension
#### 3.1 Install and Open Cline
1. **Install VS Code:**\
Download from the [VS Code website](https://code.visualstudio.com/).
2. **Install the Cline Extension:**
- Open VS Code.
- Go to the Extensions Marketplace (`Ctrl+Shift+X` or `Cmd+Shift+X`).
- Search for **Cline** and install it.
#### 3.2 Configure Cline Settings
1. **Open Cline Settings:**
- Click on the settings ⚙️ to select your API Provider.
2. **Select AWS Bedrock as the API Provider:**
- From the API Provider dropdown, choose **AWS Bedrock**.
3. **Enter Your AWS API Key:**
- Input your **API Key**
- Specify the correct **AWS Region** (e.g., `us-east-1` or your enterprise-approved region).
4. **Select a Model:**
- Choose an on-demand model (e.g., **anthropic.claude-3-5-sonnet-20241022-v2:0**).
5. **Save and Test:**
- Click **Done/Save** to apply your settings.
- Test the integration by sending a simple prompt (e.g., "Generate a Python function to check if a number is prime.").
---
### Step 4: Security, Monitoring, and Best Practices
1. **Secure Access:**
- Prefer AWS SSO/federated roles over long-lived API Key when possible.
- [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
2. **Enhance Network Security:**
- Consider setting up [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-overview.html) to securely connect to Bedrock.
3. **Monitor and Log Activity:**
- Enable AWS CloudTrail to log Bedrock API calls.
- Use CloudWatch to monitor metrics like invocation count, latency, and token usage.
- Set up alerts for abnormal activity.
4. **Handle Errors and Manage Costs:**
- Implement exponential backoff for throttling errors.
- Use AWS Cost Explorer and set billing alerts to track usage.\
[AWS Cost Management](https://docs.aws.amazon.com/cost-management/latest/userguide/what-is-aws-cost-management.html)
5. **Regular Audits and Compliance:**
- Periodically review IAM roles and CloudTrail logs.
- Follow internal data privacy and governance policies.
---
### Conclusion
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models.
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
For further details, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your internal cloud team. Happy coding!
---
_This guide will be updated as AWS Bedrock and Cline evolve. Always refer to the latest documentation and internal policies for up-to-date practices._
@@ -5,7 +5,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
### Overview
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Titan) through AWS.\
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
- **Enterprise Focus:** This guide is tailored for organizations with established AWS environments (using IAM roles, AWS SSO, AWS Organizations, etc.) to ensure secure and compliant usage.
@@ -25,7 +25,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
#### 1.2 Attach the Required Policies
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockFullAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
- `bedrock:InvokeModel`
- `bedrock:InvokeModelWithResponseStream`
@@ -52,8 +52,8 @@ You can create a custom IAM policy with these permissions and attach it to your
**Option 2: Using a Managed Policy (Simpler Initial Setup)**
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockFullAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
[View AmazonBedrockFullAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockLimitedAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
**Important Considerations:**
@@ -71,8 +71,8 @@ You can create a custom IAM policy with these permissions and attach it to your
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
2. **Verify Model Access:**
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Titan) are marked as "Access granted."
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html) if not available on-demand.
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Nova) are marked as "Access granted."
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html) if not available on-demand.
#### 2.2 Set Up AWS Marketplace Subscriptions (if needed)
@@ -138,7 +138,7 @@ You can create a custom IAM policy with these permissions and attach it to your
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockFullAccess` policy, and ensure necessary permissions.
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models and subscribe via AWS Marketplace if needed.
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
@@ -0,0 +1,399 @@
---
title: "Terminal Integration Troubleshooting Guide"
sidebarTitle: "Terminal Troubleshooting"
description: "Complete guide to resolving terminal integration issues in Cline"
---
This guide helps you resolve terminal integration issues in Cline. Terminal integration is crucial for Cline to execute commands and read their output, enabling it to understand errors, test results, and command responses.
<Tip>
If you're experiencing terminal issues, try switching to a simpler shell like `bash` in the Cline settings, under "Terminal Settings"
This resolves most terminal integration problems.
</Tip>
## Quick Diagnosis Flowchart
Follow this flowchart to quickly identify your issue:
```mermaid
graph TD
A[Terminal Issue] --> B{Can Cline execute commands?}
B -->|No| C[Shell Integration Unavailable]
B -->|Yes| D{Can Cline see the output?}
D -->|No| E[Output Capture Failed]
D -->|Yes| F{Is the output corrupted?}
F -->|Yes| G[Character Filtering Issue]
F -->|No| H{Does the command hang?}
H -->|Yes| I[Long-Running Command Issue]
H -->|No| J[Check Terminal Settings]
C --> K[Try Solution 1]
E --> L[Try Solution 2]
G --> M[Try Solution 3]
I --> N[Try Solution 4]
style A fill:#f9f,stroke:#333,stroke-width:2px
style K fill:#9f9,stroke:#333,stroke-width:2px
style L fill:#9f9,stroke:#333,stroke-width:2px
style M fill:#9f9,stroke:#333,stroke-width:2px
style N fill:#9f9,stroke:#333,stroke-width:2px
```
## Common Issues & Quick Solutions
### 1. Shell Integration Unavailable
**Symptoms:**
- Message: "Shell Integration Unavailable"
- Commands execute but Cline can't read output
- Terminal works fine manually but not with Cline
**Quick Solutions:**
#### macOS
- **Switch to bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Disable Oh-My-Zsh temporarily**:
1. If using zsh, enter `mv ~/.zshrc ~/.zshrc.backup` into the terminal
2. Restart VSCode
- **Set environment**:
1.a For Zsh users, use one of the following Zsh commands to edit your shell profile:
- `nano ~/.zshrc`
- `vim ~/.zshrc`
- `code ~/.zshrc`
1.b For Bash users
- nano ~/.bash_profile
2. Add the following to your shell config: `export TERM=xterm-256color`
3. Save your configuration
#### Windows
- **Use PowerShell 7**
1. Install from Microsoft Store
2. Go to Cline Settings
3. Left-Click the **"Terminal Settings"** tab
4. Navigate to **"Default Terminal Profile"** and select **"PowerShell 7"** from the drop-down menu
- **Disable Windows ConPTY**
1. Navigate to your VSCode Settings
2. Enter "Integrated: Windows Enable Conpty" into the Settings searchbar
3. Uncheck the option
- **Try Command Prompt**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"Command Prompt"** from the drop-down menu
#### Linux
- **Use bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Check permissions**
1. Ensure VSCode has terminal access permissions
- **Disable custom prompts**
1. Comment out prompt customizations in `.bashrc`
### 2. Command Output Not Visible
**Symptoms:**
- Cline states in chat: "[Command is running but producing no output]"
- Commands complete but Cline doesn't see results
- Commands work sometimes but not consistently
**Solutions:**
- **Increase Shell Integration Timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable Terminal Reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
- **Check for interfering extensions**
1. Disable other terminal-related VSCode extensions
### 3. Character Filtering Issues
**Symptoms:**
- Commas missing from output (JSON appears corrupted)
- Special characters stripped from terminal output
- Syntax errors that don't appear when running manually
**Solution:**
This is a known bug in output processing. Workarounds:
- Recommend AI to use file output instead
1. Tell Cline in chat or Cline rules, to use `command > output.txt` before reading the file/s
<Tip>
This family of issues is only partially solved in the latest Cline versions, so if you still face this, create a GitHub issue
if it is a persistent problem.
</Tip>
### 4. Long-Running Commands & Progress Bars
**Symptoms:**
- Docker builds never complete in Cline
- Progress bars consume thousands of tokens
- The Cline button "Proceed while running" doesn't work properly in chat
<Tip>
This family of issues has been solved in latest Cline versions but if you still face any issues, then create a GitHub issue
for this.
</Tip>
## Terminal Settings Explained
Access these in Cline by clicking the settings icon, and navigating to the "Terminal Settings" section:
### Default Terminal Profile
- **What it does**: Selects which shell Cline uses for commands
- **When to change**: If experiencing shell integration issues with your default shell
- **Recommended**: - macOS: bash (if zsh has issues) - Windows: PowerShell 7 - Linux: bash
### Shell Integration Timeout
- **What it does**: How long Cline waits for the terminal to be ready
- **Default**: 4 seconds
- **When to increase**:
- Slow shell startup (heavy .zshrc/.bashrc)
- WSL environments
- SSH connections
- **Recommended**: - Start with 10 seconds if having issues
### Enable Aggressive Terminal Reuse
- **What it does**: Reuses existing terminals even if not in the correct directory
- **When to disable**:
- Commands execute in wrong directory
- Virtual environment issues
- Terminal state corruption
- **Trade-off**: - Disabling creates more terminals but ensures clean state
### Terminal Output Line Limit
- **What it does**: Limits how many lines Cline reads from terminal output
- **Default**: 500 lines
- **When to adjust**:
- Increase for verbose build outputs
- Decrease if hitting token limits
- Set to 100 for commands with progress bars
## Platform-Specific Solutions
### macOS Issues
#### Oh-My-Zsh Conflicts
Oh-My-Zsh often interferes with shell integration. Solutions:
1. Create a minimal `.zshrc` for VSCode:
```bash
# ~/.zshrc-vscode
export TERM=xterm-256color
export PAGER=cat
# Minimal PATH and environment setup
```
2. Configure VSCode to use it:
```json
{
"terminal.integrated.env.osx": {
"ZDOTDIR": "~/.zshrc-vscode"
}
}
```
#### macOS 15+ Issues
Recent macOS versions have stricter terminal permissions:
1. System Preferences → Privacy & Security → Developer Tools
2. Add Visual Studio Code
3. Restart VSCode completely
### Windows Issues
#### PowerShell Execution Policy
If commands fail silently:
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
#### WSL Integration
For WSL issues:
1. Use WSL extension for VSCode
2. Open folder in WSL: `code .` from WSL terminal
3. Select "WSL Bash" as terminal profile in Cline
#### Path Issues
Windows path problems:
1. Use forward slashes in Cline: `C:/Users/...`
2. Quote paths with spaces: `"C:/Program Files/..."`
3. Avoid `~` - use full paths
### Linux/SSH/Container Issues
#### SSH Connections
For remote development:
1. Install Cline on the remote machine, not locally
2. Use SSH extension's integrated terminal
3. Increase timeout to 15+ seconds
#### Docker Containers
When developing in containers:
1. Install Cline in the container
2. Use Dev Containers extension
3. Ensure shell integration scripts are available
## Shell-Specific Fixes
### Zsh
```bash
# Add to ~/.zshrc
export TERM=xterm-256color
export PAGER=cat
# Disable fancy prompts for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1="%n@%m %1~ %# "
fi
```
### Bash
```bash
# Add to ~/.bashrc
export TERM=xterm-256color
export PAGER=cat
# Simple prompt for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1='\u@\h:\w\$ '
fi
```
### Fish
```fish
# Add to ~/.config/fish/config.fish
set -x TERM xterm-256color
set -x PAGER cat
# Disable fancy features in VSCode
if test "$TERM_PROGRAM" = "vscode"
function fish_prompt
echo (whoami)'@'(hostname)':'(pwd)'> '
end
end
```
### PowerShell
```powershell
# Add to $PROFILE
$env:PAGER = "cat"
# Disable progress bars
$ProgressPreference = 'SilentlyContinue'
```
## Advanced Troubleshooting
### Debug Mode
Enable terminal debugging to see what's happening:
1. Open VSCode Command Palette (Cmd/Ctrl+Shift+P)
2. Run: "Developer: Set Log Level..."
3. Choose "Trace"
4. Check Output panel → "Cline" for terminal logs
### Manual Shell Integration Test
Test if shell integration works at all:
```bash
# In VSCode terminal
echo $TERM_PROGRAM # Should show "vscode"
echo $VSCODE_SHELL_INTEGRATION # Should be "1"
```
## FAQ
### Why does Cline create so many terminals?
When shell integration fails, Cline can't reuse terminals safely (they might be running long processes). Enable shell integration or adjust the terminal reuse setting.
### Can I use my custom shell (nushell, xonsh, etc.)?
Cline officially supports bash, zsh, fish, and PowerShell. Custom shells may work but aren't guaranteed. Use bash as a fallback.
### Why do some commands work but others don't?
Commands that use interactive features (pagers, progress bars, curses) often fail. Set `PAGER=cat` and use non-interactive flags.
### How do I know if shell integration is working?
Working integration shows command output in Cline's chat. Failed integration shows "Shell Integration Unavailable" or "[Command is running but producing no output]".
## Still Having Issues?
If you've tried everything:
1. **Collect Debug Info**:
```bash
echo "Shell: $SHELL"
echo "Term: $TERM"
echo "VSCode: $TERM_PROGRAM"
which bash
bash --version
```
2. **Report the Issue**:
- Use `/reportbug` in Cline github issues
- Include your debug info
- Mention which solutions you tried
<Tip>
Remember: Most terminal issues are resolved by switching to bash and increasing the timeout. Start there before trying complex
solutions.
</Tip>
@@ -0,0 +1,51 @@
---
title: "Terminal Quick Fixes"
sidebarTitle: "Terminal Quick Fixes"
description: "Quick solutions for common terminal issues"
---
**Here is a list of common fixes, starting with the most applicable:**
- **Switch to bash** (solves most instances)
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down
- **Increase timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable terminal reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
## Platform-Specific Fixes
### macOS + Oh-My-Zsh
```bash
# Create minimal config for VSCode
echo 'export TERM=xterm-256color' > ~/.zshrc-vscode
echo 'export PAGER=cat' >> ~/.zshrc-vscode
```
### Windows PowerShell
```powershell
# Run as Administrator
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
### WSL
- Open folder from WSL: `code .`
- Select **"WSL Bash"** in Cline settings, under **"Terminal Settings"**
- Increase **"Shell integration timeout (seconds)"** to **15**
## Full Guide
For detailed troubleshooting, see the [Complete Terminal Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
+2 -2
View File
@@ -153,8 +153,8 @@ const extensionConfig = {
// Standalone-specific configuration
const standaloneConfig = {
...baseConfig,
entryPoints: ["src/standalone/standalone.ts"],
outfile: `${destDir}/standalone.js`,
entryPoints: ["src/standalone/cline-core.ts"],
outfile: `${destDir}/cline-core.js`,
// These gRPC protos need to load files from the module directory at runtime,
// so they cannot be bundled.
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
@@ -0,0 +1,123 @@
const { RuleTester: DirectApiRuleTester } = require("eslint")
const noDirectVscodeApiRule = require("../no-direct-vscode-api")
const directApiRuleTester = new DirectApiRuleTester({
parser: require.resolve("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
})
directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
valid: [
// Should allow vscode.postMessage in grpc-client-base.ts
{
code: `vscode.postMessage({ type: "grpc_request", data: {} })`,
filename: "grpc-client-base.ts",
},
{
code: `vscode.postMessage({ type: "grpc_request_cancel" })`,
filename: "/path/to/grpc-client-base.ts",
},
// Should allow in exception directories
{
code: `vscode.workspace.workspaceFolders`,
filename: "/src/hosts/vscode/host-bridge.ts",
},
{
code: `vscode.workspace.fs.stat(uri)`,
filename: "/standalone/runtime-files/helpers.ts",
},
// Should allow other vscode API calls
{
code: `vscode.window.showInformationMessage("Hello")`,
filename: "test.ts",
},
// Should allow postMessage calls on other objects
{
code: `window.postMessage({ type: "test" }, "*")`,
filename: "test.ts",
},
// Should allow variables named vscode but not calling postMessage
{
code: `const vscode = { other: "method" }; vscode.other()`,
filename: "test.ts",
},
],
invalid: [
// Should disallow vscode.postMessage in regular files
{
code: `vscode.postMessage({ type: "test", data: {} })`,
filename: "test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should disallow vscode.postMessage in components
{
code: `vscode.postMessage({ type: "apiConfiguration", apiConfiguration })`,
filename: "ApiOptions.tsx",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should disallow vscode.postMessage in test files
{
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
filename: "test.test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should disallow property access for disallowed APIs
{
code: `const folders = vscode.workspace.workspaceFolders;`,
filename: "workspace.ts",
errors: [
{
messageId: "useHostBridge",
},
],
},
// Should disallow method calls for disallowed APIs
{
code: `const relativePath = vscode.workspace.asRelativePath(filePath);`,
filename: "path-utils.ts",
errors: [
{
messageId: "usePathUtils",
},
],
},
// Should disallow nested property access
{
code: `const stats = await vscode.workspace.fs.stat(uri);`,
filename: "file-utils.ts",
errors: [
{
messageId: "useFsUtils",
},
],
},
// Should disallow getting a workspace folder
{
code: `const folder = vscode.workspace.getWorkspaceFolder(uri);`,
filename: "path-helper.ts",
errors: [
{
messageId: "usePathUtils",
},
],
},
],
})
@@ -1,74 +0,0 @@
const { RuleTester: VscodeRuleTester } = require("eslint")
const vscodePostmessageRule = require("../no-vscode-postmessage")
const vscodeRuleTester = new VscodeRuleTester({
parser: require.resolve("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
})
vscodeRuleTester.run("no-vscode-postmessage", vscodePostmessageRule, {
valid: [
// Should allow vscode.postMessage in grpc-client-base.ts
{
code: `vscode.postMessage({ type: "grpc_request", data: {} })`,
filename: "grpc-client-base.ts",
},
{
code: `vscode.postMessage({ type: "grpc_request_cancel" })`,
filename: "/path/to/grpc-client-base.ts",
},
// Should allow other vscode API calls
{
code: `vscode.window.showInformationMessage("Hello")`,
filename: "test.ts",
},
// Should allow postMessage calls on other objects
{
code: `window.postMessage({ type: "test" }, "*")`,
filename: "test.ts",
},
// Should allow variables named vscode but not calling postMessage
{
code: `const vscode = { other: "method" }; vscode.other()`,
filename: "test.ts",
},
],
invalid: [
// Should ban vscode.postMessage in regular files
{
code: `vscode.postMessage({ type: "test", data: {} })`,
filename: "test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should ban vscode.postMessage in components
{
code: `vscode.postMessage({ type: "apiConfiguration", apiConfiguration })`,
filename: "ApiOptions.tsx",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should ban vscode.postMessage in test files
{
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
filename: "test.test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
],
})
+3 -3
View File
@@ -1,13 +1,13 @@
// eslint-rules/index.js
const noProtobufObjectLiterals = require("./no-protobuf-object-literals")
const noGrpcClientObjectLiterals = require("./no-grpc-client-object-literals")
const noVscodePostmessage = require("./no-vscode-postmessage")
const noDirectVscodeApi = require("./no-direct-vscode-api")
module.exports = {
rules: {
"no-protobuf-object-literals": noProtobufObjectLiterals,
"no-grpc-client-object-literals": noGrpcClientObjectLiterals,
"no-vscode-postmessage": noVscodePostmessage,
"no-direct-vscode-api": noDirectVscodeApi,
},
configs: {
recommended: {
@@ -15,7 +15,7 @@ module.exports = {
rules: {
"local/no-protobuf-object-literals": "error",
"local/no-grpc-client-object-literals": "error",
"local/no-vscode-postmessage": "error",
"local/no-direct-vscode-api": "warn",
},
},
},
+164
View File
@@ -0,0 +1,164 @@
const { ESLintUtils } = require("@typescript-eslint/utils")
const path = require("path")
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
// Configuration of disallowed VSCode APIs and their recommended alternatives
const disallowedApis = {
"vscode.postMessage": {
messageId: "useGrpcClient",
},
"vscode.workspace.fs.stat": {
messageId: "useFsUtils",
},
"vscode.workspace.workspaceFolders": {
messageId: "useHostBridge",
},
"vscode.workspace.asRelativePath": {
messageId: "usePathUtils",
},
"vscode.workspace.getWorkspaceFolder": {
messageId: "usePathUtils",
},
}
module.exports = createRule({
name: "no-direct-vscode-api",
meta: {
type: "problem",
docs: {
description:
"Disallow direct VSCode API usage in favor of Cline's abstraction layers, except in src/hosts/vscode and standalone/runtime-files directories",
recommended: "error",
},
messages: {
useGrpcClient:
"Use gRPC service clients instead of vscode.postMessage().\n" +
"Example: AccountServiceClient.methodName(RequestType.create({...})) instead of vscode.postMessage({type: '...'}).\n" +
"Found: {{code}}",
useFsUtils:
"Use utilities in @/utils/fs instead of vscode.workspace.fs.stat.\n" +
"Example: import { isDirectory } from '@/utils/fs' or use the file system methods from the host bridge provider.\n" +
"Found: {{code}}",
useHostBridge:
"Use getHostBridgeProvider().workspaceClient.getWorkspacePaths({}) instead of vscode.workspace.workspaceFolders.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
usePathUtils:
"Use path utilities from @/utils/path instead of direct VSCode workspace path methods.\n" +
"This provides consistent path handling across different environments.\n" +
"Found: {{code}}",
},
schema: [],
},
defaultOptions: [],
create(context) {
// Check if current file is in an exception directory or is grpc-client-base.ts
const filename = context.filename
const isGrpcClientBase = path.basename(filename) === "grpc-client-base.ts"
// Skip checking files in src/hosts/vscode or standalone/runtime-files
const isExceptionDirectory = filename.includes("/src/hosts/vscode/") || filename.includes("/standalone/runtime-files/")
// Pattern for checking memberExpressions like vscode.workspace.fs.stat
function checkMemberExpression(node) {
// Skip if this file is in an exception directory or is grpc-client-base.ts
if (isGrpcClientBase || isExceptionDirectory) {
return
}
// For handling nested properties like vscode.workspace.fs.stat
function getFullPropertyPath(node) {
if (node.type !== "MemberExpression") {
return node.name || ""
}
const objectPart = getFullPropertyPath(node.object)
const propertyPart = node.property.name || ""
return objectPart ? `${objectPart}.${propertyPart}` : propertyPart
}
// Check if the expression matches one of our disallowed patterns
if (node.object && node.object.type === "Identifier" && node.object.name === "vscode") {
const fullPath = `vscode.${node.property.name}`
checkDisallowedApi(fullPath, node)
}
// Handle nested expressions like vscode.workspace.fs.stat
else if (node.object && node.object.type === "MemberExpression") {
const fullPath = getFullPropertyPath(node)
// Only proceed if it starts with vscode
if (fullPath.startsWith("vscode.")) {
checkDisallowedApi(fullPath, node)
}
}
}
// Check if an expression matches a disallowed API and report if it does
function checkDisallowedApi(expressionPath, node) {
// Check exact matches
if (disallowedApis[expressionPath]) {
reportViolation(expressionPath, node)
return
}
// Check prefix matches (for nested properties)
for (const disallowedApi in disallowedApis) {
// For direct property access like vscode.workspace.workspaceFolders
if (expressionPath === disallowedApi) {
reportViolation(disallowedApi, node)
return
}
// For method calls like vscode.workspace.asRelativePath(...)
if (expressionPath.startsWith(`${disallowedApi}.`) || expressionPath.startsWith(`${disallowedApi}(`)) {
reportViolation(disallowedApi, node)
return
}
}
}
// Report a violation with the appropriate message
function reportViolation(disallowedApi, node) {
const sourceCode = context.sourceCode
const config = disallowedApis[disallowedApi]
// For method calls, get the whole call expression
let reportNode = node
let parentNode = sourceCode.getAncestors(node).pop()
if (parentNode && parentNode.type === "CallExpression" && parentNode.callee === node) {
reportNode = parentNode
}
const callText = sourceCode.getText(reportNode).trim()
context.report({
node: reportNode,
messageId: config.messageId,
data: {
code: callText,
},
})
}
return {
// Detect basic member expressions (e.g., vscode.postMessage)
MemberExpression(node) {
checkMemberExpression(node)
},
// Detect property access through destructuring
VariableDeclarator(node) {
// Skip if this file is in an exception directory or is grpc-client-base.ts
if (isGrpcClientBase || isExceptionDirectory) {
return
}
// Destructuring pattern checks removed as developers don't use the API this way
// They always use direct imports: import * as vscode from "vscode" and direct access: vscode.thing.foo
},
}
},
})
-61
View File
@@ -1,61 +0,0 @@
const { ESLintUtils } = require("@typescript-eslint/utils")
const path = require("path")
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
module.exports = createRule({
name: "no-vscode-postmessage",
meta: {
type: "problem",
docs: {
description: "Ban vscode.postMessage() calls in favor of gRPC service clients, except in grpc-client-base.ts",
recommended: "error",
},
messages: {
useGrpcClient:
"Use gRPC service clients instead of vscode.postMessage().\n" +
"Example: AccountServiceClient.methodName(RequestType.create({...})) instead of vscode.postMessage({type: '...'}).\n" +
"Found: {{code}}",
},
schema: [],
},
defaultOptions: [],
create(context) {
// Check if current file is grpc-client-base.ts (exception case)
const filename = context.filename
const isGrpcClientBase = path.basename(filename) === "grpc-client-base.ts"
return {
// Detect vscode.postMessage calls
"CallExpression[callee.type='MemberExpression']"(node) {
// Skip if this is grpc-client-base.ts
if (isGrpcClientBase) {
return
}
const callee = node.callee
// Check for vscode.postMessage pattern
if (
callee.object &&
callee.object.type === "Identifier" &&
callee.object.name === "vscode" &&
callee.property &&
callee.property.name === "postMessage"
) {
const sourceCode = context.sourceCode
const callText = sourceCode.getText(node).trim()
context.report({
node,
messageId: "useGrpcClient",
data: {
code: callText,
},
})
}
},
}
},
})
+5
View File
@@ -6,6 +6,7 @@ interface RunDiffEvalOptions {
modelIds: string
systemPromptName: string
validAttemptsPerCase: number
maxAttemptsPerCase?: number
parsingFunction: string
diffEditFunction: string
thinkingBudget: number
@@ -71,6 +72,10 @@ export async function runDiffEvalHandler(options: RunDiffEvalOptions) {
args.push("--verbose")
}
if (options.maxAttemptsPerCase) {
args.push("--max-attempts-per-case", String(options.maxAttemptsPerCase))
}
if (options.maxCases) {
args.push("--max-cases", String(options.maxCases))
}
+2
View File
@@ -87,6 +87,7 @@ program
.option("--model-ids <model_ids>", "Comma-separated list of model IDs to test")
.option("--system-prompt-name <name>", "The name of the system prompt to use", "basicSystemPrompt")
.option("-n, --valid-attempts-per-case <number>", "Number of valid attempts per test case per model (will retry until this many valid attempts are collected)", "1")
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
@@ -102,6 +103,7 @@ program
const fullOptions = {
...options,
validAttemptsPerCase: parseInt(options.validAttemptsPerCase, 10),
maxAttemptsPerCase: options.maxAttemptsPerCase ? parseInt(options.maxAttemptsPerCase, 10) : undefined,
thinkingBudget: parseInt(options.thinkingBudget, 10),
maxCases: options.maxCases ? parseInt(options.maxCases, 10) : undefined,
}
+12 -11
View File
@@ -30,6 +30,7 @@ const diffEditingFunctions: Record<string, ConstructNewFileContentFn> = {
}
import { TestInput, TestResult, ExtractedToolCall } from "./types"
import { log } from "./helpers"
export { TestInput, TestResult, ExtractedToolCall }
interface StreamResult {
@@ -284,21 +285,21 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
}
// check that we are editing the correct file path
console.log(`Expected file path: "${originalFilePath}"`);
console.log(`Actual file path used: "${diffToolPath}"`);
log(input.isVerbose, `Expected file path: "${originalFilePath}"`)
log(input.isVerbose, `Actual file path used: "${diffToolPath}"`)
if (diffToolPath !== originalFilePath) {
console.log(`❌ File path mismatch detected!`);
log(input.isVerbose, `❌ File path mismatch detected!`)
// Enhanced logging:
if (streamResult?.assistantMessage) {
console.log(` Full model output (assistantMessage):`);
console.log(` -----------------------------------------`);
console.log(` ${streamResult.assistantMessage}`);
console.log(` -----------------------------------------`);
log(input.isVerbose, ` Full model output (assistantMessage):`)
log(input.isVerbose, ` -----------------------------------------`)
log(input.isVerbose, ` ${streamResult.assistantMessage}`)
log(input.isVerbose, ` -----------------------------------------`)
}
if (toolCall) {
console.log(` Parsed tool call that caused mismatch:`);
console.log(` ${JSON.stringify(toolCall, null, 2)}`);
console.log(` -----------------------------------------`);
log(input.isVerbose, ` Parsed tool call that caused mismatch:`)
log(input.isVerbose, ` ${JSON.stringify(toolCall, null, 2)}`)
log(input.isVerbose, ` -----------------------------------------`)
}
return {
success: false,
@@ -321,7 +322,7 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
// If it's just a string, diffSuccess stays true and replacementData stays undefined
} catch (error: any) {
diffSuccess = false
console.log("ERROR:",error)
log(input.isVerbose, `ERROR: ${error}`)
}
return {
+12 -10
View File
@@ -7,7 +7,7 @@ import { constructNewFileContent as constructNewFileContent_06_26_25 } from "./d
import { constructNewFileContent as constructNewFileContentV3 } from "../../src/core/assistant-message/diff"
import { basicSystemPrompt } from "./prompts/basicSystemPrompt-06-06-25"
import { claude4SystemPrompt } from "./prompts/claude4SystemPrompt-06-06-25"
import { formatResponse } from "./helpers"
import { formatResponse, log } from "./helpers"
import { Anthropic } from "@anthropic-ai/sdk"
import * as fs from "fs"
import * as path from "path"
@@ -40,12 +40,6 @@ const encoding = get_encoding("cl100k_base");
let openRouterModelDataGlobal: Record<string, EvalOpenRouterModelInfo> = {}; // Global to store fetched data
function log(isVerbose: boolean, message: string) {
if (isVerbose) {
console.log(message)
}
}
const systemPromptGeneratorLookup: Record<string, ConstructSystemPromptFn> = {
basicSystemPrompt: basicSystemPrompt,
claude4SystemPrompt: claude4SystemPrompt,
@@ -641,6 +635,7 @@ class NodeTestRunner {
thinkingBudgetTokens: testConfig.thinking_tokens_budget,
originalDiffEditToolCallMessage: testConfig.replay ? testCase.original_diff_edit_tool_call_message : undefined,
diffApplyFile: testConfig.diff_apply_file,
isVerbose: isVerbose,
}
if (isVerbose) {
@@ -807,8 +802,8 @@ class NodeTestRunner {
log(isVerbose, `Warning: Failed to store result in database: ${error}`);
}
// Safety check to prevent infinite loops - limit to 10 attempts per valid attempt requested
if (totalAttempts >= testConfig.number_of_runs * 10) {
// Safety check to prevent infinite loops - use configurable max attempts limit
if (totalAttempts >= testConfig.max_attempts_per_case) {
log(isVerbose, ` ⚠️ Reached maximum attempts (${totalAttempts}) for test case ${testCase.test_id}. Only got ${validAttempts}/${testConfig.number_of_runs} valid attempts.`);
break;
}
@@ -927,6 +922,7 @@ async function main() {
.option("--model-ids <model_ids>", "Comma-separated list of model IDs to test")
.option("--system-prompt-name <name>", "The name of the system prompt to use", "basicSystemPrompt")
.option("-n, --valid-attempts-per-case <number>", "Number of valid attempts per test case per model (will retry until this many valid attempts are collected)", "1")
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "diff-06-25-25")
@@ -957,6 +953,11 @@ async function main() {
}
const validAttemptsPerCase = parseInt(options.validAttemptsPerCase, 10);
// Compute dynamic default for max attempts: 10x valid attempts if not specified
const maxAttemptsPerCase = options.maxAttemptsPerCase
? parseInt(options.maxAttemptsPerCase, 10)
: validAttemptsPerCase * 10;
const runner = new NodeTestRunner(options.replay || !!options.replayRunId)
@@ -1062,6 +1063,7 @@ async function main() {
model_id: modelId,
system_prompt_name: options.systemPromptName,
number_of_runs: validAttemptsPerCase,
max_attempts_per_case: maxAttemptsPerCase,
parsing_function: options.parsingFunction,
diff_edit_function: options.diffEditFunction,
thinking_tokens_budget: parseInt(options.thinkingBudget, 10),
@@ -1129,7 +1131,7 @@ async function main() {
remainingTasks = remainingTasks.filter(task => {
const taskId = `${task.modelId}-${task.testCase.test_id}`;
if (taskStates[taskId].total >= validAttemptsPerCase * 10) {
if (taskStates[taskId].total >= task.testConfig.max_attempts_per_case) {
log(isVerbose, ` ⚠️ Reached maximum attempts for ${task.testCase.test_id} with ${task.modelId}.`);
return false;
}
@@ -492,6 +492,12 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
if (isReplaceBlockEnd(line)) {
// Finished one replace block
if (searchMatchIndex === -1) {
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...is malformatted.`,
)
}
// Store this replacement
replacements.push({
start: searchMatchIndex,
+6
View File
@@ -23,3 +23,9 @@ export const formatResponse = {
return formatImagesIntoBlocks(images)
},
}
export function log(isVerbose: boolean, message: string) {
if (isVerbose) {
console.log(message)
}
}
+2
View File
@@ -29,6 +29,7 @@ export interface TestConfig {
model_id: string
system_prompt_name: string
number_of_runs: number
max_attempts_per_case: number
parsing_function: string
diff_edit_function: string
thinking_tokens_budget: number
@@ -103,4 +104,5 @@ export interface TestInput {
thinkingBudgetTokens: number
originalDiffEditToolCallMessage?: string
diffApplyFile?: string
isVerbose: boolean
}
+765 -1270
View File
File diff suppressed because it is too large Load Diff
+5 -3
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.18.1",
"version": "3.18.14",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -392,6 +392,7 @@
"grpc-tools": "^1.13.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"minimatch": "^3.0.3",
"mintlify": "^4.0.515",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
@@ -408,8 +409,8 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.826.0",
"@aws-sdk/credential-providers": "^3.826.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/credential-providers": "^3.840.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
@@ -448,6 +449,7 @@
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"jwt-decode": "^4.0.0",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"nice-grpc": "^2.1.12",
+96 -43
View File
@@ -1,76 +1,129 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for account-related operations
service AccountService {
// Handles the user clicking the login link in the UI.
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc accountLoginClicked(EmptyRequest) returns (String);
// Handles the user clicking the login link in the UI.
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc accountLoginClicked(EmptyRequest) returns (String);
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
// Subscribe to auth callback events (when authentication tokens are received)
rpc subscribeToAuthCallback(EmptyRequest) returns (stream String);
// Subscribe to auth status update events (when authentication state changes)
rpc subscribeToAuthStatusUpdate(EmptyRequest)
returns (stream AuthState);
// Handles authentication state changes from the Firebase context.
// Updates the user info in global state and returns the updated value.
rpc authStateChanged(AuthStateChangedRequest) returns (AuthStateChanged);
// Handles authentication state changes from the Firebase context.
// Updates the user info in global state and returns the updated value.
rpc authStateChanged(AuthStateChangedRequest)
returns (AuthState);
// Fetches all user credits data (balance, usage transactions, payment transactions)
rpc fetchUserCreditsData(EmptyRequest) returns (UserCreditsData);
// Fetches all user credits data
// (balance, usage transactions, payment transactions)
rpc getUserCredits(EmptyRequest) returns (UserCreditsData);
rpc getOrganizationCredits(GetOrganizationCreditsRequest) returns (OrganizationCreditsData);
// Fetches all user organizations data
// Returns a list of UserOrganization objects
rpc getUserOrganizations(EmptyRequest) returns (UserOrganizationsResponse);
rpc setUserOrganization(UserOrganizationUpdateRequest) returns (Empty);
}
message AuthStateChangedRequest {
Metadata metadata = 1;
UserInfo user = 2;
Metadata metadata = 1;
UserInfo user = 2;
}
message AuthStateChanged {
optional UserInfo user = 1;
message AuthState {
optional UserInfo user = 1;
}
// User's information
message UserInfo {
optional string display_name = 1;
optional string email = 2;
optional string photo_url = 3;
string uid = 1;
optional string display_name = 2;
optional string email = 3;
optional string photo_url = 4;
}
message UserOrganization {
bool active = 1;
string member_id = 2;
string name = 3;
string organization_id = 4;
repeated string roles = 5; // ["admin", "member", "owner"]
}
message UserOrganizationsResponse {
repeated UserOrganization organizations = 1;
}
message UserOrganizationUpdateRequest {
optional string organization_id = 1;
}
// Response containing all user credits data
message UserCreditsData {
UserCreditsBalance balance = 1;
repeated UsageTransaction usage_transactions = 2;
repeated PaymentTransaction payment_transactions = 3;
UserCreditsBalance balance = 1;
repeated UsageTransaction usage_transactions = 2;
repeated PaymentTransaction payment_transactions = 3;
}
message GetOrganizationCreditsRequest {
string organization_id = 1;
}
message OrganizationCreditsData {
UserCreditsBalance balance = 1;
string organization_id = 2;
repeated OrganizationUsageTransaction usage_transactions = 3;
}
// User's current credit balance
message UserCreditsBalance {
double current_balance = 1;
double current_balance = 1;
}
// Usage transaction record
message UsageTransaction {
string spent_at = 1;
string creator_id = 2;
double credits = 3;
string model_provider = 4;
string model = 5;
int32 prompt_tokens = 6;
int32 completion_tokens = 7;
int32 total_tokens = 8;
string ai_inference_provider_name = 1;
string ai_model_name = 2;
string ai_model_type_name = 3;
int32 completion_tokens = 4;
double cost_usd = 5;
string created_at = 6;
double credits_used = 7;
string generation_id = 8;
string organization_id = 9;
int32 prompt_tokens = 10;
int32 total_tokens = 11;
string user_id = 12;
}
// Payment transaction record
message PaymentTransaction {
string paid_at = 1;
string creator_id = 2;
int32 amount_cents = 3;
double credits = 4;
string paid_at = 1;
string creator_id = 2;
int32 amount_cents = 3;
double credits = 4;
}
message OrganizationUsageTransaction {
string ai_inference_provider_name = 1;
string ai_model_name = 2;
string ai_model_type_name = 3;
int32 completion_tokens = 4;
double cost_usd = 5;
string created_at = 6;
double credits_used = 7;
string generation_id = 8;
string organization_id = 9;
int32 prompt_tokens = 10;
int32 total_tokens = 11;
string user_id = 12;
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service BrowserService {
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
+1
View File
@@ -27,5 +27,6 @@ export const hostServiceNameMap = {
workspace: "host.WorkspaceService",
env: "host.EnvService",
window: "host.WindowService",
diff: "host.DiffService",
// Add new host services here
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service CheckpointsService {
rpc checkpointDiff(Int64Request) returns (Empty);
rpc checkpointRestore(CheckpointRestoreRequest) returns (Empty);
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for file-related operations
service FileService {
// Copies text to clipboard
+25
View File
@@ -0,0 +1,25 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// Provides methods for diff views.
service DiffService {
// Open the diff view/editor.
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
}
message OpenDiffRequest {
optional cline.Metadata metadata = 1;
// The absolute path of the document being edited.
optional string path = 2;
// The new content for the file.
optional string content = 3;
}
message OpenDiffResponse {
// TODO(sfortune) the host needs to return a unique id for the diff editor.
}
+3
View File
@@ -13,4 +13,7 @@ service EnvService {
// Reads text from the system clipboard.
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
// Opens a URL in the user's default browser or application.
rpc openExternal(cline.StringRequest) returns (cline.Empty);
}
-36
View File
@@ -1,36 +0,0 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// UriService provides methods for working with URIs in the IDE
service UriService {
// Create a new file URI from a file path
rpc file(cline.StringRequest) returns (Uri);
// Join a URI with additional path segments
rpc joinPath(JoinPathRequest) returns (Uri);
// Parse a string URI into a Uri object
rpc parse(cline.StringRequest) returns (Uri);
}
// Uri represents a URI in the IDE
message Uri {
string scheme = 1;
string authority = 2;
string path = 3;
string query = 4;
string fragment = 5;
string fs_path = 6;
}
// Request for joining path segments to a URI
message JoinPathRequest {
cline.Metadata metadata = 1;
Uri base = 2;
repeated string path_segments = 3;
}
+41
View File
@@ -10,6 +10,8 @@ import "common.proto";
service WindowService {
// Opens a text document in the editor and returns editor information.
rpc showTextDocument(ShowTextDocumentRequest) returns (TextEditorInfo);
rpc showOpenDialogue(ShowOpenDialogueRequest) returns (SelectedResources);
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
}
message ShowTextDocumentRequest {
@@ -30,3 +32,42 @@ message TextEditorInfo {
optional int32 view_column = 2;
bool is_active = 3;
}
message ShowOpenDialogueRequest {
cline.Metadata metadata = 1;
optional bool can_select_many = 2;
optional string open_label = 3;
optional ShowOpenDialogueFilterOption filters = 4;
}
message ShowOpenDialogueFilterOption {
repeated string files = 1;
}
message SelectedResources {
repeated string paths = 1;
}
enum ShowMessageType {
ERROR = 0;
INFORMATION = 1;
WARNING = 2;
}
message ShowMessageRequest {
cline.Metadata metadata = 1;
ShowMessageType type = 2;
string message = 3;
optional ShowMessageRequestOptions options = 4;
}
message ShowMessageRequestOptions {
repeated string items = 1;
optional bool modal = 2;
optional string detail = 3;
}
message SelectedResponse {
optional string selected_option = 1;
}
+14 -3
View File
@@ -1,16 +1,15 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service McpService {
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
rpc downloadMcp(StringRequest) returns (Empty);
rpc downloadMcp(StringRequest) returns (McpDownloadResponse);
rpc restartMcpServer(StringRequest) returns (McpServers);
rpc deleteMcpServer(StringRequest) returns (McpServers);
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
@@ -119,3 +118,15 @@ message McpMarketplaceItem {
message McpMarketplaceCatalog {
repeated McpMarketplaceItem items = 1;
}
message McpDownloadResponse {
string mcp_id = 1;
string github_url = 2;
string name = 3;
string author = 4;
string description = 5;
string readme_content = 6;
string llms_installation_content = 7;
bool requires_api_key = 8;
optional string error = 9;
}
+5 -4
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for model-related operations
service ModelsService {
// Fetches available models from Ollama
@@ -165,7 +164,7 @@ message ModelsApiConfiguration {
// From ApiHandlerOptions (excluding onRetryAttempt function)
optional string api_model_id = 1;
optional string api_key = 2;
optional string cline_api_key = 3;
optional string cline_account_id = 3;
optional string task_id = 4;
optional string lite_llm_base_url = 5;
optional string lite_llm_model_id = 6;
@@ -236,4 +235,6 @@ message ModelsApiConfiguration {
optional string sap_ai_core_token_url = 71;
optional string sap_ai_core_base_url = 72;
optional string claude_code_path = 73;
}
optional string aws_authentication = 74;
optional string aws_bedrock_api_key = 75;
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// SlashService provides methods for managing slash
service SlashService {
// Sends button click message
+8 -4
View File
@@ -1,10 +1,9 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
@@ -18,6 +17,7 @@ service StateService {
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
}
message State {
@@ -112,7 +112,7 @@ message UpdateSettingsRequest {
optional int64 shell_integration_timeout = 8;
optional bool terminal_reuse_enabled = 9;
optional bool mcp_responses_collapsed = 10;
optional bool mcp_rich_display_enabled = 11;
optional string mcp_display_mode = 11;
optional int64 terminal_output_line_limit = 12;
}
@@ -125,7 +125,7 @@ message ApiConfiguration {
optional string api_base_url = 4;
// Provider-specific API keys
optional string cline_api_key = 5;
optional string cline_account_id = 5;
optional string openrouter_api_key = 6;
optional string anthropic_base_url = 7;
optional string openai_api_key = 8;
@@ -232,4 +232,8 @@ message ApiConfiguration {
// Claude Code specific
optional string claude_code_path = 77;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 78;
optional string aws_bedrock_api_key = 79;
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service TaskService {
// Cancels the currently running task
rpc cancelTask(EmptyRequest) returns (Empty);
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Enum for webview provider types
enum WebviewProviderType {
SIDEBAR = 0;
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service WebService {
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
rpc fetchOpenGraphData(StringRequest) returns (OpenGraphData);
+16 -2
View File
@@ -5,17 +5,31 @@ DIR=${1:-src/}
DEST_DIR=dist-standalone
SDK_DEST=$DEST_DIR/vscode-sdk-uses.txt
CSS_DEST=$DEST_DIR/vscode-css-uses.txt
TMP=/tmp/vscode-sdk-uses.txt.tmp
mkdir -p $DEST_DIR
{
git grep -h 'vscode\.' $DIR |
grep -Ev '//.*vscode' | # remove commented out code
grep -v vscode.commands.executeCommand | # executeCommand is handled separately
grep -Ev '"vscode' | # remove command strings that get included because they start with vscode
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
grep -E '\.[a-z][^.]+$' | # remove types (last part of identifier should be lowercase)
sort | uniq -c | sort -n | # Count occurrences
cat > $SDK_DEST
cat > $TMP
}
{
grep -rh vscode.commands.executeCommand $DIR |
perl -ne 'print if /["\x27"]/' | # Remove occurrences where the command is not on the same line (line doesnt contain quote chars) :(
sed -n 's|.*\(vscode.commands.executeCommand[^,]*\).*|\1|p'| # Remove all params after the first one
sed 's|\(".*"\).*|\1)|'| # Close the parantheses
cat >> $TMP
}
# Count occurrences
cat $TMP | sort | uniq -c | sort -n > $SDK_DEST
rm $TMP
echo Wrote uses of the vscode SDK to $(realpath $SDK_DEST)
{
Regular → Executable
+161 -69
View File
@@ -1,83 +1,175 @@
import fs from "fs"
import path from "path"
import { glob } from "glob"
#!/usr/bin/env node
import archiver from "archiver"
import { cp } from "fs/promises"
import { execSync } from "child_process"
import fs from "fs"
import { cp } from "fs/promises"
import { glob } from "glob"
import minimatch from "minimatch"
import path from "path"
const BUILD_DIR = "dist-standalone"
const SOURCE_DIR = "standalone/runtime-files"
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
await cp(SOURCE_DIR, BUILD_DIR, { recursive: true })
// Run npm install in the distribution directory
console.log("Running npm install in distribution directory...")
const cwd = process.cwd()
process.chdir(BUILD_DIR)
try {
execSync("npm install", { stdio: "inherit" })
// Move the vscode directory into node_modules.
// It can't be installed using npm because it will create a symlink which is not portable.
fs.renameSync("vscode", path.join("node_modules", "vscode"))
} catch (error) {
console.error("Error during setup:", error)
process.exit(1)
} finally {
process.chdir(cwd)
async function main() {
await installNodeDependencies()
await zipDistribution()
}
// Check for native .node modules.
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
if (nativeModules.length > 0) {
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
process.exit(1)
async function installNodeDependencies() {
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
console.log("Running npm install in distribution directory...")
const cwd = process.cwd()
process.chdir(BUILD_DIR)
try {
execSync("npm install", { stdio: "inherit" })
// Move the vscode directory into node_modules.
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
fs.renameSync("vscode", path.join("node_modules", "vscode"))
} catch (error) {
console.error("Error during setup:", error)
process.exit(1)
} finally {
process.chdir(cwd)
}
// Check for native .node modules.
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
if (nativeModules.length > 0) {
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
process.exit(1)
}
}
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 3 } })
async function zipDistribution() {
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 3 } })
output.on("close", () => {
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
})
archive.on("warning", (err) => {
console.warn(`Warning: ${err}`)
})
archive.on("error", (err) => {
throw err
})
output.on("close", () => {
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
})
archive.on("warning", (err) => {
console.warn(`Warning: ${err}`)
})
archive.on("error", (err) => {
throw err
})
archive.pipe(output)
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ["standalone.zip"],
})
archive.pipe(output)
// Add all the files from the standalone build dir.
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ["standalone.zip"],
})
// Add the whole cline directory under "extension"
archive.directory(process.cwd(), "extension", (entry) => {
// Skip certain directories.
const exclude = [
BUILD_DIR + "/",
"node_modules/", // node_modules nearly 1GB.
"webview-ui/node_modules/", // node_modules nearly 1GB.
]
// These node modules are used at runtime as assets, they need to be included.
const include = ["node_modules/@vscode/", "webview-ui/node_modules/katex"]
const name = entry.name
// Exclude the same files as the VCE vscode extension packager.
// Also ignore the dist directory, the build directory for the extension.
const isIgnored = createIsIgnored(["dist/**"])
if (include.some((prefix) => name.startsWith(prefix))) {
// Add the whole cline directory under "extension", except the for the ignored files.
archive.directory(process.cwd(), "extension", (entry) => {
if (isIgnored(entry.name)) {
log_verbose("Ignoring", entry.name)
return false
}
return entry
}
if (exclude.some((prefix) => name.startsWith(prefix))) {
return false
}
if (name.match(/(^|\/)\./)) {
// exclude dot directories
return false
}
return entry
})
})
console.log("Zipping package...")
await archive.finalize()
console.log("Zipping package...")
await archive.finalize()
}
/**
* This is based on https://github.com/microsoft/vscode-vsce/blob/fafad8a63e9cf31179f918eb7a4eeb376834c904/src/package.ts#L1695
* because the .vscodeignore format is not compatible with the `ignore` npm module.
*/
function createIsIgnored(standaloneIgnores) {
const MinimatchOptions = { dot: true }
const defaultIgnore = [
".vscodeignore",
"package-lock.json",
"npm-debug.log",
"yarn.lock",
"yarn-error.log",
"npm-shrinkwrap.json",
".editorconfig",
".npmrc",
".yarnrc",
".gitattributes",
"*.todo",
"tslint.yaml",
".eslintrc*",
".babelrc*",
".prettierrc*",
".cz-config.js",
".commitlintrc*",
"webpack.config.js",
"ISSUE_TEMPLATE.md",
"CONTRIBUTING.md",
"PULL_REQUEST_TEMPLATE.md",
"CODE_OF_CONDUCT.md",
".github",
".travis.yml",
"appveyor.yml",
"**/.git",
"**/.git/**",
"**/*.vsix",
"**/.DS_Store",
"**/*.vsixmanifest",
"**/.vscode-test/**",
"**/.vscode-test-web/**",
]
const rawIgnore = fs.readFileSync(".vscodeignore", "utf8")
// Parse raw ignore by splitting output into lines and filtering out empty lines and comments
const parsedIgnore = rawIgnore
.split(/[\n\r]/)
.map((s) => s.trim())
.filter((s) => !!s)
.filter((i) => !/^\s*#/.test(i))
// Add '/**' to possible folder names
const expandedIgnore = [
...parsedIgnore,
...parsedIgnore.filter((i) => !/(^|\/)[^/]*\*[^/]*$/.test(i)).map((i) => (/\/$/.test(i) ? `${i}**` : `${i}/**`)),
]
// Combine with default ignore list
// Also ignore the dist directory- the build directory for the extension.
const allIgnore = [...defaultIgnore, ...expandedIgnore, ...standaloneIgnores]
// Split into ignore and negate list
const [ignore, negate] = allIgnore.reduce(
(r, e) => (!/^\s*!/.test(e) ? [[...r[0], e], r[1]] : [r[0], [...r[1], e]]),
[[], []],
)
function isIgnored(f) {
return (
ignore.some((i) => minimatch(f, i, MinimatchOptions)) &&
!negate.some((i) => minimatch(f, i.substr(1), MinimatchOptions))
)
}
return isIgnored
}
/* cp -r */
async function cpr(source, dest) {
await cp(source, dest, {
recursive: true,
preserveTimestamps: true,
dereference: false, // preserve symlinks instead of following them
})
}
function log_verbose(...args) {
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
console.log(...args)
}
}
await main()
+170 -28
View File
@@ -38,62 +38,204 @@ export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
}
function createHandlerForProvider(apiProvider: string | undefined, options: any): ApiHandler {
function createHandlerForProvider(apiProvider: string | undefined, options: Omit<ApiConfiguration, "apiProvider">): ApiHandler {
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler(options)
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "openrouter":
return new OpenRouterHandler(options)
return new OpenRouterHandler({
openRouterApiKey: options.openRouterApiKey,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
openRouterProviderSorting: options.openRouterProviderSorting,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "bedrock":
return new AwsBedrockHandler(options)
return new AwsBedrockHandler({
apiModelId: options.apiModelId,
awsAccessKey: options.awsAccessKey,
awsSecretKey: options.awsSecretKey,
awsSessionToken: options.awsSessionToken,
awsRegion: options.awsRegion,
awsAuthentication: options.awsAuthentication,
awsBedrockApiKey: options.awsBedrockApiKey,
awsUseCrossRegionInference: options.awsUseCrossRegionInference,
awsBedrockUsePromptCache: options.awsBedrockUsePromptCache,
awsUseProfile: options.awsUseProfile,
awsProfile: options.awsProfile,
awsBedrockEndpoint: options.awsBedrockEndpoint,
awsBedrockCustomSelected: options.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: options.awsBedrockCustomModelBaseId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "vertex":
return new VertexHandler(options)
return new VertexHandler({
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
taskId: options.taskId,
})
case "openai":
return new OpenAiHandler(options)
return new OpenAiHandler({
openAiApiKey: options.openAiApiKey,
openAiBaseUrl: options.openAiBaseUrl,
azureApiVersion: options.azureApiVersion,
openAiHeaders: options.openAiHeaders,
openAiModelId: options.openAiModelId,
openAiModelInfo: options.openAiModelInfo,
reasoningEffort: options.reasoningEffort,
})
case "ollama":
return new OllamaHandler(options)
return new OllamaHandler({
ollamaBaseUrl: options.ollamaBaseUrl,
ollamaModelId: options.ollamaModelId,
ollamaApiOptionsCtxNum: options.ollamaApiOptionsCtxNum,
requestTimeoutMs: options.requestTimeoutMs,
})
case "lmstudio":
return new LmStudioHandler(options)
return new LmStudioHandler({
lmStudioBaseUrl: options.lmStudioBaseUrl,
lmStudioModelId: options.lmStudioModelId,
})
case "gemini":
return new GeminiHandler(options)
return new GeminiHandler({
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: options.apiModelId,
taskId: options.taskId,
})
case "openai-native":
return new OpenAiNativeHandler(options)
return new OpenAiNativeHandler({
openAiNativeApiKey: options.openAiNativeApiKey,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
})
case "deepseek":
return new DeepSeekHandler(options)
return new DeepSeekHandler({
deepSeekApiKey: options.deepSeekApiKey,
apiModelId: options.apiModelId,
})
case "requesty":
return new RequestyHandler(options)
return new RequestyHandler({
requestyApiKey: options.requestyApiKey,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
requestyModelId: options.requestyModelId,
requestyModelInfo: options.requestyModelInfo,
})
case "fireworks":
return new FireworksHandler(options)
return new FireworksHandler({
fireworksApiKey: options.fireworksApiKey,
fireworksModelId: options.fireworksModelId,
fireworksModelMaxCompletionTokens: options.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: options.fireworksModelMaxTokens,
})
case "together":
return new TogetherHandler(options)
return new TogetherHandler({
togetherApiKey: options.togetherApiKey,
togetherModelId: options.togetherModelId,
})
case "qwen":
return new QwenHandler(options)
return new QwenHandler({
qwenApiKey: options.qwenApiKey,
qwenApiLine: options.qwenApiLine,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "doubao":
return new DoubaoHandler(options)
return new DoubaoHandler({
doubaoApiKey: options.doubaoApiKey,
apiModelId: options.apiModelId,
})
case "mistral":
return new MistralHandler(options)
return new MistralHandler({
mistralApiKey: options.mistralApiKey,
apiModelId: options.apiModelId,
})
case "vscode-lm":
return new VsCodeLmHandler(options)
return new VsCodeLmHandler({
vsCodeLmModelSelector: options.vsCodeLmModelSelector,
})
case "cline":
return new ClineHandler(options)
return new ClineHandler({
taskId: options.taskId,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
openRouterProviderSorting: options.openRouterProviderSorting,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
})
case "litellm":
return new LiteLlmHandler(options)
return new LiteLlmHandler({
liteLlmApiKey: options.liteLlmApiKey,
liteLlmBaseUrl: options.liteLlmBaseUrl,
liteLlmModelId: options.liteLlmModelId,
liteLlmModelInfo: options.liteLlmModelInfo,
thinkingBudgetTokens: options.thinkingBudgetTokens,
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
taskId: options.taskId,
})
case "nebius":
return new NebiusHandler(options)
return new NebiusHandler({
nebiusApiKey: options.nebiusApiKey,
apiModelId: options.apiModelId,
})
case "asksage":
return new AskSageHandler(options)
return new AskSageHandler({
asksageApiKey: options.asksageApiKey,
asksageApiUrl: options.asksageApiUrl,
apiModelId: options.apiModelId,
})
case "xai":
return new XAIHandler(options)
return new XAIHandler({
xaiApiKey: options.xaiApiKey,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
})
case "sambanova":
return new SambanovaHandler(options)
return new SambanovaHandler({
sambanovaApiKey: options.sambanovaApiKey,
apiModelId: options.apiModelId,
})
case "cerebras":
return new CerebrasHandler(options)
return new CerebrasHandler({
cerebrasApiKey: options.cerebrasApiKey,
apiModelId: options.apiModelId,
})
case "sapaicore":
return new SapAiCoreHandler(options)
return new SapAiCoreHandler({
sapAiCoreClientId: options.sapAiCoreClientId,
sapAiCoreClientSecret: options.sapAiCoreClientSecret,
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
sapAiResourceGroup: options.sapAiResourceGroup,
sapAiCoreBaseUrl: options.sapAiCoreBaseUrl,
apiModelId: options.apiModelId,
})
case "claude-code":
return new ClaudeCodeHandler(options)
return new ClaudeCodeHandler({
claudeCodePath: options.claudeCodePath,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
default:
return new AnthropicHandler(options)
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
}
}
@@ -182,6 +182,24 @@ describe("AwsBedrockHandler", () => {
process.env["AWS_PROFILE"]!.should.equal(preAWSProfile)
})
it("should work with AWS_BEARER_TOKEN_BEDROCK", async () => {
process.env["AWS_BEARER_TOKEN_BEDROCK"] = "test-key"
const preAWSProfile = process.env["AWS_BEARER_TOKEN_BEDROCK"]
await AwsBedrockHandler["withTempEnv"](
() => {
delete process.env["AWS_BEARER_TOKEN_BEDROCK"]
},
async () => {
should.not.exist(process.env["AWS_BEARER_TOKEN_BEDROCK"])
return "test"
},
)
process.env["AWS_BEARER_TOKEN_BEDROCK"]!.should.equal(preAWSProfile)
})
})
const mockOptions: ApiHandlerOptions = {
@@ -192,6 +210,7 @@ describe("AwsBedrockHandler", () => {
awsSessionToken: "",
awsUseProfile: false,
awsProfile: "",
awsBedrockApiKey: "",
awsBedrockUsePromptCache: false,
awsUseCrossRegionInference: false,
awsBedrockEndpoint: "",
+6 -3
View File
@@ -45,8 +45,10 @@ describe("OllamaHandler", () => {
this.skip()
}
this.timeout(5000)
// Ensure client is initialized
const client = (handler as any).ensureClient()
// Mock the Ollama client's chat method
const chatStub = sinon.stub(handler["client"], "chat").resolves({
const chatStub = sinon.stub(client, "chat").resolves({
[Symbol.asyncIterator]: async function* () {
yield {
message: { content: "Hello, world!" },
@@ -139,8 +141,9 @@ describe("OllamaHandler", () => {
// Restore real timers for this test
clock.restore()
// Mock the Ollama client's chat method to fail on first call and succeed on second
const chatStub = sinon.stub(handler["client"], "chat")
// Ensure client is initialized and mock the Ollama client's chat method to fail on first call and succeed on second
const client = (handler as any).ensureClient()
const chatStub = sinon.stub(client, "chat")
// First call throws an error
chatStub.onFirstCall().rejects(new Error("API Error"))
+30 -8
View File
@@ -5,20 +5,42 @@ import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerO
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
interface AnthropicHandlerOptions {
apiKey?: string
anthropicBaseUrl?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
export class AnthropicHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Anthropic
private client: Anthropic | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: AnthropicHandlerOptions) {
this.options = options
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
}
private ensureClient(): Anthropic {
if (!this.client) {
if (!this.options.apiKey) {
throw new Error("Anthropic API key is required")
}
try {
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
} catch (error) {
throw new Error(`Error creating Anthropic client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
const modelId = model.id
@@ -44,7 +66,7 @@ export class AnthropicHandler implements ApiHandler {
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.client.messages.create(
stream = await client.messages.create(
{
model: modelId,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
@@ -118,7 +140,7 @@ export class AnthropicHandler implements ApiHandler {
break
}
default: {
stream = await this.client.messages.create({
stream = await client.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
+9 -10
View File
@@ -1,16 +1,15 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler } from ".."
import {
ApiHandlerOptions,
ModelInfo,
AskSageModelId,
askSageModels,
askSageDefaultModelId,
askSageDefaultURL,
} from "@shared/api"
import { ModelInfo, AskSageModelId, askSageModels, askSageDefaultModelId, askSageDefaultURL } from "@shared/api"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
interface AskSageHandlerOptions {
asksageApiKey?: string
asksageApiUrl?: string
apiModelId?: string
}
type AskSageRequest = {
system_prompt: string
message: {
@@ -31,11 +30,11 @@ type AskSageResponse = {
}
export class AskSageHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: AskSageHandlerOptions
private apiUrl: string
private apiKey: string
constructor(options: ApiHandlerOptions) {
constructor(options: AskSageHandlerOptions) {
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
this.options = options
this.apiKey = options.asksageApiKey || ""
+43 -11
View File
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { convertToR1Format } from "../transform/r1-format"
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "@shared/api"
import { bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
@@ -16,6 +16,24 @@ import {
// Import proper AWS SDK types
import type { Message, ContentBlock } from "@aws-sdk/client-bedrock-runtime"
interface AwsBedrockHandlerOptions {
apiModelId?: string
awsAccessKey?: string
awsSecretKey?: string
awsSessionToken?: string
awsRegion?: string
awsAuthentication?: string
awsBedrockApiKey?: string
awsUseCrossRegionInference?: boolean
awsBedrockUsePromptCache?: boolean
awsUseProfile?: boolean
awsProfile?: string
awsBedrockEndpoint?: string
awsBedrockCustomSelected?: boolean
awsBedrockCustomModelBaseId?: BedrockModelId
thinkingBudgetTokens?: number
}
// Extend AWS SDK types to include additionalModelResponseFields
interface ExtendedMetadata {
usage?: {
@@ -90,9 +108,9 @@ interface ProviderChainOptions {
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: AwsBedrockHandlerOptions
constructor(options: ApiHandlerOptions) {
constructor(options: AwsBedrockHandlerOptions) {
this.options = options
}
@@ -170,7 +188,10 @@ export class AwsBedrockHandler implements ApiHandler {
}> {
// Configure provider options
const providerOptions: ProviderChainOptions = {}
if (this.options.awsUseProfile) {
const useProfile =
(this.options.awsAuthentication === undefined && this.options.awsUseProfile) ||
this.options.awsAuthentication === "profile"
if (useProfile) {
// For profile-based auth, always use ignoreCache to detect credential file changes
// This solves the AWS Identity Manager issue where credential files change externally
providerOptions.ignoreCache = true
@@ -184,7 +205,7 @@ export class AwsBedrockHandler implements ApiHandler {
return await AwsBedrockHandler.withTempEnv(
() => {
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
if (this.options.awsUseProfile) {
if (useProfile) {
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
} else {
delete process.env["AWS_PROFILE"]
@@ -208,15 +229,26 @@ export class AwsBedrockHandler implements ApiHandler {
* Creates a BedrockRuntimeClient with the appropriate credentials
*/
private async getBedrockClient(): Promise<BedrockRuntimeClient> {
const credentials = await this.getAwsCredentials()
let auth: any
if (this.options.awsAuthentication === "apikey") {
auth = {
token: { token: this.options.awsBedrockApiKey },
authSchemePreference: ["httpBearerAuth"],
}
} else {
const credentials = await this.getAwsCredentials()
auth = {
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
},
}
}
return new BedrockRuntimeClient({
region: this.getRegion(),
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
},
...auth,
...(this.options.awsBedrockEndpoint && { endpoint: this.options.awsBedrockEndpoint }),
})
}
+47 -17
View File
@@ -1,38 +1,63 @@
import { Anthropic } from "@anthropic-ai/sdk"
import Cerebras from "@cerebras/cerebras_cloud_sdk"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "@shared/api"
import { ModelInfo, CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "@shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "@api/transform/stream"
interface CerebrasHandlerOptions {
cerebrasApiKey?: string
apiModelId?: string
}
export class CerebrasHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Cerebras
private options: CerebrasHandlerOptions
private client: Cerebras | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: CerebrasHandlerOptions) {
this.options = options
}
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
private ensureClient(): Cerebras {
if (!this.client) {
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
}
try {
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
} catch (error) {
throw new Error(`Error creating Cerebras client: ${error.message}`)
}
}
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
// Convert Anthropic messages to Cerebras format
const cerebrasMessages: Array<{
role: "system" | "user" | "assistant"
content: string
}> = [{ role: "system", content: systemPrompt }]
// Helper function to strip thinking tags from content
const stripThinkingTags = (content: string): string => {
return content.replace(/<think>[\s\S]*?<\/think>/g, "").trim()
}
// Check if this is a reasoning model that uses thinking tags
const modelId = this.getModel().id
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
// Convert Anthropic messages to Cerebras format
for (const message of messages) {
if (message.role === "user") {
@@ -50,7 +75,7 @@ export class CerebrasHandler implements ApiHandler {
: message.content
cerebrasMessages.push({ role: "user", content })
} else if (message.role === "assistant") {
const content = Array.isArray(message.content)
let content = Array.isArray(message.content)
? message.content
.map((block) => {
if (block.type === "text") {
@@ -60,12 +85,19 @@ export class CerebrasHandler implements ApiHandler {
})
.join("\n")
: message.content || ""
// Strip thinking tags from assistant messages for reasoning models
// so the model doesn't see its own thinking in the conversation history
if (isReasoningModel) {
content = stripThinkingTags(content)
}
cerebrasMessages.push({ role: "assistant", content })
}
}
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: cerebrasMessages,
temperature: 0,
@@ -74,8 +106,6 @@ export class CerebrasHandler implements ApiHandler {
// Handle streaming response
let reasoning: string | null = null // Track reasoning content for models that support thinking
const modelId = this.getModel().id
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
for await (const chunk of stream as any) {
// Type assertion for the streaming chunk
+11 -4
View File
@@ -1,15 +1,21 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { claudeCodeDefaultModelId, ClaudeCodeModelId, claudeCodeModels, type ApiHandlerOptions } from "@/shared/api"
import { claudeCodeDefaultModelId, ClaudeCodeModelId, claudeCodeModels } from "@/shared/api"
import { type ApiHandler } from ".."
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
import { runClaudeCode } from "@/integrations/claude-code/run"
import { filterMessagesForClaudeCode } from "@/integrations/claude-code/message-filter"
export class ClaudeCodeHandler implements ApiHandler {
private options: ApiHandlerOptions
interface ClaudeCodeHandlerOptions {
claudeCodePath?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
constructor(options: ApiHandlerOptions) {
export class ClaudeCodeHandler implements ApiHandler {
private options: ClaudeCodeHandlerOptions
constructor(options: ClaudeCodeHandlerOptions) {
this.options = options
}
@@ -27,6 +33,7 @@ export class ClaudeCodeHandler implements ApiHandler {
messages: filteredMessages,
path: this.options.claudeCodePath,
modelId: this.getModel().id,
thinkingBudgetTokens: this.options.thinkingBudgetTokens,
})
// Usage is included with assistant messages,
+165 -104
View File
@@ -1,143 +1,207 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import axios from "axios"
import { OpenRouterErrorResponse } from "./types"
import { withRetry } from "../retry"
import { AuthService } from "@/services/auth/AuthService"
import OpenAI from "openai"
import { version as extensionVersion } from "../../../package.json"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
interface ClineHandlerOptions {
taskId?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
openRouterProviderSorting?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
clineAccountId?: string
}
export class ClineHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private options: ClineHandlerOptions
private clineAccountService = ClineAccountService.getInstance()
private _authService: AuthService
private client: OpenAI | undefined
// TODO: replace this with a global API Host
private readonly _baseUrl = "https://api.cline.bot"
// private readonly _baseUrl = "https://core-api.staging.int.cline.bot"
// private readonly _baseUrl = "http://localhost:7777"
lastGenerationId?: string
private counter = 0
constructor(options: ApiHandlerOptions) {
constructor(options: ClineHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
},
})
this._authService = AuthService.getInstance()
}
private async ensureClient(): Promise<OpenAI> {
const clineAccountAuthToken = await this._authService.getAuthToken()
if (!clineAccountAuthToken) {
throw new Error("Cline account authentication token is required")
}
if (!this.client) {
try {
this.client = new OpenAI({
baseURL: `${this._baseUrl}/api/v1`,
apiKey: clineAccountAuthToken,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
"X-Task-ID": this.options.taskId || "",
"X-Cline-Version": extensionVersion,
},
})
} catch (error: any) {
throw new Error(`Error creating Cline client: ${error.message}`)
}
}
// Ensure the client is always using the latest auth token
this.client.apiKey = clineAccountAuthToken
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
this.lastGenerationId = undefined
try {
// Only continue the request if the user:
// 1. Has signed in to Cline with a token
// 2. Has more than 0 credits
// Or an error is thrown.
await this.clineAccountService.validateRequest()
const stream = await createOpenRouterStream(
this.client,
systemPrompt,
messages,
this.getModel(),
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
const client = await this.ensureClient()
let didOutputUsage: boolean = false
this.lastGenerationId = undefined
for await (const chunk of stream) {
// 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}`)
// 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}`)
}
let didOutputUsage: boolean = false
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
const stream = await createOpenRouterStream(
client,
systemPrompt,
messages,
this.getModel(),
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
// Check for mid-stream error via finish_reason
const choice = chunk.choices?.[0]
// OpenRouter may return finish_reason = "error" with error details
if ((choice?.finish_reason as string) === "error") {
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}`)
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
} else {
throw new Error("Cline Mid-Stream Error: Stream terminated with error status but no error details provided")
for await (const chunk of stream) {
// 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}`)
// 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}`)
}
}
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
// Check for mid-stream error via finish_reason
const choice = chunk.choices?.[0]
// OpenRouter may return finish_reason = "error" with error details
if ((choice?.finish_reason as string) === "error") {
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}`)
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
} else {
throw new Error(
"Cline Mid-Stream Error: Stream terminated with error status but no error details provided",
)
}
}
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Reasoning tokens are returned separately from the content
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
if (!didOutputUsage && chunk.usage) {
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
const modelId = this.getModel().id
if (!didOutputUsage && chunk.usage) {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
const modelId = this.getModel().id
const provider = modelId.split("/")[0]
// const provider = modelId.split("/")[0]
// // If provider is x-ai, set totalCost to 0 (we're doing a promo)
// if (provider === "x-ai") {
// totalCost = 0
// }
// If provider is x-ai, set totalCost to 0 (we're doing a promo)
if (provider === "x-ai") {
totalCost = 0
}
if (modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
if (modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens:
(chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
}
didOutputUsage = true
}
didOutputUsage = true
}
}
// Fallback to generation endpoint if usage chunk not returned
if (!didOutputUsage) {
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
// Fallback to generation endpoint if usage chunk not returned
if (!didOutputUsage) {
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
}
}
} catch (error) {
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
throw new Error("Unauthorized: Please sign in to Cline before trying again.") // match with webview-ui/src/components/chat/ChatRow.tsx
} else if (error.code === "insufficient_credits" || error.status === 402) {
throw new Error(error.error ? JSON.stringify(error.error) : "Insufficient credits or unknown error.")
}
console.error("Cline API Error:", error)
throw error instanceof Error ? error : new Error(String(error))
}
}
async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
if (this.lastGenerationId) {
try {
const response = await axios.get(`https://api.cline.bot/v1/generation?id=${this.lastGenerationId}`, {
// TODO: replace this with firebase auth
// TODO: use global API Host
const response = await axios.get(`${this.clineAccountService.baseUrl}/generation?id=${this.lastGenerationId}`, {
headers: {
Authorization: `Bearer ${this.options.clineApiKey}`,
Authorization: `Bearer ${this.options.clineAccountId}`,
},
timeout: 15_000, // this request hangs sometimes
})
@@ -175,9 +239,6 @@ export class ClineHandler implements ApiHandler {
getModel(): { id: string; info: ModelInfo } {
let modelId = this.options.openRouterModelId
if (modelId === "x-ai/grok-3") {
modelId = "x-ai/grok-3-beta"
}
const modelInfo = this.options.openRouterModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
+28 -9
View File
@@ -8,16 +8,34 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
export class DeepSeekHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface DeepSeekHandlerOptions {
deepSeekApiKey?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class DeepSeekHandler implements ApiHandler {
private options: DeepSeekHandlerOptions
private client: OpenAI | undefined
constructor(options: DeepSeekHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.deepSeekApiKey) {
throw new Error("DeepSeek API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
} catch (error) {
throw new Error(`Error creating DeepSeek client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -54,6 +72,7 @@ export class DeepSeekHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
@@ -67,7 +86,7 @@ export class DeepSeekHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+28 -9
View File
@@ -1,20 +1,38 @@
import { ApiHandler } from ".."
import { ApiHandlerOptions, doubaoDefaultModelId, DoubaoModelId, doubaoModels, ModelInfo } from "@shared/api"
import { doubaoDefaultModelId, DoubaoModelId, doubaoModels, ModelInfo } from "@shared/api"
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
interface DoubaoHandlerOptions {
doubaoApiKey?: string
apiModelId?: string
}
export class DoubaoHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
private options: DoubaoHandlerOptions
private client: OpenAI | undefined
constructor(options: DoubaoHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.doubaoApiKey) {
throw new Error("Doubao API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
} catch (error) {
throw new Error(`Error creating Doubao client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: DoubaoModelId; info: ModelInfo } {
@@ -31,12 +49,13 @@ export class DoubaoHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+31 -17
View File
@@ -2,31 +2,45 @@ import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from ".."
import {
ApiHandlerOptions,
DeepSeekModelId,
ModelInfo,
deepSeekDefaultModelId,
deepSeekModels,
openAiModelInfoSaneDefaults,
} from "../../shared/api"
import { ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
export class FireworksHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface FireworksHandlerOptions {
fireworksApiKey?: string
fireworksModelId?: string
fireworksModelMaxCompletionTokens?: number
fireworksModelMaxTokens?: number
}
constructor(options: ApiHandlerOptions) {
export class FireworksHandler implements ApiHandler {
private options: FireworksHandlerOptions
private client: OpenAI | undefined
constructor(options: FireworksHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.fireworksApiKey) {
throw new Error("Fireworks API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
} catch (error) {
throw new Error(`Error creating Fireworks client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.fireworksModelId ?? ""
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -34,7 +48,7 @@ export class FireworksHandler implements ApiHandler {
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
...(this.options.fireworksModelMaxCompletionTokens
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
+43 -19
View File
@@ -12,8 +12,15 @@ import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 900
interface GeminiHandlerOptions extends ApiHandlerOptions {
interface GeminiHandlerOptions {
isVertex?: boolean
vertexProjectId?: string
vertexRegion?: string
geminiApiKey?: string
geminiBaseUrl?: string
thinkingBudgetTokens?: number
apiModelId?: string
taskId?: string
}
/**
@@ -38,30 +45,45 @@ interface GeminiHandlerOptions extends ApiHandlerOptions {
*/
export class GeminiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: GoogleGenAI
private client: GoogleGenAI | undefined
constructor(options: GeminiHandlerOptions) {
// Store the options
this.options = options
}
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
private ensureClient(): GoogleGenAI {
if (!this.client) {
const options = this.options as GeminiHandlerOptions
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
try {
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} catch (error) {
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
}
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
}
try {
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
} catch (error) {
throw new Error(`Error creating Gemini client: ${error.message}`)
}
}
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
}
return this.client
}
/**
@@ -80,6 +102,7 @@ export class GeminiHandler implements ApiHandler {
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const { id: modelId, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
@@ -117,7 +140,7 @@ export class GeminiHandler implements ApiHandler {
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
try {
const result = await this.client.models.generateContentStream({
const result = await client.models.generateContentStream({
model: modelId,
contents: contents,
config: {
@@ -351,6 +374,7 @@ export class GeminiHandler implements ApiHandler {
*/
async countTokens(content: Array<any>): Promise<number> {
try {
const client = this.ensureClient()
const { id: model } = this.getModel()
// Convert content to Gemini format
@@ -362,7 +386,7 @@ export class GeminiHandler implements ApiHandler {
})
// Use Gemini's token counting API
const response = await this.client.models.countTokens({
const response = await client.models.countTokens({
model,
contents: [{ parts: geminiContent }],
})
+36 -11
View File
@@ -1,28 +1,52 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults, LiteLLMModelInfo } from "@shared/api"
import { ApiHandler } from ".."
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { withRetry } from "../retry"
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface LiteLlmHandlerOptions {
liteLlmApiKey?: string
liteLlmBaseUrl?: string
liteLlmModelId?: string
liteLlmModelInfo?: LiteLLMModelInfo
thinkingBudgetTokens?: number
liteLlmUsePromptCache?: boolean
taskId?: string
}
constructor(options: ApiHandlerOptions) {
export class LiteLlmHandler implements ApiHandler {
private options: LiteLlmHandlerOptions
private client: OpenAI | undefined
constructor(options: LiteLlmHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.liteLlmApiKey) {
throw new Error("LiteLLM API key is required")
}
try {
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
} catch (error) {
throw new Error(`Error creating LiteLLM client: ${error.message}`)
}
}
return this.client
}
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
const client = this.ensureClient()
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
try {
const response = await fetch(`${this.client.baseURL}/spend/calculate`, {
const response = await fetch(`${client.baseURL}/spend/calculate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -54,6 +78,7 @@ export class LiteLlmHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
@@ -101,7 +126,7 @@ export class LiteLlmHandler implements ApiHandler {
return message
})
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
messages: [enhancedSystemMessage, ...enhancedMessages],
temperature,
+25 -9
View File
@@ -6,27 +6,43 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
export class LmStudioHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface LmStudioHandlerOptions {
lmStudioBaseUrl?: string
lmStudioModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class LmStudioHandler implements ApiHandler {
private options: LmStudioHandlerOptions
private client: OpenAI | undefined
constructor(options: LmStudioHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
try {
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
} catch (error) {
throw new Error(`Error creating LM Studio client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
stream: true,
+28 -9
View File
@@ -2,24 +2,43 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, mistralDefaultModelId, MistralModelId, mistralModels, ModelInfo } from "@shared/api"
import { mistralDefaultModelId, MistralModelId, mistralModels, ModelInfo } from "@shared/api"
import { convertToMistralMessages } from "../transform/mistral-format"
import { ApiStream } from "../transform/stream"
export class MistralHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Mistral
interface MistralHandlerOptions {
mistralApiKey?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class MistralHandler implements ApiHandler {
private options: MistralHandlerOptions
private client: Mistral | undefined
constructor(options: MistralHandlerOptions) {
this.options = options
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
}
private ensureClient(): Mistral {
if (!this.client) {
if (!this.options.mistralApiKey) {
throw new Error("Mistral API key is required")
}
try {
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
} catch (error) {
throw new Error(`Error creating Mistral client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.chat
const client = this.ensureClient()
const stream = await client.chat
.stream({
model: this.getModel().id,
// max_completion_tokens: this.getModel().info.maxTokens,
+26 -8
View File
@@ -5,27 +5,45 @@ import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type ApiHandlerOptions, type NebiusModelId } from "../../shared/api"
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type NebiusModelId } from "../../shared/api"
interface NebiusHandlerOptions {
nebiusApiKey?: string
apiModelId?: string
}
export class NebiusHandler implements ApiHandler {
private client: OpenAI
private client: OpenAI | undefined
constructor(private readonly options: ApiHandlerOptions) {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
constructor(private readonly options: NebiusHandlerOptions) {}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.nebiusApiKey) {
throw new Error("Nebius API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
} catch (error) {
throw new Error(`Error creating Nebius client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = model.id.includes("DeepSeek-R1")
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: openAiMessages,
temperature: 0,
+24 -6
View File
@@ -6,17 +6,35 @@ import { convertToOllamaMessages } from "../transform/ollama-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
export class OllamaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Ollama
interface OllamaHandlerOptions {
ollamaBaseUrl?: string
ollamaModelId?: string
ollamaApiOptionsCtxNum?: string
requestTimeoutMs?: number
}
constructor(options: ApiHandlerOptions) {
export class OllamaHandler implements ApiHandler {
private options: OllamaHandlerOptions
private client: Ollama | undefined
constructor(options: OllamaHandlerOptions) {
this.options = options
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
}
private ensureClient(): Ollama {
if (!this.client) {
try {
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
} catch (error) {
throw new Error(`Error creating Ollama client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
try {
@@ -27,7 +45,7 @@ export class OllamaHandler implements ApiHandler {
})
// Create the actual API request promise
const apiPromise = this.client.chat({
const apiPromise = client.chat({
model: this.getModel().id,
messages: ollamaMessages,
stream: true,
+30 -10
View File
@@ -8,15 +8,34 @@ import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
export class OpenAiNativeHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface OpenAiNativeHandlerOptions {
openAiNativeApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class OpenAiNativeHandler implements ApiHandler {
private options: OpenAiNativeHandlerOptions
private client: OpenAI | undefined
constructor(options: OpenAiNativeHandlerOptions) {
this.options = options
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiNativeApiKey) {
throw new Error("OpenAI API key is required")
}
try {
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -38,6 +57,7 @@ export class OpenAiNativeHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
switch (model.id) {
@@ -45,7 +65,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o1-preview":
case "o1-mini": {
// o1 doesn't support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
const response = await client.chat.completions.create({
model: model.id,
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
})
@@ -61,7 +81,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o4-mini":
case "o3":
case "o3-mini": {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
@@ -85,7 +105,7 @@ export class OpenAiNativeHandler implements ApiHandler {
break
}
default: {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
// max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
+50 -26
View File
@@ -1,44 +1,68 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI, { AzureOpenAI } from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults, OpenAiCompatibleModelInfo } from "@shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface OpenAiHandlerOptions {
openAiApiKey?: string
openAiBaseUrl?: string
azureApiVersion?: string
openAiHeaders?: Record<string, string>
openAiModelId?: string
openAiModelInfo?: OpenAiCompatibleModelInfo
reasoningEffort?: string
}
constructor(options: ApiHandlerOptions) {
export class OpenAiHandler implements ApiHandler {
private options: OpenAiHandlerOptions
private client: OpenAI | undefined
constructor(options: OpenAiHandlerOptions) {
this.options = options
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiApiKey) {
throw new Error("OpenAI API key is required")
}
try {
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
@@ -68,7 +92,7 @@ export class OpenAiHandler implements ApiHandler {
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature,
+39 -17
View File
@@ -3,35 +3,59 @@ import axios from "axios"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { OpenRouterErrorResponse } from "./types"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
interface OpenRouterHandlerOptions {
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
}
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private options: OpenRouterHandlerOptions
private client: OpenAI | undefined
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
constructor(options: OpenRouterHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openRouterApiKey) {
throw new Error("OpenRouter API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
} catch (error: any) {
throw new Error(`Error creating OpenRouter client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
this.client,
client,
systemPrompt,
messages,
this.getModel(),
@@ -89,7 +113,8 @@ export class OpenRouterHandler implements ApiHandler {
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
yield {
type: "reasoning",
// @ts-ignore-next-line
@@ -190,9 +215,6 @@ export class OpenRouterHandler implements ApiHandler {
getModel(): { id: string; info: ModelInfo } {
let modelId = this.options.openRouterModelId
if (modelId === "x-ai/grok-3") {
modelId = "x-ai/grok-3-beta"
}
const modelInfo = this.options.openRouterModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
+33 -13
View File
@@ -2,7 +2,6 @@ import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
ModelInfo,
mainlandQwenModels,
internationalQwenModels,
@@ -16,19 +15,39 @@ import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { withRetry } from "../retry"
export class QwenHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface QwenHandlerOptions {
qwenApiKey?: string
qwenApiLine?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
constructor(options: ApiHandlerOptions) {
export class QwenHandler implements ApiHandler {
private options: QwenHandlerOptions
private client: OpenAI | undefined
constructor(options: QwenHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL:
this.options.qwenApiLine === "china"
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.qwenApiKey) {
throw new Error("Alibaba API key is required")
}
try {
this.client = new OpenAI({
baseURL:
this.options.qwenApiLine === "china"
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
} catch (error: any) {
throw new Error(`Error creating Alibaba client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: MainlandQwenModelId | InternationalQwenModelId; info: ModelInfo } {
@@ -51,6 +70,7 @@ export class QwenHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-r1")
const isReasoningModelFamily = model.id.includes("qwen3") || ["qwen-plus-latest", "qwen-turbo-latest"].includes(model.id)
@@ -76,7 +96,7 @@ export class QwenHandler implements ApiHandler {
temperature = undefined
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+34 -12
View File
@@ -7,6 +7,14 @@ import { convertToOpenAiMessages } from "@api/transform/openai-format"
import { calculateApiCostOpenAI } from "@utils/cost"
import { ApiStream } from "@api/transform/stream"
interface RequestyHandlerOptions {
requestyApiKey?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
requestyModelId?: string
requestyModelInfo?: ModelInfo
}
// Requesty usage includes an extra field for Anthropic use cases.
// Safely cast the prompt token details section to the appropriate structure.
interface RequestyUsage extends OpenAI.CompletionUsage {
@@ -18,23 +26,37 @@ interface RequestyUsage extends OpenAI.CompletionUsage {
}
export class RequestyHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private options: RequestyHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: RequestyHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.requestyApiKey) {
throw new Error("Requesty API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
})
} catch (error: any) {
throw new Error(`Error creating Requesty client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -57,7 +79,7 @@ export class RequestyHandler implements ApiHandler {
: {}
// @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_tokens: model.info.maxTokens || undefined,
messages: openAiMessages,
+29 -10
View File
@@ -1,26 +1,45 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "@shared/api"
import { ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "@shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "@/api/transform/openai-format"
import { ApiStream } from "@api/transform/stream"
import { convertToR1Format } from "@api/transform/r1-format"
export class SambanovaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface SambanovaHandlerOptions {
sambanovaApiKey?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class SambanovaHandler implements ApiHandler {
private options: SambanovaHandlerOptions
private client: OpenAI | undefined
constructor(options: SambanovaHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.sambanovaApiKey) {
throw new Error("SambaNova API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
} catch (error: any) {
throw new Error(`Error creating SambaNova client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -34,7 +53,7 @@ export class SambanovaHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
temperature: 0,
+148 -7
View File
@@ -2,10 +2,19 @@ import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, sapAiCoreDefaultModelId, SapAiCoreModelId, sapAiCoreModels } from "../../shared/api"
import { ModelInfo, sapAiCoreDefaultModelId, SapAiCoreModelId, sapAiCoreModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface SapAiCoreHandlerOptions {
sapAiCoreClientId?: string
sapAiCoreClientSecret?: string
sapAiCoreTokenUrl?: string
sapAiResourceGroup?: string
sapAiCoreBaseUrl?: string
apiModelId?: string
}
interface Deployment {
id: string
name: string
@@ -19,11 +28,11 @@ interface Token {
expires_at: number
}
export class SapAiCoreHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: SapAiCoreHandlerOptions
private token?: Token
private deployments?: Deployment[]
constructor(options: ApiHandlerOptions) {
constructor(options: SapAiCoreHandlerOptions) {
this.options = options
}
@@ -60,6 +69,7 @@ export class SapAiCoreHandler implements ApiHandler {
Authorization: `Bearer ${token}`,
"AI-Resource-Group": this.options.sapAiResourceGroup || "default",
"Content-Type": "application/json",
"AI-Client-Type": "Cline",
}
const url = `${this.options.sapAiCoreBaseUrl}/v2/lm/deployments?$top=10000&$skip=0`
@@ -116,6 +126,7 @@ export class SapAiCoreHandler implements ApiHandler {
Authorization: `Bearer ${token}`,
"AI-Resource-Group": this.options.sapAiResourceGroup || "default",
"Content-Type": "application/json",
"AI-Client-Type": "Cline",
}
const model = this.getModel()
@@ -133,6 +144,8 @@ export class SapAiCoreHandler implements ApiHandler {
const openAIModels = ["gpt-4o", "gpt-4", "gpt-4o-mini", "o1", "gpt-4.1", "gpt-4.1-nano", "o3-mini", "o3", "o4-mini"]
const geminiModels = ["gemini-2.5-flash", "gemini-2.5-pro"]
let url: string
let payload: any
if (anthropicModels.includes(model.id)) {
@@ -187,6 +200,9 @@ export class SapAiCoreHandler implements ApiHandler {
delete payload.stream
delete payload.stream_options
}
} else if (geminiModels.includes(model.id)) {
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/models/${model.id}:streamGenerateContent`
payload = this.convertToGeminiFormat(systemPrompt, messages)
} else {
throw new Error(`Unsupported model: ${model.id}`)
}
@@ -233,6 +249,8 @@ export class SapAiCoreHandler implements ApiHandler {
model.id === "anthropic--claude-3.7-sonnet"
) {
yield* this.streamCompletionSonnet37(response.data, model)
} else if (geminiModels.includes(model.id)) {
yield* this.streamCompletionGemini(response.data, model)
} else {
yield* this.streamCompletion(response.data, model)
}
@@ -276,7 +294,6 @@ export class SapAiCoreHandler implements ApiHandler {
const jsonData = line.slice(6)
try {
const data = JSON.parse(jsonData)
console.log("Received data:", data)
if (data.type === "message_start") {
usage.input_tokens = data.message.usage.input_tokens
yield {
@@ -339,7 +356,6 @@ export class SapAiCoreHandler implements ApiHandler {
try {
// Parse the incoming JSON data from the stream
const data = JSON.parse(toStrictJson(jsonData))
console.log("Received data:", data)
// Handle metadata (token usage)
if (data.metadata?.usage) {
@@ -415,7 +431,6 @@ export class SapAiCoreHandler implements ApiHandler {
const jsonData = line.slice(6)
try {
const data = JSON.parse(jsonData)
console.log("Received GPT data:", data)
if (data.choices && data.choices.length > 0) {
const choice = data.choices[0]
@@ -439,7 +454,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
if (data.choices && data.choices[0].finish_reason === "stop") {
if (data.choices?.[0]?.finish_reason === "stop") {
// Final usage yield, if not already provided
if (!data.usage) {
yield {
@@ -461,6 +476,88 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
private async *streamCompletionGemini(
stream: any,
model: { id: SapAiCoreModelId; info: ModelInfo },
): AsyncGenerator<any, void, unknown> {
let promptTokens = 0
let outputTokens = 0
let cacheReadTokens = 0
let thoughtsTokenCount = 0
try {
for await (const chunk of stream) {
const lines = chunk.toString().split("\n").filter(Boolean)
for (const line of lines) {
if (line.startsWith("data: ")) {
const jsonData = line.slice(6)
try {
const data = JSON.parse(jsonData)
const candidateForThoughts = data?.candidates?.[0]
const partsForThoughts = candidateForThoughts?.content?.parts
let thoughts = ""
if (partsForThoughts) {
for (const part of partsForThoughts) {
const { thought, text } = part
if (thought && text) {
thoughts += text + "\n"
}
}
}
if (thoughts.trim() !== "") {
yield {
type: "reasoning",
reasoning: thoughts.trim(),
}
}
if (data.text) {
yield {
type: "text",
text: data.text,
}
}
if (data.candidates && data.candidates[0]?.content?.parts) {
for (const part of data.candidates[0].content.parts) {
if (part.text && !part.thought) {
// Only non-thought text
yield {
type: "text",
text: part.text,
}
}
}
}
if (data.usageMetadata) {
promptTokens = data.usageMetadata.promptTokenCount ?? promptTokens
outputTokens = data.usageMetadata.candidatesTokenCount ?? outputTokens
thoughtsTokenCount = data.usageMetadata.thoughtsTokenCount ?? thoughtsTokenCount
cacheReadTokens = data.usageMetadata.cachedContentTokenCount ?? cacheReadTokens
yield {
type: "usage",
inputTokens: promptTokens - cacheReadTokens,
outputTokens,
thoughtsTokenCount,
cacheReadTokens,
}
}
} catch (error) {
console.error("Failed to parse Gemini JSON data:", error)
}
}
}
}
} catch (error) {
console.error("Error streaming Gemini completion:", error)
throw error
}
}
createUserReadableRequest(
userContent: Array<
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
@@ -495,6 +592,50 @@ export class SapAiCoreHandler implements ApiHandler {
throw new Error(`Unsupported image format: ${format}`)
}
private convertToGeminiFormat(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]) {
const contents = messages.map(this.convertAnthropicMessageToGemini)
const payload = {
contents,
systemInstruction: {
parts: [
{
text: systemPrompt,
},
],
},
generationConfig: {
maxOutputTokens: this.getModel().info.maxTokens,
temperature: 0.0,
},
}
return payload
}
private convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam) {
const role = message.role === "assistant" ? "model" : "user"
const parts = []
if (typeof message.content === "string") {
parts.push({ text: message.content })
} else if (Array.isArray(message.content)) {
for (const block of message.content) {
if (block.type === "text") {
parts.push({ text: block.text })
} else if (block.type === "image") {
parts.push({
inlineData: {
mimeType: block.source.media_type,
data: block.source.data,
},
})
}
}
}
return { role, parts }
}
private formatAnthropicMessages(messages: Anthropic.Messages.MessageParam[]): any[] {
return messages.map((m) => {
const contentBlocks: any[] = []
+29 -10
View File
@@ -1,26 +1,45 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "@api/transform/openai-format"
import { ApiStream } from "@api/transform/stream"
import { convertToR1Format } from "@api/transform/r1-format"
export class TogetherHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface TogetherHandlerOptions {
togetherApiKey?: string
togetherModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class TogetherHandler implements ApiHandler {
private options: TogetherHandlerOptions
private client: OpenAI | undefined
constructor(options: TogetherHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.togetherApiKey) {
throw new Error("Together API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
} catch (error: any) {
throw new Error(`Error creating Together client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.togetherModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
@@ -33,7 +52,7 @@ export class TogetherHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature: 0,
+55 -18
View File
@@ -6,26 +6,60 @@ import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vert
import { ApiStream } from "@api/transform/stream"
import { GeminiHandler } from "./gemini"
interface VertexHandlerOptions {
vertexProjectId?: string
vertexRegion?: string
apiModelId?: string
thinkingBudgetTokens?: number
geminiApiKey?: string
geminiBaseUrl?: string
taskId?: string
}
export class VertexHandler implements ApiHandler {
private geminiHandler: GeminiHandler
private clientAnthropic: AnthropicVertex
private options: ApiHandlerOptions
private geminiHandler: GeminiHandler | undefined
private clientAnthropic: AnthropicVertex | undefined
private options: VertexHandlerOptions
constructor(options: ApiHandlerOptions) {
constructor(options: VertexHandlerOptions) {
this.options = options
}
// Create a GeminiHandler with isVertex flag for Gemini models
this.geminiHandler = new GeminiHandler({
...options,
isVertex: true,
})
private ensureGeminiHandler(): GeminiHandler {
if (!this.geminiHandler) {
try {
// Create a GeminiHandler with isVertex flag for Gemini models
this.geminiHandler = new GeminiHandler({
...this.options,
isVertex: true,
})
} catch (error: any) {
throw new Error(`Error creating Vertex AI Gemini handler: ${error.message}`)
}
}
return this.geminiHandler
}
// Initialize Anthropic client for Claude models
this.clientAnthropic = new AnthropicVertex({
projectId: this.options.vertexProjectId,
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion,
})
private ensureAnthropicClient(): AnthropicVertex {
if (!this.clientAnthropic) {
if (!this.options.vertexProjectId) {
throw new Error("Vertex AI project ID is required")
}
if (!this.options.vertexRegion) {
throw new Error("Vertex AI region is required")
}
try {
// Initialize Anthropic client for Claude models
this.clientAnthropic = new AnthropicVertex({
projectId: this.options.vertexProjectId,
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion,
})
} catch (error: any) {
throw new Error(`Error creating Vertex AI Anthropic client: ${error.message}`)
}
}
return this.clientAnthropic
}
@withRetry()
@@ -35,10 +69,13 @@ export class VertexHandler implements ApiHandler {
// For Gemini models, use the GeminiHandler
if (!modelId.includes("claude")) {
yield* this.geminiHandler.createMessage(systemPrompt, messages)
const geminiHandler = this.ensureGeminiHandler()
yield* geminiHandler.createMessage(systemPrompt, messages)
return
}
const clientAnthropic = this.ensureAnthropicClient()
// Claude implementation
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn =
@@ -63,7 +100,7 @@ export class VertexHandler implements ApiHandler {
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.clientAnthropic.beta.messages.create(
stream = await clientAnthropic.beta.messages.create(
{
model: modelId,
max_tokens: model.info.maxTokens || 8192,
@@ -125,7 +162,7 @@ export class VertexHandler implements ApiHandler {
break
}
default: {
stream = await this.clientAnthropic.beta.messages.create({
stream = await clientAnthropic.beta.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
+7 -3
View File
@@ -5,10 +5,14 @@ import { calculateApiCostAnthropic } from "@utils/cost"
import { ApiStream } from "@api/transform/stream"
import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
import { withRetry } from "../retry"
interface VsCodeLmHandlerOptions {
vsCodeLmModelSelector?: any
}
// Cline does not update VSCode type definitions or engine requirements to maintain compatibility.
// This declaration (as seen in src/integrations/TerminalManager.ts) provides types for the Language Model API in newer versions of VSCode.
// Extracted from https://github.com/microsoft/vscode/blob/131ee0ef660d600cd0a7e6058375b281553abe20/src/vscode-dts/vscode.d.ts
@@ -124,12 +128,12 @@ declare module "vscode" {
* ```
*/
export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
private options: VsCodeLmHandlerOptions
private client: vscode.LanguageModelChat | null
private disposable: vscode.Disposable | null
private currentRequestCancellation: vscode.CancellationTokenSource | null
constructor(options: ApiHandlerOptions) {
constructor(options: VsCodeLmHandlerOptions) {
this.options = options
this.client = null
this.disposable = null
+37 -13
View File
@@ -1,26 +1,47 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "@shared/api"
import { XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "@shared/api"
import { convertToOpenAiMessages } from "@api/transform/openai-format"
import { ApiStream } from "@api/transform/stream"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { withRetry } from "../retry"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
interface XAIHandlerOptions {
xaiApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
export class XAIHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private options: XAIHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: XAIHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: this.options.xaiApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.xaiApiKey) {
throw new Error("xAI API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: this.options.xaiApiKey,
})
} catch (error: any) {
throw new Error(`Error creating xAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.getModel().id
// ensure reasoning effort is either "low" or "high" for grok-3-mini
let reasoningEffort: ChatCompletionReasoningEffort | undefined
@@ -30,7 +51,7 @@ export class XAIHandler implements ApiHandler {
reasoningEffort = undefined
}
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
@@ -50,10 +71,13 @@ export class XAIHandler implements ApiHandler {
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning_content,
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if (!shouldSkipReasoningForModel(modelId)) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning_content,
}
}
}
+6
View File
@@ -139,6 +139,10 @@ export async function createOpenRouterStream(
shouldApplyMiddleOutTransform = true
}
// hardcoded provider sorting for kimi-k2
const isKimiK2 = model.id.startsWith("moonshotai/kimi-k2")
openRouterProviderSorting = isKimiK2 ? undefined : openRouterProviderSorting
// @ts-ignore-next-line
const stream = await client.chat.completions.create({
model: model.id,
@@ -153,6 +157,8 @@ export async function createOpenRouterStream(
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
// limit providers to only those that support the 131k context window
...(isKimiK2 ? { provider: { order: ["groq", "together"], allow_fallbacks: false } } : {}),
})
return stream
+39
View File
@@ -156,6 +156,45 @@ replaced
expected: "line2\nreplaced\nline4",
isFinal: true,
},
{
name: "malformed diff - missing separator",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
+++++++ REPLACE
replaced`,
shouldThrow: true,
},
{
name: "malformed diff - trailing space on separator",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
=======
replaced
+++++++ REPLACE`,
shouldThrow: true,
},
{
name: "malformed diff - double replace markers",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
+++++++ REPLACE
first replacement
+++++++ REPLACE`,
shouldThrow: true,
},
{
name: "malformed diff - malformed separator with dashes",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
------- =======
replaced
+++++++ REPLACE`,
shouldThrow: true,
},
]
//.filter(({name}) => name === "multiple ordered replacements")
//.filter(({name}) => name === "delete then replace")
+4
View File
@@ -380,6 +380,10 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
if (isReplaceBlockEnd(line)) {
// Finished one replace block
if (searchMatchIndex === -1) {
throw new Error(`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...is malformatted.`)
}
// Store this replacement
replacements.push({
start: searchMatchIndex,
@@ -6,7 +6,7 @@ import * as path from "path"
import { FileContextTracker } from "./FileContextTracker"
import * as diskModule from "@core/storage/disk"
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
import type { WebviewProviderCreator } from "@/hosts/host-providers"
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-providers"
import * as hostProviders from "@hosts/host-providers"
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
@@ -53,7 +53,11 @@ describe("FileContextTracker", () => {
mockTaskMetadata = { files_in_context: [], model_usage: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
hostProviders.initializeHostProviders(((_) => {}) as WebviewProviderCreator, vscodeHostBridgeClient)
hostProviders.initializeHostProviders(
((_) => {}) as WebviewProviderCreator,
(() => {}) as DiffViewProviderCreator,
vscodeHostBridgeClient,
)
// Create tracker instance
taskId = "test-task-id"
@@ -36,17 +36,6 @@ export class FileContextTracker {
this.taskId = taskId
}
/**
* Gets the current working directory or returns undefined if it cannot be determined
*/
private async getCwd(): Promise<string | undefined> {
const cwd = await getCwd(undefined)
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
}
return cwd
}
/**
* File watchers are set up for each file that is tracked in the task metadata.
*/
@@ -56,8 +45,9 @@ export class FileContextTracker {
return
}
const cwd = await this.getCwd()
const cwd = await getCwd()
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
return
}
@@ -87,8 +77,9 @@ export class FileContextTracker {
*/
async trackFileContext(filePath: string, operation: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned") {
try {
const cwd = await this.getCwd()
const cwd = await getCwd()
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
return
}
@@ -244,7 +235,9 @@ export class FileContextTracker {
async storePendingFileContextWarning(files: string[]): Promise<void> {
try {
const key = `pendingFileContextWarning_${this.taskId}`
await updateWorkspaceState(this.context, key, files)
// NOTE: Using 'as any' because dynamic keys like pendingFileContextWarning_${taskId}
// are legitimate workspace state keys but don't fit the strict LocalStateKey type system
await updateWorkspaceState(this.context, key as any, files)
} catch (error) {
console.error("Error storing pending file context warning:", error)
}
@@ -256,7 +249,7 @@ export class FileContextTracker {
async retrievePendingFileContextWarning(): Promise<string[] | undefined> {
try {
const key = `pendingFileContextWarning_${this.taskId}`
const files = (await getWorkspaceState(this.context, key)) as string[]
const files = (await getWorkspaceState(this.context, key as any)) as string[]
return files
} catch (error) {
console.error("Error retrieving pending file context warning:", error)
@@ -271,7 +264,7 @@ export class FileContextTracker {
try {
const files = await this.retrievePendingFileContextWarning()
if (files) {
await updateWorkspaceState(this.context, `pendingFileContextWarning_${this.taskId}`, undefined)
await updateWorkspaceState(this.context, `pendingFileContextWarning_${this.taskId}` as any, undefined)
return files
}
} catch (error) {
@@ -302,7 +295,7 @@ export class FileContextTracker {
if (orphanedPendingContextTasks.length > 0) {
for (const key of orphanedPendingContextTasks) {
await updateWorkspaceState(context, key, undefined)
await updateWorkspaceState(context, key as any, undefined)
}
}
@@ -1,8 +1,9 @@
import * as vscode from "vscode"
import crypto from "crypto"
import { Controller } from "../index"
import { storeSecret } from "../../storage/state"
import { AuthService } from "@/services/auth/AuthService"
import { EmptyRequest, String } from "../../../shared/proto/common"
import { openExternal } from "@utils/env"
const authService = AuthService.getInstance()
/**
* Handles the user clicking the login link in the UI.
@@ -13,21 +14,5 @@ import { EmptyRequest, String } from "../../../shared/proto/common"
* @returns The login URL as a string.
*/
export async function accountLoginClicked(controller: Controller, _: EmptyRequest): Promise<String> {
// Generate nonce for state validation
const nonce = crypto.randomBytes(32).toString("hex")
await storeSecret(controller.context, "authNonce", nonce)
// Open browser for authentication with state param
console.log("Login button clicked in account page")
console.log("Opening auth page with state param")
const uriScheme = vscode.env.uriScheme
const authUrl = vscode.Uri.parse(
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
)
await vscode.env.openExternal(authUrl)
return String.create({
value: authUrl.toString(),
})
return await authService.createAuthRequest()
}
@@ -1,7 +1,9 @@
import { AuthService } from "@/services/auth/AuthService"
import { Empty } from "../../../shared/proto/common"
import type { EmptyRequest } from "../../../shared/proto/common"
import type { Controller } from "../index"
const authService = AuthService.getInstance()
/**
* Handles the account logout action
* @param controller The controller instance
@@ -10,5 +12,6 @@ import type { Controller } from "../index"
*/
export async function accountLogoutClicked(controller: Controller, _request: EmptyRequest): Promise<Empty> {
await controller.handleSignOut()
await authService.handleDeauth()
return Empty.create({})
}
@@ -1,4 +1,4 @@
import { AuthStateChangedRequest, AuthStateChanged } from "@shared/proto/account"
import { AuthStateChangedRequest, AuthState } from "@shared/proto/account"
import type { Controller } from "../index"
import { updateGlobalState } from "../../storage/state"
@@ -9,13 +9,13 @@ import { updateGlobalState } from "../../storage/state"
* @param request The auth state change request
* @returns The updated user info
*/
export async function authStateChanged(controller: Controller, request: AuthStateChangedRequest): Promise<AuthStateChanged> {
export async function authStateChanged(controller: Controller, request: AuthStateChangedRequest): Promise<AuthState> {
try {
// Store the user info directly in global state
await updateGlobalState(controller.context, "userInfo", request.user)
// Return the same user info
return AuthStateChanged.create({ user: request.user })
return AuthState.create({ user: request.user })
} catch (error) {
console.error(`Failed to update auth state: ${error}`)
throw error
@@ -0,0 +1,50 @@
import type { Controller } from "../index"
import { GetOrganizationCreditsRequest, OrganizationCreditsData, OrganizationUsageTransaction } from "@shared/proto/account"
/**
* Handles fetching all organization credits data (balance, usage, payments)
* @param controller The controller instance
* @param request Organization credits request
* @returns Organization credits data response
*/
export async function getOrganizationCredits(
controller: Controller,
request: GetOrganizationCreditsRequest,
): Promise<OrganizationCreditsData> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Call the individual RPC variants in parallel
const [balanceData, usageTransactions] = await Promise.all([
controller.accountService.fetchOrganizationCreditsRPC(request.organizationId),
controller.accountService.fetchOrganizationUsageTransactionsRPC(request.organizationId),
])
return OrganizationCreditsData.create({
balance: balanceData ? { currentBalance: balanceData.balance / 100 } : { currentBalance: 0 },
organizationId: balanceData?.organizationId || "",
usageTransactions:
usageTransactions?.map((tx) =>
OrganizationUsageTransaction.create({
aiInferenceProviderName: tx.aiInferenceProviderName,
aiModelName: tx.aiModelName,
aiModelTypeName: tx.aiModelTypeName,
completionTokens: tx.completionTokens,
costUsd: tx.costUsd,
createdAt: tx.createdAt,
creditsUsed: tx.creditsUsed,
generationId: tx.generationId,
organizationId: tx.organizationId,
promptTokens: tx.promptTokens,
totalTokens: tx.totalTokens,
userId: tx.userId,
}),
) || [],
})
} catch (error) {
console.error(`Failed to fetch organization credits data: ${error}`)
throw error
}
}
@@ -8,7 +8,7 @@ import { UserCreditsData } from "@shared/proto/account"
* @param request Empty request
* @returns User credits data response
*/
export async function fetchUserCreditsData(controller: Controller, request: EmptyRequest): Promise<UserCreditsData> {
export async function getUserCredits(controller: Controller, request: EmptyRequest): Promise<UserCreditsData> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
@@ -21,11 +21,10 @@ export async function fetchUserCreditsData(controller: Controller, request: Empt
controller.accountService.fetchPaymentTransactionsRPC(),
])
// Since generated types match exactly, no conversion needed!
return UserCreditsData.create({
balance: balance ? { currentBalance: balance.currentBalance } : { currentBalance: 0 },
usageTransactions: usageTransactions || [],
paymentTransactions: paymentTransactions || [],
balance: balance ? { currentBalance: balance.balance / 100 } : { currentBalance: 0 },
usageTransactions: usageTransactions,
paymentTransactions: paymentTransactions,
})
} catch (error) {
console.error(`Failed to fetch user credits data: ${error}`)
@@ -0,0 +1,35 @@
import type { Controller } from "../index"
import type { EmptyRequest } from "@shared/proto/common"
import { UserOrganization, UserOrganizationsResponse } from "@shared/proto/account"
/**
* Handles fetching all user credits data (balance, usage, payments)
* @param controller The controller instance
* @param request Empty request
* @returns User credits data response
*/
export async function getUserOrganizations(controller: Controller, request: EmptyRequest): Promise<UserOrganizationsResponse> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Fetch user organizations from the account service
const organizations = await controller.accountService.fetchUserOrganizationsRPC()
return UserOrganizationsResponse.create({
organizations:
organizations?.map((org) =>
UserOrganization.create({
active: org.active,
memberId: org.memberId,
name: org.name,
organizationId: org.organizationId,
roles: org.roles ? [...org.roles] : [],
}),
) || [],
})
} catch (error) {
throw error
}
}
@@ -0,0 +1,24 @@
import type { Controller } from "../index"
import { Empty } from "@shared/proto/common"
import { UserOrganizationUpdateRequest } from "@shared/proto/account"
/**
* Handles setting the user's active organization
* @param controller The controller instance
* @param request UserOrganization to set as active
* @returns Empty response
*/
export async function setUserOrganization(controller: Controller, request: UserOrganizationUpdateRequest): Promise<Empty> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Switch to the specified organization using the account service
await controller.accountService.switchAccount(request.organizationId)
return Empty.create({})
} catch (error) {
throw error
}
}
@@ -1,59 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest } from "../../../shared/proto/common"
import { String as ProtoString } from "../../../shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active authCallback subscriptions
const activeAuthCallbackSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to authCallback events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToAuthCallback(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeAuthCallbackSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeAuthCallbackSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "authCallback_subscription" }, responseStream)
}
}
/**
* Send an authCallback event to all active subscribers
* @param customToken The custom token for authentication
*/
export async function sendAuthCallbackEvent(customToken: string): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeAuthCallbackSubscriptions).map(async (responseStream) => {
try {
const event: ProtoString = {
value: customToken,
}
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending authCallback event:", error)
// Remove the subscription if there was an error
activeAuthCallbackSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -0,0 +1,5 @@
import { AuthService } from "../../../services/auth/AuthService"
const authService = AuthService.getInstance()
export const subscribeToAuthStatusUpdate = authService.subscribeToAuthStatusUpdate.bind(authService)
export const sendAuthStatusUpdateEvent = authService.sendAuthStatusUpdate.bind(authService)
+17 -5
View File
@@ -3,11 +3,12 @@ import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import * as vscode from "vscode"
import * as path from "path"
import { handleFileServiceRequest } from "./index"
import { cwd } from "@core/task"
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
import { getCwd, getDesktopDir } from "@/utils/path"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
/**
* Creates a rule file in either global or workspace rules directory
@@ -32,6 +33,7 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
throw new Error("Missing or invalid parameters")
}
const cwd = await getCwd(getDesktopDir())
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd, request.type)
if (!filePath) {
@@ -41,7 +43,13 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
if (fileExists) {
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
const message = `${fileTypeName} file "${request.filename}" already exists.`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
}),
)
// Still open it for editing
await handleFileServiceRequest(controller, "openFile", { value: filePath })
} else {
@@ -54,8 +62,12 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
await handleFileServiceRequest(controller, "openFile", { value: filePath })
vscode.window.showInformationMessage(
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
const message = `Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
}
+9 -2
View File
@@ -1,9 +1,10 @@
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import { RuleFile, RuleFileRequest } from "@shared/proto/file"
import * as path from "path"
import * as vscode from "vscode"
import { Controller } from ".."
import { FileMethodHandler } from "./index"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
/**
* Deletes a rule file from either global or workspace rules directory
@@ -44,7 +45,13 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
const message = `${fileTypeName} file "${fileName}" deleted successfully`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
return RuleFile.create({
filePath: request.rulePath,
+29 -44
View File
@@ -1,10 +1,10 @@
import { Controller } from ".."
import { RelativePathsRequest, RelativePaths } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import * as vscode from "vscode"
import { asRelativePath } from "@/utils/path"
import { RelativePaths, RelativePathsRequest } from "@shared/proto/file"
import * as path from "path"
import { StringRequest } from "@shared/proto/common"
import { getHostBridgeProvider } from "@hosts/host-providers"
import { URI } from "vscode-uri"
import { Controller } from ".."
import { FileMethodHandler } from "./index"
import { isDirectory } from "@/utils/fs"
/**
* Converts a list of URIs to workspace-relative paths
@@ -13,47 +13,32 @@ import { getHostBridgeProvider } from "@hosts/host-providers"
* @returns Response with resolved relative paths
*/
export const getRelativePaths: FileMethodHandler = async (
controller: Controller,
_controller: Controller,
request: RelativePathsRequest,
): Promise<RelativePaths> => {
const resolvedPaths = await Promise.all(
request.uris.map(async (uriString) => {
try {
// Use the host URI service client instead of directly using vscode.Uri.parse
const parseResponse = await getHostBridgeProvider().uriServiceClient.parse(
StringRequest.create({
value: uriString,
}),
)
const fileUri = vscode.Uri.parse(`${parseResponse.scheme}://${parseResponse.authority}${parseResponse.path}`)
console.log("[DEBUG] UriServiceClient.parse:", fileUri)
const relativePathToGet = vscode.workspace.asRelativePath(fileUri, false)
const result = []
for (const uriString of request.uris) {
try {
result.push(await getRelativePath(uriString))
} catch (error) {
console.error(`Error calculating relative path for ${uriString}:`, error)
}
}
return RelativePaths.create({ paths: result })
}
// If the path is still absolute, it's outside the workspace
if (path.isAbsolute(relativePathToGet)) {
console.warn(`Dropped file ${relativePathToGet} is outside the workspace. Sending original path.`)
return fileUri.fsPath.replace(/\\/g, "/")
} else {
let finalPath = "/" + relativePathToGet.replace(/\\/g, "/")
try {
const stat = await vscode.workspace.fs.stat(fileUri)
if (stat.type === vscode.FileType.Directory) {
finalPath += "/"
}
} catch (statError) {
console.error(`Error stating file ${fileUri.fsPath}:`, statError)
}
return finalPath
}
} catch (error) {
console.error(`Error calculating relative path for ${uriString}:`, error)
return null
}
}),
)
async function getRelativePath(uriString: string): Promise<string> {
const filePath = URI.parse(uriString, true).fsPath
const relativePath = await asRelativePath(filePath)
// Filter out any null values from errors
const validPaths = resolvedPaths.filter((path): path is string => path !== null)
// If the path is still absolute, it's outside the workspace
if (path.isAbsolute(relativePath)) {
throw new Error(`Dropped file ${relativePath} is outside the workspace.`)
}
return RelativePaths.create({ paths: validPaths })
let result = "/" + relativePath.replace(/\\/g, "/")
if (await isDirectory(filePath)) {
result += "/"
}
return result
}

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