Compare commits

..
Author SHA1 Message Date
Dominic Cooney cd6fabe8ae Debug harness improvements, some changes which break login. 2026-04-13 17:31:11 +09:00
Dominic Cooney f289b924d1 Reported OAuth improvements. 2026-04-11 12:42:56 +09:00
Dominic Cooney 7b9796cc75 docs: update CAVEATS.md with fixed issues
Mark as fixed: Add to Cline command, model change persistence,
MCP Servers tab loading, attached images not submitted.
2026-04-11 00:32:17 +09:00
Dominic Cooney a8c7978b5c fix: images in messages, model change persistence, MCP servers loading
Three related fixes in the SDK adapter layer:

1. Images in task/feedback messages: newTask and askResponse now
   include the images array in ClineMessage objects so attached
   images appear in the chat UI and are passed to the SDK session.

2. Model change during conversation: updateSettings now updates
   the in-memory apiConfiguration (not just disk) so model/provider
   changes take effect immediately for subsequent sessions.

3. MCP Servers tab loading: subscribeToMcpServers now sends initial
   server data as a typed message (mcpServers) instead of only via
   gRPC streaming response. The webview's dual-listen pattern picks
   up the typed message, avoiding proto encoding mismatches.
2026-04-11 00:31:23 +09:00
Dominic Cooney ce3b53e237 fix: Add to Cline right-click command works in SDK mode
The 'Add to Cline' command uses sendAddToInputEvent which relies on
gRPC streaming subscriptions. In SDK mode, these subscriptions are
never populated because the webview communicates via the SDK bridge
instead of classic gRPC streams.

- Add subscribeToAddToInput handling to WebviewGrpcBridge so the
  ChatView's gRPC subscription gets a request_id
- Add pushAddToInput method to WebviewGrpcBridge that sends both
  a gRPC streaming response and a typed message
- Add SDK bridge fallback in sendAddToInputEvent when no classic
  gRPC subscriptions are active
2026-04-11 00:24:35 +09:00
Dominic Cooney d837712366 fix: logout pushes auth status update to webview, login URL uses appBaseUrl
- WebviewGrpcBridge: track auth subscription requestId, add pushAuthStatus()
  method to send streaming auth updates to the webview's ClineAuthContext
- SdkController: add onPushAuthStatus callback, call it from clearClineAuth()
  so the webview transitions to the sign-in view after logout
- VscodeWebviewProvider: wire onPushAuthStatus callback to bridge.pushAuthStatus
- grpc-handler: handleAccountLoginClicked reads appBaseUrl from auth credentials
  instead of hardcoding https://app.cline.bot (supports staging/test environments)
- grpc-handler: remove dead buildAuthStatusData/pushAuthStatus methods that
  referenced non-existent authStatusSubscriptions map
2026-04-11 00:14:46 +09:00
Dominic Cooney d966dbf25b docs: update CAVEATS.md with fixed issues
Mark 5 issues as fixed (🟢):
- Account balance display (was ----)
- Environment dropdown persistence
- Logout button
- Sign-in button
- Cancel then can't enter new chat
2026-04-10 23:59:15 +09:00
Dominic Cooney 97fd423583 fix: SDK adapter account & state issues
- state-builder: read clineEnv from globalState and map to Environment enum
  so the webview's environment dropdown reflects the persisted value
- SdkController.updateSettings: handle clineEnv specially by persisting to
  globalState and clearing auth (tokens are environment-specific), matching
  the classic extension's updateSettings behavior
- SdkController.cancelTask: clear currentSession after abort so subsequent
  askResponse calls start a new task instead of sending to the aborted session
  (fixes: cancel then can't enter new chat)
- grpc-handler: implement accountLoginClicked (was STUB) to open the Cline
  login page in the browser (fixes: sign-in button does nothing)
2026-04-10 23:58:26 +09:00
Dominic Cooney 4344eab56a Add another bug. 2026-04-10 23:43:31 +09:00
Dominic Cooney ef943d49ff Update issues. 2026-04-10 23:41:55 +09:00
Dominic Cooney 154cce1f71 docs: add CLINE_DIR caveat to DEBUG-HARNESS.md
The debug harness spawns an isolated VSCode instance. Without
CLINE_DIR=~/.cline, the debugee won't have access to API keys
or provider configuration, causing inference to fail silently.
2026-04-10 23:00:45 +09:00
Dominic Cooney 3994ac4159 fix: use correct Cline API endpoints for account balance
The SdkController was using the wrong base URL (app.cline.bot instead
of api.cline.bot) and wrong endpoints (/api/account/credits instead of
/api/v1/users/{userId}/balance). It was also missing the required
'workos:' prefix on the auth token.

Fixed:
- User balance: GET api.cline.bot/api/v1/users/{userId}/balance
- Org balance: GET api.cline.bot/api/v1/organizations/{orgId}/balance
- Auth header: Bearer workos:{idToken}
- Balance value: divide by 100 (cents to dollars) matching the
  original ClineAccountService.fetchBalanceRPC() behavior
- Use appBaseUrl from auth credentials when set (for testing)

Updated integration tests to match the new endpoint format and
response structure.
2026-04-10 23:00:06 +09:00
Dominic Cooney 1e5af11df2 docs: update CAVEATS.md and FIXED.md with resolved issues #27-32
Marked as fixed: banners dismiss, favorites toggle, copy button overlap,
credit balance, logout, org switching/credit persistence.
2026-04-10 20:25:57 +09:00
Dominic Cooney 9a4fec5775 feat: account operations - credits, logout, org switching + integration tests
Credits: getUserCredits/getOrganizationCredits fetch from Cline API with stored auth token.
Logout: accountLogoutClicked clears auth credentials via clearClineAuthInfo().
Org switch: setUserOrganization updates active org in stored credentials.
Integration tests: 7 tests with mock HTTP server covering all account flows.
2026-04-10 20:24:38 +09:00
Dominic Cooney 33f5d5ff99 fix: banner dismissal, history favorites, and copy button overlap
Banner dismissal:
- state-builder now reads dismissedBanners from globalState (was hardcoded undefined)
- grpc-handler writes {bannerId, dismissedAt} objects (was plain strings)
- Both normalize legacy plain string entries for backward compatibility

History favorites:
- Fixed proto field name mismatch: handler reads taskId/isFavorited (was id/isFavorite)

Copy button:
- Moved bottom-right position from bottom-1 to bottom-2.5 to avoid overlap
- Added pb-4 padding to chat text content for code block clearance

Fixes CAVEATS issues: banners can't be dismissed, can't mark favorites, copy button obscured
2026-04-10 20:14:10 +09:00
Dominic Cooney bcb5457c04 Move fixed issues out of the way, add new list of issues. 2026-04-10 19:56:14 +09:00
Dominic Cooney 8dc3fe673d unbreak npx vsce package 2026-04-10 19:10:06 +09:00
Dominic Cooney d4a71eb269 fix: navigate to chat view after clicking history items
handleShowTaskWithId() in grpc-handler.ts loaded the task data and
pushed state but never fired navigate('chat') to tell the webview
to switch from the History view to the Chat view. Added
onNavigateCallback('chat') after showTaskWithId() completes.

Fixes both the History tab click and RECENT section click paths.
CAVEATS.md #26.
2026-04-10 19:10:06 +09:00
Dominic Cooney 9f98e0d9d2 feat: wire MCP tools into ClineCore sessions via extraTools
Load configured MCP servers from ~/.cline/data/settings/cline_mcp_settings.json
and pass their tools as extraTools to ClineCore sessions, making them visible
and invocable by the agent.

Implementation:
- getOrCreateMcpManager() reads MCP server registrations, creates an
  InMemoryMcpManager with @modelcontextprotocol/sdk client factory
- Supports stdio, streamableHttp, and SSE transport types
- MCP manager is cached across sessions (servers are long-lived processes)
- 30s timeout prevents MCP loading from blocking session start
- Individual server connection failures logged but don't block other servers

Verified via debug harness: agent sees kamibiki__kb_search, kb_status,
kb_index tools and successfully invokes kb_status returning real data.

Fixes CAVEATS.md issue #15.
2026-04-10 19:10:05 +09:00
Dominic Cooney 1938ca5744 Uncommitted agent scree. 2026-04-10 19:10:05 +09:00
candieduniverse 0c12f5220c Get the build working 2026-04-10 19:10:05 +09:00
candieduniverse 0fe207c018 Remove reportbug compatibility plumbing 2026-04-10 19:10:05 +09:00
candieduniverse 3ed2097c28 Retire unused legacy prompt families 2026-04-10 19:10:05 +09:00
candieduniverse 64d00e1344 Downgrade Focus Chain setting writes to compatibility mode 2026-04-10 19:10:05 +09:00
candieduniverse cdeca5fc50 Hide legacy Focus Chain setting from settings UI 2026-04-10 19:10:05 +09:00
candieduniverse 75d5b96927 Decouple task progress checklist from Focus Chain 2026-04-10 19:10:05 +09:00
candieduniverse 6a99ed7337 Expand feature removal migration backlog 2026-04-10 19:10:05 +09:00
candieduniverse f516b2d355 Clean up task progress tool spec docs 2026-04-10 19:10:05 +09:00
candieduniverse 546fb1f229 Rename prompt task progress constants 2026-04-10 19:10:05 +09:00
candieduniverse 95a4b910c8 Remove openFocusChainFile compatibility RPC 2026-04-10 19:10:05 +09:00
candieduniverse 53a5433e72 Remove dead focus chain telemetry hooks 2026-04-10 19:10:05 +09:00
candieduniverse 842a68d5d7 Hide dead focus chain file affordance 2026-04-10 19:10:05 +09:00
candieduniverse c42add2e1e Keep feedback prompts independent of focus chain 2026-04-10 19:10:05 +09:00
candieduniverse 55e2c1a772 Decouple task progress prompts from focus chain 2026-04-10 19:10:05 +09:00
candieduniverse ab8eaa00d0 Sync cleanup plan with completed doc slices 2026-04-10 19:10:05 +09:00
candieduniverse 6af7624142 Clean up remaining workflow product copy 2026-04-10 19:10:05 +09:00
candieduniverse e2661ffacc Reframe terminal settings as fallback controls 2026-04-10 19:10:05 +09:00
candieduniverse 28f6fb016e Clarify checkpoints as current migration-era behavior 2026-04-10 19:10:05 +09:00
candieduniverse 5720f272a0 Relabel focus chain settings as legacy 2026-04-10 19:10:05 +09:00
candieduniverse 22c2ec7570 Demote focus chain to legacy docs surface 2026-04-10 19:10:05 +09:00
candieduniverse f68fbbaf84 Demote memory bank to legacy docs surface 2026-04-10 19:10:05 +09:00
candieduniverse 76d36c4797 Update feature removal cleanup ledger progress 2026-04-10 19:10:04 +09:00
candieduniverse 796a6b90c9 Relabel workflow config UI as legacy 2026-04-10 19:10:04 +09:00
candieduniverse a4ba38a776 Reframe workflows as legacy compatibility docs 2026-04-10 19:10:04 +09:00
candieduniverse bea291576d Remove deep-planning and hide reportbug product surfaces 2026-04-10 19:10:04 +09:00
candieduniverse a72c07a5bf Create implementation plan doc 2026-04-10 19:10:04 +09:00
Dominic Cooney 97333c1f9e Uncommitted agent scree. 2026-04-10 19:10:04 +09:00
Dominic Cooney 66f7bca93e docs: update CAVEATS.md — mark all fixed issues, reorganize sections
All 24 issues now in Fixed section with condensed descriptions.
Only #15 (MCP tools not visible to agent) remains as Open Issue.
2026-04-10 19:10:04 +09:00
Dominic Cooney 7e44407344 fix: implement Cline model discovery and update recommended model text
- Implement handleRefreshClineModels handler in grpc-handler.ts that
  reads from disk cache first, then fetches from Cline API directly
  using globalThis.fetch (avoids ClineEnv initialization requirement)
- Convert API response to ModelInfo records with proper protobuf format
- Remove refreshClineModelsRpc from stub list
- Update ClineModelPicker.tsx fallback text from claude-sonnet-4.5 to
  claude-sonnet-4.6 to match current recommended model

Fixes #20 (model type-ahead search empty) and #21 (outdated model recommendation)
2026-04-10 19:10:04 +09:00
Dominic Cooney acd9fe5c6a fix(#14): omit placeholder size from delete task button tooltip
DeleteTaskButton tooltip now shows 'Delete Task' when taskSize is
unavailable, instead of 'Delete Task (size: --)'. When size data is
available, it shows 'Delete Task (12.4 KB)'.
2026-04-10 19:10:04 +09:00
Dominic Cooney f27a5648f6 fix(#5): suppress token usage bar when token counts are zero
ContextWindow.tsx now returns null when tokenData.used === 0, preventing
a brief flash of '0 / 200.0k' in the token usage bar when api_req_started
fires with zeroed counts before real data arrives.
2026-04-10 19:10:04 +09:00
Dominic Cooney 84591294b7 fix(#4): clear chat input immediately on send, before async gRPC calls
Previously setInputValue('') was called after await TaskServiceClient
newTask/askResponse completed, causing a visible delay where the typed
text lingered in the input field during the network round-trip.

Now the input, active quote, images, and files are cleared synchronously
before any async operations, giving immediate visual feedback.
2026-04-10 19:10:04 +09:00
Dominic Cooney f291104aad fix(#19): remove Terminal Execution Mode dropdown from settings
The Terminal Execution Mode dropdown (VS Code Terminal / Background Exec)
is legacy — only background terminal execution is supported. Removed the
dropdown, its handler (handleExecutionModeChange), and unused imports
(PlatformType, usePlatform) from TerminalSettingsSection.tsx.

Verified via debug harness: Terminal settings now shows Default Terminal
Profile, Shell integration timeout, Enable aggressive terminal reuse,
and Terminal output limit.
2026-04-10 19:10:04 +09:00
Dominic Cooney 46ebcf548d fix(#16): remove Workflows tab from Cline Rules modal
Workflows are no longer a feature. Remove the Workflows tab button,
description text, content section (remote/global/local workflow toggle
lists), and remote workflows banner condition from ClineRulesToggleModal.

The currentView state type is narrowed from 4 options to 3:
'rules' | 'hooks' | 'skills'. Workflow-related state/functions are kept
since they're still used by slash command components.

Verified via debug harness: modal now shows only Rules and Skills tabs.
2026-04-10 19:10:04 +09:00
Dominic Cooney 4bd8c3d422 docs: document #15 MCP tools investigation — ClineCore MCP wiring gap
Issue #15 (MCP tools missing) is caused by ClineCoreSession in
cline-session-factory.ts not passing MCP configuration through coreConfig
when calling host.start(). @clinebot/core has full MCP support
(InMemoryMcpManager, registerMcpServersFromSettingsFile) but the SDK
session factory never wires MCP servers into sessions.

Documented the investigation findings and three possible fix paths in
CAVEATS.md for future work.
2026-04-10 19:09:23 +09:00
Dominic Cooney 8d318dec47 fix(account): show logged-in state by handling subscribeToAuthStatusUpdate streaming
Three fixes:
1. webview-grpc-bridge.ts: Added explicit case for subscribeToAuthStatusUpdate
   in handleStreamingRequest(). Previously fell into the default no-op case,
   so the webview never received the auth state and always showed AccountWelcomeView
   ('Sign up with Cline') even when credentials existed on disk.

2. grpc-handler.ts: Added 'roles' field to getUserOrganizations response.
   Missing roles caused isAdminOrOwner() to crash on .findIndex().

3. helpers.ts: Added null safety (activeOrg.roles ?? []) in isAdminOrOwner()
   as defense against undefined roles field.
2026-04-10 19:09:23 +09:00
Dominic Cooney 7e86ec8766 fix(mcp): add null safety for marketplace catalog items in getMcpServerDisplayName
The getMcpServerDisplayName function crashed with 'Cannot read properties of
undefined (reading find)' when mcpMarketplaceCatalog.items was undefined.

This happened because the refreshMcpMarketplace gRPC stub returns an empty
object {}, which replaces the default { items: [] } state. When the Configure
tab renders ServerRow components that call getMcpServerDisplayName(), the
.items.find() call crashes and takes down the entire React app.

Fix: Add optional chaining (?.items?.find) to handle undefined catalog/items.
2026-04-10 19:09:22 +09:00
Dominic Cooney d3f3901549 docs: update CAVEATS.md - mark #22, #24, #25 as fixed 2026-04-10 19:09:22 +09:00
Dominic Cooney 397910a957 fix(sdk): history view empty - getTaskHistory returned wrong key name
The grpc-handler's handleGetTaskHistory returned { data: { history } }
but the webview's HistoryView component reads response.tasks (matching
the proto TaskHistoryArray.tasks field). Changed the return to
{ data: { tasks, totalCount } }.

Also added proper support for the GetTaskHistoryRequest filter params:
- favoritesOnly: filter to favorited tasks only
- searchQuery: case-insensitive text search on task descriptions
- sortBy: newest/oldest/mostExpensive/mostTokens sorting
- currentWorkspaceOnly: filter by workspace cwd

Maps HistoryItem fields to TaskItem proto shape (id, task, ts,
isFavorited, totalCost, tokensIn, tokensOut, cacheWrites, cacheReads).

Also fixes pre-existing lint issues in the file:
- isNaN → Number.isNaN
- console.warn → Logger.log
- Added cwd to GrpcHandlerDelegate interface

Fixes CAVEATS.md issue #24.
2026-04-10 19:09:22 +09:00
Dominic Cooney 2e0c975e98 fix: persist planActSeparateModelsSetting and autoApprovalSettings to globalState.json
updateSettings() and updateAutoApprovalSettings() in SdkController were routing
all writes through saveApiConfiguration(), which only writes keys in
ApiHandlerSettingsKeys or SecretKeys. User settings like planActSeparateModelsSetting
and autoApprovalSettings are USER_SETTINGS_FIELDS, so they were silently dropped.

Fix: updateSettings() now also writes each key directly to globalState.json via
writeGlobalStateKey(), and updateAutoApprovalSettings() bypasses saveApiConfiguration()
entirely, writing directly to globalState.json.

Also fixes pre-existing lint issues: unused isTaskRunning field, unused response
parameter, and console.error → Logger.error.

Fixes CAVEATS #22 and #25.
2026-04-10 19:09:22 +09:00
Dominic Cooney 519946a14c Some fixes, mo problems. 2026-04-10 19:09:22 +09:00
Dominic Cooney fd0402b2fc docs: document terminal settings radical simplification
The SDK uses background terminal execution, making most of the
TerminalSettingsSection obsolete. Added detailed table of what to
remove vs keep, list of related files to delete, and added to
Phase 2 checklist.
2026-04-10 19:09:22 +09:00
Dominic Cooney 0ab4deb91b docs: update CAVEATS #13 with real root cause and verification 2026-04-10 19:09:22 +09:00
Dominic Cooney 1dcddfada4 fix: implement getAvailableTerminalProfiles gRPC handler
The stub returned {data:{}}, so response.profiles was undefined. The
webview called setAvailableTerminalProfiles(undefined) which overwrote
the default [], then profilesToShow.map() crashed React — blank webview.

Now calls getAvailableTerminalProfiles() from utils/shell.ts which
returns platform-specific profiles (Default, zsh, bash on macOS).

Verified via debug harness:
- Handler returns {data:{profiles:[...]}} with 3 profiles
- Navigated to Settings > Terminal tab
- 'Default Terminal Profile' dropdown renders with Default/zsh/bash
- 'Shell integration timeout', 'terminal reuse' all visible
- React did not crash (rootEmpty: false)
2026-04-10 19:09:22 +09:00
Dominic Cooney 9cbed22c84 docs: add Caveats section to DEBUG-HARNESS.md
- Document 'Introducing Cline Kanban' promo overlay that obscures the
  sidebar on fresh launches, with dismissal commands
- Document CDP disconnect after window reload requiring full harness restart
2026-04-10 19:09:22 +09:00
Dominic Cooney 674a02a6b9 fix: terminal settings crash, workflows text, CAVEATS update
- Fix #13: Add missing availableTerminalProfiles: [] to state-builder.ts
  preventing React crash when TerminalSettingsSection calls .map() on undefined
- Fix #12: Update ClineRulesToggleModal tooltip from 'Manage Cline Rules &
  Workflows' to 'Manage Cline Rules', fix aria-label similarly
- Update CAVEATS.md: move issues #8-13 to Fixed section, only #4 and #5
  (minor UX issues) remain open
2026-04-10 19:09:22 +09:00
Dominic Cooney 4afa9d3ea3 fix: resolve broken gRPC handlers for SDK bridge
- Fix #8: Plus button now clears task via SDK bridge (clearSdkTask)
  and sends typed navigate('chat') message
- Fix #9: Implement searchFiles handler with real filesystem walk
  for @ mentions / autocomplete
- Fix #10: selectFiles returns { images: dataURL[], files: path[] }
  matching proto StringArrays (values1/values2) webview expects
- Fix #11: Plan/Act mode toggle converts proto enum values
  (0=PLAN, 1=ACT) to app string format
- Fix #12: Simplify chat placeholder text (remove 'workflows')
- Fix #13: scrollToSettings fires navigate callback via typed message

Top bar buttons (settings, history, chat, account, MCP, worktrees)
now send typed navigate messages alongside classic gRPC subscription
events, enabling navigation in SDK mode.
2026-04-10 19:09:22 +09:00
Dominic Cooney 22f300c945 More notes about missing grpcs. 2026-04-10 19:09:22 +09:00
Dominic Cooney 655917bd3a Agent doc updates. 2026-04-10 19:09:22 +09:00
Dominic Cooney 3f39d856be Agentic debugging tool improvements. 2026-04-10 19:09:22 +09:00
Dominic Cooney 68c37dc52d Agent documented bugs/caveats. 2026-04-10 19:09:22 +09:00
Dominic Cooney e57663aef9 Cline/Anthropic inference works. 2026-04-10 19:09:22 +09:00
Dominic Cooney a508e4dbab Start new task button appears on task completion; works. 2026-04-10 19:09:22 +09:00
Dominic Cooney 1cb691a496 Settings persist across restarts, Ollama output not garbled. 2026-04-10 19:09:22 +09:00
Dominic Cooney e4340039ea Local inference with ollama works, output is garbled (might be phi4-mini tho...) 2026-04-10 19:09:22 +09:00
Dominic Cooney d35c4ba893 cwd and workspaceRoot; suspicious string munging with Cline API keys. 2026-04-10 19:09:22 +09:00
Dominic Cooney 91abcea979 Picks up saved model settings. 2026-04-10 19:09:22 +09:00
Dominic Cooney 2df16d91e2 Wire ClineCoreSession but keys apparently missing. 2026-04-10 19:09:22 +09:00
Dominic Cooney 1ef9542b85 More agent work (some prevaricating.) 2026-04-10 19:09:22 +09:00
Dominic Cooney 1cb41432a4 Model displays and submitting a chat switches to thinking. 2026-04-10 19:09:22 +09:00
Dominic Cooney f0f08a4205 More agent work. 2026-04-10 19:09:21 +09:00
Dominic Cooney 4c248208e8 Phase 1, Unit 9 2026-04-10 19:09:21 +09:00
Dominic Cooney 446507ed06 Phase 1 units 4-8. 2026-04-10 19:09:21 +09:00
Dominic Cooney ca56aa9bfe Phase 1 to unit 3 2026-04-10 19:09:21 +09:00
Dominic Cooney 219eab9372 phase 1 unit 1: SDK foundation — deps, test infra, vitest config
- Add @clinebot/core, agents, llms, shared v0.0.28 as dependencies
- Add vitest as dev dependency for SDK adapter tests
- Create src/sdk/ directory structure with index.ts and test fixtures
- Add vitest.config.sdk.ts with Vite plugin to fix SDK package
  browser/development export condition issues
- Add npm scripts: test:sdk, test:sdk:watch
- Exclude src/sdk/ from mocha (.mocharc.json ignore)
- Regenerate prompt snapshots from phase 0 cleanup
- All 1349 existing unit tests pass, 5 new SDK foundation tests pass
- Classic extension build verified green
2026-04-10 19:09:21 +09:00
Dominic Cooney dbd8604bd4 phase 0: remove browser automation, shadow git checkpoints, focus chain, deep planning, /reportbug
Removed implementations and replaced with no-op stubs:
- Browser automation (BrowserSession, BrowserDiscovery, BrowserToolHandler)
- Shadow git checkpoints (CheckpointTracker, factory, initializer, etc.)
- Focus chain (FocusChainManager, tool variants)
- Deep planning (/deep-planning slash command)
- /reportbug slash command
- Go CLI artifacts (go.work.sum)

~15,500 lines deleted across 88 files. Build and type-check pass.
Debug harness is unaffected.
2026-04-10 19:09:21 +09:00
Dominic Cooney 6f6fbdaabd Add debug harness. 2026-04-10 19:09:21 +09:00
Dominic Cooney 5dc3bbde78 Cherry pick visibility, migration from last attempt. 2026-04-10 19:09:21 +09:00
3470 changed files with 417600 additions and 362261 deletions
-208
View File
@@ -1,208 +0,0 @@
---
name: cline-sdk
description: Comprehensive Cline SDK skill for building AI agents. Covers the Agent runtime, ClineCore sessions, custom tools, plugins, events, LLM providers, scheduling, multi-agent teams, and production deployment. Use for any task involving @cline/sdk or its sub-packages.
metadata:
references: agent, clinecore
---
# Cline SDK Skill
Consolidated skill for building AI agents with the Cline SDK. Use the decision trees below to find the right entry point and API surface, then load detailed references.
## Critical Rules
Follow these rules in all Cline SDK code:
1. Install with `npm install @cline/sdk`. The `@cline/sdk` package re-exports everything from `@cline/core`, `@cline/agents`, `@cline/llms`, and `@cline/shared`.
2. Requires Node.js 22 or later.
3. Use `createTool()` from `@cline/sdk` (or `@cline/shared`) to define tools. Tool names must be `snake_case`.
4. Return errors as structured data from tool `execute` functions. Throwing counts as a "mistake" against the agent's mistake limit.
5. Use `lifecycle: { completesRun: true }` on tools that should end the agent loop (e.g. a "submit answer" tool).
6. When using `ClineCore`, always call `dispose()` when done to clean up resources.
7. The standalone `Agent` and `ClineCore` have different event systems. For `Agent`: use `agent.subscribe()` to get `AgentRuntimeEvent` types (text streaming is `"assistant-text-delta"`, result text is `result.outputText`). For `ClineCore`: use `cline.subscribe()` to get `CoreSessionEvent` types (text streaming is `"chunk"` with `payload.type === "text"`, result text is `result.text`). There is no top-level `onEvent` field on `AgentRuntimeConfig` -- use `agent.subscribe()` or `hooks.onEvent` instead. Do not use event types like `"content_update"` or `"content_start"` with `agent.subscribe()` -- those are internal legacy types from the ClineCore adapter layer.
## How to Use This Skill
### Reference File Structure
The two main API surfaces (`Agent` and `ClineCore`) follow a 4-file pattern. Cross-cutting concepts are single-file guides.
Each main API surface in `./references/<api>/` contains:
| File | Purpose | When to Read |
|------|---------|--------------|
| `REFERENCE.md` | Overview, when to use, quick start | Always read first |
| `api.md` | Full API: classes, methods, config, types | Writing code |
| `patterns.md` | Common patterns, best practices | Implementation guidance |
| `gotchas.md` | Pitfalls, limitations, debugging | Troubleshooting |
Cross-cutting concepts in `./references/<concept>/` have `REFERENCE.md` as the entry point.
### Reading Order
1. Start with `REFERENCE.md` for your chosen API surface
2. Then read additional files relevant to your task:
- Writing agent code -> `api.md`
- Common patterns -> `patterns.md`
- Creating tools -> `tools/REFERENCE.md`
- Adding plugins/hooks -> `plugins/REFERENCE.md`
- Configuring LLM providers -> `providers/REFERENCE.md`
- Streaming events -> `events/REFERENCE.md`
- Deploying to production -> `production/REFERENCE.md`
- Scheduling agents -> `scheduling/REFERENCE.md`
- Multi-agent orchestration -> `multi-agent/REFERENCE.md`
- Debugging -> `gotchas.md`
### Example Paths
```
./references/agent/REFERENCE.md # Start here for lightweight agents
./references/clinecore/REFERENCE.md # Start here for full runtime
./references/agent/api.md # Agent class, config, methods
./references/tools/REFERENCE.md # Creating and using tools
./references/plugins/REFERENCE.md # Plugin system
./references/providers/REFERENCE.md # LLM provider configuration
```
## Quick Decision Trees
### "Which API surface should I use?"
```
Which API?
+-- I want a simple, stateless agent with custom tools
| +-- agent/ (Agent class from @cline/agents)
+-- I need session persistence, built-in tools, config discovery
| +-- clinecore/ (ClineCore from @cline/core)
+-- I want built-in file/shell/search/web tools
| +-- clinecore/ (has built-in tools; Agent does not)
+-- I want scheduled or recurring agents
| +-- clinecore/ (automation API)
+-- I need multi-process or multi-client session sharing
| +-- clinecore/ (hub-backed runtime)
+-- I'm building a browser-compatible agent
| +-- agent/ (no Node.js dependencies)
```
### "I need to create tools"
```
Tools?
+-- Define a custom tool with schema -> tools/REFERENCE.md
+-- Use built-in tools (bash, editor, read_files) -> tools/REFERENCE.md (built-in section)
+-- Control tool approval/policies -> tools/REFERENCE.md (policies section)
+-- Tool that ends the agent loop -> tools/REFERENCE.md (completion tools)
+-- Package tools as a reusable plugin -> plugins/REFERENCE.md
```
### "I need to handle events"
```
Events?
+-- Stream text/reasoning in real time -> events/REFERENCE.md
+-- Track token usage and costs -> events/REFERENCE.md
+-- Watch tool calls -> events/REFERENCE.md
+-- Detect completion/errors -> events/REFERENCE.md
+-- Hook into lifecycle stages -> plugins/REFERENCE.md
```
### "I need to configure a model provider"
```
Providers?
+-- Anthropic (Claude) -> providers/REFERENCE.md
+-- OpenAI (GPT) -> providers/REFERENCE.md
+-- Google (Gemini/Vertex) -> providers/REFERENCE.md
+-- AWS Bedrock -> providers/REFERENCE.md
+-- Mistral -> providers/REFERENCE.md
+-- OpenAI-compatible (vLLM, Together, etc.) -> providers/REFERENCE.md
+-- Custom/self-hosted provider -> providers/REFERENCE.md
```
### "I need plugins or hooks"
```
Plugins?
+-- Package tools + hooks together -> plugins/REFERENCE.md
+-- Observe tool calls (logging, metrics) -> plugins/REFERENCE.md
+-- Intercept lifecycle events -> plugins/REFERENCE.md
+-- Add system prompt rules -> plugins/REFERENCE.md
+-- Distribute via npm/git -> plugins/REFERENCE.md
```
### "I need multi-agent coordination"
```
Multi-agent?
+-- Spawn one-off background agents -> multi-agent/REFERENCE.md (sub-agents)
+-- Persistent cross-session teams -> multi-agent/REFERENCE.md (teams)
+-- Parent-child delegation -> multi-agent/REFERENCE.md (sub-agents)
+-- Peer-to-peer task board -> multi-agent/REFERENCE.md (teams)
```
### "I need scheduling or automation"
```
Scheduling?
+-- Recurring cron jobs -> scheduling/REFERENCE.md
+-- One-off scheduled tasks -> scheduling/REFERENCE.md
+-- Event-driven triggers -> scheduling/REFERENCE.md
+-- CLI schedule management -> scheduling/REFERENCE.md
```
### "I need to go to production"
```
Production?
+-- Error handling and status checks -> production/REFERENCE.md
+-- Cost control and token limits -> production/REFERENCE.md
+-- Observability (OpenTelemetry) -> production/REFERENCE.md
+-- Security and sandboxing -> production/REFERENCE.md
+-- Deployment patterns -> production/REFERENCE.md
```
### Troubleshooting Index
- Agent loop not stopping -> `tools/REFERENCE.md` (completion tools)
- Tool errors crashing the agent -> `agent/gotchas.md` or `clinecore/gotchas.md`
- Provider auth failures -> `providers/REFERENCE.md`
- Session not persisting -> `clinecore/gotchas.md`
- Token usage too high -> `production/REFERENCE.md` (cost control)
- Hub connection issues -> `clinecore/gotchas.md`
- Plugin not loading -> `plugins/REFERENCE.md`
- Events not firing -> `events/REFERENCE.md`
## Product Index
### API Surfaces
| API | Entry File | Description |
|-----|------------|-------------|
| Agent | `./references/agent/REFERENCE.md` | Lightweight stateless agent loop |
| ClineCore | `./references/clinecore/REFERENCE.md` | Full runtime with sessions, persistence, built-in tools |
### Cross-Cutting Concepts
| Concept | Entry File | Description |
|---------|------------|-------------|
| Tools | `./references/tools/REFERENCE.md` | Built-in and custom tool creation |
| Plugins | `./references/plugins/REFERENCE.md` | Extension system with hooks |
| Events | `./references/events/REFERENCE.md` | Real-time streaming events |
| Providers | `./references/providers/REFERENCE.md` | LLM provider configuration |
| Production | `./references/production/REFERENCE.md` | Deployment, security, observability |
| Scheduling | `./references/scheduling/REFERENCE.md` | Cron jobs and automation |
| Multi-Agent | `./references/multi-agent/REFERENCE.md` | Teams and sub-agents |
### Package Map
| Package | Purpose |
|---------|---------|
| `@cline/sdk` | Everything you need, install this one |
| `@cline/core` | Sessions, persistence, built-in tools, config, hub |
| `@cline/agents` | Stateless agent loop, tool orchestration, streaming |
| `@cline/llms` | LLM provider gateway |
| `@cline/shared` | Types, tool helpers, hook engine |
## Resources
Repository: https://github.com/cline/cline
SDK Source: https://github.com/cline/cline/tree/main/sdk
Documentation: https://docs.cline.bot/sdk/overview
Discord: https://discord.gg/cline
@@ -1,107 +0,0 @@
# Agent Runtime
The `Agent` class (also exported as `AgentRuntime`) is the lightweight, stateless agent loop from `@cline/agents`. It handles the core iteration cycle: send messages to an LLM, execute tool calls, collect results, and repeat until the task is done.
## When to Use Agent
| Use Agent when... | Use ClineCore instead when... |
|---|---|
| You want a simple agent with custom tools | You need built-in tools (bash, editor, etc.) |
| You want minimal dependencies | You need session persistence |
| You need browser compatibility | You need config discovery from `.cline/` |
| You're building a stateless worker | You need multi-process session sharing |
| You want full control over the runtime | You want batteries-included setup |
## Quick Start
```typescript
import { Agent } from "@cline/sdk"
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
const result = await agent.run("What is the capital of France?")
console.log(result.outputText)
```
## Core Concepts
The Agent operates in a loop:
1. Accept user input (string, message, or array of messages)
2. Build turn context (system prompt, messages, tools)
3. Call the LLM provider
4. If the model returns tool calls, execute them and loop back to step 3
5. If the model returns text without tool calls, the run completes
6. Emit events throughout for streaming
The agent is stateless in the sense that it does not persist anything to disk. Conversation history is held in memory and can be accessed via `snapshot()`.
## Key APIs
- `new Agent(config)` or `createAgent(config)` - Create an agent
- `agent.run(input)` - Start a run with user input
- `agent.continue(input?)` - Continue an existing conversation
- `agent.abort(reason?)` - Cancel an active run
- `agent.subscribe(listener)` - Listen to streaming events
- `agent.snapshot()` - Get current runtime state
- `agent.restore(messages)` - Replace message history
See `api.md` for full API details.
## Multi-Turn Conversations
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [],
})
const first = await agent.run("What is 2 + 2?")
console.log(first.outputText)
const second = await agent.continue("Now multiply that by 3")
console.log(second.outputText)
```
Use `agent.hasRun` to check if a run has already been executed, which determines whether to call `run()` or `continue()`.
## Event Streaming
Use `agent.subscribe()` to stream events in real time. Register the listener before calling `run()` to avoid missing early events.
There is no top-level `onEvent` field on the Agent config. For an async alternative, use `hooks.onEvent` (see `api.md` and `gotchas.md`).
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [],
})
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
const result = await agent.run("What is the capital of France?")
```
See `events/REFERENCE.md` for the full event type catalog.
## Next Steps
- `api.md` - Full Agent API reference
- `patterns.md` - Common patterns and best practices
- `gotchas.md` - Pitfalls and debugging
- `../tools/REFERENCE.md` - Creating custom tools
- `../events/REFERENCE.md` - Event system details
- `../providers/REFERENCE.md` - Provider configuration
@@ -1,231 +0,0 @@
# Agent API Reference
## Constructor
```typescript
import { Agent } from "@cline/sdk"
const agent = new Agent(config: AgentRuntimeConfig)
```
Also available via factory function:
```typescript
import { createAgent } from "@cline/sdk"
const agent = createAgent(config)
```
## AgentRuntimeConfig
Two config forms exist as a discriminated union:
### With Provider ID (recommended)
```typescript
interface AgentRuntimeConfigWithProvider {
providerId: string // e.g. "anthropic", "openai", "gemini"
modelId: string // e.g. "claude-sonnet-4-6", "gpt-5.5"
apiKey?: string // provider API key
baseUrl?: string // custom endpoint
headers?: Record<string, string>
systemPrompt?: string
tools?: AgentTool[]
initialMessages?: AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
hooks?: Partial<AgentRuntimeHooks>
plugins?: AgentPlugin[]
}
```
### With Pre-built Model
```typescript
interface AgentRuntimeConfigWithModel {
model: AgentModel // pre-built model from gateway
systemPrompt?: string
tools?: AgentTool[]
initialMessages?: AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
hooks?: Partial<AgentRuntimeHooks>
plugins?: AgentPlugin[]
}
```
Note: there is no top-level `onEvent` field on `AgentRuntimeConfig`. For event streaming, use `agent.subscribe()` or `hooks.onEvent` (see AgentRuntimeHooks below).
## Methods
### run(input)
Start the agent with user input. Returns when the agent loop completes.
```typescript
const result: AgentRunResult = await agent.run("Build a REST API")
```
Input can be a string, an `AgentMessage`, or an array of `AgentMessage[]`.
### continue(input?)
Continue an existing conversation with optional new input.
```typescript
const result = await agent.continue("Now add authentication")
```
### abort(reason?)
Cancel the currently active run.
```typescript
agent.abort("User cancelled")
```
### subscribe(listener)
Register a listener for streaming events.
```typescript
const unsubscribe = agent.subscribe((event: AgentRuntimeEvent) => {
// handle event
})
// Later: stop listening
unsubscribe()
```
### snapshot()
Get the current runtime state including message history.
```typescript
const state: AgentRuntimeStateSnapshot = agent.snapshot()
```
### restore(messages)
Replace the agent's message history.
```typescript
agent.restore(previousMessages)
```
### hasRun
Boolean property indicating whether `run()` has been called at least once.
```typescript
if (agent.hasRun) {
await agent.continue(input)
} else {
await agent.run(input)
}
```
## AgentRunResult
Returned by `run()` and `continue()`.
```typescript
interface AgentRunResult {
agentId: string
agentRole?: string
runId: string
status: "completed" | "aborted" | "failed"
iterations: number
outputText: string
messages: readonly AgentMessage[]
usage: AgentUsage
error?: Error
}
```
### Status Values
- `"completed"` - Agent finished normally
- `"aborted"` - Cancelled via `abort()`
- `"failed"` - Unrecoverable error
## AgentMessage
```typescript
interface AgentMessage {
id: string
role: "user" | "assistant" | "tool"
content: AgentMessagePart[]
createdAt: number
metadata?: Record<string, unknown>
modelInfo?: { id: string; provider: string; family?: string }
metrics?: {
inputTokens: number
outputTokens: number
cacheReadTokens?: number
cacheWriteTokens?: number
cost?: number
}
}
```
## AgentUsage
```typescript
interface AgentUsage {
inputTokens: number
outputTokens: number
cacheReadTokens: number
cacheWriteTokens: number
totalInputTokens: number
totalOutputTokens: number
totalCost?: number
}
```
## AgentRuntimeHooks
```typescript
interface AgentRuntimeHooks {
beforeRun?(context): AgentStopControl | undefined
afterRun?(context): void
beforeModel?(context): AgentBeforeModelResult | undefined
afterModel?(context): AgentStopControl | undefined
beforeTool?(context): AgentBeforeToolResult | undefined
afterTool?(context): AgentAfterToolResult | undefined
onEvent?(event: AgentRuntimeEvent): void | Promise<void>
}
```
Hooks can intercept and modify behavior at each stage. Return a stop control from `beforeRun`, `afterModel`, or `beforeTool` to halt the agent loop.
`hooks.onEvent` receives the same `AgentRuntimeEvent` types as `agent.subscribe()`, but hook callbacks are awaited (can be async), while `subscribe()` listeners are called synchronously. Use `subscribe()` for UI streaming and `hooks.onEvent` for async side effects like logging to an external service.
## AgentRuntimeStateSnapshot
```typescript
interface AgentRuntimeStateSnapshot {
messages: readonly AgentMessage[]
usage: AgentUsage
iterations: number
status: string
}
```
## Factory: createAgentRuntime
Lower-level factory that returns the same `Agent` class:
```typescript
import { createAgentRuntime } from "@cline/sdk"
const runtime = createAgentRuntime(config)
```
## See Also
- `REFERENCE.md` - Overview and quick start
- `patterns.md` - Common patterns
- `../tools/REFERENCE.md` - Tool creation
- `../events/REFERENCE.md` - Event types
- `../providers/REFERENCE.md` - Provider setup
@@ -1,134 +0,0 @@
# Agent Gotchas
## Agent Loop Never Stops
If the agent keeps iterating without completing:
- Make sure at least one tool has `lifecycle: { completesRun: true }` if you want the agent to explicitly finish.
- Without any tools, the agent will complete after the model returns text without tool calls.
- If using tools, ensure the system prompt guides the model toward calling the completion tool when done.
- Check that `completesRun` tools return successfully (not throwing errors).
## Tool Errors Count as Mistakes
When a tool's `execute` function throws an exception, the SDK counts it as a "mistake." After too many mistakes, the agent stops with a `mistake_limit` finish reason.
Instead, return errors as structured data:
```typescript
// Bad: throwing
execute: async (input) => {
throw new Error("File not found")
}
// Good: returning error data
execute: async (input) => {
return { error: "File not found", path: input.path }
}
```
## run() vs continue()
- Call `run()` for the first interaction. It sets up the conversation.
- Call `continue()` for subsequent messages. It appends to the existing conversation.
- Calling `run()` a second time resets the conversation history.
- Use `agent.hasRun` to check which method to call.
## Browser Compatibility
`@cline/agents` (and by extension, the `Agent` class) is browser-safe with no Node.js dependencies. However, `@cline/core` and `ClineCore` require Node.js 22+. If you import from `@cline/sdk`, you get everything including the Node-only code. For browser usage, import directly from `@cline/agents`:
```typescript
import { Agent } from "@cline/agents"
```
## No Top-Level onEvent on Agent Config
`AgentRuntimeConfig` does not have a top-level `onEvent` field. Passing `onEvent` to `new Agent({ onEvent: ... })` has no effect. There are two ways to receive events:
```typescript
// Option 1: subscribe() - synchronous, best for UI streaming
const agent = new Agent({ ...config })
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
// Option 2: hooks.onEvent - awaited, best for async side effects
const agent = new Agent({
...config,
hooks: {
onEvent: async (event) => {
if (event.type === "assistant-text-delta") {
await logToService(event.text)
}
},
},
})
```
Both receive the same `AgentRuntimeEvent` types. Prefer `subscribe()` for streaming UI.
## Event Listener Timing
Register event listeners via `subscribe()` before calling `run()`:
```typescript
// Good: subscribe before run
agent.subscribe(handler)
const result = await agent.run(input)
// Bad: subscribing after run starts loses early events
const promise = agent.run(input)
agent.subscribe(handler) // may miss events
```
## Tool Input Schema Matters
The model uses the tool's `inputSchema` to decide what arguments to pass. A vague or missing schema leads to incorrect tool calls.
- Use `z.enum()` for fixed value sets, not free-form strings
- Describe every property with `.describe()` in Zod or `description` in JSON Schema
- Include constraints (rate limits, max values) in the tool description
## Memory and Long Conversations
The Agent holds all messages in memory. For long-running conversations, memory usage grows with each turn. Consider:
- Using `ClineCore` with compaction for long sessions
- Periodically creating a new agent with a summary of the conversation
- Monitoring `result.usage.totalInputTokens` to track context growth
## Abort Signal Handling in Tools
Long-running tools should respect the abort signal:
```typescript
execute: async (input, context) => {
for (const item of items) {
if (context.abortSignal?.aborted) {
return { partial: results, aborted: true }
}
results.push(await process(item))
}
return { results }
}
```
## Provider API Key
If you get authentication errors, check:
- `apiKey` is set in the config or via environment variables
- The key matches the `providerId` (e.g., Anthropic key for `providerId: "anthropic"`)
- For OpenAI-compatible providers, both `apiKey` and `baseUrl` are set
See `../providers/REFERENCE.md` for provider-specific setup.
## See Also
- `api.md` - Full API reference
- `patterns.md` - Common patterns
- `../tools/REFERENCE.md` - Tool creation
- `../clinecore/REFERENCE.md` - Use ClineCore for persistence
@@ -1,258 +0,0 @@
# Agent Patterns
## Interactive CLI Agent
A multi-turn conversational agent in the terminal with streaming output:
```typescript
import { Agent } from "@cline/sdk"
import * as readline from "node:readline"
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant. Keep responses concise.",
tools: [],
})
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
function prompt(): void {
rl.question("\nYou: ", async (input) => {
const trimmed = input.trim()
if (!trimmed || trimmed === "exit") {
rl.close()
return
}
process.stdout.write("\nAssistant: ")
if (agent.hasRun) {
await agent.continue(trimmed)
} else {
await agent.run(trimmed)
}
process.stdout.write("\n")
prompt()
})
}
prompt()
```
## Conversational Agent (Slack Bot, Chat App)
Maintain per-thread agents with conversation memory:
```typescript
import { Agent } from "@cline/sdk"
const agents = new Map<string, Agent>()
async function handleMessage(threadId: string, message: string) {
let agent = agents.get(threadId)
if (!agent) {
agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a concise assistant.",
tools: [],
})
agents.set(threadId, agent)
}
const result = agent.hasRun
? await agent.continue(message)
: await agent.run(message)
return result.outputText
}
```
## Streaming UI
Build a real-time UI by handling events via `subscribe()`:
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [myTool],
})
agent.subscribe((event) => {
switch (event.type) {
case "assistant-text-delta":
ui.appendText(event.text)
break
case "assistant-message":
ui.endText()
break
case "turn-started":
ui.startTurn(event.iteration)
break
case "turn-finished":
if (event.toolCallCount > 0) ui.showToolCount(event.toolCallCount)
break
case "usage-updated":
ui.updateUsage(event.usage.inputTokens, event.usage.outputTokens)
break
}
})
const result = await agent.run("Hello!")
```
## Structured Output via Completion Tool
Use a tool with `completesRun: true` to extract structured data:
```typescript
import { Agent, createTool } from "@cline/sdk"
import { z } from "zod"
const submitReview = createTool({
name: "submit_review",
description: "Submit the final code review with structured feedback.",
inputSchema: z.object({
summary: z.string(),
issues: z.array(z.object({
file: z.string(),
line: z.number(),
severity: z.enum(["error", "warning", "info"]),
message: z.string(),
})),
approved: z.boolean(),
}),
lifecycle: { completesRun: true },
execute: async (input) => input,
})
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "Review the code diff and submit structured feedback.",
tools: [submitReview],
})
const result = await agent.run(diffContent)
const review = result.toolCalls.find(tc => tc.name === "submit_review")
console.log(review?.output)
```
## Agent with Abort/Timeout
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "Analyze this data.",
tools: [],
})
const timeout = setTimeout(() => agent.abort("Timeout"), 30_000)
try {
const result = await agent.run(data)
if (result.status === "aborted") {
console.log("Agent was aborted")
} else {
console.log(result.outputText)
}
} finally {
clearTimeout(timeout)
}
```
## Agent with Plugins
```typescript
import { Agent } from "@cline/sdk"
import type { AgentPlugin } from "@cline/sdk"
const loggingPlugin: AgentPlugin = {
name: "logging",
manifest: { capabilities: ["hooks"] },
setup() {},
hooks: {
beforeTool({ toolCall }) {
console.log(`Calling tool: ${toolCall.toolName}`)
},
afterRun({ result }) {
console.log(`Completed in ${result.iterations} iterations`)
},
},
}
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [myTool],
plugins: [loggingPlugin],
})
```
## Restoring State Across Sessions
Save and restore agent state manually:
```typescript
// Save state
const snapshot = agent.snapshot()
const serialized = JSON.stringify(snapshot.messages)
// Later: restore
const agent2 = new Agent({ ...config })
const messages = JSON.parse(serialized)
agent2.restore(messages)
const result = await agent2.continue("Continue where we left off")
```
For automatic persistence, use `ClineCore` instead.
## Pre-Built Model via Gateway
For advanced provider configuration:
```typescript
import { Agent } from "@cline/sdk"
import { createGateway } from "@cline/llms"
const gateway = createGateway({
providerConfigs: [
{ providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
{ providerId: "openai", apiKey: process.env.OPENAI_API_KEY },
],
})
const model = gateway.createAgentModel({
providerId: "anthropic",
modelId: "claude-opus-4-7",
})
const agent = new Agent({
model,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
```
## See Also
- `api.md` - Full API reference
- `gotchas.md` - Common pitfalls
- `../tools/REFERENCE.md` - Creating tools
- `../plugins/REFERENCE.md` - Plugin system
@@ -1,131 +0,0 @@
# ClineCore Runtime
`ClineCore` is the full-featured runtime from `@cline/core`. It wraps the `Agent` loop with session persistence, built-in tools (bash, editor, file reading, search, web fetch), config discovery, plugin loading, and optional hub-backed multi-process support.
## When to Use ClineCore
| Use ClineCore when... | Use Agent instead when... |
|---|---|
| You need built-in tools (bash, editor, etc.) | You only need custom tools |
| You want session persistence to disk | Stateless is fine |
| You need config discovery from `.cline/` dirs | You handle config yourself |
| You want scheduled/automated agents | You don't need scheduling |
| You need multi-client session sharing | Single-process is fine |
| You're building a full application | You want minimal dependencies |
## Quick Start
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
const session = await cline.start({
prompt: "Set up CI with GitHub Actions",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
cwd: "/path/to/project",
enableTools: true,
},
})
console.log(session.result?.text)
await cline.dispose()
```
## Core Concepts
### Sessions
Every `cline.start()` call creates a session with a unique ID. Sessions persist their messages and metadata to SQLite. You can list, read, resume, and delete sessions.
### Built-in Tools
ClineCore provides these tools automatically when `enableTools: true`:
| Tool | Description |
|------|-------------|
| `bash` | Execute shell commands |
| `editor` | Edit files |
| `read_files` | Read file contents |
| `apply_patch` | Apply unified diffs |
| `search` | Search file contents and structure |
| `fetch_web` | HTTP requests and web content |
### Config Discovery
ClineCore watches `.cline/` directories for:
- Rules (system prompt additions)
- Skills (domain knowledge)
- Workflows (multi-step procedures)
- Hooks (lifecycle logic)
- Plugins (tool + hook bundles)
- MCP servers (external tool providers)
### Backend Modes
| Mode | Description |
|------|-------------|
| `"auto"` (default) | Tries to connect to a local hub; falls back to in-process if unavailable |
| `"local"` | In-process execution, local SQLite storage, no hub |
| `"hub"` | Requires a compatible local WebSocket hub; fails if unavailable |
| `"remote"` | Connects to an explicit remote hub endpoint |
The default mode is `"auto"`. For simple scripts and CLI tools, `"local"` avoids hub discovery overhead. Hub mode enables multi-client session sharing (e.g., a dashboard watching a running session from another process).
## Key APIs
- `ClineCore.create(options)` - Create and initialize
- `cline.start(input)` - Start a new session
- `cline.send({ sessionId, prompt })` - Send follow-up message
- `cline.subscribe(listener)` - Listen to session events
- `cline.list()` - List sessions
- `cline.get(sessionId)` - Get session metadata
- `cline.readMessages(sessionId)` - Read persisted messages
- `cline.getAccumulatedUsage(sessionId)` - Token/cost totals
- `cline.abort(sessionId)` - Abort a session
- `cline.delete(sessionId)` - Delete a session
- `cline.dispose()` - Clean up resources
See `api.md` for full API details.
## Event Streaming
`cline.subscribe()` emits `CoreSessionEvent` types. These are different from the `AgentRuntimeEvent` types emitted by the standalone `Agent` class -- see `../events/REFERENCE.md` for the full comparison.
```typescript
cline.subscribe((event) => {
switch (event.type) {
case "chunk":
if (event.payload.type === "text") {
process.stdout.write(event.payload.text)
}
break
case "ended":
console.log(`Session ended: ${event.payload.finishReason}`)
break
}
})
```
ClineCore results use `AgentResult` with `.text` (not `.outputText` like the standalone Agent's `AgentRunResult`).
## Session Persistence
Sessions are stored at:
```
~/.cline/data/sessions/
sessions.db # SQLite database
[session-id].json # Message history
```
## Next Steps
- `api.md` - Full ClineCore API reference
- `patterns.md` - Common patterns and best practices
- `gotchas.md` - Pitfalls and debugging
- `../tools/REFERENCE.md` - Custom tool creation
- `../plugins/REFERENCE.md` - Plugin system
- `../scheduling/REFERENCE.md` - Scheduled agents
@@ -1,304 +0,0 @@
# ClineCore API Reference
## Creating ClineCore
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create(options: ClineCoreOptions)
```
### ClineCoreOptions
```typescript
interface ClineCoreOptions {
clientName: string // identifies your app
distinctId?: string // user/instance identifier
backendMode?: "auto" | "local" | "hub" | "remote"
hub?: HubOptions
remote?: RemoteOptions
capabilities?: RuntimeCapabilities
toolPolicies?: Record<string, ToolPolicy>
automation?: boolean | ClineCoreAutomationOptions
fetch?: typeof fetch
}
```
### RuntimeCapabilities
```typescript
interface RuntimeCapabilities {
requestToolApproval?: (request: ToolApprovalRequest) => Promise<ToolApprovalResult>
// ... other capability callbacks
}
```
## Starting Sessions
### start(input)
```typescript
const session = await cline.start(input: ClineCoreStartInput)
```
Returns a `StartSessionResult`:
```typescript
interface StartSessionResult {
sessionId: string
manifest: SessionManifest
manifestPath: string
messagesPath: string
result?: AgentResult
}
```
### ClineCoreStartInput
```typescript
interface ClineCoreStartInput {
prompt: string
config: CoreSessionConfig
source?: string
interactive?: boolean
sessionMetadata?: Record<string, unknown>
initialMessages?: AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
capabilities?: RuntimeCapabilities
}
```
### CoreSessionConfig
```typescript
interface CoreSessionConfig {
cwd?: string // working directory
providerId: string // LLM provider
modelId: string // model identifier
apiKey?: string // provider API key
systemPrompt?: string // custom system prompt
tools?: readonly AgentTool[] // additional custom tools
enableTools?: boolean // enable built-in tools
hooks?: Partial<AgentRuntimeHooks> // runtime hooks
extensions?: AgentPlugin[] // plugins loaded inline
pluginPaths?: string[] // paths to plugin packages
extensionLoading?: "isolated" | "direct"
extensionContext?: { // context passed to plugin setup()
workspace?: { rootPath: string; cwd: string }
}
checkpointConfig?: CoreCheckpointConfig
compactionConfig?: CoreCompactionConfig
telemetry?: ITelemetryService
logger?: BasicLogger
enableSpawnAgent?: boolean // enable sub-agent spawning
enableAgentTeams?: boolean // enable team coordination
teamName?: string // team identifier
}
```
`extensions` passes plugin objects directly. `pluginPaths` points to directories with `package.json` containing a `cline.plugins` field. Set `extensionContext.workspace` so plugins receive `ctx.workspaceInfo` in their `setup()` call -- without it, `ctx.workspaceInfo` is undefined.
## Follow-Up Messages
### send({ sessionId, prompt })
Send a follow-up message to an existing session:
```typescript
const result = await cline.send({
sessionId: session.sessionId,
prompt: "Now add authentication",
})
```
Returns `AgentResult | undefined`.
## Event Subscription
### subscribe(listener, options?)
```typescript
const unsubscribe = cline.subscribe(
(event: CoreSessionEvent) => {
// handle events
},
{ sessionId: "optional-filter" }
)
```
### CoreSessionEvent
```typescript
type CoreSessionEvent =
| { type: "chunk"; payload: SessionChunkEvent }
| { type: "agent_event"; payload: { sessionId: string, event: AgentEvent } }
| { type: "ended"; payload: SessionEndedEvent }
| { type: "team_progress"; payload: SessionTeamProgressEvent }
| { type: "status"; payload: { sessionId: string, status: string } }
| { type: "hook"; payload: SessionToolEvent }
```
## Session Management
### list(limit?, options?)
```typescript
const sessions: SessionRecord[] = await cline.list(50)
```
### get(sessionId)
```typescript
const session: SessionRecord = await cline.get(sessionId)
```
### readMessages(sessionId)
```typescript
const messages: AgentMessage[] = await cline.readMessages(sessionId)
```
### getAccumulatedUsage(sessionId)
```typescript
const usage = await cline.getAccumulatedUsage(sessionId)
// usage.usage - root agent only
// usage.aggregateUsage - root + subagents/teammates
```
### update(sessionId, updates)
```typescript
await cline.update(sessionId, { title: "New title" })
```
### abort(sessionId, reason?)
```typescript
await cline.abort(sessionId, "User cancelled")
```
### stop(sessionId)
```typescript
await cline.stop(sessionId)
```
### delete(sessionId)
```typescript
await cline.delete(sessionId)
```
### restore(input)
Restore a session from a checkpoint:
```typescript
await cline.restore({ sessionId, checkpointId })
```
### dispose(reason?)
Clean up all resources. Always call this when done:
```typescript
await cline.dispose("Shutting down")
```
## AgentResult
Returned by session operations:
```typescript
interface AgentResult {
text: string
usage: LegacyAgentUsage
messages: MessageWithMetadata[]
toolCalls: ToolCallRecord[]
iterations: number
finishReason: "completed" | "max_iterations" | "aborted" | "mistake_limit" | "error"
model: { id: string; provider: string; info?: ModelInfo }
startedAt: Date
endedAt: Date
durationMs: number
}
```
## Tool Policies
Control tool access at the session level:
```typescript
const session = await cline.start({
prompt: "Review the code",
config: { ... },
toolPolicies: {
read_files: { autoApprove: true },
bash: { autoApprove: false },
editor: { enabled: false },
},
})
```
### ToolPolicy
```typescript
interface ToolPolicy {
enabled?: boolean // false = tool is hidden from the model
autoApprove?: boolean // false = requires approval callback
}
```
## Interactive Approval
```typescript
const cline = await ClineCore.create({
clientName: "my-app",
capabilities: {
requestToolApproval: async (request) => {
console.log(`Tool: ${request.toolName}, Input: ${JSON.stringify(request.input)}`)
const approved = await askUser(`Allow ${request.toolName}?`)
return { approved }
},
},
})
```
## Automation API
When `automation` is enabled in `ClineCore.create()`:
```typescript
const cline = await ClineCore.create({
clientName: "my-app",
automation: true,
})
// Access automation methods
cline.automation.start()
cline.automation.stop()
cline.automation.reconcile(specs)
cline.automation.ingestEvent(event)
cline.automation.listEvents()
cline.automation.listSpecs()
cline.automation.listRuns()
```
## Settings API
```typescript
// Read settings
const settings = await cline.settings.list()
// Toggle tools, plugins, MCP servers
await cline.settings.toggle({ type: "tool", name: "bash", enabled: true })
```
## See Also
- `REFERENCE.md` - Overview and quick start
- `patterns.md` - Common patterns
- `gotchas.md` - Pitfalls
- `../tools/REFERENCE.md` - Tool creation
- `../plugins/REFERENCE.md` - Plugin system
@@ -1,148 +0,0 @@
# ClineCore Gotchas
## Always Call dispose()
`ClineCore` holds resources (file watchers, database connections, hub connections). Failing to call `dispose()` can leave orphan processes and file locks.
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
try {
// ... use cline
} finally {
await cline.dispose()
}
```
## Node.js 22 Required
ClineCore and `@cline/core` require Node.js 22 or later. If you're on an older version, you'll get runtime errors. Check with `node --version`.
## Session Config vs Global Config
Tool policies can be set at two levels:
- Global: in `ClineCore.create({ toolPolicies })` -- applies to all sessions
- Per-session: in `cline.start({ toolPolicies })` -- overrides global for that session
Per-session policies take precedence.
## enableTools Must Be Explicit
Built-in tools (bash, editor, read_files, etc.) are not available unless you set `enableTools: true` in the session config:
```typescript
await cline.start({
prompt: "Read package.json",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
enableTools: true, // required for built-in tools
},
})
```
Without this, the agent only has access to custom tools you provide via `config.tools`.
## cwd Matters for Built-in Tools
Built-in tools like `bash`, `editor`, and `read_files` operate relative to `config.cwd`. If not set, they use the process working directory. Always set it explicitly for predictable behavior:
```typescript
config: {
cwd: "/absolute/path/to/project",
// ...
}
```
## Hub Startup Latency
With `backendMode: "auto"`, the first session may be slow if a hub daemon needs to be spawned. For immediate responsiveness:
- Use `backendMode: "local"` for in-process execution (fastest startup)
- Pre-warm the hub with `cline hub ensure` CLI command
- Accept the one-time startup cost and let subsequent sessions reuse the hub
## Session Storage Location
Sessions are stored at `~/.cline/data/sessions/`. This includes:
- `sessions.db` - SQLite database with session metadata
- `[session-id].json` - Individual message history files
If you're running in a container or ephemeral environment, these paths may not persist across restarts.
## requestToolApproval Blocks Execution
When a tool policy has `autoApprove: false` and you provide a `requestToolApproval` callback, the agent loop blocks until your callback resolves. If your callback never resolves (e.g., waiting for user input that never comes), the session hangs.
For automated pipelines, either:
- Set all tools to `autoApprove: true`
- Implement a timeout in your approval callback
## Plugin Discovery Paths
ClineCore discovers plugins from:
- Global: `~/.cline/plugins/`
- Workspace: `.cline/plugins/`
For SDK consumers, pass plugins via `extensions: [plugin]` or `pluginPaths: ["./path"]` in the session config.
If a plugin isn't loading, verify:
- The file is in one of the discovery directories, or passed via `extensions`/`pluginPaths`
- The file exports a default plugin object with a non-empty `manifest.capabilities` array
- Every `api.register*` call in `setup()` has a matching capability declared
- If `hooks` is present on the plugin, `"hooks"` is in `capabilities`
## extensionContext.workspace Is Required for Plugins
If your plugins use `ctx.workspaceInfo` (e.g., to resolve workspace paths), you must set `extensionContext.workspace` in the session config. Without it, `ctx.workspaceInfo` is undefined:
```typescript
await cline.start({
config: {
extensions: [myPlugin],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
})
```
The CLI sets this automatically, but SDK consumers must set it explicitly.
## send() Requires an Active Session
`cline.send()` only works on sessions that are still active. If a session has already completed, `send()` may return `undefined` or fail. Check session status with `cline.get(sessionId)` first.
## Result May Be Undefined
`session.result` can be `undefined` if the session was started but hasn't completed yet (e.g., in a non-blocking hub mode). Check for this:
```typescript
const session = await cline.start({ ... })
if (session.result) {
console.log(session.result.text)
} else {
console.log("Session started but not yet complete")
}
```
## Compaction and Long Sessions
For long-running sessions, message history grows and eventually exceeds the model's context window. ClineCore handles this via compaction, which summarizes older messages. Configure it via `compactionConfig`:
```typescript
config: {
compactionConfig: {
strategy: "summarize",
// ...
},
}
```
The default strategy works for most cases, but extremely long sessions may benefit from tuning.
## See Also
- `api.md` - Full API reference
- `patterns.md` - Common patterns
- `../agent/gotchas.md` - Agent-level gotchas
- `../tools/REFERENCE.md` - Tool troubleshooting
- `../providers/REFERENCE.md` - Provider troubleshooting
@@ -1,279 +0,0 @@
# ClineCore Patterns
## Basic Session with Built-in Tools
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
const session = await cline.start({
prompt: "Read package.json and summarize the dependencies",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
cwd: process.cwd(),
enableTools: true,
},
})
console.log(session.result?.text)
await cline.dispose()
```
## Streaming Session with UI Updates
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
cline.subscribe((event) => {
switch (event.type) {
case "chunk":
if (event.payload.type === "text") {
ui.appendText(event.payload.text)
}
break
case "ended":
ui.showComplete(event.payload.finishReason)
break
}
})
await cline.start({
prompt: "Refactor the auth module",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: "/path/to/project",
enableTools: true,
},
})
```
## Multi-Turn Session
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
const session = await cline.start({
prompt: "Create a new Express server",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: "/path/to/project",
enableTools: true,
},
})
// Follow-up
const result = await cline.send({
sessionId: session.sessionId,
prompt: "Now add a health check endpoint",
})
console.log(result?.text)
await cline.dispose()
```
## Tiered Permission Model
Auto-approve reads, require approval for writes:
```typescript
const cline = await ClineCore.create({
clientName: "my-app",
toolPolicies: {
read_files: { autoApprove: true },
search: { autoApprove: true },
fetch_web: { autoApprove: true },
bash: { autoApprove: false },
editor: { autoApprove: false },
apply_patch: { autoApprove: false },
},
capabilities: {
requestToolApproval: async (request) => {
const approved = await promptUser(
`Allow ${request.toolName}?\n${JSON.stringify(request.input, null, 2)}`
)
return { approved }
},
},
})
```
## Custom Tools Alongside Built-ins
```typescript
import { ClineCore, createTool } from "@cline/sdk"
import { z } from "zod"
const deployTool = createTool({
name: "deploy",
description: "Deploy the application to the specified environment.",
inputSchema: z.object({
environment: z.enum(["staging", "production"]),
}),
execute: async (input) => {
const result = await runDeployment(input.environment)
return { url: result.url, status: "deployed" }
},
})
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
prompt: "Deploy the app to staging",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
enableTools: true,
tools: [deployTool],
},
})
```
## Session with Plugins
Load plugins inline with `extensions` and provide workspace context so plugins can access `ctx.workspaceInfo`:
```typescript
import { ClineCore } from "@cline/sdk"
import myPlugin from "./my-plugin"
const cline = await ClineCore.create({
clientName: "my-app",
backendMode: "local",
})
await cline.start({
prompt: "Do the thing my plugin enables",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
enableTools: true,
extensions: [myPlugin],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
})
await cline.dispose()
```
For directory-based plugin packages, use `pluginPaths` instead:
```typescript
config: {
pluginPaths: ["./my-cline-plugin"],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
}
```
See `../plugins/REFERENCE.md` for the full plugin authoring guide.
## Session Listing and Replay
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
// List recent sessions
const sessions = await cline.list(10)
for (const session of sessions) {
console.log(`${session.id}: ${session.title}`)
}
// Read messages from a past session
const messages = await cline.readMessages(sessions[0].id)
for (const msg of messages) {
console.log(`[${msg.role}] ${msg.content}`)
}
// Check usage
const usage = await cline.getAccumulatedUsage(sessions[0].id)
console.log(`Total tokens: ${usage.aggregateUsage.totalInputTokens + usage.aggregateUsage.totalOutputTokens}`)
```
## Graceful Shutdown
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
process.on("SIGTERM", async () => {
await cline.dispose("SIGTERM received")
process.exit(0)
})
// Run sessions...
```
## Stateless Worker Pattern
For request/response workloads (API endpoints, queue consumers):
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({
clientName: "worker",
backendMode: "local",
})
async function handleRequest(prompt: string, workspace: string) {
const session = await cline.start({
prompt,
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: workspace,
enableTools: true,
},
})
return {
text: session.result?.text,
usage: session.result?.usage,
sessionId: session.sessionId,
}
}
```
## Hub-Backed Multi-Client
Multiple clients can attach to the same session:
```typescript
// Process 1: start session
const cline = await ClineCore.create({
clientName: "backend",
backendMode: "hub",
})
const session = await cline.start({
prompt: "Long running refactor task",
config: { ... },
})
// Process 2: attach and stream events
const viewer = await ClineCore.create({
clientName: "dashboard",
backendMode: "hub",
})
viewer.subscribe((event) => {
dashboard.render(event)
}, { sessionId: session.sessionId })
```
## See Also
- `api.md` - Full API reference
- `gotchas.md` - Common pitfalls
- `../tools/REFERENCE.md` - Tool creation
- `../plugins/REFERENCE.md` - Plugin system
- `../scheduling/REFERENCE.md` - Scheduled agents
@@ -1,269 +0,0 @@
# Events
The Cline SDK has three event layers. Which one you use depends on whether you're working with the standalone `Agent` class or `ClineCore`.
## Which Events Do I Get?
| If you use... | You subscribe with... | You receive... | Text streaming event |
|---|---|---|---|
| Standalone `Agent` | `agent.subscribe()` | `AgentRuntimeEvent` | `assistant-text-delta` |
| `ClineCore` | `cline.subscribe()` | `CoreSessionEvent` | `chunk` (with `payload.type === "text"`) |
These are different event types with different shapes. Do not mix them up.
## Layer 1: AgentRuntimeEvent (Standalone Agent)
Emitted by the `Agent` class via `agent.subscribe()`. This is what you get when using `new Agent(...)` directly. Every event includes a `snapshot` field with the current `AgentRuntimeStateSnapshot`.
### Run Lifecycle
```typescript
{ type: "run-started", snapshot }
{ type: "run-finished", snapshot, result: AgentRunResult }
{ type: "run-failed", snapshot, error: Error }
```
### Turns
```typescript
{ type: "turn-started", snapshot, iteration: number }
{ type: "turn-finished", snapshot, iteration: number, toolCallCount: number }
```
### Text Streaming
```typescript
// Streaming text delta (arrives as chunks during generation)
{ type: "assistant-text-delta", snapshot, iteration: number, text: string, accumulatedText: string }
// Streaming reasoning delta (when model uses extended thinking)
{ type: "assistant-reasoning-delta", snapshot, iteration: number, text: string }
// Complete assistant message after model finishes
{ type: "assistant-message", snapshot, iteration: number, message: AgentMessage, finishReason: string }
```
### Messages
```typescript
// Fired when any message (user or assistant) is added to conversation history
{ type: "message-added", snapshot, message: AgentMessage }
```
### Tool Events
```typescript
{ type: "tool-started", snapshot, toolCall: { toolName: string, toolCallId: string, input: unknown } }
{ type: "tool-updated", snapshot, toolCall: { toolName: string, toolCallId: string }, update: string }
{ type: "tool-finished", snapshot, toolCall: { toolName: string, toolCallId: string }, message: AgentMessage }
```
### Usage
```typescript
{
type: "usage-updated",
snapshot,
usage: {
inputTokens: number,
outputTokens: number,
cacheReadTokens?: number,
cacheWriteTokens?: number,
totalCost?: number,
},
}
```
### Notices
```typescript
{ type: "status-notice", snapshot, message: string, metadata?: Record<string, unknown> }
```
### Subscribing
Use `agent.subscribe()`. Register the listener before calling `run()` to avoid missing early events.
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
agent.subscribe((event) => {
switch (event.type) {
case "assistant-text-delta":
process.stdout.write(event.text)
break
case "tool-started":
console.log(`\nUsing tool: ${event.toolCall.toolName}`)
break
case "usage-updated":
console.log(`Cost: $${event.usage.totalCost?.toFixed(4)}`)
break
case "run-finished":
console.log(`\nDone: ${event.result.status}`)
break
}
})
const result = await agent.run("Hello!")
```
You can also receive events through hooks (these are awaited, so they can be async):
```typescript
const agent = new Agent({
...config,
hooks: {
onEvent: async (event) => {
// Same AgentRuntimeEvent types as subscribe()
},
},
})
```
## Layer 2: AgentEvent (ClineCore Internal)
When using `ClineCore`, a `RuntimeEventAdapter` translates Layer 1 events into a legacy format called `AgentEvent`. You do not interact with this layer directly -- it is projected into `CoreSessionEvent` for subscribers. The key mappings:
| AgentRuntimeEvent (Layer 1) | AgentEvent (Layer 2) |
|---|---|
| `turn-started` | `iteration_start` |
| `turn-finished` | `iteration_end` |
| `assistant-text-delta` | `content_start` (text) |
| `assistant-message` | `content_end` (text) |
| `tool-started` | `content_start` (tool) |
| `tool-updated` | `content_update` (tool) |
| `tool-finished` | `content_end` (tool) |
| `usage-updated` | `usage` (with computed deltas) |
| `run-finished` | `done` |
| `run-failed` | `error` |
| `run-started`, `message-added` | (suppressed, not emitted) |
This layer exists for backwards compatibility. If you see event types like `content_update` or `iteration_start` in other documentation, they refer to this layer, not to what `agent.subscribe()` emits.
## Layer 3: CoreSessionEvent (ClineCore Subscriber)
Emitted by `ClineCore` via `cline.subscribe()`. These are higher-level session events.
```typescript
type CoreSessionEvent =
| { type: "chunk"; payload: SessionChunkEvent }
| { type: "agent_event"; payload: { sessionId: string, event: AgentEvent } }
| { type: "ended"; payload: SessionEndedEvent }
| { type: "team_progress"; payload: SessionTeamProgressEvent }
| { type: "status"; payload: { sessionId: string, status: string } }
| { type: "hook"; payload: SessionToolEvent }
```
### SessionChunkEvent
```typescript
interface SessionChunkEvent {
type: "text" | "reasoning"
text: string
sessionId: string
}
```
### SessionEndedEvent
```typescript
interface SessionEndedEvent {
sessionId: string
finishReason: "completed" | "max_iterations" | "aborted" | "mistake_limit" | "error"
result?: AgentResult
}
```
### Subscribing
```typescript
cline.subscribe((event) => {
switch (event.type) {
case "chunk":
if (event.payload.type === "text") {
process.stdout.write(event.payload.text)
}
break
case "ended":
console.log(`Finished: ${event.payload.finishReason}`)
break
}
})
```
Filter by session:
```typescript
cline.subscribe(handler, { sessionId: "specific-session-id" })
```
## Hub Events (Layer 3b)
When ClineCore runs in hub mode (via `backendMode: "hub"` or `"auto"` when a hub is available), events are projected over WebSocket using `HubEventName` types like `assistant.delta`, `iteration.started`, `tool.started`, etc. You do not interact with these directly -- `cline.subscribe()` still gives you `CoreSessionEvent` regardless of backend mode.
## Result Type Differences
The standalone Agent and ClineCore return different result types:
| API | Result type | Text property |
|---|---|---|
| `agent.run()` | `AgentRunResult` | `result.outputText` |
| `cline.start()` / `cline.send()` | `AgentResult` | `result.text` |
## Common Patterns
### Streaming Text (Standalone Agent)
```typescript
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
```
### Streaming Text (ClineCore)
```typescript
cline.subscribe((event) => {
if (event.type === "chunk" && event.payload.type === "text") {
process.stdout.write(event.payload.text)
}
})
```
### Usage Tracking (Standalone Agent)
```typescript
agent.subscribe((event) => {
if (event.type === "usage-updated" && event.usage.totalCost) {
console.log(`Running cost: $${event.usage.totalCost.toFixed(4)}`)
}
})
```
### Tool Call Logging (Standalone Agent)
```typescript
agent.subscribe((event) => {
if (event.type === "tool-started") {
console.log(`Tool started: ${event.toolCall.toolName}`)
}
if (event.type === "tool-finished") {
console.log(`Tool finished: ${event.toolCall.toolName}`)
}
})
```
## See Also
- `../agent/REFERENCE.md` - Agent runtime overview
- `../clinecore/REFERENCE.md` - ClineCore session management
- `../plugins/REFERENCE.md` - Plugin hooks for lifecycle events
- `../production/REFERENCE.md` - Observability in production
@@ -1,157 +0,0 @@
# Multi-Agent Coordination
The Cline SDK supports two models for multi-agent work: sub-agents (parent-child) and teams (peer-to-peer).
## Sub-Agents vs Teams
| Feature | Sub-Agents | Teams |
|---------|-----------|-------|
| Enable with | `enableSpawnAgent: true` | `enableAgentTeams: true` |
| Persistence | Session-scoped only | Across sessions |
| Coordination | Parent-child hierarchy | Peer-to-peer |
| Shared state | None | Task board, mailbox, mission log |
| Best for | One-off delegation | Complex multi-session projects |
## Sub-Agents
Sub-agents are spawned by a parent agent during a run. They execute independently and report results back.
### Enabling Sub-Agents
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
prompt: "Refactor the auth module and update tests",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
enableSpawnAgent: true,
enableTools: true,
},
})
```
When `enableSpawnAgent` is true, the agent gets access to sub-agent tools:
| Tool | Description |
|------|-------------|
| `start_subagent` | Spawn a background agent with a task |
| `message_subagent` | Send a message to a running sub-agent |
| `handoff_to_agent` | Delegate the current task entirely |
| `submit_and_exit` | Signal completion |
### How Sub-Agents Work
1. The parent agent decides a subtask can be delegated
2. It calls `start_subagent` with a role, task description, and optionally a preset
3. The sub-agent runs independently in the background
4. The parent can check status or send follow-up messages
5. Sub-agent results are available to the parent when complete
## Teams
Teams provide persistent, cross-session coordination between agents.
### Enabling Teams
```typescript
await cline.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
enableAgentTeams: true,
teamName: "auth-sprint",
enableTools: true,
},
})
```
### Team Tools
When `enableAgentTeams` is true, the coordinator agent gets:
| Tool | Description |
|------|-------------|
| `team_spawn_teammate` | Create a new agent with a role and task |
| `team_delegate_task` | Assign a task to an existing teammate |
| `team_check_status` | Check on a delegated task's progress |
| `team_get_result` | Get the completed result from a teammate |
### Team Persistence
Teams store shared state in:
```
~/.cline/data/teams/[team-name]/
task-board.json # task assignments and status
mailbox.json # inter-agent messages
mission-log.json # coordination log
```
This state persists across sessions, so team members can pick up where they left off.
### CLI Team Access
```bash
cline --team-name auth-sprint "Continue the auth refactor"
```
## Choosing Between Sub-Agents and Teams
Use sub-agents when:
- You need one-off parallel execution within a single session
- Tasks are independent and don't need to communicate with each other
- Results only matter to the parent agent
Use teams when:
- Work spans multiple sessions over time
- Agents need to coordinate and share progress
- Tasks have dependencies between them
- You want a persistent record of multi-agent collaboration
## Patterns
### Parallel Research with Sub-Agents
A parent agent spawns multiple sub-agents to research different topics simultaneously:
```typescript
await cline.start({
prompt: `Research these three topics in parallel:
1. Current best practices for JWT auth
2. OAuth 2.0 provider comparison
3. Session management patterns
Spawn a sub-agent for each topic, then synthesize the results.`,
config: {
enableSpawnAgent: true,
enableTools: true,
// ...
},
})
```
### Team Sprint
A coordinator manages a multi-session project:
```typescript
await cline.start({
prompt: `You are the coordinator for the auth-sprint team.
Review the task board and delegate the next highest-priority task
to a teammate. Check status on any in-progress tasks.`,
config: {
enableAgentTeams: true,
teamName: "auth-sprint",
enableTools: true,
// ...
},
})
```
## See Also
- `../clinecore/REFERENCE.md` - ClineCore runtime
- `../clinecore/api.md` - Session config for teams
- `../tools/REFERENCE.md` - Tool system
- `../plugins/REFERENCE.md` - Plugin system
@@ -1,649 +0,0 @@
# Plugins
A Cline plugin is a TypeScript module that extends any agent built on the Cline SDK. The same plugin runs in the Cline CLI, VS Code and JetBrains extensions, and any custom app built on `@cline/core`.
A plugin can:
- Register tools the model can call.
- Hook into the agent loop before/after runs, model calls, and tool calls.
- Rewrite provider messages before they hit the model (custom compaction, redaction, context shaping).
- Register slash commands, prompt rules, providers, and automation event types.
A plugin ships in one of two shapes:
1. Single-file plugin -- one `.ts` file that exports a default plugin object. Drop it in a discovery folder and it loads.
2. Plugin package -- a directory with `package.json`, npm dependencies, and optionally bundled assets. Installable via `cline plugin install`.
Both shapes use the same plugin API.
## The Mental Model
When the host starts a session, it builds a registry of plugins and runs four phases:
1. resolve -- collect the plugin objects.
2. validate -- check each plugin's `manifest`. Capabilities must be non-empty; declared hook stages must have matching handlers; if `hooks` is present, `"hooks"` must be in `capabilities`.
3. setup -- call each plugin's `setup(api, ctx)` once. This is where you `registerTool`, `registerCommand`, etc.
4. activate -- registry is frozen, the agent loop starts, and your hooks/tools are live.
Two invariants the registry enforces:
- Every contribution requires a matching capability. Calling `api.registerRule(...)` without `"rules"` in `manifest.capabilities` throws.
- Capabilities and handlers must agree. Declaring `"hooks"` without a `hooks` object, or vice versa, fails validation.
After validation, registration is one-shot -- no dynamic register/unregister during the session.
## The Smallest Working Plugin
```typescript
import type { AgentPlugin } from "@cline/core"
import { createTool } from "@cline/core"
const plugin: AgentPlugin = {
name: "hello-plugin",
manifest: {
capabilities: ["tools"],
},
setup(api, ctx) {
api.registerTool(
createTool({
name: "say_hello",
description: "Greet a person by name.",
inputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
},
async execute({ name }: { name: string }) {
return { greeting: `Hello, ${name}!` }
},
}),
)
},
}
export default plugin
```
The agent will see `say_hello` as a callable tool.
## The Manifest
```typescript
manifest: {
capabilities: ["tools", "hooks"], // required, non-empty array
paths?: string[], // optional, multi-entry packages
providerIds?: string[], // optional, provider plugins
modelIds?: string[], // optional, model plugins
}
```
### The Complete Capability List
| Capability | What It Unlocks in `api` |
|-----------|--------------------------|
| `"tools"` | `api.registerTool()` |
| `"commands"` | `api.registerCommand()` (slash commands in chat surfaces) |
| `"rules"` | `api.registerRule()` (string injected into the system prompt) |
| `"messageBuilders"` | `api.registerMessageBuilder()` (rewrites provider-bound messages) |
| `"providers"` | `api.registerProvider()` (custom model provider) |
| `"automationEvents"` | `api.registerAutomationEventType()` and `ctx.automation?.ingestEvent()` |
| `"hooks"` | The runtime `hooks` object on the plugin (lifecycle callbacks) |
You declare any combination -- most real plugins need 1-3 capabilities.
## setup(api, ctx) -- The Registration Phase
`setup()` runs once per session before the agent loop starts. Everything you register here is frozen for the lifetime of the session.
### The api Object
Each `register*` method requires the matching capability in your manifest:
```typescript
api.registerTool(tool) // requires "tools"
api.registerCommand({ name, description, handler }) // requires "commands"
api.registerRule({ id, content, source }) // requires "rules"
api.registerMessageBuilder({ name, build }) // requires "messageBuilders"
api.registerProvider({ name, description }) // requires "providers"
api.registerAutomationEventType({ eventType, source }) // requires "automationEvents"
```
### The ctx Object -- Host-Provided Session Context
The second argument carries everything the host knows about the current session. All fields are optional, so feature-detect before using them -- the same plugin must work in hosts that supply less context (unit tests, sandboxed plugin processes).
```typescript
ctx.session?.sessionId // string, stable core session id
ctx.client?.name // host: "cline-cli", "cline-vscode", etc.
ctx.user // authenticated user/org info, when available
ctx.workspaceInfo // { rootPath, hint, latestGitBranchName,
// latestGitCommitHash, associatedRemoteUrls }
ctx.automation?.ingestEvent // emit normalized automation events
ctx.logger?.log // structured logger scoped to this plugin
ctx.telemetry // ITelemetryService, only present in-process
```
Two rules about `ctx.workspaceInfo`:
1. Always prefer `ctx.workspaceInfo?.rootPath` over `process.cwd()`. The CLI may have been launched with `--cwd` without calling `chdir`, and VS Code workspaces don't share a single CWD. `workspaceInfo` is sourced from the session config and is always correct.
2. Don't use `import.meta.url` tricks to find "the workspace". That gives you the plugin's own location, not the user's project.
### Persisting State Across Hooks
`setup()` runs first; hooks fire later. The simplest way to share state is module-level variables:
```typescript
let sessionWorkspaceRoot: string | undefined
let sessionBranch: string | undefined
const plugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
setup(api, ctx) {
sessionWorkspaceRoot = ctx.workspaceInfo?.rootPath
sessionBranch = ctx.workspaceInfo?.latestGitBranchName
},
hooks: {
beforeTool({ toolCall, input }) {
if (sessionBranch === "main" && toolCall.toolName === "run_commands") {
// inspect input, optionally block
}
return undefined
},
},
}
```
A single Node process may host multiple sessions concurrently. If your plugin will run in a multi-session host, key your state by `ctx.session?.sessionId`:
```typescript
const stateBySession = new Map<string, MyState>()
setup(api, ctx) {
const id = ctx.session?.sessionId
if (id) stateBySession.set(id, /* ... */)
}
```
## Runtime Hooks
Runtime hooks are typed in-process callbacks on the same hook layer the runtime uses internally. They run inside the agent loop with full type information -- no IPC, no JSON marshaling.
Declare `"hooks"` in `manifest.capabilities`, then add a `hooks` property:
```typescript
const plugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
hooks: {
beforeRun(ctx) { /* ... */ },
beforeTool({ toolCall, input }) { /* ... */ },
afterTool({ toolCall, result }) { /* ... */ },
afterRun({ result }) { /* ... */ },
onEvent(event) { /* ... */ },
},
}
```
### The Seven Hooks
| Hook | Fires | Can Stop the Loop? | Common Uses |
|------|-------|--------------------|-------------|
| `beforeRun` | Before the runtime loop starts | Yes | Greet, log, attach session metadata |
| `afterRun` | After the runtime loop finishes (success, abort, or fail) | No | Notifications, metrics, persistent logs |
| `beforeModel` | Before each model request | Yes (mutate req) | Inject context, last-mile prompt edits |
| `afterModel` | After each model response, before tool execution | Yes | Block based on model output |
| `beforeTool` | Before each tool execution | Yes (`{ stop }`) | Audit, redact, block dangerous tools |
| `afterTool` | After each tool execution | Can replace result | Post-process, redact secrets in tool output |
| `onEvent` | On every `AgentRuntimeEvent` emitted by the runtime | No | Streaming UIs, telemetry pipes |
### Stopping the Loop from a Hook
Several hooks return an optional control object. The most common pattern is `beforeTool` blocking a destructive tool call:
```typescript
beforeTool({ toolCall, input }) {
if (toolCall.toolName === "run_commands") {
const { commands } = input as { commands?: string[] }
if (sessionBranch === "main" && commands?.some(c => c.startsWith("git push"))) {
return { stop: true, reason: "Blocked git push on protected branch" }
}
}
return undefined // explicit "continue"
}
```
Returning `undefined` (or omitting `return`) lets execution continue normally.
### afterRun Semantics
`afterRun` fires for every terminal status -- `completed`, `aborted`, `failed`. If you only want to act on success:
```typescript
afterRun({ result }) {
if (result.status !== "completed") return
// notify, log success metrics, etc.
}
```
### Plugin Hooks vs File Hooks
The runtime supports two hook systems:
- File hooks -- external scripts in `.cline/hooks/` invoked with serialized JSON. Right for user/workspace-specific scripts that don't ship with code.
- Plugin runtime hooks -- typed in-process callbacks. Right when the behavior belongs to a reusable extension and needs typed access to the runtime.
Core adapts file hooks onto the runtime hook layer, so you don't need both. If you're shipping a plugin, write it as runtime hooks.
## Message Builders
Message builders rewrite the provider-bound message list before the model call. They run after runtime messages are converted into SDK message blocks but before core's built-in safety builder.
Use them for:
- Custom compaction policies (replace middle history with a summary).
- Redacting PII or secrets before they reach the provider.
- Reshaping context for a specific model's strengths.
```typescript
api.registerMessageBuilder({
name: "summarize-middle-history",
build(messages) {
if (estimateTokens(messages) < THRESHOLD) return messages
return [...prefix, summary, ...recent]
},
})
```
Multiple builders run in registration order; the output of one is the input of the next.
When to use `beforeModel` instead: reach for the `beforeModel` hook only if you need the runtime snapshot or want to mutate the request object itself. Pure message rewrites belong in a builder.
## Automation Events
Plugins can declare normalized event types and emit them into Cline automation. Hosts that don't have automation enabled simply ignore both -- feature-detect `ctx.automation`.
```typescript
manifest: { capabilities: ["automationEvents"] },
setup(api, ctx) {
api.registerAutomationEventType({
eventType: "github.pull_request.opened",
source: "github",
description: "A new GitHub PR was opened",
attributesSchema: { /* JSON Schema for envelope.attributes */ },
})
if (!ctx.automation) return // host has no automation
ctx.automation.ingestEvent({
eventId: "pr-1234",
eventType: "github.pull_request.opened",
source: "github",
subject: "owner/repo#1234",
occurredAt: new Date().toISOString(),
attributes: { /* ... */ },
})
}
```
## Loading a Plugin
There are three ways a plugin gets into a session:
### Auto-Discovery (CLI)
The CLI scans these directories on startup:
- `<workspace>/.cline/plugins/` -- project-scoped plugins.
- `~/.cline/plugins/` -- user-scoped plugins.
Drop a `.ts` or `.js` file in, run `cline`, done:
```bash
mkdir -p .cline/plugins
cp my-plugin.ts .cline/plugins/
cline -i "do the thing my plugin enables"
```
### Explicit extensions in SDK Config
When you build your own host with `ClineCore`, pass the plugin object directly:
```typescript
import plugin from "./my-plugin"
import { ClineCore } from "@cline/core"
const host = await ClineCore.create({ backendMode: "local" })
await host.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY ?? "",
cwd: process.cwd(),
enableTools: true,
systemPrompt: "You are a helpful assistant.",
extensions: [plugin],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
prompt: "...",
interactive: false,
})
```
### pluginPaths for Directory-Based Plugins
When the plugin is a directory with `package.json`, point `pluginPaths` at the directory:
```typescript
config: {
pluginPaths: ["./path/to/my-plugin-package"],
}
```
Or install with the CLI:
```bash
cline plugin install ./path/to/my-plugin-package
cline plugin install @scope/my-cline-plugin # from npm
cline plugin install --git github.com/owner/repo # from git
```
## Single-File Plugin Template
Save as `my-plugin.ts`, drop in `.cline/plugins/`:
```typescript
import { type AgentPlugin, ClineCore, createTool } from "@cline/core"
let sessionRoot: string | undefined
const plugin: AgentPlugin = {
name: "my-plugin",
manifest: {
capabilities: ["tools", "hooks"],
},
setup(api, ctx) {
sessionRoot = ctx.workspaceInfo?.rootPath
api.registerTool(
createTool({
name: "do_thing",
description: "Do the thing this plugin exists for.",
inputSchema: {
type: "object",
properties: { target: { type: "string" } },
required: ["target"],
},
async execute(input) {
const { target } = input as { target: string }
return { ok: true, target, root: sessionRoot }
},
}),
)
},
hooks: {
beforeRun() {
console.log("[my-plugin] run started")
},
afterRun({ result }) {
if (result.status !== "completed") return
console.log(`[my-plugin] done in ${result.iterations} iteration(s)`)
},
},
}
async function runDemo(): Promise<void> {
const host = await ClineCore.create({ backendMode: "local" })
try {
const result = await host.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY ?? "",
cwd: process.cwd(),
enableTools: true,
systemPrompt: "You are a helpful assistant. Use tools when needed.",
extensions: [plugin],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
prompt: "Use do_thing on the target 'world'.",
interactive: false,
})
console.log(result.result?.text ?? "")
} finally {
await host.dispose()
}
}
if (import.meta.main) {
await runDemo()
}
export { plugin, runDemo }
export default plugin
```
Copy it, rename the tool, swap in your logic. The `runDemo()` function lets you test with `ANTHROPIC_API_KEY=sk-... bun run my-plugin.ts`.
## Plugin Package
Use a plugin package when you need npm dependencies, multiple entry points, bundled assets, or npm/git distribution.
### Layout
```
my-cline-plugin/
+-- package.json
+-- tsconfig.json (optional, for local typechecking)
+-- index.ts (the plugin entry point)
+-- README.md
+-- assets/ (optional, bundled content)
+-- templates/
+-- schemas/
```
### package.json -- The Discovery Contract
```json
{
"name": "my-cline-plugin",
"version": "0.1.0",
"private": true,
"description": "What this plugin does, in one sentence.",
"type": "module",
"exports": {
".": "./index.ts"
},
"cline": {
"plugins": [
{
"paths": ["./index.ts"],
"capabilities": ["tools", "hooks"]
}
]
},
"peerDependencies": {
"@cline/core": "*"
},
"peerDependenciesMeta": {
"@cline/core": { "optional": true }
},
"dependencies": {
"zod": "^4.1.5"
}
}
```
Key fields:
- `type: "module"` -- required. Cline plugins are ES modules.
- `cline.plugins` -- the discovery contract. Array of entries, each with `paths` (entry files) and `capabilities` (pre-declared, validated before importing).
- `peerDependencies` for `@cline/core` -- the host already provides it. Marking it optional lets you typecheck in isolation.
### Bundling Assets
Resolve asset paths with `import.meta.url`, not `process.cwd()`:
```typescript
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { readFileSync, existsSync } from "node:fs"
const MODULE_DIR = dirname(fileURLToPath(import.meta.url))
const TEMPLATES_DIR = join(MODULE_DIR, "assets", "templates")
function loadTemplate(name: string): string | undefined {
const path = join(TEMPLATES_DIR, `${name}.md`)
return existsSync(path) ? readFileSync(path, "utf8") : undefined
}
```
This is the only place `import.meta.url` is appropriate in a plugin -- locating files inside the plugin package. For workspace paths, always use `ctx.workspaceInfo?.rootPath`.
### The Override Pattern (Bundled / Global / Project)
A package can ship default assets and let users override them. The convention is a three-tier lookup, last write wins by `name`:
1. bundled -- files inside the plugin package (defaults shipped with the plugin).
2. global -- files under `~/.cline/data/settings/<kind>/` (user overrides).
3. project -- files under `<workspace>/.cline/<kind>/` (project overrides).
### Multiple Plugin Entries
If your package exposes more than one plugin, list each in `cline.plugins`:
```json
"cline": {
"plugins": [
{ "paths": ["./tools-plugin.ts"], "capabilities": ["tools"] },
{ "paths": ["./hooks-plugin.ts"], "capabilities": ["hooks"] }
]
}
```
Each entry file should `export default` its own plugin object.
## Testing Your Plugin
### Unit Tests
The plugin object is plain data. Drive `setup()` against a minimal context and exercise tools directly:
```typescript
import plugin from "../my-plugin"
const tools: unknown[] = []
const api = {
registerTool: (t: unknown) => tools.push(t),
registerCommand: () => {},
registerRule: () => {},
registerMessageBuilder: () => {},
registerProvider: () => {},
registerAutomationEventType: () => {},
}
await plugin.setup?.(api as never, {
workspaceInfo: { rootPath: "/tmp/fake-workspace" },
})
// Now `tools` contains the registered tools -- call tool.execute(input, ctx)
```
### End-to-End with runDemo()
Add a `runDemo()` in your plugin file (see the single-file template above) that boots a real `ClineCore` session:
```bash
ANTHROPIC_API_KEY=sk-... bun run my-plugin.ts
```
### CLI Smoke Test
```bash
mkdir -p .cline/plugins
cp my-plugin.ts .cline/plugins/
cline -i "trigger something that exercises the plugin"
```
For packages:
```bash
cline plugin install ./my-cline-plugin
cline -i "..."
```
If the plugin fails validation or setup, the CLI prints a clear error and continues without it.
## Common Gotchas
- "capabilities must be a non-empty array" -- you forgot `manifest.capabilities`, or it's `[]`.
- "registerRule requires the 'rules' capability" -- capability/handler drift. Add `"rules"` to capabilities, or stop calling `registerRule`.
- Tool not visible to the model -- check `enableTools: true` on the session config, and that you're declaring `"tools"` in capabilities.
- `ctx.workspaceInfo` is undefined in SDK tests -- the host didn't pass `extensionContext.workspace`. In SDK code, set it explicitly (see the ClineCore loading example above).
- State leaking across sessions -- module-level variables are shared across sessions in the same process. Key by `ctx.session?.sessionId` if your host runs multiple sessions concurrently.
- `afterRun` firing on aborts -- guard with `if (result.status !== "completed") return`.
- Heavy work in `setup()` -- `setup()` blocks session start. Defer expensive work into the first tool call or `beforeRun`.
- Importing host internals -- only import from `@cline/core`. Reaching into host-specific packages (e.g. CLI internals) will break in non-CLI hosts.
- Sandboxed plugins and `telemetry` -- telemetry is process-local. Feature-detect `ctx.telemetry` and expect it to be undefined in sandboxed plugin processes.
- Resolving bundled assets -- use `import.meta.url` + `fileURLToPath` to find files inside your package; never `process.cwd()`. For workspace paths, do the opposite: use `ctx.workspaceInfo?.rootPath`, never `import.meta.url`.
- Plugin name collisions -- `name` must be unique within a session. If two plugins share a name, validation fails. Namespace by package (`my-org-redactor`, not `redactor`).
## Decision Guide -- Which Extension Point?
| You want to... | Use |
|----------------|-----|
| Give the model a new capability | `registerTool` |
| Add a slash command in chat surfaces | `registerCommand` |
| Inject text into the system prompt | `registerRule` |
| Rewrite messages before they hit the provider | `registerMessageBuilder` |
| Add a custom model provider | `registerProvider` |
| Emit normalized cron/webhook events | `registerAutomationEventType` + `ctx.automation` |
| Observe or steer the agent loop | `hooks.*` |
| Block a dangerous tool call | `hooks.beforeTool` returning `{ stop: true }` |
| Notify on completion | `hooks.afterRun` (gate on `status === "completed"`) |
| Tweak each model request | `hooks.beforeModel` |
| Stream events to a UI | `hooks.onEvent` |
| Ship reusable templates with the plugin | Bundle assets next to `index.ts`, resolve via `import.meta.url` |
| Let users override defaults globally or per-project | Three-tier lookup: bundled / global / project |
## Pre-Ship Checklist
- `manifest.capabilities` is a non-empty array.
- Every `api.register*` call has a matching capability declared.
- If `hooks` is present, `"hooks"` is in `capabilities`.
- `ctx.workspaceInfo?.rootPath` is used for workspace paths (not `process.cwd()`).
- Optional `ctx` fields are feature-detected.
- Tool names are snake_case verbs; descriptions are written for the model.
- Tool inputs have JSON Schema with `required` set.
- `afterRun` handlers gate on `result.status === "completed"` if they only want successes.
- State that must not leak between concurrent sessions is keyed by `ctx.session?.sessionId`.
- (Package) `package.json` has `type: "module"`, `cline.plugins`, and `@cline/core` as an optional peer dep.
- (Package) Bundled assets resolved via `import.meta.url`, not `process.cwd()`.
- Smoke test: drop the plugin into `.cline/plugins/` (or `cline plugin install`), run `cline -i "..."`, watch it work.
## Plugin Examples from SDK
The SDK repo includes these example plugins:
| Plugin | Description |
|--------|-------------|
| `weather-metrics.ts` | Tool registration + lifecycle metrics |
| `mac-notify.ts` | macOS Notification Center alerts |
| `custom-compaction.ts` | Custom message compaction via message builders |
| `background-terminal.ts` | Detached shell job management |
| `automation-events.ts` | Plugin-emitted automation events |
| `gitignore-read-files-guard.ts` | File access policy enforcement via beforeTool |
| `web-search.ts` | Web search via Exa API |
| `typescript-lsp/` | TypeScript Language Service tools (plugin package) |
| `agents-squad/` | Multi-agent team orchestration (plugin package) |
## See Also
- `../tools/REFERENCE.md` - Tool creation
- `../events/REFERENCE.md` - Event system
- `../agent/REFERENCE.md` - Using plugins with Agent
- `../clinecore/REFERENCE.md` - Using plugins with ClineCore
@@ -1,253 +0,0 @@
# Going to Production
Guidelines for deploying Cline SDK agents in production environments.
## Error Handling
Always check the result status:
```typescript
const result = await agent.run(input)
switch (result.status) {
case "completed":
console.log("Success:", result.outputText)
break
case "aborted":
console.log("Cancelled:", result.error?.message)
break
case "failed":
console.error("Failed:", result.error)
break
}
```
For ClineCore, check `finishReason`:
```typescript
const session = await cline.start({ ... })
switch (session.result?.finishReason) {
case "completed":
// normal completion
break
case "max_iterations":
// agent hit iteration limit
break
case "aborted":
// manually cancelled
break
case "mistake_limit":
// too many tool errors
break
case "error":
// unrecoverable error
break
}
```
## Cost Control
### Token Limits
Set maximum tokens per turn and iteration limits:
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
maxTokensPerTurn: 4096,
maxIterations: 10,
tools: [...],
})
```
### Model Selection
Use cheaper models for simple tasks:
```typescript
// Simple classification or formatting
{ providerId: "anthropic", modelId: "claude-haiku-4-5" }
// Complex reasoning and code generation
{ providerId: "anthropic", modelId: "claude-sonnet-4-6" }
// Hardest tasks requiring deep reasoning
{ providerId: "anthropic", modelId: "claude-opus-4-7" }
```
### Usage Tracking
Monitor spending in real time:
```typescript
agent.subscribe((event) => {
if (event.type === "usage-updated" && event.usage.totalCost) {
if (event.usage.totalCost > MAX_BUDGET) {
agent.abort("Budget exceeded")
}
}
})
```
## Observability
### OpenTelemetry Integration
The SDK supports OpenTelemetry for traces, metrics, and logs:
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({
clientName: "my-app",
// OpenTelemetry config is picked up from environment
// OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, etc.
})
```
### Structured Logging
Use the `BasicLogger` interface for injectable logging:
```typescript
import type { BasicLogger } from "@cline/sdk"
const logger: BasicLogger = {
debug: (msg, meta) => console.debug(msg, meta),
log: (msg, meta) => console.log(msg, meta),
error: (msg, meta) => console.error(msg, meta),
}
await cline.start({
config: {
logger,
// ...
},
})
```
### Custom Metrics via Plugins
```typescript
const metricsPlugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
setup() {},
hooks: {
beforeRun() {
metrics.increment("agent.runs.started")
},
afterRun({ result }) {
metrics.increment("agent.runs.completed")
metrics.histogram("agent.iterations", result.iterations)
metrics.histogram("agent.tokens.output", result.usage.outputTokens)
},
beforeTool({ toolCall }) {
metrics.increment(`agent.tools.${toolCall.toolName}`)
},
},
}
```
## Security
### Sandbox Tool Execution
Validate tool inputs to prevent path traversal and injection:
```typescript
execute: async (input) => {
const safePath = path.resolve(WORKSPACE_ROOT, input.path)
if (!safePath.startsWith(WORKSPACE_ROOT)) {
return { error: "Path traversal attempt blocked" }
}
return await readFile(safePath, "utf-8")
}
```
### API Key Management
- Use environment variables, never hardcode keys
- Rotate keys regularly
- Use different keys for development and production
```typescript
{
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY, // never a literal string
}
```
### Tool Policy Hardening
Disable tools you don't need and require approval for dangerous ones:
```typescript
toolPolicies: {
read_files: { autoApprove: true },
search: { autoApprove: true },
bash: { autoApprove: false }, // require approval
editor: { autoApprove: false },
apply_patch: { autoApprove: false },
fetch_web: { enabled: false }, // disable entirely
}
```
## Deployment Patterns
### Stateless Worker
For request/response workloads (API endpoints, queue consumers):
```typescript
const cline = await ClineCore.create({
clientName: "worker",
backendMode: "local",
})
app.post("/agent", async (req, res) => {
const session = await cline.start({
prompt: req.body.prompt,
config: { ... },
})
res.json({ text: session.result?.text, usage: session.result?.usage })
})
```
### Persistent Service
For long-running services with session management:
```typescript
const cline = await ClineCore.create({
clientName: "service",
backendMode: "hub",
})
process.on("SIGTERM", async () => {
await cline.dispose("SIGTERM")
process.exit(0)
})
```
### Scheduled Automation
See `../scheduling/REFERENCE.md` for recurring agent tasks.
## Retry and Resilience
- Tool `execute` functions support `retryable: true` (default) and `maxRetries: 3` (default)
- Provider API calls are retried automatically on transient failures
- Use `timeoutMs` on tools to prevent hanging
- Monitor `mistake_limit` finish reason to detect systematic tool failures
## See Also
- `../agent/REFERENCE.md` - Agent overview
- `../clinecore/REFERENCE.md` - ClineCore overview
- `../tools/REFERENCE.md` - Tool configuration
- `../plugins/REFERENCE.md` - Metrics plugins
- `../scheduling/REFERENCE.md` - Scheduled agents
@@ -1,257 +0,0 @@
# Model Providers
The Cline SDK supports every major LLM provider out of the box via `@cline/llms`.
## Supported Providers
| Provider ID | Models |
|-------------|--------|
| `"anthropic"` | Claude Opus 4.7, Sonnet 4.6, Haiku 4.5 |
| `"openai"` | GPT-5.5, GPT-5.3 Codex |
| `"gemini"` | Gemini 3.1 Pro Preview, Gemini 3 Flash Preview |
| `"vertex"` | Google models via Vertex AI |
| `"bedrock"` | Claude, Llama via AWS Bedrock |
| `"mistral"` | Mistral Large, Codestral |
| `"openai-compatible"` | vLLM, Together, Fireworks, Groq, etc. |
## Basic Configuration
### With Agent
```typescript
import { Agent } from "@cline/sdk"
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
```
### With ClineCore
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
prompt: "Hello",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
},
})
```
## Provider-Specific Configuration
### Anthropic
```typescript
{
providerId: "anthropic",
modelId: "claude-opus-4-7", // or "claude-sonnet-4-6", "claude-haiku-4-5"
apiKey: process.env.ANTHROPIC_API_KEY,
}
```
### OpenAI
```typescript
{
providerId: "openai",
modelId: "gpt-5.5",
apiKey: process.env.OPENAI_API_KEY,
}
```
### Google (Gemini)
```typescript
{
providerId: "gemini",
modelId: "gemini-3.1-pro-preview",
apiKey: process.env.GOOGLE_API_KEY,
}
```
### Google (Vertex AI)
```typescript
{
providerId: "vertex",
modelId: "gemini-3.1-pro-preview",
// Uses application default credentials or service account
}
```
### AWS Bedrock
```typescript
{
providerId: "bedrock",
modelId: "anthropic.claude-sonnet-4-6",
// Uses AWS credential chain (env vars, config file, IAM role)
// Set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
}
```
### Mistral
```typescript
{
providerId: "mistral",
modelId: "mistral-large-latest",
apiKey: process.env.MISTRAL_API_KEY,
}
```
### OpenAI-Compatible
For any provider with an OpenAI-compatible API:
```typescript
{
providerId: "openai-compatible",
modelId: "my-model",
apiKey: process.env.API_KEY,
baseUrl: "https://api.together.xyz/v1",
}
```
Works with: vLLM, Together AI, Fireworks, Groq, Ollama, LiteLLM, etc.
## Custom Base URL
Override the API endpoint for any provider:
```typescript
{
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.API_KEY,
baseUrl: "https://my-proxy.example.com/v1",
}
```
## Custom Headers
Pass additional headers to API requests:
```typescript
{
providerId: "openai",
modelId: "gpt-5.5",
apiKey: process.env.API_KEY,
headers: {
"X-Custom-Header": "value",
},
}
```
## Gateway API
For advanced multi-provider setups, use the Gateway directly:
```typescript
import { createGateway, DefaultGateway } from "@cline/llms"
const gateway = createGateway({
providerConfigs: [
{ providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
{ providerId: "openai", apiKey: process.env.OPENAI_API_KEY },
],
})
// Create a model for a specific provider
const model = gateway.createAgentModel({
providerId: "anthropic",
modelId: "claude-opus-4-7",
})
// Use with Agent
const agent = new Agent({ model, systemPrompt: "...", tools: [] })
```
### Gateway Methods
```typescript
gateway.registerProvider(registration) // add a custom provider
gateway.configureProvider(config) // update provider settings
gateway.listProviders() // list available providers
gateway.listModels(providerId?) // list available models
gateway.createAgentModel(selection) // create model for agent
gateway.stream(request) // raw streaming (AsyncIterable)
```
## Provider Registry
Query and register providers programmatically:
```typescript
import {
getAllProviders,
getProviderIds,
getProvider,
getModelsForProvider,
registerProvider,
registerModel,
createHandler,
} from "@cline/llms"
// List all registered providers
const providers = getAllProviders()
// Get models for a provider
const models = getModelsForProvider("anthropic")
// Register a custom provider
registerProvider({
id: "my-provider",
name: "My Custom Provider",
handler: createHandler({ ... }),
})
```
## Model Metadata
Access model info (context window, pricing, capabilities):
```typescript
import { getModelsForProvider } from "@cline/llms"
const models = getModelsForProvider("anthropic")
for (const model of models) {
console.log(`${model.id}: context=${model.contextWindow}, input=$${model.inputPrice}/MTok`)
}
```
## Cost Tracking
Track per-request and cumulative costs:
```typescript
// Via events
agent.subscribe((event) => {
if (event.type === "usage-updated") {
console.log(`Cost: $${event.usage.totalCost?.toFixed(4)}`)
}
})
// Via result
const result = await agent.run("...")
console.log(`Total cost: $${result.usage.totalCost?.toFixed(4)}`)
// Via ClineCore accumulated usage
const usage = await cline.getAccumulatedUsage(sessionId)
```
## See Also
- `../agent/REFERENCE.md` - Using providers with Agent
- `../clinecore/REFERENCE.md` - Using providers with ClineCore
- `../production/REFERENCE.md` - Cost control in production
@@ -1,227 +0,0 @@
# Scheduling and Automation
The Cline SDK supports scheduled, one-off, and event-driven agent execution through the automation subsystem in `@cline/core`.
## Overview
Three trigger types:
| Trigger | Description |
|---------|-------------|
| `schedule` | Recurring jobs via cron expressions |
| `one_off` | Single execution tasks |
| `event` | Triggered by external events (GitHub, Linear, custom) |
## CLI Schedule Management
```bash
# Create a recurring schedule
cline schedule create "Daily standup" \
--cron "0 9 * * MON-FRI" \
--prompt "Summarize open PRs and blockers" \
--workspace /path/to/project \
--model anthropic/claude-sonnet-4-6
# List schedules
cline schedule list
# Trigger a schedule immediately
cline schedule trigger <schedule-id>
# Pause/resume
cline schedule pause <schedule-id>
cline schedule resume <schedule-id>
# Delete
cline schedule delete <schedule-id>
# View past executions
cline schedule executions <schedule-id>
```
## Cron Expressions
| Expression | Meaning |
|-----------|---------|
| `0 9 * * MON-FRI` | 9 AM weekdays |
| `0 */6 * * *` | Every 6 hours |
| `0 8 * * MON` | Mondays at 8 AM |
| `*/30 * * * *` | Every 30 minutes |
| `0 0 1 * *` | First of every month |
## File-Based Specs
Create Markdown files in `~/.cline/cron/` (global) or `.cline/cron/` (workspace):
### Recurring Schedule
```markdown
---
trigger: schedule
schedule: "0 9 * * MON-FRI"
timezone: America/New_York
mode: exclusive
prompt: "Check for dependency updates and create PRs for any outdated packages."
modelSelection:
providerId: anthropic
modelId: claude-sonnet-4-6
tools:
enabled: true
---
Additional context or instructions for the agent go in the body.
```
### One-Off Task
```markdown
---
trigger: one_off
prompt: "Generate a comprehensive test coverage report."
modelSelection:
providerId: anthropic
modelId: claude-sonnet-4-6
---
```
### Event-Driven
```markdown
---
trigger: event
eventType: github.pull_request.opened
filters:
repository: myorg/myrepo
debounceMs: 5000
cooldownMs: 60000
prompt: "Review the PR for security issues and code quality."
modelSelection:
providerId: anthropic
modelId: claude-sonnet-4-6
---
```
## CronSpec Types
```typescript
interface CronScheduleSpec {
trigger: "schedule"
schedule: string // cron expression
timezone?: string
mode?: "exclusive" | "concurrent"
prompt: string
modelSelection?: { providerId: string; modelId?: string }
extensionLoading?: "isolated" | "direct"
configExtensions?: RuntimeConfigExtensionKind[]
tools?: { enabled?: boolean; names?: string[] }
}
interface CronOneOffSpec {
trigger: "one_off"
prompt: string
modelSelection?: { providerId: string; modelId?: string }
}
interface CronEventSpec {
trigger: "event"
eventType: string // e.g., "github.pull_request.opened"
filters?: Record<string, unknown>
debounceMs?: number
cooldownMs?: number
prompt: string
modelSelection?: { providerId: string; modelId?: string }
}
```
## Programmatic Automation API
```typescript
const cline = await ClineCore.create({
clientName: "my-app",
automation: true,
})
// Start automation service
cline.automation.start()
// Ingest an external event
cline.automation.ingestEvent({
eventId: "evt-123",
eventType: "github.pull_request.opened",
source: "github",
timestamp: Date.now(),
payload: { pr: { number: 42, title: "..." } },
})
// List specs, runs, events
const specs = await cline.automation.listSpecs()
const runs = await cline.automation.listRuns()
const events = await cline.automation.listEvents()
// Reconcile specs from directory
await cline.automation.reconcile(specDirectory)
// Stop automation
cline.automation.stop()
```
## Event Ingestion from Plugins
Plugins can declare and emit automation events:
```typescript
const webhookPlugin: AgentPlugin = {
name: "webhook-events",
manifest: { capabilities: ["automationEvents"] },
setup(api) {
api.registerAutomationEventType({
type: "webhook.received",
description: "External webhook received",
})
},
}
```
Submit events via the plugin context:
```typescript
ctx.automation.ingestEvent({
eventId: "evt-456",
eventType: "webhook.received",
source: "custom",
timestamp: Date.now(),
payload: { ... },
})
```
## Concurrency Control
| Mode | Behavior |
|------|----------|
| `"exclusive"` | Skip if previous run still active |
| `"concurrent"` | Allow overlapping runs |
## Run Reports
Each completed run writes a Markdown report to `.cline/cron/reports/<run-id>.md` with:
- Run metadata (spec, trigger, timing)
- Summary of agent output
- Usage (tokens, cost)
- Tool calls made
- Trigger event context (for event-driven runs)
## Use Cases
- Daily standup summaries
- Automated dependency update checks
- PR review on open
- Codebase health reports
- Scheduled security scans
- Event-driven CI/CD workflows
## See Also
- `../clinecore/REFERENCE.md` - ClineCore runtime
- `../clinecore/api.md` - Automation API details
- `../plugins/REFERENCE.md` - Plugin events
- `../production/REFERENCE.md` - Production deployment
@@ -1,259 +0,0 @@
# Tools
Tools are how agents interact with the world. The Cline SDK supports both built-in tools (via ClineCore) and custom tools you define yourself.
## Creating Custom Tools
Use `createTool()` from `@cline/sdk` (or `@cline/shared`):
```typescript
import { createTool } from "@cline/sdk"
const myTool = createTool({
name: "search_issues",
description: "Search GitHub issues by query. Returns up to 10 results.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
state: { type: "string", enum: ["open", "closed", "all"] },
},
required: ["query"],
},
execute: async (input) => {
const issues = await github.searchIssues(input.query, input.state)
return { issues, count: issues.length }
},
})
```
### With Zod Schema
```typescript
import { createTool } from "@cline/sdk"
import { z } from "zod"
const deployTool = createTool({
name: "deploy",
description: "Deploy the app to the specified environment.",
inputSchema: z.object({
environment: z.enum(["staging", "production"]).describe("Target environment"),
version: z.string().optional().describe("Version tag, defaults to latest"),
}),
execute: async (input) => {
const result = await deploy(input.environment, input.version)
return { url: result.url, status: "deployed" }
},
})
```
### Tool Config Options
```typescript
createTool({
name: string, // snake_case, unique per agent
description: string, // what the tool does (model reads this)
inputSchema: JSONSchema | ZodSchema, // input validation
execute: async (input, context, onChange?) => output,
timeoutMs?: number, // default: 30000
retryable?: boolean, // default: true
maxRetries?: number, // default: 3
lifecycle?: {
completesRun?: boolean // true = ends agent loop on success
},
})
```
### AgentToolContext
The second argument to `execute` provides runtime context:
```typescript
interface AgentToolContext {
agentId: string
conversationId: string
iteration: number
abortSignal?: AbortSignal
metadata?: Record<string, unknown>
}
```
## Tool Naming Rules
- Names must be `snake_case` (e.g., `search_issues`, `deploy_app`)
- Names must be unique within a single agent's tool set
- Choose descriptive names since the model uses them to decide which tool to call
## Tool Descriptions Matter
The model reads the tool description to decide when and how to use it. Write clear, specific descriptions:
```typescript
// Bad: vague
description: "Does deployment stuff"
// Good: specific with constraints
description: "Deploy the application to staging or production. " +
"Staging deployments are immediate. Production requires a passing CI build. " +
"Returns the deployment URL and status."
```
Include constraints, rate limits, and expected behavior in the description.
## Error Handling in Tools
Return errors as structured data instead of throwing:
```typescript
// Good: return error data
execute: async (input) => {
const file = await readFile(input.path).catch(() => null)
if (!file) {
return { error: "File not found", path: input.path }
}
return { content: file }
}
```
Thrown exceptions count as "mistakes" against the agent's mistake limit. Returned error data lets the agent adjust its approach.
## Completion Tools
Tools with `lifecycle: { completesRun: true }` end the agent loop when they execute successfully:
```typescript
const submitAnswer = createTool({
name: "submit_answer",
description: "Submit the final answer and end the task.",
inputSchema: z.object({
answer: z.string(),
confidence: z.number().min(0).max(1),
}),
lifecycle: { completesRun: true },
execute: async (input) => input,
})
```
The model sees the tool result and the run ends. Access the output via `result.toolCalls`.
## Built-in Tools (ClineCore Only)
When using `ClineCore` with `enableTools: true`, these tools are available automatically:
| Tool | Name | What It Does |
|------|------|-------------|
| Shell | `bash` | Execute shell commands in the session workspace |
| Editor | `editor` | Create and edit files |
| Read | `read_files` | Read file contents |
| Patch | `apply_patch` | Apply unified diffs to files |
| Search | `search` | Search file contents and directory structure |
| Web | `fetch_web` | Fetch web content via HTTP |
Built-in tools respect the `cwd` setting in `CoreSessionConfig`.
## Tool Policies
Control which tools are available and whether they require approval:
```typescript
// In Agent config
const agent = new Agent({
tools: [toolA, toolB, toolC],
toolPolicies: {
tool_a: { autoApprove: true }, // runs without asking
tool_b: { autoApprove: false }, // requires approval
tool_c: { enabled: false }, // hidden from model
},
})
// In ClineCore session
await cline.start({
prompt: "...",
config: { ... },
toolPolicies: {
bash: { autoApprove: true },
editor: { autoApprove: false },
},
})
```
### Policy Options
| Policy | Effect |
|--------|--------|
| `{ autoApprove: true }` | Tool runs without approval |
| `{ autoApprove: false }` | Triggers approval callback before running |
| `{ enabled: false }` | Tool is hidden from the model entirely |
| No policy set | Defaults to enabled and auto-approved |
## Abort Signal in Long-Running Tools
Respect the abort signal for tools that take a long time:
```typescript
execute: async (input, context) => {
const results = []
for (const item of input.items) {
if (context.abortSignal?.aborted) {
return { results, aborted: true, processed: results.length }
}
results.push(await processItem(item))
}
return { results, processed: results.length }
}
```
## Streaming Tool Output
Use the `onChange` callback (third argument) to stream partial results:
```typescript
execute: async (input, context, onChange) => {
let progress = 0
for (const step of steps) {
progress++
onChange?.(`Processing step ${progress}/${steps.length}...`)
await processStep(step)
}
return { completed: true }
}
```
## Testing Tools
Tools are plain async functions, so they're straightforward to test:
```typescript
import { describe, it, expect } from "vitest"
describe("deploy tool", () => {
it("deploys to staging", async () => {
const context = { agentId: "test", conversationId: "test", iteration: 1 }
const result = await deployTool.execute({ environment: "staging" }, context)
expect(result.status).toBe("deployed")
})
})
```
## MCP Tool Integration
ClineCore can connect to MCP (Model Context Protocol) servers for additional tools. Configure in `.cline/mcp-servers.json`:
```json
{
"servers": {
"my-server": {
"command": "node",
"args": ["./mcp-server.js"]
}
}
}
```
MCP tools appear alongside built-in and custom tools automatically.
## See Also
- `../agent/REFERENCE.md` - Using tools with Agent
- `../clinecore/REFERENCE.md` - Using tools with ClineCore
- `../plugins/REFERENCE.md` - Packaging tools as plugins
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: use correct base URL for Vertex AI global endpoint with Claude models
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/cline-sdk
+33
View File
@@ -0,0 +1,33 @@
# CLI Development
The CLI lives in `cli/` and uses React Ink for terminal UI.
- If needed, look at `cli/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
- Never use `dimColor` with gray (e.g. `<Text color="gray" dimColor>`) - it's too hard to read. Use `color="gray"` for secondary text and normal foreground (no color) for primary text.
- When thinking about how to handle state or messages from core, look at webview for how it communicates with the vs code extension.
- When updating the webview, consider and suggest to the user to update the CLI TUI since we want to provide a similar experience to our terminal users as we do our vs code extension users.
## Adding New API Providers
When adding a new API provider to the extension, you must also update the CLI:
1. **Update `cli/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
```typescript
import { newProviderDefaultModelId, newProviderModels } from "@/shared/api"
export const providerModels = {
// ...existing providers
"new-provider": { models: newProviderModels, defaultId: newProviderDefaultModelId },
}
```
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli/src/utils/provider-config.ts`:
```typescript
import { applyProviderConfig } from "../utils/provider-config"
// After successful auth:
await applyProviderConfig({ providerId: "new-provider", controller })
```
This handles setting provider, default model, API key mapping, state persistence, and rebuilding the API handler.
3. **Provider-specific auth**: If the provider uses OAuth (like `openai-codex`), add handling in `SettingsPanelContent.tsx`'s `handleProviderSelect` callback. See the existing Codex OAuth flow as a reference.
+51
View File
@@ -0,0 +1,51 @@
# Debug Harness
HTTP-controlled debugger for the VSCode extension at `src/dev/debug-harness/server.ts`.
## Quick start
```bash
# Build extension first if needed (protos + esbuild):
npm run protos && IS_DEV=true node esbuild.mjs
# Launch (skip-build if already built):
npx tsx src/dev/debug-harness/server.ts --skip-build --auto-launch
# In another terminal:
curl localhost:19229/api -d '{"method":"status"}'
```
## Key commands
All via `POST localhost:19229/api` with `{"method":"...", "params":{...}}`:
- **`launch`** / **`shutdown`** — lifecycle
- **`ui.screenshot`** — screenshot to `/tmp/cline-debug/`; returns `{path}`**use `read_file` on the path to examine, do NOT `open` the file** (Preview.app covers the VSCode window)
- **`ui.open_sidebar`** — open the Cline sidebar
- **`ext.set_breakpoint`** `{file, line, condition?}` — breakpoint by source file (sourcemap-resolved)
- **`ext.evaluate`** `{expression, callFrameId?}` — eval in extension host
- **`ext.resume`** / **`ext.step_over`** / **`ext.step_into`** — stepping
- **`ext.call_stack`** — inspect when paused
- **`web.evaluate`** `{expression}` — eval in webview
- **`web.post_message`** `{message}` — send postMessage to extension host via exposed vsCodeApi
- **`wait_for_pause`** `{timeout?}` — block until breakpoint hit
- **`ui.locator`** `{role?, testId?, text?, action?, frame?}` — Playwright locator (auto-retries on stale sidebar frame)
- **`ui.react_input`** `{text, selector?, clear?, submit?}` — set React textarea value via `execCommand('insertText')`; works reliably across multiple tasks
- **`ui.send_message`** `{text, images?, files?, responseType?}` — send chat message bypassing the textarea entirely (via gRPC postMessage)
- **`ui.command_palette`** `{command}` — run VSCode command
## Caveats
- **⚠️ Dismiss "Introducing Cline Kanban" overlay FIRST**: On fresh launches a full-screen promo overlay blocks the sidebar. **Dismiss it immediately after `ui.open_sidebar`**, before any other interaction or screenshot. Most reliable method:
```bash
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
curl localhost:19229/api -d '{"method": "web.evaluate", "params": {"expression": "document.querySelector(\".sr-only\")?.parentElement?.click()"}}'
```
- **Screenshots — don't open the file**: `ui.screenshot` and `ui.sidebar_screenshot` save PNGs to `/tmp/cline-debug/` and return the `{path}`. Use `read_file` on the path to examine screenshots. Running `open <path>` launches Preview.app on macOS which covers the VSCode window.
- **Scripts count = 0 after launch**: CDP connects after extension host starts, so scripts parsed during startup aren't tracked. Breakpoints still work via sourcemap resolution.
- **Port 9230**: Extension host inspector. If another VSCode instance uses this port, the harness will fail to connect. Kill other debug instances first.
- **macOS only** for now (Playwright Electron launch behavior).
- **Webview CDP**: `connect_webview` may fail depending on Electron version. `web.evaluate` still works via Playwright's `frame.evaluate()` fallback.
- **Sourcemap paths**: esbuild outputs relative paths like `../src/extension.ts` in the sourcemap. The resolver handles this, but if a file isn't found, use `ext.source_files` to see exact paths.
See `src/dev/debug-harness/README.md` for full API reference.
+1 -1
View File
@@ -176,7 +176,7 @@ Present a final summary:
- Slack message copied to clipboard: yes
Remind the user to:
1. Manually trigger the publish release GitHub Action at: https://github.com/cline/cline/actions/workflows/ext-vscode-publish-stable.yml (paste `v{VERSION}` as the tag)
1. Manually trigger the publish release GitHub Action at: https://github.com/cline/cline/actions/workflows/publish.yml (paste `v{VERSION}` as the tag)
2. Post the Slack message to announce the hotfix
## Important Notes
+1 -1
View File
@@ -43,7 +43,7 @@ git push origin v<version>
### 4) Trigger publish workflow
Tell the maintainer to run:
https://github.com/cline/cline/actions/workflows/ext-vscode-publish-stable.yml
https://github.com/cline/cline/actions/workflows/publish.yml
Use `v<version>` as the release tag.
+2 -3
View File
@@ -20,9 +20,8 @@ command = "chmod +x ./scripts/run-extension-host.sh && ./scripts/run-extension-h
name = "CLI"
icon = "run"
command = '''
cd sdk
bun install
bun run cli
npm run cli:build
npm run cli:run
'''
[[actions]]
+2
View File
@@ -1,4 +1,6 @@
demo.gif filter=lfs diff=lfs merge=lfs -text
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
webview-ui/src/assets/cline_kanban_demo.mp4 filter=lfs diff=lfs merge=lfs -text
webview-ui/src/assets/cline_kanban_demo.webm filter=lfs diff=lfs merge=lfs -text
* text=auto eol=lf
+1 -1
View File
@@ -1,2 +1,2 @@
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @dominiccooney
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
/README.md @saoudrizwan @juanpflores
-6
View File
@@ -26,12 +26,6 @@ body:
placeholder: 'e.g., 1.2.3'
validations:
required: true
- type: checkboxes
id: beta
attributes:
label: Beta version
options:
- label: I am using a beta version of Cline
- type: textarea
id: what-happened
attributes:
+2 -1
View File
@@ -5,6 +5,7 @@ This is a VS Code extension. Read `.clinerules/general.md` for tribal knowledge
## Architecture
- **Core** (`src/`): `extension.ts``WebviewProvider``Controller` (single source of truth) → `Task` (agent loop).
- **Webview** (`webview-ui/`): React/Vite app. State via `ExtensionStateContext.tsx`, synced through message passing.
- **CLI** (`cli/`): React Ink terminal UI sharing core logic. Update CLI when changing webview features.
- **Communication**: Protobuf-defined gRPC-like protocol over VS Code message passing. Schemas in `proto/`.
- **MCP**: `src/services/mcp/McpHub.ts`.
@@ -27,7 +28,7 @@ Three proto conversion updates are **required** or the provider silently resets
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts`.
3. `convertProtoToApiProvider()` in the same file.
Also update: `src/shared/api.ts`, `src/shared/providers/providers.json`, `src/core/api/index.ts`, `webview-ui/.../providerUtils.ts`, `webview-ui/.../validate.ts`, `webview-ui/.../ApiOptions.tsx`.
Also update: `src/shared/api.ts`, `src/shared/providers/providers.json`, `src/core/api/index.ts`, `webview-ui/.../providerUtils.ts`, `webview-ui/.../validate.ts`, `webview-ui/.../ApiOptions.tsx`, and `cli/src/components/ModelPicker.tsx`.
For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/model-utils.ts` and set `apiFormat: ApiFormat.OPENAI_RESPONSES` on models.
+2 -2
View File
@@ -2,7 +2,7 @@ version: 2
updates:
# Main extension dependencies
- package-ecosystem: "npm"
directory: "/apps/vscode"
directory: "/"
schedule:
interval: "weekly"
# Group all updates into a single PR
@@ -20,7 +20,7 @@ updates:
# Webview UI dependencies
- package-ecosystem: "npm"
directory: "/apps/vscode/webview-ui"
directory: "/webview-ui"
schedule:
interval: "weekly"
groups:
-435
View File
@@ -1,435 +0,0 @@
name: cli-publish
on:
schedule:
- cron: "0 12 * * *"
workflow_dispatch:
inputs:
publish_target:
description: "Which publish flow to run"
required: true
default: "main"
type: choice
options:
- main
- nightly
git_tag:
description: "Existing release tag to publish when publish_target=main, for example cli-v0.1.0"
required: false
type: string
confirm_publish:
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
required: false
type: string
force_nightly_publish:
description: "Force nightly publish even with no commits in last 24h"
required: false
type: boolean
default: false
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: sdk
jobs:
publish-main:
name: Publish cline
permissions:
contents: write
id-token: write
if: |
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'main' &&
github.event.inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git_tag }}
fetch-depth: 0
fetch-tags: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Verify publish tooling
run: |
NPM_VERSION=$(npm --version)
echo "npm ${NPM_VERSION}"
IFS=. read -r major minor patch <<EOF
${NPM_VERSION}
EOF
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
echo "npm 11.5.1 or newer is required for trusted publishing"
exit 1
fi
- name: Install dependencies
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Validate release tag
id: version
env:
TAG: ${{ github.event.inputs.git_tag }}
run: |
if [ -z "$TAG" ]; then
echo "git_tag is required when publish_target=main"
exit 1
fi
if ! printf "%s\n" "$TAG" | grep -Eq '^cli-v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "git_tag must look like cli-vX.Y.Z, got: ${TAG}"
exit 1
fi
VERSION="${TAG#cli-v}"
PACKAGE_VERSION=$(node -p "require('./apps/cli/package.json').version")
if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
echo "sdk/apps/cli/package.json version ${PACKAGE_VERSION} does not match ${TAG}"
exit 1
fi
if ! printf "%s\n" "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "sdk/apps/cli/package.json has invalid version: ${VERSION}"
exit 1
fi
TAG_COMMIT=$(git rev-parse "${TAG}^{commit}")
HEAD_COMMIT=$(git rev-parse HEAD)
if [ "$TAG_COMMIT" != "$HEAD_COMMIT" ]; then
echo "${TAG} does not point at the checked out commit"
exit 1
fi
git fetch origin +main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$HEAD_COMMIT" origin/main; then
echo "${TAG} is not reachable from origin/main"
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- name: Build SDK packages
run: bun run build:sdk
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_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 }}
- name: Run tests
run: bun run test
- name: Build platform binaries
run: bun script/build.ts --install-native-variants --skip-sdk-build
working-directory: sdk/apps/cli
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_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 }}
- name: Verify build output
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
EXPECTED=(
"@cline/cli-darwin-arm64"
"@cline/cli-darwin-x64"
"@cline/cli-linux-arm64"
"@cline/cli-linux-x64"
"@cline/cli-windows-arm64"
"@cline/cli-windows-x64"
)
for package_name in "${EXPECTED[@]}"; do
dir="apps/cli/dist/${package_name#@cline/}"
if [ ! -f "$dir/package.json" ]; then
echo "Missing package manifest: $dir/package.json"
exit 1
fi
actual_name=$(node -p "require('./$dir/package.json').name")
actual_version=$(node -p "require('./$dir/package.json').version")
if [ "$actual_name" != "$package_name" ]; then
echo "Expected $package_name, got $actual_name"
exit 1
fi
if [ "$actual_version" != "$VERSION" ]; then
echo "Expected $package_name@$VERSION, got $actual_version"
exit 1
fi
ls -lh "$dir/bin/"
done
- name: Publish to NPM with latest tag
env:
NPM_CONFIG_PROVENANCE: "true"
run: bun script/publish-npm.ts --tag latest
working-directory: sdk/apps/cli
- name: Get Previous CLI Tag
id: prev_tag
env:
CURRENT_TAG: ${{ steps.version.outputs.tag }}
run: |
PREV_TAG=$(git describe --tags --abbrev=0 --match 'cli-v*' "$CURRENT_TAG^" 2>/dev/null || echo "")
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
- name: Get Changelog Entry
id: changelog
run: |
# Grab content between the first "## " header and the next one in apps/cli/CHANGELOG.md
CONTENT=$(awk '/^## [0-9]/{if(found) exit; found=1; next} found{print}' apps/cli/CHANGELOG.md)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.version.outputs.tag }}
name: "CLI v${{ steps.version.outputs.version }}"
body: |
${{ steps.changelog.outputs.content }}
${{ steps.prev_tag.outputs.prev_tag != '' && format('**Full Changelog**: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.tag) || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Summary
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Published cline@${VERSION} to npm with dist-tag 'latest'"
echo "Install with: npm install -g cline"
- name: Post release to Slack
uses: slackapi/slack-github-action@v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
payload: |
channel: "C0APVKGGZFC"
text: "Cline CLI v${{ steps.version.outputs.version }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "Cline CLI v${{ steps.version.outputs.version }}"
- type: "section"
text:
type: "mrkdwn"
text: ${{ toJSON(steps.changelog.outputs.content) }}
- type: "context"
elements:
- type: "mrkdwn"
text: "<https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}|View on npm>${{ steps.prev_tag.outputs.prev_tag != '' && format(' | Full Changelog: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.tag) || '' }}"
publish-nightly:
name: Publish cline nightly
permissions:
contents: read
id-token: write
if: |
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
(
github.event_name == 'schedule' ||
(
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'nightly'
)
)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for recent commits
id: check_commits
env:
FORCE_PUBLISH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
run: |
if [ "$FORCE_PUBLISH" = "true" ]; then
echo "force_nightly_publish enabled, proceeding with publish"
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$(git rev-list --count HEAD --since='24 hours ago')" -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "Found recent commits, proceeding with publish"
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup Bun
if: steps.check_commits.outputs.skip != 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Verify publish tooling
if: steps.check_commits.outputs.skip != 'true'
run: |
NPM_VERSION=$(npm --version)
echo "npm ${NPM_VERSION}"
IFS=. read -r major minor patch <<EOF
${NPM_VERSION}
EOF
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
echo "npm 11.5.1 or newer is required for trusted publishing"
exit 1
fi
- name: Install dependencies
if: steps.check_commits.outputs.skip != 'true'
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build SDK packages
if: steps.check_commits.outputs.skip != 'true'
run: bun run build:sdk
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_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 }}
- name: Run tests
if: steps.check_commits.outputs.skip != 'true'
run: bun run test
- name: Generate nightly version
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
BASE_VERSION=$(node -p "require('./apps/cli/package.json').version")
TIMESTAMP=$(date +%s)
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: ${BASE_VERSION}"
echo "Generated nightly version: ${VERSION}"
echo "base_version=${BASE_VERSION}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Update nightly package version
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
node -e '
const fs = require("node:fs");
const path = "apps/cli/package.json";
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
pkg.version = process.env.VERSION;
fs.writeFileSync(path, `${JSON.stringify(pkg, null, "\t")}\n`);
'
cat apps/cli/package.json | grep '"version"'
- name: Build platform binaries
if: steps.check_commits.outputs.skip != 'true'
run: bun script/build.ts --install-native-variants --skip-sdk-build
working-directory: sdk/apps/cli
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_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 }}
- name: Verify build output
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
EXPECTED=(
"@cline/cli-darwin-arm64"
"@cline/cli-darwin-x64"
"@cline/cli-linux-arm64"
"@cline/cli-linux-x64"
"@cline/cli-windows-arm64"
"@cline/cli-windows-x64"
)
for package_name in "${EXPECTED[@]}"; do
dir="apps/cli/dist/${package_name#@cline/}"
if [ ! -f "$dir/package.json" ]; then
echo "Missing package manifest: $dir/package.json"
exit 1
fi
actual_name=$(node -p "require('./$dir/package.json').name")
actual_version=$(node -p "require('./$dir/package.json').version")
if [ "$actual_name" != "$package_name" ]; then
echo "Expected $package_name, got $actual_name"
exit 1
fi
if [ "$actual_version" != "$VERSION" ]; then
echo "Expected $package_name@$VERSION, got $actual_version"
exit 1
fi
ls -lh "$dir/bin/"
done
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
run: bun script/publish-npm.ts --tag nightly
working-directory: sdk/apps/cli
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Published cline@${VERSION} to npm with dist-tag 'nightly'"
echo "Install with: npm install -g cline@nightly"
+83
View File
@@ -0,0 +1,83 @@
name: CLI TUI Tests
on:
pull_request:
branches:
- main
workflow_dispatch:
workflow_call:
permissions:
contents: read
jobs:
cli-tui-tests:
name: CLI TUI Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build CLI
run: npm run cli:build
- name: Run TUI Tests
id: tui_tests
run: |
npm run test:e2e:cli:tui 2>&1 | tee tui-test-output.log
exit_code=${PIPESTATUS[0]}
echo "tui_exit_code=$exit_code" >> $GITHUB_OUTPUT
exit $exit_code
- name: Write failure summary
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
run: |
echo "## ❌ CLI TUI Tests Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Step outcome:** \`${{ steps.tui_tests.outcome }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Test Output" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if [ -f tui-test-output.log ]; then
cat tui-test-output.log >> $GITHUB_STEP_SUMMARY
else
echo "(no test output captured — process may have been killed before output was flushed)" >> $GITHUB_STEP_SUMMARY
fi
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Debugging" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **TUI traces** are attached as artifacts below — download and inspect them to see terminal state at the point of failure." >> $GITHUB_STEP_SUMMARY
echo "- **To view a trace replay/Run a TUI Trace: ** run \`npx tui-test show-trace path/to/trace/file\` in your terminal" >> $GITHUB_STEP_SUMMARY
echo "- **Full test log** is also attached as an artifact." >> $GITHUB_STEP_SUMMARY
echo "- Tests run with \`retries: 2\` so any failure shown is a consistent failure, not a flake." >> $GITHUB_STEP_SUMMARY
- name: Upload TUI traces
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-traces
path: tests/e2e/cli/tui-traces/
retention-days: 14
if-no-files-found: warn
- name: Upload test log
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-log
path: tui-test-output.log
retention-days: 14
if-no-files-found: warn
@@ -0,0 +1,85 @@
name: Smoke Tests
on:
push:
branches: [main]
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
pull_request:
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: smoke-tests-${{ github.ref }}
cancel-in-progress: true
jobs:
smoke-tests:
name: Smoke Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build and install CLI
run: |
npm run protos
cd cli && npm install && npm run build && npm link
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH
- name: Verify CLI
run: cline --version
- name: Run smoke tests
env:
CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }}
run: |
cline auth -p cline -k "$CLINE_API_KEY" -m "anthropic/claude-sonnet-4.5"
max_attempts=3
for attempt in $(seq 1 $max_attempts); do
echo "::group::Attempt $attempt of $max_attempts"
if npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel; then
echo "::endgroup::"
echo "Smoke tests passed on attempt $attempt"
exit 0
fi
echo "::endgroup::"
if [ $attempt -lt $max_attempts ]; then
echo "::warning::Smoke tests failed on attempt $attempt, retrying..."
sleep 10
fi
done
echo "::error::Smoke tests failed after $max_attempts attempts"
exit 1
- name: Generate summary
if: always()
run: cat evals/smoke-tests/results/latest/summary.md >> $GITHUB_STEP_SUMMARY
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: smoke-test-results-${{ github.run_id }}
path: evals/smoke-tests/results/latest/
retention-days: 30
@@ -1,4 +1,4 @@
name: ext-vscode-test-e2e
name: E2E Tests
on:
push:
@@ -12,53 +12,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
detect-changes:
runs-on: ubuntu-latest
name: Detect Changes
outputs:
e2e: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.e2e == 'true' }}
steps:
- id: force
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
run: echo "run_all=true" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
if: steps.force.outputs.run_all != 'true'
- uses: dorny/paths-filter@v3
if: steps.force.outputs.run_all != 'true'
id: filter
with:
filters: |
e2e:
- 'apps/vscode/src/**'
- 'apps/vscode/webview-ui/**'
- 'apps/vscode/proto/**'
- 'apps/vscode/tests/**'
- 'apps/vscode/scripts/**'
- 'apps/vscode/standalone/**'
- 'apps/vscode/assets/**'
- 'apps/vscode/walkthrough/**'
- 'apps/vscode/package.json'
- 'apps/vscode/package-lock.json'
- 'apps/vscode/buf.yaml'
- 'apps/vscode/tsconfig*.json'
- 'apps/vscode/biome.jsonc'
- 'apps/vscode/esbuild.mjs'
- 'apps/vscode/.mocharc.json'
- 'apps/vscode/.vscode-test.mjs'
- 'apps/vscode/.vscodeignore'
- 'apps/vscode/playwright*.ts'
- '.github/workflows/ext-vscode-test-e2e.yml'
matrix_prep:
needs: detect-changes
if: needs.detect-changes.outputs.e2e == 'true'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
@@ -68,8 +23,7 @@ jobs:
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
e2e:
needs: [detect-changes, matrix_prep]
if: needs.detect-changes.outputs.e2e == 'true'
needs: matrix_prep
strategy:
fail-fast: false
matrix:
@@ -79,9 +33,6 @@ jobs:
permissions:
id-token: write
contents: read
defaults:
run:
working-directory: apps/vscode
steps:
- uses: actions/checkout@v4
- name: Setup Node.js environment
@@ -94,24 +45,24 @@ jobs:
uses: actions/cache@v4
id: root-cache
with:
path: apps/vscode/node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('apps/vscode/package-lock.json') }}
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: apps/vscode/webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('apps/vscode/webview-ui/package-lock.json') }}
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache VS Code installation
- name: Cache VS Code
uses: actions/cache@v4
id: vscode-cache
with:
path: apps/vscode/.vscode-test
key: vscode-${{ runner.os }}-stable-${{ hashFiles('apps/vscode/.vscode-test.mjs', 'apps/vscode/package.json') }}
path: .vscode-test
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
restore-keys: |
vscode-${{ runner.os }}-stable-
@@ -124,7 +75,7 @@ jobs:
~/.cache/ms-playwright
~/Library/Caches/ms-playwright
~/AppData/Local/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('apps/vscode/package-lock.json') }}
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
@@ -1,99 +0,0 @@
name: ext-vscode-publish-nightly
on:
workflow_dispatch:
run-name: "Publish Nightly from ${{ github.ref_name }} @ ${{ github.sha }}"
# Prevent concurrent publish runs on the same branch. The nightly publish script
# generates the extension version from a seconds-resolution timestamp, so parallel
# runs on the same ref can collide on the same version and cause publish failures
# or inconsistent tagging. Runs on different branches proceed independently.
concurrency:
group: ext-vscode-publish-nightly-${{ github.ref }}
cancel-in-progress: false
permissions: {}
jobs:
test:
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
permissions:
contents: read
pull-requests: read
uses: ./.github/workflows/ext-vscode-test.yml
publish:
needs: test
permissions:
contents: write
name: Publish Cline (Nightly) Extension
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
runs-on: ubuntu-latest
environment: PublishNightly
steps:
- name: Checkout selected branch
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
lfs: true
persist-credentials: false
- name: Show build source
working-directory: ${{ github.workspace }}
run: |
echo "Building ref: $GITHUB_REF"
echo "Building sha: $GITHUB_SHA"
git --no-pager log -1 --oneline
- name: Setup Node.js
uses: actions/setup-node@v4
with:
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
node-version: 22
- name: Install root dependencies
run: npm ci --include=optional
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Publish Nightly Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_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 }}
run: npm run publish:marketplace:nightly
- name: Tag published commit
working-directory: ${{ github.workspace }}
env:
GH_TOKEN: ${{ github.token }}
run: |
SAFE_REF=$(echo "$GITHUB_REF_NAME" | tr '/[:upper:]' '-[:lower:]' | tr -cd 'a-z0-9._-')
SHORT_SHA=$(git rev-parse --short=12 HEAD)
TIMESTAMP=$(date -u +"%Y%m%d%H%M%S")
TAG="nightly-${SAFE_REF}-${TIMESTAMP}-${SHORT_SHA}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Cline Nightly published from ${GITHUB_REF_NAME} at ${GITHUB_SHA}"
# Use an explicit HTTPS remote with GH_TOKEN because checkout was run with
# persist-credentials: false, so actions/checkout did not persist a git credential helper.
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${TAG}"
echo "Tagged published commit: $TAG"
-357
View File
@@ -1,357 +0,0 @@
name: ext-vscode-test
on:
push:
branches:
- main
workflow_dispatch:
pull_request:
branches:
- main
workflow_call:
# Set default permissions for all jobs
permissions:
contents: read # Needed to check out code
pull-requests: read # Needed for changed-file detection on pull requests
jobs:
detect-changes:
runs-on: ubuntu-latest
name: Detect Changes
outputs:
vscode: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.vscode == 'true' }}
testing_platform: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.testing_platform == 'true' }}
steps:
- id: force
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
run: echo "run_all=true" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
if: steps.force.outputs.run_all != 'true'
- uses: dorny/paths-filter@v3
if: steps.force.outputs.run_all != 'true'
id: filter
with:
filters: |
vscode:
- 'apps/vscode/src/**'
- 'apps/vscode/webview-ui/**'
- 'apps/vscode/proto/**'
- 'apps/vscode/tests/**'
- 'apps/vscode/scripts/**'
- 'apps/vscode/standalone/**'
- 'apps/vscode/assets/**'
- 'apps/vscode/walkthrough/**'
- 'apps/vscode/package.json'
- 'apps/vscode/package-lock.json'
- 'apps/vscode/buf.yaml'
- 'apps/vscode/tsconfig*.json'
- 'apps/vscode/biome.jsonc'
- 'apps/vscode/esbuild.mjs'
- 'apps/vscode/.mocharc.json'
- 'apps/vscode/.nycrc*.json'
- 'apps/vscode/.vscode-test.mjs'
- 'apps/vscode/test-setup.js'
- '.github/workflows/ext-vscode-test.yml'
testing_platform:
- 'apps/vscode/src/**'
- 'apps/vscode/proto/**'
- 'apps/vscode/standalone/**'
- 'apps/vscode/testing-platform/**'
- 'apps/vscode/tests/specs/**'
- 'apps/vscode/package.json'
- 'apps/vscode/package-lock.json'
- 'apps/vscode/buf.yaml'
- 'apps/vscode/tsconfig*.json'
- 'apps/vscode/esbuild.mjs'
- 'apps/vscode/.vscodeignore'
- 'apps/vscode/scripts/**'
- '.github/workflows/ext-vscode-test.yml'
quality-checks:
needs: detect-changes
if: needs.detect-changes.outputs.vscode == 'true' || needs.detect-changes.outputs.testing_platform == 'true'
runs-on: ubuntu-latest
name: Quality Checks
defaults:
run:
working-directory: apps/vscode
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
- name: Run Quality Checks (Parallel)
run: npm run ci:check-all
vscode-test:
needs: [detect-changes, quality-checks]
if: needs.detect-changes.outputs.vscode == 'true'
env:
VSCODE_TEST_VERSION: 1.103.0
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
name: ${{ matrix.os == 'ubuntu-latest' && 'vscode test' || format('vscode test ({0})', matrix.os) }}
defaults:
run:
shell: bash
working-directory: apps/vscode
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
- name: Set up NPM on Windows
if: runner.os == 'Windows'
run: |
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
- name: Cache VS Code test runtime
if: runner.os == 'Windows'
uses: actions/cache@v4
with:
path: .vscode-test
key: vscode-test-runtime-${{ runner.os }}-${{ env.VSCODE_TEST_VERSION }}
# Build the extension and tests (without redundant checks)
- name: Build Tests and Extension
id: build_step
run: npm run ci:build
- name: Unit Tests with coverage - Linux
id: unit_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: |
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
- name: Unit Tests - Non-Linux
id: unit_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: |
npm run test:unit
- name: Extension Integration Tests - Linux
id: integration_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: xvfb-run -a npm run test:coverage
- name: Extension Integration Tests - Non-Linux
id: integration_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: |
for attempt in 1 2 3; do
echo "Running extension integration tests (attempt ${attempt}/3)"
if npm run test:integration; then
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Extension integration tests failed after 3 attempts"
exit 1
fi
echo "Extension integration tests failed; retrying after short delay"
sleep 5
done
- name: Webview Tests with Coverage
id: webview_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: |
cd webview-ui
npm run test:coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
# Only upload artifacts on Linux - We only need coverage from one OS
if: runner.os == 'Linux'
with:
name: pr-coverage-reports
path: |
apps/vscode/coverage-unit/lcov.info
apps/vscode/webview-ui/coverage/lcov.info
test-platform-integration:
needs: [detect-changes, quality-checks]
if: needs.detect-changes.outputs.testing_platform == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/vscode
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
apps/vscode/testing-platform/package-lock.json
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
- name: Download ripgrep binaries
run: npm run download-ripgrep
- name: Compile Standalone
run: npm run compile-standalone
- name: Install testing platform dependencies
run: cd testing-platform && npm ci
- name: Running testing platform integration spec tests
timeout-minutes: 7
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
with:
name: test-platform-integration-core-coverage
path: apps/vscode/coverage/**/lcov.info
# Keep the required "test" check as a tiny aggregate gate instead of the conditional
# VS Code matrix. GitHub treats conditionally skipped jobs as successful required
# checks, so the gate below preserves the old required check name while making sure
# whichever filtered test jobs were selected actually passed.
test:
needs: [detect-changes, quality-checks, vscode-test, test-platform-integration]
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
name: test
steps:
- name: Verify selected test jobs
env:
DETECT_CHANGES_RESULT: ${{ needs.detect-changes.result }}
QUALITY_CHECKS_RESULT: ${{ needs.quality-checks.result }}
VSCODE_CHANGED: ${{ needs.detect-changes.outputs.vscode }}
TESTING_PLATFORM_CHANGED: ${{ needs.detect-changes.outputs.testing_platform }}
VSCODE_TEST_RESULT: ${{ needs.vscode-test.result }}
TEST_PLATFORM_RESULT: ${{ needs.test-platform-integration.result }}
run: |
if [ "$DETECT_CHANGES_RESULT" != "success" ]; then
echo "detect-changes did not succeed: $DETECT_CHANGES_RESULT"
exit 1
fi
if [ "$VSCODE_CHANGED" != "true" ] && [ "$TESTING_PLATFORM_CHANGED" != "true" ]; then
echo "No root test paths changed; skipping root test requirements."
exit 0
fi
if [ "$QUALITY_CHECKS_RESULT" != "success" ]; then
echo "quality-checks did not succeed: $QUALITY_CHECKS_RESULT"
exit 1
fi
if [ "$VSCODE_CHANGED" = "true" ] && [ "$VSCODE_TEST_RESULT" != "success" ]; then
echo "vscode-test did not succeed: $VSCODE_TEST_RESULT"
exit 1
fi
if [ "$TESTING_PLATFORM_CHANGED" = "true" ] && [ "$TEST_PLATFORM_RESULT" != "success" ]; then
echo "test-platform-integration did not succeed: $TEST_PLATFORM_RESULT"
exit 1
fi
echo "Selected root test jobs passed."
qlty:
needs: [detect-changes, quality-checks, vscode-test, test-platform-integration]
if: ${{ !cancelled() && needs.quality-checks.result == 'success' && (needs.vscode-test.result == 'success' || needs.vscode-test.result == 'skipped') && (needs.test-platform-integration.result == 'success' || needs.test-platform-integration.result == 'skipped') && (needs.detect-changes.outputs.vscode == 'true' || needs.detect-changes.outputs.testing_platform == 'true') }}
runs-on: ubuntu-latest
# Run on PRs to main, pushes to main, and manual dispatches
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download unit tests coverage reports
if: needs.detect-changes.outputs.vscode == 'true'
uses: actions/download-artifact@v4
with:
name: pr-coverage-reports
path: apps/vscode
- name: Upload core unit tests coverage to Qlty
if: needs.detect-changes.outputs.vscode == 'true'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
apps/vscode/coverage-unit/lcov.info
tag: unit:core
- name: Upload webview-ui unit tests coverage to Qlty
if: needs.detect-changes.outputs.vscode == 'true'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
apps/vscode/webview-ui/coverage/lcov.info
tag: unit:webview-ui
add-prefix: webview-ui/
- name: Download test platform integration core coverage artifact
if: needs.detect-changes.outputs.testing_platform == 'true'
uses: actions/download-artifact@v4
continue-on-error: true
id: download-integration-coverage
with:
name: test-platform-integration-core-coverage
path: apps/vscode/integration-core-coverage-reports
- name: Upload core integration tests coverage to Qlty
if: needs.detect-changes.outputs.testing_platform == 'true' && steps.download-integration-coverage.outcome == 'success'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
files: apps/vscode/integration-core-coverage-reports/**/lcov.info
tag: integration:core
@@ -1,4 +1,4 @@
name: repo-label-issues
name: Auto-label Issues
on:
issues:
@@ -15,7 +15,7 @@ jobs:
script: |
const body = context.payload.issue.body || '';
const labels = context.payload.issue.labels.map(l => l.name);
// Check if JetBrains Plugin is selected
if (body.match(/###\s*Plugin Type\s*\n+JetBrains Plugin/i)) {
if (!labels.includes('JetBrains')) {
@@ -51,15 +51,3 @@ jobs:
});
}
}
// Check if beta version checkbox is checked
if (body.includes('- [X] I am using a beta version of Cline') || body.includes('- [x] I am using a beta version of Cline')) {
if (!labels.includes('beta')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['beta']
});
}
}
+111
View File
@@ -0,0 +1,111 @@
name: Publish NPM Release
on:
workflow_call:
inputs:
confirm_publish:
description: 'Type "publish" to confirm you want to publish to NPM'
required: true
type: string
permissions:
contents: write # Required for pushing tags
id-token: write # Required for npm trusted publishing (OIDC)
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' && 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: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Install root dependencies and CLI dependencies
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
run: npm run protos
- name: Read release version
id: version
run: |
# Read version from cli/package.json
VERSION=$(node -p "require('./cli/package.json').version")
echo "Release version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- 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 }}
run: node scripts/package-npm.mjs
- 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
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
cd dist-standalone
npm publish --tag latest --access public
- name: Tag release
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v${{ steps.version.outputs.version }}-cli"
git push origin "v${{ steps.version.outputs.version }}-cli"
- 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 }}"
- name: Post release to Slack
uses: slackapi/slack-github-action@v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
payload: |
channel: "C0APVKGGZFC"
text: "Cline CLI v${{ steps.version.outputs.version }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Cline CLI v${{ steps.version.outputs.version }}*"
- type: "context"
elements:
- type: "mrkdwn"
text: "<https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}|View on npm>"
+134
View File
@@ -0,0 +1,134 @@
name: Publish NPM Nightly
on:
workflow_call:
inputs:
force_publish:
description: "Force publish even if there are no commits in the last 24 hours"
required: false
type: boolean
default: false
permissions:
contents: read
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
uses: ./.github/workflows/test.yml
publish-npm-nightly:
needs: test
name: Publish Cline CLI (Nightly) to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check for recent commits
id: check_commits
run: |
if [ "${{ inputs.force_publish }}" = "true" ]; then
echo "force_publish enabled, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Found recent commits, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node.js
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
if: steps.check_commits.outputs.skip != 'true'
run: npm run protos
- name: Generate nightly version with timestamp
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
# Read base version from cli/package.json (e.g., "2.0.0")
BASE_VERSION=$(node -p "require('./cli/package.json').version")
# Generate timestamp (Unix epoch seconds)
TIMESTAMP=$(date +%s)
# Create unique nightly version: 1.0.9-nightly.1736365200
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: $BASE_VERSION"
echo "Generated nightly version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update cli/package.json with nightly version
if: steps.check_commits.outputs.skip != 'true'
run: |
# Update version with timestamp-based nightly version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('cli/package.json', 'utf8'));
pkg.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
"
echo "Using version ${{ steps.version.outputs.version }} for build"
cat cli/package.json | grep '"version"'
- name: Build and package CLI
if: steps.check_commits.outputs.skip != 'true'
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
OTEL_TELEMETRY_ENABLED: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
run: node scripts/package-npm.mjs
- name: Verify build output
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
cd dist-standalone
npm publish --tag nightly --access public
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
echo ""
echo "📦 Install with: npm install -g cline@nightly"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
+215
View File
@@ -0,0 +1,215 @@
# Build and Pack CLI
#
# Builds a CLI tarball from any branch/commit and publishes it as a GitHub Release.
# Requires write access to the repository (maintainers/collaborators only).
#
# Security: Split into two jobs to isolate untrusted build code from write tokens.
# The build job runs arbitrary ref code with zero permissions. The release job
# only runs trusted GitHub Actions with write scope.
#
# Usage (helper script, auto-detects current branch):
# ./scripts/build-cli-artifact.sh
# ./scripts/build-cli-artifact.sh feature/my-changes
# ./scripts/build-cli-artifact.sh feature/my-changes 1234 # comments on PR
#
# Usage (gh CLI directly):
# gh workflow run pack-cli.yml -f ref=main
# gh workflow run pack-cli.yml -f ref=abc123 -f pr_number=1234
#
# Install the built CLI (no auth required):
# npm install -g https://github.com/cline/cline/releases/download/cli-build-<sha>/cline-<ver>.tgz
#
# Find releases:
# gh release list --limit 10
name: Build and Pack CLI
permissions:
contents: read
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch, tag, or commit SHA to build (leave empty for default branch)'
required: false
type: string
pr_number:
description: 'PR number to comment on with install instructions (optional)'
required: false
type: number
jobs:
# ── Build job: runs untrusted ref code with ZERO permissions ──
build:
name: Build CLI
runs-on: ubuntu-latest
permissions: {}
outputs:
commit_sha: ${{ steps.commit.outputs.sha }}
tarball: ${{ steps.pack.outputs.tarball }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Get commit SHA
id: commit
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "Building from commit: $COMMIT_SHA"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install dependencies
run: npm ci --include=optional
- name: Generate Protos
run: npm run protos
- name: Build standalone package
run: node scripts/package-npm.mjs
- name: Create Tarball
id: pack
run: |
cd dist-standalone
TARBALL=$(npm pack)
echo "tarball=$TARBALL" >> $GITHUB_OUTPUT
echo "Created tarball: $TARBALL"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cli-tarball
path: dist-standalone/*.tgz
# ── Release job: only trusted Actions code, with write permissions ──
release:
name: Release CLI
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: cli-tarball
path: dist-standalone
- name: Create GitHub Release
id: create_release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const commit = '${{ needs.build.outputs.commit_sha }}';
const tarball = '${{ needs.build.outputs.tarball }}';
// Delete existing release/tag if re-running for the same commit
const tagName = `cli-build-${commit}`;
try {
const existing = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tagName
});
await github.rest.repos.deleteRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: existing.data.id
});
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${tagName}`
});
core.info(`Deleted existing release for ${tagName}`);
} catch (e) {
// Release doesn't exist yet, that's fine
}
// Create a release
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `CLI Build (${commit})`,
body: `Automated CLI build from commit ${commit}\n\nInstall with:\n\`\`\`bash\nnpm install -g https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}\n\`\`\``,
draft: false,
prerelease: true
});
// Upload the tarball as a release asset
const tarballPath = path.join('dist-standalone', tarball);
const tarballData = fs.readFileSync(tarballPath);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.data.id,
name: tarball,
data: tarballData
});
const downloadUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}`;
core.setOutput('release_url', release.data.html_url);
core.setOutput('download_url', downloadUrl);
- name: Comment on PR with download instructions
if: inputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const commit = '${{ needs.build.outputs.commit_sha }}';
const releaseUrl = '${{ steps.create_release.outputs.release_url }}';
const downloadUrl = '${{ steps.create_release.outputs.download_url }}';
const prNumber = ${{ inputs.pr_number || 0 }};
if (!prNumber) return;
const comment = `## 📦 CLI Build Ready
A CLI build has been created for commit \`${commit}\`.
### Install Directly from URL (No Authentication Required!)
\`\`\`bash
npm install -g ${downloadUrl}
\`\`\`
### Alternative: Download and Install
\`\`\`bash
curl -L ${downloadUrl} -o cline.tgz
npm install -g ./cline.tgz
\`\`\`
📦 [View Release](${releaseUrl})
`;
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Summary
run: |
echo "✅ CLI build complete!"
echo ""
echo "📦 Release: ${{ steps.create_release.outputs.release_url }}"
echo "🔗 Download URL: ${{ steps.create_release.outputs.download_url }}"
echo ""
echo "Install from anywhere (no authentication required):"
echo " npm install -g ${{ steps.create_release.outputs.download_url }}"
@@ -0,0 +1,60 @@
name: Publish CLI (Trusted)
on:
schedule:
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
inputs:
publish_target:
description: "Which publish flow to run"
required: true
default: "main"
type: choice
options:
- main
- nightly
confirm_publish:
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
required: false
type: string
force_nightly_publish:
description: "Force nightly publish even with no commits in last 24h"
required: false
type: boolean
default: false
permissions:
id-token: write # Required for npm trusted publishing (OIDC)
contents: write # Required because npm-main creates/pushes git tags
checks: write # Required by nested reusable test workflow
pull-requests: write # Required by nested reusable test workflow
jobs:
cli-tui-tests:
uses: ./.github/workflows/cli-tui-tests.yml
publish-main:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'main' &&
github.event.inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
)
uses: ./.github/workflows/npm-main.yaml
secrets: inherit
with:
confirm_publish: ${{ github.event.inputs.confirm_publish }}
publish-nightly:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish_target == 'nightly')
)
uses: ./.github/workflows/npm-nightly.yaml
secrets: inherit
with:
force_publish: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
+77
View File
@@ -0,0 +1,77 @@
name: "Publish Nightly Release"
on:
schedule:
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
permissions:
contents: write
packages: write
checks: write
pull-requests: write
jobs:
test:
uses: ./.github/workflows/test.yml
publish:
needs: test
name: Publish Cline (Nightly) Extension
if: github.repository == 'cline/cline'
runs-on: ubuntu-latest
environment: PublishNightly
steps:
- uses: actions/checkout@v4
with:
lfs: true
- name: Check for recent commits
run: |
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
echo "No commits in last 24 hours, exiting"
exit 0
fi
echo "Found recent commits, proceeding with build"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
node-version: 22
- name: Install root dependencies
run: npm ci --include=optional
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Verify LFS media assets are resolved
run: |
for FILE in webview-ui/src/assets/cline_kanban_demo.mp4 webview-ui/src/assets/cline_kanban_demo.webm; do
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
exit 1
fi
done
- name: Publish Extension as Pre-release
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_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 }}
run: npm run publish:marketplace:nightly
@@ -1,4 +1,4 @@
name: ext-vscode-publish-stable
name: "Publish Release"
on:
workflow_dispatch:
@@ -29,16 +29,13 @@ permissions:
jobs:
test:
uses: ./.github/workflows/ext-vscode-test.yml
uses: ./.github/workflows/test.yml
publish:
needs: test
name: Publish Extension
runs-on: ubuntu-latest
environment: publish
defaults:
run:
working-directory: apps/vscode
steps:
- uses: actions/checkout@v4
@@ -50,11 +47,9 @@ jobs:
- name: Resolve Release Tag
id: resolve_tag
working-directory: ${{ github.workspace }}
env:
TAG: ${{ github.event.inputs.tag }}
AUTO_CREATE: ${{ github.event.inputs.auto_create_tag_from_main }}
run: |
TAG="${{ github.event.inputs.tag }}"
AUTO_CREATE="${{ github.event.inputs.auto_create_tag_from_main }}"
TESTED_SHA="${{ github.sha }}"
WORKFLOW_REF="${{ github.ref }}"
@@ -139,6 +134,15 @@ jobs:
fi
echo "Tag and package version match: $TAG"
- name: Verify LFS media assets are resolved
run: |
for FILE in webview-ui/src/assets/cline_kanban_demo.mp4 webview-ui/src/assets/cline_kanban_demo.webm; do
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
exit 1
fi
done
- name: Package and Publish Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
@@ -153,19 +157,11 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
RELEASE_TYPE: ${{ github.event.inputs.release-type }}
run: |
# Swap README.marketplace.md into README.md so both the GitHub
# release artifact (vsce package below) and the marketplace
# publish (npm run publish:marketplace below, which swaps
# internally as an idempotent no-op) ship the same README.
node scripts/marketplace-readme.mjs swap-in
trap 'node scripts/marketplace-readme.mjs restore' EXIT
# Required to generate the .vsix
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
if [ "$RELEASE_TYPE" = "pre-release" ]; then
if [ "${{ github.event.inputs.release-type }}" = "pre-release" ]; then
npm run publish:marketplace:prerelease
echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
else
@@ -175,7 +171,6 @@ jobs:
- name: Get Previous Tag
id: prev_tag
working-directory: ${{ github.workspace }}
run: |
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
@@ -183,7 +178,6 @@ jobs:
- name: Get Changelog Entry
id: changelog
working-directory: ${{ github.workspace }}
run: |
# Get content between first ## [ and second ## [
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
@@ -195,7 +189,7 @@ jobs:
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.resolve_tag.outputs.tag }}
files: "apps/vscode/*.vsix"
files: "*.vsix"
body: |
${{ steps.changelog.outputs.content }}
-282
View File
@@ -1,282 +0,0 @@
name: sdk-publish
on:
workflow_dispatch:
inputs:
channel:
description: "Publish channel"
required: true
type: choice
options:
- nightly
- latest
default: nightly
force_publish:
description: "Force publish even if there are no commits in the last 24 hours"
required: false
type: boolean
default: false
confirm_publish:
description: 'Required when channel=latest. Type "publish" to confirm release publish.'
required: false
type: string
schedule:
# Run nightly at 2:00 AM UTC
- cron: "0 2 * * *"
defaults:
run:
working-directory: sdk
jobs:
test:
permissions:
contents: read
uses: ./.github/workflows/sdk-test.yml
publish-sdk:
needs: test
name: Publish SDK Packages
permissions:
contents: write
id-token: write
if: |
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
(
github.event_name != 'workflow_dispatch' ||
inputs.channel != 'latest' ||
(
inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
)
)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Determine publish channel
id: channel
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_CHANNEL: ${{ inputs.channel }}
run: |
# Default to nightly for scheduled runs
if [ "$EVENT_NAME" = "schedule" ]; then
echo "channel=nightly" >> $GITHUB_OUTPUT
else
echo "channel=$INPUT_CHANNEL" >> $GITHUB_OUTPUT
fi
- name: Check for recent commits
id: check_commits
env:
CHANNEL: ${{ steps.channel.outputs.channel }}
FORCE_PUBLISH: ${{ inputs.force_publish }}
run: |
# Always publish for latest (production) releases
if [ "$CHANNEL" = "latest" ]; then
echo "Production release requested, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ "$FORCE_PUBLISH" = "true" ]; then
echo "force_publish enabled, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ "$(git rev-list --count HEAD --since="24 hours ago")" -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Found recent commits, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Verify trusted publishing context
if: steps.check_commits.outputs.skip != 'true'
run: |
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "GitHub OIDC request environment is unavailable. Ensure this job has id-token: write for npm trusted publishing."
exit 1
fi
echo "GitHub OIDC request environment is available for npm trusted publishing."
- name: Setup Bun
if: steps.check_commits.outputs.skip != 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Verify publish tooling
if: steps.check_commits.outputs.skip != 'true'
run: |
NPM_VERSION=$(npm --version)
echo "npm ${NPM_VERSION}"
IFS=. read -r major minor patch <<EOF
${NPM_VERSION}
EOF
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
echo "npm 11.5.1 or newer is required for trusted publishing"
exit 1
fi
- name: Install dependencies
if: steps.check_commits.outputs.skip != 'true'
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build SDK
if: steps.check_commits.outputs.skip != 'true'
run: bun run build:sdk
- name: Generate shared version
if: steps.check_commits.outputs.skip != 'true'
id: version
env:
CHANNEL: ${{ steps.channel.outputs.channel }}
run: |
BASE_VERSION=$(node -p "require('./packages/llms/package.json').version")
if [ "$CHANNEL" = "nightly" ]; then
TIMESTAMP=$(date +%s)
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
else
VERSION="$BASE_VERSION"
fi
echo "Base version: $BASE_VERSION"
echo "Channel: $CHANNEL"
echo "Publish version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update all package versions and lockfile
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: bun scripts/version.ts "$VERSION"
- name: Verify publishability
if: steps.check_commits.outputs.skip != 'true'
run: bun scripts/check-publish.ts
- name: Prepare package tarball directory
if: steps.check_commits.outputs.skip != 'true'
run: mkdir -p "$RUNNER_TEMP/sdk-npm-packs"
# Pack with Bun so workspace/catalog protocols are resolved in the tarball,
# then publish that tarball with npm so npm trusted publishing can use GitHub OIDC.
# Publish sequentially in dependency order: shared → llms → agents → core → sdk
- name: Publish @cline/shared
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/shared@${VERSION} with tag '${CHANNEL}'..."
cd packages/shared
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Publish @cline/llms
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/llms@${VERSION} with tag '${CHANNEL}'..."
cd packages/llms
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Publish @cline/agents
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/agents@${VERSION} with tag '${CHANNEL}'..."
cd packages/agents
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Publish @cline/core
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/core@${VERSION} with tag '${CHANNEL}'..."
cd packages/core
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Publish @cline/sdk
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/sdk@${VERSION} with tag '${CHANNEL}'..."
cd packages/sdk
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Create package tags for production publish
if: steps.check_commits.outputs.skip != 'true' && steps.channel.outputs.channel == 'latest'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
for PKG in shared llms agents core sdk; do
TAG="sdk/${PKG}/v${VERSION}"
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "Tag already exists locally: ${TAG}"
else
git tag -a "${TAG}" -m "@cline/${PKG}@${VERSION}"
echo "Created tag: ${TAG}"
fi
# Ensure remote has the tag; this is idempotent if tag already exists remotely.
git push origin "refs/tags/${TAG}"
done
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
CHANNEL: ${{ steps.channel.outputs.channel }}
run: |
echo "Published SDK packages with tag '${CHANNEL}':"
echo " - @cline/shared@${VERSION}"
echo " - @cline/llms@${VERSION}"
echo " - @cline/agents@${VERSION}"
echo " - @cline/core@${VERSION}"
echo " - @cline/sdk@${VERSION}"
if [ "$CHANNEL" = "latest" ]; then
echo "Created git tags:"
echo " - sdk/shared/v${VERSION}"
echo " - sdk/llms/v${VERSION}"
echo " - sdk/agents/v${VERSION}"
echo " - sdk/core/v${VERSION}"
echo " - sdk/sdk/v${VERSION}"
fi
-112
View File
@@ -1,112 +0,0 @@
name: sdk-test
on:
push:
branches:
- main
paths:
- "sdk/**"
- ".github/workflows/sdk-test.yml"
workflow_dispatch:
pull_request:
branches:
- main
paths:
- "sdk/**"
- ".github/workflows/sdk-test.yml"
workflow_call:
permissions:
contents: read
defaults:
run:
working-directory: sdk
jobs:
quality-checks:
runs-on: ubuntu-latest
name: Quality Checks
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Install dependencies
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Typecheck
run: |
bun run build:sdk
bun run -F @cline/cli build
bun run types
- name: Lint & Format
run: bun run lint
test:
needs: quality-checks
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
node-version: "24.x"
- os: windows-latest
node-version: "24.x"
runs-on: ${{ matrix.os }}
name: Test (${{ matrix.os }}, Node ${{ matrix.node-version }})
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build SDK
id: build_sdk_step
run: bun run build:sdk
- name: Build CLI
id: build_cli_step
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' }}
run: bun -F @cline/cli build
- name: Run Tests
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os != 'windows-latest' }}
run: bun run test
- name: Run SDK Tests (Windows)
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'windows-latest' }}
run: bun -F './packages/**' test
- name: Smoke test SQLite under Node
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && matrix.os != 'windows-latest' }}
timeout-minutes: 10
run: bun scripts/ci-node-smoke.ts
- name: Run TUI e2e tests
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
run: bun -F @cline/cli test:e2e:cli:tui
- name: Verify packages are publishable
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
run: bun scripts/check-publish.ts
@@ -1,6 +1,6 @@
# This workflow will only label and/or close 30 issues at a time in order to avoid exceeding a rate limit.
# More info: https://docs.github.com/en/actions/use-cases-and-examples/project-management/closing-inactive-issues
name: repo-stale-issues
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
+32
View File
@@ -0,0 +1,32 @@
name: Test Stale Issues Workflow
on:
workflow_dispatch:
inputs:
days-before-stale:
description: "Days before an issue becomes stale"
required: true
default: "1"
days-before-close:
description: "Days before a stale issue is closed"
required: true
default: "1"
jobs:
test-stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@28ca103
with:
days-before-issue-stale: ${{ github.event.inputs.days-before-stale }}
days-before-issue-close: ${{ github.event.inputs.days-before-close }}
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for ${{ github.event.inputs.days-before-stale }} days with no activity."
close-issue-message: "This issue was closed because it has been inactive for ${{ github.event.inputs.days-before-close }} days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
exempt-issue-labels: "pinned,security"
repo-token: ${{ secrets.GITHUB_TOKEN }}
debug-only: true
+224
View File
@@ -0,0 +1,224 @@
name: Tests
on:
push:
branches:
- main
workflow_dispatch:
pull_request:
branches:
- main
workflow_call:
# Set default permissions for all jobs
permissions:
contents: read # Needed to check out code
checks: write # Needed to report test results
pull-requests: write # Needed to add comments/annotations to PRs
jobs:
quality-checks:
runs-on: ubuntu-latest
name: Quality Checks
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
- name: Run Quality Checks (Parallel)
run: npm run ci:check-all
test:
needs: quality-checks
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
name: ${{ matrix.os == 'ubuntu-latest' && 'test' || format('test ({0})', matrix.os) }}
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
- name: Set up NPM on Windows
if: runner.os == 'Windows'
run: |
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
# Build the extension and tests (without redundant checks)
- name: Build Tests and Extension
id: build_step
run: npm run ci:build
- name: Unit Tests with coverage - Linux
id: unit_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: |
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
- name: Unit Tests - Non-Linux
id: unit_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: |
npm run test:unit
- name: Extension Integration Tests - Linux
id: integration_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: xvfb-run -a npm run test:coverage
- name: Extension Integration Tests - Non-Linux
id: integration_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: npm run test:integration
- name: Webview Tests with Coverage
id: webview_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: |
cd webview-ui
npm run test:coverage
- name: CLI Tests
id: cli_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: cd cli && npm run test:run
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
# Only upload artifacts on Linux - We only need coverage from one OS
if: runner.os == 'Linux'
with:
name: pr-coverage-reports
path: |
coverage-unit/lcov.info
webview-ui/coverage/lcov.info
test-platform-integration:
needs: quality-checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
testing-platform/package-lock.json
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
- name: Download ripgrep binaries
run: npm run download-ripgrep
- name: Compile Standalone
run: npm run compile-standalone
- name: Install testing platform dependencies
run: cd testing-platform && npm ci
- name: Running testing platform integration spec tests
timeout-minutes: 7
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
with:
name: test-platform-integration-core-coverage
path: coverage/**/lcov.info
qlty:
needs: [test, test-platform-integration]
runs-on: ubuntu-latest
# Run on PRs to main, pushes to main, and manual dispatches
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download unit tests coverage reports
uses: actions/download-artifact@v4
with:
name: pr-coverage-reports
path: .
- name: Upload core unit tests coverage to Qlty
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
coverage-unit/lcov.info
tag: unit:core
- name: Upload webview-ui unit tests coverage to Qlty
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
webview-ui/coverage/lcov.info
tag: unit:webview-ui
add-prefix: webview-ui/
- name: Download test platform integration core coverage artifact
uses: actions/download-artifact@v4
continue-on-error: true
id: download-integration-coverage
with:
name: test-platform-integration-core-coverage
path: integration-core-coverage-reports
- name: Upload core integration tests coverage to Qlty
if: steps.download-integration-coverage.outcome == 'success'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
files: integration-core-coverage-reports/**/lcov.info
tag: integration:core
@@ -1,4 +1,4 @@
name: ext-jb-test-integration
name: Trigger Jetbrains Plugin <-> Cline Tests
on:
pull_request_target:
types: [opened, reopened]
@@ -15,11 +15,9 @@ jobs:
trigger-integration-test:
name: Run Tests
runs-on: ubuntu-latest
# Auto-run only for trusted PR authors. Anyone else needs a maintainer
# to opt their PR in by commenting /test-jetbrains.
# Run on PR open/reopen, or when someone comments /test-jetbrains on a PR
if: |
(github.event_name == 'pull_request_target' &&
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.pull_request.author_association)) ||
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '/test-jetbrains') &&
@@ -29,8 +27,8 @@ jobs:
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.CLINE_JETBRAINS_APP_ID }}
private-key: ${{ secrets.CLINE_JETBRAINS_APP_KEY }}
app-id: 1998650
private-key: ${{ secrets.CLINE_JETBRAINS_WORKFLOW_KEY }}
owner: cline
repositories: intellij-plugin
+5 -19
View File
@@ -17,8 +17,8 @@ pnpm-lock.yaml
.actrc
CLAUDE.local.md
apps/vscode/webview-ui/src/**/*.js
apps/vscode/webview-ui/src/**/*.js.map
webview-ui/src/**/*.js
webview-ui/src/**/*.js.map
# Ignore coverage directories and files
coverage
@@ -35,9 +35,9 @@ coverage-unit
.worktrees
## Generated files ##
apps/vscode/src/generated/
apps/vscode/src/shared/proto/
apps/vscode/webview-ui/src/services/grpc-client.ts
src/generated/
src/shared/proto/
webview-ui/src/services/grpc-client.ts
*.tsbuildinfo
# E2E Tests
@@ -56,17 +56,3 @@ evals/smoke-tests/results/
secrets.json
tui-traces
tests/**/cache
# Backup created by scripts/marketplace-readme.mjs while publishing.
# Should never be committed: only exists if a publish aborts mid-swap.
.README.github.bak
# SDK Session files / User data
.cline/data
.cline/tmp
*.db
*.db-shm
*.db-wal
.cline/**/managed.json
.cline/**/bundle.json
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,9 @@
"src/**/__tests__/*.ts",
"src/test/services/**/*.test.ts"
],
"ignore": [
"src/sdk/**"
],
"require": [
"ts-node/register",
"source-map-support/register",
+1 -1
View File
@@ -1 +1 @@
22
lts/*
-3
View File
@@ -1,3 +0,0 @@
bun 1.3.13
node 22
@@ -1,6 +1,5 @@
import { defineConfig } from "@vscode/test-cli"
import path from "path"
const vscodeTestVersion = process.env.VSCODE_TEST_VERSION ?? "stable"
export default defineConfig({
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
@@ -13,7 +12,7 @@ export default defineConfig({
require: ["./test-setup.js"],
},
workspaceFolder: "test-workspace",
version: vscodeTestVersion,
version: "stable",
extensionDevelopmentPath: path.resolve("./"),
launchArgs: ["--disable-extensions"],
})
+1 -2
View File
@@ -5,7 +5,6 @@
"connor4312.esbuild-problem-matchers",
"ms-vscode.extension-test-runner",
"bradlc.vscode-tailwindcss",
"biomejs.biome",
"oven.bun-vscode"
"biomejs.biome"
]
}
+31 -144
View File
@@ -10,23 +10,23 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-workspace-trust",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"${workspaceFolder}/apps/vscode",
"${workspaceFolder}",
"--disable-extensions"
],
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js"
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
}
},
@@ -35,22 +35,22 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-workspace-trust",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"${workspaceFolder}/apps/vscode"
"${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js"
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "staging"
}
},
@@ -59,22 +59,22 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-workspace-trust",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"${workspaceFolder}/apps/vscode"
"${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js"
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "local"
}
},
@@ -84,27 +84,27 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--user-data-dir=${workspaceFolder}/apps/vscode/dist/tmp/user",
"--user-data-dir=${workspaceFolder}/dist/tmp/user",
"--profile-temp",
"--sync=off",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
"${workspaceFolder}/apps/vscode"
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js"
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "clean-tmp-user",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"TEMP_PROFILE": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
}
},
@@ -117,13 +117,13 @@
],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${workspaceFolder}/apps/vscode/**",
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"cwd": "${workspaceFolder}/apps/vscode",
"cwd": "${workspaceFolder}",
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js",
"${workspaceFolder}/apps/vscode/dist-standalone/**/*.js"
"${workspaceFolder}/dist/**/*.js",
"${workspaceFolder}/dist-standalone/**/*.js"
],
"preLaunchTask": "compile-standalone",
"runtimeExecutable": "npx",
@@ -131,11 +131,11 @@
"tsx"
],
"program": "scripts/test-standalone-core-api-server.ts",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"PROTOBUS_PORT": "26040",
"HOSTBRIDGE_PORT": "26041",
"WORKSPACE_DIR": "${workspaceFolder}/apps/vscode",
"WORKSPACE_DIR": "${workspaceFolder}",
"E2E_TEST": "true",
"CLINE_ENVIRONMENT": "local"
},
@@ -151,10 +151,10 @@
],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${workspaceFolder}/apps/vscode/**",
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"cwd": "${workspaceFolder}/apps/vscode",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "npx",
"runtimeArgs": [
"mocha"
@@ -169,7 +169,7 @@
"--exit",
"${file}"
],
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
"NODE_ENV": "test",
@@ -188,7 +188,7 @@
"run",
"storybook"
],
"cwd": "${workspaceFolder}/apps/vscode/webview-ui",
"cwd": "${workspaceFolder}/webview-ui",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"serverReadyAction": {
@@ -199,119 +199,6 @@
"env": {
"IS_DEV": "true"
}
},
{
"name": "Launch Bun CLI (Prompt)",
"type": "bun",
"request": "launch",
"cwd": "${workspaceFolder}/sdk/apps/cli",
"runtime": "bun",
"runtimeArgs": [
"--conditions=development"
],
"program": "${workspaceFolder}/sdk/apps/cli/src/index.ts",
"args": [
"${input:cliPrompt}"
],
"env": {
"CLINE_BUILD_ENV": "development"
},
"presentation": {
"hidden": true
}
},
{
"name": "Launch RPC Server",
"type": "bun",
"request": "launch",
"cwd": "${workspaceFolder}/sdk/apps/cli",
"runtime": "bun",
"runtimeArgs": [
"--conditions=development"
],
"program": "${workspaceFolder}/sdk/apps/cli/src/index.ts",
"args": [
"rpc",
"start"
],
"env": {
"CLINE_BUILD_ENV": "development",
"CLINE_DEBUG_PORT_BASE": "9230"
},
"presentation": {
"hidden": true
}
},
{
"name": "Attach RPC Runtime (9230)",
"type": "bun",
"request": "attach",
"url": "ws://127.0.0.1:9230",
"localRoot": "${workspaceFolder}/sdk",
"remoteRoot": "${workspaceFolder}/sdk",
"presentation": {
"hidden": true
}
},
{
"name": "Attach Hook Worker (9231)",
"type": "bun",
"request": "attach",
"url": "ws://127.0.0.1:9231",
"localRoot": "${workspaceFolder}/sdk",
"remoteRoot": "${workspaceFolder}/sdk",
"presentation": {
"hidden": true
}
},
{
"name": "Attach Plugin Sandbox (9232)",
"type": "bun",
"request": "attach",
"url": "ws://127.0.0.1:9232",
"localRoot": "${workspaceFolder}/sdk",
"remoteRoot": "${workspaceFolder}/sdk",
"presentation": {
"hidden": true
}
},
{
"name": "Attach Connector Child (9233)",
"type": "bun",
"request": "attach",
"url": "ws://127.0.0.1:9233",
"localRoot": "${workspaceFolder}/sdk",
"remoteRoot": "${workspaceFolder}/sdk",
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Launch RPC Server Debugger",
"configurations": [
"Launch RPC Server",
"Attach RPC Runtime (9230)"
]
},
{
"name": "Launch CLI Debugger",
"configurations": [
"Launch Bun CLI (Prompt)",
"Attach RPC Runtime (9230)",
"Attach Hook Worker (9231)",
"Attach Plugin Sandbox (9232)",
"Attach Connector Child (9233)"
]
}
],
"inputs": [
{
"id": "cliPrompt",
"type": "promptString",
"description": "Prompt to send to the CLI",
"default": "hey"
}
]
}
+1 -2
View File
@@ -1,6 +1,5 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.insertFinalNewline": true,
"files.exclude": {
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
@@ -17,7 +16,7 @@
// Protobuf settings
"protoc": {
"options": [
"--proto_path=apps/vscode/proto"
"--proto_path=proto"
]
},
// Enable Lint and format using Biome
+2 -29
View File
@@ -11,9 +11,6 @@
"problemMatcher": [],
"presentation": {
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode"
}
},
{
@@ -26,7 +23,6 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
@@ -78,7 +74,6 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
@@ -99,7 +94,6 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true",
"IS_TEST": "true"
@@ -137,7 +131,6 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
@@ -176,7 +169,6 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
@@ -215,7 +207,6 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true",
"IS_TEST": "true"
@@ -235,9 +226,6 @@
"presentation": {
"group": "watch",
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode"
}
},
{
@@ -252,10 +240,7 @@
"reveal": "always",
"group": "watchers"
},
"group": "build",
"options": {
"cwd": "${workspaceFolder}/apps/vscode"
}
"group": "build"
},
{
"label": "tasks: watch-tests",
@@ -277,7 +262,7 @@
"dependsOn": [
"watch"
],
"command": "rm -rf ${workspaceFolder}/apps/vscode/dist/tmp/user && mkdir -p ${workspaceFolder}/apps/vscode/dist/tmp/user"
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
},
{
"type": "npm",
@@ -294,22 +279,10 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
}
},
{
"label": "build-sdk",
"type": "shell",
"command": "bun run build:sdk",
"problemMatcher": [
"$tsc"
],
"options": {
"cwd": "${workspaceFolder}/sdk"
}
}
],
"inputs": [
+2 -12
View File
@@ -2,10 +2,6 @@
.vscode/**
.vscode-test/**
.worktrees/**
# Agent tooling, never shipped in the VSIX
.agents/**
.claude/**
.codex/**
CLAUDE.local.md
out/
dist-standalone/
@@ -26,14 +22,8 @@ eslint-rules/**
.husky/**
.env
# sdk (separate monorepo with its own build/release pipeline)
sdk/**
# Source-of-truth for the marketplace README (the .vsix only ever sees the
# README.md that scripts/marketplace-readme.mjs swaps into place). The backup
# only exists if a publish aborts mid-swap; neither should ship in the .vsix.
README.marketplace.md
.README.github.bak
# cli
cli/**
# Custom
**/demo.gif
+940
View File
@@ -0,0 +1,940 @@
# SDK Migration — Architecture & Design
Evergreen reference for the Cline SDK migration project. For the
living implementation plan, see `migration.md`.
## References
### Code References
Cline SDK is at ~/clients/cline/sdk-wip
Cline (core, classic VSCode extension, CLI) is at ~/clients/cline/cline
JetBrains Plugin is at ~/clients/cline/intellij-plugin
IntelliJ open source reference is at ~/clients/cline/intellij-community
JCEF (Java-Chromium embedded framework reference) is at ~/clients/cline/jcef
You can use kb_search with these identifiers to understand the
existing code and the SDK code:
cline - Cline core, classic VSCode extension, CLI
sdk - Cline SDK
plugin - JetBrains plugin
vscode - Visual Studio Code opens source
ij - IntelliJ open source
jcef - JCEF IntelliJ's embedded Chromium layer
Prototype VSCode extension on SDK is at ~/clients/cline/sdk-vscode-sample
Prototype JetBrains plugin on SDK is at ~/clients/cline/sdk-intellij-plugin-sample
These are prototypes with features missing and added, so refer to them
as examples, but don't overindex on them.
### Documentation references
See the ~/clients/cline/cline/docs for extension product documentation
and ~/clients/cline/sdk-wip/*.md for SDK documentation.
### Background on the products & architecture
There's a VSCode extension in cline/src. A large part of its UI is a
React-based webview in cline/webview-ui.
There's a JetBrains plugin in the intellij-plugin repo. It packages
the core of the VSCode extension, including the webview, and
communicates with it with protobufs. There's a bunch of stuff in the
cline repo called "standalone" which is what JetBrains communicates
with.
In cline/cli there's a CLI and Kanban tool. Those use the SDK/are
being ported separately to the SDK, so you don't need to worry about
them. It is OK if you have to break them. Just ignore them.
Note, there was an earlier, failed attempt at a cli in go. The go cli
used to use "standalone" like JetBrains. If there's any old go support
cluttering up the repo, it is fine to delete it and clean it up.
The source code and docs mentioned above are the best reference to the
product architecture, behavior, etc. Feel free to ask clarifying
questions when necessary.
---
## Features
### Features to remove
Terminal integration: There is legacy code in the VSCode extension,
and stubs in the RPC system for JetBrains, which used the IDE's
integrated terminals. We have decided we don't need these old modes
and they should be removed. Instead we will rely on "background
terminal". This literally means the node code forks and execs a shell
and uses pipes to communicate with it.
Browser automation: Remove the system that uses Playwright to automate
browsers. These use cases are now well served by third-party MCP
tools.
"Shadow git" checkpointing system. This is too slow, especially on
Windows. The way the Kanban project uses the existing git repo to
store references, and only after each user message, is better. So we
will drop the "shadow copy" git checkpointing system.
Memory bank, structured context: multi-file documentation
(projectbrief, productContext, activeContext, systemPatterns,
techContext, progress.)
Memory bank, persistence: cross-session context preservation.
Focus chain, task tracking: Auto-generated to-do list with real-time
progress indicators.
Focus chain, integration: Editable focus chain integration with deep
planning and reminders.
Deep planning exploration, output and the /deep-planning command.
Workflows (definition, natural language + XML tool syntax, MCP tools
and user input prompts) ... these have been superceded by SKILLS
moving forward.
Slash commands no longer necessary:
/deep-planning (codebase investigation + plan... we have plan/act mode)
/reportbug (bug reporting with diagnostics)
Custom workflows (/workflow.md for user-defined workflows... we have "skills" now.)
### Core features
These must work:
File operations: read, write, search, replace, list files, inspect
code definitions (functions, classes, methods)
Terminal integration: "Background terminal" must work. The agent
relies on this to run npm, git, docker, etc.
Multi-provider AI models: 30+ providers with seamless
switching. There's one provider of note: VSCode has a provider which
hooks up to Copilot using the VSCode LM Provider API. It would be good
to support this *if possible.*
Auto-approve & YOLO mode
- Granular per-tool permission controls
- YOLO mode for maximum automation
- Monitoring, notifications for long-running commands
Auto-compaction
- Summarization automatically compresses conversations when context fills
- Model support for Claude, Gemini, GPT-5, Grok, etc.
Subagents
- Parallel execution of independent research agents
- Isolation with separate context windows
- Cost tracking for task usage per subagent
Web search and web fetch
Worktrees
- git worktrees for parallel sessions
- branch management and .worktreeinclude support
- conflict resolution and merging
Workspaces
- "multi-root" workspaces/projects with multiple root folders
- @workspace:path scoped references
Jupyter Notebooks
- Generate, explain, and improve notebook cells
Cline Rules
- project-specific .cline/rules and global instructions.
- Conditional logic: Path-based activation of rules.
- Compatibility: Works with Cursor Rules, Windsurf rules, AGENTS.md
Skills
- SKILL.md format with YAML frontmatter
- Loading levels: Metadata, instructions and resources
- Scope: Global and project-specific; toggleable
Hooks
- Events: Task lifecycle + tool events (TaskStart, PreToolUse, etc.)
- Runtimes: bash, powershell for Windows
- IO: JSON
- Context injection: Be able to modify or inject context dynamically
.clineignore
- Exclusion rules, gitignore-style file/directory exclusion
- Exceptions: ! prefix for overrides
- Override behavior: Explicit @ mentions bypass ignore rules
MCP (Model Context Protocol)
- Server management: Discovery, enable/disable, restart, config editing
- Transport: stdio (local) and SSE (remote)
- ...all the typical use cases for MCP: APIs, browser automation, db queries, etc.
### Core workflows
These must work:
Task Management
Task lifecycle - create and resume tasks; view task history
Cost tracking - token using and cost monitoring per task
Plan & Act Mode
Plan mode - Explore and investigate without modifying files
Act mode - Implementation with approval gates
Model config - Separate model configuration for plan and act mode if
the user desires
State persistence - Mode switching, task switching preserves history
File context (@-mentions)
Context referencing - Reference files, folders, terminal output, git
changes, URLs, commits via @
Slash commands
/newtask (new task)
/smol (compress history)
/newrule (create rules)
### Model Configuration
We want to continue supporting our 30+ providers (Anthropic, OpenAI,
OpenAI Codex, OpenRouter, Google Gemini, AWS Bedrock, DeepSeek,
Cerebras, Qwen, Mistral, Groq, Fireworks, Together, xAI Grok,
Moonshot, Nebius, HuggingFace, LiteLLM, Ollama, LM Studio, and more.)
THE MOST IMPORTANT REQUIREMENT HERE, after continuing to support them,
is to USE THE CREDENTIALS, MODEL NAMES, CONFIGS, etc. WE HAVE
SAVED. Logging people out of their providers is really annoying to
users; regenerating API keys is painful for them.
VSCode LM API provider may be an interesting/unusual provider out of
this set; it only works in VSCode by calling a specific API.
We must continue supporting local models like Ollama and LM Studio.
We must support the Cline provider with unified auth (open a
webbrowser, handle the SSO redirect), built-in billing and credit
display, banners advertising new or free models, stealth/early access
models, organization switching.
### Enterprise Features
Security and governance
- Client-side execution only (no data transmission outside of limited
Telemetry and inference; no remote codebase indexing)
- SSO role-based access control (member, admin, owner)
- Model and tool controls per team
- Remote configuration downloaded and applied by the extension
Observability
- OpenTelemetry, Datadog, Grafana, Splunk integrations
- Real-time analytics, cost breakdown by team, selective audit logging
Infrastructure
- AWS Bedrock, Google Vertex AI, Azure OpenAI integration
- Bring-your-own-inference with custom endpoints
### Priority "P1" (mid priority) items
Checkpoints - automatic file snapshots after each change. Note, the
snapshot system in the VSCode extension and JetBrains plugin which
copies the whole repository is slow, *especially on Windows*, so we
should replace it with one that writes refs directly into the local
git repo. Look at the way the kanban project does it; this is
preferred (and maybe we should extract and share this code.)
Diffing - compare changes between checkpoints
Restore - restore files, task to a point, or both
MCP Marketplace - we could get rid of this, but ultimately we do want
this feature with major improvements like allowing remote install. For
now, consider removing it, but if it is easier to keep it around let's
do that to lay the groundwork for improvements.
### Priority "P2" (lower priority) items
Task organization - favorites for task grouping and management
File context - Drag and drop files to add to context
File context - actions - context menus to add to Cline, fix, explain, improve
Slash commands
/explain-changes (git diff explanation)
---
## Detailed Design
### Naming: "Sdk..." considered harmful
Do not name types "SdkFoo" or folders "sdk". If you need to use SdkFoo
as a way to keep two classes around in parallel while you're porting,
that's OK, but when this project is done we want to have one clean,
simple codebase; the SDK backing is an implementation detail so just
use simple noun phrases for classes, etc. and don't litter "Sdk" all
over identifiers and folders.
### Proto deprecation and removal
We don't need proto files to describe webview messages. The webview
and extension backend are both in TypeScript and are versioned and
shipped together. We just need to use shared TypeScript interfaces
between them.
We *also* don't need proto to describe JetBrains <--> node
exchanges. We just need something typed and in sync between Kotlin and
TypeScript that we can serialize. JSON probably makes sense. Protos
are OK but we have had problems with that setup creating a ton of GC
pressure on the Kotlin side, hit maximum message size limits, etc.
There are proto build steps which we can remove, as we use proto less.
protos are useful for state which is serialized. If there are files
that are persisted described by protos, it is ok/good to keep
them. Don't expand the use of protos to places protos are not already
used.
### Web View UI
The Webview UI is very dependent on state arising from implementation
details of the pre-SDK implementation. At the same time, we don't want
to build a new UI from scratch right now because it may be forcing too
many changes upon our users at once. So we aim to reuse the existing
webview, but with radical simplificiation in its state management now
that we will have a cleaner architecture in the extension "backend"
with the layering enforced by the SDK.
The webview UI had defects like showing the wrong keybindings for
JetBrains, or using tons of memory or CPU cycles by spamming state
updates really rapidly or sending n^2 state updates as they streamed
in. The first principle of this migration to the SDK is not get worse,
but at the same time, we expect the state clean-up necessary in the
webview will lead to radical simplifications which make some
low-hanging fruit available in performance. It's great to go make
those improvements where they are available.
We don't need this UI to be pixel perfect identical. We need it to be
FAMILIAR, NOT WORSE and preferably BETTER than the status quo.
### Data formats, settings
We MUST pick up existing on-disk state for settings, etc. We don't
want to log users out of their inference providers as we make this
change to the SDK.
The CLI, VSCode extension and JetBrains extension largely share state
on disk. We should continue that situation. If data migrations are
necessary, that's fine, but design them with care. In particular, we
want the long term to be fast, so we should write breadcrumbs
indicating when migration is done. In addition, users can upgrade and
downgrade their extension versions, etc. and we want to be robust to
that in addition to all kinds of failures. For example, in the past we
had problems where we overwrote a JSON settings file, perhaps racily,
and left trailing }s in the file and this caused the product to
totally fail. That's a very serious issue for our users so pay extra
effort and attention to what is happening on disk. (We want PRACTICAL
solutions and robustness and not performative solutions that just add
tons of code and complexity with no real benefit.)
Invalidating old checkpoints is acceptable, unless it is particularly
cheap to support the classic checkpoints. We won't be authoring those
checkpoints any more, and it would be heavy to migrate them.
We want to move from .clinerules (old style) to .cline/rules (new style.)
### Telemetry
We generally want to continue sending the same Telemetry events. If
that is hard, make a detailed report and we can follow up with our
backend team. Note some enterprise features depend on OTEL
observability.
### Code Sharing
In general we should share code between IDEs where there are benefits
to do so. However trivial tools, or tools specific to a given IDE, can
be wired up directly from the extension through to the SDK. (This is
something that was hard to do in the old architecture and we would
like to make easier.)
---
## Research Findings
### SDK Session Backend Extensibility
**Question**: Does the SDK's `SessionBackend` interface support
storing arbitrary per-task data (e.g., tool settings, hook
configuration, auto-approve preferences per task)?
**Answer**: Partially. The `SessionRow` has a `metadata:
Record<string, unknown> | null` field that can store arbitrary
key-value data per session. This is sufficient for per-task settings
like auto-approve preferences, tool configuration, etc.
The SDK supports three backend implementations:
1. `SqliteSessionStore` — SQLite-backed (default, preferred)
2. `FileSessionService` — JSON file-backed (fallback when SQLite
unavailable)
3. `RpcCoreSessionService` — delegates to an RPC server
For our migration, we'll use either `FileSessionService` or provide a
custom `SessionPersistenceAdapter` that reads/writes our existing task
history format. The `ClineCoreOptions.sessionService` field accepts
any backend implementing `CoreSessionService | RpcCoreSessionService |
FileSessionService`.
**Key finding**: The `SessionPersistenceAdapter` interface is the
cleanest extension point. It requires implementing: `ensureSessionsDir`,
`upsertSession`, `getSession`, `listSessions`, `updateSession`,
`deleteSession`, `enqueueSpawnRequest`, `claimSpawnRequest`. Our
`LegacySessionBackend` adapter wraps the existing
`~/.cline/data/tasks/` directory and `taskHistory` JSON array in
`globalState.json`, mapping between `SessionRow` fields and our
`HistoryItem` type:
```
HistoryItem.id → SessionRow.sessionId
HistoryItem.ts → SessionRow.startedAt (ISO string)
HistoryItem.task → SessionRow.prompt
HistoryItem.tokensIn → metadata.tokensIn
HistoryItem.tokensOut → metadata.tokensOut
HistoryItem.totalCost → metadata.totalCost
HistoryItem.modelId → SessionRow.model
HistoryItem.isFavorited → metadata.isFavorited
```
Per-task files (`api_conversation_history.json`, `ui_messages.json`)
map to `SessionRow.messagesPath` and `SessionRow.transcriptPath`.
**Decision**: We will provide a custom `SessionPersistenceAdapter`
that translates between our existing format and the SDK's interface.
No need for a separate sidecar storage layer. The `metadata` field
handles all per-task extensions.
### Telemetry Event Mapping
The extension currently emits telemetry events via a PostHog-based
`TelemetryService`. The SDK has its own `TelemetryService` with
pluggable adapters (`OpenTelemetryAdapter`, `LoggerTelemetryAdapter`).
**Mapping of current extension events → SDK events:**
| Extension Event | SDK CORE_TELEMETRY_EVENTS | Notes |
|---|---|---|
| `user.extension_activated` | `CLIENT.STARTED` ("extension.activated") | ✅ Same event name |
| `user.auth_started` | `USER.AUTH_STARTED` | ✅ Direct match |
| `user.auth_succeeded` | `USER.AUTH_SUCCEEDED` | ✅ Direct match |
| `user.auth_failed` | `USER.AUTH_FAILED` | ✅ Direct match |
| `user.auth_logged_out` | `USER.AUTH_LOGGED_OUT` | ✅ Direct match |
| `task.created` | `TASK.CREATED` | ✅ Direct match |
| `task.restarted` | `TASK.RESTARTED` | ✅ Direct match |
| `task.completed` | `TASK.COMPLETED` | ✅ Direct match |
| `task.conversation_turn` | `TASK.CONVERSATION_TURN` | ✅ Direct match |
| `task.tokens` | `TASK.TOKEN_USAGE` | ✅ Direct match |
| `task.mode` | `TASK.MODE_SWITCH` | ✅ Direct match |
| `task.tool_used` | `TASK.TOOL_USED` | ✅ Direct match |
| `task.skill_used` | `TASK.SKILL_USED` | ✅ Direct match |
| `task.diff_edit_failed` | `TASK.DIFF_EDIT_FAILED` | ✅ Direct match |
| `task.provider_api_error` | `TASK.PROVIDER_API_ERROR` | ✅ Direct match |
| `task.mention_used` | `TASK.MENTION_USED` | ✅ Direct match |
| `task.mention_failed` | `TASK.MENTION_FAILED` | ✅ Direct match |
| `task.mention_search_results` | `TASK.MENTION_SEARCH_RESULTS` | ✅ Direct match |
| `task.subagent_started` | `TASK.SUBAGENT_STARTED` | ✅ Direct match |
| `task.subagent_completed` | `TASK.SUBAGENT_COMPLETED` | ✅ Direct match |
| `hooks.discovery_completed` | `HOOKS.DISCOVERY_COMPLETED` | ✅ Direct match |
| `session.started` | `SESSION.STARTED` | ✅ Direct match |
| `session.ended` | `SESSION.ENDED` | ✅ Direct match |
**Extension events with NO SDK equivalent (need adapter-layer emit):**
| Extension Event | Action |
|---|---|
| `user.opt_out` / `user.opt_in` | Emit via SDK's `captureRequired()` |
| `user.telemetry_enabled` | Emit via SDK's `capture()` |
| `user.extension_storage_error` | Emit via SDK's `capture()` |
| `user.onboarding_progress` | Emit via SDK's `capture()` |
| `workspace.*` (initialized, vcs_detected, etc.) | Emit via SDK's `capture()` |
| `task.feedback` | Emit via SDK's `capture()` |
| `task.option_selected` / `task.options_ignored` | Emit via SDK's `capture()` |
| `task.checkpoint_used` | Emit via SDK's `capture()` |
| `task.mcp_tool_called` | Emit via SDK's `capture()` |
| `task.historical_loaded` | Emit via SDK's `capture()` |
| `task.retry_clicked` | Emit via SDK's `capture()` |
| `task.slash_command_used` | Emit via SDK's `capture()` |
| `task.feature_toggled` | Emit via SDK's `capture()` |
| `task.rule_toggled` | Emit via SDK's `capture()` |
| `task.auto_condense_toggled` | Emit via SDK's `capture()` |
| `task.yolo_mode_toggled` | Emit via SDK's `capture()` |
| `task.terminal_*` (execution, output_failure, hang) | Emit via SDK's `capture()` |
| `task.initialization` | Emit via SDK's `capture()` |
| `task.summarize_task` | Emit via SDK's `capture()` |
| `ui.*` (model_selected, button_clicked, etc.) | Emit via SDK's `capture()` |
| `hooks.enabled` / `hooks.disabled` | Emit via SDK's `capture()` |
| `hooks.cancel_requested` | Emit via SDK's `capture()` |
| `hooks.context_modified` | Emit via SDK's `capture()` |
| `worktree.*` | Emit via SDK's `capture()` |
| `host.detected` | Emit via SDK's `capture()` |
**Extension events being REMOVED (features deleted):**
| Extension Event | Reason |
|---|---|
| `task.browser_tool_start/end/error` | Browser automation removed |
| `task.focus_chain_*` (6 events) | Focus chain removed |
| `task.workspace_search_pattern` | Folded into SDK search |
| `task.subagent_enabled/disabled` | Toggle events; SDK manages directly |
| `task.cline_web_tools_toggled` | Feature simplified |
| `cline.grpc.response.size_bytes` | gRPC being removed |
**Metrics (OTEL counters/histograms):**
The extension has ~30 OTEL metrics (`cline.turns.total`,
`cline.tokens.input.total`, `cline.api.ttft.seconds`, etc.). The SDK
telemetry service supports `recordCounter`, `recordHistogram`, and
`recordGauge`. We will emit these same metrics from the adapter layer
using `telemetry.recordCounter()` / `telemetry.recordHistogram()`.
The metric names can stay the same.
**Decision**: The SDK's `ITelemetryService.capture()` is a generic
event emitter — we can emit ALL extension events through it. The
adapter layer will create a thin telemetry wrapper that provides
the same `captureTaskCreated()`, `captureToolUsage()`, etc. methods
but delegates to the SDK's telemetry service. Events where the SDK
already has a helper function (listed in the first table) use those
directly. Others use `capture({ event, properties })`. No backend
team coordination needed for the initial migration.
### JetBrains IPC Design
#### Current Architecture
```
┌── Kotlin Plugin ──────────────────────────────┐
│ │
│ CoreProcessManager │
│ └─ launches Node.js process (cline-core) │
│ └─ communicates via gRPC ProtoBus │
│ (port 26040-26340) │
│ │
│ HostBridgeService (gRPC server, port 26041) │
│ ├─ DiffService │
│ ├─ WindowService (show file, open dialog) │
│ ├─ WorkspaceService (paths, diagnostics) │
│ ├─ EnvService │
│ └─ TestingService (get webview HTML) │
│ │
│ ProtoBusProxyService │
│ └─ proxies webview ↔ cline-core gRPC │
│ │
│ JsPostMessageHandler │
│ └─ injects JS bridge into JCEF webview │
│ └─ converts postMessage → gRPC request │
│ │
│ WebViewManager │
│ └─ loads webview HTML in JCEF │
│ └─ receives gRPC responses → postMessage │
└────────────────────────────────────────────────┘
```
Problems with this architecture:
- **Proto size limits**: gRPC messages hit 256MB limits with large
conversations. The `ProtoBusProxyService` logs warnings at 10MB+.
- **Java heap pressure**: Serializing/deserializing large proto
messages stresses the JVM heap.
- **Build complexity**: Proto compilation required for both
TypeScript and Java/Kotlin.
- **Stateless-in-theory**: The design is somewhat stateless but we
haven't leveraged restart-for-reliability because state
reconstruction is expensive.
#### Target Architecture
```
┌── Kotlin Plugin ──────────────────────────────┐
│ │
│ CoreProcessManager │
│ └─ launches SDK sidecar (Node.js) │
│ └─ communicates via JSON-RPC over stdio │
│ │
│ HostCallbackService (JSON-RPC server) │
│ ├─ showTextDocument, openDialog │
│ ├─ getWorkspacePaths, getDiagnostics │
│ ├─ getEnvVars, clipboard │
│ └─ (extensible for PSI, run configs, etc.) │
│ │
│ WebviewBridge │
│ └─ receives JSON messages from sidecar │
│ └─ forwards to JCEF via executeJavaScript │
│ └─ receives postMessage from JCEF │
│ └─ forwards to sidecar via stdio │
│ │
│ WebViewManager │
│ └─ loads adapted webview in JCEF │
└────────────────────────────────────────────────┘
┌── SDK Sidecar (Node.js) ──────────────────────┐
│ │
│ SidecarMain │
│ └─ JSON-RPC over stdio (bidirectional) │
│ └─ imports @clinebot/core │
│ └─ shares SDK adapter layer with VSCode │
│ │
│ ClineCore instance │
│ └─ session management │
│ └─ tool execution │
│ └─ provider handling │
│ │
│ HostCallbackClient │
│ └─ calls back to Kotlin for IDE ops │
│ └─ registered as tool executors in SDK │
│ │
│ WebviewBridge │
│ └─ translates SDK events → webview messages │
│ └─ same code as VSCode adapter │
└────────────────────────────────────────────────┘
```
#### IPC Mechanism: JSON-RPC over stdio
**Why JSON-RPC over stdio instead of gRPC:**
- **No message size limits**: JSON over stdio has no inherent size
cap. Conversations with 100K+ tokens serialize to ~5-20MB JSON
which flows fine over pipes.
- **No heap pressure**: No proto serialization on the Java side.
Kotlin reads/writes JSON strings directly. JCEF already works
with JSON.
- **No proto compilation**: Eliminates the Java protobuf dependency
and the dual TypeScript/Java proto generation step.
- **Simple**: Well-understood protocol. Easy to debug (just read
the pipe).
**Protocol**: JSON-RPC 2.0 over stdin/stdout with newline-delimited
JSON messages. The sidecar reads from stdin and writes to stdout.
Stderr is reserved for logging.
```
→ {"jsonrpc":"2.0","method":"session/start","params":{...},"id":1}
← {"jsonrpc":"2.0","result":{"sessionId":"..."},"id":1}
← {"jsonrpc":"2.0","method":"webview/message","params":{"type":"assistant_delta","text":"..."}}
```
**Notifications** (no `id`) are used for streaming events
(assistant deltas, tool events, state updates). The Kotlin plugin
processes these and forwards them to the JCEF webview.
**Callbacks** from sidecar → Kotlin (host operations) use
JSON-RPC requests in the reverse direction:
```
← {"jsonrpc":"2.0","method":"host/showTextDocument","params":{"path":"..."},"id":100}
→ {"jsonrpc":"2.0","result":{"success":true},"id":100}
```
#### Code Sharing Between VSCode, JetBrains, and CLI
The shared SDK adapter layer contains:
```
src/sdk-adapter/
index.ts — ClineSdkHost (creates ClineCore instance)
session-backend.ts — LegacySessionBackend adapter
webview-bridge.ts — SDK events → webview message translation
provider-migration.ts — Legacy provider settings migration
approval-adapter.ts — Auto-approve settings → SDK tool policies
telemetry-adapter.ts — Extension telemetry → SDK telemetry
types.ts — WebviewInbound, WebviewOutbound types
```
Each host then has a thin integration layer:
- **VSCode** (`src/hosts/vscode/sdk-extension.ts`): In-process.
Creates `ClineSdkHost`, registers VSCode LM handler, manages
webview lifecycle. Uses `postMessage` for webview communication.
- **JetBrains** (`src/sidecar/main.ts`): Separate process. Creates
`ClineSdkHost`, reads/writes JSON-RPC on stdio. Registers
`HostCallbackClient` for IDE operations. The webview bridge code
is identical — it just sends messages over stdio instead of
`postMessage`.
#### Statefulness and Reliability
The sidecar is **stateful** — it holds the `ClineCore` instance with
active sessions in memory. However, it is designed for **graceful
restart**:
- **Session persistence**: All session state is written to disk
after each turn (messages, manifest, transcript). On restart, the
sidecar re-reads the session index and can resume.
- **Crash detection**: The Kotlin plugin monitors the sidecar
process. If it exits unexpectedly, the plugin restarts it after a
brief delay (same as current `CoreProcessManager.RESTART_DELAY`).
- **Smaller messages**: Because the protocol is JSON-RPC with
incremental streaming (notifications for each delta), the
messages are much smaller than the current gRPC approach which
sends full state snapshots. This eliminates the heap pressure
that made the current system unreliable.
- **Interrupted operations**: If the sidecar crashes mid-turn, the
next startup detects the unfinished session (status = "running"
but no live process) and marks it as interrupted, just like the
current task resumption flow.
#### JetBrains-Specific Tools
The HostCallback pattern makes it easy to add JetBrains-specific
capabilities without changing shared code:
1. **Registration**: The sidecar's `HostCallbackClient` declares
what capabilities the host supports (e.g., `"psi"`, `"runConfigs"`).
2. **Tool Executors**: JetBrains-specific tool executors are
registered in `ClineCoreOptions.defaultToolExecutors` when the
sidecar starts. For example, a `getDiagnostics` executor that
calls `host/getDiagnostics` via JSON-RPC to get IntelliJ's PSI
analysis results.
3. **No shared code changes**: Adding a new JetBrains capability
requires:
- Implementing the handler in Kotlin (`HostCallbackService`)
- Adding a JSON-RPC method in the sidecar's `HostCallbackClient`
- Optionally registering a custom tool executor
Example for exposing JetBrains PSI:
```kotlin
// Kotlin side
"host/getPsiStructure" -> {
val file = PsiManager.getInstance(project).findFile(virtualFile)
// ... extract structure
respondWithJson(result)
}
```
```typescript
// Sidecar side - registered as a custom tool executor
defaultToolExecutors: {
list_code_definition_names: async (args) => {
// Call back to JetBrains for richer PSI-based results
const result = await hostCallback.call("host/getPsiStructure", { path: args.path });
return result;
}
}
```
---
## Architecture Overview
### Current Architecture
```
┌─── VSCode Extension ──┐ ┌── JetBrains Plugin ──┐ ┌──── CLI ────┐
│ WebviewProvider │ │ Kotlin Plugin │ │ React Ink │
│ Controller │ │ CoreProcessManager │ │ ClineAgent │
│ Task │ │ ProtoBusProxy │ │ │
│ API providers (30+) │ │ JCEF WebView │ │ │
│ McpHub │ │ │ │ │
│ Webview (React) │ │ ↓ gRPC │ │ │
│ │ │ cline-core │ │ │
│ proto/cline/*.proto │ │ (standalone Node) │ │ │
└────────────────────────┘ └──────────────────────┘ └─────────────┘
```
### Target Architecture
```
┌─── VSCode Extension ──┐ ┌── JetBrains Plugin ──┐ ┌──── CLI ────┐
│ SDK Adapter Layer │ │ Kotlin Plugin │ │ React Ink │
│ @clinebot/core │ │ │ │ TUI │
│ (in-process) │ │ ↓ JSON-RPC/stdio │ │ │
│ Webview (adapted) │ │ SDK sidecar (Node) │ │ │
│ │ │ @clinebot/core │ │ │
│ registerHandler │ │ │ │ │
│ ("vscode-lm", ...) │ │ Webview (adapted) │ │ │
└────────────────────────┘ └──────────────────────┘ └─────────────┘
All clients backed by:
@clinebot/core → @clinebot/agents → @clinebot/llms
↓ ↓ ↓
Sessions Tools/Hooks Providers
Storage MCP Bridge Model Catalog
Telemetry Teams/Spawn Handler Registry
```
---
## What the SDK Already Provides
These capabilities exist in the SDK and do not need to be rebuilt:
1. **Legacy provider settings migration**
`migrateLegacyProviderSettings()` reads `globalState.json` +
`secrets.json`, writes to `providers.json`. Handles Anthropic,
OpenAI, OpenAI Codex OAuth, OpenRouter, Bedrock, custom
OpenAI-compatible endpoints, etc. Existing providers are never
overwritten. Migrated entries are tagged `tokenSource: "migration"`.
2. **30+ provider handlers** — Anthropic, OpenAI (chat + responses
API), Google Gemini, AWS Bedrock, Vertex AI, DeepSeek, Ollama,
LM Studio, Mistral, Groq, Fireworks, Together, xAI, Cerebras,
LiteLLM, Nebius, HuggingFace, and more.
3. **Custom handler registry**`registerHandler(id, factory)` and
`registerAsyncHandler(id, factory)` for providers that need
host-specific dependencies (e.g., VSCode LM API).
4. **MCP management**`InMemoryMcpManager` with stdio, SSE, and
streamableHttp transports. Config loader reads from
`~/.cline/data/settings/mcp.json` with Zod validation. Supports
legacy format migration.
5. **Tool framework** — 8 built-in tools: `read_files`,
`search_codebase`, `run_commands`, `editor`, `apply_patch`,
`fetch_web_content`, `skills`, `ask_question`. Preset system with
`development` (act mode) and `readonly` (plan mode) presets.
Per-tool enable/disable. Policy-based approval (auto-approve,
require-approval, per-tool overrides). Model-aware tool routing
(e.g., OpenAI models use `apply_patch` instead of `editor`).
6. **Session lifecycle**`ClineCore.create()``host.start()` /
`host.send()` / `host.abort()` / `host.stop()` / `host.subscribe()`
Interactive mode with prompt queueing (`queue`/`steer` delivery).
Event subscription for streaming.
7. **Telemetry**`TelemetryService` with pluggable adapters:
`OpenTelemetryAdapter` (for enterprise OTEL), `LoggerTelemetryAdapter`.
Standard events: `session.started`, `session.ended`,
`task.created`, `task.conversation_turn`, `task.tool_used`, etc.
See "Telemetry Event Mapping" above for full mapping.
8. **Rules & Skills** — Discovery from `.clinerules/`,
`~/Documents/Cline/Rules`, `~/.cline/data/settings/rules/`.
SKILL.md format with YAML frontmatter. Global and project scopes.
9. **Hooks**`HookEngine` with lifecycle events. Node subprocess
hook helpers for bash/powershell execution.
10. **Subagents/Teams**`AgentTeamsRuntime`, spawn tools, team
coordination with concurrent teammate agents.
11. **System prompt generation**`getClineDefaultSystemPrompt()`
with platform-aware customization.
12. **OAuth token management**`RuntimeOAuthTokenManager` handles
automatic token refresh during sessions for OAuth providers
(Cline, OpenAI Codex).
13. **Storage isolation**`CLINE_DIR`, `CLINE_DATA_DIR`,
`CLINE_SESSION_DATA_DIR` environment variables plus
`setClineDir()` / `setHomeDir()` APIs for test isolation.
---
## Test Strategy
See `migration.md` for the phase-by-phase test plan. This section
covers the evergreen test infrastructure and classification.
### Test Infrastructure
**Extension unit tests** use Mocha with a custom `requires.ts` that
mocks `vscode` and `@integrations/checkpoints` modules. Config in
`.mocharc.json`. These tests run without VSCode.
**Webview tests** use Vitest with React Testing Library. Independent
from the extension — they test React components in isolation.
**E2E tests** use Playwright to drive a real VSCode instance with
the extension loaded. They test chat, auth, diff editing, and editor
integration against a mock API server on localhost:7777.
**SDK adapter tests** use Vitest (simpler setup, better TypeScript
support, no need for vscode-mock since adapter layer is
VSCode-independent). Config in `vitest.config.sdk.ts`.
### SDK Storage Isolation for Tests
The SDK fully supports isolated test environments via environment
variables and API calls:
```typescript
import { setClineDir, setHomeDir } from "@clinebot/shared/storage";
const tempHome = mkdtempSync(join(tmpdir(), "test-home-"));
process.env.HOME = tempHome;
process.env.CLINE_DIR = join(tempHome, ".cline");
process.env.CLINE_DATA_DIR = join(tempHome, ".cline", "data");
setHomeDir(tempHome);
setClineDir(process.env.CLINE_DIR);
```
### Test Framework Decisions
- **Keep Mocha** for extension unit tests (existing infrastructure)
- **Keep Vitest** for webview and CLI tests
- **Keep Playwright** for VSCode E2E tests
- **Add Vitest** for new SDK adapter tests
- **Isolated home directories** for all new tests touching storage
---
## Manual QA Guide
This section is for the QA team. It describes what has been removed
(so you don't file bugs for missing features) and what areas carry
the most regression risk after the SDK migration.
### Removed Features — Do Not File Bugs
- **Browser automation** — The built-in Playwright browser tool is
gone. Users should use third-party MCP browser tools instead.
- **IDE terminal integration** — Commands now run exclusively in a
"background terminal" (headless shell). No terminal tab opens.
- **Shadow-git checkpoints** — Existing checkpoints are invalidated.
- **Memory bank / structured context** — All memory bank files and
UI removed.
- **Focus chain** — No focus chain panel or inline indicators.
- **Deep planning / `/deep-planning`** — Plan/Act mode remains as
the replacement.
- **`/reportbug`** — Removed.
- **Workflows** — Skills (SKILL.md format) are the replacement.
- **Custom workflow slash commands** — Skills replace this.
### Risk Areas — VSCode Extension
1. **Provider credentials & model selection** — Verify existing API
keys survive the upgrade and downgrade.
2. **Cline provider OAuth / SSO** — Test sign-in, sign-out, token
refresh, org switching.
3. **Chat streaming & message display** — Watch for missing/duplicated
messages, broken streaming, performance regressions.
4. **Tool approval flow** — Verify auto-approve, YOLO mode, per-tool
permissions.
5. **Plan/Act mode** — Verify toggling, separate model configs,
state persistence.
6. **Task history & resume** — Old tasks appear, can be resumed; new
tasks are saved.
7. **MCP servers** — Existing configs picked up, tools work.
8. **VSCode LM provider (Copilot)** — Verify it still works.
9. **Settings UI** — All toggles and inputs persist correctly.
10. **Webview performance** — Long conversations should not cause
sluggishness.
### Risk Areas — JetBrains Extension
1. **Sidecar process lifecycle** — Starts reliably, auto-restarts,
shuts down cleanly.
2. **Webview communication** — Messages arrive, state is fresh.
3. **Large conversations** — 100K+ tokens without OOM.
4. **Host operations** — Open file, diagnostics, clipboard all work.
5. **Keybindings** — Correct for the JetBrains platform.
### Risk Areas — CLI
1. **Agent backend replacement** — Core loop works in TUI and
headless modes.
2. **Provider & model picker** — All providers appear, defaults
correct.
3. **Shared state with IDE clients** — Credentials and history
visible across clients.
4. **Slash commands** — Removed commands don't appear; remaining
ones work.
5. **Worktrees & `--cwd`** — Function correctly.
6. **ACP (Agent Communication Protocol)** — Programmatic usage works.
+145
View File
@@ -0,0 +1,145 @@
# SDK Migration Caveats & Known Issues
Tracking issues found during the migration from the legacy inference system to the ClineCore SDK.
## Status Legend
- 🔴 **Blocker** — prevents core functionality
- 🟡 **Minor** — cosmetic or UX annoyance
- 🟢 **Fixed** — resolved
NOTE:
1. Use your debugging tool (DEBUG-HARNESS.md) to reproduce issues.
2. Use your debugging tool to validate your fixes.
3. Commit one verified change together.
4. Work on these in any order you prefer.
---
🟢 Under accounts, the when logged in the "current balance" is ---- and
the reload button does nothing. **Fixed:** getUserCredits handler fetches
balance from Cline API using stored auth token.
🟢 Under accounts, the "cline environment" dropdown doesn't change from
production when you select "staging" or "local". **Fixed:** state-builder
now reads `clineEnv` from globalState and maps to Environment enum;
updateSettings handler persists clineEnv and clears auth on change.
🟢 Under accounts, the logout button does nothing. **Fixed:**
accountLogoutClicked handler clears auth credentials from disk.
🟢 Reportedly under accounts you can't sign in. **Fixed:**
accountLoginClicked handler (was STUB) now opens the Cline login page
in the browser.
🔴 When you have a low credit balance, even after you change accounts
(for example from one "org" to another) or refreshing you keep getting
an error "Insufficient balance. Your Cline Credits balance ..."
🔴 In chat, a chat response has a "copy" button that is
obscured/partially obscured by the last generated code block. In the
classic extension, this appears with enough space around it to be
visible.
🟢 Changing the model during a conversation does not, *apparently*,
change the model used for inference. **Fixed:** updateSettings now
updates the in-memory apiConfiguration (not just disk) so
model/provider changes take effect immediately for subsequent sessions.
🔴 The OpenAI compatible provider produces "404 404 page not found"
errors.
🔴 When running tools (for example, prompt the agent to use kb_status)
output rectangles appear but they are blank.
🔴 When prompted with multiple-step work (like 1. Do this 2. Do that)
the chat displays "0/0 TODOs".
🟡 Checkpoints appear in options, but checkpoints don't appear in
chats; we need to overhaul the checkpoints system anyway see
ARCHITECTURE.md.
🟢 In the history section, you can't mark chats as favorites.
🟡 Banners (for example "Try Claude Sonnet 4.6") can be dismissed, but
there are no < and > buttons visible to page between them.
🟢 "Add to Cline" right click menu (use the command to trigger it)
does not do anything. **Fixed:** sendAddToInputEvent now falls back to
the SDK bridge's pushAddToInput when no classic gRPC subscriptions are
active; WebviewGrpcBridge handles subscribeToAddToInput streaming and
sends via both gRPC response and typed message.
🟢 When a task is cancelled, you can't enter a new chat and send that
chat in addition. (The repro is: Run a task, click cancel relatively
quickly, type a new prompt, try to hit enter/click the arrow.) **Fixed:**
cancelTask now clears currentSession after abort so subsequent
askResponse calls start a new task instead of sending to the aborted
session.
🟢 MCP Servers tab never finishes loading (may be a workos: token
prefix problem?) **Fixed:** subscribeToMcpServers now sends initial
server data as a typed message (mcpServers) instead of only via gRPC
streaming response, which the webview's dual-listen pattern picks up.
🟢 Attached images (via drag and drop or the + icon to attach an image
file) aren't submitted to models. **Fixed:** newTask and askResponse
now include the images array in ClineMessage objects so attached images
appear in the chat UI and are passed to the SDK session.
🔴 Changing the account profile in the accounts tab (for example from
Cline External, which has budget, to Cline Internal Testing Org, which
doesn't) doesn't switch to that profile for inference.
🟡 Account panel may show logged-out state on launch despite being
logged in. Inference still works. The `subscribeToAuthStatusUpdate`
streaming subscription in the webview may not be established before
the bridge pushes initial auth data, causing a race condition. On most
launches the auth state loads correctly (verified via debug harness),
but the user reports intermittent occurrences.
🔴 "Sign up with Cline" button does not do the IDE login flow — it
opens the dashboard (`https://app.cline.bot/login`) instead. In
origin/main, `accountLoginClicked` calls
`AuthService.createAuthRequest()` which starts a local HTTP server for
the OAuth callback, calls the Cline API auth endpoint with the
callback URL, and opens the resulting OAuth redirect URL. The SDK does
not have access to `AuthService` or `HostProvider.getCallbackUrl()`,
so it falls back to opening the dashboard URL directly. Users who are
not logged in cannot authenticate through the extension UI.
**Requires:** SDK support for OAuth callback flows (see
SDK-FEATURE-REQUESTS.md).
🔴 Buttons in the MCP Servers popup do nothing. The restart (🔄),
enable/disable toggle, and delete (🔴) buttons on individual MCP
servers are all no-ops. The gRPC handler stubs these methods:
`restartMcpServer`, `deleteMcpServer`, `toggleMcpServer`,
`toggleToolAutoApprove`, `authenticateMcpServer`, `updateMcpTimeout`.
In origin/main these go through `controller.mcpHub` which manages live
MCP server connections. The SDK reads MCP settings from disk but does
not expose server lifecycle management to the webview. See SDK-MCP.md
for details, we need to implement much more elaborate MCP support
client side to work with the SDK, via a custom RuntimeBuilder and tool
client factory that supports streamable HTTP; watches the file for
changes and either restarts a session or causes the tool definitions
to change; etc.
🔴 Buttons in the MCP Servers → Configure tab do nothing. Same root
cause as above — the configure tab shows servers (e.g. "linear",
"kamibiki") with restart/toggle/delete controls, but all interactions
are stubbed. The "Configure MCP Servers" and "Advanced MCP Settings"
links also depend on `openMcpSettings` which may or may not be wired.
🟡 MCP Marketplace never loads. The Marketplace tab shows "No MCP
servers found in the marketplace". **Partial fix applied:**
`subscribeToMcpMarketplaceCatalog` in `webview-grpc-bridge.ts` now
reads from the disk cache (`~/.cline/data/cache/mcp_marketplace_catalog.json`)
via `readMcpMarketplaceCatalogFromCache()` and pushes the catalog to
the webview as a streaming response. This works if the cache was
previously populated by the classic extension. However,
`refreshMcpMarketplace` (which fetches fresh data from the API) is
still stubbed because it requires an authenticated API call to
`https://api.cline.bot/v1/mcp/marketplace`. If no cache file exists
(fresh install), the marketplace will remain empty.
**Note:** Could not validate with debug harness since it runs the
classic extension, not the SDK adapter.
-147
View File
@@ -1,152 +1,5 @@
# Changelog
## [3.86.0]
### Added
- Add Claude Opus 4.8 provider support, including 1M-context variants where available.
- Add Moonshot Kimi K2.6 model support.
### Fixed
- Show prompt-cache support for Qwen 3.7 Max in the Cline provider.
- Fix the VS Code nightly publish workflow startup permissions.
### Changed
- Move the VS Code extension project into `apps/vscode`.
## [3.85.0]
### Added
- Add GPT-5.5 support to SAP AI Core.
- Add DeepSeek V4 Flash and Pro models.
- Add Gemini 3.5 Flash to Gemini and Vertex providers.
- Add `/lg-task` URI webhook integration for LG dashboard flows.
### Fixed
- Fix Vertex AI global endpoint handling for Claude models.
- Route Poolside Laguna models through next-gen prompts and native tool calling.
### Changed
- Update `diff` and `protobufjs` dependencies.
## [3.84.0]
### Added
- Add SAP AI Core support for additional hosted models
### Fixed
- Disable the MCP "Restart Server" button when a server is toggled off.
### Changed
- Remove the Cline Kanban launch modal and bundled demo media from the VS Code extension startup flow.
## [3.83.0]
### Fixed
- Show a clear "Searching..." state in the @-mention file picker
- Improve @-mention file search performance
- Allow `write_to_file` to create or overwrite files with empty content.
- Fix validation failures for MCP servers that require an object.
- Enable OpenRouter prompt cache control for Qwen models.
- Update Axios and SAP Connectivity dependencies
### Changed
- Use the VS Code-specific `README.marketplace.md` when packaging and publishing the VS Code extension
- Add telemetry to @-mention search to help diagnose local, remote, and multi-root workspace search behavior.
## [3.82.0]
### Added
- Restore VS Code foreground terminal support and settings.
- Add latest OpenAI, SAP AI Core, and Z AI models.
### Fixed
- Fix hook template JSON escaping.
- Improve ripgrep file search error handling.
### Changed
- Remove hardcoded model lists from docs.
## [3.81.0]
### Added
- Add GPT-5.5 model support for OpenAI Codex subscription users.
### Fixed
- Remove hardcoded "Whats New" fallback items in webview; only remote-configured welcome banners are shown.
### Changed
- Improve cline-core memory diagnostics used by the extension runtime:
- enable near-heap-limit heap snapshots
- add periodic memory usage logging
- log discovered heap snapshots on abnormal exits for easier OOM debugging
## [3.80.0]
### Added
- Wire up remote `globalSkills` from enterprise remote config with full UI, toggle support, and system prompt integration — enterprise-managed skills now appear under a dedicated "Enterprise Skills" section and support `alwaysEnabled` enforcement
- Onboarding flow now uses dynamically fetched recommended models instead of a hardcoded list, with a fallback to the welcome view on failure
- Add dedicated "Quota Exceeded" error message in the chat error UI when Cline account spend caps are hit
### Fixed
- Fix OOM crashes during long conversations by setting `--max-old-space-size=8192` for the cline-core Node.js process (was defaulting to ~2 GB)
- Show detailed error information in the chat error row instead of a generic caught error message
- Update `axios` to 1.15.0 across all packages
### Changed
- Remove foreground terminal mode — all task command execution now defaults to background mode, removing the VS Code integrated terminal dependency and related settings UI
- Remove old hardcoded announcement banners
## [3.79.0]
### Added
- Add Claude Opus 4.7 model support
- Add Azure Blob Storage as a storage provider
- Add `globalSkills` to remote config
- Inline value reuse in user-level remote-config discovery
### Fixed
- Fix cache reflection for Cline and Vercel API handlers
- Fix stuck `command_output` ask when terminal command ends unexpectedly
- Add `use_subagents` to system prompt for GLM, Hermes, and XS models
- Fix action injection security risk
### Changed
- Remove deprecated evals tool
## [3.78.0]
### Added
- Add a dedicated "Spend Limit Reached" error UI when spend caps are hit
- Docs updates
### Fixed
- Show actual `read_file` line ranges in chat UI
## [3.77.0]
### Added
+1
View File
@@ -1,2 +1,3 @@
@.clinerules/general.md
@.clinerules/network.md
@.clinerules/cli.md
+8 -8
View File
@@ -42,14 +42,15 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
```bash
code cline
```
3. Install [bun](https://bun.com)
4. Install the necessary dependencies for the extension and webview-gui:
3. Install the necessary dependencies for the extension and webview-gui:
```bash
cd apps/vscode && npm run install:all && cd ../..
cd sdk && bun run build && cd ..
npm run install:all
```
5. Generate Protocol Buffer files (required before first build):
6. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
4. Generate Protocol Buffer files (required before first build):
```bash
npm run protos
```
5. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
@@ -61,7 +62,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
2. Push your branch and create a PR on GitHub. Our CI will:
- Run tests and checks
3. Testing
- Run `cd apps/vscode && npm run test` to run tests locally.
- Run `npm run test` to run tests locally.
- Before submitting PR, run `npm run format:fix` to format your code
### Extension
@@ -73,7 +74,6 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
- If you dismissed the prompts, you can install them manually from the Extensions panel
2. **Local Development**
- cd into the vscode extension, `cd apps/vscode`
- Run `npm run install:all` to install dependencies
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
- Run `npm run test` to run tests locally
+134
View File
@@ -0,0 +1,134 @@
# E2E Debugging Visibility
This is a sub-project of the project described in migration.md. We are
engaging in a big change to the VSCode extension. You gain visibility
into the extension through tests, but sometimes that is not enough.
Your goal is to create way where you can launch the VSCode extension
and have access to the node debugger and webview debugger so that you
can set break points, evaluate expressions, inject input (consider
Microsoft's work with Playwright in VSCode, but anything that works is
fine) step, etc. so that you can observe execution and find and fix
problems without the tedious cycle off adding print statements,
running a test which may hang, fixing something, removing the print
statements, etc.
For this step to be complete, you need to demonstrate you have the
ability to:
1. Build and run the VSCode extension in an unminified form, including
the webview unminified.
2. Add and remove breakpoints, including conditional breakpoints, on
the extension side.
3. Add and remove breakpoints, including conditional breakpoints, on
the webview side.
4. Evaluate expressions at breakpoints. You should be able to refer to
local variables, that is, the extension code should be
unminified. (Concatenated is fine as long as you can find your way
around.)
5. Run, step at breakpoints.
6. Generate UI actions like opening the Cline sidebar, focusing
elements, typing, etc.
7. Take screenshots that you can view.
You need to use this tool inside your agentic loop, that is, you will
need to drive both of these debugees simultaneously from one loop, so
you may need to write yourself a tool which blocks until one of the
debugees hits a breakpoint; can use a timeout and let you break and
examine isolates and stacks; things of that nature.
We are working on macOS, it is fine if this tool just works on macOS
for now.
## Caveats
- **`CLINE_DIR` environment variable**: Because the debug harness
spawns a fresh VSCode instance that runs the Cline extension, and
because *you* (the agent) share state with that extension (API keys,
provider settings, task history), you **must** set `CLINE_DIR=~/.cline`
when launching the harness. Without it the debugee uses an isolated
data directory and won't have your API keys or provider configuration,
causing inference to fail silently (requests hang or error).
```bash
CLINE_DIR=~/.cline npx tsx src/dev/debug-harness/server.ts --skip-build --auto-launch
```
- **⚠️ "Introducing Cline Kanban" promotion — DISMISS FIRST**: On
fresh launches, a full-screen promotional overlay ("Introducing Cline
Kanban") may appear in the sidebar webview. It obscures all other UI
elements, so screenshots will show only the promo and interactions
with the chat input, settings buttons, etc. will fail. **You must
dismiss it immediately after opening the sidebar, before doing
anything else.** This is easy to forget — if your screenshots look
wrong or interactions fail, this is almost certainly why.
**Method 1 — Click the close button via DOM** (most reliable):
```bash
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
curl localhost:19229/api -d '{
"method": "web.evaluate",
"params": {"expression": "document.querySelector(\".sr-only\")?.parentElement?.click()"}
}'
```
This finds the `<span class="sr-only">Close</span>` element and
clicks its parent `<button>`.
**Method 2 — Press ESC** (simpler but less reliable):
```bash
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
curl localhost:19229/api -d '{
"method": "web.evaluate",
"params": {"expression": "document.activeElement.dispatchEvent(new KeyboardEvent(\"keydown\", {key: \"Escape\", code: \"Escape\", keyCode: 27, bubbles: true}))"}
}'
```
If neither works, take a screenshot (`ui.screenshot`) to see what's
on screen and identify the current dismiss control.
- **Screenshots**: `ui.screenshot` and `ui.sidebar_screenshot` save
PNG files to `/tmp/cline-debug/` and return `{path}` in the JSON
response. **Do NOT open the screenshot file with `open`** — on macOS
this launches Preview.app which covers the VSCode window you're
debugging. Use `read_file` on the returned path to examine the image
without disrupting the debuggee.
- **Debuggee launch delay**: The post-launch activation delay is 1
second. If the extension hasn't fully loaded by the time you interact
with it, just retry — `ui.open_sidebar` and `findSidebar` have their
own internal polling with timeouts.
- **Top toolbar buttons**: The "new task", "mcp servers", "history",
"accounts", and "settings" toolbar buttons are not in the webview DOM
(they're VSCode UI chrome). Instead of trying to click them, run the
associated VSCode commands directly:
- New Task: `cline.plusButtonClicked`
- MCP Servers: `cline.showMcpServers`
- History: `cline.showHistory`
- Accounts: `cline.showAccount`
- Settings: `cline.openSettings`
Example:
```bash
curl localhost:19229/api -d '{
"method": "ui.command_palette",
"params": {"command": "cline.openSettings"}
}'
```
- **CDP disconnects after window reload**: If you use
`workbench.action.reloadWindow` (e.g., to pick up a rebuilt
webview), the extension host CDP connection drops. You must do a full
`shutdown` + relaunch of the debug harness to reconnect.
## References
You can see the vscode source code in ~/clients/cline/vscode and
search it with kb_search vscode
You can search the cline source code (snapshot) with kb_search cline.
File diff suppressed because it is too large Load Diff
+237
View File
@@ -0,0 +1,237 @@
These are issues the agent claims to have fixed, but should be verified:
## Fixed
### 1. 🟢 Remote inference fails with ECONNREFUSED (localhost:4000)
**File:** `src/sdk/cline-session-factory.ts`
**Symptom:** Sending any message with the Cline provider fails after 6 retries with "Cannot connect to API" / ECONNREFUSED to `http://localhost:4000/v1/chat/completions`.
**Root cause:** Base URL overrides (`openAiBaseUrl`, `openRouterBaseUrl`, `liteLlmBaseUrl`) were applied unconditionally to ALL providers, clobbering the Cline API URL. A stale `openAiBaseUrl: "http://localhost:4000/v1"` in `~/.cline/data/globalState.json` overwrote `https://api.cline.bot/api/v1`.
**Fix:** Guard each base URL override with a provider check so it only applies to its respective provider.
---
### 2. 🟢 Task history not persisted after completion
**File:** `src/sdk/SdkController.ts`
**Symptom:** After a task completes successfully, the task did NOT appear in the RECENT section when returning to the home screen.
**Root cause:** `newTask()` created a `currentTaskItem` but never pushed it to `this.taskHistory[]`. `clearTask()` discarded it without saving. No disk persistence implementation existed.
**Fix:** `SdkController` now persists tasks on three paths: (1) `done` event updates `currentTaskItem` with final usage and calls `persistCurrentTask()`, (2) `clearTask()` calls `persistCurrentTask()` before resetting, (3) `cancelTask()` persists the in-progress task. `LegacyStateReader` gained `saveTaskHistory()`, `saveUiMessages()`, and `deleteTaskDirectory()` methods for disk I/O.
### 3. 🟢 Task resumption not implemented
**File:** `src/sdk/SdkController.ts`
**Symptom:** Cannot resume a previous task from history.
**Fix:** `showTaskWithId()` now finds the task in history, loads saved UI messages via `legacyState.readUiMessages()`, restores them into the translator, and sets `currentTaskItem`. The task view renders with full message history.
### 6. 🟢 Settings persistence is best-effort / incomplete
**File:** `src/sdk/SdkController.ts`
**Symptom:** `updateSettings()` was a no-op stub with a TODO comment.
**Fix:** `updateSettings()` now persists settings to `globalState.json` via `legacyState.saveApiConfiguration()`. `updateAutoApprovalSettings()` also persists via the same mechanism.
### 7. 🟢 Completed task not appearing in RECENT section
**Where:** Home screen → RECENT section
**Symptom:** After completing a task and clicking "New Task", the completed task did not appear in the RECENT history list.
**Fix:** Resolved by issue #2 fix — tasks are now persisted to `taskHistory` on completion, so they appear in RECENT.
### 8. 🟢 Top bar buttons are non-functional
**Where:** Header bar — accounts, settings, new chat, history buttons
**Symptom:** Clicking any of the top bar buttons (accounts icon, settings gear, new chat +, task history) does nothing. No navigation occurs, no panels open.
**Root cause:** gRPC stub. The webview subscribes to `subscribeToSettingsButtonClicked`, etc. — these are event streams pushed from the extension host when VSCode title bar buttons are clicked.
**Fix:** Extension.ts button commands now send typed `navigate` messages via `WebviewGrpcBridge.navigate()`, bypassing gRPC streaming subscriptions. Plus button also calls `clearSdkTask()` to reset the SDK session.
### 9. 🟢 @ mentions / autocomplete not working
**Where:** Chat input textarea
**Symptom:** Typing `@` in the chat input does not trigger any autocomplete dropdown. No filename suggestions, no context items offered.
**Root cause:** gRPC stub. The `@` autocomplete calls `FileServiceClient.searchFiles()` to get matching file paths.
**Fix:** Implemented `searchFiles` handler in grpc-handler.ts that delegates to `SdkController.searchFiles()`, which does a real filesystem walk of the workspace directory (max depth 8, skips node_modules/.git/etc). Returns results with `mentionsRequestId` for proper request correlation.
### 10. 🟢 Add files/images button (+) does nothing
**Where:** Bottom bar, "+" button next to chat input
**Symptom:** Clicking the "+" button to add files and images produces no response — no file picker, no dropdown, no action.
**Root cause:** gRPC stub. The button calls `FileServiceClient.selectFiles()` which opens a native file picker dialog.
**Fix:** Implemented `selectFiles` handler that returns `StringArrays` format (`values1` = image data URLs, `values2` = file paths). VscodeWebviewProvider callback reads image files as base64 data URLs and returns relative paths for non-images.
### 11. 🟢 Cannot switch from Plan mode back to Act mode
**Where:** Bottom bar Plan/Act toggle
**Symptom:** Clicking "Plan" successfully switches to Plan mode. However, clicking "Act" after that does NOT switch back to Act mode.
**Root cause:** Proto enum conversion bug — the webview sends numeric enum values (0=PLAN, 1=ACT) but the handler expected string values.
**Fix:** `handleTogglePlanActMode` now converts proto enum values: `0/"PLAN" → "plan"`, `1/"ACT" → "act"`, with fallback for already-converted string values.
### 12. 🟢 "Manage cline rules and workflows" still mentions workflows
**Where:** ClineRulesToggleModal tooltip and aria-label
**Symptom:** The tooltip and aria-label still said "Manage Cline Rules & Workflows".
**Fix:** Updated tooltip to "Manage Cline Rules" and aria-label to "Show/Hide Cline Rules". Also simplified chat placeholder text to remove "workflows" mention.
### 4. 🟢 Input text not cleared immediately on send
**Where:** Webview chat input
**Symptom:** After typing a message and pressing send/enter, the text remains visible in the input field briefly before clearing. Creates a feeling of lag.
**Root cause:** In `useMessageHandlers.ts`, `setInputValue("")` was called AFTER `await TaskServiceClient.newTask(...)` or `await TaskServiceClient.askResponse(...)` completed. The network round-trip caused visible delay before the input cleared.
**Fix:** Moved `setInputValue("")`, `setActiveQuote(null)`, `setSelectedImages([])`, `setSelectedFiles([])` to execute immediately when `hasContent` is true, before any async gRPC calls. React schedules a re-render synchronously, clearing the input before the network round-trip.
### 5. 🟢 api_req_started fires with zeroed token counts
**Where:** Message stream / ChatRow rendering
**Symptom:** An `api_req_started` partial message fires with `{"tokensIn":0,"tokensOut":0,"cost":0}` before real counts arrive, causing a brief flash of "0 / 200.0k" in the token usage bar.
**Root cause:** `ContextWindow.tsx` rendered the token bar whenever `tokenData` existed (i.e., when `contextWindow > 0`), regardless of whether `lastApiReqTotalTokens` was 0.
**Fix:** Added `tokenData.used === 0` guard to the null-return check in `ContextWindow.tsx`. The token bar now only renders when real (non-zero) token data is available.
### 14. 🟢 "Delete chat" button shows placeholder size
**Where:** Task history → delete button tooltip / label
**Symptom:** The "Delete chat" button tooltip displays `Delete Task (size: --)` when task size data is unavailable.
**Root cause:** `DeleteTaskButton.tsx` unconditionally rendered `(size: ${taskSize ? formatSize(taskSize) : "--"})`, showing "--" when `taskSize` is undefined.
**Fix:** Changed to conditionally include size: `taskSize ? \`Delete Task (${formatSize(taskSize)})\` : "Delete Task"`. The tooltip now shows just "Delete Task" when size is unavailable, or "Delete Task (12.4 KB)" when it is.
### 13. 🟢 Terminal settings navigates to blank/stuck webview
**Where:** Settings → Terminal tab
**Symptom:** Opening terminal settings causes React to crash, leaving a blank webview.
**Root cause:** `getAvailableTerminalProfiles` was a gRPC stub returning `{data:{}}`. The webview called `setAvailableTerminalProfiles(response.profiles)` where `response.profiles` was `undefined`, overwriting the default `[]`. Then `TerminalSettingsSection` called `profilesToShow.map()` on `undefined`, crashing React.
**Fix:** Implemented real `handleGetAvailableTerminalProfiles()` handler in grpc-handler.ts that calls `getAvailableTerminalProfiles()` from `utils/shell.ts`, returning platform-specific profiles (Default, zsh, bash on macOS). Also added `availableTerminalProfiles: []` to state-builder.ts as a safety net, and wired `scrollToSettings` to fire `navigate("settings", { targetSection })` via the bridge.
**Verified:** Debug harness confirmed handler returns `{data:{profiles:[{id:"default",...},{id:"zsh",...},{id:"bash",...}]}}`, Settings → Terminal tab renders "Default Terminal Profile" dropdown with all 3 options, shell integration timeout, and terminal reuse settings.
### 16. 🟢 Cline Rules popup still has a "Workflows" tab
**Where:** Scales-of-justice icon → Cline Rules modal
**Symptom:** The "Manage Cline Rules" popup contains a "Workflows" tab. Issue #12 fixed the tooltip text, but the tab itself still exists inside the modal.
**Root cause:** The `ClineRulesToggleModal` component had a full "Workflows" tab with toggle lists for global, local, and remote workflows, plus a description section. Workflows are no longer a feature.
**Fix:** Removed the Workflows tab button, workflows description text, workflows content section (remote/global/local workflow toggle lists), and the remote workflows banner condition from `ClineRulesToggleModal.tsx`. The `currentView` state type was narrowed from `"rules" | "workflows" | "hooks" | "skills"` to `"rules" | "hooks" | "skills"`.
### 17. 🟢 Account pane shows "Sign up with Cline" despite being logged in
**Where:** Account panel / pane
**Symptom:** Even when the user is already authenticated and logged in, the account pane still displays "Sign up with Cline" and other sign-up prompts as if the user were not authenticated.
**Root cause:** `subscribeToAuthStatusUpdate` is a streaming subscription. The bridge's `handleStreamingRequest()` fell into the `default` no-op case, so auth state was never pushed to the webview.
**Fix:** Added explicit `subscribeToAuthStatusUpdate` case in `handleStreamingRequest()` that reads auth credentials from disk and pushes them. Added `roles` to org data and null safety in `isAdminOrOwner()`.
### 18. 🟢 "Sign up with Cline" button does nothing (moot)
**Where:** Account pane → Sign up button
**Fix:** Resolved by #17 — the sign-up button is no longer shown when the user is already authenticated.
### 19. 🟢 Terminal settings still shows "Terminal Execution Mode" option
**Where:** Settings → Terminal
**Fix:** Removed the Terminal Execution Mode dropdown, its handler, and unused imports from `TerminalSettingsSection.tsx`.
### 20. 🟢 Cline provider model type-ahead search does not work
**Where:** Settings → Model selector (Cline provider)
**Root cause:** `refreshClineModelsRpc` was a gRPC stub returning `{}`. The webview never received any model data.
**Fix:** Implemented `handleRefreshClineModels()` in grpc-handler.ts that reads from disk cache first, then falls back to fetching from the Cline API using `globalThis.fetch`. Converts API response to `ModelInfo` records and returns in protobuf format.
### 21. 🟢 Cline provider recommends possibly outdated model
**Where:** Settings → Model selector (Cline provider)
**Fix:** Updated fallback recommendation text in `ClineModelPicker.tsx` from `anthropic/claude-sonnet-4.5` to `anthropic/claude-sonnet-4.6`.
### 22. 🟢 "Use different models for Plan and Act" checkbox immediately unchecks
**Where:** Settings → Model configuration
**Root cause:** `updateSettings()` was writing raw settings instead of merging individual known keys.
**Fix:** `updateSettings()` now iterates known settings keys and writes each one individually. `buildExtensionState()` reads `planActSeparateModels` from `globalState`.
### 23. 🟢 MCP settings Configure tab crashes React
**Where:** MCP Servers → Configure tab
**Root cause:** `refreshMcpMarketplace` stub returns `{}`, replacing the default `{ items: [] }` state, causing `items.find()` to crash.
**Fix:** Added optional chaining (`?.items?.find`) in `getMcpServerDisplayName()`.
### 24. 🟢 History tab is empty and search does nothing
**Where:** History tab (task history list)
**Root cause:** `handleGetTaskHistory()` returned `{ data: { history } }` but webview reads `response.tasks`.
**Fix:** Changed return to `{ data: { tasks, totalCount } }`. Implemented server-side filtering/sorting.
### 25. 🟢 Auto-approve options immediately uncheck when toggled
**Where:** Auto-approve options flyout
**Root cause:** Same as #22 — `updateAutoApprovalSettings()` was not persisting properly.
**Fix:** Fixed alongside #22.
---
### 15. 🟢 MCP tools are missing / not visible to the agent
**Where:** Agent tool execution
**Symptom:** MCP tools that should be available to the agent are not discovered or listed. The agent cannot see or use any MCP-provided tools during task execution.
**Root cause:** `ClineCoreSession` in `cline-session-factory.ts` didn't pass MCP configuration through `coreConfig` when calling `host.start()`. The MCP settings file existed at `~/.cline/data/settings/cline_mcp_settings.json` but the session factory never wired MCP servers into the ClineCore session.
**Fix:** Added `getOrCreateMcpManager()` to `cline-session-factory.ts` that reads MCP server registrations via `resolveMcpServerRegistrations()`, creates an `InMemoryMcpManager` with a client factory using `@modelcontextprotocol/sdk` (supporting stdio, streamableHttp, and SSE transports), connects to all non-disabled servers, generates `Tool[]` via `createMcpTools()`, and passes them as `extraTools` in `coreConfig`. The MCP manager is cached across sessions (servers are long-lived processes). Connection has a 30s timeout to avoid blocking session start. Individual server connection failures are logged but don't prevent other servers or the session from starting.
**Verified:** Debug harness confirmed agent lists `kamibiki__kb_search`, `kamibiki__kb_status`, `kamibiki__kb_index` from the kamibiki MCP server, and successfully invoked `kamibiki__kb_status` returning real indexing data (6 repos, 1M+ embeddings).
---
### 27. 🟢 Banners (e.g., "Try Claude Sonnet 4.6") can't be dismissed
**Where:** Home screen → banner carousel
**Symptom:** Clicking the X dismiss button on any banner does nothing — the banner remains visible and reappears on reload.
**Root cause:** Two issues: (1) `state-builder.ts` hardcoded `dismissedBanners: undefined` instead of reading from globalState, so dismissed banners were never communicated to the webview. (2) `grpc-handler.ts` wrote dismissed banner IDs as plain strings instead of the `{ bannerId, dismissedAt }` objects the webview expects.
**Fix:** State builder now reads `dismissedBanners` from globalState with `normalizeDismissedBanners()` that handles both legacy plain strings and new objects. Handler writes proper `{ bannerId, dismissedAt }` objects and normalizes legacy entries on read.
**Verified:** Debug harness confirmed banners dismiss correctly — carousel shrinks as each banner is dismissed and stays dismissed across reloads.
### 28. 🟢 Can't mark chats as favorites in history
**Where:** History tab → star button on task items
**Symptom:** Clicking the star icon on a history item does nothing — the favorite state never changes.
**Root cause:** Proto field name mismatch in `handleToggleTaskFavorite()`: handler read `request.params?.id` and `request.params?.isFavorite`, but the webview sends `taskId` and `isFavorited` (proto field names from `TaskFavoriteRequest`).
**Fix:** Handler now reads both proto names (`taskId`/`isFavorited`) with fallback to legacy names (`id`/`isFavorite`).
### 29. 🟢 Copy button obscured by last code block in chat
**Where:** Chat response text with code blocks
**Symptom:** The response copy button overlaps with the last code block, making it hard to see and click.
**Root cause:** `CopyButton.tsx` positioned the bottom-right copy button at `bottom-1` (4px from bottom edge), which overlapped with code block content. No padding existed between the markdown content and the button.
**Fix:** Changed position to `bottom-2.5` (10px clearance) and added `pb-4` padding to the chat text content wrapper for code block clearance.
### 30. 🟢 Current balance shows "----" / reload button does nothing
**Where:** Account pane → credit balance display
**Symptom:** The current balance always shows "----" and the reload button has no effect.
**Root cause:** `getUserCredits` handler returned `{ credits: undefined }` instead of calling the Cline API. The webview reads `response.balance.currentBalance` which was always undefined.
**Fix:** `getUserCredits` and `getOrganizationCredits` now fetch real balance data from the Cline API using the stored auth token (`Bearer` header). Includes 10s timeout and error handling.
**Tested:** Integration tests with mock HTTP server verify real balance data flows through correctly (7 tests).
### 31. 🟢 Logout button does nothing
**Where:** Account pane → logout button
**Symptom:** Clicking logout has no effect — the user remains logged in.
**Root cause:** `accountLogoutClicked` was a STUB (silent no-op) in grpc-handler.
**Fix:** Implemented `handleAccountLogout()` that calls `LegacyStateReader.clearClineAuthInfo()` to remove `cline:clineAccountId` from secrets.json, then pushes state update so the webview shows the sign-in view.
**Tested:** Integration test verifies credentials are cleared from disk and auth status shows unauthenticated after logout.
### 32. 🟢 Low credit balance persists after account switching
**Where:** Account pane after switching organizations
**Symptom:** After switching from a low-balance org to a high-balance org, the "Insufficient balance" error persists.
**Root cause:** `setUserOrganization` was a STUB. The active org was never updated on disk, so credit queries always returned the same org's data.
**Fix:** Implemented `handleSetUserOrganization()` that calls `LegacyStateReader.setActiveOrganization()` to update the `active` flag on orgs in stored credentials. Each credit fetch is a fresh API call keyed by org ID, so switching orgs correctly fetches the new org's balance.
**Tested:** Integration test with mock server verifies: switch from low-balance org → high-balance org returns correct (high) balance, not stale (low) balance.
## Open Issues
---
### 26. 🟢 Clicking history items does not open them
**Where:** History tab → clicking any task item; also RECENT section on home screen
**Symptom:** History items display correctly in the History view, but clicking on them does nothing — the view stays on the History tab instead of navigating to the chat view with the loaded task.
**Root cause:** `handleShowTaskWithId()` in `grpc-handler.ts` called `this.delegate.showTaskWithId(id)` to load the task data and push state, but never fired `this.onNavigateCallback?.("chat")` to tell the webview to navigate from the History view to the Chat view.
**Fix:** Added `this.onNavigateCallback?.("chat")` after `showTaskWithId()` completes in `grpc-handler.ts`. This sends a typed `navigate` message to the webview, which triggers `navigateToChat()` — hiding the History view and revealing the Chat view with the loaded task.
**Verified:** Debug harness confirmed clicking items in both the History tab and RECENT section on the home screen now navigates to the chat view with full message history loaded.
---
## Observations (not bugs, just notes)
### UI Rendering — Task Completion View
The completed task view renders correctly:
- Task header with cost badge (e.g. "$0.0072")
- Token usage bar (e.g. "1.4k / 200.0k")
- Response text displayed properly
- "Task Completed" card with green checkmark and the result
- "Start New Task" button appears below the chat
- Input area changes to "Type a message..." (follow-up mode)
### Feature Card Carousel
The home screen shows a rotating feature card carousel (1/4 through 4/4) promoting:
- Claude Sonnet 4.6
- MiniMax M2.5
- ChatGPT integration
- Jupyter Notebooks
Each with a dismiss (X) button per-card.
### Model Selector
Bottom bar correctly shows `cline:anthropic/claud...` (truncated) with Plan/Act toggle. Act mode is the default.
### Debug Harness: `ui.send_message` and `ui.react_input(submit:true)` Don't Start Tasks
Both `ui.send_message` (gRPC postMessage) and `ui.react_input` with `submit:true` report success but don't actually start a new task—the webview stays on the home screen. The workaround is to use `ui.react_input` (without submit) to set the text, then dispatch a KeyboardEvent via `web.evaluate`:
```
curl -s localhost:19229/api -d '{"method":"ui.react_input","params":{"text":"your message","clear":true}}'
curl -s localhost:19229/api -d '{"method":"web.evaluate","params":{"expression":"(() => { const ta = document.querySelector(\"textarea\"); ta.focus(); ta.dispatchEvent(new KeyboardEvent(\"keydown\",{key:\"Enter\",code:\"Enter\",keyCode:13,which:13,bubbles:true})); return \"ok\"; })()"}}'
```
### Debug Harness Limitations (Fixed)
All three limitations below have been addressed:
- ~~Programmatic textarea input doesn't reliably trigger React state updates after the first task.~~ **Fixed**: Two new commands added:
- `ui.react_input` — Uses `document.execCommand('insertText')` which fires real InputEvents that React's onChange handler processes correctly, even after multiple tasks.
- `ui.send_message` — Bypasses the textarea entirely by sending gRPC requests via `postMessage` directly to the extension host.
- ~~The `web.evaluate` context can't access the VS Code API.~~ **Fixed**: The webview now exposes the VS Code API as `window.__clineVsCodeApi`, and a new `web.post_message` command lets the harness send arbitrary messages to the extension host through it.
- ~~The `ui.locator` Playwright actions don't reliably target elements inside the webview iframe.~~ **Fixed**: `findSidebar()` now validates cached frame references (checking for both detached and stale frames), `getTarget()` accepts a `forceRefresh` flag, and `ui.locator` automatically retries with frame re-discovery when targeting sidebar elements.
+84 -168
View File
@@ -1,20 +1,18 @@
<p align="center">
<img src="assets/icons/icon.png" width="80" alt="Cline" />
</p>
<div align="center"><sub>
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
<h1 align="center">Cline</h1>
# Cline
<p align="center">
The open source coding agent in your IDE and terminal.
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
</p>
<div align="center">
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://docs.cline.bot" target="_blank"><strong>Docs</strong></a>
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Download on VS Marketplace</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
@@ -26,209 +24,127 @@ The open source coding agent in your IDE and terminal.
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
</td>
<td align="center">
<a href="https://cline.bot/join-us" target="_blank"><strong>Join us!</strong></a>
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
</td>
</tbody>
</table>
</div>
</div>
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
<br>
Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
<div align="center">
<table>
<tr>
<td align="center" width="50%">
1. Enter your task and add images to convert mockups into functional apps or fix bugs with screenshots.
2. Cline starts by analyzing your file structure & source code ASTs, running regex searches, and reading relevant files to get up to speed in existing projects. By carefully managing what information is added to context, Cline can provide valuable assistance even for large, complex projects without overwhelming the context window.
3. Once Cline has the information he needs, he can:
- Create and edit files + monitor linter/compiler errors along the way, letting him proactively fix issues like missing imports and syntax errors on his own.
- Execute commands directly in your terminal and monitor their output as he works, letting him e.g., react to dev server issues after editing a file.
- For web development tasks, Cline can launch the site in a headless browser, click, type, scroll, and capture screenshots + console logs, allowing him to fix runtime errors and visual bugs.
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
### CLI
Run Cline in your terminal.
Interactive chat or fully headless
for CI/CD and scripting.
```
npm i -g cline
```
<a href="./sdk/apps/cli/README.md">Learn more</a>
<br><br>
</td>
<td align="center" width="50%">
### Kanban
Run many agents in parallel from a
web-based task board. Each card gets its own
worktree, auto-commit, and dependency chains.
```
npm i -g kanban
```
<a href="https://github.com/cline/kanban">Learn more</a>
<br><br>
</td>
</tr>
<tr>
<td align="center" width="50%">
### VS Code Extension
AI coding assistant in your editor.
Create files, run commands, browse the web,
and use tools with human-in-the-loop approval.
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev">Install from VS Marketplace</a>
<br><br>
</td>
<td align="center" width="50%">
### JetBrains Plugin
The same Cline experience in IntelliJ IDEA,
PyCharm, WebStorm, GoLand, and the rest of
the JetBrains family.
<a href="https://plugins.jetbrains.com/plugin/28247-cline">Install from JetBrains Marketplace</a>
<br><br>
</td>
</tr>
</table>
</div>
<div align="center">
<table>
<tr>
<td align="center">
### SDK
Build your own AI agents and integrations powered by the same engine that runs the CLI, Kanban, VS Code extension, and JetBrains plugin. Custom tools, multi-agent teams, connectors, scheduled automations, and more.
```
npm install @cline/sdk
```
<a href="https://docs.cline.bot/cline-sdk/overview">Documentation</a>
<br><br>
</td>
</tr>
</table>
</div>
> [!TIP]
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
---
## Index
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
| Product | Description | Location | CHANGELOG |
|---------|------------|--------------|--------------|
| **SDK** | Node.js programmatic agent API and extension exports. | [`sdk/`](https://github.com/cline/cline/tree/main/sdk) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/sdk/CHANGELOG.md) |
| **CLI** | Terminal UI, headless mode, shell commands, and CLI-specific flows. | [`sdk/apps/cli/`](https://github.com/cline/cline/tree/main/sdk/apps/cli) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/sdk/apps/cli/CHANGELOG.md) |
| **VS Code Extension** | The Marketplace extension and extension host integration. | [`/`](https://github.com/cline/cline/tree/main) (WIP migrating) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/CHANGELOG.md) |
| **JetBrains Plugin** | JetBrains-hosted client that talks to the shared agent core. | Currently we are not open-sourcing JetBrains plugins | - |
| **Kanban** | Web-based multi-agent task board. | [`cline/kanban`](https://github.com/cline/kanban) | [CHANGELOG.md](https://github.com/cline/kanban/blob/main/CHANGELOG.md) |
| **Docs site** | Public documentation pages. | [`docs/`](https://docs.cline.bot/) | - |
### Use any API and Model
## Edits Code Across Your Project
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
Cline reads your project structure, understands the relationships between files, and makes coordinated changes across your codebase. It monitors linter and compiler errors as it works, fixing issues like missing imports, type mismatches, and syntax errors before you even see them. In VS Code and JetBrains, every edit shows up as a diff you can review, modify, or revert. All changes are tracked with checkpoints, so you can easily undo the agent's work.
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
## Runs Bash Commands
<!-- Transparent pixel to create line break after floating image -->
Cline executes commands directly in your terminal and watches the output in real time. Install packages, run build scripts, execute tests, deploy applications, manage databases. For long-running processes like dev servers, Cline continues working in the background and reacts to new output as it appears, catching compile errors, test failures, and server crashes as they happen.
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Plan and Act
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
Toggle between Plan mode and Act mode. In Plan mode, Cline explores your codebase, asks clarifying questions, and lays out a strategy. Once you're aligned, switch to Act mode and Cline executes the plan. Every file edit and terminal command requires your approval, so you stay in control of what actually changes. Or toggle auto-approve and let Cline run autonomously.
### Run Commands in Terminal
## Rules and Skills
Thanks to the new [shell integration updates in VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), Cline can execute commands directly in your terminal and receive the output. This allows him to perform a wide range of tasks, from installing packages and running build scripts to deploying applications, managing databases, and executing tests, all while adapting to your dev environment & toolchain to get the job done right.
Define project-specific rules in `.clinerules` files that guide how Cline works in your codebase: coding standards, architecture conventions, deployment procedures, testing requirements. Rules are picked up automatically by the CLI, VS Code extension, and JetBrains plugin. Use skills to let the model load specific rules when needed.
For long running processes like dev servers, use the "Proceed While Running" button to let Cline continue in the task while the command runs in the background. As Cline works hell be notified of any new terminal output along the way, letting him react to issues that may come up, such as compile-time errors when editing files.
## Works With Every Model
<!-- Transparent pixel to create line break after floating image -->
Cline is not locked to a single AI provider. Use whichever model fits your workflow:
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
| Provider | Models |
|----------|--------|
| Anthropic | Claude Opus, Sonnet, Haiku |
| OpenAI | GPT series model |
| Google | Gemini series model |
| OpenRouter | 200+ models from any provider |
| Vercel AI Gateway | Models through Vercel AI Gateway |
| AWS Bedrock | Claude, Llama, and more |
| Azure / GCP Vertex | All hosted models |
| Cerebras / Groq | Fast inference models |
| Ollama / LM Studio | Run local models on your machine |
| Any OpenAI-compatible API | Self-hosted or third-party endpoints |
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
## Extend With Plugins or MCP Servers
### Create and Edit Files
Extend Cline's capabilities with plugins. Using the SDK, register tools and lifecycle hooks programmatically through the plugin system for logging, auditing, policy enforcement, or adding domain-specific capabilities. Simple plugin example below.
Cline can create and edit files directly in your editor, presenting you a diff view of the changes. You can edit or revert Cline's changes directly in the diff view editor, or provide feedback in chat until you're satisfied with the result. Cline also monitors linter/compiler errors (missing imports, syntax errors, etc.) so he can fix issues that come up along the way on his own.
```typescript
import { Agent, createTool } from "@cline/sdk"
All changes made by Cline are recorded in your file's Timeline, providing an easy way to track and revert modifications if needed.
const deployTool = createTool({
name: "deploy",
description: "Deploy the current branch to staging.",
inputSchema: { type: "object", properties: { env: { type: "string" } }, required: ["env"] },
execute: async (input) => {
// your deployment logic
},
})
<!-- Transparent pixel to create line break after floating image -->
const agent = new Agent({ tools: [deployTool], /* ... */ })
```
...or use [MCP servers](https://github.com/modelcontextprotocol) to connect to databases, query APIs, manage cloud infrastructure, and interact with external systems. Use [community-built servers](https://github.com/modelcontextprotocol/servers) or ask Cline to create custom tools on the fly. In the CLI, manage servers with `cline mcp`.
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Multi-Agent Teams
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
Coordinate multiple agents working together on complex tasks. A coordinator agent breaks the work into subtasks and delegates to specialist agents, each with their own tools and context. Team state persists across sessions so you can pick up where you left off.
### Use the Browser
```bash
cline --team-name auth-sprint "Plan and implement user authentication with tests"
```
With Claude Sonnet's new [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) capability, Cline can launch a browser, click elements, type text, and scroll, capturing screenshots and console logs at each step. This allows for interactive debugging, end-to-end testing, and even general web use! This gives him autonomy to fixing visual bugs and runtime issues without you needing to handhold and copy-pasting error logs yourself.
## Scheduled Agents
Try asking Cline to "test the app", and watch as he runs a command like `npm run dev`, launches your locally running dev server in a browser, and performs a series of tests to confirm that everything works. [See a demo here.](https://x.com/sdrzn/status/1850880547825823989)
Run agents on cron schedules for recurring automations. Daily PR summaries, weekly dependency checks, codebase health reports. Schedules persist across restarts and run independently of any terminal session.
<!-- Transparent pixel to create line break after floating image -->
```bash
cline schedule create "PR summary" \
--cron "0 9 * * MON-FRI" \
--prompt "List all open PRs and their review status" \
--workspace /path/to/repo
```
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Connect to Slack, Telegram, Discord, and More
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
Chat with your agent from any messaging platform: Telegram, Slack, Discord, Google Chat, WhatsApp, and Linear. Each conversation thread maps to an agent session with full context. Set up access control to restrict who can interact with your agent.
### "add a tool that..."
```bash
cline connect telegram -k $BOT_TOKEN
cline connect slack --token $SLACK_TOKEN --signing-secret $SECRET --base-url $URL
```
Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol), Cline can extend his capabilities through custom tools. While you can use [community-made servers](https://github.com/modelcontextprotocol/servers), Cline can instead create and install tools tailored to your specific workflow. Just ask Cline to "add a tool" and he will handle everything, from creating a new MCP server to installing it into the extension. These custom tools then become part of Cline's toolkit, ready to use in future tasks.
## Headless CLI for CI/CD
- "add a tool that fetches Jira tickets": Retrieve ticket ACs and put Cline to work
- "add a tool that manages AWS EC2s": Check server metrics and scale instances up or down
- "add a tool that pulls the latest PagerDuty incidents": Fetch details and ask Cline to fix bugs
Run Cline with zero interaction for scripting and automation. Pipe input, get JSON output, chain commands, integrate into CI/CD pipelines.
<!-- Transparent pixel to create line break after floating image -->
```bash
cline "Run tests and fix any failures"
git diff origin/main | cline "Review these changes for issues"
cline --json "List all TODO comments" | jq -r 'select(.type == "agent_event" and .event.text) | .event.text'
```
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
### Add Context
**`@url`:** Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Cline the latest docs
**`@problems`:** Add workspace errors and warnings ('Problems' panel) for Cline to fix
**`@file`:** Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
### Checkpoints: Compare and Restore
As Cline works through a task, the extension can take an internal snapshot of your workspace at each step. You can use the 'Compare' button to see a diff between the snapshot and your current workspace, and the 'Restore' button to roll back to that point.
For example, when working with a local web server, you can use 'Restore Workspace Only' to quickly test different versions of your app, then use 'Restore Task and Workspace' when you find the version you want to continue building from. This lets you safely explore different approaches without losing progress while the current checkpoint system is available.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Contributing
Start with the [Contributing Guide](CONTRIBUTING.md). Join our [Discord](https://discord.gg/cline) and head to the `#contributors` channel to connect with other contributors. Check our [careers page](https://cline.bot/join-us) for full-time roles.
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
## Enterprise
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
## License
+436
View File
@@ -0,0 +1,436 @@
# Cline SDK — MCP Server Management Reference
This document describes how the Cline SDK handles MCP (Model Context Protocol) server lifecycle, configuration, and what is — and isn't — exposed for client integration. It is intended for client developers building UI around MCP server management.
---
## Summary: The Claim That "The SDK Lacks Hooks" Is Wrong
The SDK **does** provide a full MCP manager with lifecycle operations. The `McpManager` interface in `@clinebot/core` supports:
- `registerServer()` / `unregisterServer()` — add or remove servers
- `connectServer()` / `disconnectServer()` — start or stop connections
- `setServerDisabled()` — toggle enable/disable
- `listServers()` — get snapshots of all servers with status
- `refreshTools()` — force-refresh tool lists from a server
- `dispose()` — shut down all servers
The **actual gap** is narrower: there is no built-in file-watcher that auto-reloads `cline_mcp_settings.json` when it changes, and the RPC layer (`@clinebot/rpc`) does not currently expose MCP management endpoints. This means clients that manage MCP settings through the settings file must bridge the gap between file edits and runtime state themselves.
---
## SDK Architecture for MCP
### Layer 1: Settings File (`cline_mcp_settings.json`)
The SDK reads MCP server configuration from a JSON settings file:
```json
{
"mcpServers": {
"docs": {
"transport": { "type": "stdio", "command": "node", "args": ["./mcp.js"] }
},
"remote": {
"transport": { "type": "streamableHttp", "url": "https://mcp.example.com" },
"disabled": true
}
}
}
```
**Location resolution** (`resolveDefaultMcpSettingsPath()`):
- `CLINE_MCP_SETTINGS_PATH` env var (if set)
- Otherwise defaults to the platform-specific Cline data directory
**SDK utilities** for this file (all exported from `@clinebot/core`):
- `resolveDefaultMcpSettingsPath()` — get the path
- `hasMcpSettingsFile()` — check if it exists
- `loadMcpSettingsFile()` — parse and validate with Zod
- `resolveMcpServerRegistrations()` — parse file → `McpServerRegistration[]`
- `registerMcpServersFromSettingsFile(manager)` — parse file and register all servers into a manager
### Layer 2: McpManager (`InMemoryMcpManager`)
`packages/core/src/extensions/mcp/manager.ts` — the runtime MCP lifecycle manager.
```typescript
interface McpManager extends McpToolProvider {
registerServer(registration: McpServerRegistration): Promise<void>;
unregisterServer(serverName: string): Promise<void>;
connectServer(serverName: string): Promise<void>;
disconnectServer(serverName: string): Promise<void>;
setServerDisabled(serverName: string, disabled: boolean): Promise<void>;
listServers(): readonly McpServerSnapshot[];
refreshTools(serverName: string): Promise<readonly McpToolDescriptor[]>;
callTool(request: McpToolCallRequest): Promise<McpToolCallResult>;
dispose(): Promise<void>;
}
```
Key behaviors:
- **Lazy connection**: Servers are registered in disconnected state; connection happens on first `listTools()` or `callTool()`.
- **Transport change detection**: If `registerServer()` is called with a changed transport config, the existing connection is torn down and the client is recreated.
- **Exclusive locking**: Per-server operation locks prevent concurrent connect/disconnect races.
- **Tool caching**: `listTools()` caches results for `toolsCacheTtlMs` (default 5 seconds). Use `refreshTools()` to force a refresh.
- **Disable = disconnect**: Calling `setServerDisabled(name, true)` immediately disconnects the server.
### Layer 3: McpServerClient (Transport Layer)
`packages/core/src/extensions/mcp/client.ts` — the actual MCP protocol client.
The default factory (`createDefaultMcpServerClientFactory()`) creates `StdioMcpClient` instances:
- Spawns child processes for `stdio` transport
- Implements MCP JSON-RPC protocol (both newline-delimited and framed modes)
- Auto-negotiates protocol mode by trying newline first, then framed
- Protocol version: `2024-11-05`
- Connect timeout: 1.5s, request timeout: 5s
```typescript
interface McpServerClient {
connect(): Promise<void>;
disconnect(): Promise<void>;
listTools(): Promise<readonly McpToolDescriptor[]>;
callTool(request: { name: string; arguments?: Record<string, unknown> }): Promise<McpToolCallResult>;
}
type McpServerClientFactory = (registration: McpServerRegistration) => Promise<McpServerClient> | McpServerClient;
```
**Transport types supported**:
| Type | Status |
|---|---|
| `stdio` | ✅ Fully implemented in `StdioMcpClient` |
| `sse` | ⚠️ Type defined, but no built-in client (factory only creates `StdioMcpClient`) |
| `streamableHttp` | ⚠️ Type defined, but no built-in client |
Clients needing SSE or StreamableHTTP support must provide a custom `McpServerClientFactory`.
### Layer 4: Tool Bridge
`packages/core/src/extensions/mcp/tools.ts` via `createMcpTools()` — converts MCP server tools into the SDK's `Tool` type for use in the agent loop.
```typescript
interface CreateMcpToolsOptions {
serverName: string;
provider: McpToolProvider; // Usually the McpManager
nameTransform?: McpToolNameTransform;
timeoutMs?: number;
retryable?: boolean;
maxRetries?: number;
}
```
Default name transform: `{serverName}__{toolName}` (e.g. `docs__search`).
---
## How the Runtime Builder Uses MCP
`packages/core/src/runtime/runtime-builder.ts``loadConfiguredMcpTools()`:
1. Resolves the MCP settings file path
2. Creates a fresh `InMemoryMcpManager`
3. Calls `registerMcpServersFromSettingsFile()` to load all servers
4. Creates `Tool[]` via `createMcpTools()` for each non-disabled server
5. Returns the tools + a `shutdown()` callback that calls `manager.dispose()`
**Critical limitation**: This is done once at session build time. There is **no file watcher** that reloads MCP settings when they change during a session. If the settings file is edited mid-session, the running session won't see the changes.
---
## How Existing Apps Handle MCP Settings
### Tauri Apps (`apps/code`, `apps/desktop`)
Both Tauri apps implement MCP settings management **entirely in Rust** at the Tauri command level, bypassing the SDK's McpManager:
```rust
// apps/code/src-tauri/src/main.rs (identical pattern in apps/desktop)
fn list_mcp_servers() -> Result<McpServersResponse, String>
fn set_mcp_server_disabled(name, disabled) -> Result<McpServersResponse, String>
fn upsert_mcp_server(input) -> Result<McpServersResponse, String>
fn delete_mcp_server(name) -> Result<McpServersResponse, String>
```
These commands:
- Read/write `cline_mcp_settings.json` directly
- Return the full server list after each mutation
- Do **not** interact with any running `McpManager` instance
The frontend (`apps/code/components/views/settings/mcp-view.tsx`) calls these Tauri commands:
- `list_mcp_servers` — refresh the displayed server list
- `set_mcp_server_disabled` — toggle enable/disable
- `upsert_mcp_server` — add or edit a server
- `delete_mcp_server` — remove a server
### Node.js Host (`apps/code/host/commands.ts`)
The Code app's Node.js host also implements MCP CRUD directly:
```typescript
// Direct file reads/writes, not using McpManager
function readMcpServersResponse(): JsonRecord // reads cline_mcp_settings.json
function writeMcpServersMap(servers: JsonRecord) // writes cline_mcp_settings.json
function ensureMcpSettingsFile(): string // ensures file exists
```
Commands: `list_mcp_servers`, `set_mcp_server_disabled`, `upsert_mcp_server`, `delete_mcp_server`, `ensure_mcp_settings_file`
### CLI (`apps/cli`)
The CLI has `clite config mcp` / `clite list mcp` for listing configured MCP servers. It uses the SDK's `resolveMcpServerRegistrations()` to read the settings file.
---
## What the SDK DOES Expose (Exported from `@clinebot/core`)
### Full Type & Implementation Exports
```typescript
// Manager
export { InMemoryMcpManager } from "./extensions/mcp";
export type { McpManager, McpManagerOptions } from "./extensions/mcp";
// Client factory
export { createDefaultMcpServerClientFactory } from "./extensions/mcp";
export type { McpServerClient, McpServerClientFactory } from "./extensions/mcp";
// Config loading
export { hasMcpSettingsFile, loadMcpSettingsFile, registerMcpServersFromSettingsFile,
resolveDefaultMcpSettingsPath, resolveMcpServerRegistrations } from "./extensions/mcp";
export type { LoadMcpSettingsOptions, McpSettingsFile, RegisterMcpServersFromSettingsOptions } from "./extensions/mcp";
// Types
export type { McpServerRegistration, McpServerSnapshot, McpConnectionStatus,
McpServerTransportConfig, McpStdioTransportConfig, McpSseTransportConfig,
McpStreamableHttpTransportConfig } from "./extensions/mcp";
// Tool bridge
export { createMcpTools } from "./extensions/mcp";
export type { CreateMcpToolsOptions, McpToolCallRequest, McpToolCallResult,
McpToolDescriptor, McpToolNameTransform, McpToolProvider } from "./extensions/mcp";
// Policies
export { createDisabledMcpToolPolicies, createDisabledMcpToolPolicy } from "./extensions/mcp";
```
---
## What's Missing / The Actual Gaps
### 1. No RPC Endpoints for MCP Management
`packages/rpc/src` has **zero** MCP-related code. There are no gRPC/RPC methods for:
- Listing MCP servers
- Registering/unregistering servers
- Connecting/disconnecting servers
- Toggling server disabled state
- Refreshing tools
This means RPC-backed clients cannot manage MCP through the RPC layer.
### 2. No Settings File Watcher
The SDK has a `UnifiedConfigFileWatcher` for agents, skills, rules, and workflows — but **not for MCP settings**. When another client edits `cline_mcp_settings.json`, running sessions don't see the change.
### 3. No Live Manager Exposure to Clients
The runtime builder creates an `InMemoryMcpManager` internally during `loadConfiguredMcpTools()`, but it's encapsulated — only the resulting `Tool[]` and a `shutdown()` callback are returned. The manager itself is not exposed to the caller, so clients can't call `connectServer()`, `disconnectServer()`, etc. on a running session's MCP manager.
### 4. SSE/StreamableHTTP Client Not Implemented
The transport types are defined, but the default client factory only produces `StdioMcpClient`. Clients needing SSE or StreamableHTTP must provide their own `McpServerClientFactory`.
---
## What a Client Needs to Do Today
To implement full MCP server management UI:
### Settings CRUD (Works Now)
Read and write `cline_mcp_settings.json` directly. The SDK provides:
- `resolveDefaultMcpSettingsPath()` — find the file
- `loadMcpSettingsFile()` — parse it
- Write it yourself (it's just JSON with `{ mcpServers: { ... } }`)
This is exactly what the Tauri apps and Node.js host do today.
### Runtime Lifecycle (Partial)
For a new session, MCP tools are automatically loaded from the settings file by the runtime builder.
For mid-session changes (restart, delete, toggle), clients currently have two options:
1. **Edit the settings file and restart the session** — the next session build will pick up the changes
2. **Create and manage your own `InMemoryMcpManager`** — the SDK exports everything needed:
```typescript
const manager = new InMemoryMcpManager({
clientFactory: createDefaultMcpServerClientFactory(),
});
await manager.registerServer({ name: "docs", transport: { type: "stdio", command: "node", args: ["./mcp.js"] } });
await manager.connectServer("docs");
const tools = await manager.listTools("docs");
await manager.disconnectServer("docs");
await manager.unregisterServer("docs");
```
### Cross-Client Sync (Not Built)
If multiple clients share the same settings file, there is no notification mechanism. Clients would need their own file watcher (e.g. `fs.watch()` / `chokidar`) on `cline_mcp_settings.json`.
---
## How MCP Tools Become Visible to the Agent (and the Client)
### MCP Tools Are Injected as Regular SDK Tools
The `createMcpTools()` function converts each MCP tool descriptor into a standard `Tool` object (from `@clinebot/shared`). These tools are **indistinguishable** from built-in tools once created — they have a `name`, `description`, `inputSchema`, and an `execute` function.
```typescript
// packages/core/src/extensions/mcp/tools.ts
export async function createMcpTools(options: CreateMcpToolsOptions): Promise<Tool[]> {
const descriptors = await options.provider.listTools(options.serverName);
return descriptors.map((descriptor) => createTool({
name: nameTransform({ serverName, toolName: descriptor.name }), // e.g. "docs__search"
description: descriptor.description || `Execute MCP tool "${descriptor.name}" from server "${serverName}".`,
inputSchema: descriptor.inputSchema,
execute: async (input, context) => options.provider.callTool({
serverName, toolName: descriptor.name, arguments: input, context,
}),
}));
}
```
### The Runtime Builder Merges MCP Tools with Built-in Tools
In `DefaultRuntimeBuilder.build()` (line ~460-476):
```typescript
if (normalized.enableTools) {
tools.push(...createBuiltinToolsList(...)); // SDK built-in tools
const mcpRuntime = await loadConfiguredMcpTools(); // MCP tools
tools.push(...mcpRuntime.tools);
mcpShutdown = mcpRuntime.shutdown;
}
```
The resulting `tools: Tool[]` array — containing **both** built-in and MCP tools — is returned in the `BuiltRuntime`:
```typescript
interface BuiltRuntime {
tools: Tool[]; // ← includes MCP tools
hooks?: AgentHooks;
shutdown: (reason: string) => Promise<void> | void;
// ...
}
```
### The Agent Receives All Tools (Including MCP) Uniformly
The `Agent` (from `@clinebot/agents`) receives the merged `tools` array. It doesn't know or care which tools came from MCP vs built-in. The LLM sees all tools in its tool definitions and can call any of them.
### Client Control Over MCP Tools
Clients **can** control MCP tools through the same mechanisms they use for any tool:
1. **Tool Policies** — Enable/disable or require approval per tool name:
```typescript
toolPolicies: {
"docs__search": { enabled: true, autoApprove: true },
"docs__write": { enabled: true, autoApprove: false }, // requires approval
"risky__delete": { enabled: false }, // completely disabled
}
```
2. **MCP-specific disable policies** — The SDK provides helpers to disable all tools from a specific MCP server:
```typescript
import { createDisabledMcpToolPolicies } from "@clinebot/core";
const policies = createDisabledMcpToolPolicies({
serverName: "risky-server",
toolNames: ["delete", "modify", "drop"],
});
// → { "risky-server__delete": { enabled: false }, ... }
```
3. **CLI flags** — `--tool-enable <name>` and `--tool-disable <name>` work for MCP tools too (they operate on the transformed name like `docs__search`).
4. **Tool approval callback** — When `autoApprove: false`, the agent calls `requestToolApproval()` before executing the tool, giving the client a chance to approve/reject each call.
5. **`enableTools: false`** — Disables ALL tools including MCP.
### What Clients Can See
The `BuiltRuntime.tools` array is visible to the caller of `runtimeBuilder.build()`. The session manager and host apps can inspect it to know exactly which tools (including MCP tools) are available.
MCP tools follow the naming convention `{serverName}__{toolName}` by default, so clients can identify which tools came from which MCP server by parsing the name prefix.
---
## Remote MCP Servers (StreamableHTTP / SSE)
### The SDK's Design Intent
The SDK clearly **intends** to support remote MCP servers. The evidence:
1. **Transport types are fully defined and validated**:
```typescript
interface McpStreamableHttpTransportConfig {
type: "streamableHttp";
url: string;
headers?: Record<string, string>;
}
interface McpSseTransportConfig {
type: "sse";
url: string;
headers?: Record<string, string>;
}
```
2. **Config loader validates all three transports** — The Zod schemas accept `stdio`, `sse`, and `streamableHttp` equally. Legacy formats (`url` without explicit type) default to `sse`; `transportType: "http"` maps to `streamableHttp`.
3. **Manager is transport-agnostic** — The `McpManager` uses `McpServerClientFactory` to create clients. It doesn't care about transport type; that's the factory's job.
4. **Tests use `streamableHttp` registrations** — The manager test suite registers servers with `transport: { type: "streamableHttp", url: "https://mcp.example.test" }` and they work fine (with a mock client factory).
5. **Settings file and UI accept all transports** — Both Tauri apps and the Code app UI offer `stdio`, `sse`, and `streamableHttp` as choices.
### What's Actually Implemented vs Not
| Concern | Status |
|---|---|
| Transport type definitions | ✅ Complete |
| Settings file parsing for all transports | ✅ Complete |
| Settings file CRUD (UI/Tauri/CLI) for all transports | ✅ Complete |
| `McpManager` lifecycle for all transports | ✅ Complete (transport-agnostic) |
| `StdioMcpClient` (spawns child process) | ✅ Complete |
| HTTP/SSE client (connects to remote URL) | ❌ Not in default factory |
### The Gap for Remote Servers
The **only** thing missing is that `createDefaultMcpServerClientFactory()` returns a `StdioMcpClient` unconditionally — it doesn't check `registration.transport.type` and will fail for `sse` or `streamableHttp` transports.
A client can fix this by providing a custom factory:
```typescript
const manager = new InMemoryMcpManager({
clientFactory: async (registration) => {
if (registration.transport.type === "stdio") {
return createDefaultMcpServerClientFactory()(registration);
}
if (registration.transport.type === "streamableHttp") {
return new MyStreamableHttpMcpClient(registration);
}
if (registration.transport.type === "sse") {
return new MySseMcpClient(registration);
}
throw new Error(`Unsupported transport: ${registration.transport.type}`);
},
});
```
The client just needs to implement the `McpServerClient` interface (4 methods: `connect`, `disconnect`, `listTools`, `callTool`).
---
## Recommendation for Improvement
To close the gap, the SDK could:
1. **Add MCP settings to the config watcher system** — create an `McpConfigDefinition` for `UnifiedConfigFileWatcher` to auto-detect changes to `cline_mcp_settings.json`
2. **Expose the McpManager from the runtime builder** — return it alongside the tool list so clients can call lifecycle methods
3. **Add MCP management to the RPC layer** — implement gRPC methods that proxy to `McpManager`
4. **Implement SSE/StreamableHTTP clients** — extend the default client factory
+251
View File
@@ -0,0 +1,251 @@
# Cline SDK — Provider Credentials & OAuth Reference
This document describes how the Cline SDK publishes inference provider metadata, handles credential resolution, and orchestrates OAuth authentication flows. It is intended for client developers integrating with `@clinebot/llms` and `@clinebot/core`.
---
## Provider Catalog
The SDK owns the canonical list of inference providers. It is **not** produced by clients.
### Where Providers Are Defined
| Layer | Location | What It Owns |
|---|---|---|
| `@clinebot/llms` | `packages/llms/src/gateway/builtins.ts` | `BUILTIN_SPECS` array — every built-in provider's `id`, `name`, `description`, `family`, `capabilities`, `apiKeyEnv`, `defaultModelId`, default `baseUrl` |
| `@clinebot/llms` | `packages/llms/src/provider/ids.ts` | `BUILT_IN_PROVIDER` enum and `BUILT_IN_PROVIDER_IDS` array |
| `@clinebot/shared` | `packages/shared/src/llms/gateway.ts` | `GatewayProviderManifest` type — the runtime shape clients receive |
| `@clinebot/llms` | `packages/llms/src/gateway/provider-keys.ts` | Mapping from external `modelsDevKey` identifiers to runtime/generated provider IDs |
### BuiltinSpec Shape
Each built-in provider is declared as a `BuiltinSpec`:
```typescript
interface BuiltinSpec {
id: string; // e.g. "anthropic", "openai-native", "cline"
name: string; // Human-readable name
description: string;
family: ProviderFamily; // Protocol family: "openai-compatible", "anthropic", "google", etc.
protocol?: ProviderProtocol;
client?: ProviderClient;
capabilities?: ProviderCapability[]; // "reasoning" | "prompt-cache" | "tools" | "oauth" | "temperature" | "files"
modelsProviderId?: string;
defaultModelId?: string;
modelsFactory?: () => Record<string, ModelInfo>;
env?: readonly ("browser" | "node")[];
apiKeyEnv?: readonly string[]; // Environment variable names for API key resolution
docsUrl?: string;
defaults?: GatewayProviderSettings; // Includes default baseUrl
}
```
### GatewayProviderManifest (Runtime Shape)
`toManifest()` converts a `BuiltinSpec` into a `GatewayProviderManifest`, which is what clients interact with at runtime:
```typescript
interface GatewayProviderManifest {
id: string;
name: string;
description?: string;
defaultModelId: string;
models: readonly GatewayModelDefinition[];
env?: readonly ("browser" | "node")[];
api?: string; // Default base URL
apiKeyEnv?: readonly string[]; // Env var names for credential resolution
docsUrl?: string;
metadata?: Record<string, JsonValue | undefined>;
}
```
---
## Credential Resolution
### API Key Resolution Order
The SDK resolves credentials in `packages/llms/src/gateway/http.ts` via `resolveApiKey()`:
1. **Explicit `apiKey`** — passed directly in provider config
2. **`apiKeyResolver()`** — async callback (e.g. fetch from keychain)
3. **`apiKeyEnv`** — iterate environment variable names from the provider manifest; first non-empty value wins
If all fail, `getMissingApiKeyError()` produces a message naming the expected env vars:
> `Missing API key for provider "anthropic". Set apiKey explicitly or one of: ANTHROPIC_API_KEY.`
### Per-Provider Credential Metadata Examples
| Provider | `apiKeyEnv` |
|---|---|
| `anthropic` | `["ANTHROPIC_API_KEY"]` |
| `openai-native` | `["OPENAI_API_KEY"]` |
| `gemini` | `["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"]` |
| `vertex` | `["GCP_PROJECT_ID", "GOOGLE_CLOUD_PROJECT", "GOOGLE_APPLICATION_CREDENTIALS", "GEMINI_API_KEY", "GOOGLE_API_KEY"]` |
| `bedrock` | `["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]` |
| `sapaicore` | `["AICORE_SERVICE_KEY", "VCAP_SERVICES"]` |
| `cline` | `["CLINE_API_KEY"]` |
| `openrouter` | `["OPENROUTER_API_KEY"]` |
| `deepseek` | `["DEEPSEEK_API_KEY"]` |
Most OpenAI-compatible providers follow the pattern `["{PROVIDER}_API_KEY"]`.
### Provider-Specific Settings
Some providers need additional configuration beyond an API key:
- **Vertex/GCP**: `gcpProjectId`, `gcpRegion`
- **Bedrock/AWS**: `awsAuthentication` (`"iam" | "api-key" | "profile"`), `awsRegion`, `awsAccessKey`, `awsSecretKey`, `awsSessionToken`, `awsProfile`
These are passed through `ProviderSelectionConfig.settings` rather than `apiKeyEnv`.
---
## OAuth Authentication
### Which Providers Support OAuth
Only providers with `"oauth"` in their `capabilities` array support OAuth:
| Provider | OAuth Implementation |
|---|---|
| `cline` | `packages/core/src/auth/cline.ts` — Cline API OAuth |
| `openai-codex` | `packages/core/src/auth/codex.ts` — ChatGPT/OpenAI Codex OAuth with PKCE |
| `oca` | `packages/core/src/auth/oca.ts` — Oracle Code Assist OAuth with PKCE |
The CLI confirms this in `apps/cli/src/commands/auth.ts`:
```typescript
// Only these three providers support CLI OAuth flow
if (providerId === "cline") return oauthApi.loginClineOAuth(...)
if (providerId === "oca") return oauthApi.loginOcaOAuth(...)
if (providerId === "openai-codex") return oauthApi.loginOpenAICodex(...)
throw new Error(`Provider "${providerId}" does not support CLI OAuth flow`)
```
### Responsibility Split: SDK vs Client
| Concern | Owner | Details |
|---|---|---|
| Spawn local callback server | **SDK** | `startLocalOAuthServer()` in `packages/core/src/auth/server.ts` |
| Build authorization URL | **SDK** | Each auth module constructs the URL with redirect_uri, state, etc. |
| Open browser / present URL | **Client** | SDK calls `callbacks.onAuth({ url, instructions })` — client decides how to handle |
| Collect redirect code | **SDK** | Local HTTP server parses `?code=&state=` from redirect |
| Exchange code for tokens | **SDK** | Each auth module handles the token exchange |
| Prompt for manual code input | **Client** | SDK calls `callbacks.onPrompt()` or `callbacks.onManualCodeInput()` as fallback |
### The SDK Does NOT Open Browsers
The SDK never calls `open()` or launches a browser. It uses a callback-based interface:
```typescript
// packages/core/src/auth/types.ts
interface OAuthLoginCallbacks {
onAuth: (info: { url: string; instructions?: string }) => void; // SDK emits URL here
onPrompt: (prompt: OAuthPrompt) => Promise<string>; // SDK asks for input here
onProgress?: (message: string) => void;
onManualCodeInput?: () => Promise<string>; // Fallback if redirect fails
}
```
### The SDK DOES Spawn the Local Callback Server
`packages/core/src/auth/server.ts` exports `startLocalOAuthServer()`:
- Creates a `node:http` server on `127.0.0.1`
- Tries a list of candidate ports in order, skipping `EADDRINUSE`
- Listens on a configured callback path (e.g. `/callback`)
- Extracts `code`, `state`, `provider`, `error` from the redirect URL query params
- Returns a success HTML page to the browser ("Authentication Successful — You can close this window")
- Auto-closes after 3 seconds via embedded `<script>`
- Times out after 5 minutes by default
```typescript
interface LocalOAuthServer {
callbackUrl: string; // e.g. "http://127.0.0.1:54321/callback"
waitForCallback: () => Promise<OAuthCallbackPayload>; // Resolves when redirect arrives
cancelWait: () => void;
close: () => void;
}
```
### OAuth Redirect URLs Are NOT in Provider Metadata
Redirect/callback URLs are **dynamically constructed at runtime**, not published in the provider manifest:
- **Cline**: Dynamic port → `http://localhost:{port}/callback`
- **OpenAI Codex**: Hardcoded `http://localhost:1455/auth/callback` (fixed port, fixed client ID `app_EMoamEEZ73f0CkXaXp7hrann`)
- **OCA**: Dynamic port with configurable path, default `/oauth/callback`
### Client Integration Helper
`packages/core/src/auth/client.ts` provides a convenience adapter:
```typescript
interface OAuthClientCallbacksOptions {
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
onOutput?: (message: string) => void;
openUrl?: (url: string) => void | Promise<void>; // Client provides browser-open function
onOpenUrlError?: (context: { url: string; error: unknown }) => void;
}
function createOAuthClientCallbacks(options): OAuthLoginCallbacks
```
The `openUrl` field is where a CLI passes its `open` implementation, a Tauri app passes shell open, etc.
### End-to-End OAuth Flow
```
1. Client calls SDK login function (e.g. loginClineOAuth)
2. SDK → startLocalOAuthServer() → binds to 127.0.0.1:{port}
3. SDK → builds authorization URL with redirect_uri = callback server URL
4. SDK → callbacks.onAuth({ url, instructions })
5. Client → opens browser (or displays URL to user)
6. User → authenticates in browser
7. Provider → redirects to http://127.0.0.1:{port}/callback?code=...&state=...
8. SDK's local server → captures code and state, renders success page
9. SDK → exchanges authorization code for tokens (provider-specific)
10. SDK → returns OAuthCredentials { access, refresh, expires, accountId?, email? }
```
If the local server redirect times out or fails, the SDK falls back to:
- `onManualCodeInput()` — ask user to paste code
- `onPrompt({ message: "Paste the authorization code (or full redirect URL):" })` — final fallback
- `parseAuthorizationInput()` can handle both raw codes and full URLs with query params
### OAuthCredentials Shape
```typescript
interface OAuthCredentials {
access: string; // Access token
refresh: string; // Refresh token
expires: number; // Expiration timestamp (ms since epoch)
accountId?: string; // Provider-specific account ID
email?: string; // For display/telemetry
metadata?: Record<string, unknown>;
}
```
### Provider-Specific OAuth Details
**Cline OAuth** (`packages/core/src/auth/cline.ts`):
- Authorization URL: `{apiBaseUrl}/auth/authorize?client_type=extension&callback_url=...&state=...`
- Token endpoint: `{apiBaseUrl}/auth/token`
- Default API base: `https://api.cline.bot`
- Supports provider passthrough (callback can include `?provider=google` etc.)
**OpenAI Codex OAuth** (`packages/core/src/auth/codex.ts`):
- Uses PKCE (code challenge + verifier)
- Authorization: `https://auth.openai.com/oauth/authorize`
- Token: `https://auth.openai.com/oauth/token`
- Client ID: `app_EMoamEEZ73f0CkXaXp7hrann`
- Fixed redirect: `http://localhost:1455/auth/callback`
- Scopes: `openid profile email offline_access`
- JWT claim path: `https://api.openai.com/auth`
**OCA OAuth** (`packages/core/src/auth/oca.ts`):
- Uses PKCE (S256 code challenge)
- Supports `internal` and `external` mode with separate IDCS URLs and client IDs
- Authorization: `{idcsUrl}/oauth2/v1/authorize`
- Token: `{idcsUrl}/oauth2/v1/token`
- Configurable callback ports and path
+5 -3
View File
@@ -8,7 +8,9 @@ We actively patch only the most recent minor release of Cline. Older versions re
We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions.
To report a security issue, please submit your report through our [Bugcrowd Vulnerability Disclosure Program](https://bugcrowd.com/engagements/clinebot-vdp-ess). Bugcrowd will manage communication and triage on our behalf.
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/cline/cline/security/advisories/new) tab.
The team will send a response indicating the next steps in handling your report. After the initial reply, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
When reporting, please include:
@@ -16,10 +18,10 @@ When reporting, please include:
- Steps to reproduce or a proof of concept
- Any logs, stack traces, or screenshots that might help us understand the problem
Please keep the details private until a resolution has been reached.
We acknowledge reports within 48 hours and aim to release a fix or mitigation within 30 days. While we work on a resolution, please keep the details private.
## Escalation
If you are unable to submit through Bugcrowd, you may send an email to security@cline.bot.
If you do not receive an acknowledgement of your report within 5 business days, you may send an email to security@cline.bot.
Thank you for helping us keep Cline users safe.
-146
View File
@@ -1,146 +0,0 @@
<div align="center"><sub>
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
# Cline
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Download on VS Marketplace</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
</td>
<td align="center">
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
</td>
<td align="center">
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
</td>
<td align="center">
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
</td>
</tbody>
</table>
</div>
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
1. Enter your task and add images to convert mockups into functional apps or fix bugs with screenshots.
2. Cline starts by analyzing your file structure & source code ASTs, running regex searches, and reading relevant files to get up to speed in existing projects. By carefully managing what information is added to context, Cline can provide valuable assistance even for large, complex projects without overwhelming the context window.
3. Once Cline has the information he needs, he can:
- Create and edit files + monitor linter/compiler errors along the way, letting him proactively fix issues like missing imports and syntax errors on his own.
- Execute commands directly in your terminal and monitor their output as he works, letting him e.g., react to dev server issues after editing a file.
- For web development tasks, Cline can launch the site in a headless browser, click, type, scroll, and capture screenshots + console logs, allowing him to fix runtime errors and visual bugs.
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
> [!TIP]
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
---
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
### Use any API and Model
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
### Run Commands in Terminal
Thanks to the new [shell integration updates in VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), Cline can execute commands directly in your terminal and receive the output. This allows him to perform a wide range of tasks, from installing packages and running build scripts to deploying applications, managing databases, and executing tests, all while adapting to your dev environment & toolchain to get the job done right.
For long running processes like dev servers, use the "Proceed While Running" button to let Cline continue in the task while the command runs in the background. As Cline works hell be notified of any new terminal output along the way, letting him react to issues that may come up, such as compile-time errors when editing files.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
### Create and Edit Files
Cline can create and edit files directly in your editor, presenting you a diff view of the changes. You can edit or revert Cline's changes directly in the diff view editor, or provide feedback in chat until you're satisfied with the result. Cline also monitors linter/compiler errors (missing imports, syntax errors, etc.) so he can fix issues that come up along the way on his own.
All changes made by Cline are recorded in your file's Timeline, providing an easy way to track and revert modifications if needed.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
### Use the Browser
With Claude Sonnet's new [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) capability, Cline can launch a browser, click elements, type text, and scroll, capturing screenshots and console logs at each step. This allows for interactive debugging, end-to-end testing, and even general web use! This gives him autonomy to fixing visual bugs and runtime issues without you needing to handhold and copy-pasting error logs yourself.
Try asking Cline to "test the app", and watch as he runs a command like `npm run dev`, launches your locally running dev server in a browser, and performs a series of tests to confirm that everything works. [See a demo here.](https://x.com/sdrzn/status/1850880547825823989)
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
### "add a tool that..."
Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol), Cline can extend his capabilities through custom tools. While you can use [community-made servers](https://github.com/modelcontextprotocol/servers), Cline can instead create and install tools tailored to your specific workflow. Just ask Cline to "add a tool" and he will handle everything, from creating a new MCP server to installing it into the extension. These custom tools then become part of Cline's toolkit, ready to use in future tasks.
- "add a tool that fetches Jira tickets": Retrieve ticket ACs and put Cline to work
- "add a tool that manages AWS EC2s": Check server metrics and scale instances up or down
- "add a tool that pulls the latest PagerDuty incidents": Fetch details and ask Cline to fix bugs
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
### Add Context
**`@url`:** Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Cline the latest docs
**`@problems`:** Add workspace errors and warnings ('Problems' panel) for Cline to fix
**`@file`:** Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
### Checkpoints: Compare and Restore
As Cline works through a task, the extension takes a snapshot of your workspace at each step. You can use the 'Compare' button to see a diff between the snapshot and your current workspace, and the 'Restore' button to roll back to that point.
For example, when working with a local web server, you can use 'Restore Workspace Only' to quickly test different versions of your app, then use 'Restore Task and Workspace' when you find the version you want to continue building from. This lets you safely explore different approaches without losing progress.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Contributing
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
## Enterprise
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
## License
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

@@ -1,90 +0,0 @@
#!/usr/bin/env node
// Swap README.md with README.marketplace.md so the VS Code Marketplace listing
// (which is generated from the README baked into the .vsix at package time)
// keeps the extension-focused content even after the repo's README.md is
// repurposed as a multi-product landing page.
//
// The README files diverge in two directions:
// - README.md is what GitHub renders on the repo home page. We want this to
// cover the SDK, JetBrains plugin, CLI, and VS Code extension together.
// - README.marketplace.md is what users see on the VS Code Marketplace and
// inside the extension after install. It stays focused on the VS Code UX.
//
// vsce reads README.md from the extension root at `vsce package` / `vsce publish`
// time and has no flag to point it elsewhere, so we copy README.marketplace.md
// over README.md just before packaging and put the original back afterwards.
//
// swapIn is idempotent: if README.md already matches README.marketplace.md
// (e.g., an outer wrapper has already swapped), it no-ops instead of erroring
// on the backup file. This lets nested callers (ext-vscode-publish-stable.yml wrapping the whole
// step, plus the individual npm scripts swapping internally) coexist safely.
import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const projectRoot = path.join(__dirname, "..")
const README_PATH = path.join(projectRoot, "README.md")
const MARKETPLACE_PATH = path.join(projectRoot, "README.marketplace.md")
const BACKUP_PATH = path.join(projectRoot, ".README.github.bak")
function readFile(p) {
return fs.readFileSync(p, "utf-8")
}
export function swapIn() {
if (!fs.existsSync(MARKETPLACE_PATH)) {
throw new Error(`Missing ${MARKETPLACE_PATH}. The marketplace README must exist before publishing.`)
}
if (!fs.existsSync(README_PATH)) {
throw new Error(`Missing ${README_PATH}. Cannot swap in marketplace README.`)
}
if (readFile(README_PATH) === readFile(MARKETPLACE_PATH)) {
return { skipped: true }
}
if (fs.existsSync(BACKUP_PATH)) {
throw new Error(
`Stale backup at ${BACKUP_PATH}. A previous publish may have aborted before restoring README.md. ` +
`Move it back to README.md manually before retrying.`,
)
}
fs.copyFileSync(README_PATH, BACKUP_PATH)
fs.copyFileSync(MARKETPLACE_PATH, README_PATH)
return { skipped: false }
}
export function restore() {
if (!fs.existsSync(BACKUP_PATH)) {
return { skipped: true }
}
fs.copyFileSync(BACKUP_PATH, README_PATH)
fs.unlinkSync(BACKUP_PATH)
return { skipped: false }
}
const invokedAsCli = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)
if (invokedAsCli) {
const cmd = process.argv[2]
try {
if (cmd === "swap-in") {
const result = swapIn()
console.log(result.skipped ? "marketplace-readme: already swapped, skipping" : "marketplace-readme: swapped in")
} else if (cmd === "restore") {
const result = restore()
console.log(result.skipped ? "marketplace-readme: no backup, skipping" : "marketplace-readme: restored")
} else {
console.error("Usage: marketplace-readme.mjs <swap-in|restore>")
process.exit(2)
}
} catch (err) {
console.error(`marketplace-readme: ${err.message}`)
process.exit(1)
}
}
@@ -1,55 +0,0 @@
#!/usr/bin/env node
// Wraps the marketplace publish flow (vsce + ovsx) so the .vsix gets packaged
// with the marketplace-flavored README instead of the GitHub-flavored README.
//
// vsce reads README.md from the extension root at publish time and there's no
// flag to point it elsewhere, so we swap README.marketplace.md into place
// first and restore the original on the way out. The swap helper is
// idempotent, so this is safe to run nested under another wrapper (e.g., the
// CI step in .github/workflows/ext-vscode-publish-stable.yml that also packages a .vsix for the
// GitHub release artifact before invoking this script).
//
// Usage:
// node scripts/publish-marketplace.mjs # release channel
// node scripts/publish-marketplace.mjs --pre-release # pre-release channel
import { execFileSync } from "node:child_process"
import { restore, swapIn } from "./marketplace-readme.mjs"
const isPrerelease = process.argv.includes("--pre-release")
const result = swapIn()
let interrupted = false
const cleanupOnSignal = (exitCode) => () => {
interrupted = true
try {
if (!result.skipped) {
restore()
}
} catch (err) {
console.error(`marketplace-readme: failed to restore on signal: ${err.message}`)
}
process.exit(exitCode)
}
process.on("SIGINT", cleanupOnSignal(130))
process.on("SIGTERM", cleanupOnSignal(143))
try {
const vsceArgs = ["publish", "--allow-package-secrets", "sendgrid"]
if (isPrerelease) {
vsceArgs.push("--pre-release")
}
execFileSync("vsce", vsceArgs, { stdio: "inherit" })
const ovsxArgs = ["ovsx", "publish"]
if (isPrerelease) {
ovsxArgs.push("--pre-release")
}
execFileSync("npx", ovsxArgs, { stdio: "inherit" })
} finally {
if (!interrupted && !result.skipped) {
restore()
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"version": 1,
"skills": {
"cline-sdk": {
"source": "cline/sdk-skill",
"sourceType": "github",
"skillPath": "skill/cline-sdk/SKILL.md",
"computedHash": "ce565d78d5bd1c40075248a3a925b2059c5c024bfcdbbf1f55bf3440cae34144"
}
}
}
@@ -1,236 +0,0 @@
import { afterEach, describe, it } from "mocha"
import sinon from "sinon"
import "should"
import { anthropicModels } from "@shared/api"
import { ANTHROPIC_FAST_MODE_BETA, AnthropicHandler } from "../anthropic"
describe("AnthropicHandler", () => {
afterEach(() => {
sinon.restore()
})
const createAsyncIterable = (data: readonly unknown[] = []) => ({
[Symbol.asyncIterator]: async function* () {
yield* data
},
})
describe("getModel", () => {
it("should return the fast mode model when configured", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-6:fast",
})
const result = handler.getModel()
result.id.should.equal("claude-opus-4-6:fast")
result.info.should.deepEqual(anthropicModels["claude-opus-4-6:fast"])
})
it("should return the 1m fast mode model when configured", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-6:1m:fast",
})
const result = handler.getModel()
result.id.should.equal("claude-opus-4-6:1m:fast")
result.info.should.deepEqual(anthropicModels["claude-opus-4-6:1m:fast"])
})
it("should return the 4.7 model when configured", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-7",
})
const result = handler.getModel()
result.id.should.equal("claude-opus-4-7")
result.info.should.deepEqual(anthropicModels["claude-opus-4-7"])
})
it("should return the 4.7 1m model when configured", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-7:1m",
})
const result = handler.getModel()
result.id.should.equal("claude-opus-4-7:1m")
result.info.should.deepEqual(anthropicModels["claude-opus-4-7:1m"])
})
it("should return the 4.8 model when configured", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-8",
})
const result = handler.getModel()
result.id.should.equal("claude-opus-4-8")
result.info.should.deepEqual(anthropicModels["claude-opus-4-8"])
})
it("should return the 4.8 1m model when configured", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-8:1m",
})
const result = handler.getModel()
result.id.should.equal("claude-opus-4-8:1m")
result.info.should.deepEqual(anthropicModels["claude-opus-4-8:1m"])
})
})
describe("createMessage", () => {
it("should route fast mode requests through the beta messages API", async () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-6:fast",
})
const standardCreate = sinon.stub().resolves(createAsyncIterable())
const betaCreate = sinon.stub().callsFake(function (this: { _client?: object }, _params: unknown) {
should.exist(this._client)
return Promise.resolve(createAsyncIterable())
})
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
messages: {
create: standardCreate,
},
beta: {
messages: {
_client: {},
create: betaCreate,
},
},
})
for await (const _chunk of handler.createMessage("system prompt", [{ role: "user", content: "Hello" }])) {
}
sinon.assert.notCalled(standardCreate)
sinon.assert.calledOnce(betaCreate)
sinon.assert.calledWithMatch(betaCreate, {
model: "claude-opus-4-6",
betas: [ANTHROPIC_FAST_MODE_BETA],
speed: "fast",
stream: true,
})
})
it("should include the 1m beta when routing 1m fast mode requests through the beta messages API", async () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-6:1m:fast",
})
const standardCreate = sinon.stub().resolves(createAsyncIterable())
const betaCreate = sinon.stub().callsFake(function (this: { _client?: object }, _params: unknown) {
should.exist(this._client)
return Promise.resolve(createAsyncIterable())
})
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
messages: {
create: standardCreate,
},
beta: {
messages: {
_client: {},
create: betaCreate,
},
},
})
for await (const _chunk of handler.createMessage("system prompt", [{ role: "user", content: "Hello" }])) {
}
sinon.assert.notCalled(standardCreate)
sinon.assert.calledOnce(betaCreate)
sinon.assert.calledWithMatch(betaCreate, {
model: "claude-opus-4-6",
betas: [ANTHROPIC_FAST_MODE_BETA, "context-1m-2025-08-07"],
speed: "fast",
stream: true,
})
})
it("should include the 1m beta header for Claude Opus 4.7 1m requests", async () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-7:1m",
reasoningEffort: "high",
})
const standardCreate = sinon.stub().resolves(createAsyncIterable())
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
messages: {
create: standardCreate,
},
beta: {
messages: {
_client: {},
create: sinon.stub().resolves(createAsyncIterable()),
},
},
})
for await (const _chunk of handler.createMessage("system prompt", [{ role: "user", content: "Hello" }])) {
}
sinon.assert.calledOnce(standardCreate)
const requestBody = standardCreate.firstCall.args[0] as Record<string, any>
const requestOptions = standardCreate.firstCall.args[1] as Record<string, any>
requestBody.model.should.equal("claude-opus-4-7")
requestBody.thinking.should.deepEqual({ type: "adaptive" })
requestOptions.should.deepEqual({
headers: {
"anthropic-beta": "context-1m-2025-08-07",
},
})
})
it("should use adaptive thinking and output_config for Claude Opus adaptive models", async () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-opus-4-7",
reasoningEffort: "xhigh",
})
const standardCreate = sinon.stub().resolves(createAsyncIterable())
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
messages: {
create: standardCreate,
},
beta: {
messages: {
_client: {},
create: sinon.stub().resolves(createAsyncIterable()),
},
},
})
for await (const _chunk of handler.createMessage("system prompt", [{ role: "user", content: "Hello" }])) {
}
sinon.assert.calledOnce(standardCreate)
const requestBody = standardCreate.firstCall.args[0] as Record<string, any>
requestBody.should.have.property("thinking")
requestBody.thinking.should.deepEqual({ type: "adaptive" })
requestBody.should.have.property("output_config")
requestBody.output_config.should.deepEqual({ effort: "xhigh" })
should(requestBody.temperature).equal(undefined)
})
})
})
@@ -1,53 +0,0 @@
import "should"
import { moonshotModels } from "@shared/api"
import type { ClineStorageMessage } from "@shared/messages/content"
import sinon from "sinon"
import { MoonshotHandler } from "../moonshot"
interface MoonshotRequestPayload {
model: string
temperature: number
max_tokens: number
}
describe("MoonshotHandler", () => {
afterEach(() => {
sinon.restore()
})
const createAsyncIterable = (data: unknown[] = []): AsyncIterable<unknown> => ({
[Symbol.asyncIterator]: async function* () {
yield* data
},
})
it("supports kimi-k2.6 model metadata", async () => {
const handler = new MoonshotHandler({
moonshotApiKey: "test-api-key",
apiModelId: "kimi-k2.6",
})
const model = handler.getModel()
model.id.should.equal("kimi-k2.6")
model.info.should.deepEqual(moonshotModels["kimi-k2.6"])
const createStub = sinon.stub().resolves(createAsyncIterable([]))
sinon.stub(handler as unknown as { ensureClient: () => unknown }, "ensureClient").returns({
chat: {
completions: {
create: createStub,
},
},
})
const messages: ClineStorageMessage[] = [{ role: "user", content: "hi" }]
for await (const _chunk of handler.createMessage("system", messages)) {
// Consume stream to trigger request execution.
}
const payload = createStub.firstCall.args[0] as MoonshotRequestPayload
payload.model.should.equal("kimi-k2.6")
payload.temperature.should.equal(moonshotModels["kimi-k2.6"].temperature)
payload.max_tokens.should.equal(moonshotModels["kimi-k2.6"].maxTokens)
})
})
@@ -1,23 +0,0 @@
import "should"
import { vertexGlobalModels } from "@shared/api"
import { VertexHandler } from "../vertex"
describe("VertexHandler", () => {
it("supports Gemini 3.5 Flash model metadata", () => {
const handler = new VertexHandler({
vertexProjectId: "test-project",
vertexRegion: "global",
apiModelId: "gemini-3.5-flash",
})
const model = handler.getModel()
model.id.should.equal("gemini-3.5-flash")
model.info.contextWindow!.should.equal(1_048_576)
model.info.inputPrice!.should.equal(1.5)
model.info.outputPrice!.should.equal(9)
model.info.cacheReadsPrice!.should.equal(0.15)
model.info.supportsGlobalEndpoint!.should.equal(true)
model.info.supportsReasoning!.should.equal(true)
vertexGlobalModels.should.have.property("gemini-3.5-flash")
})
})
@@ -1,274 +0,0 @@
import { getSkillsDirectoriesForScan } from "@core/storage/disk"
import type { GlobalInstructionsFile } from "@shared/remote-config/schema"
import type { SkillContent, SkillMetadata } from "@shared/skills"
import { fileExistsAtPath, isDirectory } from "@utils/fs"
import * as fs from "fs/promises"
import * as path from "path"
import { Logger } from "@/shared/services/Logger"
import { parseYamlFrontmatter } from "./frontmatter"
/**
* A remote skill entry after frontmatter validation.
* name is always frontmatter.name (canonical). A warning is logged if entry.name drifts.
*/
export interface ValidatedRemoteSkill {
name: string
description: string
alwaysEnabled: boolean
contents: string
}
export interface SkillToggleState {
globalSkillsToggles?: Record<string, boolean>
localSkillsToggles?: Record<string, boolean>
remoteSkillsToggles?: Record<string, boolean>
remoteSkillEntries?: GlobalInstructionsFile[]
}
/**
* Parse and validate remote skill entries from GlobalInstructionsFile[].
*
* Validates:
* - frontmatter.name and frontmatter.description are present strings
* - Warns if entry.name does not match frontmatter.name (drift)
*
* Returns only valid entries. Callers share this single validation point
* instead of duplicating frontmatter parsing.
*/
export function parseRemoteSkillEntries(entries: GlobalInstructionsFile[]): ValidatedRemoteSkill[] {
return entries
.map((entry) => {
const { data: frontmatter } = parseYamlFrontmatter(entry.contents)
if (!frontmatter.name || typeof frontmatter.name !== "string") return null
if (!frontmatter.description || typeof frontmatter.description !== "string") return null
// Warn on drift but use frontmatter.name as the canonical identity.
// The dashboard should keep entry.name in sync, but we don't reject on mismatch
// since that would silently hide org-configured skills from users.
if (entry.name !== frontmatter.name) {
Logger.warn(`Remote skill entry.name "${entry.name}" does not match frontmatter.name "${frontmatter.name}"`)
}
return {
name: frontmatter.name,
description: frontmatter.description as string,
alwaysEnabled: entry.alwaysEnabled,
contents: entry.contents,
}
})
.filter((e): e is NonNullable<typeof e> => e !== null)
}
/** Parse YAML frontmatter from markdown content (shared helper). */
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
const result = parseYamlFrontmatter(fileContent)
if (result.parseError) {
Logger.warn("Failed to parse YAML frontmatter:", result.parseError)
}
return { data: result.data, content: result.body }
}
/**
* 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") {
Logger.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") {
Logger.warn(`Skill at ${skillDir} missing required 'name' field`)
return null
}
if (!frontmatter.description || typeof frontmatter.description !== "string") {
Logger.warn(`Skill at ${skillDir} missing required 'description' field`)
return null
}
// Name must match directory name per spec
if (frontmatter.name !== skillName) {
Logger.warn(`Skill name "${frontmatter.name}" doesn't match directory "${skillName}"`)
return null
}
return {
name: skillName,
description: frontmatter.description,
path: skillMdPath,
source,
}
} catch (error) {
Logger.warn(`Failed to load skill at ${skillDir}:`, error)
return null
}
}
/**
* Discover all skills from global (~/.cline/skills), remote config, and project directories.
*
* Precedence (highest wins on name collision via getAvailableSkills):
* remote (enterprise) > disk-global (user personal) > project (workspace)
*
* This is achieved by the array order + getAvailableSkills iterating in reverse (last wins):
* [project..., disk-global..., remote...]
*/
export async function discoverSkills(cwd: string, remoteSkillEntries?: GlobalInstructionsFile[]): Promise<SkillMetadata[]> {
const skills: SkillMetadata[] = []
const scanDirs = getSkillsDirectoriesForScan(cwd)
// Collect project and disk-global skills separately so we can insert remote between them
const projectSkills: SkillMetadata[] = []
const diskGlobalSkills: SkillMetadata[] = []
for (const dir of scanDirs) {
const dirSkills = await scanSkillsDirectory(dir.path, dir.source)
if (dir.source === "project") {
projectSkills.push(...dirSkills)
} else {
diskGlobalSkills.push(...dirSkills)
}
}
// Remote skills: validated via parseRemoteSkillEntries and keyed by frontmatter.name.
const remoteSkills: SkillMetadata[] = parseRemoteSkillEntries(remoteSkillEntries || []).map((entry) => ({
name: entry.name,
description: entry.description,
path: `remote:${entry.name}`,
source: "global" as const,
}))
// Insert in order: project → disk-global → remote
// getAvailableSkills iterates backwards so remote (last) wins, then disk-global, then project
skills.push(...projectSkills, ...diskGlobalSkills, ...remoteSkills)
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
}
export function filterEnabledSkills(skills: SkillMetadata[], toggleState: SkillToggleState = {}): SkillMetadata[] {
const globalSkillsToggles = toggleState.globalSkillsToggles ?? {}
const localSkillsToggles = toggleState.localSkillsToggles ?? {}
const remoteSkillsToggles = toggleState.remoteSkillsToggles ?? {}
const remoteSkillMap = new Map(
parseRemoteSkillEntries(toggleState.remoteSkillEntries || []).map((entry) => [entry.name, entry]),
)
return skills.filter((skill) => {
if (skill.path.startsWith("remote:")) {
const name = skill.path.replace("remote:", "")
const entry = remoteSkillMap.get(name)
if (entry?.alwaysEnabled) {
return true
}
return remoteSkillsToggles[name] !== false
}
const toggles = skill.source === "global" ? globalSkillsToggles : localSkillsToggles
return toggles[skill.path] !== false
})
}
export async function discoverAvailableSkills(cwd: string, toggleState: SkillToggleState = {}): Promise<SkillMetadata[]> {
const allSkills = await discoverSkills(cwd, toggleState.remoteSkillEntries)
return filterEnabledSkills(getAvailableSkills(allSkills), toggleState)
}
/**
* Get full skill content including instructions.
* For remote skills, pass remoteSkillEntries so content can be loaded without disk I/O.
*/
export async function getSkillContent(
skillName: string,
availableSkills: SkillMetadata[],
remoteSkillEntries?: GlobalInstructionsFile[],
): Promise<SkillContent | null> {
const skill = availableSkills.find((s) => s.name === skillName)
if (!skill) return null
// Remote skills have no file on disk — retrieve content from the provided entries.
// Try entry.name first (fast path when dashboard is in sync), fall back to frontmatter match.
if (skill.path.startsWith("remote:")) {
let entry = (remoteSkillEntries || []).find((e) => e.name === skillName)
if (!entry) {
entry = (remoteSkillEntries || []).find((e) => {
const { data } = parseYamlFrontmatter(e.contents)
return typeof data.name === "string" && data.name === skillName
})
}
if (!entry) return null
const { body } = parseYamlFrontmatter(entry.contents)
return {
...skill,
instructions: body.trim(),
}
}
try {
const fileContent = await fs.readFile(skill.path, "utf-8")
const { content: body } = parseFrontmatter(fileContent)
return {
...skill,
instructions: body.trim(),
}
} catch {
return null
}
}
@@ -1,32 +0,0 @@
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
import { ensureWorkflowsDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { ClineRulesToggles } from "@shared/cline-rules"
import path from "path"
import { Controller } from "@/core/controller"
/**
* Refresh the workflow toggles
*/
export async function refreshWorkflowToggles(
controller: Controller,
workingDirectory: string,
): Promise<{
globalWorkflowToggles: ClineRulesToggles
localWorkflowToggles: ClineRulesToggles
}> {
// Global workflows
const globalWorkflowToggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
const globalClineWorkflowsFilePath = await ensureWorkflowsDirectoryExists()
const updatedGlobalWorkflowToggles = await synchronizeRuleToggles(globalClineWorkflowsFilePath, globalWorkflowToggles)
controller.stateManager.setGlobalState("globalWorkflowToggles", updatedGlobalWorkflowToggles)
const workflowRulesToggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
controller.stateManager.setWorkspaceState("workflowToggles", updatedWorkflowToggles)
return {
globalWorkflowToggles: updatedGlobalWorkflowToggles,
localWorkflowToggles: updatedWorkflowToggles,
}
}
@@ -1,45 +0,0 @@
import { discoverChromeInstances } from "@services/browser/BrowserDiscovery"
import { BrowserSession } from "@services/browser/BrowserSession"
import { BrowserConnection } from "@shared/proto/cline/browser"
import { EmptyRequest } from "@shared/proto/cline/common"
import { Controller } from "../index"
/**
* Discover Chrome instances
* @param controller The controller instance
* @param request The request message
* @returns The browser connection result
*/
export async function discoverBrowser(controller: Controller, _request: EmptyRequest): Promise<BrowserConnection> {
try {
const discoveredHost = await discoverChromeInstances()
if (discoveredHost) {
// Don't update the remoteBrowserHost state when auto-discovering
// This way we don't override the user's preference
// Test the connection to get the endpoint
const browserSession = new BrowserSession(controller.stateManager)
const result = await browserSession.testConnection(discoveredHost)
return BrowserConnection.create({
success: true,
message: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
endpoint: result.endpoint || "",
})
} else {
return BrowserConnection.create({
success: false,
message:
"No Chrome instances found. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
endpoint: "",
})
}
} catch (error) {
return BrowserConnection.create({
success: false,
message: `Error discovering browser: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
})
}
}
@@ -1,47 +0,0 @@
import { BrowserConnectionInfo } from "@shared/proto/cline/browser"
import { EmptyRequest } from "@shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
import { Controller } from "../index"
/**
* Get information about the current browser connection
* @param controller The controller instance
* @param request The request message
* @returns The browser connection info
*/
export async function getBrowserConnectionInfo(controller: Controller, _: EmptyRequest): Promise<BrowserConnectionInfo> {
try {
// Get browser settings from extension state
const browserSettings = controller.stateManager.getGlobalSettingsKey("browserSettings")
// Check if there's an active browser session by using the controller's handleWebviewMessage approach
// This is similar to what's done in controller/index.ts for the "getBrowserConnectionInfo" message
if (controller.task?.browserSession) {
// Access the browser session through the controller's task property
// Using indexer notation to access private property
const browserSession = controller.task.browserSession
const connectionInfo = browserSession.getConnectionInfo()
// Convert from BrowserSession.BrowserConnectionInfo to proto.BrowserConnectionInfo
return BrowserConnectionInfo.create({
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host || "", // Ensure host is never undefined
})
}
// Fallback to browser settings if no active browser session
return BrowserConnectionInfo.create({
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost || "",
})
} catch (error: unknown) {
Logger.error("Error getting browser connection info:", error)
return BrowserConnectionInfo.create({
isConnected: false,
isRemote: false,
host: "",
})
}
}
@@ -1,29 +0,0 @@
import { ChromePath } from "@shared/proto/cline/browser"
import { EmptyRequest } from "@shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
import { BrowserSession } from "../../../services/browser/BrowserSession"
import { Controller } from "../index"
/**
* Get the detected Chrome executable path
* @param controller The controller instance
* @param request The empty request message
* @returns The detected Chrome path and whether it's bundled
*/
export async function getDetectedChromePath(controller: Controller, _: EmptyRequest): Promise<ChromePath> {
try {
const browserSession = new BrowserSession(controller.stateManager)
const result = await browserSession.getDetectedChromePath()
return ChromePath.create({
path: result.path,
isBundled: result.isBundled,
})
} catch (error) {
Logger.error("Error getting detected Chrome path:", error)
return ChromePath.create({
path: "",
isBundled: false,
})
}
}
@@ -1,24 +0,0 @@
import { EmptyRequest, String as StringMessage } from "@shared/proto/cline/common"
import { BrowserSession } from "../../../services/browser/BrowserSession"
import { Controller } from "../index"
/**
* Relaunch Chrome in debug mode
* @param controller The controller instance
* @param request The empty request message
* @returns The browser relaunch result as a string message
*/
export async function relaunchChromeDebugMode(controller: Controller, _: EmptyRequest): Promise<StringMessage> {
try {
const browserSession = new BrowserSession(controller.stateManager)
// Relaunch Chrome in debug mode
await browserSession.relaunchChromeDebugMode(controller)
// The actual result will be sent via the ProtoBus in the BrowserSession.relaunchChromeDebugMode method
// Here we just return a message as a placeholder
return { value: "Chrome relaunch initiated" }
} catch (error) {
throw new Error(`Error relaunching Chrome: ${error instanceof Error ? error.message : globalThis.String(error)}`)
}
}
@@ -1,61 +0,0 @@
import { discoverChromeInstances } from "@services/browser/BrowserDiscovery"
import { BrowserSession } from "@services/browser/BrowserSession"
import { BrowserConnection } from "@shared/proto/cline/browser"
import { StringRequest } from "@shared/proto/cline/common"
import { Controller } from "../index"
/**
* Test connection to a browser instance
* @param controller The controller instance
* @param request The request message
* @returns The browser connection result
*/
export async function testBrowserConnection(controller: Controller, request: StringRequest): Promise<BrowserConnection> {
try {
const browserSession = new BrowserSession(controller.stateManager)
const text = request.value || ""
// If no text is provided, try auto-discovery
if (!text) {
try {
const discoveredHost = await discoverChromeInstances()
if (discoveredHost) {
// Test the connection to the discovered host
const result = await browserSession.testConnection(discoveredHost)
return BrowserConnection.create({
success: result.success,
message: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
endpoint: result.endpoint || "",
})
} else {
return BrowserConnection.create({
success: false,
message:
"No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
endpoint: "",
})
}
} catch (error) {
return BrowserConnection.create({
success: false,
message: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
})
}
} else {
// Test the provided URL
const result = await browserSession.testConnection(text)
return BrowserConnection.create({
success: result.success,
message: result.message,
endpoint: result.endpoint || "",
})
}
} catch (error) {
return BrowserConnection.create({
success: false,
message: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
})
}
}
@@ -1,40 +0,0 @@
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
import { telemetryService } from "../../../services/telemetry"
import { Empty, StringRequest } from "../../../shared/proto/cline/common"
import { ensureFocusChainFile, extractFocusChainListFromText } from "../../task/focus-chain/file-utils"
import { Controller } from ".."
/**
* Opens or creates a focus chain checklist markdown file for editing
* The file is stored at <globalStorage>/tasks/<taskId>/focus_chain_taskid_<taskId>.md
*/
export async function openFocusChainFile(controller: Controller, request: StringRequest): Promise<Empty> {
if (!request.value) {
throw new Error("Task ID is required")
}
const taskId = request.value
// Get the current focus chain list from the task's most recent task_progress message
let initialFocusChainContent: string | undefined
const currentTask = controller.task
if (currentTask) {
// Get the task's message history and find the most recent task_progress message
// TODO - can we decouple this from ClineMessages?
const clineMessages = currentTask.messageStateHandler.getClineMessages()
const lastProgressMessage = clineMessages
.slice()
.reverse()
.find((m) => m.say === "task_progress")
if (lastProgressMessage && lastProgressMessage.text) {
initialFocusChainContent = extractFocusChainListFromText(lastProgressMessage.text) || undefined
}
}
const focusChainFilePath = await ensureFocusChainFile(taskId, initialFocusChainContent)
telemetryService.captureFocusChainListOpened(taskId)
await openFileIntegration(focusChainFilePath)
return Empty.create()
}
@@ -1,174 +0,0 @@
import {
type FileSearchSource,
RipgrepError,
type SearchWorkspaceFilesResult,
searchWorkspaceFiles,
searchWorkspaceFilesMultiroot,
} from "@services/search/file-search"
import { telemetryService } from "@services/telemetry"
import { FileSearchRequest, FileSearchResults, FileSearchType } from "@shared/proto/cline/file"
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
import { type FsInfo, getFsInfo } from "@utils/fs-info"
import { getWorkspacePath } from "@utils/path"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
// error_reason values surfaced on FileSearchResults; see proto/cline/file.proto.
const ERROR_REASON_WORKSPACE_UNAVAILABLE = "workspace_unavailable"
const ERROR_REASON_RIPGREP_SPAWN_FAILED = "ripgrep_spawn_failed"
const ERROR_REASON_UNKNOWN = "unknown"
function classifyError(error: unknown): { errorReason: string; errorMessage: string } {
const errorMessage = error instanceof Error ? error.message : String(error)
if (error instanceof RipgrepError) {
const firstStderrLine = error.stderr ? error.stderr.trim().split("\n", 1)[0] : ""
return {
errorReason: ERROR_REASON_RIPGREP_SPAWN_FAILED,
errorMessage: firstStderrLine || errorMessage,
}
}
return { errorReason: ERROR_REASON_UNKNOWN, errorMessage }
}
// Fire-and-forget the FS-class lookup + telemetry capture. The picker awaits
// the searchFiles response, so we must not block it on a slow/hung mount —
// `getFsInfo` does a `realpath` and a `mount`/`stat -f` that, even with the
// outer timeout in fs-info, can still cost seconds on a stale network FS.
function captureWithFsContext(fsContextPath: string | undefined, capture: (fsContext: FsInfo) => void | Promise<void>): void {
getFsInfo(fsContextPath)
.then(capture)
.catch((err) => Logger.warn(`searchFiles: telemetry capture failed: ${err}`))
}
/**
* Searches for files in the workspace with fuzzy matching
* @param controller The controller instance
* @param request The request containing search query, and optionally a mentionsRequestId and workspace_hint
* @returns Results containing matching files/folders
*/
export async function searchFiles(controller: Controller, request: FileSearchRequest): Promise<FileSearchResults> {
// Best-effort path used for FS-class telemetry. Declared in the function
// scope so the catch block can also reference it. When the request carries
// a workspaceHint we tag against the matched root; for cross-root searches
// (no hint) we fall back to the primary root, since attributing one event
// to "the root that mattered" is impossible without per-root events.
let fsContextPath: string | undefined
try {
// Map enum to string for the search service
let selectedTypeString: "file" | "folder" | undefined
if (request.selectedType === FileSearchType.FILE) {
selectedTypeString = "file"
} else if (request.selectedType === FileSearchType.FOLDER) {
selectedTypeString = "folder"
}
// Extract hint, ensure workspaceManager is ready, check for multiroot
const workspaceHint = request.workspaceHint
const workspaceManager = await controller.ensureWorkspaceManager()
const hasMultirootSupport = workspaceManager && workspaceManager.getRoots()?.length > 0
let searchResult: SearchWorkspaceFilesResult
if (hasMultirootSupport) {
// Tag the actually-searched root, not always the primary —
// otherwise an SSHFS secondary root looks like a fast primary
// in dashboards. searchWorkspaceFilesMultiroot resolves the hint
// the same way (by name).
const hintedRoot = workspaceHint
? (workspaceManager.getRootByName(workspaceHint) ??
workspaceManager.getRoots().find((r) => r.path === workspaceHint))
: undefined
fsContextPath = hintedRoot?.path ?? workspaceManager.getRoots()[0]?.path
searchResult = await searchWorkspaceFilesMultiroot(
request.query || "",
workspaceManager,
request.limit || 20,
selectedTypeString,
workspaceHint,
)
} else {
// Legacy single workspace search
const workspacePath = await getWorkspacePath()
if (!workspacePath) {
Logger.error("Error in searchFiles: No workspace path available")
telemetryService.captureMentionFailed("folder", "workspace_unavailable", "No workspace path available")
return {
results: [],
mentionsRequestId: request.mentionsRequestId,
errorReason: ERROR_REASON_WORKSPACE_UNAVAILABLE,
errorMessage: "No workspace path available",
}
}
fsContextPath = workspacePath
// Call file search service with query from request
searchResult = await searchWorkspaceFiles(
request.query || "",
workspacePath,
request.limit || 20, // Use default limit of 20 if not specified
selectedTypeString,
)
}
const searchSource: FileSearchSource = searchResult.source
// Convert search results to proto FileInfo objects using the conversion function
const protoResults = convertSearchResultsToProtoFileInfos(searchResult.items)
// Track search results telemetry
// Determine search type for telemetry
let searchType: "file" | "folder" | "all" = "all"
if (request.selectedType === FileSearchType.FILE) {
searchType = "file"
} else if (request.selectedType === FileSearchType.FOLDER) {
searchType = "folder"
}
captureWithFsContext(fsContextPath, (fsContext) =>
telemetryService.captureMentionSearchResults(
request.query || "",
protoResults.length,
searchType,
protoResults.length === 0,
fsContext,
searchSource,
),
)
// Return successful results
return { results: protoResults, mentionsRequestId: request.mentionsRequestId }
} catch (error) {
const { errorReason, errorMessage } = classifyError(error)
Logger.error(`Error in searchFiles (errorReason=${errorReason}):`, error)
const mentionType =
request.selectedType === FileSearchType.FILE
? "file"
: request.selectedType === FileSearchType.FOLDER
? "folder"
: "folder" // Default to folder for "all" searches
const errorType: "ripgrep_spawn_failed" | "permission_denied" | "unknown" =
errorReason === ERROR_REASON_RIPGREP_SPAWN_FAILED
? "ripgrep_spawn_failed"
: error instanceof Error && error.message.includes("permission")
? "permission_denied"
: "unknown"
// fsContextPath may be unset if we threw before resolving the workspace;
// getFsInfo handles undefined and returns the unknown sentinel.
captureWithFsContext(fsContextPath, (fsContext) =>
telemetryService.captureMentionFailed(mentionType, errorType, errorMessage, fsContext),
)
return {
results: [],
mentionsRequestId: request.mentionsRequestId,
errorReason,
errorMessage,
}
}
}
@@ -1,53 +0,0 @@
import { ClineRulesToggles, RuleScope, ToggleWorkflowRequest } from "@shared/proto/cline/file"
import { Logger } from "@/shared/services/Logger"
import { Controller } from ".."
/**
* Toggles a workflow on or off
* @param controller The controller instance
* @param request The request containing the workflow path and enabled state
* @returns The updated workflow toggles
*/
export async function toggleWorkflow(controller: Controller, request: ToggleWorkflowRequest): Promise<ClineRulesToggles> {
const { workflowPath, enabled, scope } = request
if (!workflowPath || typeof enabled !== "boolean" || scope === undefined) {
Logger.error("toggleWorkflow: Missing or invalid parameters", {
workflowPath,
scope,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
})
throw new Error("Missing or invalid parameters for toggleWorkflow")
}
// Handle the three different scopes
let toggles: Record<string, boolean>
switch (scope) {
case RuleScope.GLOBAL: {
toggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
toggles[workflowPath] = enabled
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
break
}
case RuleScope.LOCAL: {
toggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
toggles[workflowPath] = enabled
controller.stateManager.setWorkspaceState("workflowToggles", toggles)
break
}
case RuleScope.REMOTE: {
toggles = controller.stateManager.getGlobalStateKey("remoteWorkflowToggles")
toggles[workflowPath] = enabled
controller.stateManager.setGlobalState("remoteWorkflowToggles", toggles)
break
}
default:
throw new Error(`Invalid scope: ${scope}`)
}
await controller.postStateToWebview()
// Return the updated toggles
return ClineRulesToggles.create({ toggles: toggles })
}
@@ -1,77 +0,0 @@
import * as disk from "@core/storage/disk"
import axios from "axios"
import { expect } from "chai"
import fs from "fs/promises"
import { afterEach, beforeEach, describe, it } from "mocha"
import sinon from "sinon"
import { ClineEnv, Environment } from "@/config"
import { StateManager } from "@/core/storage/StateManager"
import { getFeatureFlagsService } from "@/services/feature-flags"
import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
import { Logger } from "@/shared/services/Logger"
import { refreshClineModels } from "../refreshClineModels"
describe("refreshClineModels", () => {
let sandbox: sinon.SinonSandbox
beforeEach(() => {
sandbox = sinon.createSandbox()
sandbox.stub(Logger, "log")
sandbox.stub(Logger, "error")
})
afterEach(() => {
sandbox.restore()
})
it("marks Qwen 3.7 Max as prompt-cache capable when Cline model pricing includes cache reads", async () => {
sandbox.stub(getFeatureFlagsService(), "getBooleanFlagEnabled").callsFake((flag) => {
return flag === FeatureFlag.EXTENSION_CLINE_MODELS_ENDPOINT
})
sandbox.stub(ClineEnv, "config").returns({
environment: Environment.production,
appBaseUrl: "https://app.cline-mock.bot",
apiBaseUrl: "https://api.cline-mock.bot",
mcpBaseUrl: "https://api.cline-mock.bot/v1/mcp",
})
sandbox.stub(StateManager, "get").returns({
getModelsCache: () => null,
setModelsCache: () => {},
} as any)
sandbox.stub(disk, "ensureCacheDirectoryExists").resolves("/tmp")
sandbox.stub(fs, "writeFile").resolves()
sandbox.stub(axios, "get").resolves({
data: {
data: [
{
id: "qwen/qwen3.7-max",
name: "Qwen: Qwen3.7 Max",
description: null,
context_length: 1_000_000,
top_provider: {
max_completion_tokens: 65_536,
context_length: 1_000_000,
is_moderated: false,
},
architecture: {
modality: "text->text",
},
pricing: {
prompt: "0.00000125",
completion: "0.00000375",
input_cache_read: "0.00000025",
},
supported_parameters: ["include_reasoning", "reasoning"],
},
],
},
})
const models = await refreshClineModels({} as any)
const qwen37 = models["qwen/qwen3.7-max"]
expect(qwen37.supportsPromptCache).to.equal(true)
expect(qwen37.cacheReadsPrice).to.equal(0.25)
expect(qwen37.cacheWritesPrice).to.equal(undefined)
})
})
@@ -1,10 +0,0 @@
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { Controller } from ".."
/**
* Report bug slash command logic
*/
export async function reportBug(controller: Controller, _request: StringRequest): Promise<Empty> {
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
return Empty.create()
}
@@ -1,88 +0,0 @@
import type { ApiProviderInfo } from "@/core/api"
import type { SystemPromptContext } from "@/core/prompts/system-prompt/types"
import { getDeepPlanningRegistry } from "./registry"
import { generateGemini3Template } from "./variants/gemini3"
import { generateGPT51Template } from "./variants/gpt51"
const focusChainIntro: string = `**Task Progress Parameter:**
When creating the new task, you must include a task_progress parameter that breaks down the implementation into trackable steps. This parameter should be included inside the tool call, but not located inside of other content/argument blocks. This should follow the standard Markdown checklist format with "- [ ]" for incomplete items.`
/**
* Generates the deep-planning slash command response with model-family-aware variant selection
* @param focusChainSettings Optional focus chain settings to include in the prompt
* @param providerInfo Optional API provider info for model family detection
* @param enableNativeToolCalls Optional flag to determine if native tool calling is enabled
* @returns The deep-planning prompt string with appropriate variant and focus chain settings applied
*/
export function getDeepPlanningPrompt(
focusChainSettings?: { enabled: boolean },
providerInfo?: ApiProviderInfo,
enableNativeToolCalls?: boolean,
): string {
// Create context for variant selection
const context: SystemPromptContext = {
providerInfo: providerInfo || ({} as ApiProviderInfo),
ide: "vscode",
}
// Get the appropriate variant from registry
const registry = getDeepPlanningRegistry()
const variant = registry.get(context)
const newTaskInstructions = generateNewTaskInstructions(enableNativeToolCalls ?? false)
const focusChainParam = focusChainSettings?.enabled ? focusChainIntro : ""
// For variants with extensive focus chain prompting, generate template with focus chain flag
let template: string
if (variant.id === "gpt-51") {
template = generateGPT51Template(focusChainSettings?.enabled ?? false, enableNativeToolCalls ?? false)
} else if (variant.id === "gemini-3") {
template = generateGemini3Template(focusChainSettings?.enabled ?? false, enableNativeToolCalls ?? false)
} else {
template = variant.template
template = template.replace("{{FOCUS_CHAIN_PARAM}}", focusChainParam)
template = template.replace("{{NEW_TASK_INSTRUCTIONS}}", newTaskInstructions)
}
return template
}
/**
* Generates the new_task tool instructions based on whether native tool calling is enabled
* @param enableNativeToolCalls Whether native tool calling is enabled
* @returns The new_task tool instructions string
*/
function generateNewTaskInstructions(enableNativeToolCalls: boolean): string {
if (enableNativeToolCalls) {
return `
**new_task Tool Definition:**
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
\`\`\`json
{
"name": "new_task",
"arguments": {
"context": "Your detailed context here following the 5-point structure..."
}
}
\`\`\`
The context parameter should include all five sections as described above.`
} else {
return `
**new_task Tool Definition:**
When you are ready to create the implementation task, you must call the new_task tool with the following structure:
\`\`\`xml
<new_task>
<context>Your detailed context here following the 5-point structure...</context>
</new_task>
\`\`\`
The context parameter should include all five sections as described above.`
}
}
// Export types for external use
export type { DeepPlanningRegistry, DeepPlanningVariant } from "./types"
@@ -1,100 +0,0 @@
import type { SystemPromptContext } from "@/core/prompts/system-prompt/types"
import { Logger } from "@/shared/services/Logger"
import type { DeepPlanningVariant, DeepPlanningRegistry as IDeepPlanningRegistry } from "./types"
import {
createAnthropicVariant,
createGemini3Variant,
createGeminiVariant,
createGenericVariant,
createGPT51Variant,
} from "./variants"
/**
* Singleton registry for managing deep-planning prompt variants
* Selects appropriate variant based on model family detection
*/
class DeepPlanningRegistry implements IDeepPlanningRegistry {
private static instance: DeepPlanningRegistry | null = null
private variants: Map<string, DeepPlanningVariant> = new Map()
private genericVariant: DeepPlanningVariant
private constructor() {
// Initialize all variants
this.registerVariant(createAnthropicVariant())
this.registerVariant(createGeminiVariant())
this.registerVariant(createGemini3Variant())
this.registerVariant(createGPT51Variant())
// Generic variant must be registered last as fallback
const genericVariant = createGenericVariant()
this.registerVariant(genericVariant)
this.genericVariant = genericVariant
}
/**
* Get the singleton instance of the registry
*/
public static getInstance(): DeepPlanningRegistry {
if (!DeepPlanningRegistry.instance) {
DeepPlanningRegistry.instance = new DeepPlanningRegistry()
}
return DeepPlanningRegistry.instance
}
/**
* Register a new variant in the registry
*/
public register(variant: DeepPlanningVariant): void {
this.registerVariant(variant)
}
/**
* Internal method to register a variant
*/
private registerVariant(variant: DeepPlanningVariant): void {
this.variants.set(variant.id, variant)
}
/**
* Get the appropriate variant based on the system prompt context
* Uses matcher functions to determine which variant to use
* Falls back to generic variant if no match or on error
*/
public get(context: SystemPromptContext): DeepPlanningVariant {
try {
// Try each variant's matcher function (except generic which is last)
for (const variant of this.variants.values()) {
// Skip generic variant in iteration (it's the fallback)
if (variant.id === "generic") {
continue
}
// Test if this variant matches the context
if (variant.matcher(context)) {
return variant
}
}
// No match found, return generic variant
return this.genericVariant
} catch (error) {
// On any error, safely fall back to generic variant
Logger.warn("Error selecting deep-planning variant, falling back to generic:", error)
return this.genericVariant
}
}
/**
* Get all registered variants
*/
public getAll(): DeepPlanningVariant[] {
return Array.from(this.variants.values())
}
}
/**
* Export singleton instance getter
*/
export function getDeepPlanningRegistry(): DeepPlanningRegistry {
return DeepPlanningRegistry.getInstance()
}
@@ -1,38 +0,0 @@
import type { SystemPromptContext } from "@/core/prompts/system-prompt/types"
/**
* Configuration for a deep-planning prompt variant
*/
export interface DeepPlanningVariant {
/** Unique identifier for this variant (e.g., "anthropic", "gemini", "gpt-5", "generic") */
id: string
/** Human-readable description of this variant */
description: string
/** The model family this variant is designed for */
family: string
/** Version number for this variant */
version: number
/** Matcher function to determine if this variant should be used */
matcher: (context: SystemPromptContext) => boolean
/** The complete prompt template string */
template: string
}
/**
* Registry for deep-planning prompt variants
*/
export interface DeepPlanningRegistry {
/** Get the appropriate variant based on context */
get(context: SystemPromptContext): DeepPlanningVariant
/** Register a new variant */
register(variant: DeepPlanningVariant): void
/** Get all registered variants */
getAll(): DeepPlanningVariant[]
}

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