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
212 changed files with 9151 additions and 5216 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add zai-glm-4.7 to Cerebras model list
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Adding support for responses api to OCA provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
add bash command permission system to cline
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add create-pull-request skill
-6
View File
@@ -1,6 +0,0 @@
---
"claude-dev": patch
---
Revert #8341 (0d04205dc) due to regressions in diff view/document truncation (see #8423, #8429).
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
feat(vercel-ai-gateway): add model refresh and improve reasoning support
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix the selection of remotely configured providers
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
show cline command permission denials in the CLI
+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
---
Reduce the number of network requests for the users profile
-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 }}"
+22 -7
View File
@@ -75,21 +75,36 @@ jobs:
if: steps.check_commits.outputs.skip != 'true' && steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
- name: Read nightly version
- name: Generate nightly version with timestamp
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
# Read version from cli/package-nightly.json
VERSION=$(node -p "require('./cli/package-nightly.json').version")
echo "Nightly version: $VERSION"
# 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: Setup cli/package.json for build
- name: Update cli/package.json with nightly version
if: steps.check_commits.outputs.skip != 'true'
run: |
# Copy nightly package.json to cli/package.json for build
cp cli/package-nightly.json cli/package.json
# 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'
+5
View File
@@ -41,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
-68
View File
@@ -1,68 +0,0 @@
{
"name": "cline",
"version": "1.0.8-nightly.29",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "cline-core.js",
"bin": {
"cline": "./bin/cline",
"cline-host": "./bin/cline-host"
},
"man": "./man/cline.1",
"scripts": {
"postinstall": "node postinstall.js"
},
"bundleDependencies": [
"@grpc/grpc-js",
"@grpc/reflection",
"better-sqlite3",
"grpc-health-check",
"open",
"vscode-uri"
],
"engines": {
"node": ">=20.0.0"
},
"keywords": [
"cline",
"claude",
"dev",
"mcp",
"openrouter",
"coding",
"agent",
"autonomous",
"chatgpt",
"sonnet",
"ai",
"llama",
"cli"
],
"author": {
"name": "Cline Bot Inc."
},
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline"
},
"homepage": "https://cline.bot",
"bugs": {
"url": "https://github.com/cline/cline/issues"
},
"dependencies": {
"@grpc/grpc-js": "^1.13.3",
"@grpc/reflection": "^1.0.4",
"better-sqlite3": "^12.2.0",
"grpc-health-check": "^2.0.2",
"open": "^10.1.2",
"vscode-uri": "^3.1.0"
},
"os": [
"darwin",
"linux"
],
"cpu": [
"x64",
"arm64"
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "1.0.8",
"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 {
+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.
+64 -38
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",
@@ -112,6 +112,7 @@
"@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",
@@ -1181,7 +1182,6 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2644,7 +2644,6 @@
"node_modules/@grpc/grpc-js": {
"version": "1.9.15",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.8",
"@types/node": ">=12.12.47"
@@ -3228,7 +3227,6 @@
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@hono/node-server": "^1.19.7",
"ajv": "^8.17.1",
@@ -3297,7 +3295,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -4913,7 +4910,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4",
@@ -4926,7 +4924,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4",
@@ -4939,7 +4938,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4",
@@ -4952,7 +4952,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4",
@@ -4965,7 +4966,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4",
@@ -4978,7 +4980,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4",
@@ -4991,7 +4994,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4",
@@ -5004,7 +5008,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4",
@@ -5017,7 +5022,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4",
@@ -5030,7 +5036,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4",
@@ -5043,7 +5050,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4",
@@ -5056,7 +5064,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4",
@@ -5069,7 +5078,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4",
@@ -5082,7 +5092,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4",
@@ -5095,7 +5106,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4",
@@ -5108,7 +5120,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.4",
@@ -5121,7 +5134,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4",
@@ -5134,7 +5148,8 @@
"optional": true,
"os": [
"openharmony"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4",
@@ -5147,7 +5162,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4",
@@ -5160,7 +5176,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4",
@@ -5173,7 +5190,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4",
@@ -5186,7 +5204,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@sap-ai-sdk/ai-api": {
"version": "2.1.0",
@@ -6749,7 +6768,8 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/get-folder-size": {
"version": "3.0.4",
@@ -6764,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,
@@ -6774,7 +6801,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -7478,7 +7504,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8245,7 +8270,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -9459,8 +9483,7 @@
},
"node_modules/devtools-protocol": {
"version": "0.0.1342118",
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/diff": {
"version": "5.2.0",
@@ -12436,7 +12459,6 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -12670,7 +12692,6 @@
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"license": "MPL-2.0",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -15551,6 +15572,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -15571,6 +15593,7 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -16238,6 +16261,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -17663,6 +17687,7 @@
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
@@ -17679,6 +17704,7 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -18042,7 +18068,6 @@
"version": "5.5.3",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -18302,6 +18327,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -18376,6 +18402,7 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -19071,7 +19098,6 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+3 -2
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"
@@ -349,7 +349,7 @@
"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",
@@ -416,6 +416,7 @@
"@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",
+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
}
+4
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 {
@@ -372,6 +374,8 @@ message UpdateSettingsRequest {
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
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`,
+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()
+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",
+1 -2
View File
@@ -241,7 +241,7 @@ export class OcaHandler implements ApiHandler {
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) {
@@ -303,7 +303,6 @@ export class OcaHandler implements ApiHandler {
}
async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
console.log("Uses Responses API")
const client = this.ensureClient()
// Convert messages to Responses API input format
+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",
+1 -1
View File
@@ -71,7 +71,7 @@ export class VercelAIGatewayHandler 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 {
+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",
+2 -2
View File
@@ -180,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 }
@@ -212,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 } }
: {}),
})
@@ -139,7 +139,7 @@ export async function createVercelAIGatewayStream(
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
...getOpenAIToolParams(tools),
...(model.id.includes("gemini") && geminiThinkingLevel
...(model.id.includes("gemini-3") && geminiThinkingLevel
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
: {}),
})
+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) {
@@ -63,10 +63,10 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
}
const modelInfo = model.model_info
const supportedApiList = modelInfo.supported_api_list ?? [CHAT_COMPLETIONS_API]
const apiFormat: ApiFormat = supportedApiList.includes(RESPONSES_API)
? ApiFormat.OPENAI_RESPONSES
: ApiFormat.OPENAI_CHAT
console.log(modelId, supportedApiList)
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,
@@ -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()
@@ -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",
@@ -296,7 +296,7 @@
},
{
"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.",
"parameters": {
"type": "OBJECT",
"properties": {
@@ -8,6 +8,7 @@ import { getFeedbackSection } from "./feedback"
import { getMcp } from "./mcp"
import { getObjectiveSection } from "./objective"
import { getRulesSection } from "./rules"
import { getSkillsSection } from "./skills"
import { getSystemInfo } from "./system_info"
import { getUpdatingTaskProgress } from "./task_progress"
import { getToolUseSection } from "./tool_use"
@@ -36,6 +37,7 @@ export function getSystemPromptComponents() {
id: SystemPromptSection.CAPABILITIES,
fn: getCapabilitiesSection,
},
{ id: SystemPromptSection.SKILLS, fn: getSkillsSection },
{ id: SystemPromptSection.RULES, fn: getRulesSection },
{ id: SystemPromptSection.OBJECTIVE, fn: getObjectiveSection },
{
@@ -0,0 +1,23 @@
import type { PromptVariant, SystemPromptContext } from "../types"
/**
* Generate the skills section for the system prompt.
*/
export async function getSkillsSection(_variant: PromptVariant, context: SystemPromptContext): Promise<string | undefined> {
const skills = context.skills
if (!skills || skills.length === 0) return undefined
const skillsList = skills.map((skill) => ` - "${skill.name}": ${skill.description}`).join("\n")
return `SKILLS
The following skills provide specialized instructions for specific tasks. When a user's request matches a skill description, use the use_skill tool to load and activate the skill.
Available skills:
${skillsList}
To use a skill:
1. Match the user's request to a skill based on its description
2. Call use_skill with the skill_name parameter set to the exact skill name
3. Follow the instructions returned by the tool`
}
@@ -8,6 +8,7 @@ export enum SystemPromptSection {
CLI_SUBAGENTS = "CLI_SUBAGENTS_SECTION",
TODO = "TODO_SECTION",
CAPABILITIES = "CAPABILITIES_SECTION",
SKILLS = "SKILLS_SECTION",
RULES = "RULES_SECTION",
SYSTEM_INFO = "SYSTEM_INFO_SECTION",
OBJECTIVE = "OBJECTIVE_SECTION",
@@ -28,7 +28,7 @@ const NATIVE_GPT_5: ClineToolSpec = {
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.
IMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.
IMPORTANT: 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.
Use this tool when:
- After reading files and before making any edits - explain your analysis and what changes you plan to make
@@ -16,6 +16,7 @@ export * from "./read_file"
export * from "./replace_in_file"
export * from "./search_files"
export * from "./use_mcp_tool"
export * from "./use_skill"
export * from "./web_fetch"
export * from "./web_search"
export * from "./write_to_file"
@@ -18,6 +18,7 @@ import { read_file_variants } from "./read_file"
import { replace_in_file_variants } from "./replace_in_file"
import { search_files_variants } from "./search_files"
import { use_mcp_tool_variants } from "./use_mcp_tool"
import { use_skill_variants } from "./use_skill"
import { web_fetch_variants } from "./web_fetch"
import { web_search_variants } from "./web_search"
import { write_to_file_variants } from "./write_to_file"
@@ -47,6 +48,7 @@ export function registerClineToolSets(): void {
...replace_in_file_variants,
...search_files_variants,
...use_mcp_tool_variants,
...use_skill_variants,
...web_fetch_variants,
...web_search_variants,
...write_to_file_variants,
@@ -0,0 +1,23 @@
import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import type { ClineToolSpec } from "../spec"
const id = ClineDefaultTool.USE_SKILL
const generic: ClineToolSpec = {
id,
variant: ModelFamily.GENERIC,
name: "use_skill",
description:
"Load and activate a skill by name. Skills provide specialized instructions for specific tasks. Use this tool ONCE when a user's request matches one of the available skill descriptions shown in the SKILLS section of your system prompt. After activation, follow the skill's instructions directly - do not call use_skill again.",
contextRequirements: (context) => context.skills !== undefined && context.skills.length > 0,
parameters: [
{
name: "skill_name",
required: true,
instruction: "The name of the skill to activate (must match exactly one of the available skill names)",
},
],
}
export const use_skill_variants = [generic]
+2
View File
@@ -7,6 +7,7 @@ import type { McpHub } from "@/services/mcp/McpHub"
import type { BrowserSettings } from "@/shared/BrowserSettings"
import type { FocusChainSettings } from "@/shared/FocusChainSettings"
import { ModelFamily } from "@/shared/prompts"
import type { SkillMetadata } from "@/shared/skills"
import { ClineDefaultTool } from "@/shared/tools"
import type { ClineToolSpec } from "./spec"
import { SystemPromptSection } from "./templates/placeholders"
@@ -96,6 +97,7 @@ export interface SystemPromptContext {
readonly ide: string
readonly supportsBrowserUse?: boolean
readonly mcpHub?: McpHub
readonly skills?: SkillMetadata[]
readonly focusChainSettings?: FocusChainSettings
readonly globalClineRulesFileInstructions?: string
readonly localClineRulesFileInstructions?: string
@@ -32,6 +32,7 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
SystemPromptSection.SYSTEM_INFO,
SystemPromptSection.OBJECTIVE,
SystemPromptSection.USER_INSTRUCTIONS,
SystemPromptSection.SKILLS,
)
.tools(
ClineDefaultTool.BASH,
@@ -51,6 +52,7 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
ClineDefaultTool.PLAN_MODE,
ClineDefaultTool.MCP_DOCS,
ClineDefaultTool.TODO,
ClineDefaultTool.USE_SKILL,
)
.placeholders({
MODEL_FAMILY: "devstral",
@@ -30,6 +30,10 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.SKILLS}}}
====
{{${SystemPromptSection.FEEDBACK}}}
====
@@ -38,11 +38,11 @@ export const config = createVariant(ModelFamily.GEMINI_3)
SystemPromptSection.EDITING_FILES,
SystemPromptSection.FEEDBACK,
SystemPromptSection.TODO,
SystemPromptSection.MCP,
SystemPromptSection.TASK_PROGRESS,
SystemPromptSection.SYSTEM_INFO,
SystemPromptSection.OBJECTIVE,
SystemPromptSection.USER_INSTRUCTIONS,
SystemPromptSection.SKILLS,
)
.tools(
ClineDefaultTool.BASH,
@@ -64,6 +64,7 @@ export const config = createVariant(ModelFamily.GEMINI_3)
ClineDefaultTool.MCP_DOCS,
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
)
.placeholders({
MODEL_FAMILY: ModelFamily.GEMINI_3,
@@ -22,6 +22,10 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.SKILLS}}}
====
{{${SystemPromptSection.EDITING_FILES}}}
@@ -45,6 +45,7 @@ export const config = createVariant(ModelFamily.GENERIC)
SystemPromptSection.SYSTEM_INFO,
SystemPromptSection.OBJECTIVE,
SystemPromptSection.USER_INSTRUCTIONS,
SystemPromptSection.SKILLS,
)
.tools(
ClineDefaultTool.BASH,
@@ -63,6 +64,7 @@ export const config = createVariant(ModelFamily.GENERIC)
ClineDefaultTool.MCP_DOCS,
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
)
.placeholders({
MODEL_FAMILY: "generic",
@@ -30,6 +30,10 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.SKILLS}}}
====
{{${SystemPromptSection.FEEDBACK}}}
====
@@ -33,6 +33,7 @@ export const config = createVariant(ModelFamily.GLM)
SystemPromptSection.SYSTEM_INFO,
SystemPromptSection.OBJECTIVE,
SystemPromptSection.USER_INSTRUCTIONS,
SystemPromptSection.SKILLS,
)
.tools(
ClineDefaultTool.BASH,
@@ -51,6 +52,7 @@ export const config = createVariant(ModelFamily.GLM)
ClineDefaultTool.MCP_DOCS,
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
)
.placeholders({
MODEL_FAMILY: ModelFamily.GLM,
@@ -14,6 +14,8 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
## {{${SystemPromptSection.CAPABILITIES}}}
## {{${SystemPromptSection.SKILLS}}}
## {{${SystemPromptSection.EDITING_FILES}}}
## {{${SystemPromptSection.TODO}}}
@@ -42,6 +42,7 @@ export const config = createVariant(ModelFamily.GPT_5)
SystemPromptSection.SYSTEM_INFO,
SystemPromptSection.OBJECTIVE,
SystemPromptSection.USER_INSTRUCTIONS,
SystemPromptSection.SKILLS,
)
.tools(
ClineDefaultTool.BASH,
@@ -62,6 +63,7 @@ export const config = createVariant(ModelFamily.GPT_5)
ClineDefaultTool.MCP_DOCS,
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
)
.placeholders({
MODEL_FAMILY: ModelFamily.GPT_5,
@@ -35,6 +35,10 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.SKILLS}}}
====
{{${SystemPromptSection.FEEDBACK}}}
====
@@ -34,6 +34,7 @@ export const config = createVariant(ModelFamily.HERMES)
SystemPromptSection.SYSTEM_INFO,
SystemPromptSection.OBJECTIVE,
SystemPromptSection.USER_INSTRUCTIONS,
SystemPromptSection.SKILLS,
)
.tools(
ClineDefaultTool.BASH,
@@ -53,6 +54,7 @@ export const config = createVariant(ModelFamily.HERMES)
ClineDefaultTool.MCP_DOCS,
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
)
.placeholders({
MODEL_FAMILY: "hermes",
@@ -12,6 +12,8 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
## {{${SystemPromptSection.CAPABILITIES}}}
## {{${SystemPromptSection.SKILLS}}}
## {{${SystemPromptSection.EDITING_FILES}}}
## {{${SystemPromptSection.TODO}}}
@@ -48,6 +48,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
SystemPromptSection.SYSTEM_INFO,
SystemPromptSection.OBJECTIVE,
SystemPromptSection.USER_INSTRUCTIONS,
SystemPromptSection.SKILLS,
)
.tools(
ClineDefaultTool.BASH,
@@ -69,6 +70,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
ClineDefaultTool.MCP_DOCS,
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
)
.placeholders({
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5_1,

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