Compare commits

...
Author SHA1 Message Date
BarreiroT 2fe23fe319 Add Banners feature flag 2026-01-14 17:00:43 -03:00
Tomás Barreiro 963abc190e Reduce the amount of banner requests (#8575)
* Reduce the amount of sent banner requests

* Revert not fetching if no token is provided

* Remove redundant null

* Make a single call

* Make another request if forceRefresh is true

* Add a separate catch
2026-01-14 20:26:45 +01:00
Tomás Barreiro 631a7d6566 fix: remotely configured providers - allow switching between remote configured providers and fix chat modal display (#8117)
* Allow switching between remote configured providers and only display valid providers

* Add changeset

* Return the provider set by the remote config

* Address comments

* Address comment

* Validate when updating settings

* Refactor

* Revert

* Use a more descriptive name

* Fix types

* Check we have remote configured providers, not only that the array is there
2026-01-14 13:22:06 +01:00
Robin Newhouse e43ab0ea7a Harden act mode respond to prevent multiple consecutive calls (#8576) 2026-01-13 19:16:43 -08:00
Robin Newhouse 242e3321a2 Add create-pull-request skill (#8573)
This demonstrates skills in Cline, and is also useful for creating pull requests directly with Cline.
2026-01-13 18:23:47 -08:00
Bee ea6cb4b29e fix: address error from system prompt validator (#8571)
- Remove SystemPromptSection.MCP from Gemini-3 component order
- Disable feedback section in XS variant component overrides
- Update variant validator to allow disabled overrides without requiring them in componentOrder/tools list

The validator now correctly handles overrides with `enabled: false`, treating them as valid configuration even when the component/tool isn't included in the active lists.
2026-01-13 17:17:52 -08:00
Ara 2c75285566 Update Package lock.json for release (#8569) 2026-01-13 14:27:17 -08:00
github-actions[bot]andArafatkatze 8279f2e145 Release notes for v3.49.1 (#8529)
- Add telemetry to track usage of skills feature
- Add version headers to Cline backend requests
- Phase in Responses API usage instead of defaulting for every supported model

- Fix workflow slash command search to be case-insensitive
- Fix model display in ModelPickerModal when using LiteLLM
- Fix LiteLLM model fetching with default base URL
- Fix crash when OpenAI-compatible APIs send usage chunks with empty or null choices arrays at end of streaming
- Fix model ID for Kat Coder Pro Free model

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-13 12:24:30 -08:00
Ara 5b94ba3ef9 Fix the model id for KatCoder Pro free models (#8558)
* Fix the model id for KatCoder Pro free models

* Fix the model id for KatCoder Pro free models

* Fix the model id for KatCoder Pro free models
2026-01-13 09:47:59 -08:00
MaxandMax Paulus 🥪 703146182a add cline pr review as a github workflow (#8434)
fix pr review workflow

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-13 09:04:47 -08:00
BeeandSaoud Rizwan 9603643b77 refactor: Chat Streaming UI (#8264)
* feat(ui): add new row components and unify ChatRow styling with tailwinds and lucid icons

- Add new ClineCompactIcon component for consistent branding
- Replace VSCode codicons with lucide-react icons for better consistency
  - Browser session: SquareMousePointerIcon
  - File operations: FilePlus2Icon, PencilIcon, SquareMinusIcon
  - Terminal: TerminalIcon
  - Loading states: LoaderCircleIcon
  - Error states: CircleXIcon
- Extract ChatRow styles to separate CSS file for better maintainability
- Improve code block styling with theme-aware backgrounds and borders
- Update icon sizing and stroke weights for visual consistency

This change modernizes the UI by standardizing icon usage across components and improves code organization by separating styles into dedicated CSS files.

* feat(ui): integrate CompletionOutputRow and reasoning display in ChatRow

Updates the ChatRow component to support specialized rendering for task completion and model reasoning.

- Integrates `CompletionOutputRow` and `PlanCompletionOutputRow` for structured completion states.
- Adds `ThinkingRow` integration and props for handling `reasoningContent`.
- Updates `ChatRowProps` to include mode and request status tracking.
- Refines Storybook mocks to demonstrate reasoning steps and detailed completion results.

* feat(webview): group low-stakes tool executions in chat view

- Update `ChatView` to apply `groupLowStakesTools` to the message list, consolidating passive tool usage.
- Overhaul `MessageRenderer` to support rendering grouped tool messages with specific display info (icons, labels) for actions like `readFile`, `listFiles`, and `searchFiles`.
- Add logic to format search regex patterns for better readability.
- Implement utility checks for calculating costs and pending states within tool groups.
- This change reduces UI clutter by visually collapsing repetitive information-gathering steps.

* refactor(ui): update checkpoint control UI and restore menu

- Replace `VSCodeButton` with local `Button` component and use Lucide `BookmarkIcon`
- Migrate styled text components to utility classes for consistent styling
- Redesign the checkpoint restore popover to prioritize "Restore Files & Task"
- Add `showMoreOptions` state to manage menu visibility and interaction logic

* refactor(chat): rename CSS file for CompletionOutputRow

Renames `ChatRow.css` to `CompletionOutputRow.css` to align with the component naming convention. This change includes updating the import in `CompletionOutputRow.tsx` to reference the correctly named stylesheet.

* clean up PlanCompletionOutputRow

* clean up

* clean up

* update e2e

* update displayName

* fix blinking cursor position

* use classnames

* Completion notch

* clean up header class

* Move Command Output component to CommandOutputRow

* Fix shimmering animation

* update TypewriterText story title

* clean up notch style

* Seperate ToolGroupRenderer into individual component. Clean up styles and message utils.

* fix truncation display

* update styles for open file links

* apply feedback - fix CompletionOutputRow & ThinkingRow

* Display old Ask block for tools

* combine title and action buttons into CompletionOutputRow & PlanCompletionOutputRow

* remove animation from Cline icon

* update styles and animation

* adjust spacing

* Fix shimmering animation

* clean up

* clean up and simplify component styles

* clean up import names

* fix markdown block and use tailwind styles

* clean up spacing

* hide scrollbar

* remove expand handler

* cline logo position

* fix(chat): align logo to top in request progress indicator

Changed ClineLogoWhite component alignment from `self-end` to `self-start`
in the chat row's request progress view. This ensures the logo aligns to
the top rather than the bottom when displaying in-progress requests,
improving visual consistency with the adjacent message content.

* fix DiffEditRow title truncation

* Keep Cline logo for output text

* fix(chat): add invisible spacer for non-rendered rows

Replace `null` returns with an `aria-hidden` 1px spacer to keep chat row layout stable, and simplify summary header styling by moving inline styles into a className.

* update activity indicators and button styling for tool group

- Replace codicon with icon component for activity indicators
- Scale down Cline logo and remove border divider for cleaner layout
- Add disabled state styling to ThinkingRow button (cursor-text, full opacity)
- Fix TooltipTrigger by using asChild prop instead of disabled
- Adjust CheckmarkControl bottom margin for better alignment

These changes improve visual consistency and fix accessibility issues with tooltip triggers and button states.
<budget:token_budget>200000</budget:token_budget>

* revert: show cline logo during stream only

* remove streaming thinking title

* spacing

* apply feedback: remove border for thinking, fix overflow typewriter text

* fix(ui): align thinking text and reasoning content positions

- Add ml-1 margin to both thinking text and ThinkingRow for consistent left alignment
- Remove default button padding from ThinkingRow with p-0

* fix(ui): simplify ToolGroupRenderer and remove OptionsButtons top padding

- Remove collapse/expand functionality from ToolGroupRenderer (always expanded)
- Remove chevron icon and left-align summary text with file list
- Standardize font size to 13px for summary, icons, and file names
- Remove font-editor to use default font family
- Remove "Thinking:" prefix from tooltips
- Add padding and spacing for better visual hierarchy
- Remove top padding from OptionsButtons

* fix(ui): restore CodeAccordian padding and overflow

* fix(ui): reduce spacing between header and content text

* fix(ui): restore task completion buttons to original style

- Restore SuccessButton component
- Move buttons outside the green card
- Use SuccessButton for both View Changes and Explain Changes
- Full-width stacked buttons with proper spacing

* fix(ui): polish Task Completed and Plan Created card styling

- Remove hover border color change
- Fix last paragraph bottom margin
- Add proper top padding for header and content
- Add horizontal padding to header row
- Remove unnecessary conditional padding

* fix(ui): style tweaks for copy button and checkpoint label

- Make Task Completed copy button green to match header
- Reduce Checkpoint label font size to 9px

* fix(ui): prevent TypewriterText from jumping on completion

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-01-12 22:11:37 -08:00
Bee c6dce7fb17 feat: force dependency pre-bundling in vite config (#8550)
Add optimizeDeps configuration with force flag to ensure Vite
re-optimizes dependencies on every build. This resolves potential
issues with stale or inconsistent dependency resolution in the
webview build process.
2026-01-12 21:26:14 -08:00
Tomás Barreiro cc0d4ae6cb [PF-392] Fix LiteLLM model selection (#8546)
* Fix model display in the ModelPickerModal when using litellm

* Add changeset

* Cleanup

* Fix model selection
2026-01-13 05:00:57 +01:00
BeeandTomás Barreiro a9365e30e9 refactor: simplify API configuration management and state handling (#8415)
* refactor: simplify API configuration management and state handling

Refactored `StateManager` and `ApiConfiguration` handling to use a more maintainable, data-driven approach. Replaced manual key mapping in `setApiConfiguration` with automated categorization based on static definitions.

- Updated `buildApiHandler` and `createHandlerForProvider` to accept `Partial<ApiConfiguration>`, improving flexibility.
- Introduced `categorizeApiConfigurationKeys` and other helpers to separate settings from secrets automatically.
- Centralized secret key definitions in `state-keys.ts` to reduce boilerplate and potential for errors when adding new providers.
- Cleaned up redundant imports and type definitions across the core API and storage modules.

* apply feedback

* clean up

* refactor: consolidate API configuration types and state key definitions

- Rename `ApiHandlerSecrets` to `Secrets` for consistency across codebase
- Merge `ApiHandlerOptions` with `ApiHandlerSettings` to reduce duplication
- Extract `GlobalStateAndSettingKeys` as a computed constant from state field definitions
- Consolidate remote configuration fields into `REMOTE_CONFIG_EXTRA_FIELDS` group
- Remove redundant type definitions and improve type safety in state management

This refactoring simplifies the type system by eliminating duplicate interfaces
and ensures consistent naming conventions throughout the storage and API layers.

* Clean up

* rename type with default

* type safe

* add unit test

* Apply suggestions from code review

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>

* apply feedback

---------

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2026-01-12 19:45:47 -08:00
canvrno efe468d9b1 Add telemetry for skills feature (#8548)
* Added telemetry for skills feature

* Use safeCapture for skills telemetry capture

* Include skill source in skill telemetry
2026-01-12 19:38:36 -08:00
Tomás Barreiro 1b6202604d Fix remote config check (#8549) 2026-01-13 04:11:17 +01:00
cryptoqueandSarah Fortune ea1dbd8bea feat: When remote config is enabled, add logic for enterprise to control local MCPs via remote config (#8175)
* add requirements

* add requirements checklist

* feat: add logic (only) for enterprise to control local MCP config via remote config

* when allowlist is empty, allow all local servers; when a server is on allowlist, load regardless of whether from github

* fix comment, use Object.keys(remoteConfig).length to check if remote config is on or not

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-01-12 18:35:42 -08:00
Saoud Rizwan e43517519e fix(e2e): increase getSidebar timeout for slower macOS CI runners (#8547) 2026-01-12 17:15:53 -08:00
520d08c5f2 Send basic telemetry headers when making requests to the Cline backend (#8413)
* Send basic telemetry headers when making requests to the Cline backend

* Add changeset

* Update src/services/account/ClineAccountService.ts

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-01-13 01:23:56 +01:00
Chaitanya Erankiandcelestial-vault 31a55ac87e fix: modelInfo for Oracle Code Assist provider not being saved in Cline CLI and removing extra log statements (#8447)
* Fixed bugs within cline cli and removed extra console.log

* Removed old models from using Responses API

* Revertred last commit'

* Added changeset

---------

Co-authored-by: celestial-vault <58194240+celestial-vault@users.noreply.github.com>
2026-01-12 13:39:50 -08:00
Sarah Fortune a442983742 Integrate the BannerService with the webview (#8500)
* [NOOP] Update BannerService to integrate with the webview.

Update the BannerService to convert the banners to the BannerCardData format for the webview.

Add a field for the banners in the `ExtensionState`.

In the WelcomeSection, get the banners from the extension state and show them in the webview.

NOOP- this is a currently a no-op because the controller is not yet populating the `banners` field in the extension state. I will submit that in a second PR because we need the handlers for the dismissal logic before we can start displaying the banners.

# Conflicts:
#	src/shared/ExtensionMessage.ts

* Update tests

* Validate the banner action type before sending it to the webview

* Handle dimiss for API banners

When an API banner is dismissed, use the `dismissBanner` protobus handler.

Add warning comments saying not to use the old banner version system. This not scalable as it requires a different protobus handlers for each type of banner. You can get the same effect by using the banner ID and appending a version number to the ID.

* Send the banners from the extension to the webview

The controllers populates the banners in the extension state.
Add a check for buttons with empty titles because they don't render properly and this is an error in the banner configuration if it happens.

* Add handler to Link action button in the webview.

* Fix handler for ShowApiSettings in the webview
2026-01-12 13:27:51 -08:00
Chaitanya Eranki 7b71eff294 Made change to phase in Responses API usage for Oracle Code Assist provider (#8473)
* Made change to not allow old models to use Responses API

* Added changeset

* Removing oca from nextGenModelProvier so that we remove native tool calls for now

* Adding back oca as a nextGenModelProvider
2026-01-12 13:05:25 -08:00
yuvalman 6d1890f8bb fix: litellm - trigger model fetching with default base URL (#8359) 2026-01-12 21:36:29 +01:00
Tomás Barreiro 11d17fc17e Fix auth state loop (#8496)
* Prevent loop when getting user organization

* Do not restore user info if the org he is switching to is already active

* Add changeset

* Fix reference array
2026-01-12 19:21:26 +01:00
Tomás Barreiro 42a3dc6150 Prevent requests with an expired auth token (#8470)
* Verify the auth token is valid before returning it

* Add changeset

* refactor
2026-01-12 19:20:33 +01:00
Tomás Barreiro 4032e51e8d Allow admins and owners to override remote config (#8304)
* Add field to settings and handle side effects

* Avoid fetching and applying remote config if it's disabled

* Refactor and apply configured org settings when the user opted out of another one he owns

* Refactor

Fix check

* Add toggle to the account view

* Add changeset

* Fix can disable remote config

* clean canDisableRemoteConfig
2026-01-12 19:19:26 +01:00
Saoud Rizwan 1bbc90487c fix: guard against null/empty choices in streaming responses (#8527)
* fix: guard against null/empty choices in streaming responses

Some OpenAI-compatible APIs (DeepSeek, Groq, OpenWebUI, etc.) send
usage chunks at the end of streaming with empty or null choices arrays.
This was causing crashes with 'Cannot read properties of undefined'.

Added optional chaining on chunk.choices across all 24 affected
provider files to safely handle these usage-only chunks.

Fixes #8384

* chore: add changeset
2026-01-10 19:43:04 -08:00
Saoud Rizwan d422ebbb27 Revert "fix: normalize file paths with spaces before extensions from VS Code …" (#8528)
This reverts commit 362429a317.
2026-01-10 18:18:51 -08:00
Yuri ChukhlibandYuri Chukhlib 7470d234ef feat: add image support for Claude 3.5 Haiku
Update Claude 3.5 Haiku model to support image processing as per 
Anthropic API release notes.

Fixes #2009

Co-authored-by: Yuri Chukhlib <yurii.chukhlib@viber.com>
2026-01-10 18:13:12 -08:00
Yuri ChukhlibandYuri Chukhlib 21b81f1844 fix: close context menu when pressing Escape key
When the @ mention context menu shows "No results found" and the user
presses Escape, the menu was not closing because setShowContextMenu(false)
was not being called.

Fixes #5532

Co-authored-by: Yuri Chukhlib <yurii.chukhlib@viber.com>
2026-01-10 18:12:31 -08:00
Yuri ChukhlibandYuri Chukhlib 362429a317 fix: normalize file paths with spaces before extensions from VS Code LM API
Some LLM providers (notably Claude Sonnet 4.5 via VS Code LM API) insert
spurious spaces before file extensions (e.g., "file .ts" instead of "file.ts").

This fix adds heuristic normalization to remove spaces immediately before
file extensions while preserving legitimate spaces in filenames.

Fixes #7827

Co-authored-by: Yuri Chukhlib <yurii.chukhlib@viber.com>
2026-01-10 18:11:33 -08:00
Yuri ChukhlibandYuri Chukhlib 09cb9ac9ac fix: make workflow slash command search case-insensitive
Users can now find workflows regardless of letter casing (e.g., searching "/testhook" finds "Testhook").

Fixes #7834

Co-authored-by: Yuri Chukhlib <yurii.chukhlib@viber.com>
2026-01-10 18:09:51 -08:00
Ara 94160faeef package update (#8499) 2026-01-09 20:18:44 -08:00
Ara f526f70e3a package update (#8498) 2026-01-09 20:03:33 -08:00
github-actions[bot]andArafatkatze d9b47378c6 v3.49.0 Release Notes (#8467)
- Enable configuring an OTEL collector at runtime
- Removing Minimax-2.1 from free model list as the free trial has ended
- Improved image display in MCP responses
- Auto-sync remote MCP servers from remote config to local settings

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-09 19:46:23 -08:00
Saoud Rizwan 0671c59e6d feat(mcp): improve image display in MCP responses (#8412)
* feat(mcp): improve image display in MCP responses

- Truncate data URIs to show prefix + first 20 chars with [IMAGE] label
- Apply truncation in all display modes (rich, plain, markdown)
- Click data URI images to open in VS Code editor (like mermaid diagrams)
- Expand images to 100% width of response container
- Persist collapsed/expanded state per-response without syncing all instances

* fix(settings): remove Collapse MCP Responses setting from UI

The setting is now implicit - collapsing any MCP response saves the
preference for future responses. Removes confusing sync behavior
between the Settings toggle and individual response toggles.
2026-01-09 18:20:55 -08:00
cryptoqueandSarah Fortune bf87887501 feat: Auto-sync remote MCP servers from remote config to local settings (#8146)
* feat: add remote config sync with extension mcp marketplace for new remote servers

* refactor: extract getMcpSettingsFilePath into disk.ts to be reused

* refactor: rename helper method to avoid ambiguity

* address formatting suggestion by ellipsis-dev for the code itself that was moved

* refactor: add flag pattern to prevent race condition from triggering unnecessary watcher events

* fix: do not re-throw error

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-01-09 16:35:28 -08:00
celestial-vault 66a81a6efa remove unused param from pushToolResult (#8474) 2026-01-09 14:59:13 -08:00
Sarah Fortune eee64c5204 Remove unused react banners (#8463) 2026-01-09 14:03:14 -08:00
Tomás Barreiro 748ba99c1c Remove the IAuthProvider (#8469)
* Remove the IAuthProvider

* Remove the comment

* Remove optional chaining
2026-01-09 20:39:39 +01:00
Juan Pablo FloresandTony Loehr 82b1a01644 Adds mcp server support (#8177)
Co-authored-by: Tony Loehr <turingxo@gmail.com>
2026-01-09 11:06:43 -08:00
Tomás Barreiro 1c6307e8ad Remove other references to OTEL_TELEMETRY_ENABLED=1 (#8468) 2026-01-09 10:22:36 -08:00
Tomás Barreiro 58d9c0af18 Enable configuring an OTEL collector at runtime (#8350)
* Replace process.env usage with a BUILD_CONSTANTS variable

* Update import

* revert doc update

* Enable configuring an OTEL collector at runtime

* Refactor

* Refactor

* Add changeset

* Do not build IS_STANDALONE

* Add comment

* Update the `.env.example` file

* Remove `true` from the selected options and revert env.example

* Use `true` for runtime variables
2026-01-09 18:57:24 +01:00
Sarah Fortune bf213c24ea Refactoring (#8462) 2026-01-08 22:35:35 -08:00
github-actions[bot]andArafatkatze 80cceaa3ae v3.48.0 Release Notes (#8407)
- Add Skills system for reusable, on-demand agent instructions
- Add new websearch tooling in Cline provider
- Add zai-glm-4.7 to Cerebras model list
- Add model refresh and improve reasoning support for Vercel AI Gateway

- Revert #8341 due to regressions in diff view/document truncation (see #8423, #8429)
- Fixed extension crash when using context menu selector

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-08 18:42:53 -08:00
Ara 359e088eb6 Gemini thinking + Katcoder support (#8459)
* Fix: Support for gemini thinking

* Fix: Katcoder

* Fix: Katcoder

* Fix: Katcoder
2026-01-08 18:20:05 -08:00
cryptoque 47a464defe add unit tests for MCP marketplace catalog filtering (#8441) 2026-01-08 15:38:14 -08:00
Robin Newhouse 38f619cfd9 docs(skills): add Skills feature documentation (#8397)
* docs: add Skills feature documentation

Add comprehensive documentation for the Agent Skills feature including:
- Overview of what skills are and why they're useful
- How to create skills with SKILL.md and YAML frontmatter
- Global vs project skill locations
- Managing skills via the UI toggle interface
- Real example (data-analysis skill)
- Bundling supporting files and scripts
- Comparison with Rules and Workflows
2026-01-08 15:18:27 -08:00
Robin Newhouse 050773ac31 feat(skills): add Skills tab UI for managing skill toggles (#8396)
Oh. Add a new Skills tab to the Rules/Workflows modal that allows users to
view and toggle skills (global and workspace), create new skills from
templates, and delete existing skills. The tab only appears when the
skillsEnabled setting is on.

Changes:
- Add proto definitions for skills operations (refreshSkills, toggleSkill,
  createSkillFile, deleteSkillFile) with corresponding message types
- Add globalSkillsToggles to Settings and localSkillsToggles to LocalState
- Implement controller handlers for skills operations
- Add skills toggle state management to ExtensionStateContext
- Add Skills tab component to ClineRulesToggleModal
- Update RuleRow and NewRuleRow components to support skill type
- Implement lazy discovery for skills in UseSkillToolHandler (skills are
  discovered on-demand at execution time and filtered by toggle state)
- Use Tailwind CSS classes for styling consistency
2026-01-08 15:07:00 -08:00
Andrei EternalandAndrei Edell 6d67ff0b94 simplify nightly versioning with timestamps (#8453)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2026-01-08 14:54:24 -08:00
celestial-vault 18f4ef8b49 remove settings dir codeowners (#8451) 2026-01-08 14:51:06 -08:00
BeeandCopilot 085e69d142 fix: prevent duplicate diff error messages during file edits (#8431)
* fix: prevent duplicate diff error messages during file edits

Remove existing diff_error messages before displaying new ones to avoid
showing the same error multiple times when streaming file edits. This
ensures users only see the error once per occurrence, improving the UX
during tool execution with parallel tool calling disabled.

* Update src/core/task/tools/handlers/WriteToFileToolHandler.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 14:33:12 -08:00
Robin Newhouse 46aa66ed9d feat: add skillsEnabled setting to gate Skills feature (#8395)
Add experimental "Enable Skills" toggle in Settings > Features that
controls whether the Skills system is active. When disabled (default),
no directory scanning occurs and the use_skill tool is not exposed.

- Add skillsEnabled to Settings interface and ExtensionState
- Add skills_enabled to proto definitions
- Gate skill discovery in Task.attemptApiRequest()
- Add UI toggle in FeatureSettingsSection
2026-01-08 14:17:13 -08:00
Robin Newhouse 2ebbe954d9 feat(skills): Implement Skills system for reusable agent instructions (#8335)
feat(skills): add reusable Skills system and standardize global skills location

- Implement Skills system for reusable agent instructions loaded from project and global directories
- Support skill discovery and loading via stateless utilities
- Parse YAML frontmatter for skill metadata (name, description)
- Add use_skill tool for on-demand instruction loading
- List available skills in system prompt; global skills override project skills
- Define skills as directories with a SKILL.md file
- Add unit tests for skill utilities
- Global skills in ~/.cline/skills
- Introduce getClineHomePath() and update docs and tests for new path
2026-01-08 13:44:06 -08:00
Andrei EternalandAndrei Edell 067f5eea09 Npm publish main and ripgrep and cleanup (#8449)
* since npm nightly worked, make npm main

* fix ripgrep, split npm and jetbrains packaging

* cli nightly package version update

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2026-01-08 12:51:43 -08:00
cryptoque 2a48bad28c feat: [extensions] remotely configure whether enterprise users can disable a remote MCP server or not (#8426)
* feat: Enable remotely configure whetherenterprise users can disable an MCP server or not
2026-01-08 10:54:20 -08:00
celestial-vault c6f4584f7d fix: prevent unwanted editor focus stealing (#8038)
* control focus stealing via new param to focusChatInput

* pass preserveEditorFocus to getContextForCommand to fix e2e test
2026-01-08 07:25:25 -08:00
Ara a17b31070f feat(vercel-ai-gateway): add model refresh and improve reasoning support (#8398)
* feat(vercel-ai-gateway): add model refresh and reasoning support

- Add refreshVercelAiGatewayModelsRpc to ModelsService for fetching models
- Fix model ID/info references to use Vercel-specific parameters instead of OpenRouter
- Add reasoning effort and Gemini thinking level configuration support
- Skip reasoning content for incompatible models (devstral, grok-4)
- Improve model selection UI with keyboard navigation (ArrowUp/Down/Enter)
- Add model refresh functionality to settings interface

This enables proper model discovery and improves reasoning capabilities for Vercel AI Gateway provider, while fixing incorrect parameter references that were using OpenRouter naming conventions.

* refactor

* refactor

* refactor

* refactor

* refactor
2026-01-08 05:57:36 -08:00
Andrei EternalandAndrei Edell cad82d518d Remove version auto-increment for npm-nightly workflow (#8442)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2026-01-07 17:31:13 -08:00
Andrei EternalandAndrei Edell b4d7ec187f fix npm workflow permissions again (#8440)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2026-01-07 17:11:27 -08:00
Andrei EternalandAndrei Edell 42af8414e4 Npm nightly workflow fix permissions (#8439)
* First pass at npm nightly publish workflow

* go & ripgrep improvements

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2026-01-07 17:09:33 -08:00
Andrei EternalandAndrei Edell 8f6b9e8362 First pass at npm nightly publish workflow (#8438)
* First pass at npm nightly publish workflow

* go & ripgrep improvements

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2026-01-07 17:06:54 -08:00
MaxandMax Paulus 🥪 f1430359db show command denied message in cline CLI (#8344)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-07 16:11:44 -08:00
Ara 932695f70b changes (#8437) 2026-01-07 16:04:14 -08:00
cryptoque 489ee936c2 feat: UI changes for remote configured MCP servers (#8409)
* feat: hide the delete server ui when user and the remote mcp server is managed by remote config

* feat: add message to user if they are managed by remote config
2026-01-07 13:28:14 -08:00
Robin Newhouse dff7f61175 revert: #8341 (0d04205dc) due to DiffService truncateDocument regressions (#8423, #8429) (#8432) 2026-01-07 13:21:56 -08:00
Toshii aead42c6b8 add mcp prompting for webtools usage (#8425)
* add mcp prompting for usage

* native tool call snap test update

* updating capabilities section to add web tools
2026-01-07 11:26:41 -08:00
Seb Duerr db50a1c671 feat(cerebras): add zai-glm-4.7 (#8411)
- Add zai-glm-4.7 to Cerebras model list\n- Update model metadata (context window + descriptions)\n- Update Cerebras provider docs\n- Include changeset for release notes
2026-01-07 10:28:12 -08:00
Chaitanya Erankiandellipsis-dev[bot] bb20f60f1d Adding Responses API support to the Oracle Code Assist(OCA) Provider (#8388)
* Made changes for adding responses suppport

* removed some logs

* Made change to disallow format

* Added logging for cline

* Fixed codex prompts

* Made changes to make cline work

* Removed extra changes

* Added reasoning effort also to chat completions

* Made changes to fix issues with cline based on bugbash

* removed extra console.log statements

* Added extra changes to make reasoningEffortOptions working properly(outputs undefined)

* Made changes to code that make it cleaner

* created utility function for responses

* Removed extra console.log lines

* Fixed issues with tests not working

* Added changeset

* Update webview-ui/src/components/settings/providers/OcaModelPicker.tsx

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

* removing openai-native changes

* Switched to using api format instead of supportsResponsesApi and supportChatApi

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2026-01-06 22:35:40 -08:00
Tomás Barreiro a7333b7177 Remove remote OTEL config type casting (#8351) 2026-01-07 06:04:08 +01:00
Tomás Barreiro f30837a850 Do not request /users/me when fetching other data (#8410)
* Do not use /users/me when fetching other data

* Add changeset
2026-01-07 02:24:29 +01:00
MaxandMax Paulus 🥪 5660b2513f add cline pr review cline workflow action (#8284)
cline pr-review bot initial

cline permission system

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-06 17:20:05 -08:00
Tomás Barreiro 64e7e5fa4c Replace process.env usage with a BUILD_CONSTANTS variable (#8349)
* Replace process.env usage with a BUILD_CONSTANTS variable

* Update import

* revert doc update

* Do not build IS_STANDALONE
2026-01-07 01:07:28 +01:00
264 changed files with 12572 additions and 5770 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add create-pull-request skill
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix the selection of remotely configured providers
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Harden act_mode_respond to prevent consecutive calls
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: Verify selected index is not -1 when checking if an option is selectable in the context menu
+196
View File
@@ -0,0 +1,196 @@
---
name: create-pull-request
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, and PR creation using the gh CLI tool.
---
# Create Pull Request
This skill guides you through creating a well-structured GitHub pull request that follows project conventions and best practices.
## Prerequisites Check
Before proceeding, verify the following:
### 1. Check if `gh` CLI is installed
```bash
gh --version
```
If not installed, inform the user:
> The GitHub CLI (`gh`) is required but not installed. Please install it:
> - macOS: `brew install gh`
> - Other: https://cli.github.com/
### 2. Check if authenticated with GitHub
```bash
gh auth status
```
If not authenticated, guide the user to run `gh auth login`.
### 3. Verify clean working directory
```bash
git status
```
If there are uncommitted changes, ask the user whether to:
- Commit them as part of this PR
- Stash them temporarily
- Discard them (with caution)
## Gather Context
### 1. Identify the current branch
```bash
git branch --show-current
```
Ensure you're not on `main` or `master`. If so, ask the user to create or switch to a feature branch.
### 2. Find the base branch
```bash
git remote show origin | grep "HEAD branch"
```
This is typically `main` or `master`.
### 3. Analyze recent commits relevant to this PR
```bash
git log origin/main..HEAD --oneline --no-decorate
```
Review these commits to understand:
- What changes are being introduced
- The scope of the PR (single feature/fix or multiple changes)
- Whether commits should be squashed or reorganized
### 4. Review the diff
```bash
git diff origin/main..HEAD --stat
```
This shows which files changed and helps identify the type of change.
## Information Gathering
Before creating the PR, you need the following information. Check if it can be inferred from:
- Commit messages
- Branch name (e.g., `fix/issue-123`, `feature/new-login`)
- Changed files and their content
If any critical information is missing, use `ask_followup_question` to ask the user:
### Required Information
1. **Related Issue Number**: Look for patterns like `#123`, `fixes #123`, or `closes #123` in commit messages
2. **Description**: What problem does this solve? Why were these changes made?
3. **Type of Change**: Bug fix, new feature, breaking change, refactor, cosmetic, documentation, or workflow
4. **Test Procedure**: How was this tested? What could break?
### Example clarifying question
If the issue number is not found:
> I couldn't find a related issue number in the commit messages or branch name. What GitHub issue does this PR address? (Enter the issue number, e.g., "123" or "N/A" for small fixes)
## Git Best Practices
Before creating the PR, consider these best practices:
### Commit Hygiene
1. **Atomic commits**: Each commit should represent a single logical change
2. **Clear commit messages**: Follow conventional commit format when possible
3. **No merge commits**: Prefer rebasing over merging to keep history clean
### Branch Management
1. **Rebase on latest main** (if needed):
```bash
git fetch origin
git rebase origin/main
```
2. **Squash if appropriate**: If there are many small "WIP" commits, consider interactive rebase:
```bash
git rebase -i origin/main
```
Only suggest this if commits appear messy and the user is comfortable with rebasing.
### Push Changes
Ensure all commits are pushed:
```bash
git push origin HEAD
```
If the branch was rebased, you may need:
```bash
git push origin HEAD --force-with-lease
```
## Create the Pull Request
**IMPORTANT**: Read and use the PR template at `.github/pull_request_template.md`. The PR body format must **strictly match** the template structure. Do not deviate from the template format.
When filling out the template:
- Replace `#XXXX` with the actual issue number, or keep as `#XXXX` if no issue exists (for small fixes)
- Fill in all sections with relevant information gathered from commits and context
- Mark the appropriate "Type of Change" checkbox(es)
- Complete the "Pre-flight Checklist" items that apply
### Create PR with gh CLI
```bash
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main
```
Alternatively, create as draft if the user wants review before marking ready:
```bash
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main --draft
```
## Post-Creation
After creating the PR:
1. **Display the PR URL** so the user can review it
2. **Remind about CI checks**: Tests and linting will run automatically
3. **Suggest next steps**:
- Add reviewers if needed: `gh pr edit --add-reviewer USERNAME`
- Add labels if needed: `gh pr edit --add-label "bug"`
## Error Handling
### Common Issues
1. **No commits ahead of main**: The branch has no changes to submit
- Ask if the user meant to work on a different branch
2. **Branch not pushed**: Remote doesn't have the branch
- Push the branch first: `git push -u origin HEAD`
3. **PR already exists**: A PR for this branch already exists
- Show the existing PR: `gh pr view`
- Ask if they want to update it instead
4. **Merge conflicts**: Branch conflicts with base
- Guide user through resolving conflicts or rebasing
## Summary Checklist
Before finalizing, ensure:
- [ ] `gh` CLI is installed and authenticated
- [ ] Working directory is clean
- [ ] All commits are pushed
- [ ] Branch is up-to-date with base branch
- [ ] Related issue number is identified, or placeholder is used
- [ ] PR description follows the template exactly
- [ ] Appropriate type of change is selected
- [ ] Pre-flight checklist items are addressed
+3 -3
View File
@@ -72,12 +72,12 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
# Example configurations:
#
# Console debugging (logs only):
# OTEL_TELEMETRY_ENABLED=1
# OTEL_TELEMETRY_ENABLED=true
# OTEL_LOGS_EXPORTER=console
# TEL_DEBUG_DIAGNOSTICS=true
#
# OTLP with gRPC (insecure, for local testing):
# OTEL_TELEMETRY_ENABLED=1
# OTEL_TELEMETRY_ENABLED=true
# OTEL_LOGS_EXPORTER=otlp
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
@@ -85,7 +85,7 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
#
# OTLP with HTTP/JSON (production):
# OTEL_TELEMETRY_ENABLED=1
# OTEL_TELEMETRY_ENABLED=true
# OTEL_LOGS_EXPORTER=otlp
# OTEL_EXPORTER_OTLP_PROTOCOL=http/json
# OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
-1
View File
@@ -1,4 +1,3 @@
/docs/
/.github/ @saoudrizwan @garoth @sjf
/README.md @saoudrizwan @nickbaumann98
/src/core/storage/ @celestial-vault @abeatrix
+312
View File
@@ -0,0 +1,312 @@
name: Cline PR Code Review
on:
pull_request:
types:
[opened, ready_for_review]
# Manual trigger for backfilling existing PRs. Run from terminal:
# gh workflow run cline-pr-review.yml -f pr_number=1234
# Or batch process open PRs:
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
# gh workflow run cline-pr-review.yml -f pr_number=$num
# sleep 60
# done
workflow_dispatch:
inputs:
pr_number:
description: "PR number to review"
required: true
type: string
concurrency:
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: true
jobs:
cline-pr-review:
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
if: |
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 60
# SECURITY: These permissions are intentionally restrictive.
# - contents: read -> cline can read the codebase but CANNOT write/push any code
# - pull-requests: write -> cline can post reviews and inline suggestions
# - issues: read -> cline can search for related issues
# NOTE: Even with pull-requests: write, cline CANNOT merge PRs because branch protection
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
permissions:
contents: read
pull-requests: write
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- name: Install and Verify Cline CLI
run: |
npx cline version # verify installation
- name: Configure Cline with Anthropic
run: |
npx cline auth --provider anthropic \
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
--modelid claude-opus-4-5-20251101
- name: Get PR number
id: pr
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
fi
- name: Review PR with Cline
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
GITHUB_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
CLINE_COMMAND_PERMISSIONS: |
{
"allow": [
"gh pr diff *",
"gh pr view *",
"gh pr checks *",
"gh pr list *",
"gh label list *",
"gh issue list *",
"gh issue view *",
"git log *",
"gh pr comment ${{ steps.pr.outputs.number }} *",
"gh pr edit ${{ steps.pr.outputs.number }} *",
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
]
}
run: |
npx cline --yolo 'You'\''re a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
PR: #'"${PR_NUMBER}"'
## Gather context
```bash
# Get full PR details
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
# Get the diff
gh pr diff '"${PR_NUMBER}"'
# Check CI status
gh pr checks '"${PR_NUMBER}"'
# Get existing review comments (to understand context and your previous feedback)
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/comments --jq '\''.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'\''
# Get conversation comments
gh pr view '"${PR_NUMBER}"' --comments
```
If this is a re-review (workflow_dispatch event):
Read your previous comments carefully. Understand what you asked for before.
Check if new commits or comments address your previous feedback.
## Check contributing guidelines
Flag (but don'\''t block) if:
- Missing changeset - For user-facing changes, check if there'\''s a `.changeset/` file:
```bash
gh pr diff '"${PR_NUMBER}"' --name-only | grep '\''.changeset/'\'' || echo '\''No changeset found'\''
```
If missing, ask them to run `npm run changeset`
- Missing tests - New features should have tests
## Find related issues and PRs
Search thoroughly for context that might help with the review:
```bash
# Find related issues for context
gh issue list --search '\''<keywords from the PR>'\'' --state all --limit 30
gh issue list --search '\''<error messages or feature names>'\'' --state all --limit 20
# Find similar PRs for reference
gh pr list --search '\''<keywords>'\'' --state all --limit 30
```
For each relevant issue or PR you find, read it including comments:
```bash
gh issue view <number> --comments
gh pr view <number> --comments
```
Look for:
- Open issues this PR might fix that weren'\''t linked in the description
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
- Context from maintainer discussions that could inform your review
## Find subject matter experts
For files changed in this PR, find who knows the code best:
```bash
# Get files changed
gh pr diff '"${PR_NUMBER}"' --name-only
# For each relevant path, find contributors
git log --since='\''6 months ago'\'' --format='\''%an'\'' -- <path> | sort | uniq -c | sort -rn | head -5
```
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
| SME | Reason |
|-----|--------|
| @username1 | Authored PR #X which modified this area |
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
| @username3 | Reviewed similar PR #Y with extensive feedback |
## Bash command usage
Don'\''t use operators like `|`, `&&`, or `;` - run each command separately and analyze the output.
When referencing command outputs, quote them properly to avoid formatting issues.
## Deep code review
This is the most important part. Don'\''t just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
Step 1: Understand the intent
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
Step 2: Form your own opinion first
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
Step 3: Compare approaches
Now look at their implementation. How does it compare to what you would have done?
- Is their approach better in some ways? Note what they did well.
- Is their approach missing something? Be specific about what and why.
- Are there edge cases they haven'\''t considered?
- Does it follow the patterns established in similar parts of the codebase?
Step 4: Look at the bigger picture
- What other files or systems does this change interact with?
- Could this break anything else?
- Is there additional work needed beyond this PR to complete the feature?
- Does this fit well with the overall architecture?
Step 5: Find reference implementations
Look for similar changes in the codebase:
```bash
git log --oneline --all --grep='\''<relevant keywords>'\'' | head -20
git log --oneline -- <similar files> | head -20
```
If this is adding a new API provider, look at how other providers are implemented.
If this is adding a new feature, look at how similar features were added.
Note where their implementation aligns with or diverges from established patterns.
Step 6: Standard code review checks
- DRY: Is there duplicated code that could be extracted?
- Error handling: Are errors handled appropriately?
- Security: Any injection risks, credential exposure, unsafe dependencies?
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
- Types: Is TypeScript used correctly? Any unsafe type assertions?
- Naming: Are variables and functions named clearly?
- Comments: Is complex logic explained? Are there outdated comments?
## Inline code suggestions
For specific code improvements, use GitHub'\''s suggestion syntax via `gh api`.
This creates suggestions the author can commit with one click.
Single-line suggestion:
```bash
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
-X POST \
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
-f event='\''COMMENT'\'' \
-f body='\'''\'' \
-F comments='\''[
{
"path": "src/example.ts",
"line": 42,
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
}
]'\''
```
Multi-line suggestion (replacing lines 40-45):
```bash
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
-X POST \
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
-f event='\''COMMENT'\'' \
-f body='\'''\'' \
-F comments='\''[
{
"path": "src/example.ts",
"start_line": 40,
"line": 45,
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
}
]'\''
```
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
## Post your review
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
Start with a warm thank you for their contribution. Be conversational, not robotic.
Include what'\''s relevant:
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
- Related issues/PRs you found that provide useful context (link to them)
- Your review findings (issues to address, suggestions, etc.)
- Clear next steps for the author
Include a '\''For Maintainers'\'' section with:
- Anything else useful to help the maintainer resolve this PR
- Related issues/PRs with context on why they'\''re relevant
- Open issues this PR might fix that weren'\''t linked in the description
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
- SME table - who should review this and why
For the SME table:
| SME | Reason |
|-----|--------|
| @username | Primary contributor to affected files |
## Update labels
Add appropriate labels based on your analysis:
```bash
gh label list --json name,description --limit 100
gh pr edit '"${PR_NUMBER}"' --add-label '\''label1,label2'\''
```
When done, add the reviewed label:
```bash
gh pr edit '"${PR_NUMBER}"' --add-label '\''Bot Reviewed'\''
```
## Remember
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like '\''let me know if you have questions'\'', '\''I can help you with'\'', or '\''feel free to ask'\'' - you won'\''t be there to follow up. Frame any questions for the maintainers who will follow up.
- Be helpful and welcoming - Many contributors are new to the project
- Be specific - Point to exact lines and suggest fixes, don'\''t give vague feedback
- Think deeply - Don'\''t just surface-level review, understand the intent and evaluate the approach
- Use inline suggestions - Make it easy for authors to accept changes
- You'\''re a first-pass reviewer - A human maintainer will do final approval'
+130
View File
@@ -0,0 +1,130 @@
name: Publish NPM Release
on:
workflow_dispatch:
inputs:
confirm_publish:
description: 'Type "publish" to confirm you want to publish to NPM'
required: true
type: string
permissions:
contents: read
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
uses: ./.github/workflows/test.yml
publish-npm-release:
needs: test
name: Publish Cline CLI to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && github.event.inputs.confirm_publish == 'publish'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: cli/go.sum
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
- name: Read release version
id: version
run: |
# Read version from cli/package.json (stable version)
VERSION=$(node -p "require('./cli/package.json').version")
echo "Release version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Download ripgrep binaries
run: npm run download-ripgrep
- name: Clean previous builds
run: rm -rf dist-standalone
- name: Generate Protos (First Pass)
run: npm run protos && npm run protos-go
- name: Compile CLI
run: npm run compile-cli
- name: Compile CLI for all platforms
run: npm run compile-cli-all-platforms
- name: Build standalone NPM package
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
OTEL_TELEMETRY_ENABLED: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: npm run compile-standalone-npm
- name: Generate Protos (Second Pass - Bug Workaround)
run: npm run protos && npm run protos-go
- name: Verify build output
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
- name: Publish to NPM with latest tag
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
cd dist-standalone
npm publish --tag latest --access public
- name: Summary
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
echo ""
echo "📦 Install with: npm install -g cline"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
+175
View File
@@ -0,0 +1,175 @@
name: Publish NPM Nightly
on:
schedule:
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
permissions:
contents: read
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
uses: ./.github/workflows/test.yml
publish-npm-nightly:
needs: test
name: Publish Cline CLI (Nightly) to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check for recent commits
id: check_commits
run: |
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Found recent commits, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node.js
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Setup Go
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: cli/go.sum
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
if: steps.check_commits.outputs.skip != 'true'
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
if: steps.check_commits.outputs.skip != 'true'
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.check_commits.outputs.skip != 'true' && steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install webview-ui dependencies
if: steps.check_commits.outputs.skip != 'true' && steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
- name: Generate nightly version with timestamp
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
# Read base version from cli/package.json (e.g., "1.0.9")
BASE_VERSION=$(node -p "require('./cli/package.json').version")
# Generate timestamp (Unix epoch seconds)
TIMESTAMP=$(date +%s)
# Create unique nightly version: 1.0.9-nightly.1736365200
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: $BASE_VERSION"
echo "Generated nightly version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update cli/package.json with nightly version
if: steps.check_commits.outputs.skip != 'true'
run: |
# Update version with timestamp-based nightly version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('cli/package.json', 'utf8'));
pkg.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
"
echo "Using version ${{ steps.version.outputs.version }} for build"
cat cli/package.json | grep '"version"'
- name: Download ripgrep binaries
if: steps.check_commits.outputs.skip != 'true'
run: npm run download-ripgrep
- name: Clean previous builds
if: steps.check_commits.outputs.skip != 'true'
run: rm -rf dist-standalone
- name: Generate Protos (First Pass)
if: steps.check_commits.outputs.skip != 'true'
run: npm run protos && npm run protos-go
- name: Compile CLI
if: steps.check_commits.outputs.skip != 'true'
run: npm run compile-cli
- name: Compile CLI for all platforms
if: steps.check_commits.outputs.skip != 'true'
run: npm run compile-cli-all-platforms
- name: Build standalone NPM package
if: steps.check_commits.outputs.skip != 'true'
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
OTEL_TELEMETRY_ENABLED: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: npm run compile-standalone-npm
- name: Generate Protos (Second Pass - Bug Workaround)
if: steps.check_commits.outputs.skip != 'true'
run: npm run protos && npm run protos-go
- name: Verify build output
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
cd dist-standalone
npm publish --tag nightly --access public
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
echo ""
echo "📦 Install with: npm install -g cline@nightly"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
+7
View File
@@ -29,6 +29,8 @@ coverage-unit
*evals.env
.env
.secrets
.github/act/.secrets
.worktrees
@@ -39,3 +41,8 @@ webview-ui/src/services/grpc-client.ts
# E2E Tests
test-results
/.github/act
/pkg
.secrets
+39 -1
View File
@@ -1,8 +1,46 @@
# Changelog
## [3.49.1]
### Added
- Add telemetry to track usage of skills feature
- Add version headers to Cline backend requests
- Phase in Responses API usage instead of defaulting for every supported model
### Fixed
- Fix workflow slash command search to be case-insensitive
- Fix model display in ModelPickerModal when using LiteLLM
- Fix LiteLLM model fetching with default base URL
- Fix crash when OpenAI-compatible APIs send usage chunks with empty or null choices arrays at end of streaming
- Fix model ID for Kat Coder Pro Free model
## [3.49.0]
- Enable configuring an OTEL collector at runtime
- Removing Minimax-2.1 from free model list as the free trial has ended
- Improved image display in MCP responses
- Auto-sync remote MCP servers from remote config to local settings
## [3.48.0]
### Added
- Add Skills system for reusable, on-demand agent instructions
- Add new websearch tooling in Cline provider
- Add zai-glm-4.7 to Cerebras model list
- Add model refresh and improve reasoning support for Vercel AI Gateway
### Fixed
- Revert #8341 due to regressions in diff view/document truncation (see #8423, #8429)
- Fixed extension crash when using context menu selector
## [3.47.0]
### Added
- Added experimental support for Background Edits (allows editing files in background without opening the diff view)
- Updated free model to MiniMax M2.1 (replacing MiniMax M2)
- Added support for Azure based identity authentication in OpenAI Compatible provider and Azure OpenAI
@@ -1680,4 +1718,4 @@ Add Opus 4.1 through Claude Code
## [0.0.6]
- Initial release
- Initial release
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "1.0.3",
"version": "1.0.9",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "cline-core.js",
"bin": {
@@ -358,6 +358,9 @@ func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cli
if openRouterInfo, ok := modelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
} else if ocaInfo, ok := modelInfo.(*cline.OcaModelInfo); ok {
apiConfig.PlanModeOcaModelInfo = ocaInfo
apiConfig.ActModeOcaModelInfo = ocaInfo
}
}
@@ -426,6 +429,9 @@ func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider
if openRouterInfo, ok := updates.ModelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
} else if ocaInfo, ok := updates.ModelInfo.(*cline.OcaModelInfo); ok {
apiConfig.PlanModeOcaModelInfo = ocaInfo
apiConfig.ActModeOcaModelInfo = ocaInfo
}
}
+2 -2
View File
@@ -190,7 +190,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
}
// Step 3: Select model
modelID, _, err := pw.selectModel(cline.ApiProvider_OCA, "")
modelID, modelInfo, err := pw.selectModel(cline.ApiProvider_OCA, "")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
@@ -198,7 +198,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
// Step 4: Apply the OCA model configuration and set as active
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: nil,
ModelInfo: modelInfo,
}
if err := UpdateProviderPartial(pw.ctx, pw.manager, cline.ApiProvider_OCA, updates, true); err != nil {
+7
View File
@@ -339,6 +339,13 @@ func (tr *ToolRenderer) RenderCommandOutput(output string) string {
return result.String()
}
func (tr *ToolRenderer) RenderCommandPermissionDenied(command string) string {
command = strings.TrimSpace(command)
rendered := tr.renderMarkdown("### Command was denied")
message := fmt.Sprintf("Cline does not have permission to execute this command: `%s`", command)
return fmt.Sprintf("\n%s\n\n%s\n", rendered, message)
}
// RenderUserResponse renders user approval/rejection feedback
func (tr *ToolRenderer) RenderUserResponse(approved bool, feedback string) string {
var symbol, status string
+14
View File
@@ -94,6 +94,8 @@ func (h *SayHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
return h.handleHookStatus(msg, dc)
case string(types.SayTypeHookOutputStream):
return h.handleHookOutputStream(msg, dc)
case string(types.SayTypeCommandPermissionDenied):
return h.handleCommandPermissionDenied(msg, dc)
default:
return h.handleDefault(msg, dc)
}
@@ -346,6 +348,18 @@ func (h *SayHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayCon
return nil
}
func (h *SayHandler) handleCommandPermissionDenied(msg *types.ClineMessage, dc *DisplayContext) error {
if msg.Text == "" {
return nil
}
// Use unified ToolRenderer
rendered := dc.ToolRenderer.RenderCommandPermissionDenied(msg.Text)
output.Print(rendered)
return nil
}
func (h *SayHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) error {
var tool types.ToolMessage
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
+9
View File
@@ -992,6 +992,15 @@ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *Stre
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeCommandPermissionDenied):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
fmt.Println()
m.displayMessage(msg, false, false, i)
coordinator.MarkProcessedInCurrentTurn(msgKey)
}
case msg.Say == string(types.SayTypeBrowserActionLaunch):
msgKey := fmt.Sprintf("%d", msg.Timestamp)
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
+5 -2
View File
@@ -89,8 +89,9 @@ const (
SayTypeTaskProgress SayType = "task_progress"
// Hook status streaming from the backend.
// These values must match the backend "say" strings emitted by the extension.
SayTypeHookStatus SayType = "hook_status"
SayTypeHookOutputStream SayType = "hook_output_stream"
SayTypeHookStatus SayType = "hook_status"
SayTypeHookOutputStream SayType = "hook_output_stream"
SayTypeCommandPermissionDenied SayType = "command_permission_denied"
)
// ToolMessage represents a tool-related message
@@ -368,6 +369,8 @@ func convertProtoSayType(sayType cline.ClineSay) string {
return string(SayTypeHookStatus)
case cline.ClineSay_HOOK_OUTPUT_STREAM:
return string(SayTypeHookOutputStream)
case cline.ClineSay_COMMAND_PERMISSION_DENIED:
return string(SayTypeCommandPermissionDenied)
default:
return "unknown"
}
+1 -1
View File
@@ -150,7 +150,7 @@
},
"features/multiroot-workspace",
"features/plan-and-act",
"features/web-tools",
"features/skills",
{
"group": "Slash Commands",
"pages": [
@@ -58,59 +58,59 @@ Enable OpenTelemetry and configure an OTLP endpoint:
```bash
# Enable OpenTelemetry
export OTEL_TELEMETRY_ENABLED=1
export CLINE_OTEL_TELEMETRY_ENABLED=true
# Configure metrics and logs export
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export CLINE_OTEL_METRICS_EXPORTER=otlp
export CLINE_OTEL_LOGS_EXPORTER=otlp
# Set your OTLP endpoint
export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
# Optional: Set protocol (default is grpc)
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export CLINE_OTEL_EXPORTER_OTLP_PROTOCOL=grpc
```
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`1` or `true`) | Disabled |
| `OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
| `OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
| `OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
| `OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
| `CLINE_OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`true`) | Disabled |
| `CLINE_OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
| `CLINE_OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
| `CLINE_OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
| `CLINE_OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
| `CLINE_OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
| `CLINE_OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
### Advanced Configuration
**Separate endpoints for metrics and logs:**
```bash
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
export CLINE_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
export CLINE_OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
```
**Custom headers for authentication:**
```bash
export OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
```
**Multiple exporters (console + OTLP):**
```bash
export OTEL_METRICS_EXPORTER=console,otlp
export OTEL_LOGS_EXPORTER=console,otlp
export CLINE_OTEL_METRICS_EXPORTER=console,otlp
export CLINE_OTEL_LOGS_EXPORTER=console,otlp
```
**Export intervals:**
```bash
# Metrics export interval in milliseconds (default: 60000)
export OTEL_METRIC_EXPORT_INTERVAL=30000
export CLINE_OTEL_METRIC_EXPORT_INTERVAL=30000
# Logs batch size and timeout
export OTEL_LOG_BATCH_SIZE=512
export OTEL_LOG_BATCH_TIMEOUT=5000
export OTEL_LOG_MAX_QUEUE_SIZE=2048
export CLINE_OTEL_LOG_BATCH_SIZE=512
export CLINE_OTEL_LOG_BATCH_TIMEOUT=5000
export CLINE_OTEL_LOG_MAX_QUEUE_SIZE=2048
```
## Integration Examples
@@ -120,11 +120,11 @@ export OTEL_LOG_MAX_QUEUE_SIZE=2048
Export to Datadog using their OTLP endpoint:
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
export CLINE_OTEL_TELEMETRY_ENABLED=true
export CLINE_OTEL_METRICS_EXPORTER=otlp
export CLINE_OTEL_LOGS_EXPORTER=otlp
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
```
### New Relic
@@ -132,11 +132,11 @@ export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
Export to New Relic:
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
export CLINE_OTEL_TELEMETRY_ENABLED=true
export CLINE_OTEL_METRICS_EXPORTER=otlp
export CLINE_OTEL_LOGS_EXPORTER=otlp
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
```
### Grafana Cloud
@@ -144,11 +144,11 @@ export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
Export to Grafana Cloud:
```bash
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
export CLINE_OTEL_TELEMETRY_ENABLED=true
export CLINE_OTEL_METRICS_EXPORTER=otlp
export CLINE_OTEL_LOGS_EXPORTER=otlp
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
```
@@ -158,9 +158,9 @@ Test your configuration with console output before sending to a real endpoint:
```bash
# Enable console output to see what data would be exported
export OTEL_TELEMETRY_ENABLED=1
export OTEL_METRICS_EXPORTER=console
export OTEL_LOGS_EXPORTER=console
export CLINE_OTEL_TELEMETRY_ENABLED=true
export CLINE_OTEL_METRICS_EXPORTER=console
export CLINE_OTEL_LOGS_EXPORTER=console
```
Then launch Cline and check the console output for metrics and logs.
@@ -171,20 +171,20 @@ Then launch Cline and check the console output for metrics and logs.
1. **Verify OpenTelemetry is enabled:**
```bash
echo $OTEL_TELEMETRY_ENABLED
echo $CLINE_OTEL_TELEMETRY_ENABLED
```
Should output `1` or `true`
Should output `true`
2. **Check exporters are configured:**
```bash
echo $OTEL_METRICS_EXPORTER
echo $OTEL_LOGS_EXPORTER
echo $CLINE_OTEL_METRICS_EXPORTER
echo $CLINE_OTEL_LOGS_EXPORTER
```
3. **Test with console exporter first:**
```bash
export OTEL_METRICS_EXPORTER=console
export OTEL_LOGS_EXPORTER=console
export CLINE_OTEL_METRICS_EXPORTER=console
export CLINE_OTEL_LOGS_EXPORTER=console
```
### Connection Errors
@@ -196,7 +196,7 @@ Then launch Cline and check the console output for metrics and logs.
2. **Check if insecure mode is needed:**
```bash
export OTEL_EXPORTER_OTLP_INSECURE=true
export CLINE_OTEL_EXPORTER_OTLP_INSECURE=true
```
3. **Verify authentication headers:**
+231
View File
@@ -0,0 +1,231 @@
---
title: "Skills"
sidebarTitle: "Skills"
description: "Extend Cline with reusable, on-demand instruction sets for specialized tasks"
---
Skills are modular instruction sets that extend Cline's capabilities for specific tasks. Each skill packages detailed guidance, workflows, and optional resources that Cline loads only when relevant to your request.
Unlike rules (which are always active), skills load on-demand. You can install dozens of skills without affecting context or performance because Cline only sees the skill name and description until it's actually needed.
<Note>
Skills is an experimental feature. Enable it in Settings → Features → Enable Skills.
</Note>
## Why Skills?
Consider how you'd onboard a new team member: you wouldn't dump every document on them at once. You'd give them a brief overview, then point them to detailed guides when they're working on specific tasks.
Skills work the same way:
- **At startup**: Cline sees only a brief description of each skill
- **When triggered**: Cline loads the full instructions for that specific skill
- **As needed**: Skills can bundle additional files that Cline reads only when referenced
This progressive loading means you can package extensive domain knowledge without burning context tokens on information that isn't relevant to the current task.
## Creating a Skill
Every skill is a directory containing a `SKILL.md` file with YAML frontmatter:
```
my-skill/
├── SKILL.md # Required: main instructions
├── docs/ # Optional: additional documentation
│ └── advanced.md
└── scripts/ # Optional: utility scripts
└── helper.sh
```
The `SKILL.md` file has two parts: metadata and instructions.
```yaml
---
name: my-skill
description: Brief description of what this skill does and when to use it.
---
# My Skill
Detailed instructions for Cline to follow when this skill is activated.
## Steps
1. First, do this
2. Then do that
3. For advanced usage, see [advanced.md](docs/advanced.md)
```
**Required fields:**
- `name`: Must exactly match the directory name
- `description`: Tells Cline when to use this skill (max 1024 characters)
The description is critical because it's how Cline decides whether to activate a skill. Be specific about what the skill does and when it should be used.
## Where Skills Live
Skills can be stored in two locations:
**Global Skills** apply to all your projects:
- **macOS/Linux:** `~/.cline/skills/`
- **Windows:** `C:\Users\USERNAME\.cline\skills\`
**Project Skills** apply only to the current workspace:
- `.cline/skills/` (recommended)
- `.clinerules/skills/`
- `.claude/skills/` (for Claude Code compatibility)
When a global skill and project skill have the same name, the global skill takes precedence. This lets you customize skills for your personal workflow while still using project defaults.
## Managing Skills
Click the scale icon below the chat input to open the rules and workflows panel. When skills are enabled, you'll see a Skills tab where you can:
- View all available skills (global and workspace)
- Toggle individual skills on or off
- Create new skills from a template
- Delete skills you no longer need
Skills are enabled by default when discovered. Toggle them off if you want them available but not active for the current project.
## How Cline Uses Skills
When you send a message, Cline sees a list of available skills with their descriptions. If your request matches a skill's description, Cline activates it using the `use_skill` tool, which loads the full instructions.
For example, if you have a skill for deploying to AWS:
```yaml
---
name: aws-deploy
description: Deploy applications to AWS using CDK. Use when deploying, updating infrastructure, or managing AWS resources.
---
```
Asking "deploy this to AWS" would trigger Cline to activate the skill, load its detailed instructions, and follow them to complete your request.
## Example: Data Analysis Skill
Here's a practical skill for data analysis tasks. Create a directory called `data-analysis/` with this `SKILL.md`:
```yaml
---
name: data-analysis
description: Analyze data files and generate insights. Use when working with CSV, Excel, or JSON data files that need exploration, cleaning, or visualization.
---
```
Then add the instructions in the body of the file:
````markdown
# Data Analysis
When analyzing data files, follow this workflow:
## 1. Understand the Data
- Read a sample of the file to understand its structure
- Identify column types and data quality issues
- Note any missing values or anomalies
## 2. Ask Clarifying Questions
Before diving in, ask the user:
- What specific insights are they looking for?
- Are there any known data quality issues?
- What format do they want for the output?
## 3. Perform Analysis
Use pandas for data manipulation:
```python
import pandas as pd
# Load and explore
df = pd.read_csv("data.csv")
print(df.head())
print(df.describe())
print(df.info())
```
For visualization, prefer matplotlib or seaborn depending on complexity.
## 4. Present Findings
- Start with a summary of key insights
- Support findings with specific numbers
- Include visualizations where they add clarity
- End with recommendations or next steps
````
## Bundling Supporting Files
Skills can include additional files that Cline accesses only when needed:
```
complex-skill/
├── SKILL.md
├── docs/
│ ├── setup.md
│ └── troubleshooting.md
├── templates/
│ └── config.yaml
└── scripts/
└── validate.py
```
Reference these in your instructions:
````markdown
For initial setup, follow [setup.md](docs/setup.md).
Use the config template at `templates/config.yaml` as a starting point.
Run the validation script to check your configuration:
```bash
python scripts/validate.py
```
````
Cline reads these files using `read_file` when the instructions reference them. Scripts can be executed directly, with only the output entering the context (not the script code itself).
## Ideas for Skills
Skills shine when you have tasks that:
- Require detailed, multi-step workflows
- Need domain-specific knowledge or best practices
- Would otherwise require repeating the same instructions across conversations
Some possibilities:
- **Release management**: Version bumping, changelog generation, git tagging, and publishing
- **Code review**: Your team's specific review checklist and quality standards
- **Database migrations**: Safely evolving schemas with rollback procedures
- **API integration**: Connecting to specific third-party services with proper error handling
- **Documentation**: Your preferred structure, style guide, and tooling
- **Debugging workflows**: Systematic approaches to diagnosing specific types of issues
- **Infrastructure**: Terraform/CDK patterns for your cloud setup
The best skills encode institutional knowledge that would otherwise live only in experienced developers' heads.
## Skills vs Rules vs Workflows
| Feature | Purpose | When Active |
|---------|---------|-------------|
| **Rules** | Define how Cline should behave | Always (or contextually) |
| **Workflows** | Step-by-step task automation | Invoked with `/workflow.md` |
| **Skills** | Domain expertise loaded on-demand | Triggered by matching requests |
**Rules** set constraints and preferences (like "always use TypeScript" or "follow this style guide").
**Workflows** are explicit sequences you invoke for specific tasks (like `/release.md` for a release process).
**Skills** are expertise that Cline activates automatically when relevant (like data analysis knowledge when you're working with CSV files).
Use rules for ongoing constraints, workflows for explicit automation, and skills for domain knowledge that should be available but not always active.
## Related Features
- [Cline Rules](/features/cline-rules) for always-active project guidance
- [Workflows](/features/slash-commands/workflows/index) for explicit task automation
- [Hooks](/features/hooks/index) for injecting custom logic at key moments
@@ -120,8 +120,38 @@ Controls a built-in browser to interact with websites or local servers. Useful f
</browser_action>
```
### Leverage MCP Tools
You can use Model Context Protocol (MCP) tools within your workflows to interact with external services like GitHub, Slack, or databases. This allows you to create powerful end-to-end automations.
### Leveraging MCP Tools
MCP tools allow Cline to interact with external services like GitHub, Slack, or databases. You can reference them in your workflows using natural language or explicit XML tags for deterministic control.
#### Natural Language (Heuristic)
Most of the time, the simplest way to use an MCP tool is to describe the action you want Cline to take.
```markdown
1. Fetch the latest issues from the github-repo MCP server.
2. Summarize the critical bugs.
3. Post the summary to the #engineering channel using the slack-notifications MCP.
```
#### Explicit XML Tag (Deterministic)
For critical automation where you need exact control over parameters, use the `use_mcp_tool` tag.
```xml
<use_mcp_tool>
<server_name>github-repo-manager</server_name>
<tool_name>create_issue</tool_name>
<arguments>
{
"owner": "cline",
"repo": "cline",
"title": "Automated Bug Report",
"body": "Found a regression in the latest build."
}
</arguments>
</use_mcp_tool>
```
### Manage Context Window
Be mindful of Cline's context window. If a workflow is too long or processes too much data, it might exceed the token limit.
+3 -2
View File
@@ -18,7 +18,8 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
Cline supports the following Cerebras models:
- `zai-glm-4.6` - Intelligent general purpose model with 1,500 tokens/s
- `zai-glm-4.6` - Fast general-purpose model on Cerebras (up to 1,000 tokens/s). To be deprecated soon.
- `zai-glm-4.7` - Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
- `llama-3.3-70b` - Meta's Llama 3.3 model optimized for speed
@@ -89,7 +90,7 @@ Works with any OpenAI-compatible tool—Cursor, Continue.dev, Cline, or any othe
- **Speed Advantage:** Cerebras excels at making reasoning models practical for real-time use. Perfect for agentic workflows that require multiple LLM calls.
- **Free Tier:** Start with the free model to experience Cerebras speed before upgrading to paid plans.
- **Context Windows:** Models support context windows ranging from 64K to 128K tokens for including substantial code context.
- **Context Windows:** Models support context windows ranging from 64K to 131K tokens for including substantial code context.
- **Rate Limits:** Generous rate limits designed for development workflows. Check your dashboard for current limits.
- **Pricing:** Competitive pricing with significant speed advantages. Visit [Cerebras Cloud](https://cloud.cerebras.ai/) for current rates.
- **Real-Time Applications:** Ideal for applications where AI response time matters—code generation, debugging, and interactive development.
+333 -4
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.47.0",
"version": "3.49.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.47.0",
"version": "3.49.1",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -89,6 +89,7 @@
"puppeteer-core": "^23.4.0",
"reconnecting-eventsource": "^1.6.4",
"serialize-error": "^11.0.3",
"shell-quote": "^1.8.3",
"simple-git": "^3.27.0",
"strip-ansi": "^7.1.2",
"tailwindcss": "^4.1.14",
@@ -111,10 +112,12 @@
"@types/clone-deep": "^4.0.4",
"@types/diff": "^5.2.1",
"@types/get-folder-size": "^3.0.4",
"@types/js-yaml": "^4.0.9",
"@types/mocha": "^10.0.7",
"@types/node": "20.x",
"@types/pdf-parse": "^1.1.4",
"@types/proxyquire": "^1.3.31",
"@types/shell-quote": "^1.7.5",
"@types/should": "^11.2.0",
"@types/sinon": "^17.0.4",
"@types/turndown": "^5.0.5",
@@ -4896,6 +4899,314 @@
"node": ">=18"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
"integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
"integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
"integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
"integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
"integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
"integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
"integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
"integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
"integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
"integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
"integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
"integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
"integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
"integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
"integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
"integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
"integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
"integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
"integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
"integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
"integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
"integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@sap-ai-sdk/ai-api": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@sap-ai-sdk/ai-api/-/ai-api-2.1.0.tgz",
@@ -6473,6 +6784,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mocha": {
"version": "10.0.7",
"dev": true,
@@ -6521,6 +6839,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/shell-quote": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz",
"integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/shimmer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
@@ -16373,9 +16698,13 @@
}
},
"node_modules/shell-quote": {
"version": "1.8.1",
"dev": true,
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
+6 -4
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.47.0",
"version": "3.49.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -346,11 +346,10 @@
"compile-cli": "scripts/build-cli.sh",
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
"build:npm": "scripts/build-npm-package.sh",
"test:install": "bash scripts/test-install.sh",
"dev:cli:watch": "node scripts/dev-cli-watch.mjs",
"postcompile-standalone": "node scripts/package-standalone.mjs",
"postcompile-standalone-npm": "node scripts/package-standalone.mjs --target=npm",
"postcompile-standalone-npm": "node scripts/package-npm.mjs",
"dev": "npm run protos && npm run watch",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.mjs --watch",
@@ -367,7 +366,7 @@
"clean:all": "npm run clean:build && npm run clean:deps",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc -b --noEmit",
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit",
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
"lint:proto": "bash ./scripts/proto-lint.sh",
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
@@ -417,10 +416,12 @@
"@types/clone-deep": "^4.0.4",
"@types/diff": "^5.2.1",
"@types/get-folder-size": "^3.0.4",
"@types/js-yaml": "^4.0.9",
"@types/mocha": "^10.0.7",
"@types/node": "20.x",
"@types/pdf-parse": "^1.1.4",
"@types/proxyquire": "^1.3.31",
"@types/shell-quote": "^1.7.5",
"@types/should": "^11.2.0",
"@types/sinon": "^17.0.4",
"@types/turndown": "^5.0.5",
@@ -532,6 +533,7 @@
"puppeteer-core": "^23.4.0",
"reconnecting-eventsource": "^1.6.4",
"serialize-error": "^11.0.3",
"shell-quote": "^1.8.3",
"simple-git": "^3.27.0",
"strip-ansi": "^7.1.2",
"tailwindcss": "^4.1.14",
+54
View File
@@ -81,6 +81,18 @@ service FileService {
// Deletes an existing hook file
rpc deleteHook(DeleteHookRequest) returns (DeleteHookResponse);
// Refreshes all skill toggles (discovers skills and their enabled state)
rpc refreshSkills(EmptyRequest) returns (RefreshedSkills);
// Toggles a skill on or off
rpc toggleSkill(ToggleSkillRequest) returns (SkillsToggles);
// Creates a new skill from template
rpc createSkillFile(CreateSkillRequest) returns (SkillsToggles);
// Deletes an existing skill directory
rpc deleteSkillFile(DeleteSkillRequest) returns (SkillsToggles);
}
// Response for refreshRules operation
@@ -278,3 +290,45 @@ message DeleteHookRequest {
message DeleteHookResponse {
HooksToggles hooks_toggles = 1;
}
// Skill information structure
message SkillInfo {
string name = 1; // Name of the skill (matches directory name)
string description = 2; // Description from SKILL.md frontmatter
string path = 3; // Full path to SKILL.md file
bool enabled = 4; // Whether the skill is enabled
}
// Response for refreshSkills operation
message RefreshedSkills {
repeated SkillInfo global_skills = 1;
repeated SkillInfo local_skills = 2;
}
// Maps from skill path to enabled/disabled status
message SkillsToggles {
map<string, bool> global_skills_toggles = 1;
map<string, bool> local_skills_toggles = 2;
}
// Request to toggle a skill
message ToggleSkillRequest {
Metadata metadata = 1;
string skill_path = 2; // Path to the skill directory
bool is_global = 3; // Whether this is a global or workspace skill
bool enabled = 4; // Whether to enable or disable the skill
}
// Request to create a skill
message CreateSkillRequest {
Metadata metadata = 1;
string skill_name = 2; // Name of the skill to create
bool is_global = 3; // Whether to create in global or workspace skills directory
}
// Request to delete a skill
message DeleteSkillRequest {
Metadata metadata = 1;
string skill_path = 2; // Path to the skill directory
bool is_global = 3; // Whether this is a global or workspace skill
}
+20 -12
View File
@@ -49,6 +49,8 @@ service ModelsService {
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
// Fetches available models from AIhubmix
rpc getAihubmixModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Vercel AI Gateway models
rpc refreshVercelAiGatewayModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -381,6 +383,10 @@ message OcaModelInfo {
string model_name = 17;
// The API format used by this model
optional ApiFormat api_format = 18;
// Supports reasoning
optional bool supports_reasoning = 19;
// reasoning effort options
repeated string reasoning_effort_options = 20;
}
// Aggregated OCA model catalog keyed by model identifier
@@ -604,12 +610,13 @@ message ModelsApiConfiguration {
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
optional string plan_mode_oca_model_id = 131;
optional OcaModelInfo plan_mode_oca_model_info = 132;
optional string plan_mode_hicap_model_id = 133;
optional OpenRouterModelInfo plan_mode_hicap_model_info = 134;
optional string plan_mode_aihubmix_model_id = 135;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 136;
optional string plan_mode_nous_research_model_id = 137;
optional string gemini_plan_mode_thinking_level = 138;
optional string plan_mode_oca_reasoning_effort = 133;
optional string plan_mode_hicap_model_id = 134;
optional OpenRouterModelInfo plan_mode_hicap_model_info = 135;
optional string plan_mode_aihubmix_model_id = 136;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 137;
optional string plan_mode_nous_research_model_id = 138;
optional string gemini_plan_mode_thinking_level = 139;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
@@ -645,10 +652,11 @@ message ModelsApiConfiguration {
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
optional string act_mode_oca_model_id = 231;
optional OcaModelInfo act_mode_oca_model_info = 232;
optional string act_mode_hicap_model_id = 233;
optional OpenRouterModelInfo act_mode_hicap_model_info = 234;
optional string act_mode_aihubmix_model_id = 235;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 236;
optional string act_mode_nous_research_model_id = 237;
optional string gemini_act_mode_thinking_level = 238;
optional string act_mode_oca_reasoning_effort = 233;
optional string act_mode_hicap_model_id = 234;
optional OpenRouterModelInfo act_mode_hicap_model_info = 235;
optional string act_mode_aihubmix_model_id = 236;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 237;
optional string act_mode_nous_research_model_id = 238;
optional string gemini_act_mode_thinking_level = 239;
}
+5
View File
@@ -230,6 +230,8 @@ message Settings {
optional bool cline_web_tools_enabled = 134;
optional bool hooks_enabled = 135;
optional bool azure_identity = 136;
optional bool skills_enabled = 137;
optional bool opt_out_of_remote_config = 138;
}
message DictationSettings {
@@ -371,6 +373,9 @@ message UpdateSettingsRequest {
optional bool cline_web_tools_enabled = 34;
optional bool enable_parallel_tool_calling = 35;
optional bool background_edit_enabled = 36;
optional string oca_reasoning_effort = 37;
optional bool skills_enabled = 38;
optional bool opt_out_of_remote_config = 39;
}
message UpdateTerminalConnectionTimeoutRequest {
+7 -5
View File
@@ -69,6 +69,7 @@ enum ClineSay {
GENERATE_EXPLANATION = 29;
HOOK_STATUS = 30;
HOOK_OUTPUT_STREAM = 31;
COMMAND_PERMISSION_DENIED = 32;
}
// Enum for ClineSayTool tool types
@@ -222,6 +223,10 @@ message ClineMessage {
ClineModelInfo model_info = 23;
}
message ShowWebviewEvent {
bool preserve_editor_focus = 1; // When true, webview should not steal focus from editor
}
// UiService provides methods for managing UI interactions
service UiService {
// Scrolls to a specific settings section in the settings view
@@ -260,11 +265,8 @@ service UiService {
// Subscribe to relinquish control events
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
// Subscribe to focus chat input events
rpc subscribeToFocusChatInput(EmptyRequest) returns (stream Empty);
// Subscribe to webview visibility change events
rpc subscribeToDidBecomeVisible(EmptyRequest) returns (stream Empty);
// Subscribe to show webview events
rpc subscribeToShowWebview(EmptyRequest) returns (stream ShowWebviewEvent);
// Returns the HTML for the webview index page. This is only used by external clients, not by the vscode webview.
rpc getWebviewHtml(EmptyRequest) returns (String);
-154
View File
@@ -1,154 +0,0 @@
#!/usr/bin/env bash
# Script to build the Cline NPM package with telemetry keys injected
# This script ensures all environment variables are properly set and builds are successful
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Required environment variables
REQUIRED_VARS=(
"TELEMETRY_SERVICE_API_KEY"
"ERROR_SERVICE_API_KEY"
)
# Optional but recommended environment variables
OPTIONAL_VARS=(
"CLINE_ENVIRONMENT"
"POSTHOG_TELEMETRY_ENABLED"
)
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Cline NPM Package Build Script${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
# Step 1: Verify required environment variables are set
echo -e "${BLUE}Step 1: Verifying environment variables...${NC}"
MISSING_VARS=()
for VAR in "${REQUIRED_VARS[@]}"; do
if [ -z "${!VAR}" ]; then
MISSING_VARS+=("$VAR")
echo -e "${RED}$VAR is not set${NC}"
else
# Show first 10 chars for verification (don't expose full key)
VAR_VALUE="${!VAR}"
echo -e "${GREEN}$VAR is set (${VAR_VALUE:0:10}...)${NC}"
fi
done
# Check optional variables
for VAR in "${OPTIONAL_VARS[@]}"; do
if [ -z "${!VAR}" ]; then
echo -e "${YELLOW}$VAR is not set (optional)${NC}"
else
echo -e "${GREEN}$VAR is set: ${!VAR}${NC}"
fi
done
if [ ${#MISSING_VARS[@]} -gt 0 ]; then
echo -e "\n${RED}Error: Missing required environment variables:${NC}"
printf '%s\n' "${MISSING_VARS[@]}"
echo -e "\n${YELLOW}Please set these variables before running the build:${NC}"
echo -e "export TELEMETRY_SERVICE_API_KEY=\"your_posthog_api_key\""
echo -e "export ERROR_SERVICE_API_KEY=\"your_error_tracking_api_key\""
exit 1
fi
# Step 2: Verify Node.js can see the environment variables
echo -e "\n${BLUE}Step 2: Verifying Node.js can access environment variables...${NC}"
if node -e "
const telemetryKey = process.env.TELEMETRY_SERVICE_API_KEY;
const errorKey = process.env.ERROR_SERVICE_API_KEY;
if (!telemetryKey || !errorKey) {
console.error('Node.js cannot see environment variables!');
process.exit(1);
}
console.log('✓ TELEMETRY_SERVICE_API_KEY visible to Node.js');
console.log('✓ ERROR_SERVICE_API_KEY visible to Node.js');
"; then
echo -e "${GREEN}✓ Node.js can access environment variables${NC}"
else
echo -e "${RED}✗ Node.js cannot access environment variables${NC}"
echo -e "${YELLOW}Make sure to use 'export' when setting variables:${NC}"
echo -e "export TELEMETRY_SERVICE_API_KEY=\"...\""
exit 1
fi
# Step 3: Clean previous builds
echo -e "\n${BLUE}Step 3: Cleaning previous builds...${NC}"
rm -rf dist-standalone
echo -e "${GREEN}✓ Cleaned dist-standalone directory${NC}"
# Step 4: Build Go CLI binaries for all platforms
echo -e "\n${BLUE}Step 4: Building Go CLI binaries for all platforms...${NC}"
if npm run compile-cli-all-platforms; then
echo -e "${GREEN}✓ Go CLI binaries built successfully${NC}"
# Verify binaries were created
if ls cli/bin/cline-* 1> /dev/null 2>&1; then
echo -e "${GREEN}✓ CLI binaries verified:${NC}"
ls -lh cli/bin/cline-* | awk '{print " " $9 " (" $5 ")"}'
else
echo -e "${RED}✗ No CLI binaries found in cli/bin/${NC}"
exit 1
fi
else
echo -e "${RED}✗ Failed to build Go CLI binaries${NC}"
exit 1
fi
# Step 5: Build the standalone package with esbuild
echo -e "\n${BLUE}Step 5: Building standalone package with esbuild...${NC}"
if npm run compile-standalone-npm; then
echo -e "${GREEN}✓ Standalone package built successfully${NC}"
else
echo -e "${RED}✗ Failed to build standalone package${NC}"
exit 1
fi
# Step 6: Verify telemetry keys were injected
echo -e "\n${BLUE}Step 6: Verifying telemetry keys were injected...${NC}"
# Check if the compiled file still has process.env references (bad)
if grep -q "process.env.TELEMETRY_SERVICE_API_KEY" dist-standalone/cline-core.js; then
echo -e "${RED}✗ Keys were NOT injected! Found 'process.env.TELEMETRY_SERVICE_API_KEY' in compiled code${NC}"
echo -e "${YELLOW}This means the environment variables were not replaced during build${NC}"
exit 1
fi
# Check if actual keys are present (good)
if grep -q "data.cline.bot" dist-standalone/cline-core.js; then
# Extract a snippet of the PostHog config
POSTHOG_CONFIG=$(grep -A 3 "data.cline.bot" dist-standalone/cline-core.js | head -5)
if echo "$POSTHOG_CONFIG" | grep -q "apiKey.*phc_"; then
echo -e "${GREEN}✓ Telemetry keys successfully injected into compiled code${NC}"
else
echo -e "${YELLOW}⚠ PostHog config found but apiKey format unclear${NC}"
echo -e "${YELLOW}Config snippet:${NC}"
echo "$POSTHOG_CONFIG"
fi
else
echo -e "${YELLOW}⚠ Could not verify PostHog config in compiled code${NC}"
fi
# Step 7: Display build summary
echo -e "\n${BLUE}========================================${NC}"
echo -e "${GREEN}Build completed successfully!${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
echo -e "${GREEN}Package location:${NC} dist-standalone/"
echo -e "${GREEN}Package version:${NC} $(node -p "require('./dist-standalone/package.json').version" 2>/dev/null || echo "unknown")"
echo ""
echo -e "${BLUE}Next steps:${NC}"
echo -e "1. Test locally: ${YELLOW}cd dist-standalone && npm link${NC}"
echo -e "2. Verify: ${YELLOW}cline version${NC}"
echo -e "3. Publish: ${YELLOW}cd dist-standalone && npm publish${NC}"
echo ""
echo -e "${YELLOW}Note: Check PostHog dashboard after running cline commands to verify telemetry${NC}"
+7
View File
@@ -43,6 +43,13 @@ const PLATFORMS = [
binaryPath: "rg",
isZip: false,
},
{
name: "linux-arm64",
archiveName: `ripgrep-${RIPGREP_VERSION}-aarch64-unknown-linux-gnu.tar.gz`,
url: `https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-aarch64-unknown-linux-gnu.tar.gz`,
binaryPath: "rg",
isZip: false,
},
{
name: "win-x64",
archiveName: `ripgrep-${RIPGREP_VERSION}-x86_64-pc-windows-msvc.zip`,
+10 -1
View File
@@ -49,9 +49,18 @@ fi
# Create installation directory
mkdir -p "$INSTALL_DIR/bin"
# Copy standalone package first (includes node_modules, cline-core.js, etc.)
# Copy standalone package first (cline-core.js, wasm files, etc.)
rsync -a --exclude='bin' "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
# Install runtime dependencies (grpc-health-check, better-sqlite3, etc.)
# These are external dependencies not bundled into cline-core.js
echo -e "${CYAN}${NC} ${DIM}Installing runtime dependencies...${NC}"
cd "$PROJECT_ROOT/standalone/runtime-files"
npm install --silent 2>/dev/null || npm install
cp -r node_modules "$INSTALL_DIR/"
cp -r vscode "$INSTALL_DIR/node_modules/"
cd "$PROJECT_ROOT"
# Detect platform for native modules
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m)
+486
View File
@@ -0,0 +1,486 @@
#!/usr/bin/env node
/**
* NPM Package Builder for Cline CLI
*
* This script builds the Cline CLI NPM package (dist-standalone/).
* It is completely independent from package-standalone.mjs (JetBrains build).
*
* Usage: node scripts/package-npm.mjs
*
* Prerequisites:
* - npm run protos && npm run protos-go
* - npm run compile-cli
* - npm run compile-cli-all-platforms
* - npm run download-ripgrep
*/
import { execSync } from "child_process"
import fs from "fs"
import { cp } from "fs/promises"
import path from "path"
const BUILD_DIR = "dist-standalone"
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
const RIPGREP_BINARIES_DIR = `${BUILD_DIR}/ripgrep-binaries`
const CLI_BINARIES_DIR = "cli/bin"
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
async function main() {
console.log("🚀 Building Cline NPM Package\n")
await installNodeDependencies()
await copyCliBinaries()
await copyRipgrepBinaries()
await copyProtoDescriptors()
await createNpmPackageFiles()
await createFakeNodeModules()
await createNpmIgnoreFile()
await createPostinstallScript()
console.log("\n✅ Build complete!")
console.log(`\n📦 NPM package ready in ${BUILD_DIR}/`)
console.log(`To publish: cd ${BUILD_DIR} && npm publish`)
}
/**
* Install node dependencies in the build directory
*/
async function installNodeDependencies() {
// Clean modules from any previous builds
await rmrf(path.join(BUILD_DIR, "node_modules"))
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
console.log("Running npm install in distribution directory...")
execSync("npm install", { stdio: "inherit", cwd: BUILD_DIR })
// 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(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`)
}
/**
* Copy CLI binaries (cline and cline-host) for all platforms
* The Go binaries are cross-compiled for darwin/linux arm64/amd64
*/
async function copyCliBinaries() {
console.log("Copying CLI binaries for all platforms...")
const platforms = [
{ os: "darwin", arch: "arm64" },
{ os: "darwin", arch: "amd64" },
{ os: "linux", arch: "amd64" },
{ os: "linux", arch: "arm64" },
]
const binDir = path.join(BUILD_DIR, "bin")
// Create bin directory
fs.mkdirSync(binDir, { recursive: true })
// Copy all platform-specific binaries
for (const { os, arch } of platforms) {
const platformSuffix = `${os}-${arch}`
// Copy cline binary
const clineSource = path.join(CLI_BINARIES_DIR, `cline-${platformSuffix}`)
const clineDest = path.join(binDir, `cline-${platformSuffix}`)
if (!fs.existsSync(clineSource)) {
console.error(`Error: CLI binary not found at ${clineSource}`)
console.error(`Please run: npm run compile-cli-all-platforms`)
process.exit(1)
}
await cpr(clineSource, clineDest)
fs.chmodSync(clineDest, 0o755)
console.log(`✓ cline-${platformSuffix} copied`)
// Copy cline-host binary
const hostSource = path.join(CLI_BINARIES_DIR, `cline-host-${platformSuffix}`)
const hostDest = path.join(binDir, `cline-host-${platformSuffix}`)
if (!fs.existsSync(hostSource)) {
console.error(`Error: CLI binary not found at ${hostSource}`)
console.error(`Please run: npm run compile-cli-all-platforms`)
process.exit(1)
}
await cpr(hostSource, hostDest)
fs.chmodSync(hostDest, 0o755)
console.log(`✓ cline-host-${platformSuffix} copied`)
}
console.log(`✓ All CLI binaries copied to ${binDir}`)
}
/**
* Copy ripgrep binaries for ALL platforms
* Ripgrep is needed by cline-core for file searching
* The postinstall script will select the correct binary for the user's platform
*/
async function copyRipgrepBinaries() {
console.log("Copying ripgrep binaries for all platforms...")
const platforms = [
{ dir: "darwin-arm64", binary: "rg" },
{ dir: "darwin-x64", binary: "rg" },
{ dir: "linux-x64", binary: "rg" },
{ dir: "linux-arm64", binary: "rg" },
// { dir: "win-x64", binary: "rg.exe" }, // Windows not supported yet
]
const ripgrepDir = path.join(BUILD_DIR, "ripgrep")
// Create ripgrep directory
fs.mkdirSync(ripgrepDir, { recursive: true })
// Check if ripgrep binaries exist, download if missing
const firstPlatform = platforms[0]
const firstBinaryPath = path.join(RIPGREP_BINARIES_DIR, firstPlatform.dir, firstPlatform.binary)
if (!fs.existsSync(firstBinaryPath)) {
console.log(`Ripgrep binaries not found, downloading...`)
try {
execSync("npm run download-ripgrep", { stdio: "inherit" })
} catch (error) {
console.error(`Error downloading ripgrep: ${error.message}`)
console.error(`Please run: npm run download-ripgrep`)
process.exit(1)
}
}
// Copy all platform-specific binaries
for (const { dir, binary } of platforms) {
const source = path.join(RIPGREP_BINARIES_DIR, dir, binary)
const dest = path.join(ripgrepDir, `rg-${dir}`)
if (!fs.existsSync(source)) {
console.error(`Error: Ripgrep binary not found at ${source}`)
console.error(`Please run: npm run download-ripgrep`)
process.exit(1)
}
await cpr(source, dest)
fs.chmodSync(dest, 0o755)
console.log(`✓ rg-${dir} copied`)
}
console.log(`✓ All ripgrep binaries copied to ${ripgrepDir}`)
}
/**
* Verify proto descriptors exist in the build directory
* The proto/descriptor_set.pb file is generated by build-proto.mjs to dist-standalone/proto/
* We do NOT copy from proto/ source because that would overwrite the freshly generated descriptor
*/
async function copyProtoDescriptors() {
console.log("Verifying proto descriptors...")
const protoDest = path.join(BUILD_DIR, "proto")
const descriptorPath = path.join(protoDest, "descriptor_set.pb")
// Check if descriptor_set.pb exists in the build directory
// It should have been generated by `npm run protos` which runs build-proto.mjs
if (!fs.existsSync(descriptorPath)) {
console.error(`Error: proto/descriptor_set.pb not found at ${descriptorPath}`)
console.error(`Please run: npm run protos`)
console.error(`Note: build-proto.mjs generates the descriptor to dist-standalone/proto/`)
process.exit(1)
}
// Verify the descriptor is recent (not stale)
const stats = fs.statSync(descriptorPath)
const ageMinutes = (Date.now() - stats.mtimeMs) / 1000 / 60
if (ageMinutes > 60) {
console.warn(`Warning: descriptor_set.pb is ${Math.round(ageMinutes)} minutes old`)
console.warn(`Consider running: npm run protos`)
}
console.log(`✓ Proto descriptors verified at ${protoDest}`)
}
/**
* Copy NPM package files (package.json, README.md, and man page) from cli/ directory
*/
async function createNpmPackageFiles() {
console.log("Copying NPM package files...")
// Copy package.json from cli/ directory
const packageJsonSource = path.join("cli", "package.json")
const packageJsonDest = path.join(BUILD_DIR, "package.json")
if (!fs.existsSync(packageJsonSource)) {
console.error(`Error: NPM package.json not found at ${packageJsonSource}`)
process.exit(1)
}
await cpr(packageJsonSource, packageJsonDest)
console.log(`✓ package.json copied from ${packageJsonSource}`)
// Copy README.md from cli/ directory
const readmeSource = path.join("cli", "README.md")
const readmeDest = path.join(BUILD_DIR, "README.md")
if (!fs.existsSync(readmeSource)) {
console.error(`Error: NPM README.md not found at ${readmeSource}`)
process.exit(1)
}
await cpr(readmeSource, readmeDest)
console.log(`✓ README.md copied from ${readmeSource}`)
// Copy man page from cli/man/ directory
const manPageSource = path.join("cli", "man", "cline.1")
const manDir = path.join(BUILD_DIR, "man")
const manPageDest = path.join(manDir, "cline.1")
if (!fs.existsSync(manPageSource)) {
console.error(`Error: Man page not found at ${manPageSource}`)
process.exit(1)
}
// Create man directory if it doesn't exist
fs.mkdirSync(manDir, { recursive: true })
await cpr(manPageSource, manPageDest)
console.log(`✓ Man page copied from ${manPageSource}`)
}
/**
* Create fake_node_modules directory with vscode stub
* This directory will be added to NODE_PATH so Node.js can find the vscode module
* without npm interfering with the real node_modules directory
*/
async function createFakeNodeModules() {
console.log("Creating fake_node_modules with vscode stub...")
const vscodeSource = path.join(BUILD_DIR, "node_modules", "vscode")
const fakeNodeModulesDir = path.join(BUILD_DIR, "fake_node_modules")
const vscodeDest = path.join(fakeNodeModulesDir, "vscode")
if (!fs.existsSync(vscodeSource)) {
console.error(`Error: vscode stub module not found at ${vscodeSource}`)
process.exit(1)
}
// Create fake_node_modules directory
fs.mkdirSync(fakeNodeModulesDir, { recursive: true })
// Copy vscode stub into fake_node_modules
await cpr(vscodeSource, vscodeDest)
console.log(`✓ fake_node_modules/vscode created at ${vscodeDest}`)
}
/**
* Create .npmignore file to ensure necessary files are included
*/
async function createNpmIgnoreFile() {
console.log("Creating .npmignore file...")
// Create .npmignore that excludes build artifacts
// Note: proto/ directory is NOT excluded because proto/descriptor_set.pb is needed at runtime
const npmignoreContent = `# Exclude build artifacts and unnecessary files
binaries/
ripgrep-binaries/
standalone.zip
cline-core.js.map
package-lock.json
tree-sitter*.wasm
node_modules/vscode
`
const npmignorePath = path.join(BUILD_DIR, ".npmignore")
fs.writeFileSync(npmignorePath, npmignoreContent)
console.log(`✓ .npmignore created`)
}
/**
* Create postinstall script for NPM package
* This script selects the correct platform-specific binary and creates symlinks
*/
async function createPostinstallScript() {
console.log("Creating postinstall script...")
const postinstallScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const os = require('os');
// Detect current platform and architecture
function getPlatformInfo() {
const platform = os.platform();
const arch = os.arch();
// Map Node.js arch names to Go arch names (for CLI binaries)
let goArch = arch;
if (arch === 'x64') {
goArch = 'amd64';
}
// Map for ripgrep binaries (uses different naming)
let rgArch = arch;
if (arch === 'arm64') {
rgArch = 'arm64';
} else if (arch === 'x64') {
rgArch = 'x64';
}
return { platform, arch, goArch, rgArch };
}
// Setup platform-specific binaries
function setupBinaries() {
const { platform, goArch, rgArch } = getPlatformInfo();
const cliPlatformSuffix = \`\${platform}-\${goArch}\`;
const rgPlatformSuffix = \`\${platform}-\${rgArch}\`;
console.log(\`Setting up Cline CLI for \${cliPlatformSuffix}...\`);
// Setup CLI binaries
const binDir = path.join(__dirname, 'bin');
// Check if platform-specific binaries exist
const clineSource = path.join(binDir, \`cline-\${cliPlatformSuffix}\`);
const clineHostSource = path.join(binDir, \`cline-host-\${cliPlatformSuffix}\`);
if (!fs.existsSync(clineSource)) {
console.error(\`Error: Binary not found for platform \${cliPlatformSuffix}\`);
console.error(\`Expected: \${clineSource}\`);
console.error(\`Supported platforms: darwin-arm64, darwin-amd64, linux-amd64, linux-arm64\`);
process.exit(1);
}
if (!fs.existsSync(clineHostSource)) {
console.error(\`Error: Binary not found for platform \${cliPlatformSuffix}\`);
console.error(\`Expected: \${clineHostSource}\`);
process.exit(1);
}
// Create symlinks or copies to the generic names
const clineTarget = path.join(binDir, 'cline');
const clineHostTarget = path.join(binDir, 'cline-host');
// Remove existing files if they exist
[clineTarget, clineHostTarget].forEach(target => {
if (fs.existsSync(target)) {
try {
fs.unlinkSync(target);
} catch (e) {
console.warn(\`Warning: Could not remove existing file \${target}: \${e.message}\`);
}
}
});
// On Unix, create symlinks; on Windows, copy files
if (platform === 'win32') {
// Windows: copy files
fs.copyFileSync(clineSource, clineTarget);
fs.copyFileSync(clineHostSource, clineHostTarget);
console.log('✓ Copied platform-specific CLI binaries');
} else {
// Unix: create symlinks
fs.symlinkSync(path.basename(clineSource), clineTarget);
fs.symlinkSync(path.basename(clineHostSource), clineHostTarget);
console.log('✓ Created symlinks to platform-specific CLI binaries');
// Make binaries executable
try {
fs.chmodSync(clineSource, 0o755);
fs.chmodSync(clineHostSource, 0o755);
fs.chmodSync(clineTarget, 0o755);
fs.chmodSync(clineHostTarget, 0o755);
} catch (error) {
console.warn(\`Warning: Could not set executable permissions: \${error.message}\`);
}
}
// Setup ripgrep binary
console.log(\`Setting up ripgrep for \${rgPlatformSuffix}...\`);
const ripgrepDir = path.join(__dirname, 'ripgrep');
const rgSource = path.join(ripgrepDir, \`rg-\${rgPlatformSuffix}\`);
const rgTarget = path.join(__dirname, 'rg');
if (!fs.existsSync(rgSource)) {
console.error(\`Error: ripgrep binary not found for platform \${rgPlatformSuffix}\`);
console.error(\`Expected: \${rgSource}\`);
console.error(\`Supported platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64\`);
process.exit(1);
}
// Remove existing rg if it exists
if (fs.existsSync(rgTarget)) {
try {
fs.unlinkSync(rgTarget);
} catch (e) {
console.warn(\`Warning: Could not remove existing ripgrep: \${e.message}\`);
}
}
// Copy ripgrep binary to root (where cline-core expects it)
fs.copyFileSync(rgSource, rgTarget);
// Make ripgrep executable (Unix only)
if (platform !== 'win32') {
try {
fs.chmodSync(rgTarget, 0o755);
} catch (error) {
console.warn(\`Warning: Could not set ripgrep executable permissions: \${error.message}\`);
}
}
console.log('✓ Copied platform-specific ripgrep binary');
console.log('✓ Cline CLI installation complete');
console.log('');
console.log('Usage:');
console.log(' cline - Start Cline CLI');
console.log(' cline-host - Start Cline host service');
console.log('');
console.log('Documentation: https://docs.cline.bot');
}
try {
setupBinaries();
} catch (error) {
console.error(\`Installation failed: \${error.message}\`);
console.error('Please report this issue at: https://github.com/cline/cline/issues');
process.exit(1);
}
`
const postinstallPath = path.join(BUILD_DIR, "postinstall.js")
fs.writeFileSync(postinstallPath, postinstallScript)
fs.chmodSync(postinstallPath, 0o755)
console.log(`✓ postinstall.js created`)
}
/* cp -r */
async function cpr(source, dest) {
log_verbose(`Copying ${source} -> ${dest}`)
await cp(source, dest, {
recursive: true,
preserveTimestamps: true,
dereference: false, // preserve symlinks instead of following them
})
}
/* rm -rf */
async function rmrf(dir) {
if (fs.existsSync(dir)) {
log_verbose(`Removing ${dir}`)
fs.rmSync(dir, { recursive: true, force: true })
}
}
function log_verbose(...args) {
if (IS_VERBOSE) {
console.log(...args)
}
}
await main()
+9 -459
View File
@@ -13,8 +13,6 @@ import { rmrf } from "./file-utils.mjs"
const BUILD_DIR = "dist-standalone"
const BINARIES_DIR = `${BUILD_DIR}/binaries`
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
const RIPGREP_BINARIES_DIR = `${BUILD_DIR}/ripgrep-binaries`
const CLI_BINARIES_DIR = "cli/bin"
const IS_DEBUG_BUILD = process.env.IS_DEBUG_BUILD === "true"
// This should match the node version packaged with the JetBrains plugin.
@@ -30,63 +28,15 @@ const SUPPORTED_BINARY_MODULES = ["better-sqlite3"]
const UNIVERSAL_BUILD = !process.argv.includes("-s")
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
// Parse --target flag (e.g., --target=npm)
// Default behavior is JetBrains build (no binaries)
// Use --target=npm for npm package build (CLI binaries but no Node.js)
const targetArg = process.argv.find((arg) => arg.startsWith("--target="))
const BUILD_TARGET = targetArg ? targetArg.split("=")[1] : "jetbrains"
const IS_NPM_BUILD = BUILD_TARGET === "npm"
// Detect current platform
function getCurrentPlatform() {
const platform = os.platform()
const arch = os.arch()
if (platform === "darwin") {
return arch === "arm64" ? "darwin-arm64" : "darwin-x64"
} else if (platform === "linux") {
return "linux-x64"
} else if (platform === "win32") {
return "win-x64"
}
throw new Error(`Unsupported platform: ${platform}-${arch}`)
}
async function main() {
const buildType = IS_NPM_BUILD ? "NPM Package" : "JetBrains"
console.log(`🚀 Building Cline ${buildType} Package\n`)
await installNodeDependencies()
if (IS_NPM_BUILD) {
await copyCliBinaries()
await copyRipgrepBinary()
await copyProtoDescriptors()
await createNpmPackageFiles()
await createFakeNodeModules()
await createNpmIgnoreFile()
await createPostinstallScript()
}
if (UNIVERSAL_BUILD && !IS_NPM_BUILD) {
console.log("\nBuilding universal package for all platforms...")
if (UNIVERSAL_BUILD) {
console.log("Building universal package for all platforms...")
await packageAllBinaryDeps()
} else if (IS_NPM_BUILD) {
console.log("\nNPM build: Keeping native modules in node_modules for npm to handle...")
} else {
console.log(`\nBuilding package for ${os.platform()}-${os.arch()}...`)
}
if (!IS_NPM_BUILD) {
console.log("\n📦 Creating final package...")
await zipDistribution()
}
console.log("\n✅ Build complete!")
if (IS_NPM_BUILD) {
console.log(`\n📦 NPM package ready in ${BUILD_DIR}/`)
console.log(`To publish: cd ${BUILD_DIR} && npm publish`)
console.log(`Building package for ${os.platform()}-${os.arch()}...`)
}
await zipDistribution()
}
async function installNodeDependencies() {
@@ -104,389 +54,6 @@ async function installNodeDependencies() {
fs.renameSync(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`)
}
/**
* Copy CLI binaries (cline and cline-host) for all platforms
* The Go binaries are cross-compiled for darwin/linux arm64/amd64
*/
async function copyCliBinaries() {
console.log("Copying CLI binaries for all platforms...")
const platforms = [
{ os: "darwin", arch: "arm64" },
{ os: "darwin", arch: "amd64" },
{ os: "linux", arch: "amd64" },
{ os: "linux", arch: "arm64" },
]
const binDir = path.join(BUILD_DIR, "bin")
// Create bin directory
fs.mkdirSync(binDir, { recursive: true })
// Copy all platform-specific binaries
for (const { os, arch } of platforms) {
const platformSuffix = `${os}-${arch}`
// Copy cline binary
const clineSource = path.join(CLI_BINARIES_DIR, `cline-${platformSuffix}`)
const clineDest = path.join(binDir, `cline-${platformSuffix}`)
if (!fs.existsSync(clineSource)) {
console.error(`Error: CLI binary not found at ${clineSource}`)
console.error(`Please run: npm run compile-cli`)
process.exit(1)
}
await cpr(clineSource, clineDest)
fs.chmodSync(clineDest, 0o755)
console.log(`✓ cline-${platformSuffix} copied`)
// Copy cline-host binary
const hostSource = path.join(CLI_BINARIES_DIR, `cline-host-${platformSuffix}`)
const hostDest = path.join(binDir, `cline-host-${platformSuffix}`)
if (!fs.existsSync(hostSource)) {
console.error(`Error: CLI binary not found at ${hostSource}`)
console.error(`Please run: npm run compile-cli`)
process.exit(1)
}
await cpr(hostSource, hostDest)
fs.chmodSync(hostDest, 0o755)
console.log(`✓ cline-host-${platformSuffix} copied`)
}
console.log(`✓ All platform binaries copied to ${binDir}`)
}
/**
* Copy proto descriptors directory
* The proto/descriptor_set.pb file is needed by cline-core for gRPC reflection
*/
async function copyProtoDescriptors() {
console.log("Copying proto descriptors...")
const protoSource = "proto"
const protoDest = path.join(BUILD_DIR, "proto")
// Check if proto directory exists
if (!fs.existsSync(protoSource)) {
console.error(`Error: proto directory not found at ${protoSource}`)
console.error(`Please ensure the proto files have been generated`)
process.exit(1)
}
// Check if descriptor_set.pb exists
const descriptorPath = path.join(protoSource, "descriptor_set.pb")
if (!fs.existsSync(descriptorPath)) {
console.error(`Error: proto/descriptor_set.pb not found at ${descriptorPath}`)
console.error(`Please run: npm run protos`)
process.exit(1)
}
// Copy the entire proto directory
await cpr(protoSource, protoDest)
console.log(`✓ Proto descriptors copied to ${protoDest}`)
}
/**
* Copy ripgrep binary for the current platform
* Ripgrep is needed by cline-core for file searching
*/
async function copyRipgrepBinary() {
const currentPlatform = getCurrentPlatform()
const binaryName = currentPlatform.startsWith("win") ? "rg.exe" : "rg"
const ripgrepBinarySource = path.join(RIPGREP_BINARIES_DIR, currentPlatform, binaryName)
const ripgrepBinaryDest = path.join(BUILD_DIR, binaryName)
console.log(`Copying ripgrep binary for ${currentPlatform}...`)
// Check if ripgrep binaries exist, download if missing
if (!fs.existsSync(ripgrepBinarySource)) {
console.log(`Ripgrep binary not found, downloading...`)
try {
execSync("npm run download-ripgrep", { stdio: "inherit" })
} catch (error) {
console.error(`Error downloading ripgrep: ${error.message}`)
console.error(`Please run: npm run download-ripgrep`)
process.exit(1)
}
// Check again after download
if (!fs.existsSync(ripgrepBinarySource)) {
console.error(`Error: Ripgrep binary still not found at ${ripgrepBinarySource}`)
console.error(`Download may have failed. Please run: npm run download-ripgrep`)
process.exit(1)
}
}
// Copy ripgrep binary to the root of dist-standalone (where cline-core.js is)
await cpr(ripgrepBinarySource, ripgrepBinaryDest)
// Make it executable (Unix only)
if (!currentPlatform.startsWith("win")) {
fs.chmodSync(ripgrepBinaryDest, 0o755)
}
console.log(`✓ Ripgrep binary copied to ${ripgrepBinaryDest}`)
}
/**
* Create a VERSION file with build metadata
*/
async function createVersionFile() {
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"))
const version = packageJson.version
const platform = getCurrentPlatform()
const buildDate = new Date().toISOString()
const versionInfo = {
version,
platform,
buildDate,
nodeVersion: TARGET_NODE_VERSION,
}
const versionPath = path.join(BUILD_DIR, "VERSION.txt")
fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2))
console.log(`✓ VERSION file created: ${version} (${platform})`)
}
/**
* Copy NPM package files (package.json, README.md, and man page) from cli/ directory
*/
async function createNpmPackageFiles() {
console.log("Copying NPM package files...")
// Copy package.json from cli/ directory
const packageJsonSource = path.join("cli", "package.json")
const packageJsonDest = path.join(BUILD_DIR, "package.json")
if (!fs.existsSync(packageJsonSource)) {
console.error(`Error: NPM package.json not found at ${packageJsonSource}`)
process.exit(1)
}
await cpr(packageJsonSource, packageJsonDest)
console.log(`✓ package.json copied from ${packageJsonSource}`)
// Copy README.md from cli/ directory
const readmeSource = path.join("cli", "README.md")
const readmeDest = path.join(BUILD_DIR, "README.md")
if (!fs.existsSync(readmeSource)) {
console.error(`Error: NPM README.md not found at ${readmeSource}`)
process.exit(1)
}
await cpr(readmeSource, readmeDest)
console.log(`✓ README.md copied from ${readmeSource}`)
// Copy man page from cli/man/ directory
const manPageSource = path.join("cli", "man", "cline.1")
const manDir = path.join(BUILD_DIR, "man")
const manPageDest = path.join(manDir, "cline.1")
if (!fs.existsSync(manPageSource)) {
console.error(`Error: Man page not found at ${manPageSource}`)
process.exit(1)
}
// Create man directory if it doesn't exist
fs.mkdirSync(manDir, { recursive: true })
await cpr(manPageSource, manPageDest)
console.log(`✓ Man page copied from ${manPageSource}`)
}
/**
* Create fake_node_modules directory with vscode stub
* This directory will be added to NODE_PATH so Node.js can find the vscode module
* without npm interfering with the real node_modules directory
*/
async function createFakeNodeModules() {
console.log("Creating fake_node_modules with vscode stub...")
const vscodeSource = path.join(BUILD_DIR, "node_modules", "vscode")
const fakeNodeModulesDir = path.join(BUILD_DIR, "fake_node_modules")
const vscodeDest = path.join(fakeNodeModulesDir, "vscode")
if (!fs.existsSync(vscodeSource)) {
console.error(`Error: vscode stub module not found at ${vscodeSource}`)
process.exit(1)
}
// Create fake_node_modules directory
fs.mkdirSync(fakeNodeModulesDir, { recursive: true })
// Copy vscode stub into fake_node_modules
await cpr(vscodeSource, vscodeDest)
console.log(`✓ fake_node_modules/vscode created at ${vscodeDest}`)
}
/**
* Create .npmignore file to ensure necessary files are included
*/
async function createNpmIgnoreFile() {
console.log("Creating .npmignore file...")
// Create .npmignore that excludes build artifacts
// Note: proto/ directory is NOT excluded because proto/descriptor_set.pb is needed at runtime
const npmignoreContent = `# Exclude build artifacts and unnecessary files
binaries/
ripgrep-binaries/
standalone.zip
cline-core.js.map
package-lock.json
tree-sitter*.wasm
node_modules/vscode
`
const npmignorePath = path.join(BUILD_DIR, ".npmignore")
fs.writeFileSync(npmignorePath, npmignoreContent)
console.log(`✓ .npmignore created`)
}
/**
* Create postinstall script for NPM package
* This script selects the correct platform-specific binary and creates symlinks
*/
async function createPostinstallScript() {
console.log("Creating postinstall script...")
const postinstallScript = `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const os = require('os');
// Detect current platform and architecture
function getPlatformInfo() {
const platform = os.platform();
const arch = os.arch();
// Map Node.js arch names to Go arch names
let goArch = arch;
if (arch === 'x64') {
goArch = 'amd64';
}
let goPlatform = platform;
return { platform: goPlatform, arch: goArch };
}
// Setup platform-specific binaries
function setupBinaries() {
const { platform, arch } = getPlatformInfo();
const platformSuffix = \`\${platform}-\${arch}\`;
console.log(\`Setting up Cline CLI for \${platformSuffix}...\`);
const binDir = path.join(__dirname, 'bin');
// Check if platform-specific binaries exist
const clineSource = path.join(binDir, \`cline-\${platformSuffix}\`);
const clineHostSource = path.join(binDir, \`cline-host-\${platformSuffix}\`);
if (!fs.existsSync(clineSource)) {
console.error(\`Error: Binary not found for platform \${platformSuffix}\`);
console.error(\`Expected: \${clineSource}\`);
console.error(\`Supported platforms: darwin-arm64, darwin-amd64, linux-amd64, linux-arm64\`);
process.exit(1);
}
if (!fs.existsSync(clineHostSource)) {
console.error(\`Error: Binary not found for platform \${platformSuffix}\`);
console.error(\`Expected: \${clineHostSource}\`);
process.exit(1);
}
// Create symlinks or copies to the generic names
const clineTarget = path.join(binDir, 'cline');
const clineHostTarget = path.join(binDir, 'cline-host');
// Remove existing files if they exist
[clineTarget, clineHostTarget].forEach(target => {
if (fs.existsSync(target)) {
try {
fs.unlinkSync(target);
} catch (e) {
console.warn(\`Warning: Could not remove existing file \${target}: \${e.message}\`);
}
}
});
// On Unix, create symlinks; on Windows, copy files
if (platform === 'win32') {
// Windows: copy files
fs.copyFileSync(clineSource, clineTarget);
fs.copyFileSync(clineHostSource, clineHostTarget);
console.log('✓ Copied platform-specific binaries');
} else {
// Unix: create symlinks
fs.symlinkSync(path.basename(clineSource), clineTarget);
fs.symlinkSync(path.basename(clineHostSource), clineHostTarget);
console.log('✓ Created symlinks to platform-specific binaries');
// Make binaries executable
try {
fs.chmodSync(clineSource, 0o755);
fs.chmodSync(clineHostSource, 0o755);
fs.chmodSync(clineTarget, 0o755);
fs.chmodSync(clineHostTarget, 0o755);
} catch (error) {
console.warn(\`Warning: Could not set executable permissions: \${error.message}\`);
}
}
// Check ripgrep binary
const rgBinary = platform === 'win32' ? 'rg.exe' : 'rg';
const rgPath = path.join(__dirname, rgBinary);
if (!fs.existsSync(rgPath)) {
console.error(\`Error: ripgrep binary not found at \${rgPath}\`);
process.exit(1);
}
// Make ripgrep executable (Unix only)
if (platform !== 'win32') {
try {
fs.chmodSync(rgPath, 0o755);
} catch (error) {
console.warn(\`Warning: Could not set ripgrep executable permissions: \${error.message}\`);
}
}
console.log('✓ Cline CLI installation complete');
console.log('');
console.log('Usage:');
console.log(' cline - Start Cline CLI');
console.log(' cline-host - Start Cline host service');
console.log('');
console.log('Documentation: https://docs.cline.bot');
}
try {
setupBinaries();
} catch (error) {
console.error(\`Installation failed: \${error.message}\`);
console.error('Please report this issue at: https://github.com/cline/cline/issues');
process.exit(1);
}
`
const postinstallPath = path.join(BUILD_DIR, "postinstall.js")
fs.writeFileSync(postinstallPath, postinstallScript)
fs.chmodSync(postinstallPath, 0o755)
console.log(`✓ postinstall.js created`)
}
/**
* Downloads prebuilt binaries for each platform for the modules that include binaries. It uses `npx prebuild-install`
* to download the binary.
@@ -539,9 +106,8 @@ async function packageAllBinaryDeps() {
}
async function zipDistribution() {
// Default JetBrains build
const zipFilename = "standalone.zip"
const zipPath = path.join(BUILD_DIR, zipFilename)
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const startTime = Date.now()
const archive = archiver("zip", { zlib: { level: 6 } })
@@ -559,31 +125,15 @@ async function zipDistribution() {
})
archive.pipe(output)
// Build ignore lists for build directory and extension directory
const ignorePatterns = ["standalone.zip", "standalone-cli.zip"]
const extensionIgnores = ["dist/**"]
// For JetBrains builds, exclude binaries from both directories
// JetBrains provides their own Node.js, so exclude all binaries
ignorePatterns.push(
"bin/**", // Exclude entire bin directory
"node-binaries/**", // Exclude all platform-specific Node.js binaries
)
extensionIgnores.push(
"cli/bin/**", // Exclude CLI binaries from extension
"node-binaries/**", // Exclude node-binaries from extension
)
console.log("JetBrains build: Excluding Node.js and CLI binaries (JetBrains provides its own Node.js)")
// Add all the files from the standalone build dir.
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ignorePatterns,
ignore: ["standalone.zip"],
})
// Exclude the same files as the VCE vscode extension packager.
const isIgnored = createIsIgnored(extensionIgnores)
// Also ignore the dist directory, the build directory for the extension.
const isIgnored = createIsIgnored(["dist/**"])
// Add the whole cline directory under "extension", except the for the ignored files.
archive.directory(process.cwd(), "extension", (entry) => {
+2 -13
View File
@@ -76,19 +76,8 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
await showVersionUpdateAnnouncement(context)
// Initialize banner service
BannerService.initialize(webview.controller)
BannerService.get()
.fetchActiveBanners()
.then((banners) => {
if (banners.length > 0) {
Logger.log(`BannerService: ${banners.length} active banner(s) fetched.`)
// Banners are now cached and can be accessed by the frontend when needed
}
})
.catch((error) => {
Logger.error("BannerService: Failed to fetch banners on startup", error)
})
// Initialize banner service and fetch banners from the API.
BannerService.initialize(webview.controller).getActiveBanners(true)
telemetryService.captureExtensionActivated()
+7 -2
View File
@@ -376,10 +376,14 @@ function createHandlerForProvider(
return new VercelAIGatewayHandler({
onRetryAttempt: options.onRetryAttempt,
vercelAiGatewayApiKey: options.vercelAiGatewayApiKey,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
openRouterModelId:
mode === "plan" ? options.planModeVercelAiGatewayModelId : options.actModeVercelAiGatewayModelId,
openRouterModelInfo:
mode === "plan" ? options.planModeVercelAiGatewayModelInfo : options.actModeVercelAiGatewayModelInfo,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
})
case "zai":
return new ZAiHandler({
@@ -394,6 +398,7 @@ function createHandlerForProvider(
ocaBaseUrl: options.ocaBaseUrl,
ocaModelId: mode === "plan" ? options.planModeOcaModelId : options.actModeOcaModelId,
ocaModelInfo: mode === "plan" ? options.planModeOcaModelInfo : options.actModeOcaModelInfo,
ocaReasoningEffort: mode === "plan" ? options.planModeOcaReasoningEffort : options.actModeOcaReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
ocaUsePromptCache:
+1 -1
View File
@@ -259,7 +259,7 @@ export class AIhubmixHandler implements ApiHandler {
const stream = await client.chat.completions.create(fixedRequestBody)
for await (const chunk of stream as any) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -199,7 +199,7 @@ export class ClineHandler implements ApiHandler {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
if (["x-ai/grok-code-fast-1", "minimax/minimax-m2.1"].includes(this.getModel().id)) {
if (["x-ai/grok-code-fast-1", "kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
totalCost = 0
}
+1 -1
View File
@@ -104,7 +104,7 @@ export class DeepSeekHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -67,7 +67,7 @@ export class DoubaoHandler implements ApiHandler {
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -60,7 +60,7 @@ export class FireworksHandler implements ApiHandler {
let reasoning: string | null = null
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (reasoning || delta?.content?.includes("<think>")) {
reasoning = (reasoning || "") + (delta.content ?? "")
}
+1 -1
View File
@@ -229,7 +229,7 @@ export class GroqHandler implements ApiHandler {
const stream = await client.chat.completions.create(requestParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
// Handle reasoning field if present (for reasoning models with parsed output)
if ((delta as any)?.reasoning) {
+1 -1
View File
@@ -66,7 +66,7 @@ export class HicapHandler implements ApiHandler {
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -86,7 +86,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
// Handle reasoning content detection
if (delta?.content) {
+1 -1
View File
@@ -97,7 +97,7 @@ export class HuggingFaceHandler implements ApiHandler {
for await (const chunk of stream) {
_chunkCount++
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
_totalContent += delta.content
+1 -1
View File
@@ -307,7 +307,7 @@ export class LiteLlmHandler implements ApiHandler {
} as LiteLlmChatCompletionCreateParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
// Handle normal text content
if (delta?.content) {
+1 -1
View File
@@ -60,7 +60,7 @@ export class LmStudioHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const choice = chunk.choices[0]
const choice = chunk.choices?.[0]
const delta = choice?.delta
if (delta?.content) {
yield {
+1 -1
View File
@@ -62,7 +62,7 @@ export class MoonshotHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -57,7 +57,7 @@ export class NebiusHandler implements ApiHandler {
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -55,7 +55,7 @@ export class NousResearchHandler implements ApiHandler {
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+79 -10
View File
@@ -1,4 +1,4 @@
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults, ModelInfo } from "@shared/api"
import OpenAI, { APIError, OpenAIError } from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
@@ -9,18 +9,23 @@ import {
} from "@/services/auth/oca/utils/constants"
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
import { Logger } from "@/services/logging/Logger"
import { OcaModelInfo } from "@/shared/api"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { ApiFormat } from "@/shared/proto/index.cline"
import { ApiHandler, type CommonApiHandlerOptions } from ".."
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
import { handleResponsesApiStreamResponse } from "../utils/responses_api_support"
export interface OcaHandlerOptions extends CommonApiHandlerOptions {
ocaBaseUrl?: string
ocaModelId?: string
ocaModelInfo?: LiteLLMModelInfo
ocaModelInfo?: OcaModelInfo
ocaReasoningEffort?: string
thinkingBudgetTokens?: number
ocaUsePromptCache?: boolean
taskId?: string
@@ -100,7 +105,7 @@ export class OcaHandler implements ApiHandler {
return this.client
}
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
async getApiCosts(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.ocaModelId || liteLlmDefaultModelId
@@ -138,8 +143,29 @@ export class OcaHandler implements ApiHandler {
}
}
async calculateCost(
modelInfo: ModelInfo,
inputTokens: number,
outputTokens: number,
_cacheWriteTokens?: number,
_cacheReadTokens?: number,
) {
const inputCost = (await this.getApiCosts(1e6, 0)) || 0
const outputCost = (await this.getApiCosts(0, 1e6)) || 0
const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6
return totalCost
}
@withRetry()
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
if (this.options.ocaModelInfo?.apiFormat == ApiFormat.OPENAI_RESPONSES) {
yield* this.createMessageResponsesApi(systemPrompt, messages, tools)
} else {
yield* this.createMessageChatApi(systemPrompt, messages, tools)
}
}
async *createMessageChatApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
@@ -193,7 +219,7 @@ export class OcaHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
const stream = await client.chat.completions.create({
const chatCompletionsParams: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: this.options.ocaModelId || liteLlmDefaultModelId,
messages: [enhancedSystemMessage, ...enhancedMessages],
temperature,
@@ -206,13 +232,16 @@ export class OcaHandler implements ApiHandler {
litellm_session_id: `cline-${this.options.taskId}`,
...getOpenAIToolParams(tools),
}), // Add session ID for LiteLLM tracking
})
}
const inputCost = (await this.calculateCost(1e6, 0)) || 0
const outputCost = (await this.calculateCost(0, 1e6)) || 0
if (this.options.ocaModelInfo?.supportsReasoningEffort) {
chatCompletionsParams["reasoning_effort"] = this.options.ocaReasoningEffort || ("medium" as any)
}
const stream = await client.chat.completions.create(chatCompletionsParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
// Handle normal text content
if (delta?.content) {
@@ -241,8 +270,11 @@ export class OcaHandler implements ApiHandler {
// Handle token usage information
if (chunk.usage) {
const totalCost =
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
const totalCost = await this.calculateCost(
this.options.ocaModelInfo!,
chunk.usage.prompt_tokens,
chunk.usage.completion_tokens,
)
// Extract cache-related information if available
// Need to use type assertion since these properties are not in the standard OpenAI types
@@ -270,6 +302,43 @@ export class OcaHandler implements ApiHandler {
}
}
async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
const client = this.ensureClient()
// Convert messages to Responses API input format
const input: OpenAI.Responses.ResponseInputItem[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAIResponsesInput(messages),
]
// Convert ChatCompletion tools to Responses API format if provided
const responseTools = tools
?.filter((tool) => tool.type === "function")
.map((tool: any) => ({
type: "function" as const,
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters,
strict: tool.function.strict ?? true, // Responses API defaults to strict mode
}))
const responsesParams: OpenAI.Responses.ResponseCreateParamsStreaming = {
model: this.options.ocaModelId || liteLlmDefaultModelId,
input,
stream: true,
tools: responseTools,
}
if (this.options.ocaModelInfo && this.options.ocaModelInfo.supportsReasoning) {
responsesParams["reasoning"] = { effort: this.options.ocaReasoningEffort as any, summary: "auto" }
}
// Create the response using Responses API
const stream = await client.responses.create(responsesParams)
yield* handleResponsesApiStreamResponse(stream, this.options.ocaModelInfo!, this.calculateCost.bind(this))
}
getModel() {
return {
id: this.options.ocaModelId || liteLlmDefaultModelId,
+1 -1
View File
@@ -123,7 +123,7 @@ export class OpenAiNativeHandler implements ApiHandler {
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -132,7 +132,7 @@ export class OpenAiHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -113,7 +113,7 @@ export class OpenRouterHandler implements ApiHandler {
this.lastGenerationId = chunk.id
}
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -122,7 +122,7 @@ export class QwenHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -99,7 +99,7 @@ export class RequestyHandler implements ApiHandler {
let lastUsage: any
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -68,7 +68,7 @@ export class SambanovaHandler implements ApiHandler {
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -66,7 +66,7 @@ export class TogetherHandler implements ApiHandler {
})
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+11 -3
View File
@@ -1,4 +1,5 @@
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
@@ -13,7 +14,9 @@ interface VercelAIGatewayHandlerOptions extends CommonApiHandlerOptions {
vercelAiGatewayApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
reasoningEffort?: string
thinkingBudgetTokens?: number
geminiThinkingLevel?: string
}
export class VercelAIGatewayHandler implements ApiHandler {
@@ -58,15 +61,18 @@ export class VercelAIGatewayHandler implements ApiHandler {
systemPrompt,
messages,
{ id: modelId, info: modelInfo },
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
tools,
this.options.geminiThinkingLevel,
)
let didOutputUsage: boolean = false
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
@@ -79,7 +85,8 @@ export class VercelAIGatewayHandler implements ApiHandler {
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
// Skip reasoning content for models that don't support it (e.g., devstral, grok-4)
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
yield {
type: "reasoning",
reasoning: typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning),
@@ -91,7 +98,8 @@ export class VercelAIGatewayHandler implements ApiHandler {
"reasoning_details" in delta &&
delta.reasoning_details &&
// @ts-ignore-next-line
delta.reasoning_details.length // exists and non-0
delta.reasoning_details.length && // exists and non-0
!shouldSkipReasoningForModel(this.options.openRouterModelId)
) {
yield {
type: "reasoning",
+1 -1
View File
@@ -69,7 +69,7 @@ export class XAIHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+1 -1
View File
@@ -95,7 +95,7 @@ export class ZAiHandler implements ApiHandler {
const toolCallProcessor = new ToolCallProcessor()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
yield {
type: "text",
+63 -11
View File
@@ -145,18 +145,13 @@ export function convertToOpenAiMessages(
const thinkingBlock = []
if (nonToolMessages.length > 0) {
nonToolMessages.forEach((part) => {
// @ts-ignore-next-line
if (part.type === "text" && part.reasoning_details) {
// @ts-ignore-next-line
if (Array.isArray(part.reasoning_details)) {
// @ts-ignore-next-line
reasoningDetails.push(...part.reasoning_details)
const anyPart = part as any
if (part.type === "text" && anyPart.reasoning_details) {
if (Array.isArray(anyPart.reasoning_details)) {
reasoningDetails.push(...anyPart.reasoning_details)
} else {
// @ts-ignore-next-line
reasoningDetails.push(part.reasoning_details)
reasoningDetails.push(anyPart.reasoning_details)
}
// @ts-ignore-next-line
// delete part.reasoning_details
}
if (part.type === "thinking" && part.thinking) {
// Reasoning details should have been moved to the text block
@@ -216,7 +211,7 @@ export function convertToOpenAiMessages(
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
tool_calls: tool_calls?.length > 0 ? tool_calls : undefined,
// Only include reasoning_details when non-empty; sending [] can trigger provider validation issues.
// @ts-ignore-next-line
// @ts-expect-error
reasoning_details: consolidatedReasoningDetails.length > 0 ? consolidatedReasoningDetails : undefined,
})
}
@@ -404,3 +399,60 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
return anthropicMessage
}
/**
* Sanitizes OpenAI messages for Gemini models by removing tool_calls that lack reasoning_details.
*
* Gemini models require thought signatures for tool calls. When switching providers mid-conversation,
* historical tool calls may not include Gemini reasoning details, which can poison the next request.
* This function drops tool_calls that lack reasoning_details and their paired tool messages.
*
* @param messages - Array of OpenAI chat completion messages
* @param modelId - The model ID to check if sanitization is needed
* @returns Sanitized array of messages (unchanged if not a Gemini model)
*/
export function sanitizeGeminiMessages(
messages: OpenAI.Chat.ChatCompletionMessageParam[],
modelId: string,
): OpenAI.Chat.ChatCompletionMessageParam[] {
if (!modelId.includes("gemini")) {
return messages
}
const droppedToolCallIds = new Set<string>()
const sanitized: OpenAI.Chat.ChatCompletionMessageParam[] = []
for (const msg of messages) {
if (msg.role === "assistant") {
const anyMsg = msg as any
const toolCalls = anyMsg.tool_calls
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
const reasoningDetails = anyMsg.reasoning_details
const hasReasoningDetails = Array.isArray(reasoningDetails) && reasoningDetails.length > 0
if (!hasReasoningDetails) {
for (const tc of toolCalls) {
if (tc?.id) {
droppedToolCallIds.add(tc.id)
}
}
// Keep any textual content, but drop the tool_calls themselves.
if (anyMsg.content) {
sanitized.push({ role: "assistant", content: anyMsg.content } as any)
}
continue
}
}
}
if (msg.role === "tool") {
const anyMsg = msg as any
if (anyMsg.tool_call_id && droppedToolCallIds.has(anyMsg.tool_call_id)) {
continue
}
}
sanitized.push(msg)
}
return sanitized
}
+8 -45
View File
@@ -9,7 +9,7 @@ import {
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import OpenAI from "openai"
import { ChatCompletionTool } from "openai/resources/chat/completions"
import { convertToOpenAiMessages } from "./openai-format"
import { convertToOpenAiMessages, sanitizeGeminiMessages } from "./openai-format"
import { convertToR1Format } from "./r1-format"
import { getOpenAIToolParams } from "./tool-call-processor"
@@ -36,45 +36,8 @@ export async function createOpenRouterStream(
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
}
// Gemini models require thought signatures for tool calls. When switching providers mid-conversation,
// historical tool calls may not include Gemini/OpenRouter reasoning details, which can poison the next request.
// Bandaid: for Gemini only, drop tool_calls that lack reasoning_details and their paired tool messages.
if (model.id.includes("gemini")) {
const droppedToolCallIds = new Set<string>()
const sanitized: OpenAI.Chat.ChatCompletionMessageParam[] = []
for (const msg of openAiMessages) {
if (msg.role === "assistant") {
const anyMsg = msg as any
const toolCalls = anyMsg.tool_calls
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
const reasoningDetails = anyMsg.reasoning_details
const hasReasoningDetails = Array.isArray(reasoningDetails) && reasoningDetails.length > 0
if (!hasReasoningDetails) {
for (const tc of toolCalls) {
if (tc?.id) droppedToolCallIds.add(tc.id)
}
// Keep any textual content, but drop the tool_calls themselves.
if (anyMsg.content) {
sanitized.push({ role: "assistant", content: anyMsg.content } as any)
}
continue
}
}
}
if (msg.role === "tool") {
const anyMsg = msg as any
if (anyMsg.tool_call_id && droppedToolCallIds.has(anyMsg.tool_call_id)) {
continue
}
}
sanitized.push(msg)
}
openAiMessages = sanitized
}
// Sanitize messages for Gemini models (removes tool_calls without reasoning_details)
openAiMessages = sanitizeGeminiMessages(openAiMessages, model.id)
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
@@ -114,7 +77,7 @@ export async function createOpenRouterStream(
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
// @ts-expect-error-next-line
cache_control: { type: "ephemeral" },
},
],
@@ -134,7 +97,7 @@ export async function createOpenRouterStream(
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
// @ts-expect-error-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
})
@@ -217,7 +180,7 @@ export async function createOpenRouterStream(
thinkingBudgetTokens &&
model.info?.thinkingConfig &&
thinkingBudgetTokens > 0 &&
!(model.id.includes("gemini") && geminiThinkingLevel)
!(model.id.includes("gemini-3") && geminiThinkingLevel)
) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: thinkingBudgetTokens }
@@ -233,7 +196,7 @@ export async function createOpenRouterStream(
// Skip reasoning for models that don't support it (e.g., devstral, grok-4)
const includeReasoning = !shouldSkipReasoningForModel(model.id)
// @ts-ignore-next-line
// @ts-expect-error-next-line
const stream = await client.chat.completions.create({
model: model.id,
max_tokens: maxTokens,
@@ -249,7 +212,7 @@ export async function createOpenRouterStream(
...(providerPreferences ? { provider: providerPreferences } : {}),
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
...getOpenAIToolParams(tools),
...(model.id.includes("gemini") && geminiThinkingLevel
...(model.id.includes("gemini-3") && geminiThinkingLevel
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
: {}),
})
@@ -5,9 +5,11 @@ import {
openRouterClaudeSonnet41mModelId,
openRouterClaudeSonnet451mModelId,
} from "@shared/api"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToOpenAiMessages, sanitizeGeminiMessages } from "../transform/openai-format"
import { convertToR1Format } from "./r1-format"
import { getOpenAIToolParams } from "./tool-call-processor"
export async function createVercelAIGatewayStream(
@@ -15,77 +17,131 @@ export async function createVercelAIGatewayStream(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
model: { id: string; info: ModelInfo },
reasoningEffort?: string,
thinkingBudgetTokens?: number,
tools?: OpenAITool[],
geminiThinkingLevel?: string,
) {
// Convert Anthropic messages to OpenAI format
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
if (isClaudeSonnet1m) {
// remove the custom :1m suffix, to create the model id openrouter API expects
// remove the custom :1m suffix, to create the model id the API expects
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
}
// Sanitize messages for Gemini models (removes tool_calls without reasoning_details)
openAiMessages = sanitizeGeminiMessages(openAiMessages, model.id)
// Prompt caching for supported models
// This handles cache_control for Claude and MiniMax models
const isAnthropicModel = model.id.startsWith("anthropic/")
const isMinimaxModel = model.id.startsWith("minimax/")
if (isAnthropicModel || isMinimaxModel) {
openAiMessages[0] = {
role: "system",
content: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
content: [
{
type: "text",
text: systemPrompt,
// @ts-expect-error-next-line
cache_control: { type: "ephemeral" },
},
],
}
// Add cache_control to the last two user messages for conversation context caching
// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((msg) => {
if (typeof msg.content === "string" && msg.content.length > 0) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// Find the last text part in the message content
const lastTextPart = msg.content.filter((part) => part.type === "text").pop()
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (lastTextPart && lastTextPart.text && lastTextPart.text.length > 0) {
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-expect-error-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
})
}
// Configure reasoning parameters similar to OpenRouter
let temperature: number | undefined = 0
// Use max tokens from model info (fetched from Vercel API)
const maxTokens = model.info?.maxTokens || undefined
// Use temperature from model info, default to 0
// Model-specific temperatures are derived in refreshVercelAiGatewayModels.ts
let temperature: number | undefined = model.info?.temperature ?? 0
let topP: number | undefined
// R1 format conversion for DeepSeek and similar reasoning models
const requiresR1Format =
model.id.startsWith("deepseek/deepseek-r1") ||
model.id === "perplexity/sonar-reasoning" ||
model.id === "qwen/qwq-32b:free" ||
model.id === "qwen/qwq-32b"
if (requiresR1Format) {
topP = 0.95
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
if (model.id.startsWith("google/gemini-3.0") || model.id === "google/gemini-3.0") {
// Recommended value from google
temperature = 1.0
}
// Reasoning/thinking budget configuration
let reasoning: { max_tokens: number } | undefined
if (isAnthropicModel) {
const budget_tokens = thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0
if (reasoningOn) {
// Check if it's an Anthropic Claude model that supports thinking
const isClaudeThinkingModel = model.id.startsWith("anthropic/claude") && model.info?.thinkingConfig
if (isClaudeThinkingModel) {
// For Claude models, match OpenRouter behavior: check even if thinkingBudgetTokens is 0
const budgetTokens = thinkingBudgetTokens || 0
if (budgetTokens !== 0) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: budget_tokens }
reasoning = { max_tokens: budgetTokens }
}
} else if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
} else if (
thinkingBudgetTokens &&
thinkingBudgetTokens > 0 &&
model.info?.thinkingConfig &&
!(model.id.includes("gemini-3") && geminiThinkingLevel)
) {
// For other models with thinkingConfig, use the standard check
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: thinkingBudgetTokens }
}
// @ts-ignore-next-line
// Skip reasoning for models that don't support it (e.g., devstral, grok-4)
const includeReasoning = !shouldSkipReasoningForModel(model.id)
// @ts-expect-error-next-line
const stream = await client.chat.completions.create({
model: model.id,
max_tokens: model.info.maxTokens,
max_tokens: maxTokens,
temperature: temperature,
top_p: topP,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
include_reasoning: true,
include_reasoning: includeReasoning,
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
...getOpenAIToolParams(tools),
...(model.id.includes("gemini-3") && geminiThinkingLevel
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
: {}),
})
return stream
+176
View File
@@ -0,0 +1,176 @@
import OpenAI from "openai"
import { Logger } from "@/services/logging/Logger"
import { ModelInfo } from "@/shared/api"
// Type that represents the OpenAI ResponseStream with its private properties
// The #private property issue can be resolved by using the AsyncIterable interface
export async function* handleResponsesApiStreamResponse(
stream: AsyncIterable<OpenAI.Responses.ResponseStreamEvent> & { _request_id?: string | null },
modelInfo: ModelInfo,
calculateCost: (
modelInfo: ModelInfo,
inputTokens: number,
outputTokens: number,
cacheWriteTokens: number,
cacheReadTokens: number,
) => Promise<number>,
) {
// Process the response stream
for await (const chunk of stream) {
// Handle different event types from Responses API
if (chunk.type === "response.output_item.added") {
const item = chunk.item
if (item.type === "function_call" && item.id) {
yield {
type: "tool_calls",
id: item.id,
tool_call: {
call_id: item.call_id,
function: {
id: item.id,
name: item.name,
arguments: item.arguments,
},
},
} as const
}
if (item.type === "reasoning" && item.encrypted_content && item.id) {
yield {
type: "reasoning",
id: item.id,
reasoning: "",
redacted_data: item.encrypted_content,
} as const
}
}
if (chunk.type === "response.output_item.done") {
const item = chunk.item
if (item.type === "function_call") {
yield {
type: "tool_calls",
id: item.id || item.call_id,
tool_call: {
call_id: item.call_id,
function: {
id: item.id,
name: item.name,
arguments: item.arguments,
},
},
} as const
}
if (item.type === "reasoning") {
yield {
type: "reasoning",
id: item.id,
details: item.summary,
reasoning: "",
} as const
}
}
if (chunk.type === "response.reasoning_summary_part.added") {
yield {
type: "reasoning",
id: chunk.item_id,
reasoning: chunk.part.text,
} as const
}
if (chunk.type === "response.reasoning_summary_text.delta") {
yield {
type: "reasoning",
id: chunk.item_id,
reasoning: chunk.delta,
} as const
}
if (chunk.type === "response.reasoning_summary_part.done") {
yield {
type: "reasoning",
id: chunk.item_id,
details: chunk.part,
reasoning: "",
} as const
}
if (chunk.type === "response.output_text.delta") {
// Handle text content deltas
if (chunk.delta) {
yield {
id: chunk.item_id,
type: "text",
text: chunk.delta,
} as const
}
}
if (chunk.type === "response.reasoning_text.delta") {
// Handle reasoning content deltas
if (chunk.delta) {
yield {
id: chunk.item_id,
type: "reasoning",
reasoning: chunk.delta,
} as const
}
}
if (chunk.type === "response.function_call_arguments.delta") {
yield {
type: "tool_calls",
tool_call: {
function: {
id: chunk.item_id,
name: chunk.item_id,
arguments: chunk.delta,
},
},
} as const
}
if (chunk.type === "response.function_call_arguments.done") {
// Handle completed function call
if (chunk.item_id && chunk.name && chunk.arguments) {
yield {
type: "tool_calls",
tool_call: {
function: {
id: chunk.item_id,
name: chunk.name,
arguments: chunk.arguments,
},
},
} as const
}
}
if (
chunk.type === "response.incomplete" &&
chunk.response?.status === "incomplete" &&
chunk.response?.incomplete_details?.reason === "max_output_tokens"
) {
console.log("Ran out of tokens")
if (chunk.response?.output_text?.length > 0) {
console.log("Partial output:", chunk.response.output_text)
} else {
console.log("Ran out of tokens during reasoning")
}
}
if (chunk.type === "response.completed" && chunk.response?.usage) {
// Handle usage information when response is complete
const usage = chunk.response.usage
const inputTokens = usage.input_tokens || 0
const outputTokens = usage.output_tokens || 0
const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0
const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0
const totalTokens = usage.total_tokens || 0
const totalCost = await calculateCost(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
Logger.log(`Total tokens from Responses API usage: ${totalTokens}`)
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens)
yield {
type: "usage",
inputTokens: nonCachedInputTokens,
outputTokens: outputTokens,
cacheWriteTokens: cacheWriteTokens,
cacheReadTokens: cacheReadTokens,
totalCost: totalCost,
id: chunk.response.id,
} as const
}
}
}
+1
View File
@@ -48,6 +48,7 @@ export const toolParamNames = [
"input",
"from_ref",
"to_ref",
"skill_name",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
@@ -0,0 +1,443 @@
/**
* Unit tests for skills utility functions
* Tests skill discovery, override resolution, toggle filtering, and content loading
*/
import { expect } from "chai"
import * as fs from "fs"
import { afterEach, beforeEach, describe, it } from "mocha"
import * as path from "path"
import * as sinon from "sinon"
import * as disk from "@/core/storage/disk"
import * as fsUtils from "@/utils/fs"
import { discoverSkills, getAvailableSkills, getSkillContent } from "../skills"
describe("Skills Utility Functions", () => {
let sandbox: sinon.SinonSandbox
let fileExistsStub: sinon.SinonStub
let isDirectoryStub: sinon.SinonStub
let readdirStub: sinon.SinonStub
let statStub: sinon.SinonStub
let readFileStub: sinon.SinonStub
let ensureSkillsDirStub: sinon.SinonStub
// Use path.join for OS-independent paths
const TEST_CWD = path.join("/test", "project")
const GLOBAL_SKILLS_DIR = path.join("/home", "user", ".cline", "skills")
beforeEach(() => {
sandbox = sinon.createSandbox()
// Stub console.warn to avoid noise in test output
sandbox.stub(console, "warn")
// Stub filesystem utilities
fileExistsStub = sandbox.stub(fsUtils, "fileExistsAtPath")
isDirectoryStub = sandbox.stub(fsUtils, "isDirectory")
readdirStub = sandbox.stub(fs.promises, "readdir")
statStub = sandbox.stub(fs.promises, "stat")
readFileStub = sandbox.stub(fs.promises, "readFile")
ensureSkillsDirStub = sandbox.stub(disk, "ensureSkillsDirectoryExists")
// Default: global skills dir
ensureSkillsDirStub.resolves(GLOBAL_SKILLS_DIR)
// Default: no directories exist
fileExistsStub.resolves(false)
isDirectoryStub.resolves(false)
})
afterEach(() => {
sandbox.restore()
})
describe("discoverSkills", () => {
it("should discover skills from global directory", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-skill"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: my-skill
description: A test skill
---
Instructions here`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(1)
expect(skills[0].name).to.equal("my-skill")
expect(skills[0].description).to.equal("A test skill")
expect(skills[0].source).to.equal("global")
})
it("should discover skills from project .clinerules/skills directory", async () => {
const projectSkillsDir = path.join(TEST_CWD, ".clinerules", "skills")
const skillDir = path.join(projectSkillsDir, "explaining-code")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(projectSkillsDir).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(projectSkillsDir).resolves(true)
readdirStub.withArgs(projectSkillsDir).resolves(["explaining-code"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: explaining-code
description: Explains code with diagrams and analogies
---
Use analogies and ASCII diagrams when explaining code.`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(1)
expect(skills[0].name).to.equal("explaining-code")
expect(skills[0].source).to.equal("project")
})
it("should discover skills from project .cline/skills directory", async () => {
const clineSkillsDir = path.join(TEST_CWD, ".cline", "skills")
const skillDir = path.join(clineSkillsDir, "debugging")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(clineSkillsDir).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(clineSkillsDir).resolves(true)
readdirStub.withArgs(clineSkillsDir).resolves(["debugging"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: debugging
description: Debug code systematically
---
Use systematic debugging approaches.`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(1)
expect(skills[0].name).to.equal("debugging")
expect(skills[0].source).to.equal("project")
})
it("should discover skills from project .claude/skills directory", async () => {
const claudeSkillsDir = path.join(TEST_CWD, ".claude", "skills")
const skillDir = path.join(claudeSkillsDir, "coding")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(claudeSkillsDir).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(claudeSkillsDir).resolves(true)
readdirStub.withArgs(claudeSkillsDir).resolves(["coding"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: coding
description: Write clean code
---
Follow best practices.`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(1)
expect(skills[0].name).to.equal("coding")
expect(skills[0].source).to.equal("project")
})
it("should handle empty skills directories gracefully", async () => {
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves([])
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
})
it("should skip non-directory entries in skills folder", async () => {
const readmePath = path.join(GLOBAL_SKILLS_DIR, "README.md")
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["README.md", "my-skill"])
statStub.withArgs(readmePath).resolves({ isDirectory: () => false })
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
fileExistsStub.withArgs(skillMdPath).resolves(true)
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: my-skill
description: A skill
---
Content`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(1)
expect(skills[0].name).to.equal("my-skill")
})
it("should skip skill directories without SKILL.md", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "incomplete-skill")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["incomplete-skill"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
fileExistsStub.withArgs(skillMdPath).resolves(false)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
})
})
describe("getAvailableSkills - Override Resolution", () => {
it("should override project skill with global skill of same name", async () => {
const globalSkillDir = path.join(GLOBAL_SKILLS_DIR, "coding")
const globalSkillMdPath = path.join(globalSkillDir, "SKILL.md")
// Setup global skill (higher priority)
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(globalSkillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["coding"])
statStub.withArgs(globalSkillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(globalSkillMdPath, "utf-8").resolves(`---
name: coding
description: Global coding skill
---
Global instructions`)
// Setup project skill with same name (lower priority)
const projectSkillsDir = path.join(TEST_CWD, ".clinerules", "skills")
const projectSkillDir = path.join(projectSkillsDir, "coding")
const projectSkillMdPath = path.join(projectSkillDir, "SKILL.md")
fileExistsStub.withArgs(projectSkillsDir).resolves(true)
fileExistsStub.withArgs(projectSkillMdPath).resolves(true)
isDirectoryStub.withArgs(projectSkillsDir).resolves(true)
readdirStub.withArgs(projectSkillsDir).resolves(["coding"])
statStub.withArgs(projectSkillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(projectSkillMdPath, "utf-8").resolves(`---
name: coding
description: Project coding skill
---
Project instructions`)
const allSkills = await discoverSkills(TEST_CWD)
const skills = getAvailableSkills(allSkills)
expect(skills).to.have.lengthOf(1)
expect(skills[0].description).to.equal("Global coding skill")
expect(skills[0].source).to.equal("global")
})
it("should keep both skills when names are different", async () => {
const globalSkillDir = path.join(GLOBAL_SKILLS_DIR, "global-skill")
const globalSkillMdPath = path.join(globalSkillDir, "SKILL.md")
// Setup global skill
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(globalSkillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["global-skill"])
statStub.withArgs(globalSkillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(globalSkillMdPath, "utf-8").resolves(`---
name: global-skill
description: A global skill
---
Content`)
// Setup project skill with different name
const projectSkillsDir = path.join(TEST_CWD, ".clinerules", "skills")
const projectSkillDir = path.join(projectSkillsDir, "project-skill")
const projectSkillMdPath = path.join(projectSkillDir, "SKILL.md")
fileExistsStub.withArgs(projectSkillsDir).resolves(true)
fileExistsStub.withArgs(projectSkillMdPath).resolves(true)
isDirectoryStub.withArgs(projectSkillsDir).resolves(true)
readdirStub.withArgs(projectSkillsDir).resolves(["project-skill"])
statStub.withArgs(projectSkillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(projectSkillMdPath, "utf-8").resolves(`---
name: project-skill
description: A project skill
---
Content`)
const allSkills = await discoverSkills(TEST_CWD)
const skills = getAvailableSkills(allSkills)
expect(skills).to.have.lengthOf(2)
const names = skills.map((s) => s.name)
expect(names).to.include("global-skill")
expect(names).to.include("project-skill")
})
})
describe("Metadata Validation", () => {
it("should reject skill with missing name field", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "bad-skill")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["bad-skill"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
description: Missing name
---
Content`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /missing required 'name' field/)
})
it("should reject skill with missing description field", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "bad-skill")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["bad-skill"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: bad-skill
---
Content`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /missing required 'description' field/)
})
it("should reject skill when name doesn't match directory name", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-dir")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-dir"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: different-name
description: Mismatched name
---
Content`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /doesn't match directory/)
})
it("should handle malformed YAML frontmatter gracefully", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "bad-yaml")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["bad-yaml"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: [invalid yaml
description: broken
---
Content`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
})
it("should handle file without frontmatter", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "no-front")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["no-front"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`Just plain markdown content without frontmatter`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(0)
})
})
describe("getSkillContent", () => {
it("should load full skill content with instructions", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-skill"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: my-skill
description: Test skill
---
These are the detailed instructions.
## Step 1
Do this first.
## Step 2
Then do this.`)
const allSkills = await discoverSkills(TEST_CWD)
const availableSkills = getAvailableSkills(allSkills)
const content = await getSkillContent("my-skill", availableSkills)
expect(content).to.not.be.null
expect(content!.name).to.equal("my-skill")
expect(content!.instructions).to.include("These are the detailed instructions")
expect(content!.instructions).to.include("Step 1")
expect(content!.instructions).to.include("Step 2")
})
it("should return null for non-existent skill", async () => {
const content = await getSkillContent("non-existent", [])
expect(content).to.be.null
})
it("should trim whitespace from instructions", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-skill"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
name: my-skill
description: Test
---
Instructions with whitespace
`)
const allSkills = await discoverSkills(TEST_CWD)
const availableSkills = getAvailableSkills(allSkills)
const content = await getSkillContent("my-skill", availableSkills)
expect(content!.instructions).to.equal("Instructions with whitespace")
})
})
})
@@ -65,6 +65,7 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
const rulesFilePaths = await readDirectory(clineRulesFilePath, [
[".clinerules", "workflows"],
[".clinerules", "hooks"],
[".clinerules", "skills"],
])
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
@@ -110,6 +111,7 @@ export async function refreshClineRulesToggles(
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
[".clinerules", "workflows"],
[".clinerules", "hooks"],
[".clinerules", "skills"],
])
controller.stateManager.setWorkspaceState("localClineRulesToggles", updatedLocalToggles)
@@ -0,0 +1,169 @@
import { ensureSkillsDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import type { SkillContent, SkillMetadata } from "@shared/skills"
import { fileExistsAtPath, isDirectory } from "@utils/fs"
import * as fs from "fs/promises"
import * as yaml from "js-yaml"
import * as path from "path"
/**
* Parse YAML frontmatter from markdown content.
*/
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
const match = fileContent.match(frontmatterRegex)
if (!match) {
return { data: {}, content: fileContent }
}
const [, yamlContent, body] = match
try {
const data = yaml.load(yamlContent) as Record<string, unknown>
return { data: data || {}, content: body }
} catch (error) {
console.warn("Failed to parse YAML frontmatter:", error)
return { data: {}, content: fileContent }
}
}
/**
* Scan a directory for skill subdirectories containing SKILL.md files.
*/
async function scanSkillsDirectory(dirPath: string, source: "global" | "project"): Promise<SkillMetadata[]> {
const skills: SkillMetadata[] = []
if (!(await fileExistsAtPath(dirPath)) || !(await isDirectory(dirPath))) {
return skills
}
try {
const entries = await fs.readdir(dirPath)
for (const entryName of entries) {
const entryPath = path.join(dirPath, entryName)
const stats = await fs.stat(entryPath).catch(() => null)
if (!stats?.isDirectory()) continue
const skill = await loadSkillMetadata(entryPath, source, entryName)
if (skill) {
skills.push(skill)
}
}
} catch (error: unknown) {
if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EACCES") {
console.warn(`Permission denied reading skills directory: ${dirPath}`)
}
}
return skills
}
/**
* Load skill metadata from a skill directory.
*/
async function loadSkillMetadata(
skillDir: string,
source: "global" | "project",
skillName: string,
): Promise<SkillMetadata | null> {
const skillMdPath = path.join(skillDir, "SKILL.md")
if (!(await fileExistsAtPath(skillMdPath))) return null
try {
const fileContent = await fs.readFile(skillMdPath, "utf-8")
const { data: frontmatter } = parseFrontmatter(fileContent)
// Validate required fields
if (!frontmatter.name || typeof frontmatter.name !== "string") {
console.warn(`Skill at ${skillDir} missing required 'name' field`)
return null
}
if (!frontmatter.description || typeof frontmatter.description !== "string") {
console.warn(`Skill at ${skillDir} missing required 'description' field`)
return null
}
// Name must match directory name per spec
if (frontmatter.name !== skillName) {
console.warn(`Skill name "${frontmatter.name}" doesn't match directory "${skillName}"`)
return null
}
return {
name: skillName,
description: frontmatter.description,
path: skillMdPath,
source,
}
} catch (error) {
console.warn(`Failed to load skill at ${skillDir}:`, error)
return null
}
}
/**
* Discover all skills from global (~/.cline/skills) and project directories.
* Returns skills in order: project skills first, then global skills.
* Global skills take precedence over project skills with the same name.
*/
export async function discoverSkills(cwd: string): Promise<SkillMetadata[]> {
const skills: SkillMetadata[] = []
const globalSkillsDir = await ensureSkillsDirectoryExists()
const projectDirs = [
path.join(cwd, GlobalFileNames.clineruleSkillsDir),
path.join(cwd, GlobalFileNames.clineSkillsDir),
path.join(cwd, GlobalFileNames.claudeSkillsDir),
]
// Load project skills first (lower priority)
for (const dir of projectDirs) {
const projectSkills = await scanSkillsDirectory(dir, "project")
skills.push(...projectSkills)
}
// Load global skills last (~/.cline/skills) - higher priority
const globalSkills = await scanSkillsDirectory(globalSkillsDir, "global")
skills.push(...globalSkills)
return skills
}
/**
* Get available skills with override resolution (global > project).
*/
export function getAvailableSkills(skills: SkillMetadata[]): SkillMetadata[] {
const seen = new Set<string>()
const result: SkillMetadata[] = []
// Iterate backwards: global skills (added last) are seen first and take precedence
for (let i = skills.length - 1; i >= 0; i--) {
const skill = skills[i]
if (!seen.has(skill.name)) {
seen.add(skill.name)
result.unshift(skill)
}
}
return result
}
/**
* Get full skill content including instructions.
*/
export async function getSkillContent(skillName: string, availableSkills: SkillMetadata[]): Promise<SkillContent | null> {
const skill = availableSkills.find((s) => s.name === skillName)
if (!skill) return null
try {
const fileContent = await fs.readFile(skill.path, "utf-8")
const { content: body } = parseFrontmatter(fileContent)
return {
...skill,
instructions: body.trim(),
}
} catch {
return null
}
}
+105
View File
@@ -0,0 +1,105 @@
import { CreateSkillRequest, SkillsToggles } from "@shared/proto/cline/file"
import fs from "fs/promises"
import path from "path"
import { ensureSkillsDirectoryExists } from "@/core/storage/disk"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/host/window"
import { fileExistsAtPath } from "@/utils/fs"
import { Controller } from ".."
import { openFile } from "./openFile"
const SKILL_TEMPLATE = `---
name: {{SKILL_NAME}}
description: Brief description of what this skill does
---
# {{SKILL_NAME}}
Instructions for the AI agent...
## Usage
Describe when and how to use this skill.
## Steps
1. First step
2. Second step
3. Third step
`
/**
* Creates a new skill from template
* @param controller The controller instance
* @param request The request containing skill name and isGlobal flag
* @returns The updated skills toggles
*/
export async function createSkillFile(controller: Controller, request: CreateSkillRequest): Promise<SkillsToggles> {
const { skillName, isGlobal } = request
if (!skillName || typeof skillName !== "string" || typeof isGlobal !== "boolean") {
console.error("createSkillFile: Missing or invalid parameters", {
skillName: typeof skillName === "string" ? skillName : `Invalid: ${typeof skillName}`,
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
})
throw new Error("Missing or invalid parameters for createSkillFile")
}
// Validate skill name (must be valid directory name)
const sanitizedName = skillName.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase()
if (!sanitizedName) {
throw new Error("Invalid skill name")
}
let skillDir: string
if (isGlobal) {
const globalSkillsDir = await ensureSkillsDirectoryExists()
skillDir = path.join(globalSkillsDir, sanitizedName)
} else {
const workspacePaths = await HostProvider.workspace.getWorkspacePaths({})
const primaryWorkspace = workspacePaths.paths[0]
if (!primaryWorkspace) {
throw new Error("No workspace folder open")
}
// Create in .cline/skills by default
const localSkillsDir = path.join(primaryWorkspace, ".cline", "skills")
await fs.mkdir(localSkillsDir, { recursive: true })
skillDir = path.join(localSkillsDir, sanitizedName)
}
// Check if skill already exists
if (await fileExistsAtPath(skillDir)) {
await HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message: `Skill "${sanitizedName}" already exists`,
})
// Return current toggles
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
return SkillsToggles.create({
globalSkillsToggles: globalToggles,
localSkillsToggles: localToggles,
})
}
// Create skill directory
await fs.mkdir(skillDir, { recursive: true })
// Create SKILL.md from template
const skillMdPath = path.join(skillDir, "SKILL.md")
const content = SKILL_TEMPLATE.replace(/\{\{SKILL_NAME\}\}/g, sanitizedName)
await fs.writeFile(skillMdPath, content, "utf-8")
// Open the file for editing
await openFile(controller, { value: skillMdPath })
// Return current toggles (new skill defaults to enabled)
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
return SkillsToggles.create({
globalSkillsToggles: globalToggles,
localSkillsToggles: localToggles,
})
}
@@ -0,0 +1,62 @@
import { DeleteSkillRequest, SkillsToggles } from "@shared/proto/cline/file"
import fs from "fs/promises"
import path from "path"
import { fileExistsAtPath } from "@/utils/fs"
import { Controller } from ".."
/**
* Deletes an existing skill directory
* @param controller The controller instance
* @param request The request containing skill path and isGlobal flag
* @returns The updated skills toggles
*/
export async function deleteSkillFile(controller: Controller, request: DeleteSkillRequest): Promise<SkillsToggles> {
const { skillPath, isGlobal } = request
if (!skillPath || typeof skillPath !== "string" || typeof isGlobal !== "boolean") {
console.error("deleteSkillFile: Missing or invalid parameters", {
skillPath: typeof skillPath === "string" ? skillPath : `Invalid: ${typeof skillPath}`,
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
})
throw new Error("Missing or invalid parameters for deleteSkillFile")
}
// Get the skill directory (skillPath points to SKILL.md, so get parent)
const skillDir = path.dirname(skillPath)
// Verify the path exists
if (!(await fileExistsAtPath(skillDir))) {
console.warn(`deleteSkillFile: Skill directory not found: ${skillDir}`)
// Return current toggles anyway
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
return SkillsToggles.create({
globalSkillsToggles: globalToggles,
localSkillsToggles: localToggles,
})
}
// Delete the skill directory
await fs.rm(skillDir, { recursive: true, force: true })
// Remove from toggles
let globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
let localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
if (isGlobal) {
const { [skillPath]: _, ...remaining } = globalToggles
globalToggles = remaining
controller.stateManager.setGlobalState("globalSkillsToggles", globalToggles)
} else {
const { [skillPath]: _, ...remaining } = localToggles
localToggles = remaining
controller.stateManager.setWorkspaceState("localSkillsToggles", localToggles)
}
await controller.postStateToWebview()
return SkillsToggles.create({
globalSkillsToggles: globalToggles,
localSkillsToggles: localToggles,
})
}
+131
View File
@@ -0,0 +1,131 @@
import { RefreshedSkills, SkillInfo } from "@shared/proto/cline/file"
import fs from "fs/promises"
import path from "path"
import { ensureSkillsDirectoryExists } from "@/core/storage/disk"
import { HostProvider } from "@/hosts/host-provider"
import { fileExistsAtPath, isDirectory } from "@/utils/fs"
import { Controller } from ".."
/**
* Parse YAML frontmatter from markdown content.
*/
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
const match = fileContent.match(frontmatterRegex)
if (!match) {
return { data: {}, content: fileContent }
}
const [, yamlContent, body] = match
// Simple YAML parsing for name and description
const data: Record<string, unknown> = {}
const lines = yamlContent.split("\n")
for (const line of lines) {
const colonIndex = line.indexOf(":")
if (colonIndex > 0) {
const key = line.slice(0, colonIndex).trim()
const value = line
.slice(colonIndex + 1)
.trim()
.replace(/^["']|["']$/g, "")
data[key] = value
}
}
return { data, content: body }
}
/**
* Scan a directory for skill subdirectories containing SKILL.md files.
*/
async function scanSkillsDirectory(dirPath: string): Promise<SkillInfo[]> {
const skills: SkillInfo[] = []
if (!(await fileExistsAtPath(dirPath)) || !(await isDirectory(dirPath))) {
return skills
}
try {
const entries = await fs.readdir(dirPath)
for (const entryName of entries) {
const entryPath = path.join(dirPath, entryName)
const stats = await fs.stat(entryPath).catch(() => null)
if (!stats?.isDirectory()) continue
const skillMdPath = path.join(entryPath, "SKILL.md")
if (!(await fileExistsAtPath(skillMdPath))) continue
try {
const fileContent = await fs.readFile(skillMdPath, "utf-8")
const { data: frontmatter } = parseFrontmatter(fileContent)
// Validate required fields
if (!frontmatter.name || typeof frontmatter.name !== "string") continue
if (!frontmatter.description || typeof frontmatter.description !== "string") continue
if (frontmatter.name !== entryName) continue
skills.push(
SkillInfo.create({
name: entryName,
description: frontmatter.description,
path: skillMdPath,
enabled: true, // Will be updated with toggle state
}),
)
} catch {
// Skip invalid skills
}
}
} catch {
// Directory read error, skip
}
return skills
}
/**
* Refreshes all skill toggles (discovers skills and their enabled state)
*/
export async function refreshSkills(controller: Controller): Promise<RefreshedSkills> {
const globalSkillsDir = await ensureSkillsDirectoryExists()
// Get workspace paths for local skills
const workspacePaths = await HostProvider.workspace.getWorkspacePaths({})
const primaryWorkspace = workspacePaths.paths[0]
// Scan global skills
const globalSkills = await scanSkillsDirectory(globalSkillsDir)
// Get global toggles and apply them
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
for (const skill of globalSkills) {
skill.enabled = globalToggles[skill.path] !== false
}
// Scan local skills from all possible directories
const localSkills: SkillInfo[] = []
if (primaryWorkspace) {
const localDirs = [
path.join(primaryWorkspace, ".clinerules", "skills"),
path.join(primaryWorkspace, ".cline", "skills"),
path.join(primaryWorkspace, ".claude", "skills"),
]
for (const dir of localDirs) {
const skills = await scanSkillsDirectory(dir)
localSkills.push(...skills)
}
}
// Get local toggles and apply them
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
for (const skill of localSkills) {
skill.enabled = localToggles[skill.path] !== false
}
return RefreshedSkills.create({
globalSkills,
localSkills,
})
}
+39
View File
@@ -0,0 +1,39 @@
import { SkillsToggles, ToggleSkillRequest } from "@shared/proto/cline/file"
import { Controller } from ".."
/**
* Toggles a skill on or off
* @param controller The controller instance
* @param request The request containing the skill path and enabled state
* @returns The updated skills toggles
*/
export async function toggleSkill(controller: Controller, request: ToggleSkillRequest): Promise<SkillsToggles> {
const { skillPath, isGlobal, enabled } = request
if (!skillPath || typeof enabled !== "boolean" || typeof isGlobal !== "boolean") {
console.error("toggleSkill: Missing or invalid parameters", {
skillPath,
isGlobal,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
})
throw new Error("Missing or invalid parameters for toggleSkill")
}
let globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
let localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
if (isGlobal) {
globalToggles = { ...globalToggles, [skillPath]: enabled }
controller.stateManager.setGlobalState("globalSkillsToggles", globalToggles)
} else {
localToggles = { ...localToggles, [skillPath]: enabled }
controller.stateManager.setWorkspaceState("localSkillsToggles", localToggles)
}
await controller.postStateToWebview()
return SkillsToggles.create({
globalSkillsToggles: globalToggles,
localSkillsToggles: localToggles,
})
}
+16 -58
View File
@@ -30,14 +30,15 @@ import { ExtensionRegistryInfo } from "@/registry"
import { AuthService } from "@/services/auth/AuthService"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { LogoutReason } from "@/services/auth/types"
import { BannerService } from "@/services/banner/BannerService"
import { featureFlagsService } from "@/services/feature-flags"
import { getDistinctId } from "@/services/logging/distinctId"
import { telemetryService } from "@/services/telemetry"
import { BannerCardData } from "@/shared/cline/banner"
import { getAxiosSettings } from "@/shared/net"
import { ShowMessageType } from "@/shared/proto/host/window"
import { getLatestAnnouncementId } from "@/utils/announcements"
import { getCwd, getDesktopDir } from "@/utils/path"
import { BannerService } from "../../services/banner/BannerService"
import { PromptRegistry } from "../prompts/system-prompt"
import {
ensureCacheDirectoryExists,
@@ -831,6 +832,8 @@ export class Controller {
const enableCheckpointsSetting = this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting")
const globalClineRulesToggles = this.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
const globalWorkflowToggles = this.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
const globalSkillsToggles = this.stateManager.getGlobalSettingsKey("globalSkillsToggles")
const localSkillsToggles = this.stateManager.getWorkspaceStateKey("localSkillsToggles")
const remoteRulesToggles = this.stateManager.getGlobalStateKey("remoteRulesToggles")
const remoteWorkflowToggles = this.stateManager.getGlobalStateKey("remoteWorkflowToggles")
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
@@ -851,6 +854,7 @@ export class Controller {
const lastDismissedModelBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedModelBannerVersion") || 0
const lastDismissedCliBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedCliBannerVersion") || 0
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
const skillsEnabled = this.stateManager.getGlobalSettingsKey("skillsEnabled")
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
@@ -874,6 +878,7 @@ export class Controller {
const distinctId = getDistinctId()
const version = ExtensionRegistryInfo.version
const environment = ClineEnv.config().environment
const banners = await this.getBanners()
// Set feature flag in dictation settings based on platform
const updatedDictationSettings = {
@@ -914,6 +919,8 @@ export class Controller {
localAgentsRulesToggles: localAgentsRulesToggles || {},
localWorkflowToggles: workflowToggles || {},
globalWorkflowToggles: globalWorkflowToggles || {},
globalSkillsToggles: globalSkillsToggles || {},
localSkillsToggles: localSkillsToggles || {},
remoteRulesToggles: remoteRulesToggles,
remoteWorkflowToggles: remoteWorkflowToggles,
shellIntegrationTimeout,
@@ -955,6 +962,9 @@ export class Controller {
nativeToolCallSetting: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
skillsEnabled,
optOutOfRemoteConfig: this.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig"),
banners,
}
}
@@ -997,64 +1007,12 @@ export class Controller {
return history
}
/**
* Initializes the BannerService if not already initialized
*/
private async ensureBannerService() {
if (!BannerService.isInitialized()) {
try {
BannerService.initialize(this)
} catch (error) {
console.error("Failed to initialize BannerService:", error)
}
}
}
/**
* Fetches non-dismissed banners for display
* @returns Array of banners that haven't been dismissed
*/
async fetchBannersForDisplay(): Promise<any[]> {
async getBanners(): Promise<BannerCardData[]> {
try {
await this.ensureBannerService()
if (BannerService.isInitialized()) {
return await BannerService.get().getNonDismissedBanners()
}
} catch (error) {
console.error("Failed to fetch banners:", error)
}
return []
}
/**
* Dismisses a banner and sends telemetry
* @param bannerId The ID of the banner to dismiss
*/
async dismissBanner(bannerId: string): Promise<void> {
try {
await this.ensureBannerService()
if (BannerService.isInitialized()) {
await BannerService.get().dismissBanner(bannerId)
await this.postStateToWebview()
}
} catch (error) {
console.error("Failed to dismiss banner:", error)
}
}
/**
* Sends a banner event for telemetry tracking
* @param bannerId The ID of the banner
* @param eventType The type of event (seen, dismiss, click)
*/
async trackBannerEvent(bannerId: string, eventType: "dismiss"): Promise<void> {
try {
await this.ensureBannerService()
if (BannerService.isInitialized()) {
await BannerService.get().sendBannerEvent(bannerId, eventType)
}
} catch (error) {
console.error("Failed to track banner event:", error)
return BannerService.get().getActiveBanners()
} catch (err) {
console.log(err)
return []
}
}
}
@@ -18,7 +18,7 @@ export async function refreshLiteLlmModels(): Promise<Record<string, ModelInfo>>
try {
// Get the LiteLLM configuration
const apiConfiguration = stateManager.getApiConfiguration()
const baseUrl = apiConfiguration.liteLlmBaseUrl || ""
const baseUrl = apiConfiguration.liteLlmBaseUrl || "http://localhost:4000"
const apiKey = apiConfiguration.liteLlmApiKey
if (!apiKey) {
+38 -2
View File
@@ -1,9 +1,14 @@
import { StringRequest } from "@shared/proto/cline/common"
import { OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models"
import { ApiFormat, OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models"
import axios from "axios"
import { HostProvider } from "@/hosts/host-provider"
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
import { DEFAULT_EXTERNAL_OCA_BASE_URL, DEFAULT_INTERNAL_OCA_BASE_URL } from "@/services/auth/oca/utils/constants"
import {
CHAT_COMPLETIONS_API,
DEFAULT_EXTERNAL_OCA_BASE_URL,
DEFAULT_INTERNAL_OCA_BASE_URL,
RESPONSES_API,
} from "@/services/auth/oca/utils/constants"
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
import { Logger } from "@/services/logging/Logger"
import { getAxiosSettings } from "@/shared/net"
@@ -57,6 +62,11 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
defaultModelId = modelId
}
const modelInfo = model.model_info
const supportedApiList = modelInfo.supported_api_list ?? [CHAT_COMPLETIONS_API]
const apiFormat: ApiFormat =
supportedApiList.includes(RESPONSES_API) && !supportedApiList.includes(CHAT_COMPLETIONS_API)
? ApiFormat.OPENAI_RESPONSES
: ApiFormat.OPENAI_CHAT
models[modelId] = OcaModelInfo.create({
maxTokens: model.litellm_params?.max_tokens || -1,
contextWindow: modelInfo.context_window,
@@ -73,6 +83,9 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
temperature: modelInfo.temperature || 0,
banner: modelInfo.banner,
modelName: modelId,
apiFormat: apiFormat,
supportsReasoning: modelInfo.is_reasoning_model || false,
reasoningEffortOptions: modelInfo.reasoning_effort_options || [],
})
}
console.log("OCA models fetched", models)
@@ -91,6 +104,25 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
? apiConfiguration.actModeOcaModelId
: defaultModelId!
let planModeOcaReasoningEffort
let actModeOcaReasoningEffort
if (
models[planModeSelectedModelId].supportsReasoning &&
models[planModeSelectedModelId].reasoningEffortOptions.length > 0
) {
planModeOcaReasoningEffort = apiConfiguration.planModeOcaReasoningEffort
? apiConfiguration.planModeOcaReasoningEffort
: models[planModeSelectedModelId].reasoningEffortOptions[0]
}
if (
models[actModeSelectedModelId].supportsReasoning &&
models[actModeSelectedModelId].reasoningEffortOptions.length > 0
) {
actModeOcaReasoningEffort = apiConfiguration.actModeOcaReasoningEffort
? apiConfiguration.actModeOcaReasoningEffort
: models[actModeSelectedModelId].reasoningEffortOptions[0]
}
// Build updates object based on plan/act mode setting
const updates: Partial<GlobalStateAndSettings> = {}
@@ -98,15 +130,19 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
if (currentMode === "plan") {
updates.planModeOcaModelId = planModeSelectedModelId
updates.planModeOcaModelInfo = models[planModeSelectedModelId]
updates.planModeOcaReasoningEffort = planModeOcaReasoningEffort
} else {
updates.actModeOcaModelId = actModeSelectedModelId
updates.actModeOcaModelInfo = models[actModeSelectedModelId]
updates.actModeOcaReasoningEffort = actModeOcaReasoningEffort
}
} else {
updates.planModeOcaModelId = planModeSelectedModelId
updates.planModeOcaModelInfo = models[planModeSelectedModelId]
updates.planModeOcaReasoningEffort = planModeOcaReasoningEffort
updates.actModeOcaModelId = actModeSelectedModelId
updates.actModeOcaModelInfo = models[actModeSelectedModelId]
updates.actModeOcaReasoningEffort = actModeOcaReasoningEffort
}
// Update state directly using batch method
@@ -7,6 +7,73 @@ import path from "path"
import { getAxiosSettings } from "@/shared/net"
import { Controller } from ".."
/**
* Derives thinkingConfig from model ID and tags.
* The Vercel API only provides a "reasoning" tag to indicate support,
* so we derive the specific configuration based on model patterns.
*/
function deriveThinkingConfig(modelId: string, tags?: string[]): ModelInfo["thinkingConfig"] {
if (!tags?.includes("reasoning")) {
return undefined
}
// Anthropic Claude models
if (modelId.startsWith("anthropic/claude")) {
return { maxBudget: 8192 }
}
// Google Gemini models
if (modelId.includes("gemini-3")) {
return {
maxBudget: 32767,
supportsThinkingLevel: true,
geminiThinkingLevel: "high",
}
}
// DeepSeek R1 models
if (modelId.startsWith("deepseek/deepseek-r1")) {
return { maxBudget: 8192 }
}
// OpenAI o-series reasoning models
if (modelId.startsWith("openai/o1") || modelId.startsWith("openai/o3")) {
return { maxBudget: 32000 }
}
// Qwen QwQ models (specific IDs to match OpenRouter)
if (modelId === "qwen/qwq-32b:free" || modelId === "qwen/qwq-32b") {
return { maxBudget: 32000 }
}
// Default for other reasoning models
return { maxBudget: 32000 }
}
/**
* Derives recommended temperature for specific model types.
* Returns undefined to use the default (0).
*/
function deriveTemperature(modelId: string): number | undefined {
// DeepSeek R1 and similar reasoning models recommend 0.7
// Use specific model IDs to match OpenRouter behavior
if (
modelId.startsWith("deepseek/deepseek-r1") ||
modelId === "perplexity/sonar-reasoning" ||
modelId === "qwen/qwq-32b:free" ||
modelId === "qwen/qwq-32b"
) {
return 0.7
}
// Gemini 3.0 recommends temperature 1.0
if (modelId.startsWith("google/gemini-3.0") || modelId === "google/gemini-3.0") {
return 1.0
}
return undefined
}
/**
* Core function: Refreshes Vercel AI Gateway models and returns application types
* @param _controller The controller instance (unused)
@@ -18,7 +85,7 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
let models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://ai-gateway.vercel.sh/v1/models", getAxiosSettings())
const response = await axios.get("https://ai-gateway.vercel.sh/v1/models?include_mappings=true", getAxiosSettings())
if (response.data?.data) {
const rawModels = response.data.data
@@ -44,6 +111,8 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
supportsImages: true, // assume all models support images since vercel ai doesn't give this info
supportsPromptCache: !!(rawModel.pricing?.input_cache_read && rawModel.pricing?.input_cache_write),
description: rawModel.description ?? "",
thinkingConfig: deriveThinkingConfig(rawModel.id, rawModel.tags),
temperature: deriveTemperature(rawModel.id),
}
models[rawModel.id] = modelInfo
@@ -1,8 +1,9 @@
import { Empty } from "@shared/proto/cline/common"
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
import { buildApiHandler } from "@/core/api"
import { ApiHandlerOptions, ApiHandlerSecrets, ApiProvider } from "@/shared/api"
import { ApiHandlerOptions, ApiProvider } from "@/shared/api"
import { UpdateApiConfigurationRequestNew } from "@/shared/proto/index.cline"
import { Secrets } from "@/shared/storage/state-keys"
import type { Controller } from "../index"
/**
@@ -69,7 +70,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
const { options: maskOptionsFields, secrets: maskSecretsFields } = parseFieldMask(updateMask)
// Process secrets based on field mask
const secrets: Partial<ApiHandlerSecrets> = {}
const secrets: Partial<Secrets> = {}
if (protoSecrets && maskSecretsFields.size > 0) {
// Validate all masked fields exist
@@ -81,7 +82,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
// Process entries that are in the mask
for (const [key, value] of Object.entries(protoSecrets)) {
if (maskSecretsFields.has(key)) {
secrets[key as keyof ApiHandlerSecrets] = value
secrets[key as keyof Secrets] = value
}
}
}
+11 -5
View File
@@ -1,9 +1,10 @@
import { BannerService } from "@/services/banner/BannerService"
import type { StringRequest } from "@/shared/proto/cline/common"
import { Empty } from "@/shared/proto/cline/common"
import type { Controller } from ".."
/**
* Dismisses a banner by ID
* Dismisses a banner and sends telemetry
* @param controller The controller instance
* @param request The request containing the banner ID to dismiss
* @returns Empty response
@@ -11,9 +12,14 @@ import type { Controller } from ".."
export async function dismissBanner(controller: Controller, request: StringRequest): Promise<Empty> {
const bannerId = request.value
if (bannerId) {
await controller.dismissBanner(bannerId)
if (!bannerId) {
return {}
}
return Empty.create()
try {
await BannerService.get().dismissBanner(bannerId)
await controller.postStateToWebview()
} catch (error) {
console.error("Failed to dismiss banner:", error)
}
return {}
}
+14 -6
View File
@@ -1,3 +1,4 @@
import { BannerService } from "@/services/banner/BannerService"
import { Empty } from "@/shared/proto/cline/common"
import type { TrackBannerEventRequest } from "@/shared/proto/cline/state"
import type { Controller } from ".."
@@ -8,12 +9,19 @@ import type { Controller } from ".."
* @param request The request containing banner ID and event type
* @returns Empty response
*/
export async function trackBannerEvent(controller: Controller, request: TrackBannerEventRequest): Promise<Empty> {
export async function trackBannerEvent(_controller: Controller, request: TrackBannerEventRequest): Promise<Empty> {
const { bannerId, eventType } = request
if (bannerId && eventType) {
await controller.trackBannerEvent(bannerId, eventType as "dismiss")
if (!bannerId) {
return {}
}
return Empty.create()
if (eventType !== "dismiss") {
console.error("Unsupported event type ", eventType)
return {}
}
try {
await BannerService.get().sendBannerEvent(bannerId, eventType)
} catch (error) {
console.error("Failed to track banner event:", error)
}
return {}
}
@@ -11,6 +11,8 @@ import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-
import { OpenaiReasoningEffort } from "@shared/storage/types"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { ClineEnv } from "@/config"
import { fetchRemoteConfig } from "@/core/storage/remote-config/fetch"
import { clearRemoteConfig } from "@/core/storage/remote-config/utils"
import { HostProvider } from "@/hosts/host-provider"
import { McpDisplayMode } from "@/shared/McpDisplayMode"
import { ShowMessageType } from "@/shared/proto/host/window"
@@ -363,6 +365,10 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("subagentsEnabled", !!request.subagentsEnabled)
}
if (request.skillsEnabled !== undefined) {
controller.stateManager.setGlobalState("skillsEnabled", !!request.skillsEnabled)
}
if (request.nativeToolCallEnabled !== undefined) {
controller.stateManager.setGlobalState("nativeToolCallEnabled", !!request.nativeToolCallEnabled)
if (controller.task) {
@@ -379,6 +385,25 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("enableParallelToolCalling", !!request.enableParallelToolCalling)
}
if (request.optOutOfRemoteConfig !== undefined) {
const hadOptedOut = controller.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig")
const isOptingOut = !!request.optOutOfRemoteConfig
const isReenablingRemoteConfig = !isOptingOut && hadOptedOut
// Update now so any subsequent function can access the updated value
controller.stateManager.setGlobalState("optOutOfRemoteConfig", isOptingOut)
if (isOptingOut && !hadOptedOut) {
clearRemoteConfig()
} else if (isReenablingRemoteConfig) {
// Fire-and-forget: We don't need to await here
// The function catches any errors and posts the updated state to the webview
// The immediate state update below shows the user's intent (opted-in),
// and we apply the actual config afterwards without blocking the settings update
fetchRemoteConfig(controller)
}
}
// Post updated state to webview
await controller.postStateToWebview()
@@ -1,57 +0,0 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
import { Controller } from "../index"
// Keep track of active didBecomeVisible subscriptions
const activeDidBecomeVisibleSubscriptions = new Set<StreamingResponseHandler<Empty>>()
/**
* Subscribe to didBecomeVisible 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 subscribeToDidBecomeVisible(
_controller: Controller,
_request: EmptyRequest,
responseStream: StreamingResponseHandler<Empty>,
requestId?: string,
): Promise<void> {
console.log(`[DEBUG] set up didBecomeVisible subscription`)
// Add this subscription to the active subscriptions
activeDidBecomeVisibleSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeDidBecomeVisibleSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "didBecomeVisible_subscription" }, responseStream)
}
}
/**
* Send a didBecomeVisible event to all active subscribers
*/
export async function sendDidBecomeVisibleEvent(): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeDidBecomeVisibleSubscriptions).map(async (responseStream) => {
try {
const event = Empty.create({})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending didBecomeVisible event:", error)
// Remove the subscription if there was an error
activeDidBecomeVisibleSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -1,55 +0,0 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
import type { Controller } from "../index"
// Keep track of active focus chat input subscriptions
const focusChatInputSubscriptions = new Set<StreamingResponseHandler<Empty>>()
/**
* Subscribe to focus chat input events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request
*/
export async function subscribeToFocusChatInput(
_controller: Controller,
_request: EmptyRequest,
responseStream: StreamingResponseHandler<Empty>,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
focusChatInputSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
focusChatInputSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "focus_chat_input_subscription" }, responseStream)
}
}
/**
* Send a focus chat input event to all active subscribers
*/
export async function sendFocusChatInputEvent(): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(focusChatInputSubscriptions).map(async (responseStream) => {
try {
const event = Empty.create({})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending focus chat input event:", error)
// Remove the subscription if there was an error
focusChatInputSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -0,0 +1,57 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { ShowWebviewEvent } from "@shared/proto/cline/ui"
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
import type { Controller } from "../index"
// Keep track of active show webview subscriptions
const showWebviewSubscriptions = new Set<StreamingResponseHandler<ShowWebviewEvent>>()
/**
* Subscribe to show webview events
* @param controller The controller instance
* @param request The show webview request containing preserveEditorFocus flag
* @param responseStream The streaming response handler
* @param requestId The ID of the request
*/
export async function subscribeToShowWebview(
_controller: Controller,
_request: EmptyRequest,
responseStream: StreamingResponseHandler<ShowWebviewEvent>,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
showWebviewSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
showWebviewSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "show_webview_subscription" }, responseStream)
}
}
/**
* Send a show webview event to all active subscribers
* @param preserveEditorFocus When true, the webview should not steal focus from the editor
*/
export async function sendShowWebviewEvent(preserveEditorFocus: boolean = false): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(showWebviewSubscriptions).map(async (responseStream) => {
try {
const event = ShowWebviewEvent.create({ preserveEditorFocus })
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending show webview event:", error)
// Remove the subscription if there was an error
showWebviewSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,292 @@
import { ParseEntry, parse } from "shell-quote"
import { COMMAND_PERMISSIONS_ENV_VAR, CommandPermissionConfig, PermissionValidationResult, ShellOperatorMatch } from "./types"
const OPERATOR_DESCRIPTIONS: Record<string, string> = {
";": "command chaining (semicolon)",
"&&": "command chaining (AND)",
"||": "command chaining (OR)",
"|": "pipe",
">": "output redirection",
">>": "append redirection",
"<": "input redirection",
">&": "file descriptor redirection",
"<&": "file descriptor duplication",
"|&": "pipe with stderr",
}
const LINE_SEPARATOR_REGEX = /[\n\r\u2028\u2029\u0085]/
const LINE_SEPARATOR_DESCRIPTIONS: Record<string, ShellOperatorMatch> = {
"\n": { operator: "\\n", description: "newline (command separator)" },
"\r": { operator: "\\r", description: "carriage return (potential command separator)" },
"\u2028": { operator: "U+2028", description: "unicode line separator" },
"\u2029": { operator: "U+2029", description: "unicode paragraph separator" },
"\u0085": { operator: "U+0085", description: "unicode next line" },
}
/**
* Controls command execution permissions based on environment variable configuration.
* Uses glob pattern matching to allow/deny specific commands.
*
* Configuration is read from the CLINE_COMMAND_PERMISSIONS environment variable.
* Format: {"allow": ["pattern1", "pattern2"], "deny": ["pattern3"]}
*
* Rule evaluation:
* 1. If shell operators are detected outside quotes DENIED (security)
* 2. If deny rules are defined and command matches a deny pattern DENIED
* 3. If allow rules are defined and command matches an allow pattern ALLOWED
* 4. If allow rules are defined but command doesn't match any DENIED (deny by default)
* 5. If no rules are defined (env var not set) ALLOWED (backward compatibility)
*/
export class CommandPermissionController {
private config: CommandPermissionConfig | null = null
constructor() {
this.config = this.parseConfig()
}
/**
* Parse the CLINE_COMMAND_PERMISSIONS environment variable
* @returns Parsed configuration or null if not set or invalid
*/
private parseConfig(): CommandPermissionConfig | null {
const envValue = process.env[COMMAND_PERMISSIONS_ENV_VAR]
if (!envValue) {
return null
}
try {
const parsed = JSON.parse(envValue)
return {
allow: Array.isArray(parsed.allow) ? parsed.allow : undefined,
deny: Array.isArray(parsed.deny) ? parsed.deny : undefined,
allowOperators: Array.isArray(parsed.allowOperators) ? parsed.allowOperators : undefined,
}
} catch (error) {
console.error(`Failed to parse ${COMMAND_PERMISSIONS_ENV_VAR}:`, error)
return null
}
}
/**
* Check if an operator is in the allowOperators list
* @param operator - The operator to check
* @returns true if the operator is allowed
*/
private isOperatorAllowed(operator: string): boolean {
return Boolean(this.config?.allowOperators?.includes(operator))
}
/**
* Validate if a command is allowed to execute based on configured permissions
* @param command - The command string to validate
* @returns PermissionValidationResult indicating if command is allowed and why
*/
validateCommand(command: string): PermissionValidationResult {
// No config = allow everything (backward compatibility)
if (!this.config) {
return { allowed: true, reason: "no_config" }
}
// Check for shell operators FIRST (security check)
const shellOperator = this.detectShellOperator(command)
if (shellOperator) {
return {
allowed: false,
reason: "shell_operator_detected",
detectedOperator: shellOperator.operator,
}
}
// Check deny rules first (deny takes precedence)
if (this.config.deny) {
for (const pattern of this.config.deny) {
if (this.matchesPattern(command, pattern)) {
return { allowed: false, matchedPattern: pattern, reason: "denied" }
}
}
}
// Check allow rules
if (this.config.allow && this.config.allow.length > 0) {
for (const pattern of this.config.allow) {
if (this.matchesPattern(command, pattern)) {
return { allowed: true, matchedPattern: pattern, reason: "allowed" }
}
}
// Allow rules defined but no match = deny by default
return { allowed: false, reason: "no_match_deny_default" }
}
// No allow rules defined, and no deny matched = allow
return { allowed: true, reason: "no_config" }
}
/**
* Check if a command matches a wildcard pattern.
*
* Uses simple wildcard matching where `*` matches any characters (including `/` and newlines).
* This is different from file glob matching where `*` doesn't cross directory boundaries.
* For command permission matching, we want `*` to match any sequence of characters
* so that patterns like `gh pr comment *` match `gh pr comment 123 --body-file /tmp/file.txt`
* or commands with multiline arguments like `gh pr comment 123 --body "line1\nline2"`.
*
* Supported patterns:
* - `*` matches any sequence of characters (including / and newlines)
* - `?` matches exactly one character
*
* @param command - The command to check
* @param pattern - The wildcard pattern to match against
* @returns true if command matches the pattern
*/
private matchesPattern(command: string, pattern: string): boolean {
const regex = new RegExp(
"^" +
pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars
.replace(/\*/g, ".*") // * becomes .*
.replace(/\?/g, ".") + // ? becomes .
"$",
"s", // s flag enables dotAll (. matches newlines)
)
return regex.test(command)
}
/**
* Detect shell operators using shell-quote parser.
* This prevents command chaining/injection attacks like:
* gh pr view 123; rm -rf /
* gh pr view 123 && malicious_command
* gh pr view $(malicious_command)
*
* Operators inside quotes are allowed (they're literal characters):
* echo "hello; world" # OK - semicolon is inside quotes
*
* @param command - The command string to check
* @returns ShellOperatorMatch if an operator is found outside quotes, null otherwise
*/
private detectShellOperator(command: string): ShellOperatorMatch | null {
const dangerousCharMatch = this.detectDangerousCharsOutsideQuotes(command)
if (dangerousCharMatch) {
return dangerousCharMatch
}
try {
// Parse the command using shell-quote
// shell-quote returns an array where:
// - strings are regular arguments
// - objects with 'op' key are shell operators
// - objects with 'comment' key are comments
// - objects with 'pattern' key are glob patterns (we allow these)
const parsed = parse(command, (varName: string) => `$${varName}`)
// Check each parsed element for operators
for (const entry of parsed) {
const operatorMatch = this.checkParsedEntry(entry)
if (operatorMatch) {
return operatorMatch
}
}
return null
} catch {
// If parsing fails, be conservative and block the command
// This could indicate malformed shell syntax being used for injection
return { operator: "parse_error", description: "command parsing failed (potential injection)" }
}
}
/**
* Detect dangerous characters outside of quoted strings.
* This includes newlines, carriage returns, unicode line separators, and backticks.
*
* For newlines/carriage returns: They are safe inside ANY quotes (single or double)
* because they become literal characters in the argument value.
*
* For backticks: They are only safe inside SINGLE quotes because double quotes
* still allow command substitution.
*
* Examples:
* gh pr comment 123 --body "line1\nline2" -> ALLOWED (newline in quotes)
* gh pr comment 123\nrm -rf / -> BLOCKED (newline outside quotes)
* echo `date` -> BLOCKED (backtick outside quotes)
* echo "hello `date`" -> BLOCKED (backtick in double quotes - executes!)
* echo 'hello `date`' -> ALLOWED (backtick in single quotes - literal)
*
* @param command - The command string to check
* @returns ShellOperatorMatch if dangerous chars found outside appropriate quotes, null otherwise
*/
private detectDangerousCharsOutsideQuotes(command: string): ShellOperatorMatch | null {
let inSingleQuote = false
let inDoubleQuote = false
let isEscaped = false
for (let i = 0; i < command.length; i++) {
const char = command[i]
// If previous char was an unescaped backslash, this char is escaped
if (isEscaped) {
isEscaped = false
continue
}
// Check for escape sequence (only outside single quotes)
// In single quotes, backslashes are literal
if (char === "\\" && !inSingleQuote) {
isEscaped = true
continue
}
// Handle double quotes - we track them to know when single quotes are literal
if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote
continue
}
// Handle single quotes - only toggle when NOT inside double quotes
// Inside double quotes, single quotes are literal characters
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote
continue
}
const inAnyQuote = inSingleQuote || inDoubleQuote
// Check for newlines and carriage returns outside ANY quotes
// These are command separators when outside quotes
if (!inAnyQuote && LINE_SEPARATOR_REGEX.test(char)) {
return LINE_SEPARATOR_DESCRIPTIONS[char]
}
// Check for backticks outside SINGLE quotes only
// Backticks in double quotes ARE executed as command substitution in bash
if (char === "`" && !inSingleQuote) {
return { operator: "`", description: "command substitution (backtick)" }
}
}
return null
}
/**
* Check a parsed entry from shell-quote for dangerous operators.
*
* @param entry - A parsed entry from shell-quote
* @returns ShellOperatorMatch if dangerous operator found, null otherwise
*/
private checkParsedEntry(entry: ParseEntry): ShellOperatorMatch | null {
// null entries, string entries, glob patterns, and comments are safe
if (!entry || typeof entry === "string" || "pattern" in entry || "comment" in entry) {
return null
}
if (typeof entry.op === "string") {
if (this.isOperatorAllowed(entry.op)) {
return null
}
const description = OPERATOR_DESCRIPTIONS[entry.op] || `shell operator (${entry.op})`
return { operator: entry.op, description }
}
return null
}
}
+3
View File
@@ -0,0 +1,3 @@
export { CommandPermissionController } from "./CommandPermissionController"
export type { CommandPermissionConfig, PermissionValidationResult } from "./types"
export { COMMAND_PERMISSIONS_ENV_VAR } from "./types"
+32
View File
@@ -0,0 +1,32 @@
/**
* Configuration structure for command permissions from environment variable
*/
export interface CommandPermissionConfig {
allow?: string[] // Glob patterns for allowed commands
deny?: string[] // Glob patterns for denied commands
allowOperators?: string[] // Shell operators to allow (e.g., [">", ">>"] to allow file writing)
}
/**
* Result of a permission validation check
*/
export interface PermissionValidationResult {
allowed: boolean
matchedPattern?: string // The pattern that matched (for error messages)
reason: "no_config" | "allowed" | "denied" | "no_match_deny_default" | "shell_operator_detected"
detectedOperator?: string // The shell operator that was detected (for error messages)
}
/**
* Environment variable name for command permissions
*/
export const COMMAND_PERMISSIONS_ENV_VAR = "CLINE_COMMAND_PERMISSIONS"
/**
* Shell operators that indicate command chaining, piping, substitution, or redirection.
* These are security-sensitive because they can be used to bypass command restrictions.
*/
export interface ShellOperatorMatch {
operator: string
description: string
}
+3
View File
@@ -25,6 +25,9 @@ export const formatResponse = {
clineIgnoreError: (path: string) =>
`Access to ${path} is blocked by the .clineignore file settings. You must try to continue in the task without using this file, or ask the user to update the .clineignore file.`,
permissionDeniedError: (reason: string) =>
`Command execution blocked by CLINE_COMMAND_PERMISSIONS: ${reason}. You must try a different approach or ask the user to update the permission settings.`,
noToolsUsed: (usingNativeToolCalls: boolean) =>
usingNativeToolCalls
? "[ERROR] You did not use a tool in your previous response! Please retry with a tool use."
@@ -56,6 +56,7 @@ CAPABILITIES
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
====
@@ -54,6 +54,7 @@ CAPABILITIES
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the replace_in_file tool to implement changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
====
@@ -36,6 +36,7 @@ CAPABILITIES
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
====
@@ -56,6 +56,7 @@ CAPABILITIES
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
====
@@ -618,6 +618,7 @@ CAPABILITIES
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
====
@@ -582,6 +582,7 @@ CAPABILITIES
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the replace_in_file tool to implement changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
====
@@ -542,6 +542,7 @@ CAPABILITIES
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
====
@@ -598,6 +598,7 @@ CAPABILITIES
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
====
@@ -265,7 +265,7 @@
"type": "function",
"function": {
"name": "web_fetch",
"description": "Fetches and analyzes content from a specified URL.",
"description": "Fetches and analyzes content from a specified URL. IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.",
"strict": false,
"parameters": {
"type": "object",
@@ -295,7 +295,7 @@
"type": "function",
"function": {
"name": "web_search",
"description": "Performs a web search and returns relevant results with titles and URLs.",
"description": "Performs a web search and returns relevant results with titles and URLs. IMPORTANT: If an MCP-provided web search tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.",
"strict": false,
"parameters": {
"type": "object",
@@ -338,7 +338,7 @@
"type": "function",
"function": {
"name": "act_mode_respond",
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool when it adds value to the user experience, but always follow it with an actual tool call - never call it twice in a row.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
"strict": false,
"parameters": {
"type": "object",

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