Compare commits

..
Author SHA1 Message Date
abeatrix cc0675d31f wip: move connectors to hub 2026-07-15 00:39:14 +08:00
Bee c3033d6f13 fix(vscode): refreshGroqModels caused cacheReadsPrice undefined error (#12213) 2026-07-10 08:52:06 +08:00
Max cfb1327a1b fix vscode hmr not working (#12212) 2026-07-09 16:49:35 -07:00
Alex Taboada 264af96e1b fix(vscode): prevent infinite loading when initializing task with an image (#12203) 2026-07-09 18:16:52 +02:00
Robin Newhouse 10cb9bd97a Add compaction budget hardening (#12142)
* Add compaction budget projection contract

* Tighten budget projection contract types

* Tighten dropped block action paths

* Add pure compaction budget projection engine

* Fix budget projection truncation accounting

* Drop provider-native blocks during budget projection

* Recompute protected tail after thinking pruning

* Align budget projection test tool results

* Clean up budget projection fixture indentation

* fix(core): narrow compaction protected tail

* Fix budget projection action accounting

* Budget agentic compaction summary input

* Harden agentic summary budget fallback

* Align agentic compaction test tool result

* Align agentic file ops with projected input

* Budget basic compaction projections

* Clarify basic projection budget logging

* Align basic sanitization image expectation

* Align basic compaction budget expectation

* Emit compaction budget emergency telemetry

* Tighten compaction budget telemetry types

* Preserve compaction status notice reasons

* fix(core): account compaction tokens consistently

* fix(core): align skipped compaction token accounting
2026-07-09 02:15:55 -07:00
Saoud Rizwan 2ee18e7f0c chore(cli): release v3.0.39 2026-07-08 21:15:22 -07:00
Saoud Rizwan 0b65506a2b chore(sdk): release v0.0.59 2026-07-08 20:00:37 -07:00
Saoud Rizwan 3502608081 fix(telemetry): emit telemetry from the detached hub daemon process (#12177)
* feat(sdk): emit telemetry from the hub daemon process

The detached hub daemon hosts the LocalRuntimeHost that emits
task.conversation_turn and task.tokens for every hub-backed session
(CLI in prefer-hub mode, desktop app, connectors), but the daemon
entrypoint never created a telemetry handle - startHubWebSocketServer
received telemetry: undefined and every capture in the daemon-side
runtime was a no-op. Sessions billed normally on the backend while
reporting nothing to OTel.

- create a ConfiguredTelemetryHandle in the daemon entry and pass it to
  the websocket server and schedule runtime handlers
- identify from the cached cline account at startup and re-resolve
  periodically, since the long-lived daemon often starts before login
  or outlives an account switch
- flush and dispose the handle on graceful and fatal shutdown

* fix(sdk): flush daemon telemetry when server startup fails

If startHubWebSocketServer throws, dispose the telemetry handle before
rethrowing so failed daemon starts are visible in telemetry instead of
dying silently.

* fix(sdk): bound daemon telemetry flush and reuse settings manager

- Race dispose's flush against a 5s deadline so a hung exporter can't
  keep a crashed daemon alive holding the hub port (before this PR the
  daemon exited immediately on fatal errors; the flush must not change
  that materially).
- Construct ProviderSettingsManager once instead of every identity
  refresh; its constructor runs legacy-migration and provider
  registration side effects, and getProviderSettings re-reads the file
  per call anyway.
- Test the dispose-on-startup-failure path and the cline-hub-daemon
  platform metadata.

* fix(sdk): label daemon telemetry cline_type as hub

Review feedback from @abeatrix: daemon-hosted sessions can be triggered
by the CLI, desktop app, or connectors, so daemon-emitted events should
not share the CLI process's cline_type. Existing values are "cli" and
"VSCode Extension"; the daemon now reports "hub" (with the finer
platform=cline-hub-daemon kept as-is).
2026-07-08 19:35:26 -07:00
Saoud Rizwan ed3107f9ec Revert "docs: add Cline free models page (#12183)" (#12185)
This reverts commit 6bce48aad4.
2026-07-08 19:32:36 -07:00
Renee HuangandSaoud Rizwan 6bce48aad4 docs: add Cline free models page (#12183)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-08 19:22:32 -07:00
BeeandSaoud Rizwan 1e1b6af51c fix(sdk): set versioned Cline client-identity headers for Cline provider (#12182)
* fix(sdk): set versioned Cline client-identity headers for Cline provider

* address feedback

* feat: add platform metadata to client context

Include platform, platformVersion, and isMultiRoot in extension client
context for CLI, ACP, and VS Code sessions. This provides downstream
core/session logic with richer runtime information and distinguishes ACP
clients from the standard CLI client.

* lint

* clean up

* fix: resolve client host identity via HostProvider for standalone compatibility

cline-session-factory.ts is also bundled into the standalone cline-core
(JetBrains), where the 'vscode' module resolves to the generated Proxy-stub
module: vscode.env.appName and vscode.version return Proxy objects, which
would flow into X-PLATFORM/X-PLATFORM-VERSION header values and fail at
request serialization.

Resolve the identity through HostProvider.env.getHostVersion() instead —
the VS Code hostbridge returns the identical values (vscode.env.appName,
vscode.version, ClineClient.VSCode, extension version), and JetBrains'
hostbridge returns its real host values, so the standalone stops reporting
itself as the VS Code extension as a bonus. Multi-root detection goes
through HostProvider.workspace.getWorkspacePaths() for the same reason.
Both resolvers degrade gracefully (undefined/false) if the host bridge is
unavailable, in which case the header builder falls back to source-derived
values.

* Add unit test as proof

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-08 19:08:25 -07:00
Saoud Rizwan ee49900232 chore(greptile): update telemetry review rules for the monorepo layout (#12180)
The .greptile config was written for the pre-merge standalone cline/sdk
repo and never updated after the monorepo merge:

- the sdk-telemetry-doc-update rule enforced an Event Catalog in DOC.md,
  a file that does not exist in this repo (it now emits a false P2 on
  every PR touching core-events.ts, e.g. #12177)
- rules.md cited PR #357, apps/vscode/src/hub-daemon.ts, and
  apps/vscode/src/telemetry.ts - none of which exist here
- the 'Hub Daemon Metadata Forwarding' section described an argv-based
  metadata payload that was never implemented in this repo; replaced
  with the actual daemon-owned telemetry pattern from #12177
- the opted-out-test rule now describes the real convention: assert the
  event flows through capture (no-op for OptedOutTelemetryService), not
  captureRequired
2026-07-08 16:43:14 -07:00
Saoud Rizwan a1d5589d19 feat: allow selecting Cline free models on the ClinePass provider (#12140)
* feat(llms): include Cline free models in the cline-pass catalog

* feat(vscode): show Subscribed/Free model tabs on the ClinePass provider

* feat(cli): show Subscribed/Free sections in the ClinePass model picker

* fix(cli): drop redundant browse-all entry from ClinePass picker

* fix(cli): show only subscribed models in ClinePass onboarding picker

* feat(cli): include free models and quota explainer in ClinePass onboarding picker

* fix: shorten ClinePass free section copy

* fix(cli): strip redundant free markers from sectioned picker names

* fix: drop free from ClinePass free section copy

* fix: tighten ClinePass free section copy

* refactor: address review feedback on ClinePass free models

- single buildFeaturedModelEntries(providerId) dispatcher, builders private
- rename isClineProvider to isClineManagedProvider (includes cline-pass)
- use isClineManagedProvider in the free-model cost check
- themed tab border, pretty names on free model cards
- clearer cline-pass cost test name

* fix: address ClinePass free-model review blockers

- Stop re-sorting the cline-pass live catalog by release date in
  mergeKnownModels: free models carry OpenRouter release dates, so the
  sort could put a free model first and make it the fallback default
  when the bundled default id rotates out of the live clinePass bucket.
  Preserve the normalize-time order (pass models first) and pin it with
  an end-to-end resolveProviderConfig test.
- Add the browse-all escape to the CLI ClinePass picker when the
  clinePass bucket is empty (bundled fallback after a fetch failure),
  so a subscriber isn't left with a free-models-only picker.
- Rename ErrorRow's local isClineManagedProvider to
  isClineUsageBillingProvider: it only matches the cline provider,
  unlike the shared util of the same name that also matches cline-pass.
2026-07-08 15:08:27 -07:00
Tomás BarreiroandSaoud Rizwan 721fda2e99 Add ClinePass limit error (#12162)
* Add ClinePass limit error

* refactor regex

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-08 14:54:20 -07:00
Saoud Rizwan 5e78861eb5 fix(vscode): update ClinePass onboarding option copy (#12173) 2026-07-08 13:21:12 -07:00
Robin Newhouse 29798f59f3 Persist VS Code manual compaction sidecar (#11900)
* Persist VS Code manual compaction sidecar

* Fix compaction test isolation

* Address PR feedback on compaction comments

* Fix compaction test core mock hoisting

* fix(vscode): avoid compaction session rebuild

* fix(core): validate active compaction from persisted transcript

* fix(vscode): harden manual compaction sidecar flow
2026-07-08 13:18:22 -07:00
Ara 177d0eb07f Remove Cline model picker recommendation copy (#12170) 2026-07-08 12:15:53 -07:00
Bee 869a87a220 fix(core): use no-emit TypeScript config for checks (#12139)
* fix(core): use no-emit TypeScript config for checks

Update the core package TypeScript config to run checks without emitting files,
allowing broader workspace sources via the package parent rootDir. Simplify the
dev config so it only extends the main package config and avoids duplicated
compiler overrides.

* feedback

* remove dead code
2026-07-08 11:15:16 -07:00
MaxandMax Paulus 🥪 0cfd0bbe05 Fix VS Code F5 webview debug flow (#12027)
* fix vscode f5 settings

- fixed the hot module reloading issue while debugging the extension.
- also fixed issue where deb:webview task wasn't showing as complete

* fix vscode webview dev cleanup

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-07-08 10:53:41 -07:00
08f656532f docs: add Atomic Chat local provider setup guide (#11966)
* docs: add Atomic Chat local provider setup guide

Document Atomic Chat alongside Ollama and LM Studio in the local models
overview and add a dedicated provider configuration page.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update overview.mdx

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
2026-07-08 10:48:58 -07:00
Tomás Barreiro 10dece6677 Remove all ClinePass GLM 5.1 references (#12107)
* Remove all ClinePass GLM 5.1 references

* fix other references
2026-07-08 14:59:10 +02:00
Sufiyan Khan 885a2936b6 docs(authorizing): remove model-specific wording from generic setup step (#12156)
Step 4 in the IDE setup flow says 'Choose your desired Claude model'
but applies to all providers (OpenAI, Gemini, DeepSeek, local, etc.).
Drop 'Claude' to keep it provider-agnostic.
2026-07-08 21:22:33 +09:00
Dominic CooneyandCline Agent 90c427740d perf(sdk): stop listSessions hot loop from hanging the extension host (#11967)
* perf(sdk): stop listSessions hot loop from hanging the extension host

getStateToPostToWebview rebuilt the full task history on nearly every
streaming/session event, and each rebuild ran persistence-service.listSessions,
which synchronously read + Zod-parsed every session manifest. The 10s metadata
cache meant to absorb this was wiped on every per-turn updateTaskUsage, so each
state post paid the full synchronous scan, saturating the extension-host event
loop (observed as a tight listSessions/readFileUtf8 loop in CPU profiles).

- Debounce/coalesce postStateToWebview: trailing 50ms debounce plus a single
  queued follow-up so bursts collapse into one rebuild; dispose() tears it down.
- Add an async, title-only manifest reader (readSessionManifestTitle) and use it
  in listSessions to resolve titles concurrently off-thread, instead of a
  synchronous readFileSync + full SessionManifestSchema (Zod) parse per row. The
  existing sync manifest methods are left intact.
- On single-session updates, patch just the changed record in the merged-history
  cache in place instead of invalidating it, so frequent per-turn usage updates
  no longer force the next state post to re-enumerate and re-merge every session.

* refactor(sdk): strengthen session history cache patching

Replace patchMetadataHistoryCacheRecord (boolean-returning, metadata-only,
no re-sort) with updateCachedSessionRecord (void, updates prompt +
metadata + updatedAt, re-sorts via shared comparator).

- Void return eliminates the ignorable fallback contract.
- Mirrors all fields the persistence layer writes (prompt, metadata,
  updatedAt) so cache and disk stay consistent.
- Re-sorts after patching so the updated record bubbles to the correct
  position, using a shared compareSessionHistoryRecordsByRecencyDesc
  comparator also used by listHistory.
- Derives updatedAt from the HistoryItem timestamp instead of constructing
  a second clock value.
- Self-invalidates on cache miss so callers never manage the fallback.

Adds tests for in-place patching, re-sorting, per-turn usage hot path,
and cache-miss invalidation.

* fix(sdk): await in-flight state post during dispose

Greptile feedback: dispose() did not await a concurrently-running
runDebouncedStatePost, so an in-flight flushStateToWebview could access
torn-down resources after disposal.

Track the runDebouncedStatePost promise in statePostInFlightPromise.
In dispose(), after setting isDisposed and clearing the timer, await
the in-flight promise (swallowing errors) before tearing down downstream
resources. The !this.isDisposed guard in the loop prevents further
iterations after disposal.

* fix(sdk): address review feedback on state-post debounce and cache patch

Three issues from code review of the listSessions hot-loop fix:

1. dispose() could await the wrong promise. A second debounced timer
   firing while a flush was already running overwrote
   statePostInFlightPromise with a throwaway resolved promise from the
   join path, so dispose() could return while the original flush was
   still executing. Extract the debounce/coalesce state machine into
   StatePostDebouncer, and only track the promise from the call that
   actually starts a new flush loop.

2. postStateToWebview() swallowed flush errors, resolving every pending
   caller even when flushStateToWebview() threw. Callers awaiting
   postStateToWebview() now see the rejection, matching pre-debounce
   behavior.

3. Cache patching derived the cached updatedAt from HistoryItem.ts,
   but the persistence adapter always stamps updatedAt with the
   wall-clock write time. Callers like toggleTaskFavorite() reuse an
   old HistoryItem whose ts predates the write, which let the cached
   ordering diverge from disk until the 10s TTL expired. Stamp the
   cache patch with the write time instead.

Adds unit tests for StatePostDebouncer covering the dispose race and
error-propagation regressions, and a sdk-task-history test for the
stale-updatedAt cache-ordering regression.

* fix(sdk): don't patch cache when session update write didn't land

Beatrix's review feedback: updateSession() ignored the { updated:
boolean } result from host.update() and unconditionally patched the
metadata cache. When persistence returns updated: false (session
deleted/missing, or an optimistic-concurrency retry exhausted by a
racing writer), the webview could show a fake updated record until the
cache TTL expired.

Check the write result: only patch the cache when updated === true,
otherwise invalidate it so the next read re-enumerates from disk.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-07-08 13:23:18 +09:00
alex-lum e6028168f2 fix(sdk/cli): emit user_id in SDK/CLI telemetry identity attributes (CLINE-2406) (#11581)
* fix(sdk/cli): emit user_id in telemetry identity attributes

Per CLINE-2406, downstream analytics expects an explicit user_id field
in authenticated SDK/CLI OpenTelemetry log attributes.

Changes:
- sdk/packages/core/src/services/telemetry/core-events.ts: add
  user_id: account.id alongside the existing account_id in
  identifyAccount() updateCommonProperties call.
- sdk/packages/core/src/services/telemetry/core-events.test.ts: new
  identifyAccount suite verifying user_id, account_id, distinct_id, and
  org context fields for authenticated user without org, with active org,
  absent/blank id handling, and no-op when telemetry is undefined.
- apps/cli/src/main.ts: after loading Cline provider settings in the
  runtime path, read auth.accountId and call identifyTelemetryAccount so
  subsequent task.* and workspace.* events carry user_id. Document
  user.extension_activated as pre-auth by design for subcommand flows.
- apps/cli/src/main.test.ts: three new tests covering saved accountId
  triggers identity, missing accountId skips identity, non-Cline
  provider skips identity.

* fix(sdk/cli): address review feedback on telemetry identity

- Use trimmed distinctId for user_id in identifyAccount() to keep
  user_id and distinct_id consistent when IDs have whitespace
- Remove fragile type cast in CLI main.ts; ProviderSettings already
  exposes auth.accountId via AuthSettingsSchema
2026-07-07 19:20:03 -07:00
216 changed files with 6827 additions and 1443 deletions
-8
View File
@@ -39,14 +39,6 @@
"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": [
"sdk/packages/core/src/services/telemetry/core-events.ts"
],
"severity": "medium"
}
]
}
+1 -5
View File
@@ -16,13 +16,9 @@
"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."
},
{
"path": "DOC.md",
"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": "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."
"description": "Architecture reference. Telemetry design decisions and completion semantics (submit_and_exit anchoring) are documented here. Use as ground truth for design intent."
},
{
"path": "sdk/AGENTS.md",
+20 -17
View File
@@ -36,8 +36,11 @@ event names. It exports:
1. Add the constant to `CORE_TELEMETRY_EVENTS`
2. Add a typed `capture*()` helper alongside it (with a typed `properties` parameter)
3. Update the Event Catalog section in `DOC.md`
4. Add a unit test in `core-events.test.ts` asserting the event is dropped when telemetry is opted out
3. Add a unit test in `core-events.test.ts` asserting the event flows through the
opt-out-respecting `capture` path and never `captureRequired` (opt-out is enforced by
`OptedOutTelemetryService`, whose `capture` is a no-op — the test convention is
"emits X as a normal opt-out-respecting event"). Events that intentionally bypass
opt-out must use `captureRequired` and assert that explicitly.
## The Activation Funnel
@@ -82,7 +85,7 @@ The CLI accepts `--config <dir>`. The CLI **must** apply `setClineDir(...)` and
and any other on-disk telemetry state lands under `~/.cline` instead of the user's chosen
config dir.
The canonical pattern is in `apps/cli/src/main.ts` (PR #357):
The canonical pattern is in `apps/cli/src/main.ts`:
```ts
if (configDir) setClineDir(configDir);
@@ -90,18 +93,18 @@ setHomeDir(homedir());
captureCliExtensionActivated(); // <-- after dir overrides
```
## Hub Daemon Metadata Forwarding
## Hub Daemon Telemetry
Hosts that spawn a detached `@cline/core/hub/daemon-entry` process must forward telemetry
metadata into the daemon argv so the daemon can reconstruct an equivalent
`ITelemetryService`. The expected payload is base64-encoded JSON with snake_case keys:
The detached hub daemon (`sdk/packages/core/src/hub/daemon/entry.ts`) hosts the
`LocalRuntimeHost` that emits `task.conversation_turn` and `task.tokens` for every
hub-backed session, so the daemon must own its own `ITelemetryService`. It builds one via
`createHubDaemonTelemetry()` (`sdk/packages/core/src/hub/daemon/telemetry.ts`), which
identifies from the cached cline account (re-resolved periodically, since the daemon often
starts before login) and flushes on every shutdown path, including startup failure.
```
{ extension_version, cline_type, platform, platform_version, os_type, os_version, is_remote_workspace }
```
The reference implementation is `apps/vscode/src/hub-daemon.ts` (PR #357). Without this
forwarding, hub-backed sessions silently drop their lifecycle telemetry.
Flag changes that remove this wiring, construct runtime hosts inside the daemon without
passing its telemetry handle, or add daemon exit paths that skip the flush — hub-backed
sessions would silently drop their lifecycle telemetry (this exact bug shipped once).
## Auth Lifecycle Completeness
@@ -120,10 +123,10 @@ canonical examples of all four phases.
## Single Telemetry Service Per Host
On VS Code, the telemetry handle is built **once** in `activate()`
(`apps/vscode/src/telemetry.ts`) and the same instance is passed into the sidebar, panel
command, and daemon spawn payload. Do not let individual controllers construct their own
`ITelemetryService` — that fragments distinct-id state, opt-out tracking, and flush ownership.
On VS Code, all callers go through the lazy `telemetryService` proxy in
`apps/vscode/src/services/telemetry/index.ts`, which constructs the service once on first
use. Do not let individual controllers construct their own `ITelemetryService` — that
fragments distinct-id state, opt-out tracking, and flush ownership.
The CLI follows the same pattern via the `getCliTelemetryService()` singleton in
`apps/cli/src/utils/telemetry.ts`, which is memoized by the activation gate in
+7 -7
View File
@@ -68,7 +68,7 @@
"command": "bun run build:webview",
"group": "build",
"problemMatcher": [],
"isBackground": true,
"isBackground": false,
"label": "npm: build:webview",
"dependsOn": [
"npm: protos"
@@ -89,7 +89,7 @@
"command": "bun run build:webview:test",
"group": "build",
"problemMatcher": [],
"isBackground": true,
"isBackground": false,
"label": "npm: build:webview:test",
"dependsOn": [
"npm: protos"
@@ -114,16 +114,16 @@
{
"pattern": [
{
"regexp": ".",
"regexp": "^(?!)((?:.*))$",
"kind": "file",
"file": 1,
"location": 2,
"message": 3
"message": 1
}
],
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "."
"beginsPattern": "^Building webview for|^\\s*VITE",
"endsPattern": "^.*Local:\\s+http://127\\.0\\.0\\.1:[0-9]+/"
}
}
],
+10
View File
@@ -1,5 +1,15 @@
# Cline CLI Changelog
## 3.0.39
- You can now select Cline free models on the ClinePass provider in the model picker
- Removed the retired ClinePass GLM 5.1 model
- Fixed OpenAI Codex model metadata under the GPT Subscription provider
- `str_replace` edits now report accurate diffs
- Fixed context compaction so canonical session history is preserved
- The detached hub daemon now emits telemetry, and telemetry identity now includes `user_id`
- Cline provider requests now send versioned client-identity headers
## 3.0.38
- New plan/act accent palette: act mode is now blue (`#79b8ff`) and plan mode amber, replacing the old cyan/yellow — applied across dialogs, the model selector, config, onboarding, markdown, and syntax highlighting, with light-theme variants tuned for contrast
+1 -9
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/cli",
"displayName": "cline",
"version": "3.0.38",
"version": "3.0.39",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"type": "module",
"publishConfig": {
@@ -68,19 +68,11 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
"@chat-adapter/discord": "^4.23.0",
"@chat-adapter/gchat": "^4.23.0",
"@chat-adapter/linear": "^4.23.0",
"@chat-adapter/slack": "^4.23.0",
"@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",
"@opentui/react": "0.1.102",
"chat": "^4.23.0",
"commander": "^14.0.3",
"fzf": "^0.5.2",
"marked": "^15.0.12",
+24 -3
View File
@@ -21,6 +21,10 @@ import type {
StopReason,
} from "@agentclientprotocol/sdk";
import { PROTOCOL_VERSION, RequestError } from "@agentclientprotocol/sdk";
import {
resolveSystemPrompt,
resolveWorkspaceRoot,
} from "@cline/cline-hub/connectors";
import {
type AgentEvent,
type ClineCore,
@@ -30,11 +34,10 @@ import {
} from "@cline/core";
import type { Message } from "@cline/shared";
import { getPersistedProviderApiKey } from "../commands/auth";
import { resolveSystemPrompt } from "../runtime/prompt";
import { subscribeToAgentEvents } from "../runtime/session-events";
import { createCliCore } from "../session/session";
import { getCliBuildInfo } from "../utils/common";
import { randomSessionId, resolveWorkspaceRoot } from "../utils/helpers";
import { randomSessionId } from "../utils/helpers";
import type { Config } from "../utils/types";
import {
ACP_AUTH_METHODS,
@@ -511,6 +514,7 @@ export class AcpAgent implements Agent {
private async buildConfig(session: SessionState): Promise<Config> {
const cwd = session.cwd || process.cwd();
const workspaceRoot = resolveWorkspaceRoot(cwd);
// Resolve credentials: env vars take precedence, then session provider.
const providerId = process.env.CLINE_PROVIDER ?? session.currentProviderId;
const apiKey = process.env.CLINE_API_KEY ?? this.authResult?.apiKey ?? "";
@@ -519,6 +523,7 @@ export class AcpAgent implements Agent {
providerId,
mode: session.currentMode,
});
const cliBuildInfo = getCliBuildInfo();
return {
providerId,
@@ -537,7 +542,23 @@ export class AcpAgent implements Agent {
enableAgentTeams: false,
enableTools: true,
cwd,
workspaceRoot: resolveWorkspaceRoot(cwd),
workspaceRoot,
extensionContext: {
client: {
name: "cline-acp",
version: cliBuildInfo.version,
platform: "cli",
platformVersion: cliBuildInfo.version,
isMultiRoot: false,
},
workspace: {
rootPath: workspaceRoot,
cwd,
workspaceName: cwd,
ide: "Terminal Shell",
platform: process.platform,
},
},
};
}
}
+24 -5
View File
@@ -1,5 +1,22 @@
import { getConnector, listConnectors } from "../connectors/registry";
import type { ConnectIo, ConnectStopResult } from "../connectors/types";
import {
type ConnectIo,
type ConnectStopResult,
getConnector,
listConnectors,
} from "@cline/cline-hub/connectors";
import { createCliLoggerAdapter } from "../logging/adapter";
import { resolveCliSessionMetadata } from "../utils/enterprise";
import { ensureOAuthProviderApiKey } from "./auth";
function withConnectorHost(io: ConnectIo): ConnectIo {
return {
...io,
createLogger: createCliLoggerAdapter,
resolveSessionMetadata: resolveCliSessionMetadata,
ensureProviderApiKey: (input) =>
ensureOAuthProviderApiKey({ ...input, io }),
};
}
export async function stopAllConnectors(
io: ConnectIo,
@@ -16,7 +33,7 @@ export async function stopAllConnectors(
continue;
}
executed += 1;
const result = await connector.stopAll(io);
const result = await connector.stopAll(withConnectorHost(io));
stoppedProcesses += result.stoppedProcesses;
stoppedSessions += result.stoppedSessions;
}
@@ -49,7 +66,9 @@ export async function runStopConnector(
io.writeErr(`connect adapter "${adapterName}" does not support stop`);
return 1;
}
const result: ConnectStopResult = await connector.stopAll(io);
const result: ConnectStopResult = await connector.stopAll(
withConnectorHost(io),
);
io.writeln(
`[connect] ${connector.name} stopped processes=${result.stoppedProcesses} sessions=${result.stoppedSessions}`,
);
@@ -66,7 +85,7 @@ export async function runConnectAdapter(
io.writeErr(`unknown connect adapter "${adapterName}"`);
return 1;
}
return connector.run(passthroughArgs, io);
return connector.run(passthroughArgs, withConnectorHost(io));
}
export function formatAdapterList(): string {
+2 -1
View File
@@ -71,8 +71,9 @@ vi.mock("@cline/core", () => ({
ensureFileExists: mockEnsureFileExists,
}));
vi.mock("../connectors/common", () => ({
vi.mock("@cline/cline-hub/connectors", () => ({
isProcessRunning: vi.fn(() => false),
listActiveConnectors: vi.fn(() => []),
}));
vi.mock("./connect", () => ({
+5 -5
View File
@@ -1,6 +1,11 @@
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
import {
type ActiveConnectorRecord,
isProcessRunning,
listActiveConnectors,
} from "@cline/cline-hub/connectors";
import {
clearHubDiscovery,
ensureFileExists,
@@ -14,11 +19,6 @@ import {
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
import { Command } from "commander";
import open from "open";
import { isProcessRunning } from "../connectors/common";
import {
type ActiveConnectorRecord,
listActiveConnectors,
} from "../connectors/status";
import { getCliBuildInfo } from "../utils/common";
import { c, writeln } from "../utils/output";
import { stopAllConnectors } from "./connect";
+2 -2
View File
@@ -11,8 +11,8 @@ vi.mock("@cline/core", () => ({
sendHubCommand: mockSendHubCommand,
}));
vi.mock("../utils/hub-runtime", () => ({
ensureCliHubServer: mockEnsureCliHubServer,
vi.mock("@cline/cline-hub/connectors", () => ({
ensureHubServer: mockEnsureCliHubServer,
parseHubEndpointOverride: (rawAddress: string | undefined) => {
const trimmed = rawAddress?.trim();
if (!trimmed) {
+5 -5
View File
@@ -1,13 +1,13 @@
import {
ensureHubServer,
parseHubEndpointOverride,
} from "@cline/cline-hub/connectors";
import {
createLocalHubScheduleRuntimeHandlers,
HubScheduleCommandService,
HubScheduleService,
sendHubCommand,
} from "@cline/core";
import {
ensureCliHubServer,
parseHubEndpointOverride,
} from "../../utils/hub-runtime";
import type { CommandIo } from "./types";
export class HubScheduleClient {
@@ -190,7 +190,7 @@ export async function ensureSchedulerHub(
}
try {
const requestedEndpoint = parseHubEndpointOverride(address);
const { url: hubUrl } = await ensureCliHubServer(
const { url: hubUrl } = await ensureHubServer(
workspaceRoot,
requestedEndpoint,
);
+2 -2
View File
@@ -1,5 +1,6 @@
import { type ChildProcess, spawn } from "node:child_process";
import { realpathSync } from "node:fs";
import { ensureHubServer } from "@cline/cline-hub/connectors";
import {
clearHubDiscovery,
isAutoUpdateEnabledGlobally,
@@ -11,7 +12,6 @@ import {
} from "@cline/core";
import { resolveClineBuildEnv } from "@cline/shared";
import { version } from "../../package.json";
import { ensureCliHubServer } from "../utils/hub-runtime";
import { c, writeErr, writeln } from "../utils/output";
import {
getInstalledKanbanVersion,
@@ -337,7 +337,7 @@ async function restartHubServerIfRunning(): Promise<void> {
// Re-ensure a fresh hub instance is spawned.
try {
await ensureCliHubServer(process.cwd()); // return value intentionally unused here
await ensureHubServer(process.cwd()); // return value intentionally unused here
writeln(`${c.green}${c.reset} ${c.dim}[hub] server restarted${c.reset}`);
} catch (err) {
writeErr(
-17
View File
@@ -1,17 +0,0 @@
export type ConnectIo = {
writeln: (text?: string) => void;
writeErr: (text: string) => void;
};
export type ConnectStopResult = {
stoppedProcesses: number;
stoppedSessions: number;
};
export interface ConnectCommandDefinition {
name: string;
description: string;
run(args: string[], io: ConnectIo): Promise<number>;
showHelp(io: ConnectIo): void;
stopAll?(io: ConnectIo): Promise<ConnectStopResult>;
}
+82 -7
View File
@@ -108,7 +108,7 @@ const loggingMocks = vi.hoisted(() => ({
flushCliLoggerAdapters: vi.fn(),
}));
const hubRuntimeMocks = vi.hoisted(() => ({
ensureCliHubServer: vi.fn(async () => ({
ensureHubServer: vi.fn(async () => ({
url: "ws://127.0.0.1:25463",
authToken: "test-token",
})),
@@ -192,8 +192,10 @@ vi.mock("./utils/feature-flags", () => ({
setCliFeatureFlagsAccountContext:
featureFlagMocks.setCliFeatureFlagsAccountContext,
}));
vi.mock("./runtime/prompt", () => ({
vi.mock("@cline/cline-hub/connectors", () => ({
resolveSystemPrompt: promptMocks.resolveSystemPrompt,
resolveWorkspaceRoot: (cwd: string) => cwd,
...hubRuntimeMocks,
}));
vi.mock("./commands/kanban", () => kanbanMocks);
vi.mock("./commands/dashboard", () => dashboardMocks);
@@ -202,7 +204,6 @@ vi.mock("./commands/update", () => updateMocks);
vi.mock("./commands/history", () => historyMocks);
vi.mock("./utils/history-resume", () => historyResumeMocks);
vi.mock("./logging/adapter", () => loggingMocks);
vi.mock("./utils/hub-runtime", () => hubRuntimeMocks);
vi.mock("./utils/telemetry", () => telemetryMocks);
vi.mock("./utils/worktree", () => worktreeMocks);
@@ -239,8 +240,8 @@ describe("runCli lightweight command dispatch", () => {
taskId: "task-1",
repoRoot: "/tmp/source",
});
hubRuntimeMocks.ensureCliHubServer.mockReset();
hubRuntimeMocks.ensureCliHubServer.mockResolvedValue({
hubRuntimeMocks.ensureHubServer.mockReset();
hubRuntimeMocks.ensureHubServer.mockResolvedValue({
url: "ws://127.0.0.1:25463",
authToken: "test-token",
});
@@ -1013,6 +1014,80 @@ describe("runCli lightweight command dispatch", () => {
);
});
it("identifies saved Cline accountId for telemetry before runtime events", async () => {
// CLINE-2406: when persisted Cline auth includes an accountId, the
// runtime path must call identifyTelemetryAccount(accountContext) so
// subsequent task.* and workspace.* events carry user_id.
const clineSettings = {
provider: "cline",
model: "anthropic/claude-sonnet-4.6",
auth: { accountId: "usr-abc-123", refreshToken: "rt-token" },
};
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue(
clineSettings,
);
providerSettingsMocks.getProviderSettings.mockReturnValue(clineSettings);
authMocks.normalizeProviderId.mockImplementation(
(providerId?: string) => providerId ?? "cline",
);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(telemetryMocks.identifyTelemetryAccount).toHaveBeenCalledWith(
expect.objectContaining({
id: "usr-abc-123",
provider: "cline",
}),
);
});
it("does not call identifyTelemetryAccount in runtime path when no saved Cline accountId", async () => {
// CLINE-2406: when no persisted accountId is found (anonymous/unauthenticated),
// identifyTelemetryAccount should not be called from the runtime path.
const clineSettings = {
provider: "cline",
model: "anthropic/claude-sonnet-4.6",
// no auth / no accountId
};
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue(
clineSettings,
);
providerSettingsMocks.getProviderSettings.mockReturnValue(clineSettings);
authMocks.normalizeProviderId.mockImplementation(
(providerId?: string) => providerId ?? "cline",
);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(telemetryMocks.identifyTelemetryAccount).not.toHaveBeenCalled();
});
it("does not call identifyTelemetryAccount from runtime path when provider is not cline", async () => {
// CLINE-2406: identity identification from saved settings only applies
// to Cline-provider sessions; other providers use different auth flows.
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue({
provider: "openrouter",
model: "openai/gpt-5",
});
providerSettingsMocks.getProviderSettings.mockReturnValue({
provider: "openrouter",
model: "openai/gpt-5",
});
authMocks.normalizeProviderId.mockImplementation(
(providerId?: string) => providerId ?? "openrouter",
);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(telemetryMocks.identifyTelemetryAccount).not.toHaveBeenCalled();
});
it("runs kanban before loading runtime modules", async () => {
process.argv = ["bun", "src/index.ts", "kanban"];
@@ -1087,7 +1162,7 @@ describe("runCli lightweight command dispatch", () => {
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledTimes(1);
expect(hubRuntimeMocks.ensureCliHubServer).not.toHaveBeenCalled();
expect(hubRuntimeMocks.ensureHubServer).not.toHaveBeenCalled();
});
it("rejects yolo runs with a single bare prompt token", async () => {
@@ -1105,7 +1180,7 @@ describe("runCli lightweight command dispatch", () => {
expect.stringContaining("Unknown command or unquoted prompt: hello"),
);
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
expect(hubRuntimeMocks.ensureCliHubServer).not.toHaveBeenCalled();
expect(hubRuntimeMocks.ensureHubServer).not.toHaveBeenCalled();
});
it("rewrites /team prompts and enables teams in single-prompt mode", async () => {
+25 -3
View File
@@ -1,6 +1,7 @@
import { fstatSync } from "node:fs";
import { homedir } from "node:os";
import { basename } from "node:path";
import { resolveWorkspaceRoot } from "@cline/cline-hub/connectors";
import type { ToolPolicy } from "@cline/core";
import { registerDisposable } from "@cline/shared";
@@ -15,6 +16,7 @@ import {
getPreferredKanbanInstaller,
} from "./commands/update";
import { CLI_DEFAULT_CHECKPOINT_CONFIG } from "./runtime/defaults";
import { getCliBuildInfo } from "./utils/common";
import {
buildCliCompactionConfig,
CLI_COMPACTION_MODE_EXPECTED_TEXT,
@@ -26,7 +28,6 @@ import {
import {
configureSandboxEnvironment,
normalizeAutoApproveArgs,
resolveWorkspaceRoot,
} from "./utils/helpers";
import {
c,
@@ -46,6 +47,7 @@ import { rewriteTeamPrompt, TEAM_COMMAND_USAGE } from "./utils/team-command";
import {
captureCliExtensionActivated,
getCliTelemetryService,
identifyTelemetryAccount,
} from "./utils/telemetry";
import type { Config } from "./utils/types";
import { runConnectWizard } from "./wizards/connect";
@@ -70,7 +72,7 @@ async function createProviderSettingsManager() {
async function loadCliRuntimeModules() {
const [coreServer, prompt, runAgentModule] = await Promise.all([
import("@cline/core"),
import("./runtime/prompt"),
import("@cline/cline-hub/connectors"),
import("./runtime/run-agent"),
]);
return {
@@ -962,6 +964,19 @@ export async function runCli(): Promise<void> {
);
let selectedProviderSettings =
providerSettingsManager.getProviderSettings(provider);
// Apply locally persisted Cline account identity so subsequent events
// (task.*, workspace.initialized) carry user_id when available.
// Note: user.extension_activated fires anonymously earlier in startup
// and cannot be retroactively updated; this is by design for
// lightweight subcommand and pre-auth CLI flows. See CLINE-2406.
if (provider === "cline") {
const savedAccountId = selectedProviderSettings?.auth?.accountId;
if (savedAccountId) {
identifyTelemetryAccount({ id: savedAccountId, provider: "cline" });
}
}
const persistedApiKey = getPersistedProviderApiKey(
provider,
selectedProviderSettings,
@@ -1029,6 +1044,7 @@ export async function runCli(): Promise<void> {
reasoningEffort: args.reasoningEffort,
persistedReasoning: selectedProviderSettings?.reasoning,
});
const cliBuildInfo = getCliBuildInfo();
const { createCliLoggerAdapter } = await import("./logging/adapter");
const loggerAdapter = createCliLoggerAdapter({
runtime: "cli",
@@ -1079,7 +1095,13 @@ export async function runCli(): Promise<void> {
cwd,
workspaceRoot,
extensionContext: {
client: { name: "cline-cli" },
client: {
name: "cline-cli",
version: cliBuildInfo.version,
platform: "cli",
platformVersion: cliBuildInfo.version,
isMultiRoot: false,
},
workspace: {
rootPath: workspaceRoot,
cwd,
@@ -1,9 +1,9 @@
import { describe, expect, it, vi } from "vitest";
import {
type ChatCommandState,
chatCommandHost,
createChatCommandHost,
} from "../../utils/chat-commands";
} from "@cline/cline-hub/connectors";
import { describe, expect, it, vi } from "vitest";
import type { Config } from "../../utils/types";
import {
type InteractiveChatCommandRuntime,
@@ -1,9 +1,9 @@
import type { InteractiveTurnResult } from "../../tui/types";
import type { ChatCommandHost } from "../../utils/chat-commands";
import type { ChatCommandHost } from "@cline/cline-hub/connectors";
import {
type ChatCommandState,
maybeHandleChatCommand,
} from "../../utils/chat-commands";
} from "@cline/cline-hub/connectors";
import type { InteractiveTurnResult } from "../../tui/types";
import {
enableTeamsForPrompt,
rewriteTeamPrompt,
@@ -1,7 +1,7 @@
import { resolveSystemPrompt } from "@cline/cline-hub/connectors";
import { createTool } from "@cline/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Config } from "../../utils/types";
import { resolveSystemPrompt } from "../prompt";
import {
ACT_MODE_CONTINUATION_PROMPT,
type AppliedModeChange,
@@ -12,7 +12,7 @@ import {
sendTurnWithActModeContinuation,
} from "./mode";
vi.mock("../prompt", () => ({
vi.mock("@cline/cline-hub/connectors", () => ({
resolveSystemPrompt: vi.fn(async (input: { mode?: string }) => {
return `system prompt for ${input.mode ?? "unknown"}`;
}),
+1 -1
View File
@@ -1,6 +1,6 @@
import { resolveSystemPrompt } from "@cline/cline-hub/connectors";
import { createTool } from "@cline/shared";
import type { Config } from "../../utils/types";
import { resolveSystemPrompt } from "../prompt";
export type InteractiveUiMode = "plan" | "act";
@@ -1,5 +1,5 @@
import type { ChatCommandState } from "@cline/cline-hub/connectors";
import type { TeamEvent } from "@cline/core";
import type { ChatCommandState } from "../../utils/chat-commands";
import type { Config } from "../../utils/types";
import {
CLI_DEFAULT_CHECKPOINT_CONFIG,
@@ -1,3 +1,4 @@
import type { ChatCommandState } from "@cline/cline-hub/connectors";
import {
createSessionCompactionState,
type ProviderSettingsManager,
@@ -9,7 +10,6 @@ import {
} from "@cline/core";
import type { AgentTool, Message } from "@cline/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ChatCommandState } from "../../utils/chat-commands";
import type { Config } from "../../utils/types";
const createCliCoreMock = vi.hoisted(() => vi.fn());
@@ -1,3 +1,7 @@
import type {
ChatCommandState,
ForkSessionResult,
} from "@cline/cline-hub/connectors";
import {
type AgentEvent,
type AgentHooks,
@@ -18,10 +22,6 @@ import {
import type { Message } from "@cline/shared";
import { createCliCore } from "../../session/session";
import { submitAndExitInTerminal } from "../../utils/approval";
import type {
ChatCommandState,
ForkSessionResult,
} from "../../utils/chat-commands";
import { createRuntimeHooks } from "../../utils/hooks";
import { setActiveCliSession } from "../../utils/output";
import { loadInteractiveResumeMessages } from "../../utils/resume";
@@ -411,43 +411,43 @@ export function createInteractiveSessionRuntime(input: {
});
};
const restartWithMessages = async (
messages: Message[],
sessionMetadata?: Record<string, unknown>,
initialCompactionState?: SessionCompactionState,
): Promise<void> => {
sessionStartGeneration += 1;
pendingResumeSessionId = undefined;
startupError = undefined;
// Publish the restart as the in-flight startup. Teardown leaves a window
// with no active session, and without this barrier a concurrent
// ensureReady() (e.g. a message submitted right after a plan/act toggle)
// reads that window as "no session" and boots an empty session that then
// races the restarted one for the active slot.
const restart = (async () => {
await stopCurrentSession();
clearActiveSession();
await startFreshSession(
messages,
sessionMetadata,
initialCompactionState,
);
})().catch((error) => {
startupError = error;
throw error;
});
startupPromise = restart;
try {
await restart;
} finally {
// Restore the pre-restart steady state (startupPromise unset) so a
// failed restart stays retryable by the next ensureReady(). A newer
// startup that already replaced the barrier is left alone.
if (startupPromise === restart) {
startupPromise = undefined;
}
const restartWithMessages = async (
messages: Message[],
sessionMetadata?: Record<string, unknown>,
initialCompactionState?: SessionCompactionState,
): Promise<void> => {
sessionStartGeneration += 1;
pendingResumeSessionId = undefined;
startupError = undefined;
// Publish the restart as the in-flight startup. Teardown leaves a window
// with no active session, and without this barrier a concurrent
// ensureReady() (e.g. a message submitted right after a plan/act toggle)
// reads that window as "no session" and boots an empty session that then
// races the restarted one for the active slot.
const restart = (async () => {
await stopCurrentSession();
clearActiveSession();
await startFreshSession(
messages,
sessionMetadata,
initialCompactionState,
);
})().catch((error) => {
startupError = error;
throw error;
});
startupPromise = restart;
try {
await restart;
} finally {
// Restore the pre-restart steady state (startupPromise unset) so a
// failed restart stays retryable by the next ensureReady(). A newer
// startup that already replaced the barrier is left alone.
if (startupPromise === restart) {
startupPromise = undefined;
}
};
}
};
const restartWithCurrentMessages = async (): Promise<void> => {
const [{ messages, status }, compactionState] = await Promise.all([
+1 -1
View File
@@ -105,7 +105,7 @@ vi.mock("./interactive-welcome", () => ({
resolveClineWelcomeLine: vi.fn(async () => undefined),
}));
vi.mock("./prompt", () => ({
vi.mock("@cline/cline-hub/connectors", () => ({
buildUserInputMessage: vi.fn(async () => ({
prompt: "prompt",
userImages: [],
+1 -1
View File
@@ -1,3 +1,4 @@
import { buildUserInputMessage } from "@cline/cline-hub/connectors";
import {
type AgentEvent,
type AgentResult,
@@ -41,7 +42,6 @@ import {
CLI_DEFAULT_LOOP_DETECTION,
} from "./defaults";
import { describeAbortSource, resolveMistakeLimitDecision } from "./format";
import { buildUserInputMessage } from "./prompt";
import { subscribeToAgentEvents } from "./session-events";
function printModelProviderInfo(config: Config): void {
+6 -3
View File
@@ -1,3 +1,9 @@
import {
buildUserInputMessage,
type ChatCommandState,
chatCommandHost,
createWorkspaceChatCommandHost,
} from "@cline/cline-hub/connectors";
import {
getCurrentContextSize,
type ProviderSettings,
@@ -24,7 +30,6 @@ import {
} from "../tui/interactive-welcome";
import { disableOpenTuiGraphicsProbe } from "../tui/opentui-env";
import type { QueuedPromptItem } from "../tui/types";
import { type ChatCommandState, chatCommandHost } from "../utils/chat-commands";
import { applyCliCompactionMode } from "../utils/compaction-mode";
import {
shouldZeroClineFreeModelCost,
@@ -36,7 +41,6 @@ import {
writeErr,
writeln,
} from "../utils/output";
import { createWorkspaceChatCommandHost } from "../utils/plugin-chat-commands";
import { readRepoStatus } from "../utils/repo-status";
import type { Config } from "../utils/types";
import {
@@ -62,7 +66,6 @@ import {
} from "./interactive/mode";
import { assertInteractivePreflight } from "./interactive/preflight";
import { createInteractiveSessionRuntime } from "./interactive/session-runtime";
import { buildUserInputMessage } from "./prompt";
import { getUIEventEmitter } from "./session-events";
type ModelChangeReasoningConfig = {
+5 -8
View File
@@ -6,7 +6,7 @@ const {
startRuntimeSession,
sendRuntimeSession,
buildUserInputMessage,
ensureCliHubServer,
ensureHubServer,
emitJsonLine,
writeErr,
writeln,
@@ -16,7 +16,7 @@ const {
startRuntimeSession: vi.fn(),
sendRuntimeSession: vi.fn(),
buildUserInputMessage: vi.fn(),
ensureCliHubServer: vi.fn(),
ensureHubServer: vi.fn(),
emitJsonLine: vi.fn(),
writeErr: vi.fn(),
writeln: vi.fn(),
@@ -31,12 +31,9 @@ vi.mock("@cline/core", () => ({
},
}));
vi.mock("./prompt", () => ({
vi.mock("@cline/cline-hub/connectors", () => ({
buildUserInputMessage,
}));
vi.mock("../utils/hub-runtime", () => ({
ensureCliHubServer,
ensureHubServer,
}));
vi.mock("../utils/output", () => ({
@@ -69,7 +66,7 @@ describe("runZen", () => {
userImages: [],
userFiles: [],
});
ensureCliHubServer.mockResolvedValue({
ensureHubServer.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
authToken: "test-token",
});
+5 -3
View File
@@ -1,11 +1,13 @@
import {
buildUserInputMessage,
ensureHubServer,
} from "@cline/cline-hub/connectors";
import type { UserInstructionConfigService } from "@cline/core";
import { HubSessionClient } from "@cline/core";
import type { ChatStartSessionRequest } from "@cline/shared";
import { resolveCliSessionMetadata } from "../utils/enterprise";
import { ensureCliHubServer } from "../utils/hub-runtime";
import { c, emitJsonLine, writeErr, writeln } from "../utils/output";
import type { Config } from "../utils/types";
import { buildUserInputMessage } from "./prompt";
const ZEN_DISPATCH_ACK_TIMEOUT_MS = 5_000;
@@ -51,7 +53,7 @@ export async function runZen(
let hubUrl: string;
let hubAuthToken: string;
try {
const hub = await ensureCliHubServer(workspaceRoot);
const hub = await ensureHubServer(workspaceRoot);
hubUrl = hub.url;
hubAuthToken = hub.authToken;
} catch (error) {
+1 -1
View File
@@ -1,3 +1,4 @@
import { resolveWorkspaceRoot } from "@cline/cline-hub/connectors";
import type {
AgentConfig,
BasicLogger,
@@ -16,7 +17,6 @@ import {
prepareCliEnterpriseIntegration,
} from "../utils/enterprise";
import { getCliFeatureFlagsService } from "../utils/feature-flags";
import { resolveWorkspaceRoot } from "../utils/helpers";
import { getCliTelemetryService } from "../utils/telemetry";
import type { ConversationHistory } from "./export";
@@ -0,0 +1,106 @@
import type {
ClineRecommendedModel,
ClineRecommendedModelsData,
} from "@cline/core";
export type ClineModelPickerTier = "recommended" | "subscribed" | "free";
export interface ClineModelPickerItem {
kind: "model";
model: ClineRecommendedModel;
tier: ClineModelPickerTier;
}
export interface ClineModelPickerBrowse {
kind: "browse";
}
export type ClineModelPickerEntry =
| ClineModelPickerItem
| ClineModelPickerBrowse;
export const CLINE_MODEL_PICKER_TIER_LABELS: Record<
ClineModelPickerTier,
string
> = {
recommended: "Recommended",
subscribed: "Subscribed",
free: "Free",
};
// Featured entries for the sectioned picker, keyed by provider: cline gets
// Recommended/Free with a browse-all escape into the full catalog; cline-pass
// gets Subscribed/Free (see buildClinePassModelEntries for why no browse-all).
export function buildFeaturedModelEntries(
providerId: string,
data: ClineRecommendedModelsData,
): ClineModelPickerEntry[] {
return providerId === "cline-pass"
? buildClinePassModelEntries(data)
: buildClineModelEntries(data);
}
function buildClineModelEntries(
data: ClineRecommendedModelsData,
): ClineModelPickerEntry[] {
const entries: ClineModelPickerEntry[] = [];
for (const m of data.recommended) {
entries.push({ kind: "model", model: m, tier: "recommended" });
}
for (const m of data.free) {
entries.push({ kind: "model", model: m, tier: "free" });
}
entries.push({ kind: "browse" });
return entries;
}
// Shown under the Free section header when picking a model for ClinePass
export const CLINE_PASS_FREE_SECTION_DESCRIPTION =
"Try with limited usage, separate from ClinePass quota.";
// ClinePass shows the subscription's models plus the Cline free models — both
// providers hit the same Cline API, so free models are selectable in place
// (they ride usage billing at $0 instead of the subscription quota).
// No "browse all" entry when the clinePass bucket is populated: unlike cline,
// the ClinePass catalog contains exactly these two buckets, so the sections
// already list every selectable model. An empty clinePass bucket means the
// fetch fell back to the bundled list (which has no pass models) — without an
// escape into the full catalog a subscriber could only pick free models, so
// browse-all comes back in that degraded mode.
function buildClinePassModelEntries(
data: ClineRecommendedModelsData,
): ClineModelPickerEntry[] {
const entries: ClineModelPickerEntry[] = [];
for (const m of data.clinePass) {
entries.push({ kind: "model", model: m, tier: "subscribed" });
}
for (const m of data.free) {
entries.push({ kind: "model", model: m, tier: "free" });
}
if (data.clinePass.length === 0) {
entries.push({ kind: "browse" });
}
return entries;
}
// The quota explainer only makes sense in the ClinePass picker, which is the
// only picker that has a "subscribed" section
export function freeTierDescriptionFor(
entries: ClineModelPickerEntry[],
): string | undefined {
const isClinePassPicker = entries.some(
(entry) => entry.kind === "model" && entry.tier === "subscribed",
);
return isClinePassPicker ? CLINE_PASS_FREE_SECTION_DESCRIPTION : undefined;
}
// OpenRouter marks free variants with "(free)" in names and ":free" in ids to
// disambiguate them from their paid twins. Inside the sectioned pickers the
// Free header already says it, so the markers are redundant — but keep them in
// flat lists (e.g. browse-all), where both variants appear side by side.
export function stripFreeMarker(displayName: string): string {
return displayName
.replace(/\s*\(free\)\s*$/i, "")
.replace(/:free$/i, "")
.trim();
}
@@ -0,0 +1,98 @@
import { describe, expect, it } from "vitest";
import {
buildFeaturedModelEntries,
CLINE_PASS_FREE_SECTION_DESCRIPTION,
freeTierDescriptionFor,
stripFreeMarker,
} from "./cline-model-entries";
const model = (id: string) => ({ id, name: id, description: "", tags: [] });
describe("cline model picker entries", () => {
it("builds Recommended/Free sections for the cline provider", () => {
const entries = buildFeaturedModelEntries("cline", {
recommended: [model("anthropic/claude-sonnet-5")],
free: [model("deepseek/deepseek-v4-flash")],
clinePass: [model("cline-pass/glm-5.1")],
});
expect(entries).toEqual([
{
kind: "model",
model: model("anthropic/claude-sonnet-5"),
tier: "recommended",
},
{
kind: "model",
model: model("deepseek/deepseek-v4-flash"),
tier: "free",
},
{ kind: "browse" },
]);
});
it("builds Subscribed/Free sections for the cline-pass provider", () => {
const entries = buildFeaturedModelEntries("cline-pass", {
recommended: [model("anthropic/claude-sonnet-5")],
free: [model("deepseek/deepseek-v4-flash")],
clinePass: [model("cline-pass/glm-5.1"), model("cline-pass/kimi-k2.6")],
});
expect(entries).toEqual([
{ kind: "model", model: model("cline-pass/glm-5.1"), tier: "subscribed" },
{
kind: "model",
model: model("cline-pass/kimi-k2.6"),
tier: "subscribed",
},
{
kind: "model",
model: model("deepseek/deepseek-v4-flash"),
tier: "free",
},
]);
});
it("adds the browse-all escape when the clinePass bucket is empty", () => {
// The fetch fell back to the bundled list (no pass models); the sections
// alone would leave a subscriber able to pick only free models.
const entries = buildFeaturedModelEntries("cline-pass", {
recommended: [],
free: [model("deepseek/deepseek-v4-flash")],
clinePass: [],
});
expect(entries).toEqual([
{
kind: "model",
model: model("deepseek/deepseek-v4-flash"),
tier: "free",
},
{ kind: "browse" },
]);
});
it("attaches the quota explainer only to the ClinePass picker's free section", () => {
const data = {
recommended: [model("anthropic/claude-sonnet-5")],
free: [model("deepseek/deepseek-v4-flash")],
clinePass: [model("cline-pass/glm-5.1")],
};
expect(
freeTierDescriptionFor(buildFeaturedModelEntries("cline-pass", data)),
).toBe(CLINE_PASS_FREE_SECTION_DESCRIPTION);
expect(
freeTierDescriptionFor(buildFeaturedModelEntries("cline", data)),
).toBe(undefined);
});
it("strips redundant free markers from display names", () => {
expect(stripFreeMarker("Laguna M.1 (free)")).toBe("Laguna M.1");
expect(stripFreeMarker("Trinity Large Preview (FREE)")).toBe(
"Trinity Large Preview",
);
expect(stripFreeMarker("laguna-m.1:free")).toBe("laguna-m.1");
expect(stripFreeMarker("DeepSeek V4 Flash")).toBe("DeepSeek V4 Flash");
});
});
@@ -1,7 +1,6 @@
// @jsxImportSource @opentui/react
import {
type ClineRecommendedModel,
type ClineRecommendedModelsData,
fetchClineRecommendedModels,
} from "@cline/core";
@@ -9,20 +8,23 @@ import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import "opentui-spinner/react";
import { palette } from "../../palette";
import {
CLINE_MODEL_PICKER_TIER_LABELS,
type ClineModelPickerEntry,
freeTierDescriptionFor,
stripFreeMarker,
} from "./cline-model-entries";
export interface ClineModelPickerItem {
kind: "model";
model: ClineRecommendedModel;
tier: "recommended" | "free";
}
export interface ClineModelPickerBrowse {
kind: "browse";
}
export type ClineModelPickerEntry =
| ClineModelPickerItem
| ClineModelPickerBrowse;
export {
buildFeaturedModelEntries,
CLINE_MODEL_PICKER_TIER_LABELS,
type ClineModelPickerBrowse,
type ClineModelPickerEntry,
type ClineModelPickerItem,
type ClineModelPickerTier,
freeTierDescriptionFor,
stripFreeMarker,
} from "./cline-model-entries";
function tagColor(tag: string): string {
if (tag === "FREE") return palette.success;
@@ -39,12 +41,13 @@ function resolveDisplayName(
for (const key of candidates) {
if (!key) continue;
const hit = knownModels[key] as { name?: string } | undefined;
if (hit?.name) return hit.name;
if (hit?.name) return stripFreeMarker(hit.name);
}
}
return modelId.includes("/")
const fallback = modelId.includes("/")
? (modelId.split("/").pop() ?? modelId)
: modelId;
return stripFreeMarker(fallback);
}
export function useClineRecommendedModels() {
@@ -68,20 +71,6 @@ export function useClineRecommendedModels() {
return { data, loading };
}
export function buildClineModelEntries(
data: ClineRecommendedModelsData,
): ClineModelPickerEntry[] {
const entries: ClineModelPickerEntry[] = [];
for (const m of data.recommended) {
entries.push({ kind: "model", model: m, tier: "recommended" });
}
for (const m of data.free) {
entries.push({ kind: "model", model: m, tier: "free" });
}
entries.push({ kind: "browse" });
return entries;
}
export function ClineModelPicker(props: {
entries: ClineModelPickerEntry[];
selected: number;
@@ -103,6 +92,7 @@ export function ClineModelPicker(props: {
let lastTier: string | null = null;
let isFirstHeader = true;
const rows: ReactNode[] = [];
const freeTierDescription = freeTierDescriptionFor(entries);
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
@@ -112,14 +102,20 @@ export function ClineModelPicker(props: {
if (entry.kind === "model") {
if (entry.tier !== lastTier) {
lastTier = entry.tier;
const label = entry.tier === "recommended" ? "Recommended" : "Free";
const label = CLINE_MODEL_PICKER_TIER_LABELS[entry.tier];
rows.push(
<box
key={`tier-${entry.tier}`}
paddingX={1}
marginTop={isFirstHeader ? 0 : 1}
flexDirection="column"
>
<text fg="gray">{label}</text>
{entry.tier === "free" && freeTierDescription && (
<text fg="gray">
<em>{freeTierDescription}</em>
</text>
)}
</box>,
);
isFirstHeader = false;
@@ -3,7 +3,12 @@ import type { ChoiceContext } from "@opentui-ui/dialog";
import { useDialogKeyboard } from "@opentui-ui/dialog/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { palette } from "../../palette";
import type { ClineModelPickerEntry } from "./cline-model-picker";
import {
CLINE_MODEL_PICKER_TIER_LABELS,
type ClineModelPickerEntry,
freeTierDescriptionFor,
stripFreeMarker,
} from "./cline-model-picker";
import { CHANGE_PROVIDER_ACTION } from "./model-selector";
import { ProviderRow } from "./provider-row";
@@ -29,12 +34,13 @@ function resolveDisplayName(
for (const key of candidates) {
if (!key) continue;
const hit = knownModels[key] as { name?: string } | undefined;
if (hit?.name) return hit.name;
if (hit?.name) return stripFreeMarker(hit.name);
}
}
return modelId.includes("/")
const fallback = modelId.includes("/")
? (modelId.split("/").pop() ?? modelId)
: modelId;
return stripFreeMarker(fallback);
}
export function ClineModelSelectorContent(
@@ -62,11 +68,13 @@ export function ClineModelSelectorContent(
key: string;
kind: "header" | "model" | "browse";
label: string;
description?: string;
tags: string[];
isCurrent: boolean;
entryIndex: number;
}[] = [];
let lastTier: string | null = null;
const freeTierDescription = freeTierDescriptionFor(entries);
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (!entry) continue;
@@ -76,7 +84,9 @@ export function ClineModelSelectorContent(
rows.push({
key: `tier-${entry.tier}`,
kind: "header",
label: entry.tier === "recommended" ? "Recommended" : "Free",
label: CLINE_MODEL_PICKER_TIER_LABELS[entry.tier],
description:
entry.tier === "free" ? freeTierDescription : undefined,
tags: [],
isCurrent: false,
entryIndex: -1,
@@ -156,8 +166,18 @@ export function ClineModelSelectorContent(
if (row.kind === "header") {
const isFirst = idx === 0;
return (
<box key={row.key} paddingX={1} marginTop={isFirst ? 0 : 1}>
<box
key={row.key}
paddingX={1}
marginTop={isFirst ? 0 : 1}
flexDirection="column"
>
<text fg="gray">{row.label}</text>
{row.description && (
<text fg="gray">
<em>{row.description}</em>
</text>
)}
</box>
);
}
+12 -3
View File
@@ -26,7 +26,7 @@ import {
ProviderPickerContent,
UseExistingOrReconfigureContent,
} from "../components/dialogs/provider-picker";
import { buildClineModelEntries } from "../components/model-selector/cline-model-picker";
import { buildFeaturedModelEntries } from "../components/model-selector/cline-model-picker";
import {
BROWSE_ALL_ACTION,
ClineModelSelectorDialogContent,
@@ -341,7 +341,13 @@ export function useModelSelector(opts: {
continue;
}
if (config.providerId === "cline") {
if (
config.providerId === "cline" ||
config.providerId === "cline-pass"
) {
// ClinePass gets the same sectioned picker with Subscribed/Free
// sections — free models are selectable while staying on ClinePass
const featuredProviderId = config.providerId;
const clineResult = await dialog.choice<string>({
style: { maxHeight: termHeight - 2 },
content: (ctx: ChoiceContext<string>) => (
@@ -351,7 +357,10 @@ export function useModelSelector(opts: {
currentProviderName={providerDisplayName}
knownModels={config.knownModels as Record<string, unknown>}
loadEntries={async () =>
buildClineModelEntries(await fetchClineRecommendedModels())
buildFeaturedModelEntries(
featuredProviderId,
await fetchClineRecommendedModels(),
)
}
/>
),
+1 -1
View File
@@ -8,7 +8,7 @@ import {
isImagePath,
loadImageAsDataUrl,
resolveExistingImagePath,
} from "../../utils/image-attachments";
} from "@cline/cline-hub/connectors";
const COMMAND_TIMEOUT_MS = 1500;
const MAX_CLIPBOARD_BYTES = 20 * 1024 * 1024;
+36 -16
View File
@@ -29,7 +29,7 @@ import {
loadIndividualSubscriptionPlansFromProviderSettings,
} from "../../cline-account";
import {
buildClineModelEntries,
buildFeaturedModelEntries,
type ClineModelPickerEntry,
useClineRecommendedModels,
} from "../../components/model-selector/cline-model-picker";
@@ -206,11 +206,14 @@ export function useOnboardingController(props: OnboardingControllerProps) {
const modelList = useSearchableList(modelItems, createCustomModelItem);
// Cline featured model picker
// Cline featured model picker (ClinePass gets Subscribed/Free sections)
const recommended = useClineRecommendedModels();
const clineEntries: ClineModelPickerEntry[] = useMemo(
() => (recommended.data ? buildClineModelEntries(recommended.data) : []),
[recommended.data],
() =>
recommended.data
? buildFeaturedModelEntries(activeProviderId, recommended.data)
: [],
[recommended.data, activeProviderId],
);
const [clineModelSelected, setClineModelSelected] = useState(0);
const [clineModelReasoningIds, setClineModelReasoningIds] = useState<
@@ -221,20 +224,37 @@ export function useOnboardingController(props: OnboardingControllerProps) {
>(undefined);
useEffect(() => {
getLocalProviderModels("cline")
.then(({ models }) => {
const ids = new Set<string>();
for (const m of models) {
// The featured picker serves both cline and cline-pass, so pool reasoning
// support and display names from both catalogs
void Promise.allSettled(
["cline", "cline-pass"].map((providerId) =>
getLocalProviderModels(providerId),
),
).then((results) => {
const ids = new Set<string>();
for (const result of results) {
if (result.status !== "fulfilled") continue;
for (const m of result.value.models) {
if (m.supportsReasoning) ids.add(m.id);
}
setClineModelReasoningIds(ids);
})
.catch(() => {});
resolveProviderConfig("cline")
.then((resolved) => {
if (resolved?.knownModels) setClineKnownModels(resolved.knownModels);
})
.catch(() => {});
}
setClineModelReasoningIds(ids);
});
void Promise.allSettled(
["cline", "cline-pass"].map((providerId) =>
resolveProviderConfig(providerId),
),
).then((results) => {
const merged: Record<string, unknown> = {};
for (const result of results) {
if (result.status === "fulfilled" && result.value?.knownModels) {
Object.assign(merged, result.value.knownModels);
}
}
if (Object.keys(merged).length > 0) {
setClineKnownModels(merged);
}
});
}, []);
// Thinking level
@@ -135,9 +135,9 @@ describe("onboarding model helpers", () => {
expect(getOAuthProviderLabel("oca")).toBe("oca");
});
it("uses the featured Cline model picker only for the Cline provider", () => {
it("uses the featured Cline model picker for the Cline and ClinePass providers", () => {
expect(shouldUseFeaturedClineModelPicker("cline")).toBe(true);
expect(shouldUseFeaturedClineModelPicker("cline-pass")).toBe(false);
expect(shouldUseFeaturedClineModelPicker("cline-pass")).toBe(true);
expect(shouldUseFeaturedClineModelPicker("anthropic")).toBe(false);
});
});
+2 -1
View File
@@ -207,5 +207,6 @@ export function getOAuthProviderLabel(providerId: string): string {
}
export function shouldUseFeaturedClineModelPicker(providerId: string): boolean {
return providerId === "cline";
// ClinePass uses the featured picker too, with Subscribed/Free sections
return providerId === "cline" || providerId === "cline-pass";
}
+37 -1
View File
@@ -1,9 +1,45 @@
import type { AgentEvent, TeamEvent } from "@cline/core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { handleEvent, handleTeamEvent } from "./events";
import {
handleEvent,
handleTeamEvent,
resolveStatusNoticeLabel,
} from "./events";
import { setCurrentOutputMode } from "./output";
import type { Config } from "./types";
describe("resolveStatusNoticeLabel", () => {
it("maps compaction status reasons to stable labels", () => {
expect(
resolveStatusNoticeLabel({
type: "notice",
noticeType: "status",
displayRole: "status",
message: "auto-compacting",
reason: "auto_compaction",
} as AgentEvent),
).toBe("auto-compacting");
expect(
resolveStatusNoticeLabel({
type: "notice",
noticeType: "status",
displayRole: "status",
message: "manual",
reason: "manual_compaction",
} as AgentEvent),
).toBe("compacting");
expect(
resolveStatusNoticeLabel({
type: "notice",
noticeType: "status",
displayRole: "status",
message: "compaction-budget-adjusted",
reason: "compaction_budget_emergency",
} as AgentEvent),
).toBe("context budget adjusted");
});
});
describe("handleEvent text formatting", () => {
let output = "";
+7 -2
View File
@@ -27,8 +27,13 @@ export function resolveStatusNoticeLabel(
if (event.type !== "notice" || event.displayRole !== "status") {
return undefined;
}
if (event.reason === "auto_compaction") {
return "auto-compacting";
switch (event.reason) {
case "auto_compaction":
return "auto-compacting";
case "manual_compaction":
return "compacting";
case "compaction_budget_emergency":
return "context budget adjusted";
}
return event.message.trim() || undefined;
}
@@ -53,6 +53,37 @@ describe("shouldZeroClineFreeModelCost", () => {
expect(fetchMock).not.toHaveBeenCalled();
});
it("zeros cost of free models selected on the cline-pass provider", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => {
return new Response(
JSON.stringify({
free: [{ id: "deepseek/deepseek-v4-flash" }],
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
}),
);
await expect(
shouldZeroClineFreeModelCost({
providerId: "cline-pass",
modelId: "deepseek/deepseek-v4-flash",
baseUrl: "https://cline.test/api/v1",
}),
).resolves.toBe(true);
// subscription (cline-pass/...) models are not in the free bucket
await expect(
shouldZeroClineFreeModelCost({
providerId: "cline-pass",
modelId: "cline-pass/glm-5.1",
baseUrl: "https://cline.test/api/v1",
}),
).resolves.toBe(false);
});
it("does not match a paid model by only the final path segment", async () => {
vi.stubGlobal(
"fetch",
+3 -1
View File
@@ -73,7 +73,9 @@ function getClineFreeModelIds(baseUrl: string): Promise<readonly string[]> {
export async function shouldZeroClineFreeModelCost(
config: Pick<Config, "providerId" | "modelId" | "baseUrl">,
): Promise<boolean> {
if (config.providerId !== "cline") return false;
// Free models are also selectable on ClinePass — they ride usage billing at $0
if (config.providerId !== "cline" && config.providerId !== "cline-pass")
return false;
const modelId = normalizeModelId(config.modelId);
if (!modelId) return false;
-14
View File
@@ -1,4 +1,3 @@
import { spawnSync } from "node:child_process";
import { appendFileSync, existsSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
@@ -40,19 +39,6 @@ export function randomSessionId(): string {
return `${Date.now()}_${nanoid(5)}_cli`;
}
export function resolveWorkspaceRoot(cwd: string): string {
const result = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
encoding: "utf8",
});
if (result.status === 0) {
const value = result.stdout.trim();
if (value) {
return value;
}
}
return cwd;
}
export function truncate(str: string, maxLen: number): string {
const oneLine = str.replace(/\n/g, " ").trim();
if (oneLine.length <= maxLen) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { resolveCliLaunchSpec } from "./internal-launch";
import { resolveCliLaunchSpec } from "@cline/cline-hub/connectors";
export interface HistoryResumeCommand {
launcher: string;
+13 -2
View File
@@ -5,7 +5,8 @@
"description": "Browser dashboard for the Cline hub: live clients, sessions, streaming chat, and hub restart.",
"type": "module",
"exports": {
".": "./src/server.ts"
".": "./src/server.ts",
"./connectors": "./src/connectors/index.ts"
},
"scripts": {
"build:webview": "bun run --cwd src/webview build",
@@ -16,8 +17,18 @@
"typecheck": "bun tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@chat-adapter/discord": "^4.23.0",
"@chat-adapter/gchat": "^4.23.0",
"@chat-adapter/linear": "^4.23.0",
"@chat-adapter/slack": "^4.23.0",
"@chat-adapter/telegram": "^4.23.0",
"@chat-adapter/whatsapp": "^4.23.0",
"@cline/core": "workspace:*",
"@cline/llms": "workspace:*",
"@cline/shared": "workspace:*"
"@cline/shared": "workspace:*",
"@gramio/format": "^0.7.0",
"chat": "^4.23.0",
"commander": "^14.0.3",
"zod": "^4.3.6"
}
}
@@ -2,24 +2,15 @@ import {
createDiscordAdapter,
type DiscordAdapter,
} from "@chat-adapter/discord";
import type { ChatStartSessionRequest } from "@cline/core";
import {
createUserInstructionConfigService,
HubSessionClient,
} from "@cline/core";
import { createUserInstructionConfigService } from "@cline/core";
import { HubSessionClient } from "@cline/core/hub";
import type {
ChatStartSessionRequest,
ConnectDiscordOptions,
DiscordConnectorState,
} from "@cline/shared";
import { Chat, ConsoleLogger, type Thread, ThreadImpl } from "chat";
import type { Command } from "commander";
import { createCliLoggerAdapter } from "../../logging/adapter";
import {
ensureCliHubServer,
parseHubEndpointOverride,
resolveDefaultCliRpcAddress,
} from "../../utils/hub-runtime";
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
import { ConnectorBase } from "../base";
import {
createChatSdkLogger,
@@ -33,6 +24,13 @@ import {
maybeHandleConnectorApprovalReply,
} from "../connector-host";
import { dispatchConnectorHook } from "../hooks";
import {
ensureHubServer,
parseHubEndpointOverride,
resolveDefaultHubRpcAddress,
} from "../hub-runtime";
import { createConnectorLogger } from "../logger";
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
import {
type PendingConnectorApproval,
truncateConnectorText,
@@ -686,7 +684,7 @@ function isRestorableThread(
async function restoreDiscordThreadSubscriptions(input: {
bot: Pick<Chat, "reviver">;
bindingsPath: string;
logger: ReturnType<typeof createCliLoggerAdapter>;
logger: ReturnType<typeof createConnectorLogger>;
}): Promise<number> {
const bindings = readBindings<DiscordThreadState>(input.bindingsPath);
const restoredThreadIds = new Set<string>();
@@ -761,7 +759,7 @@ class DiscordConnector extends ConnectorBase<
.option(
"--rpc-address <host:port>",
"RPC address",
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
)
.option("--host <host>", "Webhook listen host")
.option("--port <port>", "Webhook listen port")
@@ -859,7 +857,7 @@ class DiscordConnector extends ConnectorBase<
rpcAddress:
opts.rpcAddress?.trim() ||
process.env.CLINE_RPC_ADDRESS?.trim() ||
resolveDefaultCliRpcAddress(),
resolveDefaultHubRpcAddress(),
hookCommand:
opts.hookCommand?.trim() ||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
@@ -993,7 +991,7 @@ class DiscordConnector extends ConnectorBase<
return 0;
}
const loggerAdapter = createCliLoggerAdapter({
const loggerAdapter = createConnectorLogger(io, {
runtime: "cli",
component: "discord-connect",
});
@@ -1039,11 +1037,10 @@ class DiscordConnector extends ConnectorBase<
cwd: commandCwd,
workspaceRoot: startRequest.workspaceRoot || commandCwd,
});
const { url: rpcAddress, authToken: rpcAuthToken } =
await ensureCliHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const clientId = `discord-${process.pid}-${Date.now()}`;
const client = new HubSessionClient({
@@ -1139,6 +1136,7 @@ class DiscordConnector extends ConnectorBase<
resolveMuteTarget: ({ target }) => resolveDiscordMuteTarget(target),
createEmptyRuntimeReplyResolver:
createDiscordEmptyRuntimeReplyResolver,
resolveSessionMetadata: io.resolveSessionMetadata,
getSessionMetadata: (currentThread, _clientId, currentState) => ({
userName: options.userName,
applicationId: options.applicationId,
@@ -1,23 +1,13 @@
import { createGoogleChatAdapter } from "@chat-adapter/gchat";
import type { ChatStartSessionRequest } from "@cline/core";
import {
createUserInstructionConfigService,
HubSessionClient,
} from "@cline/core";
import { createUserInstructionConfigService } from "@cline/core";
import { HubSessionClient } from "@cline/core/hub";
import type {
ChatStartSessionRequest,
ConnectGoogleChatOptions,
GoogleChatConnectorState,
} from "@cline/shared";
import { Chat, ConsoleLogger, type Thread } from "chat";
import type { Command } from "commander";
import type { CliLoggerAdapter } from "../../logging/adapter";
import { createCliLoggerAdapter } from "../../logging/adapter";
import {
ensureCliHubServer,
parseHubEndpointOverride,
resolveDefaultCliRpcAddress,
} from "../../utils/hub-runtime";
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
import { ConnectorBase } from "../base";
import {
createChatSdkLogger,
@@ -31,6 +21,13 @@ import {
maybeHandleConnectorApprovalReply,
} from "../connector-host";
import { dispatchConnectorHook } from "../hooks";
import {
ensureHubServer,
parseHubEndpointOverride,
resolveDefaultHubRpcAddress,
} from "../hub-runtime";
import { createConnectorLogger } from "../logger";
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
import {
type PendingConnectorApproval,
truncateConnectorText,
@@ -55,6 +52,7 @@ import {
import type {
ConnectCommandDefinition,
ConnectIo,
ConnectorLoggerAdapter,
ConnectStopResult,
} from "../types";
import {
@@ -166,7 +164,7 @@ async function persistGoogleChatThreadContext(input: {
async function deliverScheduledResult(input: {
bot: Chat;
client: HubSessionClient;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
bindingsPath: string;
userName: string;
scheduleId: string;
@@ -253,7 +251,7 @@ class GoogleChatConnector extends ConnectorBase<
.option(
"--rpc-address <host:port>",
"RPC address",
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
)
.option("--host <host>", "Webhook listen host")
.option("--port <port>", "Webhook listen port")
@@ -319,7 +317,7 @@ class GoogleChatConnector extends ConnectorBase<
rpcAddress:
opts.rpcAddress?.trim() ||
process.env.CLINE_RPC_ADDRESS?.trim() ||
resolveDefaultCliRpcAddress(),
resolveDefaultHubRpcAddress(),
hookCommand:
opts.hookCommand?.trim() ||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
@@ -446,7 +444,7 @@ class GoogleChatConnector extends ConnectorBase<
return 0;
}
const loggerAdapter = createCliLoggerAdapter({
const loggerAdapter = createConnectorLogger(io, {
runtime: "cli",
component: "gchat-connect",
});
@@ -543,11 +541,10 @@ class GoogleChatConnector extends ConnectorBase<
cwd: commandCwd,
workspaceRoot: startRequest.workspaceRoot || commandCwd,
});
const { url: rpcAddress, authToken: rpcAuthToken } =
await ensureCliHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const clientId = `gchat-${process.pid}-${Date.now()}`;
const client = new HubSessionClient({
@@ -614,6 +611,7 @@ class GoogleChatConnector extends ConnectorBase<
chatCommandHost,
activeTurns,
turnKey: queueKey,
resolveSessionMetadata: io.resolveSessionMetadata,
getSessionMetadata: (currentThread, _clientId, currentState) => ({
userName: options.userName,
gchatThreadId: currentThread.id,
@@ -1,19 +1,12 @@
import type { ChatStartSessionRequest } from "@cline/core";
import {
createUserInstructionConfigService,
HubSessionClient,
} from "@cline/core";
import type { ConnectLinearOptions, LinearConnectorState } from "@cline/shared";
import { createUserInstructionConfigService } from "@cline/core";
import { HubSessionClient } from "@cline/core/hub";
import type {
ChatStartSessionRequest,
ConnectLinearOptions,
LinearConnectorState,
} from "@cline/shared";
import { type Adapter, Chat, ConsoleLogger, type Thread } from "chat";
import type { Command } from "commander";
import type { CliLoggerAdapter } from "../../logging/adapter";
import { createCliLoggerAdapter } from "../../logging/adapter";
import {
ensureCliHubServer,
parseHubEndpointOverride,
resolveDefaultCliRpcAddress,
} from "../../utils/hub-runtime";
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
import { ConnectorBase } from "../base";
import {
createChatSdkLogger,
@@ -27,6 +20,13 @@ import {
maybeHandleConnectorApprovalReply,
} from "../connector-host";
import { dispatchConnectorHook } from "../hooks";
import {
ensureHubServer,
parseHubEndpointOverride,
resolveDefaultHubRpcAddress,
} from "../hub-runtime";
import { createConnectorLogger } from "../logger";
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
import {
type PendingConnectorApproval,
truncateConnectorText,
@@ -51,6 +51,7 @@ import {
import type {
ConnectCommandDefinition,
ConnectIo,
ConnectorLoggerAdapter,
ConnectStopResult,
} from "../types";
import { getConnectorSystemPrompt } from "./prompts";
@@ -204,7 +205,7 @@ async function persistLinearThreadContext(input: {
async function deliverScheduledResult(input: {
bot: Chat;
client: HubSessionClient;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
bindingsPath: string;
userName: string;
scheduleId: string;
@@ -313,7 +314,7 @@ class LinearConnector extends ConnectorBase<
.option(
"--rpc-address <host:port>",
"RPC address",
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
)
.option("--host <host>", "Webhook listen host")
.option("--port <port>", "Webhook listen port")
@@ -399,7 +400,7 @@ class LinearConnector extends ConnectorBase<
rpcAddress:
opts.rpcAddress?.trim() ||
process.env.CLINE_RPC_ADDRESS?.trim() ||
resolveDefaultCliRpcAddress(),
resolveDefaultHubRpcAddress(),
hookCommand:
opts.hookCommand?.trim() ||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
@@ -513,7 +514,7 @@ class LinearConnector extends ConnectorBase<
return 0;
}
const loggerAdapter = createCliLoggerAdapter({
const loggerAdapter = createConnectorLogger(io, {
runtime: "cli",
component: "linear-connect",
});
@@ -578,11 +579,10 @@ class LinearConnector extends ConnectorBase<
cwd: commandCwd,
workspaceRoot: startRequest.workspaceRoot || commandCwd,
});
const { url: rpcAddress, authToken: rpcAuthToken } =
await ensureCliHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const clientId = `linear-${process.pid}-${Date.now()}`;
const client = new HubSessionClient({
@@ -651,6 +651,7 @@ class LinearConnector extends ConnectorBase<
chatCommandHost,
activeTurns,
turnKey: queueKey,
resolveSessionMetadata: io.resolveSessionMetadata,
getSessionMetadata: (currentThread, _clientId, currentState) => ({
userName: options.userName,
linearThreadId: currentThread.id,
@@ -1,10 +1,11 @@
import { createSlackAdapter, type SlackAdapter } from "@chat-adapter/slack";
import type { ChatStartSessionRequest } from "@cline/core";
import {
createUserInstructionConfigService,
HubSessionClient,
} from "@cline/core";
import type { ConnectSlackOptions, SlackConnectorState } from "@cline/shared";
import { createUserInstructionConfigService } from "@cline/core";
import { HubSessionClient } from "@cline/core/hub";
import type {
ChatStartSessionRequest,
ConnectSlackOptions,
SlackConnectorState,
} from "@cline/shared";
import {
type Adapter,
Chat,
@@ -14,14 +15,6 @@ import {
ThreadImpl,
} from "chat";
import type { Command } from "commander";
import type { CliLoggerAdapter } from "../../logging/adapter";
import { createCliLoggerAdapter } from "../../logging/adapter";
import {
ensureCliHubServer,
parseHubEndpointOverride,
resolveDefaultCliRpcAddress,
} from "../../utils/hub-runtime";
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
import { ConnectorBase } from "../base";
import {
createChatSdkLogger,
@@ -35,6 +28,13 @@ import {
maybeHandleConnectorApprovalReply,
} from "../connector-host";
import { dispatchConnectorHook } from "../hooks";
import {
ensureHubServer,
parseHubEndpointOverride,
resolveDefaultHubRpcAddress,
} from "../hub-runtime";
import { createConnectorLogger } from "../logger";
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
import {
type PendingConnectorApproval,
truncateConnectorText,
@@ -61,6 +61,7 @@ import {
import type {
ConnectCommandDefinition,
ConnectIo,
ConnectorLoggerAdapter,
ConnectStopResult,
} from "../types";
import {
@@ -351,7 +352,7 @@ async function deliverScheduledResult(input: {
bot: Chat;
slack: SlackAdapter;
client: HubSessionClient;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
bindingsPath: string;
userName: string;
scheduleId: string;
@@ -481,7 +482,7 @@ class SlackConnector extends ConnectorBase<
.option(
"--rpc-address <host:port>",
"RPC address",
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
)
.option("--host <host>", "Webhook listen host")
.option("--port <port>", "Webhook listen port")
@@ -592,7 +593,7 @@ class SlackConnector extends ConnectorBase<
rpcAddress:
opts.rpcAddress?.trim() ||
process.env.CLINE_RPC_ADDRESS?.trim() ||
resolveDefaultCliRpcAddress(),
resolveDefaultHubRpcAddress(),
hookCommand:
opts.hookCommand?.trim() ||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
@@ -707,7 +708,7 @@ class SlackConnector extends ConnectorBase<
return 0;
}
const loggerAdapter = createCliLoggerAdapter({
const loggerAdapter = createConnectorLogger(io, {
runtime: "cli",
component: "slack-connect",
});
@@ -772,11 +773,10 @@ class SlackConnector extends ConnectorBase<
cwd: commandCwd,
workspaceRoot: startRequest.workspaceRoot || commandCwd,
});
const { url: rpcAddress, authToken: rpcAuthToken } =
await ensureCliHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const clientId = `slack-${process.pid}-${Date.now()}`;
const client = new HubSessionClient({
@@ -856,6 +856,7 @@ class SlackConnector extends ConnectorBase<
chatCommandHost,
activeTurns,
turnKey: queueKey,
resolveSessionMetadata: io.resolveSessionMetadata,
getSessionMetadata: (
currentThread,
_clientId,
@@ -1,7 +1,7 @@
import type { SentMessage, Thread } from "chat";
import { describe, expect, it, vi } from "vitest";
import type { CliLoggerAdapter } from "../../logging/adapter";
import type { ConnectorThreadState } from "../thread-bindings";
import type { ConnectorLoggerAdapter } from "../types";
import {
buildTelegramFormattedPayload,
buildTelegramFormattedPayloads,
@@ -16,7 +16,7 @@ function createLogger() {
log: vi.fn(),
error: vi.fn(),
},
} as unknown as CliLoggerAdapter;
} as unknown as ConnectorLoggerAdapter;
}
function createThread(id = "telegram:123") {
@@ -1,7 +1,7 @@
import { markdownToFormattable } from "@gramio/format/markdown";
import type { Thread } from "chat";
import type { CliLoggerAdapter } from "../../logging/adapter";
import type { ConnectorThreadState } from "../thread-bindings";
import type { ConnectorLoggerAdapter } from "../types";
const TELEGRAM_API_BASE = "https://api.telegram.org";
const TELEGRAM_MESSAGE_LIMIT = 4096;
@@ -211,7 +211,7 @@ export async function postTelegramFormattedReply<
thread: Thread<TState>;
text: string;
botToken: string;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
apiBaseUrl?: string;
fetchImpl?: FetchLike;
}): Promise<void> {
@@ -1,23 +1,13 @@
import { createTelegramAdapter } from "@chat-adapter/telegram";
import type { ChatStartSessionRequest } from "@cline/core";
import {
createUserInstructionConfigService,
HubSessionClient,
} from "@cline/core";
import { createUserInstructionConfigService } from "@cline/core";
import { HubSessionClient } from "@cline/core/hub";
import type {
ChatStartSessionRequest,
ConnectTelegramOptions,
TelegramConnectorState,
} from "@cline/shared";
import { Chat, ConsoleLogger, type Thread } from "chat";
import type { Command } from "commander";
import type { CliLoggerAdapter } from "../../logging/adapter";
import { createCliLoggerAdapter } from "../../logging/adapter";
import {
ensureCliHubServer,
parseHubEndpointOverride,
resolveDefaultCliRpcAddress,
} from "../../utils/hub-runtime";
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
import { ConnectorBase } from "../base";
import { createChatSdkLogger, enqueueThreadTurn } from "../chat-runtime";
import { isProcessRunning } from "../common";
@@ -27,6 +17,13 @@ import {
maybeHandleConnectorApprovalReply,
} from "../connector-host";
import { dispatchConnectorHook } from "../hooks";
import {
ensureHubServer,
parseHubEndpointOverride,
resolveDefaultHubRpcAddress,
} from "../hub-runtime";
import { createConnectorLogger } from "../logger";
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
import {
type PendingConnectorApproval,
truncateConnectorText,
@@ -51,6 +48,7 @@ import {
import type {
ConnectCommandDefinition,
ConnectIo,
ConnectorLoggerAdapter,
ConnectStopResult,
} from "../types";
import {
@@ -268,7 +266,7 @@ async function persistTelegramThreadContext(input: {
async function deliverScheduledResult(input: {
bot: Chat;
client: HubSessionClient;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
bindingsPath: string;
botUsername: string;
scheduleId: string;
@@ -443,7 +441,7 @@ class TelegramConnector extends ConnectorBase<
.option(
"--rpc-address <host:port>",
"RPC address",
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
)
.addHelpText(
"after",
@@ -508,7 +506,7 @@ class TelegramConnector extends ConnectorBase<
rpcAddress:
opts.rpcAddress?.trim() ||
process.env.CLINE_RPC_ADDRESS?.trim() ||
resolveDefaultCliRpcAddress(),
resolveDefaultHubRpcAddress(),
hookCommand: allowedUserId
? buildTelegramAllowedUserHookCommand(
normalizeAllowedTelegramUserId(allowedUserId),
@@ -670,7 +668,7 @@ class TelegramConnector extends ConnectorBase<
return 0;
}
const loggerAdapter = createCliLoggerAdapter({
const loggerAdapter = createConnectorLogger(io, {
runtime: "cli",
component: "telegram-connect",
});
@@ -713,11 +711,10 @@ class TelegramConnector extends ConnectorBase<
cwd: commandCwd,
workspaceRoot: startRequest.workspaceRoot || commandCwd,
});
const { url: rpcAddress, authToken: rpcAuthToken } =
await ensureCliHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const clientId = `telegram-${process.pid}-${Date.now()}`;
const client = new HubSessionClient({
@@ -826,6 +823,7 @@ class TelegramConnector extends ConnectorBase<
logger: loggerAdapter,
});
},
resolveSessionMetadata: io.resolveSessionMetadata,
getSessionMetadata: (currentThread, _clientId, currentState) => ({
botUserName: options.botUsername,
telegramThreadId: currentThread.id,
@@ -1,23 +1,13 @@
import { createWhatsAppAdapter } from "@chat-adapter/whatsapp";
import type { ChatStartSessionRequest } from "@cline/core";
import {
createUserInstructionConfigService,
HubSessionClient,
} from "@cline/core";
import { createUserInstructionConfigService } from "@cline/core";
import { HubSessionClient } from "@cline/core/hub";
import type {
ChatStartSessionRequest,
ConnectWhatsAppOptions,
WhatsAppConnectorState,
} from "@cline/shared";
import { Chat, ConsoleLogger, type Thread } from "chat";
import type { Command } from "commander";
import type { CliLoggerAdapter } from "../../logging/adapter";
import { createCliLoggerAdapter } from "../../logging/adapter";
import {
ensureCliHubServer,
parseHubEndpointOverride,
resolveDefaultCliRpcAddress,
} from "../../utils/hub-runtime";
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
import { ConnectorBase } from "../base";
import {
createChatSdkLogger,
@@ -31,6 +21,13 @@ import {
maybeHandleConnectorApprovalReply,
} from "../connector-host";
import { dispatchConnectorHook } from "../hooks";
import {
ensureHubServer,
parseHubEndpointOverride,
resolveDefaultHubRpcAddress,
} from "../hub-runtime";
import { createConnectorLogger } from "../logger";
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
import {
type PendingConnectorApproval,
truncateConnectorText,
@@ -55,6 +52,7 @@ import {
import type {
ConnectCommandDefinition,
ConnectIo,
ConnectorLoggerAdapter,
ConnectStopResult,
} from "../types";
import {
@@ -190,7 +188,7 @@ async function persistWhatsAppThreadContext(input: {
async function deliverScheduledResult(input: {
bot: Chat;
client: HubSessionClient;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
bindingsPath: string;
options: ConnectWhatsAppOptions;
scheduleId: string;
@@ -296,7 +294,7 @@ class WhatsAppConnector extends ConnectorBase<
.option(
"--rpc-address <host:port>",
"RPC address",
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
)
.option("--host <host>", "Webhook listen host")
.option("--port <port>", "Webhook listen port")
@@ -367,7 +365,7 @@ class WhatsAppConnector extends ConnectorBase<
rpcAddress:
opts.rpcAddress?.trim() ||
process.env.CLINE_RPC_ADDRESS?.trim() ||
resolveDefaultCliRpcAddress(),
resolveDefaultHubRpcAddress(),
hookCommand:
opts.hookCommand?.trim() ||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
@@ -485,7 +483,7 @@ class WhatsAppConnector extends ConnectorBase<
return 0;
}
const loggerAdapter = createCliLoggerAdapter({
const loggerAdapter = createConnectorLogger(io, {
runtime: "cli",
component: "whatsapp-connect",
});
@@ -545,11 +543,10 @@ class WhatsAppConnector extends ConnectorBase<
cwd: commandCwd,
workspaceRoot: startRequest.workspaceRoot || commandCwd,
});
const { url: rpcAddress, authToken: rpcAuthToken } =
await ensureCliHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
parseHubEndpointOverride(options.rpcAddress),
);
const clientId = `whatsapp-${process.pid}-${Date.now()}`;
const client = new HubSessionClient({
@@ -623,6 +620,7 @@ class WhatsAppConnector extends ConnectorBase<
chatCommandHost,
activeTurns,
turnKey: queueKey,
resolveSessionMetadata: io.resolveSessionMetadata,
getSessionMetadata: (currentThread, _clientId, currentState) => ({
userName: options.userName,
phoneNumberId: options.phoneNumberId,
@@ -1,6 +1,6 @@
import { existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { resolveClineDataDir } from "@cline/core";
import { resolveClineDataDir } from "@cline/shared/storage";
import { Command, CommanderError } from "commander";
import {
isProcessRunning,
@@ -158,6 +158,7 @@ export abstract class ConnectorBase<Options, State>
["connect", this.name],
input.rawArgs,
input.childEnvVar,
input.io,
);
if (!pid) {
input.io.writeErr(input.launchFailureMessage);
@@ -1,6 +1,6 @@
import { stat } from "node:fs/promises";
import { resolve } from "node:path";
import { resolveWorkspaceRoot } from "./helpers";
import { resolveWorkspaceRoot } from "./workspace";
export type ChatCommandState = {
enableTools: boolean;
@@ -1,6 +1,6 @@
import type { CliLoggerAdapter } from "../logging/adapter";
import type { ConnectorLoggerAdapter } from "./types";
export function createChatSdkLogger(adapter: CliLoggerAdapter) {
export function createChatSdkLogger(adapter: ConnectorLoggerAdapter) {
return {
child(prefix: string) {
return createChatSdkLogger(adapter.child({ chatLogger: prefix }));
@@ -80,18 +80,20 @@ export async function startConnectorWebhookServer(input: {
const hostHeader = req.headers.host || `${input.host}:${input.port}`;
const requestUrl = new URL(req.url || "/", `http://${hostHeader}`);
const body = await readRequestBody(req);
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (Array.isArray(value)) {
for (const entry of value) {
headers.append(key, entry);
}
} else if (typeof value === "string") {
headers.append(key, value);
}
}
const request = new Request(requestUrl.toString(), {
method: req.method,
headers: new Headers(
Object.entries(req.headers).flatMap(([key, value]) => {
if (Array.isArray(value)) {
return value.map((entry) => [key, entry] as [string, string]);
}
return typeof value === "string" ? [[key, value]] : [];
}),
),
headers,
body,
duplex: body ? "half" : undefined,
});
const handler =
input.routes[requestUrl.pathname] ??
@@ -1,4 +1,4 @@
import { dirname, resolve } from "node:path";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
@@ -26,16 +26,15 @@ describe("spawnDetachedConnector", () => {
});
it("preserves bun conditions and resolves the cli entrypoint for detached launches", () => {
const connectorsDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(connectorsDir, "../../../../");
const entryPath = fileURLToPath(import.meta.url);
expect(
__test__.buildDetachedConnectorCommand(
["connect", "telegram"],
["-m", "ClineAdapterBot", "-k", "token-123"],
"/Users/test/.bun/bin/bun",
"./apps/cli/src/index.ts",
entryPath,
["--conditions=development"],
repoRoot,
dirname(entryPath),
{},
),
).toEqual({
@@ -44,7 +43,7 @@ describe("spawnDetachedConnector", () => {
"--inspect=127.0.0.1:0",
"--enable-source-maps",
"--conditions=development",
resolve(repoRoot, "apps/cli/src/index.ts"),
entryPath,
"connect",
"telegram",
"-m",
@@ -57,16 +56,15 @@ describe("spawnDetachedConnector", () => {
});
it("uses a dynamic connector inspector port for development node launches", () => {
const connectorsDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(connectorsDir, "../../../../");
const entryPath = fileURLToPath(import.meta.url);
expect(
__test__.buildDetachedConnectorCommand(
["connect", "telegram"],
["-m", "ClineAdapterBot"],
"/usr/local/bin/node",
"./apps/cli/src/index.ts",
entryPath,
[],
repoRoot,
dirname(entryPath),
{ CLINE_BUILD_ENV: "development" },
),
).toEqual({
@@ -74,7 +72,7 @@ describe("spawnDetachedConnector", () => {
childArgs: [
"--inspect=127.0.0.1:0",
"--enable-source-maps",
resolve(repoRoot, "apps/cli/src/index.ts"),
entryPath,
"connect",
"telegram",
"-m",
@@ -8,12 +8,12 @@ import {
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import type { HubSessionClient, HubSessionRow } from "@cline/core";
import { ensureParentDir, resolveClineDataDir } from "@cline/core";
import type { HubSessionClient, HubSessionRow } from "@cline/core/hub";
import { withResolvedClineBuildEnv } from "@cline/shared";
import { createCliLoggerAdapter } from "../logging/adapter";
import { logSpawnedProcess } from "../logging/process";
import { resolveCliLaunchSpec } from "../utils/internal-launch";
import { ensureParentDir, resolveClineDataDir } from "@cline/shared/storage";
import { resolveCliLaunchSpec } from "./internal-launch";
import { createConnectorLogger } from "./logger";
import type { ConnectIo } from "./types";
export function parseBooleanFlag(rawArgs: string[], flag: string): boolean {
return rawArgs.includes(flag);
@@ -154,6 +154,7 @@ export function spawnDetachedConnector(
commandPrefixArgs: string[],
rawArgs: string[],
childEnvKey: string,
io: ConnectIo,
options?: {
logPath?: string;
component?: string;
@@ -163,7 +164,7 @@ export function spawnDetachedConnector(
const command = buildDetachedConnectorCommand(commandPrefixArgs, rawArgs);
if (!command) {
try {
const logger = createCliLoggerAdapter({
const logger = createConnectorLogger(io, {
runtime: "cli",
component: options?.component ?? "connectors",
});
@@ -198,24 +199,26 @@ export function spawnDetachedConnector(
// processes otherwise allocate a new visible console.
windowsHide: true,
});
logSpawnedProcess({
createConnectorLogger(io, {
runtime: "cli",
component: options?.component ?? "connectors",
command: [command.launcher, ...command.childArgs],
}).core.log("Process spawned", {
command: [command.launcher, ...command.childArgs].join(" "),
commandArgs: command.childArgs,
executable: command.launcher,
childPid: child.pid ?? undefined,
cwd: process.cwd(),
detached: true,
metadata: {
childEnvKey,
purpose: "connector.detached",
logPath: options?.logPath,
...options?.metadata,
},
childEnvKey,
purpose: "connector.detached",
logPath: options?.logPath,
...options?.metadata,
});
child.unref();
return child.pid ?? 0;
} catch (error) {
try {
const logger = createCliLoggerAdapter({
const logger = createConnectorLogger(io, {
runtime: "cli",
component: options?.component ?? "connectors",
});
@@ -1,14 +1,12 @@
import { readFileSync } from "node:fs";
import { basename } from "node:path";
import type { UserInstructionConfigService } from "@cline/core";
import type { HubSessionClient } from "@cline/core/hub";
import type {
ChatRunTurnRequest,
ChatStartSessionRequest,
HubSessionClient,
UserInstructionConfigService,
} from "@cline/core";
} from "@cline/shared";
import type { SentMessage, Thread } from "chat";
import type { CliLoggerAdapter } from "../logging/adapter";
import { buildUserInputMessage, resolveSystemPrompt } from "../runtime/prompt";
import {
type ChatCommandHost,
type ChatCommandState,
@@ -16,8 +14,9 @@ import {
type MuteCommandInput,
maybeHandleChatCommand,
normalizeCommandName,
} from "../utils/chat-commands";
} from "./chat-commands";
import { authorizeConnectorEvent, dispatchConnectorHook } from "./hooks";
import { buildUserInputMessage, resolveSystemPrompt } from "./prompt";
import {
createConnectorRuntimeTurnStream,
formatConnectorApprovalPrompt,
@@ -44,6 +43,7 @@ import {
setParticipantMuted,
setThreadMuted,
} from "./thread-bindings";
import type { ConnectIo, ConnectorLoggerAdapter } from "./types";
export type ActiveConnectorTurn = {
sessionId: string;
@@ -211,7 +211,7 @@ export async function handleConnectorUserTurn<
baseStartRequest: ChatStartSessionRequest;
explicitSystemPrompt: string | undefined;
clientId: string;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
transport: string;
botUserName?: string;
ownerParticipantKeys?: string[];
@@ -225,6 +225,7 @@ export async function handleConnectorUserTurn<
clientId: string,
currentState: TState,
) => Record<string, unknown>;
resolveSessionMetadata?: ConnectIo["resolveSessionMetadata"];
getScheduleDeliveryMetadata?: (
thread: Thread<TState>,
) => Record<string, unknown>;
@@ -939,6 +940,7 @@ export async function handleConnectorUserTurn<
input.clientId,
currentState,
),
resolveSessionMetadata: input.resolveSessionMetadata,
reusedLogMessage: input.reusedLogMessage,
startedLogMessage: input.startedLogMessage,
});
@@ -5,7 +5,7 @@ import type {
ConnectorHookEvent,
} from "@cline/shared";
import { z } from "zod";
import type { CliLoggerAdapter } from "../logging/adapter";
import type { ConnectorLoggerAdapter } from "./types";
const ConnectorAuthorizationDecisionSchema = z.object({
action: z.enum(["allow", "deny"]).default("allow"),
@@ -17,7 +17,7 @@ const ConnectorAuthorizationDecisionSchema = z.object({
export async function dispatchConnectorHook(
command: string | undefined,
hookPayload: ConnectorHookEvent,
logger: CliLoggerAdapter,
logger: ConnectorLoggerAdapter,
): Promise<void> {
const trimmed = command?.trim();
if (!trimmed) {
@@ -70,7 +70,7 @@ export async function authorizeConnectorEvent(
botUserName?: string;
request: ConnectorAuthorizationRequest;
},
logger: CliLoggerAdapter,
logger: ConnectorLoggerAdapter,
): Promise<ConnectorAuthorizationDecision> {
const trimmed = command?.trim();
if (!trimmed) {
@@ -4,14 +4,14 @@ import {
type HubEndpointOverrides,
resolveDefaultHubHost,
resolveDefaultHubPort,
} from "@cline/core";
} from "@cline/core/hub";
/**
* Build a `host:port` rpc address string that respects the current build
* environment. In development, this picks the dev hub port to avoid
* colliding with a production Cline hub on the standard port.
*/
export function resolveDefaultCliRpcAddress(): string {
export function resolveDefaultHubRpcAddress(): string {
return `${resolveDefaultHubHost()}:${resolveDefaultHubPort()}`;
}
@@ -39,7 +39,7 @@ export function parseHubEndpointOverride(
}
}
export async function ensureCliHubServer(
export async function ensureHubServer(
workspaceRoot: string,
endpoint: HubEndpointOverrides = {},
): Promise<DetachedHubResolution> {
+12
View File
@@ -0,0 +1,12 @@
export * from "./catalog";
export * from "./chat-commands";
export { isProcessRunning } from "./common";
export * from "./hub-runtime";
export * from "./image-attachments";
export * from "./internal-launch";
export * from "./plugin-chat-commands";
export * from "./prompt";
export * from "./registry";
export * from "./status";
export * from "./types";
export * from "./workspace";
@@ -1,4 +1,4 @@
import { dirname, resolve } from "node:path";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import {
@@ -14,13 +14,12 @@ describe("internal launch helpers", () => {
});
it("resolves the source entrypoint when running from TypeScript", () => {
const utilsDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(utilsDir, "../../../../");
const entryPath = fileURLToPath(import.meta.url);
const spec = resolveCliLaunchSpec({
execPath: "/Users/test/.bun/bin/bun",
argv: ["bun", "./apps/cli/src/index.ts"],
argv: ["bun", entryPath],
execArgv: ["--conditions=development"],
cwd: repoRoot,
cwd: dirname(entryPath),
env: {},
});
@@ -30,9 +29,9 @@ describe("internal launch helpers", () => {
"--inspect=127.0.0.1:0",
"--enable-source-maps",
"--conditions=development",
resolve(repoRoot, "apps/cli/src/index.ts"),
entryPath,
],
identityPath: resolve(repoRoot, "apps/cli/src/index.ts"),
identityPath: entryPath,
mode: "source",
});
});
@@ -52,13 +51,12 @@ describe("internal launch helpers", () => {
});
it("adds node debug flags for development node launches", () => {
const utilsDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(utilsDir, "../../../../");
const entryPath = fileURLToPath(import.meta.url);
const command = buildCliSubcommandCommand("hub", ["start"], {
execPath: "/usr/local/bin/node",
argv: ["node", "./apps/cli/src/index.ts"],
argv: ["node", entryPath],
execArgv: [],
cwd: repoRoot,
cwd: dirname(entryPath),
env: { CLINE_BUILD_ENV: "development" },
});
@@ -67,7 +65,7 @@ describe("internal launch helpers", () => {
childArgs: [
"--inspect=127.0.0.1:0",
"--enable-source-maps",
resolve(repoRoot, "apps/cli/src/index.ts"),
entryPath,
"hub",
"start",
],
+39
View File
@@ -0,0 +1,39 @@
import { join } from "node:path";
import type { BasicLogger, RuntimeLoggerConfig } from "@cline/shared";
import { noopBasicLogger } from "@cline/shared";
import { resolveClineDataDir } from "@cline/shared/storage";
import type {
ConnectIo,
ConnectorLoggerAdapter,
CreateConnectorLoggerInput,
} from "./types";
function normalizeRuntimeLoggerConfig(
input: CreateConnectorLoggerInput,
): Required<RuntimeLoggerConfig> {
return {
enabled: input.runtimeConfig?.enabled ?? false,
level: input.runtimeConfig?.level ?? "info",
destination:
input.runtimeConfig?.destination ??
join(resolveClineDataDir(), "logs", "cline.log"),
name: input.runtimeConfig?.name ?? `cline.${input.runtime}`,
bindings: input.runtimeConfig?.bindings ?? {},
};
}
export function createConnectorLogger(
io: ConnectIo,
input: CreateConnectorLoggerInput,
): ConnectorLoggerAdapter {
const hosted = io.createLogger?.(input);
if (hosted) {
return hosted;
}
const fallback: ConnectorLoggerAdapter = {
core: noopBasicLogger as BasicLogger,
runtimeConfig: normalizeRuntimeLoggerConfig(input),
child: () => fallback,
};
return fallback;
}
@@ -1,11 +1,12 @@
import { resolveAndLoadAgentPlugins } from "@cline/core";
import {
type AgentExtensionCommand,
type AgentExtensionCommandResult,
type AgentTool,
type BasicLogger,
createContributionRegistry,
resolveAndLoadAgentPlugins,
} from "@cline/core";
import type { AgentTool, Message } from "@cline/shared";
type Message,
} from "@cline/shared";
import {
type ChatCommandDefinition,
type ChatCommandHost,
@@ -1,13 +1,10 @@
import { statSync } from "node:fs";
import { homedir } from "node:os";
import { basename, resolve } from "node:path";
import {
buildWorkspaceMetadata,
mergeRulesForSystemPrompt,
type UserInstructionConfigService,
} from "@cline/core";
import type { UserInstructionConfigService } from "@cline/core";
import { buildWorkspaceMetadata, mergeRulesForSystemPrompt } from "@cline/core";
import { type AgentMode, buildClineSystemPrompt } from "@cline/shared";
import { isImagePath, loadImageAsDataUrl } from "../utils/image-attachments";
import { isImagePath, loadImageAsDataUrl } from "./image-attachments";
const MODE_TAG_INSTRUCTIONS = `# Plan / Act Modes
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import type { CliLoggerAdapter } from "../logging/adapter";
import { createConnectorRuntimeTurnStream } from "./runtime-turn";
import type { ConnectorLoggerAdapter } from "./types";
type StreamHandlers = {
onEvent: (event: {
@@ -50,7 +50,7 @@ describe("createConnectorRuntimeTurnStream", () => {
sessionId: "session-1",
request,
clientId: "client-1",
logger: { core: {} } as unknown as CliLoggerAdapter,
logger: { core: {} } as unknown as ConnectorLoggerAdapter,
transport: "telegram",
conversationId: "thread-1",
onToolStatus: async (message) => {
@@ -82,7 +82,7 @@ describe("createConnectorRuntimeTurnStream", () => {
sessionId: "session-1",
request: { config: {} as never, prompt: "hi" },
clientId: "client-1",
logger: { core: { log } } as unknown as CliLoggerAdapter,
logger: { core: { log } } as unknown as ConnectorLoggerAdapter,
transport: "discord",
conversationId: "thread-1",
})) {
@@ -132,7 +132,7 @@ describe("createConnectorRuntimeTurnStream", () => {
sessionId: "session-1",
request: { config: {} as never, prompt: "hi" },
clientId: "client-1",
logger: { core: {} } as unknown as CliLoggerAdapter,
logger: { core: {} } as unknown as ConnectorLoggerAdapter,
transport: "telegram",
conversationId: "thread-1",
onFailed: async (error) => {
@@ -179,7 +179,7 @@ describe("createConnectorRuntimeTurnStream", () => {
sessionId: "session-1",
request: { config: {} as never, prompt: "hi" },
clientId: "client-1",
logger: { core: {} } as unknown as CliLoggerAdapter,
logger: { core: {} } as unknown as ConnectorLoggerAdapter,
transport: "telegram",
conversationId: "thread-1",
onFailed: async (error) => {
@@ -1,5 +1,6 @@
import type { ChatRunTurnRequest, HubSessionClient } from "@cline/core";
import type { CliLoggerAdapter } from "../logging/adapter";
import type { HubSessionClient } from "@cline/core/hub";
import type { ChatRunTurnRequest } from "@cline/shared";
import type { ConnectorLoggerAdapter } from "./types";
export type PendingConnectorApproval = {
approvalId: string;
@@ -136,7 +137,7 @@ export function createConnectorRuntimeTurnStream(input: {
sessionId: string;
request: ChatRunTurnRequest;
clientId: string;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
transport: string;
conversationId: string;
onToolStatus?: (message: string) => Promise<void>;
@@ -1,17 +1,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
const {
mockGetLastUsedProviderSettings,
mockGetProviderSettings,
mockResolveSystemPrompt,
mockGetProviderCollection,
mockGetBooleanFlagEnabled,
} = vi.hoisted(() => ({
mockGetLastUsedProviderSettings: vi.fn(),
mockGetProviderSettings: vi.fn(),
mockResolveSystemPrompt: vi.fn(),
mockGetProviderCollection: vi.fn(),
mockGetBooleanFlagEnabled: vi.fn(),
}));
vi.mock("@cline/core", async () => {
@@ -28,47 +26,29 @@ vi.mock("@cline/core", async () => {
return mockGetProviderSettings(providerId);
}
},
CoreSessionService: class {},
SqliteSessionStore: class {},
Llms: {
...actual.Llms,
getProviderCollection: mockGetProviderCollection,
},
};
});
vi.mock("../runtime/prompt", () => ({
vi.mock("@cline/llms", async () => {
const actual =
await vi.importActual<typeof import("@cline/llms")>("@cline/llms");
return {
...actual,
getProviderCollection: mockGetProviderCollection,
};
});
vi.mock("./prompt", () => ({
resolveSystemPrompt: mockResolveSystemPrompt,
}));
vi.mock("../utils/helpers", () => ({
vi.mock("./workspace", () => ({
resolveWorkspaceRoot: vi.fn((cwd: string) => cwd),
}));
vi.mock("../utils/feature-flags", () => ({
getCliFeatureFlagsService: () => ({
getBooleanFlagEnabled: mockGetBooleanFlagEnabled,
}),
}));
vi.mock("../commands/auth", async () => {
const actual =
await vi.importActual<typeof import("../commands/auth")>(
"../commands/auth",
);
return {
...actual,
ensureOAuthProviderApiKey: vi.fn(),
};
});
import { buildConnectorStartRequest } from "./session-runtime";
describe("buildConnectorStartRequest", () => {
beforeEach(() => {
mockGetBooleanFlagEnabled.mockReturnValue(false);
});
afterEach(() => {
vi.clearAllMocks();
delete process.env.OPENROUTER_API_KEY;
@@ -125,12 +105,12 @@ describe("buildConnectorStartRequest", () => {
io: { writeln: vi.fn(), writeErr: vi.fn() },
loggerConfig: { enabled: false, level: "info", destination: "stdout" },
systemRules: "Rules",
defaultModel: "cline-pass/glm-5.1",
defaultModel: "cline-pass/glm-5.2",
});
expect(request.provider).toBe("cline-pass");
expect(request.apiKey).toBe("workos:resolved-token");
expect(request.model).toBe("cline-pass/glm-5.1");
expect(request.model).toBe("cline-pass/glm-5.2");
});
it("uses auth material resolved by provider settings manager", async () => {
@@ -153,11 +133,11 @@ describe("buildConnectorStartRequest", () => {
io: { writeln: vi.fn(), writeErr: vi.fn() },
loggerConfig: { enabled: false, level: "info", destination: "stdout" },
systemRules: "Rules",
defaultModel: "cline-pass/glm-5.1",
defaultModel: "cline-pass/glm-5.2",
});
expect(request.provider).toBe("cline-pass");
expect(request.apiKey).toBe("workos:resolved-token");
expect(request.model).toBe("cline-pass/glm-5.1");
expect(request.model).toBe("cline-pass/glm-5.2");
});
});
@@ -1,22 +1,18 @@
import type { ChatStartSessionRequest, RuntimeLoggerConfig } from "@cline/core";
import {
CoreSessionService,
HubSessionClient,
Llms,
ProviderSettingsManager,
SqliteSessionStore,
} from "@cline/core";
import type { Thread } from "chat";
import {
ensureOAuthProviderApiKey,
getPersistedProviderApiKey,
isOAuthProvider,
normalizeProviderId,
} from "../commands/auth";
import type { CliLoggerAdapter } from "../logging/adapter";
import { resolveSystemPrompt } from "../runtime/prompt";
import { resolveCliSessionMetadata } from "../utils/enterprise";
import { resolveWorkspaceRoot } from "../utils/helpers";
ProviderSettingsManager,
SqliteSessionStore,
} from "@cline/core";
import { HubSessionClient } from "@cline/core/hub";
import * as Llms from "@cline/llms";
import type {
ChatStartSessionRequest,
RuntimeLoggerConfig,
} from "@cline/shared";
import type { Thread } from "chat";
import {
parseLocalRowMetadata,
parseRowMetadata,
@@ -24,12 +20,14 @@ import {
readSessionReplyText,
} from "./common";
import { dispatchConnectorHook } from "./hooks";
import { resolveSystemPrompt } from "./prompt";
import {
type ConnectorThreadState,
loadThreadState,
persistMergedThreadState,
} from "./thread-bindings";
import type { ConnectIo } from "./types";
import type { ConnectIo, ConnectorLoggerAdapter } from "./types";
import { resolveWorkspaceRoot } from "./workspace";
async function resolveProviderApiKeyFromEnv(
provider: string,
@@ -83,12 +81,16 @@ export async function buildConnectorStartRequest(input: {
"";
if (!apiKey && isOAuthProvider(provider)) {
const oauthResult = await ensureOAuthProviderApiKey({
if (!input.io.ensureProviderApiKey) {
throw new Error(
`Connector host cannot authenticate OAuth provider "${provider}"`,
);
}
const oauthResult = await input.io.ensureProviderApiKey({
providerId: provider,
currentApiKey: apiKey,
existingSettings: selectedProviderSettings,
providerSettingsManager,
io: input.io,
});
selectedProviderSettings = oauthResult.selectedProviderSettings;
apiKey = oauthResult.apiKey ?? "";
@@ -143,7 +145,7 @@ export async function getOrCreateSessionId<
thread: Thread<TState>;
client: HubSessionClient;
startRequest: ChatStartSessionRequest;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
clientId: string;
transport: string;
bindingsPath: string;
@@ -151,6 +153,7 @@ export async function getOrCreateSessionId<
hookCommand?: string;
hookBotUserName?: string;
sessionMetadata: Record<string, unknown>;
resolveSessionMetadata?: ConnectIo["resolveSessionMetadata"];
reusedLogMessage: string;
startedLogMessage?: string;
}): Promise<string> {
@@ -219,9 +222,9 @@ export async function getOrCreateSessionId<
if (!sessionId) {
throw new Error("runtime start returned an empty session id");
}
const remoteConfigMetadata = await resolveCliSessionMetadata(sessionId).catch(
() => undefined,
);
const remoteConfigMetadata = await input
.resolveSessionMetadata?.(sessionId)
.catch(() => undefined);
await input.client
.updateSession({
@@ -1,6 +1,6 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { resolveClineDataDir } from "@cline/core";
import { resolveClineDataDir } from "@cline/shared/storage";
function isProcessRunning(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) {
@@ -1,5 +1,5 @@
import { readFileSync, writeFileSync } from "node:fs";
import { ensureParentDir } from "@cline/core";
import { ensureParentDir } from "@cline/shared/storage";
import type { Lock, QueueEntry, StateAdapter } from "chat";
type PersistedStateSnapshot = {
@@ -1,13 +1,13 @@
import type { HubSessionClient } from "@cline/core";
import type { HubSessionClient } from "@cline/core/hub";
import type { TeamProgressProjectionEvent } from "@cline/shared";
import type { Chat, Thread } from "chat";
import type { CliLoggerAdapter } from "../logging/adapter";
import { truncateConnectorText } from "./runtime-turn";
import {
type ConnectorThreadBinding,
type ConnectorThreadState,
readBindings,
} from "./thread-bindings";
import type { ConnectorLoggerAdapter } from "./types";
function formatCountLabel(input: {
count: number;
@@ -139,7 +139,7 @@ export function startConnectorTaskUpdateRelay<
client: HubSessionClient;
clientId: string;
bot: Chat;
logger: CliLoggerAdapter;
logger: ConnectorLoggerAdapter;
bindingsPath: string;
transport: string;
postToThread?: (input: {
+45
View File
@@ -0,0 +1,45 @@
import type { ProviderSettings, ProviderSettingsManager } from "@cline/core";
import type { BasicLogger, RuntimeLoggerConfig } from "@cline/shared";
export type ConnectorLoggerAdapter = {
readonly core: BasicLogger;
readonly runtimeConfig: RuntimeLoggerConfig;
child(bindings: Record<string, unknown>): ConnectorLoggerAdapter;
};
export type CreateConnectorLoggerInput = {
runtime: "cli" | "rpc-runtime";
component?: string;
runtimeConfig?: RuntimeLoggerConfig;
};
export type ConnectIo = {
writeln: (text?: string) => void;
writeErr: (text: string) => void;
createLogger?: (input: CreateConnectorLoggerInput) => ConnectorLoggerAdapter;
resolveSessionMetadata?: (
sessionId: string,
) => Promise<Record<string, unknown> | undefined>;
ensureProviderApiKey?: (input: {
providerId: string;
currentApiKey?: string;
existingSettings?: ProviderSettings;
providerSettingsManager: ProviderSettingsManager;
}) => Promise<{
apiKey?: string;
selectedProviderSettings?: ProviderSettings;
}>;
};
export type ConnectStopResult = {
stoppedProcesses: number;
stoppedSessions: number;
};
export interface ConnectCommandDefinition {
name: string;
description: string;
run(args: string[], io: ConnectIo): Promise<number>;
showHelp(io: ConnectIo): void;
stopAll?(io: ConnectIo): Promise<ConnectStopResult>;
}
@@ -0,0 +1,14 @@
import { spawnSync } from "node:child_process";
export function resolveWorkspaceRoot(cwd: string): string {
const result = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
encoding: "utf8",
});
if (result.status === 0) {
const value = result.stdout.trim();
if (value) {
return value;
}
}
return cwd;
}
+4 -2
View File
@@ -2,9 +2,11 @@ import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { basename } from "node:path";
import process from "node:process";
import {
listActiveConnectors,
listConnectorCatalog,
} from "@cline/cline-hub/connectors";
import { withResolvedClineBuildEnv } from "@cline/shared";
import { listConnectorCatalog } from "../../../cli/src/connectors/catalog";
import { listActiveConnectors } from "../../../cli/src/connectors/status";
import {
PLATFORMS,
shouldIncludeField,
+1 -1
View File
@@ -1,4 +1,4 @@
import { listActiveConnectors } from "../../../cli/src/connectors/status";
import { listActiveConnectors } from "@cline/cline-hub/connectors";
import type { WebviewHubState } from "../webview-protocol";
import {
clientSummariesPayload,
+1
View File
@@ -20,6 +20,7 @@
"clean": "rm -rf webview/.next webview/out node_modules dist && (cd src-tauri && rm -rf target node_modules dist)"
},
"dependencies": {
"@cline/cline-hub": "workspace:*",
"@base-ui/react": "^1.2.0",
"@cline/core": "workspace:*",
"@cline/llms": "workspace:*",
@@ -2,9 +2,11 @@ import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { basename, join, normalize } from "node:path";
import process from "node:process";
import {
listActiveConnectors,
listConnectorCatalog,
} from "@cline/cline-hub/connectors";
import { withResolvedClineBuildEnv } from "@cline/shared";
import { listConnectorCatalog } from "../../../cli/src/connectors/catalog";
import { listActiveConnectors } from "../../../cli/src/connectors/status";
import {
PLATFORMS,
shouldIncludeField,

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