Compare commits

...

90 Commits

Author SHA1 Message Date
Max Paulus 🥪 c93356cf22 remove unused code/files 2026-06-05 21:27:45 -07:00
Max Paulus 🥪 ab71a8946c delete unused files 2026-06-05 15:45:21 -07:00
Max Paulus 🥪 2df0ad4661 Add edit and regenerate for VS Code chat messages
Allow user feedback messages in the VS Code extension to be edited inline and regenerated from that point. Adds a TaskService RPC, truncates persisted SDK history before the selected visible user prompt, and starts a new session with the edited prompt. Also ensures the regenerated active task appears in extension history while SDK history catches up.
2026-06-05 15:03:01 -07:00
Max Paulus 🥪 1dc74bfd6b fix webview-ui tests 2026-06-05 12:39:43 -07:00
Max Paulus 🥪 00117d18e4 show model list if possible for openai compatible 2026-06-05 12:30:38 -07:00
Max Paulus 🥪 7351e2a8c0 fix onboarding model selection not persisting 2026-06-05 11:50:42 -07:00
Max Paulus 🥪 cbbb115f18 remove provider-specific views and just use genericprovidersettings.tsx 2026-06-05 11:45:26 -07:00
Max Paulus 🥪 d1a3af4bae dry up duplicate code and create useProviderModelSelection 2026-06-05 11:18:52 -07:00
Max Paulus 🥪 2563532e84 dry up provider api key logic 2026-06-05 11:11:00 -07:00
Max Paulus 🥪 21c35c95da dry up some duplicate code 2026-06-05 11:07:54 -07:00
Max Paulus 🥪 b423611e11 fix onboarding models 2026-06-05 10:54:32 -07:00
Max Paulus 🥪 2812c0f760 fix failing biome/lint 2026-06-05 10:34:46 -07:00
Mikołaj Kondratek 54ac72bd75 Remove unused import 2026-06-05 10:03:12 -07:00
Mikołaj Kondratek a23b86e81a fix(sdk): drop dead autoContinue branch in mode rebuild
cf25cd66a ("make extension plan mode more similar to CLI") removed the
file-level ACT_MODE_CONTINUATION_PROMPT constant and stopped passing the
autoContinue / continuationPrompt options when rebuilding a session for
a mode change, but left the corresponding block inside
rebuildSessionForMode in place. The block still references the deleted
constant, so tsc fails on the SDK migration branch with TS2304: Cannot
find name "ACT_MODE_CONTINUATION_PROMPT".

No caller passes options to rebuildSessionForMode anymore, so the block
is dead. Drop the block and narrow the signature to take only newMode.
Existing tests already invoke rebuildSessionForMode(<mode>) with no
second argument and assert that fireAndForgetSend is not called on a
mode rebuild, so they keep passing.
2026-06-05 10:03:12 -07:00
Mikołaj Kondratek d50bcb777c fix(terminal): capture standalone terminal output on Windows and harden PowerShell command handling (#11133)
* fix(terminal): surface standalone terminal spawn diagnostics

Add Logger calls at every chokepoint of the standalone terminal pipeline
so the (currently silent) failure modes around JetBrains-hosted
cline-core become debuggable from cline-core-service.log.

Lines added, all using the existing Logger facility (no new
dependencies, no behavioral changes):

* StandaloneTerminalProcess.run() now logs:
  - `[StandaloneTerminalProcess] run() entered: shell=… cwd=… args=…`
    on entry, before the try block;
  - `[StandaloneTerminalProcess] spawned pid=… for shell=…` right
    after child_process.spawn returns;
  - `[StandaloneTerminalProcess] close: code=… signal=… fullOutputLen=…`
    inside the `close` handler (the `fullOutputLen` reveals when the
    child exits 0 with empty pipes — the symptom in issue #10948);
  - `[StandaloneTerminalProcess] child error: …` in the `error`
    handler;
  - `[StandaloneTerminalProcess] spawn threw synchronously: …` in
    the outer catch.

* StandaloneTerminalManager.runCommand() now logs entry
  (`[StandaloneTerminalManager] runCommand terminalId=…: <cmd>`) and
  attaches a `.catch` to the previously fire-and-forget
  `process.run(…)` Promise so an unhandled rejection surfaces as
  `[StandaloneTerminalManager] process.run rejected for terminal …`
  instead of disappearing.

* CommandExecutor.execute() extends the existing "Executing command
  in … terminal" line with `mode=<terminalExecutionMode>` and
  `managerCtor=<manager.constructor.name>`, so it's possible to
  confirm whether the `vscodeTerminal` path is in fact backed by a
  `StandaloneTerminalManager` on JetBrains (it is — see
  notes/issue-10948-…md).

* CommandOrchestrator.orchestrateCommandExecution() logs the
  `process.once("completed")` event with `exitCode`/`signal`/
  `terminalType`, the "resolved completed" return branch with the
  line/byte totals, and emits a `WARN` on the silent "still running"
  fall-through. The last one matters because the original repro
  reported "Command executed successfully (exit code 0)" with empty
  output — the WARN makes that branch loud the next time it fires.

These logs are what made the two distinct bugs in #10948 visible
(see the 2026-05-28 update in
notes/issue-10948-terminal-output-investigation-2026-05-27.md). They
stay in to keep the next regression debuggable.

Refs: cline/cline#10948

* fix(terminal): keep Windows child stdio attached to parent pipes

The non-cmd Windows branch in StandaloneTerminalProcess.run() spawned
the shell (powershell.exe in practice) with `detached: true` and no
`windowsHide`. When cline-core is launched by the JetBrains plugin it
has no console of its own, so Windows CreateProcess allocates a NEW
console for the detached child and the child's stdio routes to that
new console instead of the pipe handles the parent created. From the
parent's point of view the pipes immediately EOF, `close` fires with
`code=0`, and `fullOutput` is 0 bytes — exactly the symptom reported
in cline/cline#10948 ("Command executed successfully (exit code 0)"
with no output and no filesystem effect).

This bug applies to every command the agent runs through the
standalone terminal path on Windows, not just the
double-wrapped-PowerShell cases (verified by re-running a clean
`dir <file>` after the diagnostics from the previous commit landed:
`run() entered` and `spawned pid=<num>` both fired, then `close: code=0
fullOutputLen=0`).

Fix:

* `detached: process.platform !== "win32"` — keep the existing
  POSIX behavior (a separate process group helps `tree-kill`), but
  drop it on Windows where `tree-kill` walks the PID tree with
  `taskkill /T` and doesn't need a process group.
* `windowsHide: true` — matches every other `child_process.spawn`
  call site in cline-core (git, MCP, hooks, browser) and flips on
  `CREATE_NO_WINDOW`, keeping the child attached to our pipes
  without popping a console window.

Verified on Windows 11 + IntelliJ IDEA 2026.1 + Cline plugin
1.1.59-Internal: `dir <path>`-style probes now produce a non-zero
`fullOutputLen` in the close log, and the captured output bytes
match what would have been visible interactively. PowerShell
double-wrapping (the other half of #10948) is handled in a
follow-up commit.

Refs: cline/cline#10948

* fix(terminal): harden PowerShell command wrapping for standalone shell

`StandaloneTerminalProcess.getShellArgs()` blindly wrapped every
PowerShell command as `["-Command", command]`. That has two
end-user-visible failure modes on Windows, both observed in
cline/cline#10948:

1. The agent's `run_commands` tool call sometimes arrives already
   prefixed with `powershell -Command "…"`. We then spawned
   `powershell.exe -Command 'powershell -Command "…"'`, and the
   outer shell shredded the inner single/double-quote pairs while
   re-parsing its `-Command` argument. The inner pwsh saw
   quote-empty `Test-Path` calls, fell through to the `else` branch
   and reported "File not found" — to ITS stdout, which the outer
   inherited but the file deletion the LLM intended never ran.
2. The user's `$PROFILE` script ran on every spawn, leaking
   non-deterministic noise (e.g.
   `%windir%\System32\REG.exe : The module '%windir%' could not be
   loaded`) into the captured output and confusing the agent.
3. Bonus: the POSIX branch used `["-l", "-c", command]`. The `-l`
   re-sources login files on every command, which is slow and lets
   greeter scripts leak into output.
4. Bonus: the cmd branch used `["/c", command]`. `/d` skips
   AutoRun, `/s` makes the embedded-quote handling deterministic.

Fix:

* PowerShell branch returns
  `["-NoProfile", "-NonInteractive", "-Command", unwrap(command)]`.
  `-NoProfile` suppresses (1) the spurious profile noise that
  contaminated the captured output, and `-NonInteractive` ensures
  the child doesn't deadlock waiting on a prompt no one will answer.
* `unwrapPowerShell(command)` strips a leading
  `powershell|pwsh [.exe] -Command|-c "…"` (or single-quoted)
  wrapper that the LLM sometimes emits, fixing the double-pass
  argument-quoting destruction. If the command does not match the
  exact wrapper shape it is returned verbatim — worst case is "no
  change", preserving pre-fix behavior.
* cmd branch returns `["/d", "/s", "/c", command]`, matching the
  canonical helper in cline/sdk/packages/shared/src/parse/shell.ts.
* POSIX branch returns `["-c", command]`, dropping the unhelpful
  `-l`. Also matches the SDK helper.

Verified on Windows 11 + IntelliJ IDEA 2026.1 + Cline plugin
1.1.59-Internal in combination with the previous "keep Windows
child stdio attached" commit: `Remove-Item CHANGELOG.md` now
deletes the file, the agent's verification `Get-ChildItem CHANGELOG*`
returns nothing, and the profile-load REG.exe error no longer leaks
into captured output.

Refs: cline/cline#10948

* refactor(terminal): tone down standalone terminal diagnostics

The diagnostics added while chasing #10948 were intentionally loud so the
two bugs were visible. Now that the fixes are in, reduce them to a normal
operating posture:

* Demote fine-grained traces to `debug`: the per-spawn `spawning …` and
  `spawned pid=…` lines, `StandaloneTerminalManager.runCommand`, and the
  orchestrator's `resolved completed` summary.
* Drop the orchestrator's `completed event` line entirely — the
  `resolved completed` debug line already carries exit code, signal, and
  line/byte totals.
* Stop echoing the full command in the manager line and stop echoing the
  args vector in the spawn line. The command is still logged once at
  `info` by CommandExecutor (unchanged, pre-existing), so we go back from
  three command echoes to one. Commands routinely embed secrets
  (Authorization headers, tokens), so fewer copies on disk is better.

Kept loud on purpose:

* `info` on `close: code=… fullOutputLen=…` — the single line that proves
  the Windows stdio-capture fix and the most useful per-command signal.
* `warn` on `resolved without completion event` — the silent-success
  canary for the #10948 failure mode.
* `error` on child error / synchronous spawn failure / unhandled
  process.run rejection.

Refs: cline/cline#10948

* fix(terminal): tighten PowerShell unwrap regex and extract to a pure module

Two review follow-ups for the #10948 shell-arg handling:

1. The wrapper-strip regex used a greedy `([\s\S]*)` body, so a command
   like `powershell -Command "foo" "bar"` would match with the body
   captured as `foo" "bar`, silently rewriting a command into something
   different. Replace the body with a tempered match `((?:(?!\1).)*)`
   that cannot contain the captured delimiter, so anything other than
   exactly one quoted token is returned verbatim. Worst case is now
   "no change" rather than an incorrect rewrite. The legitimate
   double-wrapped case from #10948 (outer ", inner ') still unwraps.

2. `getShellArgs` and `unwrapPowerShell` were private methods on
   StandaloneTerminalProcess, untestable without spawning a process.
   Move them to a pure `shellArgs.ts` module. `getShellArgs` now takes
   an injectable `platform` (defaulting to `process.platform`) purely so
   the win32-vs-posix branch is testable; behavior is unchanged. This
   also gives us a single local seam to later consolidate onto the
   canonical `@cline/shared` helper (tracked as a follow-up).

No behavioral change beyond the regex correctness fix.

Refs: cline/cline#10948

* test(terminal): cover shell-arg construction and PowerShell unwrap

Add mocha unit tests (matching the repo's node:assert/strict + __tests__/
convention so the existing mocharc spec globs pick them up) for the newly
extracted shellArgs module:

* unwrapPowerShell: double-quote and single-quote wrappers, powershell.exe
  -c form, the #10948 nested-quote repro (inner quotes preserved),
  non-wrapped passthrough, and the two regressions the tightened regex
  must reject (`… "foo" "bar"` and a command that merely mentions
  powershell mid-string).
* getShellArgs: PowerShell -> -NoProfile -NonInteractive -Command (with
  unwrap), cmd -> /d /s /c, POSIX -> -c. The injectable platform arg lets
  these run on any CI host.

This closes the M1 review finding (the regex was the riskiest line in the
change and had zero coverage) and exercises the cmd/POSIX flag changes
called out in M2.

Refs: cline/cline#10948

* docs(terminal): drop issue references and clarify windowsHide comment

Remove inline issue-number references from source comments and a test
name; that context belongs in the commit history, not the code. Also add
a one-line note that windowsHide is a no-op on non-Windows platforms,
since it is set unconditionally while the surrounding comment is
Windows-specific.

No behavior change.

* refactor(terminal): drop warn on the non-completion return path

The orchestrator's final fall-through return is a normal, expected path:
the process resolved via `continue` without a `completed` event (e.g. a
terminal mode without shell integration, or proceed-while-running flows).
Logging it at `warn` cries wolf on healthy runs, so remove it. The
genuine failure mode this was meant to catch surfaces through the
`close`/error logs and the result string itself.

* fix(terminal): address review feedback on standalone spawn paths

Three follow-ups from code review:

* StandaloneTerminalManager.runCommand: the unawaited process.run()
  .catch only logged. run() emits "error" for failures it catches, but a
  rejection escaping without an "error" event would leave the outer
  promise (resolved via the "continue"/"error" events) pending forever,
  stalling the caller. Re-emit "error" from the catch so both paths stay
  consistent. Cannot trigger today (no await outside run()'s try/catch)
  but the guard exists precisely for future rejections.

* shellArgs POSIX branch: document that dropping the login flag (`-l`)
  is intentional and relies on the child inheriting the parent's PATH via
  process.env, with a note that a GUI-launched IDE without a login PATH
  is the edge case to watch.

* StandaloneTerminalProcess cmd.exe branch: add windowsHide:true. The
  console-allocation/window-pop problem is not exclusive to the non-cmd
  branch; a console-less parent could pop a window for cmd.exe too.
  No-op on non-Windows.
2026-06-05 10:03:11 -07:00
Ara af6778dd37 Remove Explain Changes feature (#11278)
* chore(vscode): remove explain changes entry points

* chore(vscode): remove explain changes feature
2026-06-05 10:03:11 -07:00
Max Paulus 🥪 ea68ad29c6 make extension plan mode more similar to CLI
- basically, don't auto continue when agent switches to act mode
2026-06-05 10:03:11 -07:00
Max Paulus 🥪 f96fa41870 fix zai insufficient credits issue 2026-06-05 10:03:11 -07:00
Max Paulus 🥪 3b34f6fdea fix tool use name sanitization 2026-06-05 10:03:11 -07:00
Max Paulus 🥪 ec56a140fb fix broken tsc 2026-06-05 10:03:10 -07:00
Dominic Cooney c955075db1 fix(vscode): exclude vitest src/sdk suites from CommonJS test compile
compile-tests runs 'tsc -p tsconfig.test.json' (module: commonjs) over all
src/**/*.test.ts for the VS Code integration runner. The new src/sdk vitest
suites use top-level 'await import(...)' (after vi.mock), which is invalid
under CommonJS and fails with TS1378. The integration runner never runs
src/sdk anyway (.vscode-test.mjs only globs core/test/utils/shared/
integrations/hosts/services); these run via 'npm run test:vitest'. Exclude
src/sdk/**/*.test.ts from the integration compile.
2026-06-05 10:01:21 -07:00
Dominic Cooney 701c29589b fix(vscode): restore biome --config-path so lint resolves apps/vscode/biome.jsonc
The rebase dropped '--config-path ./biome.jsonc' from the lint/format/
postprotos scripts and removed the '!!**/.vscode-test' ignore from
biome.jsonc. Without the explicit config path, biome auto-discovered the
root biome.json instead of apps/vscode/biome.jsonc, applying the wrong
rule severities (449 errors at error level for rules that are off/info in
the nested config). Restore both to match origin/main and apply the
pending buf format fix to models.proto.
2026-06-05 10:01:20 -07:00
Dominic Cooney 2135b07ed0 fix(vscode): show running state for in-progress commands
The command row reflects an executing state while a command runs. The
message translator includes the command-output marker on the running
command row so the webview renders it as executing; the row is finalized
with output and a completed flag when the command ends.

Also remove the unused onChange parameter from the foreground run_commands
path: the SDK runtime does not pass it, so it had no effect. Foreground
command output is surfaced to the chat at completion, not incrementally.

Fixes CLINE-2298 and CLINE-2162
2026-06-05 10:00:15 -07:00
Dominic Cooney 13de185d07 fix(vscode): re-enable approval buttons for consecutive asks
The footer Approve/Reject buttons stayed disabled when a second consecutive
approval ask arrived. The button configs are shared singletons (e.g.
BUTTON_CONFIGS.tool_approve), so two identical asks return the same object
reference and the effect that reset the processing latch never re-ran.

Key the processing latch on the ask identity (anchored turn timestamp plus the
button labels) rather than the config object reference, using a ref-based latch
so each new ask re-enables the buttons. Adds a regression test.

Test plan:

1. Ask the agent to generate two requests to ls /tmp at once

2. Approve (or reject) the first request

3. Check that the buttons for the second request are enabled
2026-06-05 10:00:15 -07:00
Dominic Cooney 82c9492e0c feat(vscode): add the VS Code Language Model (vscode-lm) provider
Run Cline inference through the VS Code Language Model API (vscode.lm), enabling
models contributed by any extension that registers a language model chat
provider with VS Code. GitHub Copilot is the most common such vendor, but the
implementation is vendor-agnostic — it selects models via
vscode.lm.selectChatModels and has no Copilot-specific logic.

- VsCodeLmHandler implements the Cline SDK ApiHandler and is registered with the
  SDK handler registry; the model selector travels as a vendor/family[/version/id]
  string in modelId and is parsed back here. Selector segments are
  percent-encoded so values containing slashes round-trip intact.
- Native tool calling: tool definitions are passed to sendRequest and tool calls
  are surfaced as tool-call chunks; tool results round-trip as
  LanguageModelToolResultPart, with structured tool output serialized to text and
  a trailing user message appended when a turn ends on tool results so models can
  read the output.
- Gated to VS Code: registration is conditioned on the vscode.lm API being
  present, and the provider is hidden in the UI on hosts without it (JetBrains).

Depends on @cline/{shared,llms,agents,core} 0.0.42-nightly.1780514867, the first
published SDK build with the custom-registered-handler routing this provider
needs.
2026-06-05 10:00:15 -07:00
Ara 0cb9893732 Fix approval chat replies rendering as tool errors (#11246)
* fix(vscode): route approval chat replies as user feedback

* fix(vscode): suppress approval reply denial errors

* fix(vscode): hide rejected approval tool failures

* chore(vscode): clarify denied approval suppression helper
2026-06-05 10:00:14 -07:00
Max Paulus 🥪 2b6a4a3def Persist OpenRouter provider config via catalog hook 2026-06-05 10:00:14 -07:00
Max Paulus 🥪 a0d6665d58 persist openai-codex provider model settings
- also don't show a sign in button if openai-codex is the only provider
2026-06-05 10:00:14 -07:00
Max Paulus 🥪 c3799f2db3 Persist Cline model selections to provider config 2026-06-05 10:00:14 -07:00
Dominic Cooney b74466fd64 fix(vscode): declare missing direct deps @grpc/proto-loader and @opentelemetry/api-logs
Both packages are imported directly from source but were never declared in
apps/vscode/package.json, so they only resolved transitively. On a clean
install this broke:

- @grpc/proto-loader — imported by scripts/proto-utils.mjs,
  src/standalone/utils.ts and src/standalone/hostbridge-client.ts; its absence
  made `npm run protos` (and therefore the whole build) fail on a fresh checkout.
- @opentelemetry/api-logs — imported by the OpenTelemetry telemetry providers;
  its absence produced TS2307 "Cannot find module" errors under tsc.

Versions are pinned to align with the existing dependency families already
declared in this package (@grpc/grpc-js ^1.9.x → proto-loader ^0.7.13;
the @opentelemetry/* 0.56.x line → api-logs ^0.56.0). The npm and bun
lockfiles are updated accordingly (the api-logs change also dedupes several
previously-nested copies to a single hoisted entry).
2026-06-05 10:00:14 -07:00
Max Paulus 🥪 e1674d0724 show legacy task history that is not saved in the ~/.cline folder 2026-06-05 09:58:56 -07:00
Max Paulus 🥪 11e0ea9bd4 add migration telemetry 2026-06-05 09:58:56 -07:00
Ara 81022b4dc9 fix(vscode): reuse approved tool rows (#11213)
* fix(vscode): reuse approved tool rows

* fix(vscode): clear stale approved tool rows
2026-06-05 09:58:56 -07:00
Ara 77e1ce3ead fix(llms): strip Cerebras reasoning history (#11214) 2026-06-05 09:58:55 -07:00
Max Paulus 🥪 abc710aa12 fix unauthed user flow
- show a small sign in button if user is unauthed with any provider
2026-06-05 09:58:55 -07:00
Robin Newhouse 7544131666 fix(vscode): compact Codex OAuth before input cap (#11194)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 09:58:55 -07:00
Robin Newhouse 5112170461 fix(vscode): wire auto compact into SDK sessions (#11197)
* fix(vscode): wire auto compact into SDK sessions

* test(sdk): cover both directions of useAutoCondense task override

The previous test left the global mock at `true` for both calls, so the
`taskSettings: true` branch would have passed even if task settings were
ignored entirely. Make the mock read a mutable flag and flip it to `false`
before the second call so both override directions — task `false` over
global `true`, and task `true` over global `false` — are genuinely
exercised.

* Fix mock return type in cline-session-factory test

The getGlobalSettingsKey mock inferred a literal 'false | undefined' return type, so later mockImplementation overrides returning 'true' failed type checking (TS2345). Annotate the implementation as 'boolean | undefined' to widen the inferred mock signature.

---------

Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
2026-06-05 09:58:55 -07:00
Dominic Cooney 080ba5d036 fix(vscode): keep in-progress MCP OAuth flow across reconnects
The MCP SDK calls redirectToAuthorization() on every connection attempt,
and a single server can be reconnected repeatedly (settings watcher,
reconnect handler, restart). Regenerating the OAuth `state` on each call
replaced the state stored for a flow whose authorization URL the user may
already have open, so the completed callback failed validation with
"Invalid OAuth state".

redirectToAuthorization() now keeps an in-progress, still-fresh flow
instead of starting a new one (freshness measured from when the flow
started, never extended, so a stale flow always expires). The PKCE
verifier is pinned to the kept flow so token exchange still validates.

Also add local dev/test tooling:
- src/dev/mcp-oauth-test-server: a zero-dependency OAuth AS + MCP
  StreamableHTTP server for exercising the flow locally, with
  fault-injection flags (--auto-deny, --slow-authorize, --code-ttl).
- src/extension.ts: a debug-only globalThis.__clineHandleUri hook (gated
  on CLINE_CAPTURE_BROWSER) so the debug harness can deliver simulated
  vscode:// OAuth callbacks; documented in the harness README.
2026-06-05 09:58:55 -07:00
Dominic Cooney 8a35154a0b fix(vscode): forward Bedrock region + AWS auth to the SDK gateway
Bedrock requests built by the SDK adapter dropped the AWS region and
authentication mode, so a pasted Bedrock API key (awsBedrockApiKey +
awsAuthentication "apikey") was silently ignored and requests fell
through to the SigV4 credential chain with no region.

Two bugs, both verified end-to-end against a live Bedrock endpoint via
the debug harness:

1. The SDK ProviderConfig was built with only providerId/modelId/apiKey/
   baseUrl. New bedrock-config.ts maps the legacy ApiConfiguration onto
   the SDK's structured region + aws block (including the webview's
   "credentials" radio -> SDK "iam"), wired into both inference paths
   (buildSdkProviderConfig for utility calls, buildSessionConfig for the
   main task loop).

2. The main chat path's gateway config is built by core from the
   providers.json `stored` entry, which the session's providerConfig does
   not override. A stale Bedrock entry (e.g. a legacy migration with
   region us-east-1 + SigV4 keys) silently won, sending requests to the
   wrong region (403). buildSessionConfig now persists the
   StateManager-derived Bedrock settings to providers.json so `stored` is
   authoritative. The bearer apiKey is only persisted for api-key auth to
   keep stored clean for profile/iam.

Also documents the ELECTRON_RUN_AS_NODE debug-harness gotcha in
.clinerules/general.md.
2026-06-05 09:58:55 -07:00
Max Paulus 🥪 904429ddce fix xai provider
- xai provider settings now properly updates providers.json
2026-06-05 09:58:54 -07:00
Max Paulus 🥪 f021fb5891 updat gitignore 2026-06-05 09:58:54 -07:00
Max Paulus 🥪 700038f62c sdk migration: squashed 06-05-2026 -- instead of listHistory, use host.get(sessionId) instead
fix sesion usubscriptions

fix soft-lock on auth fail retry

refactor(vscode): source provider model catalogs from @cline/llms SDK
(ENG-2116)

Migrates the vscode extension off its hand-curated static model catalogs
in apps/vscode/src/shared/api.ts and on to the @cline/llms SDK as the
single source of truth for provider/model metadata, end-to-end across
the extension host and the webview.

Net impact on the static catalog file:
  apps/vscode/src/shared/api.ts: 5092 -> 468 lines (~90% gone).

What changed at each layer
--------------------------

SDK / catalog plumbing (apps/vscode/src/sdk/model-catalog/):
  - New `ProviderCatalog.peekModels(providerId)` synchronous cache read.
  - `resolveModelInfo` rewritten: committed selection -> catalog peek
    -> await catalog.resolveModels on cache miss. No race with a
    background warmer; if the catalog truly has nothing, returns
    source: "unknown" and the webview renders a neutral loading state.
  - `applyHostModelInfoOverrides` is the canonical seam for the few
    fields the SDK does not yet carry. Today it carries only the Vertex
    `supportsGlobalEndpoint` allowlist (vertex-global-endpoint.ts, with
    a TODO to upstream into the SDK).
  - `ProviderListing` extended with SDK metadata (`is_popular`,
    `popular_rank`, `usage_cost_display`, `capabilities[]`) and plumbed
    through proto + conversion.

Extension-host handlers (apps/vscode/src/core/api/providers/):
  - New shared helper `apps/vscode/src/shared/sdk-handler-models.ts`:
    `getProviderModelFromSdk(providerId, requestedModelId,
committedInfo?)`
    returns `{ id, info }` from `getProviderCollectionSync` with
    Vertex global-endpoint overrides applied.
  - 27 handlers converted to a one-liner `getModel()` through that
    helper. Per-handler nuances preserved:
      * Anthropic: strips `:fast` and `:1m` host-side suffixes before
        SDK lookup; carries them back on the returned id so the
        per-request betas still flip.
      * Bedrock: keeps the custom Application Inference Profile ARN
        branch; base-model info from the SDK.
      * Cerebras: keeps the `qwen-3-coder-480b-free` ->
`qwen-3-coder-480b`
        paid alias.
      * Qwen / ZAi: SDK has a single catalog each; handlers keep the
        regional base-URL switch but no longer fork the catalog.
      * Wandb: keeps the "unknown id falls through to safe defaults"
        escape hatch via `MODEL_COLLECTIONS_BY_PROVIDER_ID`.

Refresh-models background tasks:
  - `refreshBasetenModels`, `refreshGroqModels`,
`refreshHuggingFaceModels`
    source their offline-fallback catalog from the SDK via
    `getProviderCollectionSync` + `adaptSdkModelInfo`. Live fetch path
    unchanged; only the seeding/fallback data changed.

Webview (apps/vscode/webview-ui/):
  - `useNormalizedApiConfiguration` always routes through gRPC
    `resolveModelInfo`. Removed the `isMigratedSdkProvider` /
    `MIGRATED_SDK_PROVIDER_IDS` feature flag and the legacy
    `normalizeApiConfiguration` switch entirely.
  - New `useStaticProviderSelection` hook for the 22 settings
    components whose catalog is now SDK-driven, and
    `useDynamicProviderSelection` for the 12 dynamic-list pickers
    (openrouter, cline, openai-compatible, ollama, lmstudio, requesty,
    litellm, hicap, groq, baseten, huggingface, vercel-ai-gateway,
    aihubmix, oca, huawei-cloud-maas, dify, fireworks, together,
    vscode-lm) so all of them stop calling the legacy switch.
  - `ModelInfoView` reads its `isGemini` check via
    `useProviderModels("gemini")` instead of importing `geminiModels`.
  - `App.stories.tsx` ships a small inline fixture instead of
    importing `bedrockModels`.
  - `ExtensionStateContext` no longer seeds `groqModelsState` /
    `basetenModelsState` from the deleted catalog; the slices start
    empty and the SDK-curated catalog is layered in by the pickers at
    render time.

Misc:
  - `src/utils/model-utils.ts`: `isAnthropicModelId` consults
    `MODEL_COLLECTIONS_BY_PROVIDER_ID["anthropic"]` instead of the
    deleted `anthropicModels` map.
  - `src/shared/storage/provider-keys.ts`: `getProviderDefaultModelId`
    no longer hard-codes 24 per-provider defaults. The function now
    consults the SDK catalog and only keeps an override map for
    providers whose default is intentionally not the SDK default
    (openrouter-shared dynamic providers and local-only providers).
  - `src/shared/openai-codex-models.ts`: relative path for
    `shape-adapter` import so both the extension and webview build
    contexts resolve it identically.

Tests retargeted to assert SDK behavior, not static-map shapes
--------------------------------------------------------------
  - claude-code, anthropic, bedrock, vertex, wandb, provider-keys
    test suites had assertions tied to deleted shapes. Rewrote them
    to either assert through the SDK catalog
    (anthropic compares against
`adaptSdkModelInfo(sdkCollection.models[id])`,
    wandb uses the SDK-declared default, etc.) or focus on the
    host-side semantics (bedrock's "global endpoint" block now tests
    `vertexModelSupportsGlobalEndpoint` directly).
  - claude-code test trimmed its 8 `[1m]`/version-pin variants down
    to three SDK-shaped cases. The 8 deleted assertions exercised
    extension-only model-id derivations that the SDK does not carry;
    matching the CLI's behavior was the explicit goal.
  - resolveModelInfo test rewritten around the new peek -> await
    -> unknown contract.
  - proto-lint: added missing `go_package` option to
    `proto/cline/remote_config.proto`.

Verification (npm scripts under apps/vscode/):
  - npm run protos          OK
  - npm run check-types     OK  (apps/vscode + apps/vscode/webview-ui)
  - npm run lint            OK  (biome + proto-lint)
  - npm run build:webview   OK  (tsc -b && vite build)
  - node esbuild.mjs        OK  (dist/extension.js produced)
  - Runtime smoke test: 27/27 provider collections resolve from the
    SDK with correct model counts, defaults, and usage-cost-display
    flags. openai-codex returns cost=hide as expected; every other
    provider returns cost=show.

fix(vscode): preserve provider model selection fields

chore(vscode): clean up SDK migration branch (comments, dead code,
scree, tests)

- Remove porting scree: docs/sdk-model-catalog/* planning docs,
TODO-resume-session.md, and stale doc references in code/comments.

- Rewrite before/after narrative comments in the 'eternal now' style;
drop transient 'Step N'/'Phase N' labels.

- Remove leftover [HistoryPerf] diagnostic logging and a dead try/catch
rethrow; minor readability/naming.

- Restore .clinerules/network.md (still relevant) and update
sdk-migration.md to drop dangling references.

- Fix latent circular-init TDZ in openai-codex-models (lazy catalog
build).

- toggleRemoteConfigSetting no longer returns a never-resolving promise.

- Move vitest config into apps/vscode and rename script test:sdk ->
test:vitest; wire it into CI.

- Repair/reimplement the SDK-adapter vitest suites (auth-service,
provider-migration, sdk-task-history) to match current behavior; all 432
tests pass.

fix(vscode): support OpenAI Compatible provider on the SDK adapter

Selecting the OpenAI Compatible provider failed with 'Unknown provider
"openai"', and manually entered model ids were displayed as the catalog
default (gpt-4o).

- Map the extension's 'openai' provider id to the SDK's
'openai-compatible' built-in at the SDK boundary (toSdkProviderId), and
convert before handing the provider id to core when building a session
config.
- Treat openai-compatible as a custom-model-id provider so model
resolution honors a user-entered model id instead of coercing it to the
catalog default. Adds providerAllowsCustomModelIds() as the shared
signal.
- Bump @cline/core, @cline/llms, @cline/shared, @cline/agents to ^0.0.42
(which registers the openai-compatible built-in) and dedupe the
dependency tree.
- Carry the tool name on reconstructed Anthropic-format tool_result
blocks to satisfy the SDK's ToolResultContent contract.
- Update tests for the refreshed SDK model catalog (Gemini default).

fix(vscode): fix webview provider-model tests and a Windows path test

The nightly publish runs the full webview-ui suite and the Windows
extension suite, which surfaced failures the extension-only run does
not.

- Harden useProviderModels against a missing providerModelsByProvider
map so a partially-mocked ExtensionStateContext no longer crashes the
hook.
- Update the SapAiCoreModelPicker and APIOptions specs to provide the
provider model-list context the components now read from the SDK
catalog, and seed the model ids each test asserts. Removes the now-dead
@shared/api sapAiCoreModels mock.
- Compare resolveDataDir() against path.join() instead of a hardcoded
POSIX path so the CLINE_DIR fallback test passes on Windows.

fix(vscode): load real @cline/llms in unit-test harness; fix/trim
provider tests

The mocha unit-test harness stubbed @cline/llms with an empty catalog,
so the SDK-migrated provider handlers (which read the real catalog)
failed ~51 tests under the nightly suite.

- src/test/requires.ts now loads the real @cline/llms by resolving its
package directory and requiring the ESM entry by absolute path (Node 22
require(esm)), bypassing the package's import-only exports map. This
restores real catalog data to provider unit tests.
- vertexModelSupportsGlobalEndpoint: also match legacy ':'
context-window/speed suffixes (e.g. claude-opus-4-7:1m), not just '@'
snapshot variants.
- refreshClineModels: derive prompt-cache support from reported
input_cache_read pricing for any provider, not just openai/google
prefixes.
- Delete the failing tests for the classic provider handlers (bedrock
cross-region/native-tool-calling, gemini metadata, cline/openrouter qwen
cache, wandb unknown-model). These handlers are only reached via
buildApiHandler (explain-changes + commit-message generation) and the
provider/catalog domain is owned and tested by the SDK; they will be
removed when buildApiHandler is retired.

refactor: remove legacy Cline model overrides, wire reasoning effort
through SDK

- Delete refreshClineModels.ts, refreshClineModelsRpc.ts, and test
- Remove refreshClineModelsRpc proto RPC
- Remove clineModels state from ExtensionStateContext, StateManager
cache, disk
- ClineModelPicker and useOnboardingModels use SDK catalog directly
- Replace ThinkingBudgetSlider with ReasoningEffortSelector for Cline
provider
- Use supportsReasoning from SDK catalog instead of hardcoded model
names
- Add ProviderReasoningPatch to proto and ProviderConfigPatch contract
- Wire reasoning effort changes through writeProviderConfig to SDK
ProviderSettingsManager
- Remove hardcoded Claude switch in openrouter-stream.ts
- Add supportsReasoning to supportsReasoningEffort check in
openrouter-stream

refactor(vscode): route buildApiHandler through the SDK; remove
OpenRouter handler

Replace the legacy per-provider buildApiHandler factory with an
SDK-backed
handler (apps/vscode/src/sdk/sdk-api-handler.ts) built via @cline/llms
createHandler(). The two standalone callers (commit-message generation
and
explain-changes) now import buildApiHandler directly from the SDK
module;
@core/api stays types-only (re-exporting SDK types) so it can keep being
imported widely without pulling the SDK runtime graph into activation.

Also:
- Delete the now-dead OpenRouterHandler and its test
(createOpenRouterStream
  stays; it is still used by ClineHandler).
- buildSdkProviderConfig never sends both reasoning.effort and
  reasoning.max_tokens (some providers reject it), and supports
  disableReasoning for fast one-shot utility calls; commit-message and
  explain-changes opt in.
- commit-message generation surfaces the real SDK stream error instead
of a
  generic "empty API response".
- getGitDiff: run git diff --staged even before the first commit; only
gate
  the git diff HEAD fallback on having commits (fixes "no changes" for
the
  initial commit of a new repo).

refactor(vscode): delete legacy API provider handlers and dead
transforms

Now that buildApiHandler routes through the @cline/llms SDK, the legacy
per-provider handler classes and their supporting code are unused.
Remove them:

- apps/vscode/src/core/api/providers/** (all 40+ handler classes, types,
tests)
- apps/vscode/src/core/api/transform/** except stream.ts (format/stream/
  tool-call helpers only the handlers used)
- apps/vscode/src/core/api/utils/** (messages/responses API support)
- apps/vscode/src/core/api/retry.ts (+ test)
- apps/vscode/src/shared/sdk-handler-models.ts (getProviderModelFromSdk;
  only the deleted handlers' getModel() used it)

core/api now contains just index.ts (types + SDK re-exports),
transform/stream.ts
(ApiStream types still referenced by the local ApiHandler interface),
and
adapters/.

Supporting changes to keep everything compiling/working:
- context-window-utils: drop the dead `api instanceof OpenAiHandler`
DeepSeek
  branch (handlers are SDK GatewayApiHandlers now); the 64k switch case
already
  handles DeepSeek context sizing.
- Relocate fetchLiteLlmModelsInfo from the deleted litellm handler into
  core/controller/models/fetchLiteLlmModels.ts (used by
refreshLiteLlmModels).
- Move the `declare module "vscode"` Language Model API augmentation
(previously
  carried by the vscode-lm handler) into
src/types/vscode-language-model.d.ts so
  live consumers (getVsCodeLmModels, vsCodeSelectorUtils) keep their
types.
- Move the @google/genai test mock out of the deleted providers dir to
  src/test/fixtures/google-genai-mock.ts and repoint test-setup.js.

refactor(vscode): delete dead classic system-prompt + slash-command code

The classic system-prompt builder (core/prompts/system-prompt/**), the
deep-planning prompt variants, core/prompts/commands.ts, and the
core/slash-commands handler are no longer reachable from any shipped
entry
point (extension host, standalone host, or webview) after the SDK
migration:
the SDK provides prompt construction (buildClineSystemPrompt) and
slash-command
handling. Removed them and their orphaned tests.

Kept core/prompts/responses.ts (still live) and its tests.

Dead-code reachability was computed with scripts/find-dead-src.mjs
(esbuild
metafile reachability from src/extension.ts +
src/standalone/cline-core.ts,
plus webview src/shared references), then gated on tsc --noEmit +
vitest.

refactor(vscode): delete dead tree-sitter code-definition service

src/services/tree-sitter/** (language parsers + queries for the classic
list_code_definition_names path) is unreachable from any shipped entry
point
after the SDK migration. Verified via scripts/find-dead-src.mjs (esbuild
reachability from extension + standalone entries, plus webview shared
refs) and
an importer cross-check (no live value or type importers), gated on
tsc --noEmit + vitest (436 passing).

refactor(vscode): delete more dead classic code (hooks, permissions,
claude-code, misc)

Removes additional source files unreachable from any shipped entry point
(extension host, standalone host, webview) after the SDK migration,
verified
via esbuild reachability + a per-file importer cross-check that excludes
any
file still referenced by a live non-test survivor or generated/test
glue, then
gated on tsc --noEmit + vitest (436 passing):

- services/ripgrep, integrations/notifications, utils/string,
utils/tabFiltering
- core/assistant-message/parse-assistant-message
- core/hooks: hook-model-context, notification-hook,
precompact-executor,
  PreToolUseHookCancellationError
- core/permissions/CommandPermissionController
- core/workspace/detection
- integrations/claude-code: run, message-filter
- integrations/editor: FileEditProvider, detect-omission
- integrations/misc: extract-file-content, extract-images

Dropped the now-dead re-exports from core/assistant-message/index.ts and
core/permissions/index.ts (those barrels stay; they still export live
types
used by generated host glue).

chore(vscode): add scripts/find-dead-src.mjs dead-code oracle

Computes src files unreachable from the shipped entry points (extension
host + standalone host used by JetBrains/CLI) plus webview shared refs,
using esbuild metafile reachability. Used to drive the
post-SDK-migration dead-code deletions; keep for future pruning. Note:
results still need a tsc-gated importer cross-check because esbuild
drops import-type-only edges.

fix(vscode): drop dead barrel re-exports of deleted files

The previous deletion commit (2c9bd62) removed
core/assistant-message/parse-assistant-message.ts and
core/permissions/CommandPermissionController.ts, but the corresponding
barrel
edits (removing their re-exports from index.ts) were dropped by
lint-staged's
stash and never committed, leaving HEAD referencing deleted modules.
Remove the
dead re-exports so the barrels only export live members.

chore(vscode): drop npm deps only used by deleted provider handlers

Remove dependencies that became unused after the legacy API provider
handlers
and tree-sitter service were deleted (no remaining imports in src,
webview, or
build config):

  @anthropic-ai/vertex-sdk, @aws-sdk/client-bedrock-runtime,
  @aws-sdk/credential-providers, @azure/identity,
  @cerebras/cerebras_cloud_sdk, @google-cloud/vertexai,
@mistralai/mistralai,
  @sap-ai-sdk/ai-api, @sap-ai-sdk/orchestration,
@sap-cloud-sdk/connectivity,
  ollama, tree-sitter-wasms, web-tree-sitter

Also remove the now-dead copyWasmFiles esbuild plugin (it only copied
tree-sitter WASM files for the deleted code-definition service). Kept
openai,
@anthropic-ai/sdk, @google/genai, and aws4fetch — still imported by live
code.

Verified: extension + standalone esbuild builds succeed, tsc --noEmit
clean,
vitest 436 passing.

refactor(vscode): remove dead cost utils and createOpenAIClient

- Delete src/utils/cost.ts (+ test):
calculateApiCostAnthropic/OpenAI/Qwen had
  no consumers left after the provider handlers were removed (only the
test
  referenced them).
- Remove createOpenAIClient from src/shared/net.ts (no remaining
callers) and
  its now-unused openai + EnvUtils imports. The proxy-aware fetch and
  getAxiosSettings exports remain.

tsc --noEmit clean, vitest 436 passing.

refactor(vscode): delete genuinely-unused telemetry/auth helpers

- services/telemetry/events/EventHandlerBase.ts: abstract base for
telemetry
  event handlers whose concrete subclasses were removed with the classic
task
  code; no remaining references (it was the only file left in events/).
- services/auth/AuthServiceMock.ts: test mock with no importers after
the
  associated test was deleted.

Note: the rest of services/* (TelemetryService, McpHub,
FeatureFlagsService,
ErrorService and their IFoo interfaces) IS live — the interfaces only
looked
unreachable to the esbuild oracle because they're consumed via type-only
imports (erased at emit). Verified via importer cross-check; tsc +
vitest green.

refactor(vscode): remove dead sapAiCoreModelDescription const

Unused leftover from the deleted SAP AI Core provider handler (found via
biome noUnusedVariables). tsc + vitest green.

refactor(vscode): delegate Cline recommended-models fetch to the SDK;
TODO others

refreshClineRecommendedModels now delegates the HTTP fetch + response
normalization + offline fallback to the SDK's
fetchClineRecommendedModels
(@cline/core), removing ~80 lines of duplicated logic. The extension
wrapper
keeps its distinct behavior: the CLINE_RECOMMENDED_MODELS_UPSTREAM
feature-flag
gate, the in-memory TTL cache, and in-flight dedup. The proxy-aware
fetch
(@/shared/net) and the configured apiBaseUrl are passed through to
preserve
network/proxy behavior. The SDK's offline fallback list is identical to
CLINE_RECOMMENDED_MODELS_FALLBACK, so offline behavior is unchanged.

Because the module now imports the ESM-only @cline/core, its unit test
moves
from mocha to vitest (joining the other SDK-touching models tests):
added to
vitest include + mocha ignore, and fetchClineRecommendedModels added to
the
vitest @cline/core stub (and the mocha/integration @cline/core mocks for
completeness). Rewrote the test vitest-native, asserting flag-gate,
delegation,
and flag re-check.

Also:
- Add TODO(sdk-consolidation) notes to the remaining bespoke
live-model-refresh
  handlers (Groq, Baseten, Hicap, HuggingFace, Vercel AI Gateway,
OpenRouter)
  documenting the path to share them with the CLI via the SDK (register
  modelsSourceUrl) and then delete the extension-only handlers + RPCs.
These
  are NOT migrated yet because the SDK does not currently live-fetch
those
  providers (only ollama/lmstudio register modelsSourceUrl), so deleting
them
  today would regress to the curated catalog.
- Remove an unused local type (RuleLoadPart) found while reviewing biome
  noUnusedVariables output.

tsc --noEmit clean; vitest 439 passing.

docs(vscode): refine sdk-consolidation TODOs for live model fetching

After inspecting the SDK's generic models-URL fetcher
(sdk/packages/core/src/services/providers/model-source.ts
`fetchModelIdsFromSource` + `resolveModelsSourceUrl`), update the TODOs
on the
bespoke refresh*Models handlers to capture the real constraint
discovered:

- The SDK fetcher exists and is provider-agnostic, but returns model
*ids only*
  (unknown ids get placeholder ModelInfo with no real
pricing/capabilities).
- `mergeKnownModels` treats a registered `modelsSourceUrl` as the
authoritative
  "installed" list (Ollama/LM Studio semantics) and DISCARDS the curated
catalog
  when the live fetch returns results.

So simply registering `modelsSourceUrl` for
Groq/Baseten/Hicap/HuggingFace/
Vercel/OpenRouter would regress rich model metadata. Proper
consolidation needs
an SDK enhancement first (merge-mode or richer per-provider parsing),
then the
extension handlers + RPCs can be deleted. refreshGroqModels.ts carries
the
detailed note; the others reference it.

refactor(vscode): unify ClineMessage id minting behind one
MessageIdMinter

Message ids (ClineMessage.ts) were minted from Date.now() in two
independent
generators: the live message translator (pure ++counter seeded once) and
the
interaction coordinator (Math.max(Date.now(), last+1)). Because the
translator
counter drifts behind wall-clock, it can later catch up to a clock-based
id
minted by the interaction coordinator, producing colliding ids for
different
messages. That breaks any merge-by-id scheme on the webview side.

Introduce a single process-wide MessageIdMinter (pure monotonic
id/seq/epoch
counters, never reads the clock) owned by MessageTranslatorState and
shared by:
- live SDK event translation,
- the interaction coordinator (tool approval / ask_question /
user_feedback),
- history rendering (sdkMessagesToClineMessages).

This makes every id globally unique within the process, so regenerated
history
ids never overlap live-session ids. Behavior is otherwise unchanged.

Also adds the message-state pipeline design + investigation docs under
src/sdk/docs.

S1 of the message-state redesign; seq/epoch stamping and the remaining
one-off
Date.now() mint sites follow in S2.

refactor(vscode): stamp seq/epoch on messages and state; fire-and-forget
delivery

S2 of the message-state redesign. Lays the groundwork for the
convergent-replica
webview reducer (S3) so the webview can never get stuck on
stale/out-of-order
delivery.

Stamping (extension-owned, synchronous, from the single
MessageIdMinter):
- Every ClineMessage flowing to the webview is stamped in
SdkMessageCoordinator with
  a fresh `seq` (freshness) and the current `epoch`
(conversation/replica fence),
  before it is stored or emitted. The same object references go to both
the message
  state handler and the partial-message stream, so both channels carry
identical
  stamps. An updated message (partial -> final, same ts) passes through
again and
  gets a higher seq, so the freshest copy always wins regardless of
arrival order.
- Every state snapshot is stamped in
SdkController.getStateToPostToWebview with a
  fresh `stateVersion` (sampled from the same counter) and the current
`epoch`.
- `epoch` is bumped at every conversation boundary via a new
  resetMessageTranslatorAndFence() wired into the existing
resetMessageTranslator
  sites (task start/clear, history open, reinit, mode rebuild,
new-session
  follow-up). iteration_start streaming resets do NOT bump it.

Transport:
- ClineMessage proto gains seq/epoch (fields 24/25) and the conversions
carry them.
- ExtensionState gains stateVersion/epoch (ride inside state_json, no
proto change).

Fire-and-forget delivery:
- sendPartialMessageEvent and sendStateUpdate no longer await
postMessage to the
  webview. A hidden/reloaded/closed webview can make postMessage hang or
resolve
  false; awaiting it could stall the backend turn loop. Correctness no
longer
  depends on any single delivery — the webview will be a convergent
replica (S3).

All new fields are optional/default-0 so the classic/legacy path is
unaffected.
Cancel's epoch bump + fence-before-abort and the remaining one-off
Date.now() mint
sites are handled in later steps (S6/S2-tail). 423 unit tests pass; tsc
clean.

fix(vscode): converge webview transcript via a pure reducer (fixes
last-message-missing)

S3 of the message-state redesign. The webview received the same
conversation over
two unordered, fire-and-forget channels — incremental partial messages
and full
state snapshots — and the state handler REPLACED clineMessages wholesale
("// HACK:
Preserve clineMessages if currentTaskItem is the same"). A stale
snapshot captured
before the last message landed could clobber the transcript, dropping
the final
message and leaving the UI stuck on "Thinking…" (reproduced earlier on
camera).

Introduce a pure convergent-replica reducer (messageReducer.ts) keyed on
the three
extension-stamped quantities from S2:
  - ts    : identity / merge key
  - seq   : freshness (higher seq wins for the same ts)
  - epoch : conversation/replica fence (newer replaces, older is
dropped, equal merges)

applyMessage / applyStateSnapshot are total and side-effect free:
  - older epoch  -> drop (straggler from a previous task/render)
  - newer epoch  -> replace the transcript wholesale (new task / history
load)
  - same epoch   -> merge by ts keeping the higher seq; a snapshot may
ADD/UPDATE
                    rows but NEVER truncate, so it can't drop a message
the partial
                    stream already delivered. Stale (older stateVersion)
snapshots are
                    ignored wholesale.

ExtensionStateContext now feeds both subscription callbacks through the
reducer via a
replicaRef, replacing the wholesale-replace HACK and the findLastIndex
append. Classic/
legacy state is unstamped (epoch 0 / version 0) and merges by ts exactly
as before.

Tests (messageReducer.test.ts): deterministic cases mapping 1:1 to the
bugs (stale
snapshot must not shrink the transcript; lower-seq ignored;
partial->final in place;
older-epoch straggler dropped; older-version snapshot ignored), PLUS an
order-independence proof — all 120 permutations of a causal log, with
duplication and
with non-final drops, converge to the same canonical state. This is the
high-confidence
guarantee that the webview cannot get stuck regardless of delivery
timing.

62 webview chat tests pass; tsc and biome clean.

feat(vscode): add authoritative TurnState (backend-owned UI mode)

S4 of the message-state redesign. Introduces the single source of truth
for the
webview's UI mode so it no longer has to be inferred from the tail of
clineMessages
(the root of RC1: missing/stuck "Thinking", vanishing approval buttons,
footer and
buttons disagreeing).

- Add TurnPhase / TurnState to shared types and ExtensionState.turnState
(rides inside
  state_json; no proto change).
- Add TurnStateTracker, owned by SdkController, sharing the one
id/seq/epoch authority.
  Each transition stamps a fresh seq so the webview keeps only the
newest TurnState.
- Set the phase at the exact lifecycle points where the backend knows
it:
    streaming         — initTask / reinit / user responded
(resolvePending*)
    awaiting_approval — handleRequestToolApproval (anchored on the ask)
    awaiting_followup — handleAskQuestion; and a turn that ends WITHOUT
attempt_completion
    completed         — turn ends and attempt_completion was used
    error             — onSendError; mistake_limit
    resumable         — cancelTask (set before abort)
    idle              — clearTask
  The completed-vs-awaiting_followup decision uses
  MessageTranslatorState.wasAttemptCompletionSeen().
- getStateToPostToWebview now ships turnState in every snapshot.

This is ADDITIVE: the webview does not read turnState yet (S5 wires
footer/buttons to
it and deletes the tail heuristics). Classic/legacy paths leave
turnState undefined and
keep the legacy behavior. 427 SDK unit tests pass (incl. new
turn-state-tracker tests);
tsc and biome clean.

fix(vscode): drive webview footer + buttons from TurnState (fixes RC1)

S5 of the message-state redesign. The webview decided "thinking vs
approving vs done"
and which buttons to show by inspecting the TAIL of clineMessages.
Because the backend
appends bookkeeping (api_req_started usage) after content and even after
approval asks,
the tail routinely meant the wrong thing — producing stuck "Thinking",
vanishing
Approve/Reject, and the footer disagreeing with the buttons (RC1,
reproduced on camera).

Now the webview reads the authoritative backend-owned TurnState (added
in S4):

- buttonConfig: add buttonsForPhase(turnState, anchoredMessage) and the
dispatcher
  getButtonConfigFromState(messages, turnState, mode). The button SET is
chosen by phase;
  approval labels (Approve vs Save, Run Command, MCP, subagents) come
from the anchored
  message (turnState.anchorTs). When turnState is absent (classic/older
state) it falls
  back to the legacy tail-walking getButtonConfigForMessages.
- ActionButtons reads turnState from useExtensionState and uses
getButtonConfigFromState.
- MessagesArea.isWaitingForResponse short-circuits to `phase ===
"streaming"` when
  turnState is present (and only shows the footer loader until a content
row is actually
  streaming); the legacy tail inference is kept as the fallback.

Button actions (approve/reject/proceed/new_task) already send a fixed
responseType
independent of clineAsk, and the SDK backend resolves the pending
approval/followup
promise — so routing is correct under TurnState. Classic fallback paths
are untouched.

New unit tests cover buttonsForPhase (every phase + anchored-label
selection +
mistake_limit-vs-api_req_failed) and getButtonConfigFromState (prefers
TurnState over a
trailing bookkeeping tail; legacy fallback). 68 webview chat tests pass;
tsc + biome clean.

fix(vscode): cancel raises the epoch fence before aborting (no
post-cancel stragglers)

S6 of the message-state redesign. sdkHost.abort() is cooperative — the
SDK can emit a
few more events after it. Previously cancelTask aborted first and only
the (post-abort)
!isRunning filter stripped two trailing ask types, letting say:*
stragglers land after
the resume_task ask and wedge the UI.

Now cancel raises the fence SYNCHRONOUSLY before the abort:
- SdkController.cancelTask sets turnState.phase = "resumable" (already
in S4), then
- SdkTaskControlCoordinator.cancelTask calls raiseCancelFence() (epoch
bump) BEFORE
  awaiting sdkHost.abort().

Any event the SDK emits after the abort request therefore carries the
OLD epoch and is
dropped by the webview's convergent reducer; the authoritative phase is
"resumable"
(Resume Task), independent of the message tail. Order matters and is
covered by a unit
test (fence before abort).

Usage accounting is exempt from the fence — it was never gated by the
message filter, so
a post-cancel usage event still bills the tokens the provider actually
generated.

The legacy !isRunning ask-only filter is now redundant but kept as
defense-in-depth.
428 SDK unit tests pass; tsc + biome clean.

refactor(vscode): translator hygiene — suppress ask_question row, drop
done synthetic ask

S7 (a + c) of the message-state redesign — removes two order-dependent
hacks now that
TurnState is authoritative.

(a) ask_question / ask_followup_question are suppressed from the generic
say:"tool"
    renderer (both content_start and content_end). The
SdkInteractionCoordinator services
    these and emits the proper ask:"followup"; the generic tool row was
an orphan partial
    that never finalized and defeated the tail heuristics. The CLI
already does this.

(c) The `done` handler no longer synthesizes a trailing
ask:"completion_result" — that
    was the "must be last message" hack (ENG-1887) that existed only
because the webview
    inferred UI mode from the array tail. `done` now emits no transcript
message and only
    signals turnComplete; the webview reads phase from TurnState
(completed when
    attempt_completion was used, else awaiting_followup). The green
"Task Completed" box
    still comes from the say:"completion_result" emitted at
attempt_completion content_end.

Translator unit tests updated to the new contract (done → 0 messages,
turnComplete=true).

Deferred to a follow-up (lower risk if left): (b) collapsing the
approval ask onto the
streaming tool's id (needs cross-coordinator id threading), and (d) the
mistake_limit
forced abort (now harmless since phase is authoritative). The
persisted-history renderer
still appends its own trailing ask so reopened tasks show the resume
affordance —
intentional.

428 SDK unit tests pass; tsc + biome clean.

fix(vscode): fix stuck/missing footer buttons from TurnState regressions

Webview now gates turnState by seq (a stale snapshot can no longer
revert
streaming->idle), never empties a live transcript on a lone newer-epoch
partial,
and routes a follow-up after completed/awaiting_followup to askResponse
instead
of starting a new task.

Backend phase emission fixes surfaced by live testing:
- post state on turn end even when the done event carries no messages
- askResponse sets phase=streaming (resume/continue shows Cancel,
unblocks send)
- a post-cancel turn-complete straggler no longer clobbers resumable

Adds reducer, send-routing, and session-event-coordinator unit tests.
See
src/sdk/docs/webview-message-state-design.md §11 for the full debugging
log.

improve openai compatible provider settings

fix(vscode): show Start New Task after multi-iteration completion turns

The completion signal (attemptCompletionSeen) was scoped to the message
translator's per-iteration reset(), so a turn that called the completion
tool
and then ran another iteration before 'done' lost the signal — the turn
ended
as awaiting_followup instead of completed and the footer showed no
'Start New Task' button despite the green Task Completed box.

Make the completion signal turn-scoped: reset() only clears streaming
pointers;
a new clearTurnOutcome() clears it at genuine turn/task boundaries
(initTask,
reinitExistingTaskFromId, askResponse). Also recognize the SDK's
built-in
submit_and_exit completion tool (summary field) alongside the VSCode
attempt_completion tool (result field).

Removes the webview-message-state design/findings docs and rewrites the
related comments to describe the system as-is.

improve OCA provider

- oca provider login now works

fix history view bugs

- deleting entries works
- favoriting works

fix open task conversation file

fix(vscode): include ambient vscode LM type decls in test tsconfig

The nightly CI "Build Tests and Extension" step (npm run ci:build)
failed
in compile-tests (tsc -p tsconfig.test.json) with:
  getVsCodeLmModels.ts: Property 'lm' does not exist on type 'typeof
import("vscode")'
  state-keys.ts: Module '"vscode"' has no exported member
'LanguageModelChatSelector'

@types/vscode is pinned to 1.84.0, which predates the Language Model
API.
The repo compensates with an ambient augmentation in
src/types/vscode-language-model.d.ts, which the main tsconfig picks up
via
its 'src/**/*' include. tsconfig.test.json overrides include to only
'src/**/*.test.ts'; listing src/types under typeRoots does not load a
loose
.d.ts (typeRoots only auto-loads @types-style package folders), so the
augmentation was missing from the test program.

Add 'src/types/**/*.d.ts' to tsconfig.test.json include so the
augmentation
is part of the test compilation.

change zai provider to user providers.json instead of statemanager

fix litellm provider

fix(vscode): load ambient vscode LM type decls in unit-test ts-node
program

The nightly CI 'Unit Tests with coverage - Linux' step (npm run
test:unit) failed in ts-node compilation:

  state-keys.ts: Module 'vscode' has no exported member
'LanguageModelChatSelector'

This surfaced as a misleading 'Cannot find package @shared/...'
ERR_MODULE_NOT_FOUND: Mocha tried require() first (which threw the
ts-node TSError), then fell back to import(), whose ESM resolver cannot
resolve the @shared/* path alias.

tsconfig.unit-test.json is driven by ts-node, which defaults to
files:false and compiles modules on demand from their imports. The loose
ambient augmentation in src/types/vscode-language-model.d.ts was
therefore never loaded, so state-keys.ts failed to compile.

Set ts-node.files=true and add src/types/**/*.d.ts to include so the
augmentation is part of the unit-test program. Mirrors the earlier
tsconfig.test.json fix for the separate build toolchain.

fix(vscode): widen ZAiProvider getEventValue to accept VSCodeDropdown
event union

The nightly 'vscode test' job (npm run ci:build -> build:webview -> tsc
-b) failed on both Linux and windows-latest:

  ZAiProvider.tsx: Argument of type 'Event | FormEvent<HTMLElement>' is
not assignable to parameter of type 'Event'

VSCodeDropdown's onChange supplies 'Event |
React.FormEvent<HTMLElement>', but the getEventValue helper (added when
the zai provider was moved to providers.json) was typed to accept only
Event. The helper only reads target.value, which exists on both, so
widen the parameter to the same union the dropdown provides.

Note: this only surfaces under 'tsc -b' / 'tsc --noEmit -p
tsconfig.app.json'; the webview's root tsconfig.json is a solution file
with files:[] so a bare 'tsc --noEmit' checks nothing.

The separate 'test / test' job failure is an aggregate gate that fails
because vscode-test failed; fixing this resolves it too.
2026-06-05 09:58:54 -07:00
Dominic Cooney a750efc17e sdk migration: squashed pre-2026-05-27 work
Squashed foundational SDK-migration work older than one week (author dates
up to 2026-05-26), combining the previous "squashed pre-2026-05-22 work"
base commit with subsequent older commits:

- sdk migration base (pre-2026-05-22 squash)
- fix(mcp): accept CLI-authored nested transport format, preserve oauth/metadata, improve schema error messages
- add telemetry to sdk extension
- improve task startup perf
- harden perf improvements
- remove timing code
- chore: fix lint and format on the vscode app
- fix: declare missing direct dependencies in apps/vscode
- fix integration tests
- fix(test): stub telemetry helpers in unit-test @cline/core mock
- ci: run publish-nightly job inside apps/vscode workspace
- remove old md files
- remove outdated samples
- step one for removing src/core/api folder
2026-06-05 09:52:20 -07:00
Tomás Barreiro e152741e1d Remove the recommended models feature flag (#11315)
* Remove the recommended models feature flag

* Fix recommended model flag CI

---------

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-06-05 08:36:18 -07:00
Mikołaj Kondratek 6fec41ea70 fix(mcp): guard settings writes in delete/add so the watcher can't empty the list (#11240)
deleteServerRPC and addRemoteServer wrote cline_mcp_settings.json without
setting isUpdatingClineSettings, so the chokidar settings watcher was not
suppressed during the plugin's own write. The watcher is configured with
atomic: true and awaitWriteFinish (stabilityThreshold 100ms); on Windows it
races the non-atomic writeFile, reads a transient/empty file, and
readAndValidateMcpSettingsFile() returns { mcpServers: {} }. updateServerConnections({})
then tears down every in-memory connection, so deleting one MCP server emptied
the whole list in the UI after navigating away and back (CLINE-2097).

Wrap both methods in the same guard the sibling RPCs already use
(toggleServerDisabledRPC, toggleToolAutoApproveRPC, updateServerTimeoutRPC):
set isUpdatingClineSettings = true before the write and clear it on a 300ms
timer in finally, so the delayed watcher "change" event is skipped. addRemoteServer
had the same latent omission and is fixed in the same change.

Known tradeoff (pre-existing, unchanged by this fix): the guard is a single
shared boolean cleared by uncoordinated 300ms timers, so two settings writes
within 300ms can clear the flag early. Because awaitWriteFinish only emits once
the file is stable, the worst case there is a redundant reconnect, not the
empty-list data loss this fixes. A deterministic guard (per-op token or
content-compare-and-skip in the watcher) is out of scope for this targeted fix.

Adds McpHub.deleteServerRPC.test.ts covering the user-visible symptom (delete
one of two servers -> remaining server still returned/persisted, list not empty)
and the guard contract (flag set during write, cleared after 300ms, cleared on
the not-found error path).
2026-06-05 08:36:04 -07:00
Saoud Rizwan 5de1a45d1d chore(cli): release v3.0.20 2026-06-04 17:19:31 -07:00
Saoud Rizwan 717a2e643a fix(cli): name installed plugin wrappers from source (#11291) 2026-06-04 16:54:47 -07:00
Saoud Rizwan e50184d316 chore(cli): release v3.0.19 2026-06-04 14:48:01 -07:00
Saoud Rizwan 08c6f7ecbe fix(cli): use npm update for auto updates (#11285)
* fix(cli): use npm update for auto updates

* fix(cli): preserve npm update dist tag
2026-06-04 14:45:24 -07:00
Saoud Rizwan 15e6a685d6 chore(cli): release v3.0.18 2026-06-04 13:49:47 -07:00
Saoud Rizwan bc0eed950b test(core): skip chmod-based plugin uninstall failure test on Windows (#11284) 2026-06-04 13:34:56 -07:00
Saoud Rizwan 48316027a7 chore(sdk): release v0.0.43 2026-06-04 13:19:13 -07:00
Saoud Rizwan 4d15d16109 docs(cli): release the SDK before the CLI in the publish-cli skill (#11280)
* docs(cli): release the SDK before the CLI in the publish-cli skill

Add a Step 0 to the publish-cli skill that gates a CLI release on an SDK
release when the SDK changed since its last release, and relocate the
publish-cli skill to the repo root.

Why release the SDK alongside the CLI: the CLI bundles the SDK source via
workspace:*, so the CLI always ships the latest SDK code, but the hub
daemon stamps a buildId that defaults to the @cline/core version and a
running hub is only respawned when that buildId changes. Bumping the SDK
version forces a stale hub to be retired and respawned with the new code.
It also keeps SDK releases on a regular cadence in step with the CLI.

Step 0 covers detecting unreleased sdk/packages changes, bumping the
shared SDK version + llms CHANGELOG, committing to main, kicking off
sdk-publish.yml on the latest channel, and waiting for it before cutting
the CLI release. Also fixes the now-stale working-directory note (commands
run from the repo root, not sdk/) and updates the DEVELOPMENT.md path.

* chore: move opentui skill to repo root

Relocate the opentui TUI skill from apps/cli to the repo root, matching
the real-dir + symlink convention used by the other root skills (real dir
in .agents/skills, symlink from .claude/skills).

* docs(sdk): reformat the SDK changelog and move it to sdk/CHANGELOG.md

The changelog covers all SDK packages (they share one version and release
together), so move it from sdk/packages/llms/ to the SDK root, parallel to
apps/cli/CHANGELOG.md. Reformat to match the CLI changelog: a titled
header with flat, version-only sections, newest on top, no dates, and no
Next Release bucket. The unreleased entry that bucket held is captured
from commits when the next SDK release is drafted. Update the publish-cli
skill to draft SDK notes from commits and prepend a ## <version> section
at sdk/CHANGELOG.md.
2026-06-04 12:48:17 -07:00
Bee d2339f57f1 fix(slack): normalize channel mentions to original post thread (#11273)
* fix(slack): normalize channel mentions to original post thread

Route top-level Slack channel mentions to the originating post thread so
replies land in the correct conversation. Add `resolveSlackChannelMentionThread`
to rewrite non-DM mention threads using the message's `thread_ts`/`ts` and
channel, while preserving DM threads and already-correct threads.

Includes unit tests covering normalization, no-op, and DM cases.

* thread id
2026-06-04 11:49:56 -07:00
Ahmad Shahzad a209825116 Sync Fireworks AI model registry with current platform offerings (#11173)
* feat: update Fireworks model registry to improve Cline UX for Fireworks API users

The VS Code extension's Fireworks model list was significantly out of
date compared to the current active models available on the Fireworks
platform. This commit updates the registry to match the current model
lineup, ensuring users can select from the latest available models.

Changes:
- Default model: accounts/fireworks/models/kimi-k2p6 (was kimi-k2p5)
- SDK default: accounts/fireworks/models/kimi-k2p6 (was minimax-m2p5)

Removed 6 stale/phantom models no longer available:
  - qwen3-vl-30b-a3b-thinking
  - qwen3-vl-30b-a3b-instruct
  - deepseek-v3p2
  - glm-4p7
  - glm-5
  - minimax-m2p1

Added 9 missing models:
  - accounts/fireworks/models/kimi-k2p6
  - accounts/fireworks/routers/kimi-k2p6-turbo
  - accounts/fireworks/models/deepseek-v4-flash
  - accounts/fireworks/models/deepseek-v4-pro
  - accounts/fireworks/models/glm-5p1
  - accounts/fireworks/routers/glm-5p1-fast
  - accounts/fireworks/models/minimax-m2p7
  - accounts/fireworks/models/qwen3p6-plus
  - accounts/fireworks/models/gpt-oss-20b

Fixed metadata for 3 overlapping models:
  - kimi-k2p5: contextWindow 262144 → 256000, maxTokens 16384 → 256000
  - minimax-m2p5: maxTokens 16384 → 196608
  - gpt-oss-120b: maxTokens 16384 → 32768, cacheReadsPrice 0.01 → 0.015

All models now have cacheWritesPrice: 0 because Fireworks does not
charge a separate rate for prompt cache writes (cache writes are
billed at the standard input rate, matching the SDK catalog).

Three models remain in the UI but are scheduled for deprecation on
June 17 and will be removed then:
  - accounts/fireworks/models/kimi-k2p5
  - accounts/fireworks/models/minimax-m2p5
  - accounts/fireworks/models/qwen3p6-plus

Files:
- apps/vscode/src/shared/api.ts
- apps/vscode/webview-ui/src/components/settings/__tests__/APIOptions.spec.tsx
- sdk/packages/llms/src/providers/builtins.ts

* fix: update qwen3p6-plus context window and maxTokens
2026-06-04 19:19:26 +02:00
Mahesh Narayan acc1a25e51 Update agent storage guidance (#11025) 2026-06-04 09:53:18 -07:00
Saoud Rizwan 5f17a6963f fix(cli): clear abort indicator immediately (#11265) 2026-06-04 00:02:16 -07:00
Saoud Rizwan c7ddb96ddb chore(cli): release v3.0.17 2026-06-03 21:07:30 -07:00
Bee 107bce75b2 fix(cli): recover stale interactive sessions and suppress shutdown races (#11259)
* fix(cli): recover stale interactive sessions and suppress shutdown hook races

This fixes the CLI/TUI regression introduced between `3.0.14` and `3.0.15` where the interactive CLI could enter a broken state after stopping and restarting Cline Hub, then attempting to cancel a request with Escape.

The affected release window was:

- `49e8c1b32` / `v3.0.14`: known-good baseline
- `c33c3176e` / `v3.0.15`: release containing the regression
- `fad8271f4 feat: Cline Hub web app (#10969)`: relevant behavior change in the window

The Hub web app change introduced new Hub-backed runtime/session lifecycle behavior. After Ctrl+C or Hub shutdown, the CLI could still retain an `activeSessionId` that no longer existed in the Hub/runtime process. On the next interactive send, the CLI attempted to reuse that stale session and received `session not found`. Because cancellation also targeted the stale session, Escape stopped working and OpenTUI ended up receiving failures during input handling, which made the TUI look corrupted.

The same lifecycle issue also explains the Ctrl+C errors:

```text
error: hook dispatch failed: Hub connection closed (code=1006, reason=Connection ended)
error: WebSocket connection to 'ws://127.0.0.1:50168/hub' failed: Failed to connect
```

Those were caused by late hook dispatches racing against Hub shutdown. The CLI was still trying to send hook events over a Hub WebSocket that had already closed.

**What changed**

- Added missing-session recovery in the interactive runtime.
  - Detects `session not found` / stale session errors.
  - Reads any recoverable messages from the missing session.
  - Clears the stale active session state.
  - Starts a new interactive runtime session.
  - Retries the current turn once against the fresh session.

- Made hook dispatch shutdown-aware.
  - Runtime hooks now mark themselves as shutting down before session disposal.
  - Hook dispatches are skipped once shutdown begins.
  - Dispatch failures during shutdown are suppressed, since the Hub transport closing is expected at that point.

- Reordered CLI cleanup.
  - Hooks are shut down before stopping/disposing runtime sessions.
  - This prevents abort/stop lifecycle events from trying to dispatch over a closing Hub connection.

**Regression coverage**

Added tests for:

- Recovering from a disappeared active interactive session and retrying against a new session.
- Ensuring hook events are not dispatched after shutdown begins.

**Verification**

Passed:

```text
bunx vitest run apps/cli/src/utils/hooks.test.ts apps/cli/src/runtime/interactive/session-runtime.test.ts
bun -F @cline/cli typecheck
bun -F @cline/cli test:unit
bun -F @cline/cli test:e2e:cli:tui
git diff --check
```

* SessionNotFoundError

* fix(core): preserve stale session errors in hub runs

* clean up

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-03 21:00:19 -07:00
Saoud Rizwan 81792d20c6 fix(sdk): keep hub daemon alive on runtime abort (#11258) 2026-06-03 20:22:55 -07:00
Saoud Rizwan e8e2af705d feat(cli): improve Telegram connector with --allowed-user-id flag (#11256)
* feat(cli): add Telegram allowed user id flag

* fix(cli): tighten connector authorization hooks
2026-06-03 17:50:07 -07:00
Bee 8f00fcf3ed chore: bun run fix (#11252) 2026-06-03 13:45:06 -07:00
Saoud Rizwan a64d17734d chore(cli): release v3.0.16 2026-06-03 13:29:33 -07:00
Saoud Rizwan 8e621817c5 feat: add plugin uninstall ability (#11247)
* feat: add plugin uninstall flow

* fix: preserve disabled plugin settings on uninstall failure
2026-06-03 13:23:35 -07:00
Bee 423fde4828 feat(cli): add Slack socket mode support (#11245)
* feat(cli): add Slack socket mode support

Add socket mode as an alternative to webhook mode for Slack
connector, allowing connections without a public URL.

- Introduce `--connection` flag to select webhook or socket mode
- Add `--app-token` option for socket mode authentication
- Make signing secret and base URL conditional on webhook mode
- Add `parseSlackConnectionMode` with validation and tests
- Update CLI platform definition to support hybrid connection type
- Update README docs with socket mode usage examples

* use base-url and remove connection flag

* isSocketMode
2026-06-03 12:00:57 -07:00
Saoud Rizwan 76af785fa6 fix: allow skills plugin capability (#11244) 2026-06-03 10:42:12 -07:00
Bee dd7042fc7b fix(core): use union schema for read files tool input validation (#11225)
* fix(core): use union schema for read files tool input validation

Move the normalizeReadFileRequests helper logic directly into the
read_files tool executor, replacing the legacy helper with inline
validation against ReadFilesInputUnionSchema. This ensures invalid
union inputs are rejected before reaching the executor.

Update tests to reflect validation behavior and add coverage for
rejecting invalid union inputs.

* add new schema support
2026-06-03 10:20:54 -07:00
Tomás Barreiro 38e5b7c26b Fix VSCode CI (#11236)
* Fix VSCode CI

* fix nightly publish

* Fix install

* Fix biome

* fix tsc

* Fix windows set-up

* Ignore .vscode-test

* Add a LICENSE to the vscode extension

* Add type roots

* Remove workspaces

* remove vscode as a bun workspace
2026-06-03 18:58:27 +02:00
Dominic Cooney 8ae99cd69f feat(llms,core): route custom registered handlers through the agent runtime (#11235)
* feat(llms,core): route custom registered handlers through the agent runtime

Expose the handler-registry helpers (hasRegisteredHandler, getRegisteredHandler,
getRegisteredHandlerAsync, isRegisteredHandlerAsync) from @cline/llms, and have
core's createAgentModelFromConfig consult the registry: when a handler is
registered for a provider, build it via createHandler and adapt its ApiHandler
surface onto the AgentModel contract (the inverse of the gateway's
toApiStreamChunk).

This lets hosts register provider handlers that need host-only dependencies
(e.g. a vscode.lm-backed handler) and have them used by the main agent loop,
not just standalone createHandler callers.

* fix(core): resolve registered handlers lazily and avoid double finish

Address review feedback:
- createAgentModelFromConfig built the handler eagerly with the sync
  createHandler, which throws for providers registered via registerAsyncHandler.
  The adapter now accepts a handler factory and resolves it on the first stream
  via createHandlerAsync, supporting both sync- and async-registered handlers.
- Guard the adapter's catch-block finish with sawFinish so a handler that emits
  an explicit done chunk and then throws does not produce two finish events.

* fix(core): preserve thought signatures and finish-reason semantics in adapter

Further review feedback on the ApiHandler -> AgentModel adapter:
- Reasoning and tool-call thought signatures are now surfaced under
  metadata.thoughtSignature (the key downstream adapters read), instead of being
  stored as metadata.signature / dropped.
- A done chunk whose incompleteReason indicates max output tokens now maps to
  finish{reason:"max-tokens"} rather than "stop".
- A turn that ends with tool calls (no explicit done) now terminates as
  finish{reason:"tool-calls"}, matching the gateway/AI-SDK adapters.

* Apply remaining changes

* fix(core): report lazy handler-factory rejection as a finish(error) event

The lazy handler resolution (await source()) ran outside the adapter's
try/catch, so a rejecting factory (e.g. when the host API is unavailable at
stream time) escaped as a raw generator exception instead of a terminal
finish{reason:"error"} event. Move the resolution inside the try block so all
failure paths converge on the same terminal finish.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-04 01:40:59 +09:00
Max 845970ba7d improve cline provider migration (#11242)
- user's who are signed in with oauth in old extension were not properly
migrating their token. this commit handles that

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-06-03 09:38:18 -07:00
Max 444a9be6ec allow baseUrl field for anthropic vendor-type providers (#11227)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-06-02 20:01:49 -07:00
Saoud Rizwan ade7775337 feat(cli): install official plugins by slug (#11230)
* feat(cli): install official plugins by keyword

* fix(cli): harden official plugin clone

* fix(cli): remove official plugin repo env override
2026-06-02 18:24:37 -07:00
Tomás Barreiro e147682945 Set-up global greptile rules, kanban and other SDK files (#11233)
* Set-up global greptile rules, kanban and other SDK files

* Fix path

* Fix stale path

* Update vitest workspace config
2026-06-03 03:24:07 +02:00
Saoud Rizwan ae78fb422c docs(sdk): add custom model provider plugin example (#11234)
* docs(sdk): add custom model provider plugin example

Add an OpenRouter-backed example plugin demonstrating the providers
capability and registerProvider. It registers an OpenAI-compatible
provider plus its model catalog with the gateway so the agent can run
inference against an endpoint Cline does not bundle.

Registers under a distinct id (openrouter-plugin) to avoid colliding
with the built-in openrouter provider.

* docs(sdk): drop redundant provider section from plugin examples readme

* docs(sdk): drop provider demo line from plugin examples readme

* fix: support plugin model providers

* docs: remove provider plugin demo

* docs: address provider example review
2026-06-02 18:22:06 -07:00
Tomás Barreiro 220a21bdcf Move sdk/apps/ to apps/ (#11200)
* Move the apps to the root dir

* Update all references from sdk/apps/ to apps/

* Update dependencies

* Install bun types

* Fix types

* Fix types

* Fix linter

* Ingore apps from vscode

* Fix security warning

* Fix windows install

* Enable windows dev mode

* Revert "Enable windows dev mode"

This reverts commit a46c99282e.

* Revert "Ingore apps from vscode"

This reverts commit 47f7b265d2.

* Revert "Fix windows install"

This reverts commit 1dabba1556.

* update the repo root

* fix root dir

* fix path

* fix other path

* Fix unrelated changes

* fix: address apps move follow-up blockers (#11228)

* fix: update root app command paths

* fix: include moved apps in root checks

* fix: clean up moved app path references

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-03 01:49:57 +02:00
Saoud Rizwan 6aef7f5280 docs(cli): refine supply-chain scan alerts sample (#11224)
- Fold provider/model setup into a single `cline` run; drop the auth command
- Remove the /yolo on step from the Telegram setup
- Present scheduling as two clear options (Telegram chat vs terminal with
  delivery flags)
- Clarify how to find the schedule id before triggering a test run
2026-06-02 14:57:48 -07:00
Saoud Rizwan 4e56ed6922 docs(cli): add supply-chain scan alerts sample (#11222)
* docs(cli): add supply-chain scan alerts sample

Walkthrough for scheduling the Cline CLI to run Perplexity's Bumblebee
scanner and deliver compromise alerts to Telegram. Covers installing the
CLI, cloning/building Bumblebee and how it stays read-only, the Telegram
connector, and creating a scheduled scan that texts a clean/alert verdict.

* docs(cli): drop unsupported --delivery-thread from supply-chain sample
2026-06-02 14:04:33 -07:00
Saoud Rizwan 3a0f182408 fix(cli): show skills in slash autocomplete (#11220) 2026-06-02 13:18:30 -07:00
Saoud Rizwan 1f7adbd87e feat(cli): group plugin skills in settings (#11219) 2026-06-02 13:17:05 -07:00
Saoud Rizwan b0590554da feat: add skills bundled with plugins (#11161)
* feat: discover skills bundled with plugins

* fix: scope plugin bundled skills to active plugins

* fix: prevent ancestor skill discovery for plugins
2026-06-02 12:50:15 -07:00
Ara af2454f8d9 chore: bump version and update changelog (v3.87.0) (#11211) 2026-06-02 10:19:08 -07:00
Shantanu Gontia 1a4bf98e31 Update Sambanova Models (#11008)
* Update Sambanova Models

* moved to vscode/

* fix context windows

* Update Sambanova Models

* fix context windows

* Update api.ts

* Update sambanova prices
2026-06-02 18:52:00 +02:00
Ara 4139db4127 feat: add MiniMax M3 model (#11210) 2026-06-02 09:16:38 -07:00
Saoud Rizwan d55916e3ab fix(cli): show MCP OAuth errors in TUI (#11196)
* fix(cli): surface MCP OAuth errors in TUI

* chore(cli): reuse MCP status label helper
2026-06-01 20:08:23 -07:00
Bee 386ded5126 feat(cli): bundle and serve Cline Hub dashboard with cline dashboard (#11195)
* feat(cli): bundle and serve Cline Hub dashboard with cline dashboard

Add the @cline/cline-hub workspace dependency to the CLI and build the
Hub webview as part of CLI packaging. Copy the generated dashboard assets
into platform-specific CLI distributions so the dashboard is available in
built artifacts.

Refactor the Cline Hub server startup into an exported function so the CLI
can start and stop the dashboard server programmatically.

* fix(cli): resolve dashboard webview in wrapper installs

Detect the platform-specific CLI package from the published wrapper layout
and use its bundled cline-hub webview assets when no explicit dist path is
set. Add test coverage for resolving assets via CLINE_WRAPPER_PATH.

* patches

* patch

* fix server detachHub on stop

Imported detachHub.
Changed ClineHubDashboardServer.stop to () => Promise<void>.
Made stop() idempotent with a stopped guard.
Clears the health interval.
Calls server.stop(true).
Always calls await detachHub(ctx) in a finally, so hub client teardown still happens if the HTTP server stop throws.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-01 17:35:24 -07:00
WaylandYang 44e15319e4 fix(plugin-sandbox): expose CLINE_PLUGIN_IMPORT_TIMEOUT_MS env override (#11065) (#11084)
* fix(plugin-sandbox): expose CLINE_PLUGIN_IMPORT_TIMEOUT_MS env override

The hardcoded 4000 ms importTimeoutMs default is too tight on Windows
cold-start; the plugin-sandbox tests already use 30_000 ms for the same
reason. This patch lets hosts raise the ceiling via env var without
touching code or adding a CLI flag, with explicit options.importTimeoutMs
still taking precedence.

Precedence: options.importTimeoutMs > env var > 4000.

Refs: #11065

* fix(plugin-sandbox): tighten env parsing + use vi.stubEnv (PR feedback)

- Number.parseInt accepts trailing garbage ("4000ms" -> 4000); switch
  to Number() + Number.isInteger() so malformed env values fall back
  to the default instead of silently consuming the numeric prefix.
- Replace manual process.env save/restore in the regression test with
  the idiomatic vi.stubEnv() / vi.unstubAllEnvs() pattern.

Per Greptile review on #11084.
2026-06-01 17:34:13 -07:00
Saoud Rizwan e424b28702 feat(cli): add plugins slash command (#11193)
* feat(cli): add plugins slash command

* fix(cli): address plugins command review feedback
2026-06-01 16:59:38 -07:00
Robin Newhouse db9971890e Add SDK telemetry for run_commands timeouts (#11149)
* feat(sdk): add run_commands timeout telemetry

* docs(sdk): document timeout telemetry event

* docs(sdk): move telemetry catalog to core docs

* fix(sdk): omit undefined timeout telemetry fields

* fix(sdk): mark timed out run_commands unsuccessful

* docs(sdk): defer telemetry catalog entry

* fix(sdk): limit run_commands timeout success override

* fix(sdk): tighten timeout telemetry plumbing
2026-06-01 16:22:04 -07:00
Tomás Barreiro d7cc9b6155 Move bun from the sdk/ to root (#11104)
* Move bun to root

* Fix scripts and pre-commit

* Update scripts

* Update workflows

* Fix cd

* fix pre-commit

* fix cli publish
2026-06-01 22:29:41 +02:00
Saoud Rizwan 05042d3ff7 docs(sdk): add env-blocker plugin example (#11192)
* docs(sdk): add env-blocker plugin example

Adds a beforeTool hook plugin that deterministically blocks the agent
from reading .env secret files via read_files, editor, or run_commands
(e.g. cat .env), while leaving .env.example/.sample/.template readable.
Demonstrates moving a security policy out of an AGENTS.md rule (a
suggestion the model can ignore) and into the execution path.

* docs(sdk): install env-blocker globally in usage examples

A secret-protection guard is most useful applied to every project, so
drop the --cwd . project-scoped install in favor of the global default.

* docs(sdk): trim env-blocker usage docs

* docs(sdk): limit env-blocker to read paths only

It is a read blocker, so only guard read_files and run_commands.
Drop the editor case (and with it the symmetric apply_patch concern),
keeping the example focused and simple.

* docs(sdk): rename env-blocker helpers for readability

collectPaths -> extractFilePaths, collectCommands -> extractShellCommands
so the beforeTool call sites read clearly at a glance.

* docs(sdk): rename commandTouchesEnv to commandReadsEnv

* docs(sdk): drop console.error from env-blocker hook
2026-06-01 12:12:16 -07:00
2021 changed files with 61042 additions and 136748 deletions
@@ -9,12 +9,13 @@ Use this skill when the user asks to release the CLI, publish `cline`, bump the
The CLI is npm-only. Do not add alternate distribution or signing steps.
> Working directory: this skill lives in the SDK sub-monorepo. Run `cd sdk` (from the repo root) before any of the shell commands below. Paths in commands and instructions (e.g. `apps/cli/package.json`, `bun release cli`) are written relative to `sdk/`.
> Working directory: run every command below from the repository root. Paths and scripts (e.g. `apps/cli/package.json`, `sdk/packages/`, `bun release cli`, `bun run version`) are written relative to the repo root.
The skill should guide the user through one release preparation flow, then offer the publish path options. The two normal publish paths are GitHub Actions and local publishing from an authenticated machine.
## Release contract
- SDK prerequisite: the CLI depends on the SDK via `workspace:*` (`@cline/core`, `@cline/shared`, and friends). If the SDK changed since its last release, release the SDK first and wait for it to finish publishing before releasing the CLI. See "Step 0: Release the SDK first if it changed" below.
- Version source: `apps/cli/package.json`.
- Main release tag: `cli-vX.Y.Z`, where `X.Y.Z` matches `apps/cli/package.json`.
- Nightly release version: `X.Y.Z-nightly.TIMESTAMP`.
@@ -30,8 +31,93 @@ The skill should guide the user through one release preparation flow, then offer
- Always ask before pushing commits or tags.
- Do not amend commits unless explicitly requested.
## Step 0: Release the SDK first if it changed
Do this before anything else in the Workflow below.
The CLI builds and ships against the SDK source in the monorepo (`workspace:*` for `@cline/core`, `@cline/shared`, and the rest), so a CLI release always contains the latest SDK code whether or not the SDK was released. The build and tests use that source too, not anything from npm. Releasing the SDK alongside the CLI is still worth doing for two reasons:
- Hub freshness. The hub daemon lives in `@cline/core` and stamps a `buildId` that defaults to the `@cline/core` package version (`resolveHubBuildId` in `sdk/packages/core/src/hub/discovery/index.ts`). A running hub is only retired and respawned when that `buildId` changes (`isCompatibleHubRecord` / `retireIncompatibleHub` in `sdk/packages/core/src/hub/daemon/index.ts`). So if the SDK code changed but the version did not, a user who upgrades the CLI keeps talking to their already-running hub, which is still executing the old SDK code. Bumping the SDK version makes the new CLI's `buildId` differ, so the stale hub is detected as incompatible and respawned with the fresh code.
- Release hygiene. We want regular SDK releases; cutting one whenever we cut a CLI release keeps the published SDK in step with what the CLI ships.
So when the SDK has changed, release it first (which bumps the `@cline/core` version), then cut the CLI release on top of that bump. Leave the CLI's SDK dependency as `workspace:*` — the fix is to release the SDK, not to pin the CLI.
1. Check for unreleased SDK changes.
```sh
git fetch origin --tags
git tag --list 'sdk/sdk/v*' 'sdk-v*' --sort=-v:refname | head -1
git log <last-sdk-tag>..origin/main --oneline --no-merges -- sdk/packages
```
`sdk/<pkg>/v*` tags are created by the `sdk-publish.yml` workflow; `sdk-v*` tags are created by the local `bun release sdk` helper. Use whichever is newest as the baseline.
If `git log` prints no commits, the SDK is already up to date. Skip the rest of Step 0 and continue with the Workflow below.
If it prints commits, sanity-check the diff (ignore entries that are only the previous version-bump commit's lockfile or generated files), then release the SDK.
2. Decide the SDK version bump.
All SDK packages share one version, read from `sdk/packages/llms/package.json`. Ask whether this is patch, minor, major, or an explicit version. Patch is the default. Do not guess if the user has not made it clear.
3. Draft the SDK release notes and update the changelog.
Draft user-facing notes from the SDK commits found in step 1, translating commit messages into user-facing language (same approach as the CLI release notes below). Prepend a new `## <version>` section with those notes to the top of `sdk/CHANGELOG.md`, using the header format `## <version>` with no date — the same flat, newest-on-top format as `apps/cli/CHANGELOG.md`. This is the SDK changelog (all SDK packages share one version) and it is maintained by hand; the `sdk-publish.yml` workflow does not read it.
4. Bump versions and regenerate.
```sh
bun run version <version>
```
This bumps every SDK `package.json` to the new version, regenerates the lockfile and the generated model catalog, formats, and builds. Review the result.
5. Commit and push the bump to `main`.
The `sdk-publish.yml` workflow publishes the version that is committed on `main` and tags that commit, so the bump must land on `main` before the workflow runs.
```sh
git add -A
git commit -m "chore(sdk): release v<version>"
```
Ask before pushing:
```sh
git push origin HEAD
```
6. Trigger the SDK publish workflow on the `latest` channel.
```sh
gh workflow run sdk-publish.yml -f channel=latest -f confirm_publish=publish
gh run list --workflow=sdk-publish.yml --limit=1 --json databaseId,url,status,createdAt --jq '.[0]'
```
The workflow runs the SDK tests, publishes `@cline/shared`, `@cline/llms`, `@cline/agents`, `@cline/core`, and `@cline/sdk` to npm with the `latest` dist-tag in dependency order, and pushes `sdk/<pkg>/v<version>` git tags.
7. Wait for the SDK workflow to succeed before starting the CLI release.
```sh
gh run watch <run-id> --exit-status
```
Do not start the CLI release until this run has finished successfully. The CLI does not install the SDK from npm, but cutting the CLI release on top of a clean, completed SDK release keeps the two in step: the CLI release commit then sits on top of the `@cline/core` version bump, so the shipped CLI carries the new version that forces a running hub to respawn with the new code, and you are not building a CLI release on top of an SDK release that failed midway.
After the SDK release succeeds, pull `main` so the CLI release is prepared on top of the SDK version bump:
```sh
git checkout main && git pull --ff-only
```
Then continue with the Workflow below.
For a local SDK publish from an authenticated machine instead of the workflow, `bun release sdk <version>` exists, but prefer the `sdk-publish.yml` workflow for normal releases so the CLI release can gate on a single GitHub Actions run.
## Workflow
Complete Step 0 first. Only proceed once the SDK is released (or you confirmed no SDK release was needed).
1. Gather context.
```sh
@@ -46,10 +132,10 @@ Find the latest CLI tag. If there is no `cli-v*` tag, use the first relevant CLI
2. Collect release commits.
```sh
git log <last-cli-tag>..HEAD --oneline --no-merges -- apps/cli packages scripts .github/workflows/cli-publish.yml
git log <last-cli-tag>..HEAD --oneline --no-merges -- apps/cli sdk/packages sdk/scripts .github/workflows/cli-publish.yml
```
If the release includes broader SDK changes that affect the CLI, also inspect commits outside `apps/cli`.
The `sdk/packages` commits matter here even though the SDK was released separately in Step 0: the CLI bundles the SDK, so SDK changes ship in this CLI release too. Read those commits and fold anything user-relevant to the CLI into the release notes (provider/model updates, behavior changes, fixes the CLI inherits). Skip SDK changes that are purely internal or have no CLI-visible effect.
3. Draft user-facing release notes.
+128
View File
@@ -0,0 +1,128 @@
# 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"}'
```
## Data Isolation
The debugee runs with `CLINE_DIR=~/.cline2` by default, separate from your real `~/.cline`.
This prevents the debugee's logout from logging out the debugger, and vice versa.
Override with `--cline-dir /tmp/test-dir`. Check with `status()``clineDir`.
## Browser Capture & OAuth
The debugee runs with `CLINE_CAPTURE_BROWSER=1`, which intercepts `openExternal()` in
`src/utils/env.ts`. URLs are captured instead of opening a real browser:
- Logged to `$CLINE_DIR/data/debug-captured-urls.jsonl`
- POSTed in real-time to `/captured-url` on the harness server
- Queryable via `oauth.captured_urls`
### OAuth API
- **`oauth.captured_urls`** `{clear?}` — URLs the debugee tried to open
- **`oauth.read_stored_token`** — Check auth token presence in secrets.json
- **`oauth.simulate_callback`** `{path, code?, state?, provider?, token?}` — Build vscode:// callback URI
- **`oauth.read_captured_urls_file`** — Read on-disk JSONL of captured URLs
### OAuth testing flow
For **Cline OAuth** (SDK local callback): The SDK starts a local HTTP server, the auth URL
is captured. To complete: open the captured URL in a real browser (it redirects back to the
SDK's callback server), OR extract the callback port and `curl http://127.0.0.1:PORT/callback?code=...`.
For **MCP/Provider OAuth** (vscode:// URI): The redirect goes to a vscode:// URI.
`oauth.simulate_callback` only *builds* the URI — it does not deliver it, and the ESM
extension host can't `require()` the handler. To actually deliver the callback, call the
debug-only hook via `ext.evaluate` (with `awaitPromise: true`):
`globalThis.__clineHandleUri("vscode://saoudrizwan.claude-dev/...?code=...&state=...")`.
It runs the same `SharedUriHandler.handleUri` as VSCode's real URI handler and exists only
when `CLINE_CAPTURE_BROWSER` is set (the harness always sets it; never ships in prod).
For end-to-end MCP OAuth, get a real `code` from the local MCP OAuth test server
(`npm run dev:mcp-oauth-test-server`).
## Navigating Views — Use Commands, Not Clicks
Don't try to find/click small sidebar icons. Use VSCode commands via command palette.
Registered in `src/registry.ts`:
| Command | View |
|---------|------|
| `cline.accountButtonClicked` | Account / sign-in |
| `cline.historyButtonClicked` | Task history |
| `cline.settingsButtonClicked` | Settings |
| `cline.mcpButtonClicked` | MCP servers |
| `cline.plusButtonClicked` | New task (chat) |
| `cline.worktreesButtonClicked` | Worktrees |
```bash
curl localhost:19229/api -d '{"method":"ui.command_palette","params":{"command":"cline.accountButtonClicked"}}'
```
## 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?, 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
## Typical Session
```bash
# 1. Launch
curl localhost:19229/api -d '{"method":"launch","params":{"skipBuild":true}}'
# 2. Open sidebar + dismiss overlays (ALWAYS do this first)
curl localhost:19229/api -d '{"method":"ui.open_sidebar"}'
curl localhost:19229/api -d '{"method":"web.evaluate","params":{"expression":"document.querySelectorAll(\".sr-only\").forEach(el => el.parentElement?.click())"}}'
# 3. Navigate to view
curl localhost:19229/api -d '{"method":"ui.command_palette","params":{"command":"cline.accountButtonClicked"}}'
# 4. Check captured OAuth URLs if testing auth
curl localhost:19229/api -d '{"method":"oauth.captured_urls"}'
# 5. Verify
curl localhost:19229/api -d '{"method":"ui.screenshot"}'
```
## Caveats
- **⚠️ Dismiss promotional overlays FIRST**: On fresh launches, full-screen promo overlays block the sidebar. **Dismiss immediately after `ui.open_sidebar`**, before any other interaction or screenshot. May need to run twice:
```bash
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
curl localhost:19229/api -d '{"method": "web.evaluate", "params": {"expression": "document.querySelectorAll(\".sr-only\").forEach(el => el.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 that 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.
- **OAuth with fake codes**: Browser capture intercepts the URL but doesn't provide a valid auth code. For real OAuth testing, open the captured URL in a browser. For unit testing, mock the token exchange.
See `src/dev/debug-harness/README.md` for full API reference.
+98 -100
View File
@@ -13,11 +13,55 @@ This file is the secret sauce for working effectively in this codebase. It captu
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
## Miscellaneous
- Avoid provider-specific string matching / hardcoded provider branches when fixing provider/config plumbing. Prefer provider metadata, shared catalog/defaults, explicit protocol/client capabilities, or centralized normalization utilities that apply by data shape rather than `providerId === "..."`. If a provider exception seems necessary, stop and explain why instead of adding ad-hoc string matching.
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
- When creating PRs, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
- Additional instructions about making requests: @.clinerules/network.md
## Searching the Codebase — Avoiding Build Output
Several directories contain build output or generated code that produces
noisy or unusable results with `search_files` / `grep`:
| Directory | What it is | Why it's a problem |
|-----------|-----------|-------------------|
| `out/` | esbuild bundle output | Mirrors `src/` structure as minified JS — every search gets duplicate hits on single-line files |
| `dist/` | Packaged extension | Entire extension bundled into one minified `extension.js` (~1 long line) |
| `dist-standalone/` | Standalone build output | Same minification issue |
| `src/generated/` | Generated protobuf code | Auto-generated from `proto/`; not the source of truth |
| `src/shared/proto/` | Generated proto type defs | Auto-generated from `proto/`; not the source of truth |
| `node_modules/` | Dependencies | Huge, not project source |
### How to skip build output
**`search_files`** — Point at `src/` (not the project root) and use `file_pattern`:
```
search_files(path="src/core", regex="myFunction", file_pattern="*.ts")
```
The `file_pattern` parameter is the most effective filter — e.g. `"*.ts"`,
`"*.tsx"`, `"*.proto"`.
**`grep` directly** — Exclude build dirs and restrict to source extensions:
```bash
grep -rn "myFunction" src/ --include="*.ts" --exclude-dir={out,dist,node_modules,generated}
```
### When you must search minified files
Sometimes you need to verify what got bundled (e.g., checking if a change
made it into the build). Minified files are typically one long line, so
normal `grep` shows the entire file as context. Use these approaches:
- **`grep -oP`** to extract just the match with limited surrounding context:
```bash
grep -oP '.{0,40}myFunction.{0,40}' dist/extension.js
```
- **`read_file`** on files in `out/src/` — these have source maps and are
more readable than `dist/extension.js` (which is the fully bundled output).
- **Source maps** — `out/src/*.js.map` and `dist/extension.js.map` can be
used to trace minified output back to original source locations.
## gRPC/Protobuf Communication
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
@@ -48,104 +92,15 @@ The extension and webview communicate via gRPC-like protocol over VS Code messag
- `src/core/controller/task/explainChanges.ts` - Handler implementation
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
## Adding a New API Provider
When adding a new provider (e.g., "openai-codex"), you must update the proto conversion layer in THREE places or the provider will silently reset to Anthropic:
1. `proto/cline/models.proto` - Add to the `ApiProvider` enum (e.g., `OPENAI_CODEX = 40;`)
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts` - Add case mapping string to proto enum
3. `convertProtoToApiProvider()` in the same file - Add case mapping proto enum back to string
**Why this matters:** Without these, the provider string hits the `default` case and returns `ANTHROPIC`. The webview, provider list, and handler all work fine, but the state silently resets when it round-trips through proto serialization. No error is thrown.
**Other files to update when adding a provider:**
- `src/shared/api.ts` - Add to `ApiProvider` union type, define models
- `src/shared/providers/providers.json` - Add to provider list for dropdown
- `src/core/api/index.ts` - Register handler in `createHandlerForProvider()`
- `webview-ui/src/components/settings/utils/providerUtils.ts` - Add cases in `getModelsForProvider()` and `normalizeApiConfiguration()`
- `webview-ui/src/utils/validate.ts` - Add validation case
- `webview-ui/src/components/settings/ApiOptions.tsx` - Render provider component
## Responses API Providers (OpenAI Codex, OpenAI Native)
Providers using OpenAI's Responses API require native tool calling. XML tools don't work with the Responses API.
**Symptoms of broken native tool calling:**
- Tools get called multiple times (e.g., `ask_followup_question` asks the same question twice)
- Tool arguments get duplicated or malformed
- The model responds but tools aren't recognized
**Root causes to check:**
1. **Provider missing from `isNextGenModelProvider()`** in `src/utils/model-utils.ts`. The native variant matchers (e.g., `native-gpt-5/config.ts`) call this function. If your provider isn't in the list, the matcher returns false and falls back to XML tools.
2. **Model missing `apiFormat: ApiFormat.OPENAI_RESPONSES`** in its model info (`src/shared/api.ts`). This property signals that the model requires native tool calling. The task runner in `src/core/task/index.ts` checks this and forces `enableNativeToolCalls: true` regardless of user settings.
**When adding a new Responses API provider:**
1. Add provider to `isNextGenModelProvider()` list in `src/utils/model-utils.ts`
2. Set `apiFormat: ApiFormat.OPENAI_RESPONSES` on all models that use the Responses API
3. The variant matcher and task runner will handle the rest automatically
## Adding Tools to System Prompt
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
1. **Add to `ClineDefaultTool` enum** in `src/shared/tools.ts`
2. **Tool definition** in `src/core/prompts/system-prompt/tools/` (create file like `generate_explanation.ts`)
- Define variants for each `ModelFamily` (generic, next-gen, xs, etc.)
- Export variants array (e.g., `export const my_tool_variants = [GENERIC, NATIVE_NEXT_GEN, XS]`)
- **Fallback behavior**: If a variant isn't defined for a model family, `ClineToolSet.getToolByNameWithFallback()` automatically falls back to GENERIC. So you only need to export `[GENERIC]` unless the tool needs model-specific behavior.
3. **Register in `src/core/prompts/system-prompt/tools/init.ts`** - Import and spread into `allToolVariants`
4. **Add to variant configs** - Each model family has its own config in `src/core/prompts/system-prompt/variants/*/config.ts`. Add your tool's enum to the `.tools()` list:
- `generic/config.ts`, `next-gen/config.ts`, `gpt-5/config.ts`, `native-gpt-5/config.ts`, `native-gpt-5-1/config.ts`, `native-next-gen/config.ts`, `gemini-3/config.ts`, `glm/config.ts`, `hermes/config.ts`, `xs/config.ts`
- **Important**: If you add to a variant's config, make sure the tool spec exports a variant for that ModelFamily (or relies on GENERIC fallback)
5. **Create handler** in `src/core/task/tools/handlers/`
6. **Wire up in `ToolExecutor.ts`** if needed for execution flow
7. **Add to tool parsing** in `src/core/assistant-message/index.ts` if needed
8. **If tool has UI feedback**: add `ClineSay` enum in proto, update `src/shared/ExtensionMessage.ts`, update `src/shared/proto-conversions/cline-message.ts`, update `webview-ui/src/components/chat/ChatRow.tsx`
## Modifying System Prompt
**Read these first:** `src/core/prompts/system-prompt/README.md`, `tools/README.md`, `__tests__/README.md`
System prompt is modular: **components** (reusable sections) + **variants** (model-specific configs) + **templates** (with `{{PLACEHOLDER}}` resolution).
**Key directories:**
- `components/` - Shared sections: `rules.ts`, `capabilities.ts`, `editing_files.ts`, etc.
- `variants/` - Model-specific: `generic/`, `next-gen/`, `xs/`, `gpt-5/`, `gemini-3/`, `hermes/`, `glm/`, etc.
- `templates/` - Template engine and placeholder definitions
**Variant tiers (ask user which to modify):**
- **Next-gen** (Claude 4, GPT-5, Gemini 2.5): `next-gen/`, `native-next-gen/`, `native-gpt-5/`, `native-gpt-5-1/`, `gemini-3/`, `gpt-5/`
- **Standard** (default fallback): `generic/`
- **Local/small models**: `xs/`, `hermes/`, `glm/`
**How overrides work:** Variants can override components via `componentOverrides` in their `config.ts`, or provide a custom template in `template.ts` (e.g., `next-gen/template.ts` exports `rules_template`). If no override, the shared component from `components/` is used.
**Example: Adding a rule to RULES section**
1. Check if variant overrides rules: look for `rules_template` in `variants/*/template.ts` or `componentOverrides.RULES` in `config.ts`
2. If shared: modify `components/rules.ts`
3. If overridden: modify that variant's template
4. XS variant is special—has heavily condensed inline content in `template.ts`
**After any changes, regenerate snapshots:**
```bash
UPDATE_SNAPSHOTS=true npm run test:unit
```
Snapshots live in `__tests__/__snapshots__/`. Tests validate across model families and context variations (browser, MCP, focus chain).
## Modifying Default Slash Commands
Three places need updates:
- `src/core/slash-commands/index.ts` - Command definitions
- `src/core/prompts/commands.ts` - System prompt integration
- `webview-ui/src/utils/slash-commands.ts` - Webview autocomplete
## Adding New Global State Keys
Adding a new key to global state requires updates in multiple places. Missing any step causes silent failures.
Required steps:
1. Type definition in `src/shared/storage/state-keys.ts` - Add to `GlobalState` or `Settings` interface
2. Read from globalState in `src/core/storage/utils/state-helpers.ts`:
- Add `const myKey = context.globalState.get<GlobalStateAndSettings["myKey"]>("myKey")` in `readGlobalStateFromDisk()`
- Add to the return object: `myKey: myKey ?? defaultValue,`
3. StateManager handles read/write via `setGlobalState()`/`getGlobalStateKey()` after initialization
2. Add any default value or transform in `src/shared/storage/state-keys.ts` if the key needs one
3. Read and write the value through `StateManager` (`setGlobalState()` / `getGlobalStateKey()`) after initialization
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
Persistent state is file-backed through `StateManager`; do not add new runtime reads or writes against VS Code `ExtensionContext` storage. That storage is only a legacy migration source.
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
- `src/core/controller/state/updateSettings.ts` for webview `updateSetting(...)`
@@ -159,22 +114,20 @@ Webview toggle gotcha: settings changes must also round-trip back in state paylo
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
## StateManager Cache vs Direct globalState Access
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
StateManager uses an in-memory cache populated during `StateManager.initialize()` from file-backed storage. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
Exception: State needed immediately at extension startup (before cache is ready)
Exception: host migration code may read legacy VS Code storage before file-backed storage is initialized.
When Window A sets state and immediately opens Window B, the new window's StateManager cache is populated from `context.globalState` during initialization. If you need to read state in Window B right at startup (e.g., in `common.ts` during `initialize()`), read directly from `context.globalState.get()` instead of StateManager's cache.
Example pattern (see `lastShownAnnouncementId` and `worktreeAutoOpenPath`):
Example pattern:
```typescript
// Writing (normal pattern)
controller.stateManager.setGlobalState("myKey", value)
// Reading at startup in common.ts (bypass cache)
const value = context.globalState.get<string>("myKey")
// Reading after initialization
const value = controller.stateManager.getGlobalStateKey("myKey")
```
This is only needed for cross-window state read during the brief startup window before StateManager cache is fully usable. Normal state access after initialization should use StateManager.
Use `context.globalState` only in VS Code migration code that copies legacy ExtensionContext values into the shared file-backed stores.
## ChatRow Cancelled/Interrupted States
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
@@ -203,3 +156,48 @@ const isGenerating = explanationInfo.status === "generating" && !wasCancelled
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
## Debug Harness: clear inherited VSCode/Electron env vars before launching
The debug harness (`apps/vscode/src/dev/debug-harness/server.ts`) launches a child
VSCode via Playwright's `_electron.launch({ env: { ...process.env, ... } })`. If you
run the harness from a process that was itself spawned by VSCode (e.g. the Cline
extension host, an integrated terminal, or an agent running inside VSCode), the
parent's VSCode/Electron env vars leak into the child and break the launch.
The fatal one is **`ELECTRON_RUN_AS_NODE=1`**: it makes the child VSCode binary run
as plain Node, so it rejects every VSCode CLI flag. Symptom:
```
.../Visual Studio Code.app/Contents/MacOS/Code: bad option: --extensionDevelopmentPath=...
Error: Process failed to launch! (Playwright _electron.launch)
```
This is NOT the macOS Playwright flakiness mentioned in the harness README — it's
env inheritance. Fix: strip the inherited vars before starting the harness:
```bash
env -u ELECTRON_RUN_AS_NODE -u ELECTRON_NO_ATTACH_CONSOLE \
-u VSCODE_CLI -u VSCODE_CODE_CACHE_PATH -u VSCODE_CRASH_REPORTER_PROCESS_TYPE \
-u VSCODE_CWD -u VSCODE_ESM_ENTRYPOINT -u VSCODE_HANDLES_UNCAUGHT_ERRORS \
-u VSCODE_IPC_HOOK -u VSCODE_NLS_CONFIG -u VSCODE_PID -u VSCODE_L10N_BUNDLE_LOCATION \
npx tsx src/dev/debug-harness/server.ts --auto-launch --skip-build
```
Check your own env with `env | grep -iE 'electron|vscode_'` first; `ELECTRON_RUN_AS_NODE=1`
present means you must scrub before launching.
Other harness notes confirmed in practice:
- The extension host is **ESM** (`VSCODE_ESM_ENTRYPOINT`), so `ext.evaluate` has no
`require` and module-internal functions aren't reachable as globals. To inspect
internal builders (e.g. `buildBedrockProviderConfig`), set a breakpoint with
`ext.set_breakpoint` and read locals via `ext.evaluate` with the paused `callFrameId`
— don't try to `require()` the bundle.
- `web.evaluate` wraps the expression as a single returned expression; multi-statement
snippets must be an IIFE `(() => { ...; return x; })()`, otherwise you get
`SyntaxError: Unexpected token ';'`.
- Webview settings inputs are `vscode-text-field` web components with debounced React
onChange. Setting `.value` + dispatching events via `web.evaluate` is unreliable for
some fields; focus the inner shadow `input` then use real keystrokes (`ui.type` +
`ui.press Tab`, or click the dropdown option) to make the value persist.
+26
View File
@@ -0,0 +1,26 @@
# SDK Adapter
The VSCode extension runs on the Cline SDK (`@cline/core`, `@cline/llms`,
`@cline/shared`) through an adapter layer in `apps/vscode/src/sdk/`. The
webview still talks gRPC; the adapter translates between gRPC handlers and SDK
calls. See `apps/vscode/src/dev/debug-harness/README.md` for the debug harness.
## Conventions
1. **Look up SDK APIs, don't guess.** Use `kb_search(name="sdk", query="...")`
before implementing against an SDK surface.
2. **Reference the pre-SDK implementation when replacing a module.** Add a
`// Replaces classic src/core/... (see origin/main)` header and use
`kb_search(name="cline", commit="origin/main")` or
`git show origin/main:path` to consult the prior implementation.
3. **Single entry point.** There is one codepath — the SDK adapter. No
`CLINE_SDK` env flag.
4. **Use `{appBaseUrl}`**, never hardcode `app.cline.bot`.
5. **Avoid `as` casts.** Use explicit conversion functions with tests. The
branded types in `apps/vscode/src/sdk/model-catalog/contracts.ts` exist so
casts are unnecessary outside parse/compute boundaries.
## Debug harness
- **Dismiss the Kanban/promo overlay** before any debug harness interaction.
- **Use the command palette** to navigate tabs in the debug harness.
+1 -1
View File
@@ -44,7 +44,7 @@ For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/mod
Modular: `components/` (shared) + `variants/` (model-specific) + `templates/` (`{{PLACEHOLDER}}`). Variants override components via `componentOverrides` in `config.ts` or custom `template.ts`. XS variant is heavily condensed inline. Always regenerate snapshots after changes.
## Global State Keys (silent failure risk)
Adding a key requires: type in `src/shared/storage/state-keys.ts`, read via `context.globalState.get()` in `src/core/storage/utils/state-helpers.ts` `readGlobalStateFromDisk()`, and add to return object. Missing the `.get()` call compiles fine but value is always `undefined`.
Adding a key requires updating the typed storage definitions in `src/shared/storage/state-keys.ts`; runtime reads and writes should go through `StateManager`, not VS Code `ExtensionContext` storage. Persistent state is file-backed so it works across VS Code, CLI, and JetBrains hosts.
## Slash Commands (3 places)
- `src/core/slash-commands/index.ts` — definitions.
+7 -7
View File
@@ -33,7 +33,7 @@ permissions:
defaults:
run:
working-directory: sdk
working-directory: .
jobs:
publish-main:
@@ -105,12 +105,12 @@ jobs:
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}"
echo "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}"
echo "apps/cli/package.json has invalid version: ${VERSION}"
exit 1
fi
@@ -147,7 +147,7 @@ jobs:
- name: Build platform binaries
run: bun script/build.ts --install-native-variants --skip-sdk-build
working-directory: sdk/apps/cli
working-directory: apps/cli
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
@@ -194,7 +194,7 @@ jobs:
env:
NPM_CONFIG_PROVENANCE: "true"
run: bun script/publish-npm.ts --tag latest
working-directory: sdk/apps/cli
working-directory: apps/cli
- name: Get Previous CLI Tag
id: prev_tag
@@ -375,7 +375,7 @@ jobs:
- 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
working-directory: apps/cli
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
@@ -424,7 +424,7 @@ jobs:
env:
NPM_CONFIG_PROVENANCE: "true"
run: bun script/publish-npm.ts --tag nightly
working-directory: sdk/apps/cli
working-directory: apps/cli
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
@@ -31,6 +31,12 @@ jobs:
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
# The VS Code extension's package.json and lockfiles live under apps/vscode/
# (the repo root has no package.json). Mirror ext-vscode-test.yml so install
# and publish steps run in the correct workspace.
defaults:
run:
working-directory: apps/vscode
steps:
- name: Checkout selected branch
@@ -53,12 +59,18 @@ jobs:
# 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
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
- name: Install root dependencies
run: npm ci --include=optional
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci --include=optional
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci --include=optional
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
@@ -114,11 +114,13 @@ jobs:
with:
node-version: 22
- name: Install root dependencies
run: npm install --include=optional
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode install --include=optional
- name: Install webview-ui dependencies
run: cd webview-ui && npm install --include=optional
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui install --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
+5 -3
View File
@@ -128,13 +128,15 @@ jobs:
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install root dependencies
run: npm ci
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
- name: Install vsce
run: npm install -g @vscode/vsce
+23 -10
View File
@@ -91,13 +91,15 @@ jobs:
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
- name: Install root dependencies
run: npm ci
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci --include=optional
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci --include=optional
- name: Run Quality Checks (Parallel)
run: npm run ci:check-all
@@ -130,16 +132,19 @@ jobs:
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
- name: Install root dependencies
run: npm ci
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci --include=optional
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci --include=optional
- name: Set up NPM on Windows
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
run: |
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
@@ -155,6 +160,11 @@ jobs:
id: build_step
run: npm run ci:build
- name: Vitest Suites (SDK adapter + model catalog)
id: vitest_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: npm run test:vitest
- name: Unit Tests with coverage - Linux
id: unit_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
@@ -229,13 +239,15 @@ jobs:
apps/vscode/webview-ui/package-lock.json
apps/vscode/testing-platform/package-lock.json
- name: Install root dependencies
run: npm ci
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci --include=optional
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci --include=optional
- name: Download ripgrep binaries
run: npm run download-ripgrep
@@ -244,7 +256,8 @@ jobs:
run: npm run compile-standalone
- name: Install testing platform dependencies
run: cd testing-platform && npm ci
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/testing-platform ci --include=optional
- name: Running testing platform integration spec tests
timeout-minutes: 7
+9 -9
View File
@@ -26,7 +26,7 @@ on:
defaults:
run:
working-directory: sdk
working-directory: .
jobs:
test:
@@ -148,7 +148,7 @@ jobs:
env:
CHANNEL: ${{ steps.channel.outputs.channel }}
run: |
BASE_VERSION=$(node -p "require('./packages/llms/package.json').version")
BASE_VERSION=$(node -p "require('./sdk/packages/llms/package.json').version")
if [ "$CHANNEL" = "nightly" ]; then
TIMESTAMP=$(date +%s)
@@ -166,11 +166,11 @@ jobs:
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: bun scripts/version.ts "$VERSION"
run: bun sdk/scripts/version.ts "$VERSION"
- name: Verify publishability
if: steps.check_commits.outputs.skip != 'true'
run: bun scripts/check-publish.ts
run: bun sdk/scripts/check-publish.ts
- name: Prepare package tarball directory
if: steps.check_commits.outputs.skip != 'true'
@@ -187,7 +187,7 @@ jobs:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/shared@${VERSION} with tag '${CHANNEL}'..."
cd packages/shared
cd sdk/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
@@ -199,7 +199,7 @@ jobs:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/llms@${VERSION} with tag '${CHANNEL}'..."
cd packages/llms
cd sdk/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
@@ -211,7 +211,7 @@ jobs:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/agents@${VERSION} with tag '${CHANNEL}'..."
cd packages/agents
cd sdk/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
@@ -223,7 +223,7 @@ jobs:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/core@${VERSION} with tag '${CHANNEL}'..."
cd packages/core
cd sdk/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
@@ -235,7 +235,7 @@ jobs:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/sdk@${VERSION} with tag '${CHANNEL}'..."
cd packages/sdk
cd sdk/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
+4 -4
View File
@@ -21,7 +21,7 @@ permissions:
defaults:
run:
working-directory: sdk
working-directory: .
jobs:
quality-checks:
@@ -96,12 +96,12 @@ jobs:
- 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
run: bun -F './sdk/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
run: bun sdk/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' }}
@@ -109,4 +109,4 @@ jobs:
- 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
run: bun sdk/scripts/check-publish.ts
+14
View File
@@ -13,6 +13,9 @@ tmp
pnpm-lock.yaml
.clineignore
.cline/enterprise
.cline/remote-config
**/.cline/remote-config
.venv
.actrc
CLAUDE.local.md
@@ -61,6 +64,17 @@ tests/**/cache
# Should never be committed: only exists if a publish aborts mid-swap.
.README.github.bak
# Tauri generated code
apps/*/src-tauri/gen
apps/*/src-tauri/bin
apps/examples/*/src-tauri/gen
apps/examples/*/src-tauri/bin
# Tauri UI test snapshots
apps/*/src/tests/.tui-test
apps/*/src/tests/tui-traces
apps/vscode/webview-ui/src/**/*.js
apps/vscode/webview-ui/src/**/*.js.map
# SDK Session files / User data
.cline/data
@@ -6,15 +6,18 @@
{
"id": "sdk-tool-handler-telemetry",
"rule": "Any new tool handler added to packages/agents/src or packages/core/src that performs a user-visible action (writes files, executes commands, modifies state, calls external APIs) must include a call to captureToolUsage() from packages/core/src/services/telemetry/core-events.ts, or emit a task.tool_used event via telemetry.capture(). Pure read-only helpers and getters are exempt. When in doubt, prefer instrumentation.",
"scope": ["packages/agents/src/**", "packages/core/src/**"],
"scope": [
"sdk/packages/agents/src/**",
"sdk/packages/core/src/**"
],
"severity": "high"
},
{
"id": "sdk-session-lifecycle-telemetry",
"rule": "New session start, end, or state-transition code paths in packages/core/src must call the appropriate typed helper from packages/core/src/services/telemetry/core-events.ts (captureTaskCreated, captureTaskCompleted, captureConversationTurnEvent, captureTokenUsage, etc.). Do not inline raw telemetry.capture() calls for session lifecycle events — always use the typed helper, which guarantees a consistent payload shape.",
"scope": [
"packages/core/src/cline-core/**",
"packages/core/src/runtime/**"
"sdk/packages/core/src/cline-core/**",
"sdk/packages/core/src/runtime/**"
],
"severity": "high"
},
@@ -22,8 +25,8 @@
"id": "sdk-no-raw-event-strings",
"rule": "All telemetry event name strings must be sourced from CORE_TELEMETRY_EVENTS in packages/core/src/services/telemetry/core-events.ts. If a PR introduces a string literal in a telemetry.capture(), telemetry.captureRequired(), or recordCounter()/recordHistogram()/recordGauge() call that does not reference CORE_TELEMETRY_EVENTS, flag it. New events must be added to CORE_TELEMETRY_EVENTS first, with a typed capture helper created alongside them.",
"scope": [
"packages/core/src/**",
"packages/agents/src/**",
"sdk/packages/core/src/**",
"sdk/packages/agents/src/**",
"apps/cli/src/**",
"apps/vscode/src/**"
],
@@ -32,13 +35,17 @@
{
"id": "sdk-auth-telemetry-completeness",
"rule": "Any new OAuth or authentication provider added under packages/core/src/auth must emit all four lifecycle events using the typed helpers from core-events.ts: captureAuthStarted (at flow entry), captureAuthSucceeded + identifyAccount (on token success), captureAuthFailed (on error), and captureAuthLoggedOut (on token invalidation or explicit logout). Flag PRs that introduce a new auth flow file without all four. Cross-reference packages/core/src/auth/cline.ts and packages/core/src/auth/codex.ts as canonical examples.",
"scope": ["packages/core/src/auth/**"],
"scope": [
"sdk/packages/core/src/auth/**"
],
"severity": "high"
},
{
"id": "sdk-telemetry-doc-update",
"rule": "Any PR that adds new event constants to CORE_TELEMETRY_EVENTS in packages/core/src/services/telemetry/core-events.ts, adds new capture* helper functions, or changes the payload shape of an existing event must update the Event Catalog section in DOC.md. Flag PRs that modify core-events.ts without a corresponding change to DOC.md.",
"scope": ["packages/core/src/services/telemetry/core-events.ts"],
"scope": [
"sdk/packages/core/src/services/telemetry/core-events.ts"
],
"severity": "medium"
}
]
@@ -1,19 +1,19 @@
{
"files": [
{
"path": "packages/core/src/services/telemetry/core-events.ts",
"path": "sdk/packages/core/src/services/telemetry/core-events.ts",
"description": "Single source of truth for all telemetry event names (CORE_TELEMETRY_EVENTS) and their typed capture helper functions. Every PR touching telemetry must be evaluated against this catalog. New events must be defined here first."
},
{
"path": "packages/shared/src/services/telemetry.ts",
"path": "sdk/packages/shared/src/services/telemetry.ts",
"description": "ITelemetryService interface definition. Defines the contract all telemetry implementations must satisfy (capture, captureRequired, recordCounter, recordHistogram, recordGauge, flush, dispose)."
},
{
"path": "packages/core/src/services/telemetry/TelemetryService.ts",
"path": "sdk/packages/core/src/services/telemetry/TelemetryService.ts",
"description": "Reference implementation of ITelemetryService used by all hosts. Multi-adapter fan-out service that forwards events to OpenTelemetry."
},
{
"path": "packages/core/src/services/telemetry/OpenTelemetryProvider.ts",
"path": "sdk/packages/core/src/services/telemetry/OpenTelemetryProvider.ts",
"description": "OpenTelemetry-backed provider that wires logs/metrics/traces exporters. Contains createConfiguredTelemetryService and createConfiguredTelemetryHandle, the canonical factories every host should use."
},
{
@@ -21,11 +21,11 @@
"description": "Public API and event documentation. The Event Catalog and 'Activation funnel' sections must be kept in sync with core-events.ts. Host integration rules (CLI dir ordering, hub daemon metadata forwarding) are documented here."
},
{
"path": "ARCHITECTURE.md",
"path": "sdk/ARCHITECTURE.md",
"description": "Architecture reference. Telemetry design decisions, completion semantics (submit_and_exit anchoring), and hub-daemon telemetry forwarding are documented here. Use as ground truth for design intent."
},
{
"path": "AGENTS.md",
"path": "sdk/AGENTS.md",
"description": "Package boundary rules. Telemetry runtime services live in @cline/core; @cline/agents must not own stateful telemetry. Use to evaluate whether a telemetry change is being made in the correct package."
}
]
@@ -10,9 +10,9 @@ The SDK uses OpenTelemetry (OTEL) as its sole telemetry transport. Events flow t
```
core-events.ts (event catalog + typed helpers)
ITelemetryService (packages/shared) ← interface contract
ITelemetryService (sdk/packages/shared) ← interface contract
TelemetryService (packages/core) ← multi-adapter fan-out
TelemetryService (sdk/packages/core) ← multi-adapter fan-out
OpenTelemetryAdapter → OpenTelemetryProvider ← OTLP transport
@@ -24,7 +24,7 @@ parallel-but-independent stacks; this `.greptile/` config covers only the SDK.
## The Single Source of Truth
`packages/core/src/services/telemetry/core-events.ts` is the single source of truth for all
`sdk/packages/core/src/services/telemetry/core-events.ts` is the single source of truth for all
event names. It exports:
- `CORE_TELEMETRY_EVENTS` — a frozen const object grouped by family
@@ -60,8 +60,8 @@ Emission ownership:
emitter in `prepareLocalRuntimeBootstrap`. Hosts must NOT re-emit these.
- `workspace.path_resolved`: emitted from default tool executors **only when**
`WorkspaceManager` exposes more than one root.
- `task.*`: emitted by core session lifecycle code in `packages/core/src/cline-core/` and
`packages/core/src/runtime/`. Hosts must not duplicate this emission.
- `task.*`: emitted by core session lifecycle code in `sdk/packages/core/src/cline-core/` and
`sdk/packages/core/src/runtime/`. Hosts must not duplicate this emission.
## `task.completed` Semantics
@@ -105,7 +105,7 @@ forwarding, hub-backed sessions silently drop their lifecycle telemetry.
## Auth Lifecycle Completeness
Every authentication provider in `packages/core/src/auth/` must emit all four auth lifecycle
Every authentication provider in `sdk/packages/core/src/auth/` must emit all four auth lifecycle
events using the typed helpers:
| Phase | Helper | Where it fires |
@@ -115,7 +115,7 @@ events using the typed helpers:
| Token error | `captureAuthFailed(provider, errorMessage)` | In the catch block |
| Token invalidation | `captureAuthLoggedOut(provider, reason)` | On invalid_grant or explicit logout |
Cross-reference `packages/core/src/auth/cline.ts` and `packages/core/src/auth/codex.ts` as
Cross-reference `sdk/packages/core/src/auth/cline.ts` and `sdk/packages/core/src/auth/codex.ts` as
canonical examples of all four phases.
## Single Telemetry Service Per Host
+11 -1
View File
@@ -1 +1,11 @@
lint-staged
if ! command -v gitleaks >/dev/null 2>&1; then
echo "gitleaks is required for the pre-commit secret scan."
echo "Install it with: brew install gitleaks"
echo "Other install options: https://github.com/gitleaks/gitleaks#installing"
exit 1
fi
gitleaks git --pre-commit --redact --staged --verbose || exit 1
cd apps/vscode && lint-staged
+22 -21
View File
@@ -5,8 +5,8 @@
"tasks": [
{
"label": "compile-standalone",
"type": "npm",
"script": "compile-standalone",
"type": "shell",
"command": "npm run compile-standalone",
"group": "build",
"problemMatcher": [],
"presentation": {
@@ -18,8 +18,8 @@
},
{
"label": "npm: protos",
"type": "npm",
"script": "protos",
"type": "shell",
"command": "npm run protos",
"problemMatcher": [],
"isBackground": false,
"presentation": {
@@ -64,8 +64,8 @@
"group": "build"
},
{
"type": "npm",
"script": "build:webview",
"type": "shell",
"command": "npm run build:webview",
"group": "build",
"problemMatcher": [],
"isBackground": true,
@@ -85,8 +85,8 @@
}
},
{
"type": "npm",
"script": "build:webview:test",
"type": "shell",
"command": "npm run build:webview:test",
"group": "build",
"problemMatcher": [],
"isBackground": true,
@@ -107,8 +107,8 @@
}
},
{
"type": "npm",
"script": "dev:webview",
"type": "shell",
"command": "npm run dev:webview",
"group": "build",
"problemMatcher": [
{
@@ -144,8 +144,8 @@
}
},
{
"type": "npm",
"script": "watch:esbuild",
"type": "shell",
"command": "npm run watch:esbuild",
"group": "build",
"problemMatcher": {
"pattern": [
@@ -183,8 +183,8 @@
}
},
{
"type": "npm",
"script": "watch:esbuild:test",
"type": "shell",
"command": "npm run watch:esbuild:test",
"group": "build",
"problemMatcher": {
"pattern": [
@@ -223,8 +223,8 @@
}
},
{
"type": "npm",
"script": "watch:tsc",
"type": "shell",
"command": "npm run watch:tsc",
"group": "build",
"problemMatcher": "$tsc-watch",
"isBackground": true,
@@ -241,8 +241,9 @@
}
},
{
"type": "npm",
"script": "watch-tests",
"type": "shell",
"command": "npm run watch-tests",
"label": "npm: watch-tests",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"dependsOn": [
@@ -280,8 +281,8 @@
"command": "rm -rf ${workspaceFolder}/apps/vscode/dist/tmp/user && mkdir -p ${workspaceFolder}/apps/vscode/dist/tmp/user"
},
{
"type": "npm",
"script": "storybook",
"type": "shell",
"command": "npm run storybook",
"group": "build",
"problemMatcher": [],
"isBackground": false,
@@ -308,7 +309,7 @@
"$tsc"
],
"options": {
"cwd": "${workspaceFolder}/sdk"
"cwd": "${workspaceFolder}"
}
}
],
+10
View File
@@ -1,5 +1,15 @@
# Changelog
## [3.87.0]
### Added
- Add MiniMax M3 model support.
### Fixed
- Update VS Code extension dependencies to resolve security issues in `@xmldom/xmldom`, `basic-ftp`, `axios`, `undici`, and other direct/transitive packages.
## [3.86.2]
### Fixed
-1
View File
@@ -1,2 +1 @@
@.clinerules/general.md
@.clinerules/network.md
+7 -3
View File
@@ -51,7 +51,7 @@ for CI/CD and scripting.
npm i -g cline
```
<a href="./sdk/apps/cli/README.md">Learn more</a>
<a href="./apps/cli/README.md">Learn more</a>
<br><br>
</td>
@@ -129,7 +129,7 @@ npm install @cline/sdk
| 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) |
| **CLI** | Terminal UI, headless mode, shell commands, and CLI-specific flows. | [`apps/cli/`](https://github.com/cline/cline/tree/main/apps/cli) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/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) |
@@ -212,8 +212,12 @@ cline schedule create "PR summary" \
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.
```bash
# Connect to Telegram
cline connect telegram -k $BOT_TOKEN
cline connect slack --token $SLACK_TOKEN --signing-secret $SECRET --base-url $URL
# Connect to Slack through webhook
cline connect slack --bot-token $SLACK_TOKEN --signing-secret $SECRET --base-url $URL
# Connect to Slack using socket mode
cline connect slack --bot-token $SLACK_TOKEN --app-token $SLACK_APP_TOKEN
```
## Headless CLI for CI/CD
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.5/schema.json",
"root": false,
"extends": ["../sdk/biome.json"],
"linter": {
"rules": {
"a11y": {
"noStaticElementInteractions": "warn"
}
}
}
}
@@ -1,5 +1,38 @@
# Cline CLI Changelog
## 3.0.20
- Installed plugin wrappers are now named from their source (npm package name, git repo, remote filename, official slug, or local directory) instead of an opaque hash, making installed plugins easier to identify.
## 3.0.19
- Fixed CLI auto-update to use `npm update` so updates apply reliably, while preserving the installed release channel (e.g. nightly).
## 3.0.18
- Fix Slack channel mentions so replies post in the original message's thread.
- Fix the abort indicator to clear immediately when a task is cancelled.
- Sync the Fireworks AI model registry and refresh the bundled model catalog with current platform offerings.
- Bump the bundled SDK to v0.0.43, which forces a running Cline Hub to restart so it picks up the latest SDK code.
## 3.0.17
- Fix a regression introduced in 3.0.15 where the interactive CLI could get stuck after stopping and restarting Cline Hub and then pressing Escape to cancel a request. The CLI now detects stale or missing sessions, recovers any pending messages, and starts a fresh session instead of failing with "session not found".
- Fix Ctrl+C and Hub shutdown races that surfaced as "hook dispatch failed" and WebSocket connection errors from late hook events racing against Hub shutdown.
- Fix the Hub daemon being shut down prematurely when a runtime request was aborted, so the daemon now stays alive.
- Improve the Telegram connector with a new `--allowed-user-id` flag to restrict which Telegram users are authorized to interact with the agent.
## 3.0.16
- Install official Cline plugins by slug off the new github.com/cline/plugins collection.
- Uninstall plugins using `cline plugin uninstall <plugin>` or in the TUI.
- Plugins can now bundle skills, and plugin skills are grouped together in settings.
- Add Slack socket mode support.
- Allow a custom base URL for Anthropic vendor-type providers.
- Fix OAuth token migration for users signed in through the old extension.
- Use a union schema for read-files tool input validation.
- Add a `CLINE_PLUGIN_IMPORT_TIMEOUT_MS` env override to control the plugin import timeout.
## 3.0.15
- Add Cline Hub, a web app for monitoring connected clients, viewing and driving sessions, streaming assistant output, and restarting the local hub, with local, LAN, and tunnel usage gated by a room secret.
@@ -416,7 +416,7 @@ Then attach VS Code or Chrome DevTools to `ws://127.0.0.1:6499`.
## Publishing
The CLI is published as the `cline` wrapper package on npm with platform-specific binaries under `@cline/cli-*`. The release flow lives in the `publish-cli` skill (`sdk/apps/cli/.cline/skills/publish-cli/SKILL.md`).
The CLI is published as the `cline` wrapper package on npm with platform-specific binaries under `@cline/cli-*`. The release flow lives in the `publish-cli` skill (`.cline/skills/publish-cli/SKILL.md` at the repo root).
From the `apps/cli` workspace:
@@ -174,6 +174,9 @@ cline connect telegram -k 123456:ABCDEF...
# Slack (webhook mode)
cline connect slack --bot-token $SLACK_BOT_TOKEN --signing-secret $SLACK_SIGNING_SECRET --base-url https://your-domain.com
# Slack (socket mode)
cline connect slack --bot-token $SLACK_BOT_TOKEN --app-token $SLACK_APP_TOKEN
# Google Chat (webhook mode)
cline connect gchat --base-url https://your-domain.com
+60 -3
View File
@@ -1,12 +1,65 @@
import { copyFileSync, mkdirSync } from "node:fs";
import {
copyFileSync,
cpSync,
existsSync,
mkdirSync,
readdirSync,
statSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { $ } from "bun";
function defineProcessEnv(name: string): string {
return JSON.stringify(process.env[name] ?? "");
}
const sourcemap = Bun.env.CLINE_SOURCEMAPS === "1" ? "linked" : "none";
const rootDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(rootDir, "../../");
const hubWebviewSourcePath = join(repoRoot, "apps/cline-hub/src/webview");
const hubWebviewDistPath = join(repoRoot, "apps/cline-hub/dist/webview");
const hubWebviewIndexPath = join(hubWebviewDistPath, "index.html");
const cliHubWebviewDistPath = join(rootDir, "dist/cline-hub/webview");
function newestFileMtimeMs(dir: string): number {
let newest = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (
entry.name === "node_modules" ||
entry.name === "dist" ||
entry.name === ".turbo"
) {
continue;
}
const path = join(dir, entry.name);
if (entry.isDirectory()) {
newest = Math.max(newest, newestFileMtimeMs(path));
} else if (entry.isFile()) {
newest = Math.max(newest, statSync(path).mtimeMs);
}
}
return newest;
}
function shouldBuildHubWebview(): boolean {
if (!existsSync(hubWebviewIndexPath)) {
return true;
}
try {
return (
newestFileMtimeMs(hubWebviewSourcePath) >
statSync(hubWebviewIndexPath).mtimeMs
);
} catch {
return true;
}
}
if (shouldBuildHubWebview()) {
console.log("Building Cline Hub webview...");
await $`bun -F @cline/cline-hub build:webview`.cwd(repoRoot);
}
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
@@ -63,10 +116,9 @@ if (result.logs.length > 0) {
}
}
const rootDir = dirname(fileURLToPath(import.meta.url));
const coreBootstrapPath = join(
rootDir,
"../../packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
"../../sdk/packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
);
const cliBootstrapPath = join(
rootDir,
@@ -74,3 +126,8 @@ const cliBootstrapPath = join(
);
mkdirSync(dirname(cliBootstrapPath), { recursive: true });
copyFileSync(coreBootstrapPath, cliBootstrapPath);
if (existsSync(hubWebviewDistPath)) {
mkdirSync(dirname(cliHubWebviewDistPath), { recursive: true });
cpSync(hubWebviewDistPath, cliHubWebviewDistPath, { recursive: true });
}
@@ -1,7 +1,7 @@
{
"name": "@cline/cli",
"displayName": "cline",
"version": "3.0.15",
"version": "3.0.20",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"type": "module",
"publishConfig": {
@@ -10,7 +10,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/cline/cline.git",
"directory": "sdk/apps/cli"
"directory": "apps/cli"
},
"keywords": [
"cline",
@@ -75,6 +75,7 @@
"@chat-adapter/telegram": "^4.23.0",
"@chat-adapter/whatsapp": "^4.23.0",
"@clack/prompts": "^1.2.0",
"@cline/cline-hub": "workspace:*",
"@gramio/format": "^0.7.0",
"@opentui-ui/dialog": "^0.1.2",
"@opentui/core": "0.1.102",
@@ -90,12 +91,15 @@
"react-devtools-core": "^7.0.1",
"react-reconciler": "0.32.0",
"yaml": "^2.8.2",
"nanoid": "^5.1.7",
"zod": "^4.1.11"
},
"devDependencies": {
"@cline/core": "workspace:*",
"@cline/shared": "workspace:*",
"@microsoft/tui-test": "^0.0.2",
"@types/react": "19.2.14"
"@types/react": "19.2.14",
"vitest": "^4.0.18",
"@types/bun": "^1.3.10"
}
}
@@ -1,6 +1,14 @@
#!/usr/bin/env bun
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
import {
cpSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
realpathSync,
statSync,
} from "node:fs";
import { join, relative, resolve } from "node:path";
import { $ } from "bun";
import {
@@ -95,6 +103,48 @@ if (!buildOptions.skipSdkBuild) {
await $`bun -F @cline/cli build`.cwd(rootDir);
}
const hubWebviewSource = join(cliDir, "../cline-hub/src/webview");
const hubWebviewDist = join(cliDir, "../cline-hub/dist/webview");
const hubWebviewIndex = join(hubWebviewDist, "index.html");
function newestFileMtimeMs(dir: string): number {
let newest = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (
entry.name === "node_modules" ||
entry.name === "dist" ||
entry.name === ".turbo"
) {
continue;
}
const path = join(dir, entry.name);
if (entry.isDirectory()) {
newest = Math.max(newest, newestFileMtimeMs(path));
} else if (entry.isFile()) {
newest = Math.max(newest, statSync(path).mtimeMs);
}
}
return newest;
}
function shouldBuildHubWebview(): boolean {
if (!existsSync(hubWebviewIndex)) {
return true;
}
try {
return (
newestFileMtimeMs(hubWebviewSource) > statSync(hubWebviewIndex).mtimeMs
);
} catch {
return true;
}
}
if (shouldBuildHubWebview()) {
console.log("Building Cline Hub webview...");
await $`bun -F @cline/cline-hub build:webview`.cwd(rootDir);
}
const binaries: Record<string, string> = {};
function findOpenTuiParserWorker(): string {
@@ -209,7 +259,7 @@ for (const item of targets) {
// Copy plugin sandbox bootstrap if it exists
const bootstrapSrc = join(
rootDir,
"packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
"sdk/packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
);
if (existsSync(bootstrapSrc)) {
const bootstrapDir = join(cliDir, `dist/${dirName}/extensions`);
@@ -218,6 +268,14 @@ for (const item of targets) {
await Bun.write(join(bootstrapDir, "plugin-sandbox-bootstrap.js"), content);
}
if (existsSync(hubWebviewDist)) {
const hubWebviewDest = join(cliDir, `dist/${dirName}/cline-hub/webview`);
mkdirSync(join(cliDir, `dist/${dirName}/cline-hub`), {
recursive: true,
});
cpSync(hubWebviewDist, hubWebviewDest, { recursive: true });
}
// Generate platform package.json
await Bun.write(
join(cliDir, `dist/${dirName}/package.json`),
@@ -90,7 +90,7 @@ function buildHostSdkDependencies(): Record<string, string> {
for (const pkg of hostSdkPackages) {
dependencies[pkg.name] = readPackageVersion(
pkg.name,
join(cliDir, "../../packages", pkg.directory, "package.json"),
join(cliDir, "../../sdk/packages", pkg.directory, "package.json"),
);
}
return dependencies;
@@ -30,6 +30,18 @@ function resolveCliAgentConfigSearchPaths(cwd: string): string[] {
return [join(cwd, ".cline", "agents"), join(clineDir, "agents")];
}
function createConfigUserInstructionService(cwd: string) {
return createUserInstructionConfigService({
skills: {
workspacePath: cwd,
includePluginSkills: true,
cwd,
},
rules: { workspacePath: cwd },
workflows: { workspacePath: cwd },
});
}
async function runWorkflowsConfigCommand(
cwd: string,
outputMode: CliOutputMode,
@@ -39,11 +51,7 @@ async function runWorkflowsConfigCommand(
string,
{ id: string; name: string; instructions: string; path: string }
>();
const service = createUserInstructionConfigService({
skills: { workspacePath: cwd },
rules: { workspacePath: cwd },
workflows: { workspacePath: cwd },
});
const service = createConfigUserInstructionService(cwd);
try {
await service.start();
for (const record of service.listRecords<WorkflowConfig>("workflow")) {
@@ -90,11 +98,7 @@ async function runRulesConfigCommand(
string,
{ name: string; instructions: string; path: string }
>();
const service = createUserInstructionConfigService({
skills: { workspacePath: cwd },
rules: { workspacePath: cwd },
workflows: { workspacePath: cwd },
});
const service = createConfigUserInstructionService(cwd);
try {
await service.start();
for (const record of service.listRecords<RuleConfig>("rule")) {
@@ -142,11 +146,7 @@ async function runSkillsConfigCommand(
path: string;
}
>();
const service = createUserInstructionConfigService({
skills: { workspacePath: cwd },
rules: { workspacePath: cwd },
workflows: { workspacePath: cwd },
});
const service = createConfigUserInstructionService(cwd);
try {
await service.start();
for (const record of service.listRecords<SkillConfig>("skill")) {
@@ -420,11 +420,7 @@ async function runToolsConfigCommand(
async function loadInteractiveConfigDataForCommand(
cwd: string,
): Promise<Awaited<ReturnType<typeof loadInteractiveConfigData>>> {
const userInstructionService = createUserInstructionConfigService({
skills: { workspacePath: cwd },
rules: { workspacePath: cwd },
workflows: { workspacePath: cwd },
});
const userInstructionService = createConfigUserInstructionService(cwd);
try {
await userInstructionService.start();
return await loadInteractiveConfigData({
+184
View File
@@ -0,0 +1,184 @@
import { mkdirSync, mkdtempSync } from "node:fs";
import { arch, platform, tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { runDashboardCommand, waitForProcessShutdown } from "./dashboard";
const ENV_KEYS = [
"WORKSPACE_ROOT",
"HOST",
"CLINE_HUB_DASHBOARD_PORT",
"PUBLIC_URL",
"ROOM_SECRET",
"CLINE_HUB_WEBVIEW_DIST_DIR",
"CLINE_WRAPPER_PATH",
] as const;
const originalEnv = Object.fromEntries(
ENV_KEYS.map((key) => [key, process.env[key]]),
);
afterEach(() => {
for (const key of ENV_KEYS) {
const value = originalEnv[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
describe("runDashboardCommand", () => {
it("starts the dashboard server, opens the invite URL, and waits for shutdown", async () => {
const output: string[] = [];
const errors: string[] = [];
const opened: string[] = [];
const stop = vi.fn();
let observedEnv:
| {
workspaceRoot: string | undefined;
host: string | undefined;
port: string | undefined;
publicUrl: string | undefined;
roomSecret: string | undefined;
webviewDistDir: string | undefined;
}
| undefined;
const webviewDistDir = mkdtempSync(join(tmpdir(), "cline-webview-dist-"));
mkdirSync(webviewDistDir, { recursive: true });
process.env.CLINE_HUB_WEBVIEW_DIST_DIR = webviewDistDir;
const exitCode = await runDashboardCommand({
cwd: "sdk",
host: "127.0.0.1",
port: "9090",
publicUrl: "http://127.0.0.1:9090",
roomSecret: "secret",
io: {
writeln: (text) => output.push(text ?? ""),
writeErr: (text) => errors.push(text),
},
startServer: async () => {
observedEnv = {
workspaceRoot: process.env.WORKSPACE_ROOT,
host: process.env.HOST,
port: process.env.CLINE_HUB_DASHBOARD_PORT,
publicUrl: process.env.PUBLIC_URL,
roomSecret: process.env.ROOM_SECRET,
webviewDistDir: process.env.CLINE_HUB_WEBVIEW_DIST_DIR,
};
return {
listenUrl: "http://127.0.0.1:9090/",
publicUrl: "http://127.0.0.1:9090",
inviteUrl: "http://127.0.0.1:9090/?roomSecret=secret",
hubUrl: "ws://127.0.0.1:25463/hub",
stop,
};
},
openUrl: async (url) => {
opened.push(url);
},
waitForShutdown: async (server) => {
await server.stop();
},
});
expect(exitCode).toBe(0);
expect(observedEnv).toEqual({
workspaceRoot: resolve("sdk"),
host: "127.0.0.1",
port: "9090",
publicUrl: "http://127.0.0.1:9090",
roomSecret: "secret",
webviewDistDir,
});
expect(opened).toEqual(["http://127.0.0.1:9090/?roomSecret=secret"]);
expect(stop).toHaveBeenCalledTimes(1);
expect(output.join("\n")).toContain("Cline dashboard listening at");
expect(output.join("\n")).toContain("ws://127.0.0.1:25463/hub");
expect(errors).toEqual([]);
expect(process.env.WORKSPACE_ROOT).toBe(originalEnv.WORKSPACE_ROOT);
expect(process.env.CLINE_HUB_WEBVIEW_DIST_DIR).toBe(webviewDistDir);
});
it("honors --no-open behavior", async () => {
const openUrl = vi.fn();
const exitCode = await runDashboardCommand({
openBrowser: false,
io: {
writeln: () => {},
writeErr: () => {},
},
startServer: async () => ({
listenUrl: "http://127.0.0.1:8787/",
publicUrl: "http://127.0.0.1:8787",
inviteUrl: "http://127.0.0.1:8787",
stop: vi.fn(),
}),
openUrl,
waitForShutdown: async () => {},
});
expect(exitCode).toBe(0);
expect(openUrl).not.toHaveBeenCalled();
});
it("finds webview assets from the published wrapper package layout", async () => {
const root = mkdtempSync(join(tmpdir(), "cline-wrapper-layout-"));
const wrapperPath = join(root, "node_modules", "cline", "bin", "cline");
const platformName = platform() === "win32" ? "windows" : platform();
const webviewDistDir = join(
root,
"node_modules",
"cline",
"node_modules",
"@cline",
`cli-${platformName}-${arch()}`,
"cline-hub",
"webview",
);
mkdirSync(join(wrapperPath, ".."), { recursive: true });
mkdirSync(webviewDistDir, { recursive: true });
process.env.CLINE_WRAPPER_PATH = wrapperPath;
delete process.env.CLINE_HUB_WEBVIEW_DIST_DIR;
let observedWebviewDistDir: string | undefined;
const exitCode = await runDashboardCommand({
openBrowser: false,
io: {
writeln: () => {},
writeErr: () => {},
},
startServer: async () => {
observedWebviewDistDir = process.env.CLINE_HUB_WEBVIEW_DIST_DIR;
return {
listenUrl: "http://127.0.0.1:8787/",
publicUrl: "http://127.0.0.1:8787",
inviteUrl: "http://127.0.0.1:8787",
stop: vi.fn(),
};
},
waitForShutdown: async () => {},
});
expect(exitCode).toBe(0);
expect(observedWebviewDistDir).toBe(webviewDistDir);
});
it("settles shutdown when server stop rejects", async () => {
const shutdown = waitForProcessShutdown({
listenUrl: "http://127.0.0.1:8787/",
publicUrl: "http://127.0.0.1:8787",
inviteUrl: "http://127.0.0.1:8787",
stop: vi.fn(async () => {
throw new Error("stop failed");
}),
});
process.emit("SIGINT", "SIGINT");
await expect(shutdown).rejects.toThrow("stop failed");
});
});
+195
View File
@@ -0,0 +1,195 @@
import { existsSync } from "node:fs";
import { arch, platform } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import open from "open";
import { c } from "../utils/output";
export interface DashboardServerHandle {
listenUrl: string;
publicUrl: string;
inviteUrl: string;
hubUrl?: string;
stop: () => void | Promise<void>;
}
interface DashboardCommandIo {
writeln: (text?: string) => void;
writeErr: (text: string) => void;
}
export interface RunDashboardCommandOptions {
cwd?: string;
host?: string;
port?: string;
publicUrl?: string;
roomSecret?: string;
openBrowser?: boolean;
io: DashboardCommandIo;
startServer?: () => Promise<DashboardServerHandle>;
openUrl?: (url: string) => Promise<void>;
waitForShutdown?: (server: DashboardServerHandle) => Promise<void>;
}
const DASHBOARD_PORT_ENV = "CLINE_HUB_DASHBOARD_PORT";
const WEBVIEW_DIST_ENV = "CLINE_HUB_WEBVIEW_DIST_DIR";
function setEnvValue(name: string, value: string | undefined): () => void {
const previous = process.env[name];
if (value === undefined) {
return () => {};
}
process.env[name] = value;
return () => {
if (previous === undefined) {
delete process.env[name];
} else {
process.env[name] = previous;
}
};
}
async function withDashboardEnvironment<T>(
options: RunDashboardCommandOptions,
fn: () => Promise<T>,
): Promise<T> {
const restore = [
setEnvValue(
"WORKSPACE_ROOT",
options.cwd ? resolve(options.cwd) : undefined,
),
setEnvValue("HOST", options.host),
setEnvValue(DASHBOARD_PORT_ENV, options.port),
setEnvValue("PUBLIC_URL", options.publicUrl),
setEnvValue("ROOM_SECRET", options.roomSecret),
setEnvValue(WEBVIEW_DIST_ENV, resolveDefaultWebviewDistDir()),
];
try {
return await fn();
} finally {
for (let i = restore.length - 1; i >= 0; i--) {
restore[i]?.();
}
}
}
function resolveDefaultWebviewDistDir(): string | undefined {
if (process.env[WEBVIEW_DIST_ENV]?.trim()) {
return undefined;
}
const moduleDir = dirname(fileURLToPath(import.meta.url));
const candidates = [
...resolveInstalledPlatformPackageWebviewCandidates(),
// Source checkout: apps/cli/src/commands/dashboard.ts
join(moduleDir, "../../../cline-hub/dist/webview"),
// Node bundle: apps/cli/dist/index.js
join(moduleDir, "cline-hub/webview"),
// Compiled platform package: apps/cli/dist/<platform>/bin/cline
join(dirname(process.execPath), "../cline-hub/webview"),
];
return candidates.find((candidate) => existsSync(candidate));
}
function resolveInstalledPlatformPackageWebviewCandidates(): string[] {
const packageName = resolvePlatformPackageName();
const starts = [
process.env.CLINE_WRAPPER_PATH
? dirname(process.env.CLINE_WRAPPER_PATH)
: undefined,
dirname(process.execPath),
].filter((value): value is string => !!value?.trim());
const candidates: string[] = [];
for (const start of starts) {
let current = start;
for (;;) {
candidates.push(
join(current, "node_modules", packageName, "cline-hub/webview"),
);
const parent = dirname(current);
if (parent === current) break;
current = parent;
}
}
return candidates;
}
function resolvePlatformPackageName(): string {
const platformName = platform() === "win32" ? "windows" : platform();
return `@cline/cli-${platformName}-${arch()}`;
}
async function startDefaultDashboardServer(): Promise<DashboardServerHandle> {
const { startClineHubDashboardServer } = await import("@cline/cline-hub");
return await startClineHubDashboardServer();
}
async function openDefaultUrl(url: string): Promise<void> {
await open(url, { wait: false });
}
export function waitForProcessShutdown(
server: DashboardServerHandle,
): Promise<void> {
return new Promise<void>((resolveShutdown, rejectShutdown) => {
let settled = false;
const cleanup = () => {
process.off("SIGINT", handleSignal);
process.off("SIGTERM", handleSignal);
};
const stop = async () => {
if (settled) return;
settled = true;
cleanup();
try {
await server.stop();
resolveShutdown();
} catch (error) {
rejectShutdown(error);
}
};
function handleSignal() {
void stop();
}
process.on("SIGINT", handleSignal);
process.on("SIGTERM", handleSignal);
});
}
export async function runDashboardCommand(
options: RunDashboardCommandOptions,
): Promise<number> {
try {
const server = await withDashboardEnvironment(options, () =>
(options.startServer ?? startDefaultDashboardServer)(),
);
const dashboardUrl =
server.inviteUrl || server.publicUrl || server.listenUrl;
options.io.writeln(
`${c.green}Cline dashboard listening at${c.reset} ${dashboardUrl}`,
);
if (server.hubUrl) {
options.io.writeln(`${c.dim}Hub endpoint: ${server.hubUrl}${c.reset}`);
}
if (options.openBrowser !== false) {
try {
await (options.openUrl ?? openDefaultUrl)(dashboardUrl);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
options.io.writeErr(`Failed to open browser: ${message}`);
}
}
await (options.waitForShutdown ?? waitForProcessShutdown)(server);
return 0;
} catch (error) {
options.io.writeErr(error instanceof Error ? error.message : String(error));
return 1;
}
}
@@ -115,8 +115,8 @@ describe("runDoctorCommand", () => {
return {
status: 0,
stdout: [
"50174 /Users/example/.bun/bin/bun /Users/example/dev/sdk/apps/cli/src/index.ts hub start --cwd /workspace",
"50190 /Users/example/.bun/bin/bun /Users/example/dev/sdk/apps/cli/src/index.ts hey",
"50174 /Users/example/.bun/bin/bun /Users/example/dev/apps/cli/src/index.ts hub start --cwd /workspace",
"50190 /Users/example/.bun/bin/bun /Users/example/dev/apps/cli/src/index.ts hey",
].join("\n"),
};
}
@@ -266,7 +266,7 @@ describe("runDoctorCommand", () => {
return {
status: 0,
stdout:
"60123 /Users/example/dev/sdk/apps/examples/desktop-app/src-tauri/bin/code-sidecar\n",
"60123 /Users/example/dev/apps/examples/desktop-app/src-tauri/bin/code-sidecar\n",
};
}
return { status: 1, stdout: "" };
@@ -1,3 +1,4 @@
import { execFileSync } from "node:child_process";
import {
existsSync,
mkdtempSync,
@@ -17,8 +18,10 @@ import {
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
installPlugin,
isOfficialPluginSlug,
parsePluginSource,
runPluginInstallCommand,
runPluginUninstallCommand,
} from "./plugin";
type FetchCall = (
@@ -47,6 +50,29 @@ describe("plugin install command", () => {
setClineDir(process.env.CLINE_DIR);
});
function runGitCommand(cwd: string, args: string[]): void {
execFileSync("git", args, { cwd, stdio: "ignore" });
}
async function createOfficialPluginsRepo(
plugins: Record<string, Record<string, string>>,
): Promise<string> {
const repo = mkdtempSync(join(root, "official-plugins-"));
for (const [slug, files] of Object.entries(plugins)) {
const pluginRoot = join(repo, "plugins", slug);
await mkdir(pluginRoot, { recursive: true });
for (const [filename, content] of Object.entries(files)) {
await writeFile(join(pluginRoot, filename), content, "utf8");
}
}
runGitCommand(repo, ["init"]);
runGitCommand(repo, ["config", "user.email", "test@example.com"]);
runGitCommand(repo, ["config", "user.name", "Cline Test"]);
runGitCommand(repo, ["add", "."]);
runGitCommand(repo, ["commit", "-m", "seed plugins"]);
return repo;
}
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
@@ -85,6 +111,25 @@ describe("plugin install command", () => {
});
});
it("parses bare official keywords as official plugin slugs", () => {
expect(isOfficialPluginSlug("clickhouse")).toBe(true);
expect(isOfficialPluginSlug("web-search")).toBe(true);
expect(isOfficialPluginSlug("WebSearch")).toBe(false);
expect(parsePluginSource("clickhouse")).toEqual({
type: "official",
slug: "clickhouse",
});
expect(parsePluginSource("web-search")).toEqual({
type: "official",
slug: "web-search",
});
expect(parsePluginSource("web-search", "npm")).toEqual({
type: "npm",
spec: "web-search",
name: "web-search",
});
});
it("rejects hostname-style sources without --git guidance", () => {
expect(() => parsePluginSource("github.com/acme/plugin")).toThrow(
/Use --git/,
@@ -169,6 +214,133 @@ describe("plugin install command", () => {
).toEqual(result.entryPaths);
});
it("installs an official plugin slug from the configured collection repo", async () => {
const officialPluginsRepo = await createOfficialPluginsRepo({
"web-search": {
"index.ts":
"export default { name: 'official-web-search', manifest: { capabilities: ['tools'] } };",
},
"other-plugin": {
"index.ts":
"export default { name: 'other-plugin', manifest: { capabilities: ['tools'] } };",
},
});
const result = await installPlugin({
source: "web-search",
cwd: workspace,
officialPluginsRepo,
});
expect(result.installPath).toContain(
join(workspace, ".cline", "plugins", "_installed", "official"),
);
expect(result.entryPaths).toHaveLength(1);
expect(readFileSync(result.entryPaths[0] ?? "", "utf8")).toContain(
"official-web-search",
);
const wrapperManifest = JSON.parse(
readFileSync(join(result.installPath, "package.json"), "utf8"),
) as { name?: string };
expect(wrapperManifest.name).toBe("web-search");
expect(existsSync(join(result.installPath, "repo"))).toBe(false);
expect(
existsSync(join(result.installPath, "package", "other-plugin")),
).toBe(false);
expect(
discoverPluginModulePaths(join(workspace, ".cline", "plugins")),
).toEqual(result.entryPaths);
});
it("installs an official package plugin and runs package dependency install", async () => {
const officialPluginsRepo = await createOfficialPluginsRepo({
"package-plugin": {
"package.json": JSON.stringify(
{
name: "package-plugin",
type: "module",
cline: {
plugins: [{ paths: ["./index.ts"] }],
},
dependencies: {
yaml: "^2.8.1",
},
},
null,
2,
),
"index.ts":
"export default { name: 'package-plugin', manifest: { capabilities: ['tools'] } };",
},
});
const npmLogPath = join(root, "official-npm-install.log");
const npmCommandPath = join(root, "official-fake-npm.sh");
writeFileSync(
npmCommandPath,
`#!/bin/sh\nprintf '%s\\n' "$PWD $*" >> "${npmLogPath}"\nexit 0\n`,
{ encoding: "utf8", mode: 0o755 },
);
const result = await installPlugin({
source: "package-plugin",
cwd: workspace,
officialPluginsRepo,
npmCommand: npmCommandPath,
});
const npmLog = readFileSync(npmLogPath, "utf8");
expect(npmLog).toContain("package install --omit=dev --omit=peer");
expect(result.entryPaths).toHaveLength(1);
expect(readFileSync(result.entryPaths[0] ?? "", "utf8")).toContain(
"package-plugin",
);
});
it("reports a clear error when an official plugin slug is missing", async () => {
const officialPluginsRepo = await createOfficialPluginsRepo({
"known-plugin": {
"index.ts":
"export default { name: 'known-plugin', manifest: { capabilities: ['tools'] } };",
},
});
await expect(
installPlugin({
source: "missing-plugin",
cwd: workspace,
officialPluginsRepo,
}),
).rejects.toThrow(
/Official Cline plugin "missing-plugin" was not found at plugins\/missing-plugin/,
);
});
it("keeps explicit relative paths as local plugin installs", async () => {
const localPluginRoot = join(workspace, "web-search");
await mkdir(localPluginRoot, { recursive: true });
await writeFile(
join(localPluginRoot, "index.ts"),
"export default { name: 'local-web-search', manifest: { capabilities: ['tools'] } };",
"utf8",
);
const result = await installPlugin({
source: "./web-search",
cwd: workspace,
});
expect(result.installPath).toContain(
join(workspace, ".cline", "plugins", "_installed", "local"),
);
const wrapperManifest = JSON.parse(
readFileSync(join(result.installPath, "package.json"), "utf8"),
) as { name?: string };
expect(wrapperManifest.name).toBe("web-search");
expect(readFileSync(result.entryPaths[0] ?? "", "utf8")).toContain(
"local-web-search",
);
});
it("times out stalled remote plugin downloads", async () => {
vi.useFakeTimers();
const source =
@@ -292,7 +464,8 @@ describe("plugin install command", () => {
const wrapperManifest = JSON.parse(
readFileSync(join(result.installPath, "package.json"), "utf8"),
) as { cline?: { plugins?: Array<{ paths?: string[] }> } };
) as { name?: string; cline?: { plugins?: Array<{ paths?: string[] }> } };
expect(wrapperManifest.name).toBe("plugin-package");
expect(wrapperManifest.cline?.plugins?.[0]?.paths).toHaveLength(1);
expect(wrapperManifest.cline?.plugins?.[0]?.paths?.[0]).toContain(
"package/index.ts",
@@ -430,6 +603,54 @@ describe("plugin install command", () => {
).toContain("installed-v1");
});
it("uninstalls a package plugin by package name", async () => {
const source = join(root, "uninstall-package");
const npmCommandPath = join(root, "fake-npm.sh");
await mkdir(source, { recursive: true });
await writeFile(
join(source, "package.json"),
JSON.stringify(
{
name: "cli-uninstall-plugin",
cline: {
plugins: [{ paths: ["./index.ts"], capabilities: ["tools"] }],
},
},
null,
2,
),
"utf8",
);
await writeFile(
join(source, "index.ts"),
"export default { name: 'cli-uninstall-plugin', manifest: { capabilities: ['tools'] } };",
"utf8",
);
writeFileSync(npmCommandPath, "#!/bin/sh\nexit 0\n", {
encoding: "utf8",
mode: 0o755,
});
const installed = await installPlugin({
source,
npmCommand: npmCommandPath,
});
const output: string[] = [];
const code = await runPluginUninstallCommand({
name: "cli-uninstall-plugin",
io: {
writeln: (text = "") => output.push(text),
writeErr: (text) => output.push(text),
},
});
expect(code).toBe(0);
expect(existsSync(installed.installPath)).toBe(false);
expect(output.join("\n")).toContain(
"Uninstalled plugin cli-uninstall-plugin",
);
});
it("prints JSON output for command callers", async () => {
const source = join(root, "json.ts");
writeFileSync(
@@ -460,6 +681,40 @@ describe("plugin install command", () => {
}
});
it("prints JSON output for official plugin installs", async () => {
const officialPluginsRepo = await createOfficialPluginsRepo({
"json-plugin": {
"index.ts":
"export default { name: 'json-plugin', manifest: { capabilities: ['tools'] } };",
},
});
const stdout: string[] = [];
const originalWrite = process.stdout.write;
process.stdout.write = ((chunk: string | Uint8Array) => {
stdout.push(String(chunk));
return true;
}) as typeof process.stdout.write;
try {
const code = await runPluginInstallCommand({
source: "json-plugin",
cwd: workspace,
officialPluginsRepo,
json: true,
io: {
writeln: () => {},
writeErr: () => {},
},
});
expect(code).toBe(0);
const parsed = JSON.parse(stdout.join("")) as { installPath: string };
expect(parsed.installPath).toContain(
join(workspace, ".cline", "plugins", "_installed", "official"),
);
} finally {
process.stdout.write = originalWrite;
}
});
it("uses shared search paths for cwd installs", async () => {
const source = join(root, "workspace.ts");
writeFileSync(
@@ -12,7 +12,16 @@ import {
} from "node:fs";
import { cp, mkdir, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, dirname, join, relative, resolve, sep } from "node:path";
import {
basename,
dirname,
extname,
join,
relative,
resolve,
sep,
} from "node:path";
import { type PluginUninstallOptions, uninstallPlugin } from "@cline/core";
import {
isPluginModulePath,
resolveClineDir,
@@ -25,6 +34,7 @@ export interface PluginInstallOptions {
cwd?: string;
force?: boolean;
npmCommand?: string;
officialPluginsRepo?: string;
io?: PluginInstallIo;
}
@@ -60,6 +70,10 @@ type ParsedPluginSource =
| {
type: "local";
path: string;
}
| {
type: "official";
slug: string;
};
type PluginInstallSourceType = "npm" | "git" | "local" | "remote";
@@ -77,6 +91,7 @@ interface PluginPackageManifest {
const INSTALLS_DIRECTORY_NAME = "_installed";
const PACKAGE_DIRECTORY_NAME = "package";
const OFFICIAL_PLUGINS_REPO = "https://github.com/cline/plugins.git";
const REMOTE_PLUGIN_FETCH_TIMEOUT_MS = 30_000;
const REMOTE_PLUGIN_MAX_BYTES = 10 * 1024 * 1024;
const HOST_PROVIDED_SDK_PREFIX = "@cline/";
@@ -121,6 +136,14 @@ function sanitizeSegment(value: string): string {
return sanitized || "plugin";
}
export function isOfficialPluginSlug(source: string): boolean {
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(source.trim());
}
function resolveOfficialPluginsRepo(override: string | undefined): string {
return override?.trim() || OFFICIAL_PLUGINS_REPO;
}
function parseNpmSpec(spec: string): { name: string } {
const trimmed = spec.trim();
const match = trimmed.match(/^(@?[^@/]+(?:\/[^@/]+)?)(?:@.+)?$/);
@@ -371,6 +394,9 @@ export function parsePluginSource(
if (git) {
return git;
}
if (isOfficialPluginSlug(trimmed)) {
return { type: "official", slug: trimmed };
}
if (looksLikeHostnamePath(trimmed)) {
throw new Error(
`Unrecognized plugin source "${source}". Use --git for hostname-style repositories or pass an explicit local path such as ./github.com/owner/repo.`,
@@ -415,6 +441,14 @@ function getInstallPath(
`${sanitizeSegment(parsed.filename)}-${hashSource(sourceKey)}`,
);
}
if (parsed.type === "official") {
return join(
pluginRoot,
INSTALLS_DIRECTORY_NAME,
"official",
`${sanitizeSegment(parsed.slug)}-${hashSource(sourceKey)}`,
);
}
return join(
pluginRoot,
INSTALLS_DIRECTORY_NAME,
@@ -423,7 +457,11 @@ function getInstallPath(
);
}
function getInstallSourceKey(parsed: ParsedPluginSource, cwd: string): string {
function getInstallSourceKey(
parsed: ParsedPluginSource,
cwd: string,
officialPluginsRepo: string,
): string {
if (parsed.type === "npm") {
return `npm:${parsed.spec}`;
}
@@ -433,9 +471,31 @@ function getInstallSourceKey(parsed: ParsedPluginSource, cwd: string): string {
if (parsed.type === "remote") {
return `remote:${parsed.url}`;
}
if (parsed.type === "official") {
return `official:${officialPluginsRepo}#plugins/${parsed.slug}`;
}
return `local:${resolve(cwd, resolveHomePath(parsed.path))}`;
}
function getWrapperPackageName(
parsed: ParsedPluginSource,
cwd: string,
): string {
if (parsed.type === "npm") {
return parsed.name;
}
if (parsed.type === "git") {
return sanitizeSegment(basename(parsed.path));
}
if (parsed.type === "remote") {
return sanitizeSegment(basename(parsed.filename, extname(parsed.filename)));
}
if (parsed.type === "official") {
return parsed.slug;
}
return sanitizeSegment(basename(resolve(cwd, resolveHomePath(parsed.path))));
}
async function runCommand(
command: string,
args: string[],
@@ -632,6 +692,7 @@ function toWrapperEntryPaths(
async function writeWrapperManifest(
wrapperRoot: string,
packageRoot: string,
packageName: string,
): Promise<string[]> {
const entryPaths = toWrapperEntryPaths(wrapperRoot, packageRoot);
await writeFile(
@@ -639,7 +700,7 @@ async function writeWrapperManifest(
JSON.stringify(
{
...WRAPPER_PACKAGE_JSON,
name: `cline-installed-plugin-${hashSource(wrapperRoot)}`,
name: packageName,
cline: {
plugins: [{ paths: entryPaths }],
},
@@ -732,6 +793,43 @@ async function installGitPackage(
return packageRoot;
}
async function installOfficialPlugin(
parsed: Extract<ParsedPluginSource, { type: "official" }>,
stagingRoot: string,
npmCommand: string,
officialPluginsRepo: string,
): Promise<string> {
const repoRoot = join(stagingRoot, "repo");
await runCommand("git", [
"clone",
"--filter=blob:none",
"--depth",
"1",
"--",
officialPluginsRepo,
repoRoot,
]);
const sourceRoot = join(repoRoot, "plugins", parsed.slug);
if (!existsSync(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
throw new Error(
`Official Cline plugin "${parsed.slug}" was not found at plugins/${parsed.slug} in ${officialPluginsRepo}`,
);
}
const packageRoot = join(stagingRoot, PACKAGE_DIRECTORY_NAME);
await cp(sourceRoot, packageRoot, {
recursive: true,
filter: (sourcePath) => {
const name = basename(sourcePath);
return name !== ".git" && name !== "node_modules";
},
});
rmSync(repoRoot, { recursive: true, force: true });
await installPackageDependencies(packageRoot, npmCommand);
return packageRoot;
}
function remotePluginSizeLimitError(url: string): Error {
return new Error(
`Remote plugin file from ${url} exceeds the ${REMOTE_PLUGIN_MAX_BYTES} byte limit`,
@@ -913,8 +1011,12 @@ export async function installPlugin(
const explicitCwd = options.cwd?.trim();
const cwd = explicitCwd ? resolve(explicitCwd) : process.cwd();
const pluginRoot = getPluginRoot(explicitCwd ? cwd : undefined);
const sourceKey = getInstallSourceKey(parsed, cwd);
const officialPluginsRepo = resolveOfficialPluginsRepo(
options.officialPluginsRepo,
);
const sourceKey = getInstallSourceKey(parsed, cwd, officialPluginsRepo);
const installPath = getInstallPath(pluginRoot, parsed, sourceKey);
const wrapperPackageName = getWrapperPackageName(parsed, cwd);
const stagingParent = join(pluginRoot, INSTALLS_DIRECTORY_NAME, ".tmp");
const stagingRoot = join(
stagingParent,
@@ -933,6 +1035,13 @@ export async function installPlugin(
packageRoot = await installNpmPackage(parsed, stagingRoot, npmCommand);
} else if (parsed.type === "git") {
packageRoot = await installGitPackage(parsed, stagingRoot, npmCommand);
} else if (parsed.type === "official") {
packageRoot = await installOfficialPlugin(
parsed,
stagingRoot,
npmCommand,
officialPluginsRepo,
);
} else if (parsed.type === "remote") {
packageRoot = await installRemoteFile(parsed, stagingRoot);
} else {
@@ -950,7 +1059,11 @@ export async function installPlugin(
? collectPluginEntries(stagingRoot).map(
(entry) => `./${toPosixPath(relative(stagingRoot, entry))}`,
)
: await writeWrapperManifest(stagingRoot, packageRoot);
: await writeWrapperManifest(
stagingRoot,
packageRoot,
wrapperPackageName,
);
if (entryPaths.length === 0) {
throw new Error(`No plugin entry files found for ${source}`);
}
@@ -985,3 +1098,22 @@ export async function runPluginInstallCommand(
return 1;
}
}
export async function runPluginUninstallCommand(
options: PluginUninstallOptions & { json?: boolean; io?: PluginInstallIo },
): Promise<number> {
try {
const result = await uninstallPlugin(options);
if (options.json) {
process.stdout.write(JSON.stringify(result));
return 0;
}
options.io?.writeln(`Uninstalled plugin ${result.name}`);
options.io?.writeln(` Removed: ${result.installPath}`);
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
options.io?.writeErr(message);
return 1;
}
}

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