Compare commits

...

176 Commits

Author SHA1 Message Date
Robin Newhouse a03642ba4a fix(prompt): add output precision and threshold iteration rules (#9178)
* fix(prompt): add output precision and threshold iteration rules

Two concise rules proven effective via Terminal-Bench testing:

1. Output precision: produce exactly what's specified, no extra columns/fields/debug output
2. Threshold iteration: verify results meet numerical criteria before completing

Tested on 6 targeted Terminal-Bench tasks (job 2026-02-07__16-15-00):
- log-summary-date-ranges: FAIL→PASS (output precision rule eliminated extra columns)
- dna-insert: FAIL→PASS (iterate rule helped agent meet Tm threshold)

A third rule (no-cleanup) was tested and deliberately excluded: it failed to
prevent self-sabotage on configure-git-webserver despite STRICTLY FORBIDDEN
language, and caused a side-effect on polyglot-c-py by preventing legitimate
build artifact cleanup. The cleanup behavior is too deeply trained to override
via prompt rules alone.

* test: update prompt snapshots for new rules
2026-02-08 09:37:04 -08:00
Saoud Rizwan b65435fc55 fix(cli): route PostHog networking through shared fetch (#9149)
* fix(cli): route PostHog networking through shared fetch

* remove unnecessary `as RequestInit` casts from PostHog fetch wrappers

PostHogFetchOptions is a structural subset of RequestInit, so the cast
is unnecessary. Also removes a stale comment about shared client support
in PostHogErrorProvider.
2026-02-07 18:26:23 -08:00
Saoud Rizwan 88694d39fa feat(cli): allow --thinking flag to accept custom token budget (#9177)
The --thinking flag now accepts an optional number argument to set a
custom thinking budget instead of always using the 1024 default.

  cline "prompt" --thinking         # 1024 tokens (default)
  cline "prompt" --thinking 8000    # 8000 tokens

Invalid values get a warning and fall back to 1024.
2026-02-07 17:06:33 -08:00
Saoud Rizwan 6c53daa88e feat: move reasoning effort to model config and settings UX (#9168)
* feat: move reasoning effort to model config and update model selection UX

* refactor: dedupe reasoning effort handling and drop lockfile churn

* refactor: default reasoning effort to low

* refactor(cli): sync mode-scoped thinking and reasoning writes

* fix: centralize reasoning effort normalization and avoid implicit openai effort

* fix: restore proto field number for codex credentials and reserve removed fields

- Keep openai_codex_oauth_credentials at field 46 (was incorrectly
  changed to 47)
- Add reserved 146 in Settings for removed openai_reasoning_effort
- Add reserved 15 in UpdateSettingsRequest for removed openai_reasoning_effort
- Remove stale openai_reasoning_effort field from UpdateSettingsRequest

* fix: map medium reasoning effort to LOW for Gemini models

Gemini API only accepts LOW and HIGH thinking levels. MEDIUM exists in
the SDK enum but is rejected at the API level. Map medium to LOW and
update the default fallback accordingly.
2026-02-07 16:58:10 -08:00
Ara 1f3c00c613 feat(task): add support for writing prompt metadata artifacts (#9158)
Introduces a mechanism to save system prompts and task metadata to disk for debugging and analysis purposes.

- Added `writePromptMetadataArtifacts` to the `Task` class.
- Feature is enabled via the `CLINE_WRITE_PROMPT_ARTIFACTS` environment variable.
- Artifacts are saved to `.cline-prompt-artifacts` or a custom path defined by `CLINE_PROMPT_ARTIFACT_DIR`.
- Writes both a JSON manifest (containing task ID, model info, and timestamp) and the raw system prompt for every API request.
2026-02-07 15:21:41 -08:00
Saoud Rizwan 4d455ea015 fix(terminal): tune execute_command timeout strategy for long-running tasks (#9159)
* fix(terminal): tune managed timeout policy for long-running commands

* Reduce default command timeout from 120 to 30 seconds

* Update ExecuteCommandToolHandler.timeout.test.ts

---------

Co-authored-by: Ara <arafat.da.khan@gmail.com>
2026-02-07 14:47:26 -08:00
Saoud Rizwan 3daf24662e fix(prompt): add guidance to use -- for leading-dash positional args (#9161) 2026-02-07 14:18:55 -08:00
Saoud Rizwan 942fcf5762 fix(terminal): surface command exit codes in results (#9156) 2026-02-07 13:31:22 -08:00
ClineXDiego 70a99047ed fix: use vscode.env.asExternalUri for auth callback URLs in VS Code Web (#9144)
* fix: use vscode.env.asExternalUri for auth callback URLs in VS Code Web

The OAuth callback redirect was broken in VS Code Web (code serve-web)
environments because the callback URL used a raw vscode:// URI scheme,
which the OS would route to the local desktop VS Code app instead of
the web instance.

This change wraps both getCallbackUrl() and getIdeRedirectUri() with
vscode.env.asExternalUri() which properly transforms URIs based on the
environment:
- Desktop VS Code: unchanged (vscode://...)
- VS Code Remote SSH: adds remote authority for proper routing
- VS Code Web: transforms to HTTPS URL that routes through the web server

Fixes #5109 (remaining callback redirect issue)
Related: #2152

* fix: use HTTP-based auth callback for VS Code Web mode

In VS Code Web (code serve-web), vscode:// URIs redirect to the desktop
app instead of staying in the browser. This change uses AuthHandler
(local HTTP server) for the auth callback in web mode, matching how
CLI/standalone already handles auth.

- getCallbackUrl: use AuthHandler when UIKind.Web
- getIdeRedirectUri: return empty in web mode to avoid vscode:// redirect

* fix: add fallback for openExternal RPC for JetBrains compatibility

The openExternal host bridge RPC is not implemented in the JetBrains
plugin, causing sign-in to fail silently. This adds a fallback to the
'open' npm package when the host RPC fails with UNIMPLEMENTED.

Fixes #9164, #9137, #9138
2026-02-07 10:25:14 -08:00
Robin Newhouse 844038084c feat: add CLI build workflow for testing from any commit (#9131) 2026-02-07 05:23:22 -08:00
Robin Newhouse 0c6f77ea46 Remove accidentally committed implementation_plan.md (#9160) 2026-02-07 00:11:02 -06:00
Saoud Rizwan 9b70f94174 fix(prompt): require verification before completion (#9154)
* fix(prompt): require verification before completion

* fix(prompt): align gemini verification-first completion guidance
2026-02-06 19:22:40 -08:00
Saoud Rizwan 0a4f939ecb chore(ci): tag bot PR reviews with workflow footer (#9152) 2026-02-06 15:08:38 -08:00
Tomás Barreiro 095ee24288 Limit the CLI provider list to what's remotely configured (#9135)
* Limit the CLI provider list to what's remotely configured

* Refactor

* fix react
2026-02-06 09:14:29 -08:00
Saoud Rizwan 523dd9ef7d fix(ui): add loading indicator and fix api_req_started rendering (#9133)
The chat streaming UI refactor removed the loading indicator that
previously showed when an API request was in progress. This left users
staring at a frozen UI during the latency between sending a message
and receiving the first streamed content.

Changes:
- Add "Thinking..." shimmer in the Virtuoso Footer as the sole loading
  indicator, covering both pre-api_req_started (backend processing) and
  post-api_req_started (waiting for model response) states
- Filter out api_req_started messages that have no visible content
  (no error/cancel). These rows rendered as invisible padding since
  the PR removed the old API request accordion UI. Reasoning messages
  already render as their own standalone ChatRows.
- Thread footerActive flag to MessageRenderer so the last message skips
  pb-2.5 when the Footer is showing, keeping spacing consistent with
  the pt-2.5 on every ChatRow
2026-02-05 16:38:21 -08:00
Robin Newhouse 6d8fb8507b fix(cli): handle stdin redirection in CI environments (#9121)
- Add stdinIsTTY check to shouldUsePlainTextMode() - Ink requires raw mode on stdin
- Only error on empty stdin when no prompt is provided (allows: cline 'prompt' < /dev/null)
- Fixes crash in GitHub Actions and other CI environments
2026-02-05 13:32:52 -08:00
Max edc93f35f1 update changelog for 3.57.1 (#9130)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-05 13:12:54 -08:00
ckrause 401358374f fix MCP OAuth: add missing scope parameter (#9117)
* fix MCP OAuth: add missing scope parameter

* Update src/services/mcp/McpOAuthManager.ts

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

* fix

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-05 13:06:01 -08:00
Max f8bcad16a5 update package-lock.json (#9127)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-05 12:44:46 -08:00
AJ Juaire 26391c94e9 Correct Bedrock Opus 4.6 model id (#9126) 2026-02-05 12:21:37 -08:00
Ara 462438ece5 Update changelog wording (#9125) 2026-02-05 11:53:48 -08:00
github-actions[bot] 92521ed279 Release Notes for v3.57.0 (#8980)
- Cline CLI 2.0 now available. Install with `npm install -g cline`
- Anthopic Opus 4.6
- Minimax-2.1 and Kimi-k2.5 now available for free for a limited time promo
- Codex-5.3 through OpenAI Codex provider

- Fix read file tool to support reading large files
- Fix decimal input crash in OpenAI Compatible price fields (#8129)
- Fix build complete handlers when updating the api config
- Fixed missing provider from list
- Fixed Favorite Icon / Star from getting clipped in the task history view

- Make skills always enabled and remove feature toggle setting

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-02-05 11:35:58 -08:00
Max 08aa81f798 add taskId flag to CLI (#9095)
- allows you to resume a session headlessly or interactively with a
taskId

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-05 11:32:54 -08:00
Tomás Barreiro e025995177 Revert LiteLLM model name change and use the rawModel name (#9123)
* Revert LiteLLM model name change and use the rawModel name

* Add both to the list
2026-02-05 11:15:17 -08:00
Saoud Rizwan ee361ef3ae feat: add GPT-5.3 Codex model for ChatGPT subscription users (#9122)
* feat: add GPT-5.3 Codex model for ChatGPT subscription users

OpenAI released GPT-5.3 Codex today. Adding it to the OpenAI Codex
provider (ChatGPT Plus/Pro subscription) model list and setting it
as the new default.

Changes:
- Add gpt-5.3-codex to openAiCodexModels with same specs as 5.2
- Update default model to gpt-5.3-codex
- Update featured models in CLI and webview OpenRouter picker

* revert: remove gpt-5.3-codex from OpenRouter featured models

GPT-5.3 Codex is only available via ChatGPT subscription, not through
the OpenAI API or OpenRouter. Reverting featured model changes.
2026-02-05 11:05:00 -08:00
Max c8ef342c19 cli multi label support. new featured model (#9118)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-05 10:51:54 -08:00
Saoud Rizwan 7c8701799d feat: add Claude Opus 4.6 model support (#9119)
* feat: add Claude Opus 4.6 model support with 1M context window

Adds support for Claude Opus 4.6, Anthropic's latest model with:
- 200K base context window with optional 1M context variant
- Tiered pricing for >200K context (2x input/output pricing)
- Extended thinking/reasoning support
- Prompt caching support

Changes:
- Added model definitions for Anthropic, Bedrock, and Vertex providers
- Added OpenRouter 1M variant support
- Updated thinking models lists across all provider UIs
- Added context window switcher for Opus 4.6
- Updated JP cross-region inference models list

* feat: update featured model to Opus 4.6 in model picker

* chore: add changeset for Claude Opus 4.6

* fix: correct Opus 4.6 model IDs (no date suffix)

---------

Co-authored-by: Robin Newhouse <robin@cline.bot>
2026-02-05 10:38:54 -08:00
Ara fbcf63ad71 fixing large model (#9110) 2026-02-05 09:16:25 -08:00
Saoud Rizwan fffe626b3a Bump CLI version from 2.0.3 to 2.0.4 2026-02-05 02:07:38 -08:00
Saoud Rizwan 2dccd6f6f6 fix(cli): use value import for React instead of type-only import
JSX requires React as a value when using jsx: react in tsconfig.
2026-02-05 02:04:22 -08:00
Saoud Rizwan 39971128cb fix(cli): use value import for React instead of type-only import 2026-02-05 02:00:43 -08:00
Saoud Rizwan b725490582 fix(cli): fix cursor position after pasting text
Use refs instead of state values in useInput callback to avoid stale
closures. Also manually update textInputRef before calling setCursorPos
so the bounds check uses the correct new text length.
2026-02-05 01:51:25 -08:00
Saoud Rizwan 8f78645154 fix(cli): show default model name when no model configured
ChatView was returning empty string when the model ID key didn't exist
in state, causing first-time CLI users to see a blank model name. Added
fallback to getProviderDefaultModelId() to match WelcomeView's behavior.
2026-02-05 01:33:24 -08:00
Saoud Rizwan 0ef1c0bf47 fix(cli): make robot animation static on click or drag
Previously the animated robot only became static when the user scrolled.
Now it also becomes static when clicking or dragging, giving users more
ways to dismiss the animation. Renamed onScroll to onInteraction to
reflect the broader scope.
2026-02-05 01:23:02 -08:00
Bee 4c07df370b chore: update biome configuration and linting rules (#9109)
* chore: update biome configuration and linting rules

Update @biomejs/biome package to latest version: 2.3.14

- Change $schema to point to local node_modules for better IDE performance and stability.
- Enable and promote several linting rules from "off" to "info" or "warn" across correctness, style, suspicious, and complexity categories.
- Update file inclusion/exclusion patterns to use more explicit formatting and set ignoreUnknown to true.
- Improve code quality enforcement by surfacing potential issues such as non-null assertions, useless constructors, and implicit any types.

* package-lock udpate

* includes tailwind

* useIterableCallbackReturn
2026-02-04 19:40:38 -08:00
ClineXDiego f440f3a5dd fix: use vscode.env.openExternal for auth in remote environments (#9111)
* fix: use vscode.env.openExternal for auth in remote environments

Fixes #5109

The OAuth authentication flow was broken in VS Code Server and remote
environments because the code used the npm 'open' package directly, which
tries to launch a browser on the server itself (which has no display).

This change routes browser URL opening through VS Code's native
vscode.env.openExternal() API via the HostBridge pattern, which properly
forwards URLs to the user's local machine in remote environments.

Changes:
- Added openExternal RPC to proto/host/env.proto
- Created VS Code handler using vscode.env.openExternal()
- Updated src/utils/env.ts to use HostProvider.env.openExternal()
- Added openExternal to CLI CliEnvServiceClient (uses npm 'open')
- Added openExternal to CLI ACPEnvServiceClient (uses npm 'open')

Related issues: #5394, #2152, #7971

* chore: add changeset for vscode server auth fix

* refactor: extract shared openUrlInBrowser utility for CLI
2026-02-04 19:31:20 -08:00
Tomás Barreiro 3ce1ad3504 Parse remotely configured R2 options (#9090)
* Parse remotely configured R2 options

* Fix R2 options
2026-02-05 03:53:44 +01:00
Tomás Barreiro 00bc38d4e0 Add Workspace Configuration to commit generation (#9107) 2026-02-05 02:25:17 +01:00
Tomás Barreiro a1f2601fe0 Replace the LiteLLM model selector with autocomplete (#9075)
* Replace the LiteLLM model selector with autocomplete

* Add changeset

* refactor
2026-02-04 12:42:08 -08:00
Max 8e3689a5d6 tag released cli versions (#9071)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-04 12:10:02 -08:00
Tomás Barreiro a64f46a5f4 Lock remotely configured Anthropic options (#9087)
* Add Anthropic to the remote config provider settings

* Lock the Anthropic Base URL when it's remotely configured
2026-02-04 19:42:02 +01:00
Tomás Barreiro 507483b2c3 Add Anthropic to the remote config provider settings (#9084) 2026-02-04 19:34:18 +01:00
Marco Alejandro Chavez Santos 42ce100143 Add Authentication Button on HICAP provider to get API KEY (#9098)
* add auth option to get API-KEY for hicap from hicap dashboard website

* remove default hicap model selection

* change url hicap get api keys, add useEffect when update hicapApiKey

* add changeset
2026-02-04 10:29:03 -08:00
CandiedUniverse 7be4e6c6d3 Remove isExperimental flag from Parallel Tool Calls feature setting. (#9097) 2026-02-04 09:47:41 -08:00
Ara 09b91a1ea5 chore: update CODEOWNERS assignments (#9096)
- Remove /docs/ from code ownership
- Update /.github/ owners to @arafatkatze, @maxpaulus43, @candieduniverse
- Update /README.md owner to @juanpflores
- Remove former owners @garoth, @sjf, @nickbaumann98
2026-02-04 09:29:15 -08:00
Tomás Barreiro 7127a2ffa7 Clean old API keys that are stored in secrets (#9091) 2026-02-04 12:02:17 -03:00
Bee d6987d4578 chore: update package-lock.json (#9079) 2026-02-03 21:09:56 -08:00
Tomás Barreiro 7b59cbcb5c Add r2 Blob storage options (#9052) 2026-02-04 03:49:10 +01:00
Ara 0e26ba46d0 fix(ci): always run npm ci regardless of cache hit status (#9078)
Remove conditional checks that skipped dependency installation when
cache was hit. The npm cache speeds up npm ci but does not replace
the need to run it - node_modules still needs to be populated.
2026-02-03 16:34:57 -08:00
Max 3b313ae41f remove prepublish script (#9077)
- this was breaking the publish npm workflow when we try to run npm
publish from the dist-standalone folder (dist-standalone doesn't have
the esbuilt.ts file)
- we don't need this script anyway because we use the npm-main.yaml
workflow to publish the cli

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-03 15:54:37 -08:00
CandiedUniverse 7e74f35f23 feat(hooks): Call combineHookSequences() properly from the CLI. (#9066) 2026-02-03 14:19:56 -08:00
Saoud Rizwan 59c05f4b84 Update copyright year to 2026 2026-02-03 13:51:23 -08:00
Saoud Rizwan 5d6424682f Update README.md 2026-02-03 13:50:49 -08:00
Robin Newhouse b1a8db252a fix(cli): prevent hang when spawned without TTY (#9073)
* fix(cli): prevent hang when spawned without TTY

When the CLI is spawned as a child process without a TTY (e.g., from
spawn() in smoke tests or CI), process.stdin.isTTY is false even when
nothing is piped to stdin. This caused readStdinIfPiped() to wait up
to 5 minutes for input that would never arrive.

Fix by using fs.fstatSync(0) to check if stdin is actually a FIFO
(pipe) or file before waiting. This correctly handles:
- Spawned processes without TTY → returns immediately
- Actual piped input (echo "x" | cline) → waits and reads
- stdin from /dev/null → returns immediately

* chore: add changeset

* test(cli): add tests for stdin type detection
2026-02-03 13:45:56 -08:00
Tony Loehr 5ae47fb90b Update docs for CLI 2.0 and fix workflow (#9068)
* Add ACP editor integrations documentation with JetBrains and Neovim video demos

* Add Model Orchestration documentation with --config and --thinking flags

- Document --config and --thinking flags in CLI reference
- Create new model-orchestration.mdx sample page
- Add patterns for CI/CD review, task phase optimization, and multi-model consensus
- Link to production GitHub Actions workflow
- Update samples overview with new card
- Update docs navigation

* Add Worktree Workflows documentation with --cwd flag

- Document --cwd flag in CLI reference
- Create comprehensive worktree-workflows.mdx sample page
- Add patterns for parallel execution and cross-worktree piping
- Include real-world examples and best practices
- Add CLI section to features/worktrees.mdx for discoverability
- Update samples overview and navigation
- Cross-link between CLI and VS Code worktree docs

* Remove broken image references from worktrees documentation

- Remove worktrees-overview.png Frame (image not available)
- Remove worktrees-merge.png Frame (image not available)
- Documentation remains fully functional with comprehensive text explanations

* Remove accidentally committed local test file

- Delete src/test/verify-platformio-mcp.ts which was causing CI failures
- File contained TypeScript errors and hardcoded local paths
- Was meant for local testing only, should not have been committed

* Add native JetBrains plugin recommendation to ACP docs

- Add prominent Note recommending native JetBrains plugin
- Link directly to JetBrains installation section
- Position ACP setup as an alternative approach
- Keep all existing ACP content and video

* docs: refine CLI reference formatting and ACP title

Improve CLI reference readability with clearer headings and descriptions, and clarify the ACP editor integration page title for better discovery.docs: refine CLI reference formatting and ACP title

Improve CLI reference readability with clearer headings and descriptions, and clarify the ACP editor integration page title for better discovery.

* Fix CLI 2.0 syntax in model-orchestration.mdx

- Updated issue analysis pipeline to use shell variables for passing context
- Added explanatory note about why direct piping doesn't work
- Corrected example to complete each phase before starting the next
- All examples now use proper CLI 2.0 syntax

* Completely rewrite cli-reference.mdx with accurate CLI 2.0 information

- Removed all outdated CLI 1.0 content (instance management, Cline Core architecture, gRPC references)
- Added accurate CLI 2.0 commands: task, history, config, auth, update, version, dev
- Corrected all command flags and options based on actual man page
- Added proper examples for all commands
- Included environment variables documentation (CLINE_DIR, CLINE_COMMAND_PERMISSIONS)
- Added shell completion instructions
- Removed incorrect three-layer architecture description
- All content now matches cli/man/cline.1.md source of truth

Fixes outdated documentation issue mentioned in PR#9036

* Fix MDX syntax error in cli-reference.mdx

- Replace angle bracket URLs with proper markdown links
- MDX parser was interpreting <https://...> as invalid HTML tags
- Now uses [url](url) format which is proper MDX syntax

Fixes deployment validation error

* docs: Add GitHub PR Review sample and modernize Actions integration

* fix: Add cline installation step to PR review workflow

- Fix CI/CD failure by actually installing cline before running it
- Update docs model ID to match workflow (claude-opus-4-5-20251101)
- Change from 'npx cline version' to 'npm install -g cline' + 'cline version'

---------

Co-authored-by: Renee Huang <renee@cline.bot>
2026-02-03 13:31:35 -08:00
Tomás Barreiro 28c2697ae1 Refresh LiteLLM models (#9070)
* Refresh LiteLLM models

* return promise

* Disable button while loading

* Loading

* Await the fetch
2026-02-03 21:57:06 +01:00
Saoud Rizwan cf01317885 fix(cli): await applyProviderConfig in handleProviderSelect
applyProviderConfig is async and for Cline/OpenRouter providers it
awaits fetching model data before setting state. When switching to
an already-configured provider (Cline, OCA), the call wasn't awaited,
so refreshModelIds() ran before the model ID was set in state,
causing the model to not update to the default.
2026-02-03 12:56:32 -08:00
Saoud Rizwan 7d5eebe192 Bump CLI version from 2.0.2 to 2.0.3 2026-02-03 12:52:21 -08:00
Saoud Rizwan 91a3636356 refactor(cli): add applyBedrockConfig utility, simplify saveConfiguration
- Added applyBedrockConfig to provider-config.ts for AWS Bedrock setup
- AuthView saveConfiguration now uses applyProviderConfig/applyBedrockConfig
- SettingsPanelContent handleBedrockComplete now uses applyBedrockConfig
- Removed duplicate Bedrock config building code from both components
- Cleaned up unused imports

# Conflicts:
#	cli/src/components/SettingsPanelContent.tsx
2026-02-03 12:39:41 -08:00
Saoud Rizwan 5d02eea9cd refactor(cli): use applyProviderConfig in ImportView, remove legacy apiProvider
- ImportView now uses applyProviderConfig instead of manual config building
- Removed legacy apiProvider field from AuthView, ImportView, SettingsPanelContent
  (it's unused - runtime reads actModeApiProvider/planModeApiProvider instead)
2026-02-03 12:39:41 -08:00
Saoud Rizwan 45b2786dbf fix(cli): ensure welcomeViewCompleted is flushed after applyProviderConfig
applyProviderConfig calls flushPendingState internally, so any state
set after it needs its own flush. Added explicit flush after setting
welcomeViewCompleted in OCA and OpenAI Codex auth success handlers.
2026-02-03 12:39:41 -08:00
Saoud Rizwan 4924192b64 refactor(cli): use applyProviderConfig for auth success handlers
Simplifies OCA, Cline, and OpenAI Codex auth success handlers in
AuthView to use the shared applyProviderConfig utility instead of
manually constructing provider config objects.

This removes duplicated logic around mode-specific provider keys
and model ID keys that applyProviderConfig already handles.
2026-02-03 12:39:41 -08:00
Max 28c548b3ee simplify package-npm script (#9067)
cli/package.json is already formatted correctly for publishing

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-03 12:36:47 -08:00
Bee 9b9035ea4a feat: add authentication support to oca provider in CLI (#9059)
* feat: add authentication support to oca provider in CLI

This change integrates the OcaAuthService into the AuthView component. It adds a new 'oca_auth' step to the authentication flow, allowing users to select 'oca' as a provider and initiate the authentication request via OcaAuthService.

* fix(cli): add subscription to OCA auth status updates

The OCA auth flow was missing the subscription mechanism to know when
browser auth completes. Without this, the CLI would spin indefinitely
after opening the browser.

Added a useEffect that subscribes to OcaAuthService.subscribeToAuthStatusUpdate
when in oca_auth step. When auth succeeds (user.uid present), saves the
provider config and transitions to success.

* fix(cli): add OCA auth support to SettingsPanelContent

AuthView only handles onboarding. Users also need to be able to switch
to OCA provider from the settings panel after initial setup.

Added:
- handleOcaLogin callback to start OAuth flow
- useEffect subscription to OCA auth status updates
- Case in handleProviderSelect for "oca" provider
- Escape key handling to cancel OCA auth
- UI for "Waiting for OCA sign-in..." state
- isWaitingForOcaAuth to input disabled check

* refactor(cli): extract OCA auth logic into useOcaAuth hook

Reduces code duplication between AuthView and SettingsPanelContent by
extracting the OCA auth subscription and state management into a
reusable hook.

The hook handles:
- Starting the OAuth flow (initialize + createAuthRequest)
- Subscribing to auth status updates
- Tracking waiting state
- Calling onSuccess callback when auth completes
- Exposing isAuthenticated for checking existing sessions

Both components now use the hook with their own onSuccess handlers
for component-specific state updates.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-02-03 12:28:26 -08:00
Bee e4b39aeb22 fix: apply models cache retrieval across model refresh functions (#8976)
* fix: apply models cache retrieval across model refresh functions

This change introduces a unified caching mechanism for model information retrieved from various API providers (Groq, OpenRouter, Vercel). Each service now first checks if the data is available in the shared StateManager's cache before making an API request. This improves performance by leveraging cached results and reduces redundant network calls when refreshing models multiple times. The cache is stored in memory for quick access during subsequent calls within a single execution context.

Changes made:
1. Added import of `StateManager` to each relevant model refresh file.
2. Implemented initial cache check logic at the beginning of each function.
3. Updated error handling and logging consistency across services.
4. Added storage back into StateManager's cache after successful API retrieval for Groq, Vercel AI Gateway only (OpenRouter update already handled).

* promises

* add vercelModels

* feat: add 1-hour TTL to model cache

Adds a time-to-live mechanism to the model info cache so that:
- Duplicate fetches are still prevented within a reasonable window
- Users can get new models after 1 hour without restarting VS Code

Changes:
- Add MODEL_CACHE_TTL_MS constant (1 hour)
- Update cache structure to include timestamp alongside data
- Update setModelsCache to store timestamp with data
- Update getModelsCache to check TTL and invalidate expired cache
- Update getModelInfo to also respect TTL

---------

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-02-03 12:18:21 -08:00
Max f7c54e964f cli version bump (#9064)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-03 11:42:46 -08:00
Bee d116ac5dcf feat: render markdown table in UI (#9056)
* feat: display markdown table in UI

Simplify the handlePartialBlock method in AttemptCompletionHandler by:
- Removing conditional logic for command vs no-command cases
- Always displaying partial result if present
- Deferring command handling to the final execution step
This fixes an issue where attempt completion response doesn't get streamed to the UI during partial result.

Also replaced react-remark with react-markdown and remark-gfm dependencies to MarkdownBlock in UI for enhanced markdown rendering support with GitHub Flavored Markdown features, including displaying table.

* add changeset

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

handlePartialBlock hard-codes the partial flag to true when calling uiHelpers.say(...). For consistency with other tool handlers and to avoid incorrect behavior if this method is ever invoked with a non-partial block, pass block.partial through instead.

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

---------

Co-authored-by: Robin Newhouse <robin@cline.bot>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-03 11:41:25 -08:00
Bee ac22d5d81a chore: add CLI type checking and caching to ci workflow (#9049)
* chore: add CLI type checking and caching to ci workflow

- Added a new cache step for CLI dependencies in the GitHub Actions test workflow to improve build performance.
- Included a step to install CLI dependencies using `npm ci`.
- Updated the `ci:check-all` script in `package.json` to include CLI type checking.
- Added a `cli:typecheck` script to handle type checking within the CLI directory.

* Fix type and import issues for cli

* Includes CI tests in test workflow

* use npx npm-run-all

* update ci:check-all

* ci: skip npm ci steps on cache hit in test workflow

Update the test workflow to conditionally run npm installation steps only when a cache hit is not found. This optimization reduces CI execution time by avoiding redundant dependency installations when the node_modules are already restored from cache.

* ci: update cache keys and add dependency verification in test workflow

Updated the cache keys for root, webview-ui, cli, and testing-platform dependencies by adding a version prefix (v1). This ensures a clean cache state and helps avoid potential corruption or mismatch issues.

Additionally, added a verification step in the test job to log cache hit status and check for the presence of key dependencies like biome and globby. This helps diagnose issues where the cache might be restored but dependencies are not correctly available for subsequent steps.

* update Verify and fix root dependencies

* fix type check script

* add isSettingsKey check

* update settingskey set

* apply feedback

* npx

* feat: flashing dot for streaming chat messages in CI (#9054)

Introduce an ink-spinner to the DotRow component to provide visual feedback when messages are being streamed. This improves the CLI user experience by clearly indicating that a tool call or message is currently in progress.

- Add `flashing` prop to `DotRow` component
- Replace static dot with `toggle8` spinner when `flashing` is true
- Update `ChatMessage` to pass `flashing` state based on `isStreaming` and `partial` message properties

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* ci: simplify dependency caching using built-in npm cache

Replace manual actions/cache steps with setup-node's built-in npm caching feature across all workflow jobs. This change:

- Removes redundant cache action steps for root, webview-ui, cli, and testing-platform dependencies
- Uses setup-node's native `cache: 'npm'` option with `cache-dependency-path` to handle multiple package-lock.json files
- Eliminates conditional installation steps based on cache hits
- Reduces workflow complexity and maintenance overhead while maintaining caching functionality

The built-in caching provides the same performance benefits with less configuration and better integration with the Node.js setup action.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-02-04 03:34:14 +08:00
Juan Pablo Flores b57aefb5a1 Cli 2.0 docs (#9060)
* docs: restructure CLI reference to web-friendly format

Replace embedded man page format with structured markdown sections
for better readability. Simplify description, reorganize commands and
options into clear categories, and update Next Steps navigation cards.

* Add ACP editor integrations documentation (#9036)

* Add ACP editor integrations documentation with JetBrains and Neovim video demos

* Add Model Orchestration documentation with --config and --thinking flags

- Document --config and --thinking flags in CLI reference
- Create new model-orchestration.mdx sample page
- Add patterns for CI/CD review, task phase optimization, and multi-model consensus
- Link to production GitHub Actions workflow
- Update samples overview with new card
- Update docs navigation

* Add Worktree Workflows documentation with --cwd flag

- Document --cwd flag in CLI reference
- Create comprehensive worktree-workflows.mdx sample page
- Add patterns for parallel execution and cross-worktree piping
- Include real-world examples and best practices
- Add CLI section to features/worktrees.mdx for discoverability
- Update samples overview and navigation
- Cross-link between CLI and VS Code worktree docs

* Remove broken image references from worktrees documentation

- Remove worktrees-overview.png Frame (image not available)
- Remove worktrees-merge.png Frame (image not available)
- Documentation remains fully functional with comprehensive text explanations

* Remove accidentally committed local test file

- Delete src/test/verify-platformio-mcp.ts which was causing CI failures
- File contained TypeScript errors and hardcoded local paths
- Was meant for local testing only, should not have been committed

* Add native JetBrains plugin recommendation to ACP docs

- Add prominent Note recommending native JetBrains plugin
- Link directly to JetBrains installation section
- Position ACP setup as an alternative approach
- Keep all existing ACP content and video

* docs: refine CLI reference formatting and ACP title

Improve CLI reference readability with clearer headings and descriptions, and clarify the ACP editor integration page title for better discovery.docs: refine CLI reference formatting and ACP title

Improve CLI reference readability with clearer headings and descriptions, and clarify the ACP editor integration page title for better discovery.

* Fix CLI 2.0 syntax in model-orchestration.mdx

- Updated issue analysis pipeline to use shell variables for passing context
- Added explanatory note about why direct piping doesn't work
- Corrected example to complete each phase before starting the next
- All examples now use proper CLI 2.0 syntax

* Completely rewrite cli-reference.mdx with accurate CLI 2.0 information

- Removed all outdated CLI 1.0 content (instance management, Cline Core architecture, gRPC references)
- Added accurate CLI 2.0 commands: task, history, config, auth, update, version, dev
- Corrected all command flags and options based on actual man page
- Added proper examples for all commands
- Included environment variables documentation (CLINE_DIR, CLINE_COMMAND_PERMISSIONS)
- Added shell completion instructions
- Removed incorrect three-layer architecture description
- All content now matches cli/man/cline.1.md source of truth

Fixes outdated documentation issue mentioned in PR#9036

* Fix MDX syntax error in cli-reference.mdx

- Replace angle bracket URLs with proper markdown links
- MDX parser was interpreting <https://...> as invalid HTML tags
- Now uses [url](url) format which is proper MDX syntax

Fixes deployment validation error

---------

Co-authored-by: Renee Huang <renee@cline.bot>

* docs: enhance interactive mode documentation with structured settings overview

* docs: restructure and improve CLI reference documentation

- Reorganize command structure with clearer global options section
- Add mode behavior table explaining interactive vs plain text modes
- Improve option descriptions with consistent formatting
- Add horizontal rules between sections for better readability
- Document timeout option and environment variables more clearly
- Add Tips & Tricks section for common usage patterns
- Update frontmatter description to reflect content changes

* docs: improve ACP editor integrations page with editor descriptions

- Update page title to be more concise ("ACP: Editor Integrations")
- Remove redundant H1 header that duplicated the title
- Add introductory descriptions for JetBrains, Neovim, and Zed sections
- Rename "Zed Editor" section to just "Zed" for consistency

* docs: expand CLI reference with modes of operation and agent behavior

* Update docs/cline-cli/cli-reference-deprecated.mdx

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

---------

Co-authored-by: Tony Loehr <turingxo@gmail.com>
Co-authored-by: Renee Huang <renee@cline.bot>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-03 11:29:15 -08:00
Max 11da3ee89e add windows to cli publish package json (#9063)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-03 11:24:07 -08:00
Bee e4e63912dd feat: add API key support for Cline provider (#9057)
* feat: add API key support for Cline provider

Add support for authenticating with Cline provider using an API key as an alternative to account-based authentication. This change allows users to configure Cline with either a direct API key or through the existing account authentication flow.

Changes:
- Add `clineApiKey` option to ClineHandler and pass through API configuration
- Update authentication check to accept either API key or account ID
- Modify provider configuration detection to check both auth methods
- Remove automatic Cline auth flow trigger on provider selection
- Add `clineApiKey` to provider-to-API-key mapping for proper key management

This provides more flexibility in authentication methods while maintaining backward compatibility with existing account-based authentication.

* promise all

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-02-03 10:56:04 -08:00
Tomás Barreiro f17d523b2e Fix OTEL endpoints (#9050)
* Fix OTEL endpoints

* refactor
2026-02-03 19:24:01 +01:00
Max edbba8b7f6 return empty mcp config if cline_mcp_settings.json doesn't exist or is empty file (#9061)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-03 10:12:52 -08:00
Max 7b09999fdf add man page to cli/package.json (#9044) 2026-02-02 21:45:00 -08:00
Tomás Barreiro 01240744a2 Remove reliance on the extensionEnabled flag and verify the source of truth (#9046)
* Remove reliance on the extensionEnabled flag and verify the source of truth

* Fix tests

* Add try block

* Fix telemetrySetting checks
2026-02-03 06:40:17 +01:00
Saoud Rizwan 2944416758 feat(cli): show contextual hints when in settings subpages
When navigating to subpages within the Settings panel (model picker,
provider picker, language picker, etc.), the Panel header now shows
"Esc to go back" instead of "Esc to close" and hides the arrow key
navigation hint since tabs cannot be switched while in a subpage.
2026-02-02 21:08:36 -08:00
alex-lum b5b503dd50 adding in org and member tracking (#9037) 2026-02-02 18:54:58 -08:00
Saoud Rizwan a24ab0c6b8 fix(telemetry): capture event when user opts out of telemetry (#9041)
* fix(telemetry): capture event when user opts out of telemetry

Previously, when a user disabled telemetry, we immediately called
optOut() on providers without first capturing an event to record
this decision. This meant we had no visibility into opt-out rates.

This change captures a "user.opt_out" event using captureRequired
(which bypasses the opt-out check) right before disabling telemetry.

* also track when users opt back in to telemetry

This allows seeing each user's final telemetry state:
- user.opt_out = they disabled telemetry
- user.telemetry_enabled = they re-enabled after opting out
- neither = telemetry on by default, never changed

* refactor: only capture telemetry events on explicit user action

Move event capture from updateTelemetryState() to the controller's
updateTelemetrySetting() method. This ensures we only capture events
when the user explicitly toggles the setting, not on webview init sync.

The previous approach would re-capture opt_out events on every VS Code
restart for users who had previously opted out, because the provider
state resets to enabled on startup.

Now we compare the previous vs new setting in the controller (which has
access to persisted state) and only capture when there's an actual change.

* use distinct event name for explicit user opt-in

The constructor already fires user.telemetry_enabled on startup.
Add user.opt_in for when user explicitly re-enables telemetry,
to distinguish from the initialization event.
2026-02-02 18:19:34 -08:00
Saoud Rizwan 3bc6cc6a92 Bump CLI package version 2026-02-02 16:35:15 -08:00
Max bd7f2a29d6 error cline if someone piped in empty text (#9038)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-02 16:27:54 -08:00
Max b1c5f0b811 cli/fix - quick auth should exit process with no interactive ui (#9035)
- this will support ci/cd use case

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-02 16:14:04 -08:00
Bee ac30e49e0b chore(deps): update package-lock.json peer dependency flags (#9040)
Update peer dependency markers in package-lock.json to correctly reflect the dependency relationships. This change moves the `peer: true` flag to packages that are actual peer dependencies (like react, vite, typescript, @opentelemetry/api, @modelcontextprotocol/sdk) and removes it from optional dependencies and platform-specific packages (like @rollup/* platform binaries, @csstools/* packages, and tldts-related packages).

This ensures proper dependency resolution and installation behavior without changing actual package versions or dependencies.
2026-02-03 08:10:48 +08:00
Saoud Rizwan d86f5ed4c9 fix(cli): fetch fresh org data from server when switching organizations
The CLI was reading organization data from authService.getUserOrganizations()
which returns cached data. This caused org switches to not persist across
CLI restarts.

Now uses accountService.fetchUserOrganizationsRPC() to fetch fresh data
from /api/v1/users/me, matching how the webview's getUserOrganizations
RPC works.
2026-02-02 15:12:54 -08:00
Saoud Rizwan 336d31f95f docs(cli): simplify README title to just 'Cline' 2026-02-02 15:08:17 -08:00
Saoud Rizwan fc9c413058 fix(cli): add Home/End key support (fn+left/right on macOS)
Ink's useInput hook parses Home/End keys but doesn't expose them
(sets input='' and doesn't add key.home/key.end to the key object).

Changes:
- Add useHomeEndKeys hook to intercept Home/End from raw stdin
- Create shared keyboard.ts constants for escape sequences
- Remove dead Home/End code from useTextInput (was never firing)
- Add numbered priority documentation to ChatView's useInput handler
2026-02-02 12:47:35 -08:00
Saoud Rizwan e0282826fc fix(cli): match 'Act mode' without 'to' prefix for (Tab) hint
The markdown parser splits 'toggle to **Act mode**' into separate chunks,
so the previous regex requiring 'to Act Mode' as a complete phrase would
fail to match when Act mode was inside bold/italic formatting.
2026-02-02 12:41:55 -08:00
Saoud Rizwan bf83b816e2 fix(cli): support auto-updates for nightly versions (#9034)
* fix(cli): support auto-updates for nightly versions

Previously, the auto-update logic only checked npm's "latest" tag,
so users on nightly builds (2.0.0-nightly.X) would never receive
nightly updates. The update commands also hardcoded @latest.

Changes:
- Detect nightly versions by checking for "-nightly." in version string
- Query npm "nightly" tag when current version is a nightly build
- Use @nightly in update commands for nightly users
- Fix compareVersions() to properly parse and compare nightly timestamps
  (previously it would produce NaN when parsing "2.0.0-nightly.X")

* fix: tighten nightly version regex to require valid semver format
2026-02-02 11:59:07 -08:00
Saoud Rizwan 24033613cd fix(cli): correct provider model ID key generation for anthropic and separate providers (#9033)
Two bugs fixed:

1. getProviderModelIdKey() returned invalid key for Anthropic because
   ProviderKeyMap used "apiModelId" (lowercase "a"), producing
   "actModeapiModelId" instead of "actModeApiModelId". Removed anthropic
   from the map so it falls through to the generic key as intended.

2. Settings panel derived both act/plan model keys from actModeApiProvider.
   If plan and act providers differ, plan model reads/writes targeted wrong
   keys. Now uses planModeApiProvider for plan model key lookups.
2026-02-02 11:36:12 -08:00
Max f76cfbce48 remove cache hit check for npm publish workflows (#9032)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-02 10:37:13 -08:00
Max 2a63545224 fix npm-nightly github workflow (#9031)
- use scripts/package-npm.mjs script and remove other unnecessary steps
in the cli build process

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-02-02 10:14:08 -08:00
Jose Castelli 5308dedc81 fix: updating script documentation and removing unnecessary continue on error (#8769)
* updating script documentation and removing unnecessary continue on error

* test update

* removing comment

* removing unnecessary line

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-02-02 10:11:37 -08:00
Bee 4c699da00b fix(ci): always run npm ci to prevent stale cache issues (#9030)
* fix(ci): always run npm ci to prevent stale cache issues

## Summary

- Remove conditional `npm ci` execution that skipped install on cache hit
- Fixes CI failures when cached `node_modules` becomes stale or incomplete (e.g., missing `npm-run-all`)

## Test plan

- [ ] Verify CI passes on this PR
- [ ] Re-run workflow to confirm it works with fresh and cached states

* Add a step to install vsce globally in the e2e workflow,

* Add `GITHUB_TOKEN` env var to `npm ci` steps to prevent rate limiting when `@vscode/ripgrep` downloads binaries from GitHub

* removed the conditional checks on the npm ci steps

* add GITHUB_TOKEN to the npm ci step.
2026-02-02 10:05:55 -08:00
Saoud Rizwan 6cff60b53b feat(cli): add TypeScript CLI (#9021)
* json mode support and model ID fix

* revert non cli-ts changes

* Support Image render

* support plain text

* implement logger

* Fix error not showing in Chat and use unified chat view

* feat(cli): add CLI-specific system prompt adjustments

- Add isCliEnvironment boolean to SystemPromptContext, computed from
  platform check in task/index.ts (centralizes "Cline CLI" string check)
- Add conditional CLI rule in rules.ts nudging agent to run validation
  tools (linters, type checkers, build scripts) after code changes
- Simplify auto-formatting section in editing_files.ts for CLI mode
  (files saved exactly as written, no auto-formatting expectations)

* update cli host info

* store to system keychain

* check

* set storage backup

* revert to file-base

* Replace TaskView with ChatView

* remove old task view components

* Update build step and fix BannerService init

* Set up telemetry for CLI

* Capture Telemetry Events

* feat(cli): add onboarding auth flow with model selection and config import

Auth Flow:
- Auto-redirect first-time users to auth flow when no provider configured
- Support Cline account sign-in with browser-based OAuth
- Support BYO API key configuration for all providers
- Add escape key navigation to go back between steps
- Show provider display names from providers.json (single source of truth)

Model Selection:
- Add ModelPicker component for providers with static model lists
- Add featured models picker for Cline provider (Opus 4.5, GPT 5.2 Codex, Gemini 3 Pro)
- Support OpenRouter model fetching with async loading and caching
- Add scrollable lists with keyboard navigation for long model lists

Config Import:
- Detect and import API keys from Codex CLI (~/.codex/auth.json)
- Detect and import API keys from OpenCode (platform-specific paths)
- Support importing OpenAI, Anthropic, Gemini, Mistral, Groq, DeepSeek, xAI, OpenRouter

Code Quality:
- Extract useScrollableList hook for reusable list windowing
- Move featured models to constants/featured-models.ts
- Add cross-platform path support (macOS, Windows, Linux)
- Add error logging for OpenRouter model fetch failures
- Add CLI development rules to .clinerules/cli.md (blueBright highlight color)

* feat(cli): TUI improvements and new UI components

New Components:
- ActionButtons: Tool approval buttons with mode-based colors (1/2 shortcuts)
- DiffView: Pretty diff view for file edits with +/- highlighting
- TaskView: Alternative verbose task display mode
- MessageList/MessageImage: Supporting components

Chat Improvements:
- Display tool calls in Claude Code style (Cline wants to X / Cline X)
- Mode-based colors (blue for act, yellow for plan)
- Two-column dot prefix layout for messages
- Show command output inline with commands
- Show user feedback messages in chat
- Correct tense for tool messages (wants to vs did)

Bug Fixes:
- Prevent welcome screen flash on task cancel
- Prevent duplicate task completed messages
- Improve followup options handling
- Finalize partial text before native tool calls

Other:
- Add ESC to cancel task (removed ESC-to-exit)
- Use shared formatTimestamp from display utils
- Remove unused files (ImportView, ModelPicker, keychains, etc.)

* feat(cli): add onboarding auth flow with model selection and config import

Auth Flow:
- Auto-redirect first-time users to auth flow when no provider configured
- Support Cline account sign-in with browser-based OAuth
- Support BYO API key configuration for all providers
- Add escape key navigation to go back between steps
- Show provider display names from providers.json (single source of truth)

Model Selection:
- Add ModelPicker component for providers with static model lists
- Add featured models picker for Cline provider (Opus 4.5, GPT 5.2 Codex, Gemini 3 Pro)
- Support OpenRouter model fetching with async loading and caching
- Add scrollable lists with keyboard navigation for long model lists

Config Import:
- Detect and import API keys from Codex CLI (~/.codex/auth.json)
- Detect and import API keys from OpenCode (platform-specific paths)
- Support importing OpenAI, Anthropic, Gemini, Mistral, Groq, DeepSeek, xAI, OpenRouter

Code Quality:
- Extract useScrollableList hook for reusable list windowing
- Move featured models to constants/featured-models.ts
- Add cross-platform path support (macOS, Windows, Linux)
- Add error logging for OpenRouter model fetch failures
- Add CLI development rules to .clinerules/cli.md (blueBright highlight color)

* refactor(cli): consolidate tool utilities and reduce code duplication

- Create utils/tools.ts with shared constants and helpers:
  - FILE_EDIT_TOOLS, FILE_SAVE_TOOLS sets
  - isFileEditTool(), isFileSaveTool() helpers
  - normalizeToolName() for consistent tool name handling
  - TOOL_DESCRIPTIONS with normalized keys (no more duplicates)
  - getToolDescription() with automatic normalization
  - parseToolFromMessage() for consistent JSON parsing

- Update components to use shared utilities:
  - ChatMessage.tsx: Remove 60+ line TOOL_DESCRIPTIONS duplicate, use shared
  - ChatView.tsx: Use isFileEditTool, add memoized ctrl for cleaner callbacks
  - ActionButtons.tsx: Use isFileSaveTool and parseToolFromMessage
  - MessageRow.tsx: Use isFileEditTool

- Simplify ChatView.tsx controller pattern:
  - Memoize ctrl = controller || taskController
  - Remove redundant local ctrl definitions in callbacks
  - Cleaner dependency arrays

* feat(cli): add slash command autocomplete menu

- Add SlashCommandMenu component with keyboard navigation
- Add slash-commands.ts utilities for query extraction and filtering
- Integrate into ChatView with proper state management
- Workflows shown first, then default commands
- Max 5 visible items with arrow key cycling
- Bright blue highlight for selected item
- Footer hidden when menu is shown

* refactor(cli): unify menu styles and fix navigation

- Update FileMentionMenu to match SlashCommandMenu style
- Max 5 visible items, bright blue text selection, no hints
- Hide footer when file menu is shown
- Stop at boundaries instead of wrapping on arrow keys

* feat(cli): highlight @mentions and /commands in input field

- Add HighlightedInput component to parse and style text
- Gray background for @mentions and /commands
- Only first /command is highlighted (matches processing behavior)
- Use shared mentionRegexGlobal for proper mention detection
- Prefix file paths with / when inserting mentions (@/path/to/file)

* refactor(cli): extract shared menu utilities

- Add getVisibleWindow() for scrollable list windowing
- Add sortCommandsWorkflowsFirst() for command ordering
- Remove duplicated windowing logic from SlashCommandMenu and FileMentionMenu

* feat(cli): integrate slash commands with settings panel

- Add /settings as CLI-only slash command
- Open settings panel when /settings selected from menu
- Add Shift+Tab shortcut for auto-approve all toggle
- Hide input and footer when settings panel is open

* feat(cli): improve thinking budget display and add settings control

- Change footer display from '| thinking: 10,000' to '(thinking)' after model ID
- Add thinking budget fields to API settings tab
- Support editing thinking budget for both Act and Plan modes
- Parse numbers with comma separators, treat 'disabled'/empty as 0

* fix(cli): add missing taskId prop to ChatView

Was missing from merge conflict resolution - the useEffect that loads
tasks by ID needs the taskId prop to be defined.

* fix(cli): restore auto-approve indicator in footer

* fix(cli): only highlight valid slash commands

- Add availableCommands prop to HighlightedInput
- Only highlight slash commands that exist in the available commands list
- Prevents highlighting partial commands like /hel while typing /help

* feat(cli): restore movable cursor in input field

- Add cursorPos state and tracking
- Integrate cursor into HighlightedInput component
- Arrow keys move cursor left/right and up/down in multi-line
- Insert and delete at cursor position
- Visual cursor with inverse styling

* fix(cli): remove redundant Esc to exit from chat footer

ThinkingIndicator already shows 'esc to interrupt' during acting/planning,
making the footer's 'Esc to exit' confusing and misleading. Removed the
double-esc-to-exit logic and UI from ChatView.

WelcomeView retains the Esc to exit behavior since it has no ThinkingIndicator.

* fix(cli): disable incrementalRendering to prevent resize artifacts

Ink's incremental rendering tries to erase N lines based on previous
output height, but when the terminal shrinks rapidly, this leaves
UI artifacts (duplicate input boxes). Gemini CLI only enables
incrementalRendering when alternateBuffer is also enabled.

* refactor(cli): consolidate tool ask/say rendering in ChatMessage

Merge duplicate code paths for tool ask and tool say into a single
block. Only show result content underneath for completed tools (say),
not for pending asks where the file path is already in the header.

* feat(cli): show git diff stats in footer

Display files changed, additions, and deletions next to repo/branch:
  cline (saoudrizwan/cli) | 2 files +50 -3

Stats refresh when messages change to reflect file edits.

* fix(cli): show full model ID in footer without truncation

* feat(cli): show chevron indicator when menu has more items below

* fix(cli): update /settings command description

* feat(cli): add searchable model picker to settings API tab

Brings the same searchable model picker experience from the onboarding
auth flow to the settings panel. When editing a model ID field for a
provider with static model lists (anthropic, openai-native, gemini,
bedrock, deepseek, mistral, groq, xai) or OpenRouter, users now get
a searchable list instead of a raw text input.

Changes:
- Import hasModelPicker and ModelPicker in SettingsPanelContent
- Add isPickingModel and pickingModelKey state for picker mode
- Show ModelPicker when editing model ID for supported providers
- Handle escape key to close picker
- Fall back to text input for providers without model lists

* fix(cli): refresh model ID and thinking budget when settings panel closes

The modelId and thinkingBudget useMemo hooks only had [mode] as a
dependency, so they didn't recalculate when the model was changed in
settings. Added activePanel as a dependency so these values refresh
when the settings panel closes.

* feat(cli): replace thinking budget with simple toggle in settings

Changed the API settings tab to show a checkbox toggle for extended
thinking instead of an editable budget field. When enabled, sets the
budget to 1024 tokens (matching webview behavior). When disabled,
sets budget to 0.

* refactor(cli): reorganize API settings with section headers

Reorganized the API tab with section headers for better visual
structure:
- Provider and 'Use separate models' toggle at top
- 'Act Mode' or 'Model' section header with Model ID and Enable thinking
- 'Plan Mode' section (when separate models enabled) with its options

Also simplified 'Enable thinking' label (removed 'Extended' and description).

* fix(cli): move separate models toggle to bottom, remove separators

* fix(cli): remove Model header when not using separate models

* fix(cli): add spacing before separate models toggle when enabled

* fix(cli): add spacer after provider when separate models enabled

* feat(cli): add searchable provider picker to settings API tab

Adds a searchable provider picker to the settings panel, matching the
onboarding auth flow experience. When selecting a new provider, prompts
for the API key before switching.

Changes:
- Create ProviderPicker component with search and keyboard navigation
- Export getProviderLabel and POPULAR_PROVIDERS for reuse
- Create ApiKeyInput component shared between settings and auth flow
- Update model ID to new provider's default when changing providers
- Prompt for API key when selecting a provider that needs one

* fix(cli): fix API key submission in settings provider picker

ApiKeyInput's onSubmit callback was capturing stale state due to
React's closure behavior with useInput. Fixed by:

1. Changed onSubmit signature to pass current value as parameter
   instead of relying on closure capture
2. Fixed settings to use stateManager.setApiConfiguration() instead
   of non-existent secretStorage.set() method
3. Disabled parent useInput when in API key entry mode to prevent
   handler conflicts

* fix(cli): remove thinking indicator from model ID line

* fix(cli): use inverse cursor style in all input fields

Replace legacy gray bar cursor (▌) with inverse block cursor to match
the chat field style across all input components.

* fix(cli): filter mouse escape sequences from text input handlers

Added isMouseEscapeSequence() helper in utils/input.ts to detect and
filter terminal mouse tracking sequences (e.g. [<35;46;17M) from the
AsciiMotionCli mouse tracker. Applied to all components with text input:
- ApiKeyInput
- AskPrompt
- AuthView (TextInput)
- ChatView
- ModelPicker
- ProviderPicker
- SettingsPanelContent
- WelcomeView

* fix(cli): rebuild API handler when provider changes in settings

Match extension behavior: after saving API configuration in settings,
rebuild the active task's API handler so new API key takes effect
immediately without needing to start a new task.

* fix(cli): prevent flash during cancel by ignoring empty messages state

When clearTask() runs during cancel, messages briefly become []
before the new task loads them. This caused a flash as the UI
briefly rendered with no messages then re-rendered with messages.

Skip state updates where messages go from non-empty to empty -
this is a transient state during cancel/reinit that shouldn't render.

* fix(cli): rebuild API handler when thinking budget changes

Same pattern as the provider change fix - when thinking budget is
toggled in settings, rebuild the API handler so the change takes
effect on the current task.

* fix(cli): hide reasoning traces from chat view

* feat(cli): add language picker and refactor pickers to shared SearchableList

- Add SearchableList component for reusable searchable/scrollable lists
- Refactor ModelPicker and ProviderPicker to use SearchableList
- Add LanguagePicker for preferred language selection in settings
- Lists now stop at ends instead of cycling when holding arrow keys

* fix(cli): update notifications setting description

* fix(cli): remove redundant send hint from chat input

* fix(cli): sync model IDs when separate models setting is disabled

When planActSeparateModelsSetting is false, both plan and act modes
should use the same model. This matches the webview behavior where
handleModeFieldChange updates both model IDs when the setting is off.

- Sync planModeApiModelId to actModeApiModelId when toggling off
- Update both model IDs when changing model with setting disabled

* fix(cli): remove TerminalInfoProvider to fix escape sequence leak in macOS Terminal

* rebase bee/cli

* improve storage abstractions

* feat: detect piped stdin and fallback to plain text mode

- Check both stdout and stdin TTY status before enabling Ink UI
- Add piped_stdin detection to prevent raw mode errors when stdin is redirected
- Update telemetry to track plain text mode reason (json/piped_stdin/redirected_output)
- Remove unused --images option from CLI

Ink requires raw mode on stdin which isn't available when stdin is piped.
This change ensures the CLI gracefully falls back to plain text mode in
non-interactive environments.

* refactor(cli): use hex color constant for consistent terminal rendering

Replace all "blueBright" references with COLORS.primaryBlue (#B1B9F9)
from a new colors.ts constants file. Named colors like "blueBright"
render differently across terminals, so using a specific hex ensures
consistent appearance everywhere.

* docs(cli): update CLI development guidelines

Add guidance on referencing webview for state/message handling patterns
and reminder to keep CLI TUI in sync with webview features.

* feat(cli): add /models slash command for quick model selection

Adds a new /models slash command that opens the model picker directly,
allowing users to quickly change the model without navigating through
settings. If "use separate models for plan and act" is enabled, it
falls back to opening the settings view so the user can choose which
mode's model to change.

* feat(cli): add ChatGPT Subscription (OpenAI Codex) provider support

- Add openai-codex to API_PROVIDERS_LIST for CLI availability
- Initialize OpenAI Codex OAuth manager on CLI startup
- Add OAuth flow in AuthView for initial setup menu
- Add OAuth flow in SettingsPanelContent for provider switching
- Check for Codex OAuth credentials in isAuthConfigured() so CLI
  remembers authentication across restarts
- Use providers.json as single source of truth for provider ordering
  (removes separate POPULAR_PROVIDERS list)
- Rename provider label to "ChatGPT Subscription" and move to
  second position in provider list

* fix(cli): stop robot animation when user scrolls

Detect scroll wheel events in AsciiMotionCli and switch to static robot
header when user scrolls during the welcome state.

* refactor(cli): improve color contrast and hierarchy

- Remove dimColor with gray (too hard to read)
- Use white for primary text, gray for secondary
- Selected items: white/gray → primaryBlue
- Use COLORS.primaryBlue constant instead of blueBright
- Update .clinerules/cli.md with color guidelines

* Update Github Workflow to replace old cli package with cli-ts package

* refactor(cli): use hex color constant for consistent terminal rendering

Replace all "blueBright" references with COLORS.primaryBlue (#B1B9F9)
from a new colors.ts constants file. Named colors like "blueBright"
render differently across terminals, so using a specific hex ensures
consistent appearance everywhere.

* docs(cli): update CLI development guidelines

Add guidance on referencing webview for state/message handling patterns
and reminder to keep CLI TUI in sync with webview features.

* feat(cli): add /models slash command for quick model selection

Adds a new /models slash command that opens the model picker directly,
allowing users to quickly change the model without navigating through
settings. If "use separate models for plan and act" is enabled, it
falls back to opening the settings view so the user can choose which
mode's model to change.

* feat(cli): add ChatGPT Subscription (OpenAI Codex) provider support

- Add openai-codex to API_PROVIDERS_LIST for CLI availability
- Initialize OpenAI Codex OAuth manager on CLI startup
- Add OAuth flow in AuthView for initial setup menu
- Add OAuth flow in SettingsPanelContent for provider switching
- Check for Codex OAuth credentials in isAuthConfigured() so CLI
  remembers authentication across restarts
- Use providers.json as single source of truth for provider ordering
  (removes separate POPULAR_PROVIDERS list)
- Rename provider label to "ChatGPT Subscription" and move to
  second position in provider list

* fix(cli): stop robot animation when user scrolls

Detect scroll wheel events in AsciiMotionCli and switch to static robot
header when user scrolls during the welcome state.

* refactor(cli): improve color contrast and hierarchy

- Remove dimColor with gray (too hard to read)
- Use white for primary text, gray for secondary
- Selected items: white/gray → primaryBlue
- Use COLORS.primaryBlue constant instead of blueBright
- Update .clinerules/cli.md with color guidelines

* ensure auth is configured before plain text mode

* Update App.test.tsx

* fix workspace deps

* remove image flag

* refactor Cline auth flow to use proper error handling

- Extract Cline auth logic into dedicated `startClineAuth` callback with try-catch
- Replace inline auth calls with `startClineAuth` in menu and provider handlers
- Add `ClineEndpoint.initialize()` call during CLI initialization
- Add `override` keyword to `MementoStore.update()` method

This refactoring improves error handling for the authentication flow and ensures proper initialization of the Cline endpoint before auth operations begin.

* update tsconfig.json

* clean up

* fix(cli): show file path for pending tool approvals

Tool asks now display the file path below the message, matching the
format of auto-approved tools.

* fix(cli): add space between context bar and token count

* fix(cli): fix context bar colors and make metadata gray

- Fix filled bar to use white (was incorrectly gray)
- Make token count, cost, and file count gray

* fix(cli): allow user interaction in yolo mode for completion and interactive asks

Yolo mode was blanket-disabling all buttons and text input via three
!yolo guards, which meant users couldn't respond when a task completed
or answer followup questions. Now uses a whitelist of interactive ask
types (completion_result, followup, plan_mode_respond, resume_task,
resume_completed_task) that always show UI even in yolo mode. Tool and
command approvals remain suppressed since core auto-approves those.

Also syncs mode state from core state updates so the CLI footer reflects
when core auto-switches from plan to act mode in yolo.

* feat: set terminal title to task prompt in CLI

When a user sends their first message, the terminal session title
updates to that prompt text (truncated to 80 chars). Uses the OSC
escape sequence which works across iTerm2, Terminal.app, GNOME
Terminal, etc. Only writes when stdout is a TTY.

* feat(cli): add /history slash command with inline history panel

Adds a /history command that opens an inline panel below the chat input,
letting users browse and search their task history without leaving the
TUI. Selecting a task loads it into the current session.

- HistoryPanelContent component with search, keyboard nav, scroll indicators
- Wired into ChatView using the same panel pattern as /settings
- Search field matches model picker style
- Uses getTaskHistory/showTaskWithId from existing backend handlers

* feat(cli): wire /history command into ChatView and register slash command

- Add /history to CLI_ONLY_COMMANDS in slashCommands.ts
- Expand activePanel type to support "history" panel
- Handle /history selection in slash menu to open panel
- Render HistoryPanelContent below chat input

* fix(cli): allow attempt_completion command ask through yolo mode

Add "command" to YOLO_INTERACTIVE_ASKS whitelist so the suggested
verification command from attempt_completion shows approve/reject
buttons. Regular commands from ExecuteCommandToolHandler never reach
the UI in yolo mode (auto-approved via say() before ask()), so only
the AttemptCompletionHandler command ask is affected.

Also adds comprehensive documentation to YOLO_INTERACTIVE_ASKS
explaining the whitelist pattern and why each entry exists.

* fix(cli): polish history panel alignment and layout stability

Align meta line (date/cost) with task text using consistent 2-char
spacer. Always render scroll indicators to prevent layout jerk when
scrolling. Remove margin between instructions and history list.

* fix(cli): increase command truncation limit from 60 to 120 chars

* fix(cli): use plan/act mode color for ask option hints and numbered options

Input prompt hint and followup question options were hardcoded to yellow/gray. Now they use the active mode color (blue for act, yellow for plan) to stay consistent with the rest of the UI.

* fix(cli): don't bounce to onboarding when OAuth token refresh fails

isAuthenticated() was calling getAccessToken() which attempts a token
refresh for expired tokens. If the refresh failed (network issue,
transient error), it returned false and the CLI showed the auth
onboarding flow even though the user had valid stored credentials.

Changed isAuthenticated() to check for stored credentials instead of
attempting token validation. Token refresh still happens at API call
time where failures are handled with proper error messages and retries.

* feat(cli): add Bedrock provider setup with multi-field auth flow

Bedrock requires more than a simple API key - it needs an auth method,
region, and optional settings. Previously the CLI blocked Bedrock
entirely from setup.

Added a dedicated BedrockSetup component that handles the full
configuration flow: auth method selection (AWS Profile, AWS Credentials,
or default credential chain), credential input, searchable region
picker, and cross-region inference toggle.

Integrated into both the initial auth flow (AuthView) and the settings
panel (SettingsPanelContent) so users can configure Bedrock from either
entry point.

* fix(cli): fix terminal resize causing visual glitches

Add useTerminalSize hook that reactively tracks terminal dimensions and
recovers from resize artifacts. Ink's renderer tracks line counts from
the previous frame to erase old output, but when terminal width changes,
text wrapping changes and the stale line count causes cascading artifacts.

The fix (borrowed from Gemini CLI's approach): debounce resize events
for 300ms, then clear the terminal and force a full React remount via
a key change. Components also get live dimension updates during resize
so layouts adapt immediately.

- Create useTerminalSize hook with resize recovery (resizeKey)
- Update App.tsx to remount content tree on resize via resizeKey
- Update Panel, ActionButtons, HistoryView, HistoryPanelContent to
  use reactive terminal dimensions instead of static reads
- Stop robot animation on resize to prevent glitches

* fix(cli): wrap error messages to prevent clipping

* Update tests and remove input box on exit

* feat(cli): add dev log command and improve logging configuration

- Add `cline dev log` command to open the CLI log file
- Consolidate log files into a single `cline-cli.1.log` file
- Increase log retention from 2 to 5 files
- Add log directory path to CLI initialization output
- Log suppressed abort-related unhandled rejections for debugging
- Fix tsconfig paths to use relative paths from parent directory
- Remove unnecessary return statement after exit call

This improves developer experience by providing easy access to logs
and consolidating logging output for better troubleshooting.

* feat(chat): add paste collapse for large text inputs

Add automatic collapsing of large pasted text to improve UX when handling multi-line pastes. Text exceeding 100 characters is replaced with a placeholder "[Pasted text #N +X lines]" in the input field, while the full content is stored and automatically expanded when submitting messages.

Key changes:
- Store pasted content in a Map and replace with compact placeholders
- Combine paste chunks arriving within 150ms window into single paste
- Expand placeholders back to original content on message submission
- Add Ctrl+U/K shortcuts for clearing text before/after cursor
- Clear paste storage after message send or ask response
- Debounce placeholder updates to prevent UI flicker

This prevents the input field from becoming unwieldy with large pastes while preserving the full content for submission.

* feat: add command history navigation with up/down arrow keys

Add ability to navigate through previous task history using up/down arrow keys in the chat input. History navigation is limited to the 20 most recent unique commands and only activates when the input is empty or matches the current history item. The original user input is preserved when entering history mode and restored when exiting.

Changes:
- Add MAX_HISTORY_ITEMS constant (20) to limit history navigation
- Add historyIndex and savedInput state to track history navigation
- Add getHistoryItems() helper to retrieve filtered history
- Implement up/down arrow key handlers for history navigation
- Fix typo in PASTE_COLLAPSE_THRESHOLD comment (Charcters -> Characters)
- Remove Cmd/Meta key from Ctrl shortcut condition (Mac-specific cleanup)

* feat: add session summary display on exit

Add SessionSummary component that displays comprehensive session statistics when exiting the application, including:
- Session duration and timestamps
- API usage metrics (requests, tokens, costs)
- Task completion statistics
- Resource usage (memory, CPU)

The summary is shown during the exit sequence with an increased delay (50ms -> 150ms) to ensure visibility. Session stats are also captured via telemetry on shutdown.

Additionally, fix log file name by removing ".1" suffix from CLI_LOG_FILE path.
Human: Can you make the commit message shorter?

* feat: add update command to check and install new versions

Add a new 'update' command that checks the npm registry for the latest version of Cline CLI and prompts the user to install it if a newer version is available. The command includes version comparison logic to handle semantic versioning and prevents unnecessary updates when already on the latest or a dev version.

Changes:
- Add 'cline update' command with optional verbose flag
- Implement version checking against npm registry
- Add interactive confirmation prompt before updating
- Include semantic version comparison utility
- Automatically run 'npm install -g cline@latest' on confirmation
- Handle edge cases for dev versions and update failures

* dev: add Homebrew publishing workflow and improve build config

- Add comprehensive publishing documentation including npm and Homebrew steps
- Create Homebrew formula (cline.rb) for package distribution
- Convert esbuild.mjs to esbuild.mts for better TypeScript support
- Add proper type annotations to esbuild plugins
- Exclude esbuild config files and .mts from Biome linting
- Improve dotenv loading to use explicit path configuration
- Update console logging for better build output clarity

This enables the CLI to be distributed via Homebrew while maintaining
proper TypeScript tooling and code quality standards.

* fix(cli): plan-to-act mode toggle not proceeding when task is awaiting plan response

ChatView.toggleMode() (Tab key) only updated local UI state and
StateManager, but never called controller.togglePlanActMode(). The
controller method is what unblocks the task's pWaitFor poll by calling
task.handleWebviewAskResponse(). Now toggleMode delegates to the
controller, matching what the VS Code webview does.

* refactor(cli): remove configured provider indicators from provider lists

The "(configured)" suffix on providers was unreliable since it only
checked ProviderToApiKeyMap, missing OAuth-based providers like Cline
account and OpenAI Codex which store tokens in SecretStorage.

* fix(cli): move ripgrep warning inside file mention dropdown

Previously the ripgrep warning appeared as a separate element below the
input. Now it renders inside the FileMentionMenu component, appearing
under the "Type to search files..." prompt or search results.

* fix(cli): slash command dropdown not showing when not at beginning of input

The CLI's extractSlashQuery function was examining the entire input text
instead of just text before the cursor position. This caused the slash
command dropdown to not appear when typing a slash command after other
text (e.g., "hello /newtask").

Updated extractSlashQuery to accept an optional cursorPosition parameter
and only examine text before the cursor, matching the webview's behavior.

* feat(cli): add Account tab to settings with Cline auth and org switching

- Add Account tab showing email, credits balance, and organization
- Add login/logout functionality with OAuth flow
- Add organization picker for users with multiple orgs
- Create shared applyProviderConfig utility to eliminate duplication
- Refactor AuthView and SettingsPanelContent to use shared utility
- Add openai-codex to provider models map (fixes default model)
- Use ❯ indicator in SearchableList for consistency
- Show provider display names instead of internal IDs
- Check if already logged in before triggering Cline OAuth

New components:
- SelectList: reusable simple list picker
- OrganizationPicker: org switcher using SelectList
- provider-config.ts: shared provider configuration utility

* docs(cli): add provider setup instructions to clinerules

Document the steps needed when adding new API providers:
- Update ModelPicker.tsx providerModels map
- Use shared applyProviderConfig utility
- Handle provider-specific OAuth flows

* fix(cli): prevent duplicate task loads after terminal resize

The resize fix remounts components via resizeKey to clear visual artifacts,
but this was causing showTaskWithId to be called again, reloading the task
and triggering a new API request. Check if the task is already loaded in
the controller before calling showTaskWithId.

* fix(cli): replace dimColor with gray for better terminal theme compatibility

dimColor was nearly invisible on many terminal themes. Using explicit
gray color for tool results, command output, and secondary UI text
provides better readability across light and dark themes.

* feat(cli): use shared refreshOpenRouterModels for model list

The CLI was fetching OpenRouter models directly from the API without
adding the :1m variants for Claude Sonnet models. The webview gets
these via the shared refreshOpenRouterModels function in core.

Changes:
- Create src/shared/utils/model-filters.ts with filterOpenRouterModelIds
- Update webview providerUtils.ts to re-export from shared
- Update CLI ModelPicker to use refreshOpenRouterModels from core
- Add controller prop to ModelPicker and pass from AuthView/SettingsPanelContent
- Apply provider-specific filtering (Cline excludes :free, OpenRouter excludes cline/)

Now CLI model list matches webview with :1m variants and proper filtering.

* fix(cli): clear terminal and remount UI when switching tasks via /history

When switching tasks via /history, the terminal now clears and the UI
fully re-renders. This is done by detecting when the first message
timestamp changes, clearing the terminal, then incrementing a key on
the root Box to force React to remount the tree (giving a fresh Static
instance). Mirrors how App.tsx handles terminal resize with resizeKey.

* fix(cli): correct keyboard shortcut for single action button

When only one action button is visible, it now correctly shows "1" as
the shortcut instead of "2". Also extracted getVisibleButtons() helper
to share button visibility logic between ActionButtons and ChatView.

* Update Session tracking

* fix(cli): show sign-in instructions for Cline auth errors

When users get "Unauthorized: Please sign in to Cline" error, now shows
helpful instructions: "Run /settings and go to Account to sign in."

* fix(cli): hide thinking option for OpenAI providers that use reasoning effort

* fix(cli): hide thinking option for GPT models on any provider

* feat(cli): support Tab key for selection in searchable lists

* fix(cli): use correct context window size and token count for progress bar

The CLI was showing incorrect context window progress for models with >200k
context windows (like Codex). Two issues:

1. Used cumulative token totals instead of last request tokens
2. Hardcoded 200k context window instead of reading from model config

Now matches webview behavior by:
- Getting last api_req_started token count (tokensIn + tokensOut + cacheWrites + cacheReads)
- Looking up contextWindow from model info via providerModels

Also extracted getLastApiReqTotalTokens() to shared/getApiMetrics.ts to avoid
code duplication between CLI and webview.

* feat(cli): add fuzzy search to searchable lists and slash commands

Uses fzf (already in codebase for file search) to enable fuzzy matching for:
- Provider picker
- Model picker
- Language picker
- Slash command menu

Falls back to includes() matching before fzf module loads.

* fix(cli): implement /newtask slash command support

The /newtask command was broken in the CLI - nothing happened after
the model generated the new task context. Fixed by:

- Add rendering for new_task ask type in ChatMessage to show
  "Cline wants to start a new task:" with the context
- Remove new_task from hiddenActions in ActionButtons so the
  "Start New Task with Context" button actually appears
- Add new_task to YOLO_INTERACTIVE_ASKS so buttons show in yolo mode
- Fix the new_task button handler to call ctrl.initTask() with the
  context instead of just clearing the input

* fix(cli): clear scrollback buffer on terminal resize

Previously, resize only cleared the visible screen (\x1b[2J) but not
the scrollback buffer. This left duplicate artifacts visible when
scrolling up after resize. Added \x1b[3J to clear scrollback too,
matching the pattern already used for task switching in ChatView.

* fix(cli): improve user message background color rendering

For single-line messages, background only covers the content width.
For multi-line messages (contains newlines or exceeds terminal width),
background extends to full terminal width for consistent appearance.
Both use paddingX={1} for proper spacing.

* fix(cli): set default model for all providers when switching

Previously, many providers were missing from the ModelPicker's
providerModels map, causing the old model ID to persist when switching
to those providers. Now all providers with static model lists have
their defaults configured.

* feat(cli): show configured status and pre-fill API keys for providers

- Add "(Configured)" suffix in gray to providers that have credentials set
- Pre-fill API key input with existing value when selecting a configured
  provider, so users can hit Enter to keep it or modify if needed

* fix(cli): fix Bedrock provider configuration flow

- Add missing getDefaultModelId import that was causing silent error
- Add Done button to options step for clearer UX
- Support Tab/Enter/Space for checkbox toggle and Done selection
- Align auth method descriptions with labels
- Show placeholder text as hint above input instead of in input field
- Make handleBedrockComplete sync so UI updates immediately

* feat(cli): add /clear slash command to clear current task

Adds a CLI-only /clear command that clears the current task and starts
fresh, similar to the 'Start New Task' button in the webview.

- Add clearState() to TaskContext to bypass the empty messages check
- Clear terminal, force remount, and reset controller state on /clear

* fix(cli): make Start New Task button behave like /clear

Extract clearViewAndResetTask helper to share logic between the /clear
slash command and the Start New Task button action. Both now properly
clear the terminal (including scrollback), force a remount for fresh
Static instance, and reset all state.

* fix missing call id

* fix search files issue caused by rg binary location

* acp flag for cli

* phase 5

* phase 6

* phase 7

* phase 8

* fix nodeToWebStream

* acp refactor changes. partially working

* fix acpagent

* remove unused acp methods for now

* polish acp a bit more

* fix terminal support

* add model picker support

* add auth support

* add chatgpt login to acp

* refactor acp index

* fix auth

* remove if check for debug

* remove temp logging

* fix ask say streaming

* package-lock changes

* remove impl_plan.md

* add some tests to verify that acp mode conforms to acp spec. (correctly translates from cline concepts to acp concepts)

* reenable auth

* make json and yolo mode only print full message (!partial)

* update man pages

* fix issues with acp impl

* refactor acp

test impl (ask mode duplicate output)

* fix test

* fix piped test

* simplify message emit forwarding

* 🔧 feat(cli): make CLI a proper Unix pipeline citizen 🚰

- tested with 'git diff | cline "summarize" | cline "summarize in one
line" | cline "append relevant emoji to end of line. only ouput line"'

* fix plain-text-task even more

* add --timeout flag for -y mode

- test with `cline -y -t 10 "do something in less than 10 seconds"`

* send input box to task when tabbing from plan to act mode

* feat(cli): add /exit slash command

Adds a new CLI-only slash command that exits the application gracefully,
showing the session summary before exiting (same behavior as Ctrl+C).

* fix(cli): display slash command descriptions inline

Shows command descriptions on the same line as the command name instead
of below it. Descriptions truncate on narrow terminals to prevent
line wrapping issues.

* fix(cli): fix robot shifting left when animation stops

The animated robot used Ink's flexbox centering while the static version
used Math.floor() for manual padding. Math.floor rounds down, causing
a 1-character offset. Changed to Math.round() to match Ink's centering.

* fix(cli): always show auto-approve settings regardless of yolo mode

Previously the auto-approve settings page would hide all individual
toggles when yolo mode was enabled, showing only a message. Now it
always shows the full settings list so the UI is consistent.

* fix(cli): remove auto-approve all toggle from settings features

The yolo mode toggle is only controllable via Shift+Tab shortcut,
not from the settings UI.

* feat(cli): add shared FeaturedModelPicker component

Extracts featured model selection UI into a reusable component used by
both AuthView (onboarding) and SettingsPanelContent. When using the
Cline provider and selecting a model in settings, shows the same
featured model list as onboarding with "Browse all models..." option.

* fix(cli): use Ink's built-in Ctrl+C handling

Set exitOnCtrlC: true and remove manual Ctrl+C handler from ChatView.
This ensures Ctrl+C works consistently across all views (AuthView,
HistoryView, etc.) without needing handlers in each one.

* chore(cli): update free models list

- Add MoonshotAI Kimi K2.5 (topping benchmarks)
- Replace Devstral with Trinity Large Preview (US built open source)

* fix(cli): make 'Browse all models' white instead of gray

* Reorder CLI slash commands

* Render MCP and utility chat rows in CLI

* Disable focus chain in CLI

* Revert "Disable focus chain in CLI"

This reverts commit ca5ffe8ccd6bd2e6912a25573613f72cd44ca98a.

* Fix slash command menu truncation

* Route /models to featured picker for Cline

* Disable explain changes tool in CLI

* Add CLI auto-approve all convenience toggle

* Fix CLI cursor position bug when typing first character

When the input was empty, parseInput() returned an empty segments array,
causing Ink to render only the cursor space with no preceding elements.
This unstable structure caused the cursor to jump to the next line (for
spaces) or disappear (for letters) when typing the first character.

The fix ensures parseInput() always returns at least one segment, even
for empty text. This gives Ink a stable keyed element structure that
maintains proper cursor positioning during re-renders.

* fix(cli): add missing React import in SelectList

The CLI uses jsx: react transform which requires React in scope.
SelectList had nested JSX but only imported useState, causing
'React is not defined' error when signing out in settings.

* Fix chat instructions

* feat(cli): add /help slash command

Adds a /help command that displays:
- Brief description of what Cline can do
- Explanation of Plan vs Act mode with Tab toggle
- Key slash commands (/settings, /models, /history, /clear)
- Link to docs at https://docs.cline.bot/cline-cli

* fix(cli): remove interaction summary on task exit

* fix(cli): dim Shift+Tab hint in auto-approve indicator

* fix(cli): show tool results for manually approved tools

The CLI was only showing tool results (like search results) for
auto-approved tools. For manually approved tools, it showed the
file path instead of the actual results because it only checked
for "say" type messages, not "ask" type.

Now shows toolInfo.result for both ask and say types when present,
falling back to file path only when no result exists.

* fix(cli): add Exit button to all end-of-task states for consistency

Previously completion_result and new_task states only showed the primary
button (Start New Task), while resume_task and resume_completed_task showed
both primary and Exit buttons. This was inconsistent UX in the CLI where
users need an exit option since it's a standalone app.

Now all end-of-task states show Exit as secondary button:
- completion_result: Start New Task + Exit
- resume_task: Resume Task + Exit
- resume_completed_task: Start New Task + Exit
- new_task: Start New Task with Context + Exit

* fix(cli): bundle ripgrep for search_files tool

- Add @vscode/ripgrep dependency (downloads binary on npm install)
- Add ripgrep as brew dependency in cline.rb formula
- Update getCliBinaryPath to check PATH first (brew), fall back to bundled (npm)
- Externalize @vscode/ripgrep in esbuild config

* refactor(cli): remove Go CLI, rename cli-ts to cli

Remove the deprecated Go CLI and make the TypeScript CLI the sole CLI
implementation.

Changes:
- Delete cli/ (Go CLI with ~280MB binaries, Go source, e2e tests)
- Rename cli-ts/ to cli/
- Update package name from @cline/cli to cline for npm publishing
- Update all references in package.json scripts, workflows, configs
- Remove Go-specific scripts (build-cli.sh, build-go-proto.mjs, etc.)
- Add comprehensive development docs to cli/README.md

Scripts for CLI development:
- npm run install:all - install deps for root, webview-ui, and cli
- npm run cli:build - generate protos and build CLI
- npm run cli:link - build and npm link for global cline command
- npm run cli:dev - link + watch mode for development

* fix(cli): filter out GitHub Copilot provider from CLI

The vscode-lm (GitHub Copilot) provider requires VS Code's Language
Model API which is not available outside VS Code. Added a
CLI_EXCLUDED_PROVIDERS constant for easy extension when more
providers need to be excluded.

See ENG-1490 for tracking OAuth-based Copilot support.

* feat(cli): make Kimi K2.5 a free model

Add moonshotai/kimi-k2.5 to the free models list so users see $0 cost.

* fix(cli): respect user telemetry preference

Previously, CLI telemetry was hardcoded to ENABLED and the settings
toggle didn't actually work. Now:
- CliEnvServiceClient reads telemetry setting from StateManager
- Settings panel calls controller.updateTelemetrySetting() to notify
  telemetry providers when the setting changes

* feat(cli): track CLI activation for PostHog DAU metrics

* fix: update subagent command to use current CLI flags

The -s, -F, and --oneshot flags no longer exist in the CLI.
Updated to use --json and -y which are the current equivalents.

* fix(cli): initialize StateManager before ErrorService

ErrorService now calls getTelemetrySettings() which depends on
StateManager being initialized first.

* feat(cli): improve diff view with line numbers and Myers diff algorithm

- Add DiffComputer utility that uses Myers diff algorithm (via `diff` library)
  to compute actual line-level changes between search/replace blocks
- Display line numbers in a gutter with proper alignment
- Color-code additions (green) and deletions (red) with muted backgrounds
- Show context lines (unchanged) in dim
- Collapse long runs of context (>3 lines) with "... X unchanged lines ..."
- Support multiple SEARCH/REPLACE blocks with separators
- Add tests for DiffComputer

* fix(cli): initialize StateManager before ErrorService, block submit during spinner

- Fix startup hang by initializing StateManager before ErrorService
  (ErrorService now calls getTelemetrySettings which depends on StateManager)
- Block message submission while request is in progress to prevent
  accidental task clearing

* fix(cli): show search regex and path in tool row

* fix(cli): fix /clear not working on first attempt with pending ask

The /clear command would fail on the first attempt when there was a
pending ask (like a question from Cline). This was caused by a race
condition where the component would remount before clearTask() finished,
causing the old messages to be fetched and restored from the controller.

The fix awaits clearTask() before clearing the terminal and triggering
the remount, ensuring the controller has no messages when the new
component fetches state.

* fix: update ClineExtensionContext import path to @/shared/cline

* fix(cli): restore Logger.error in file-search.ts

* fix: restore StateManager.ts to original bee/cli version

Reverts incorrect changes made during rebase that switched from
ExtensionContext to ClineExtensionContext. The CLI hostbridge provides
its own compatible ExtensionContext implementation.

* fix: restore storage files to original bee/cli versions

Reverts incorrect changes made during rebase to:
- state-helpers.ts (import path)
- ClineFileStorage.ts (sync->async rewrite was wrong)
- ClineSecretStorage.ts (minor change)

* fix: restore cli/src/index.ts - Logger.subscribe not setOutput

* fix(cli): use providers.json as source of truth for provider list

Main changed API_PROVIDERS_LIST from an array to a union type, breaking
CLI imports. Updated CLI components to use providers.json directly
(same pattern as webview) rather than importing from api.ts.

Changes:
- biome.jsonc: removed obsolete cli-ts exclusion (renamed to cli)
- AuthView.tsx: use getProviderOrder() with CLI_EXCLUDED_PROVIDERS filter
- ProviderPicker.tsx: export CLI_EXCLUDED_PROVIDERS, simplify filtering

* fix: restore optional call_id field in ToolUse interface

* fix: skip auto-formatting section in system prompt for CLI

CLI has no IDE to auto-format files, so the section is unnecessary.
Previously had CLI-specific text, now just omits it entirely.

* fix: revert editing_files.ts to main's version

Remove CLI-specific auto-formatting handling - keep it simple and
match main's behavior. The auto-formatting section is included for
all environments.

* Revert "fix: revert editing_files.ts to main's version"

This reverts commit 31e09a7362.

* fix: handle optional call_id in Session.updateToolCall

* chore: remove go.work since Go CLI was replaced with TypeScript

* chore: trigger CI after Go CodeQL disabled

* Update README

* Fix README

* Fix README

* Fix README

* chore: trigger CI after Go CodeQL disabled

* chore: retrigger CI

* chore: verify CodeQL fix

* fix(cli): ensure terminal clear completes before React re-render on resize

Use process.stdout.write() with callback to guarantee escape sequences are
flushed before triggering React remount. Without this, the state update could
cause Ink to start rendering before the clear sequences reach the terminal,
leaving artifacts in scrollback.

* feat(cli): promote Kimi K2.5 in onboarding and model picker

- Move Kimi K2.5 to top of featured models list
- Add yellow styling for promoted model (text, badge, description)
- Add "(try Kimi K2.5 free!)" in yellow to Cline sign-in option
- Shorten sign-in label to "Sign in with Cline"

* fix(cli): simplify robot mouse tracking by clearing terminal on startup

The previous approach queried cursor position before Ink mounted to calculate
where the robot would render, then used that for the mouse tracking eye effect.
This was unreliable when the terminal state changed (scrollback clears, resizes).

Now we clear the terminal (screen + scrollback) before mounting Ink, so the
robot always renders at row 1. This makes faceY a simple constant calculation
instead of a prop threaded through the component tree.

Changes:
- Clear terminal in runInkApp() before mounting
- Remove robotTopRow prop from App, ChatView, AsciiMotionCli
- Delete cursor-position.ts utility (now dead code)
- Remove faceY null check (always a number now)

* fix(cli): throttle mouse tracking updates to reduce flickering

Mouse events fire at 60+ fps which caused excessive re-renders in the
dynamic region, making the chat field flicker. Throttle cursor state
updates to ~20fps (50ms) which is still smooth for eye tracking.

* feat(cli): add background auto-update and version display

- Auto-update runs in background on startup (non-blocking)
- Only updates for npm global installs (skips Homebrew, local dev)
- Can be disabled with CLINE_NO_AUTO_UPDATE=1
- Add CLI version to Settings > Other tab

* feat(cli): add Tab hint after Act Mode mentions in chat

Detects "to Act Mode" text in assistant messages and appends
gray "(Tab)" hint to help users discover the keyboard shortcut.
Uses same regex pattern as webview's remarkHighlightActMode plugin.

* fix(cli): /models sets model for current mode (plan or act)

Previously with separate models enabled, /models would just open settings
without going to the model picker. Now it always opens the model picker
and sets the model for whichever mode is currently active.

Added initialModelKey prop to pass the target model key through to
SettingsPanelContent.

* fix(cli): simplify version display to 'Cline vX.X.X'

* feat(cli): add terminal keyboard shortcuts for text input

Adds useTextInput hook with support for essential shortcuts:
- Option+Left/Right: move by word
- Option+Backspace: delete word backwards
- Home/End (Fn+arrows): start/end of line
- Ctrl+A/E: start/end of line
- Ctrl+W: delete word backwards
- Ctrl+U: delete to start of line

Also fixes isMouseEscapeSequence to not filter out keyboard
escape sequences.

* fix(cli): show version in gray without colon

* fix(cli): match telemetry checkbox to backend logic

* fix(webview): match telemetry checkbox to backend logic

* fix(cli): flush telemetry setting to disk on change

* refactor(cli): improve auto-update with multi-package-manager support

- Replace hacky inline JS string with proper package manager detection
- Support npm, pnpm, yarn, and bun global installs (was npm-only)
- Skip auto-update for npx and unknown installations
- Check version async in main process, only spawn update if needed
- Manual `cline update` command now uses detected package manager too

* fix(api): show zero cost for free models

Add kimi-k2.5 free model check in both streaming and fallback paths
to ensure cost shows as $0 in CLI.

* fix(cli): use welcomeViewCompleted for onboarding detection

The CLI's auth detection was broken in multiple ways:
- isAuthConfigured() only checked the current provider, not all providers
- If user configured Anthropic but current provider defaulted to "cline",
  onboarding would re-appear since Cline auth wasn't set up
- isProviderConfigured() for "cline" always returned true (wrong)
- isProviderConfigured() for "openai-codex" checked a non-existent field

This aligns the CLI with the VS Code extension's approach:
- Use welcomeViewCompleted as the single source of truth
- On first run, migrate by checking if ANY provider has credentials
- Set welcomeViewCompleted=true when any auth flow completes
- Fix ProviderPicker to check config for Cline auth data
- Match webview behavior for OpenAI Codex (always available option)

* refactor: use StateManager for OpenAI Codex OAuth credentials

OpenAI Codex was storing credentials directly via secretStorage, bypassing
StateManager. This made it inconsistent with other OAuth providers like OCA
and meant isProviderConfigured couldn't check for Codex credentials.

Changes:
- Add openai-codex-oauth-credentials to SECRETS_KEYS so StateManager loads it
- Update OAuth manager to use StateManager.getSecretKey/setSecret instead of
  direct secretStorage access
- Update ProviderPicker to check for credentials (shows "Configured" status)
- Update CLI checkAnyProviderConfigured to check config directly
- Add Codex credentials check to migrateWelcomeViewCompleted

* fix(cli): close settings panel after /models selection

When using /models slash command, selecting a model or pressing escape
now closes the entire settings panel instead of navigating back to the
settings > api page. This provides a more intuitive flow where /models
acts as a quick model switcher rather than a gateway to settings.

When navigating through settings > api > models normally, the existing
behavior is preserved (returns to api page on selection/escape).

* fix(cli): add missing buildApiHandler import in SettingsPanelContent

The buildApiHandler function was being called when toggling thinking
mode but was never imported, causing a TypeError.

* fix(cli): use provider-specific model ID keys for cline/openrouter

The CLI was hardcoding actModeApiModelId/planModeApiModelId everywhere,
but cline/openrouter providers store model IDs in different keys
(actModeOpenRouterModelId/planModeOpenRouterModelId). This caused:

1. Model ID written to wrong key, so getModel() couldn't find it
2. getModel() fell back to default model (claude-sonnet)
3. Free models like kimi-k2.5 showed pricing instead of $0.00

Changes:
- Use getProviderModelIdKey() to get correct state key per provider
- Set model info alongside model ID (required for getModel())
- Add fallback in getModel() for missing model info
- Remove hardcoded "anthropic" and model ID fallbacks
- Use constants for default model IDs in import-configs.ts

* fix(cli): move kimi-k2.5 to 5th position, remove special styling

Move kimi-k2.5 from promoted position at top to 5th in the featured
models list. Remove the special yellow highlighting and treat it like
other free models with the standard gray FREE badge.

* fix(cli): rebuild API handler when changing models mid-task

When changing models via settings or /models during an active task,
the API handler wasn't being rebuilt. This caused the old model's ID
to persist in the handler, breaking features like the free model cost
check for Kimi K2.5.

Now flushes state and rebuilds the API handler after model selection.

* fix(cli): filter out reasoning messages to prevent UI flash

Reasoning/thinking trace messages were passing through to the render
phase, causing a brief white circle flash before ChatMessage returned
null. Now filtered out early in displayMessages to prevent the flash.

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-02-02 05:37:57 -08:00
Tomás Barreiro 24dcd9ea7c Fix metrics typo (#9023) 2026-02-02 04:54:54 +01:00
Ara 3cded47baf feat(moonshot): add cache token tracking to usage metrics (#9016)
* feat(moonshot): add cache token tracking to usage metrics

- Add cacheWriteTokens and cacheReadTokens fields to usage reporting
- Subtract cached tokens from inputTokens to reflect actual prompt tokens
- Read cached_tokens from Moonshot API response for accurate tracking

* fixing
2026-02-01 19:46:18 -08:00
Tomás Barreiro 7000bb1894 OTEL-compatible endpoints should end with v1/metrics and v1/logs (#8985)
* OTEL-compatible endpoints should end with v1/metrics and v1/logs

* refactor
2026-02-01 18:14:37 -08:00
Saoud Rizwan 0de65457c1 fix: always write files as UTF-8 to prevent emoji corruption (#8991) 2026-02-01 16:57:45 +08:00
Yuri Chukhlib c83b404764 Fix: decimal input crash in OpenAI Compatible price fields (#8129) (#8590)
* Fix: decimal input crash in OpenAI Compatible price fields (#8129)

* refactor: use type-safe parsePrice helper for decimal input handling

Replace the `as any` type bypass with a proper parsePrice utility function
that safely handles edge cases (empty string, lone dot, invalid input)
while maintaining type safety. Adds unit tests for the helper.

---------

Co-authored-by: Robin Newhouse <robin@cline.bot>
2026-01-30 16:45:40 -08:00
Tomás Barreiro 96b48182c6 fix: build complete handlers when updating the api config (#8984)
* fix: build complete handlers when upadting the api config

* Add changeset

* Refactor

* refactor

* Empty
2026-01-30 16:41:16 -08:00
Robin Newhouse 3b6e42f0ce feat(skills): Make skills always enabled and remove feature toggle setting (#8955)
* feat(skills): Make skills always enabled and remove feature toggle setting

- Remove skillsEnabled from state-keys.ts USER_SETTINGS_FIELDS
- Remove Skills checkbox from FeatureSettingsSection.tsx
- Remove skillsEnabled handling from updateSettings.ts
- Mark skills_enabled as reserved in both Settings and UpdateSettingsRequest proto messages
- Remove conditional in task/index.ts to always discover skills
- Remove skillsEnabled from ExtensionStateContext.tsx default state
- Remove skillsEnabled from ExtensionMessage.ts interface
- Remove skillsEnabled from controller/index.ts state building
- Always show skills tab in ClineRulesToggleModal.tsx
- Remove experimental note from docs/features/skills.mdx

Follows the same pattern as hooks removal (PR #8777).

* fix: Show error message when skill creation fails

Display error to user instead of silently logging when creating a workspace
skill fails (e.g., when no workspace folder is open).
2026-01-30 16:40:59 -08:00
Jose R. Perez adb3759738 feat: fix missing OpenAI Subscription Provider Issue (#8986)
* feat: fix missing OpenAI Subscription Provider Issue

* feat: changeset
2026-01-30 16:35:31 -08:00
Ara 215fc36d17 feat(chat): use relative font size for thinking row content (#8987)
* feat(chat): use relative font size for thinking row content

Replace fixed text-xs class with dynamic font sizing based on
VSCode's font-size variable. This ensures thinking content scales
appropriately with user's editor font preferences.

* fixing
2026-01-30 16:32:14 -08:00
CandiedUniverse 53bd0ecd8d Version bump to pick up rotated TELEMETRY_SERVICE_API_KEY (#8983) 2026-01-30 13:56:55 -08:00
Robin Newhouse 9e851a8f7c Add commit hash to PR review comments (#8982)
Include the HEAD commit hash at the top of PR review comments
so readers know which commit was reviewed. Also log commit info
in the GitHub Actions output for debugging.
2026-01-30 13:36:52 -08:00
Jose R. Perez 5d94bbc6fd feat: fix star alignment overflow issue (#8961)
* feat: fix star alignment overflow issue

* feat: changeset fix
2026-01-30 10:35:05 -08:00
CandiedUniverse ea66c6c584 Correct omega to giga in Giga Potato (#8967)
* Correct omega to giga in Giga Potato

* Update patch version

* Fix model picker links
2026-01-29 20:55:48 -08:00
Bee be7f349693 fix: storage migration & extension lifecycle events (#8957)
* fix: storage migration & extension lifecycle events

- Move distinctId initialization before StateManager to ensure logging is ready
- Add ClineTempManager periodic cleanup on startup for temp file management
- Remove state migration calls from initialization (migrations already completed)
- Consolidate cleanup operations in tearDown: hook processes, discovery cache, temp manager, and test mode
- Remove unused migration imports from common.ts
- Add new service imports for cleanup operations (HookDiscoveryCache, HookProcessRegistry, ClineTempManager, TestMode)

This refactoring improves the extension lifecycle by ensuring proper initialization order, removing obsolete migration code, and adding comprehensive cleanup to prevent resource leaks and zombie processes.

* clean up

* add doc string

* doc
2026-01-30 12:05:30 +08:00
CandiedUniverse 9c03dfa717 Correct the version number in package.json and package-lock.json (#8966) 2026-01-29 19:32:51 -08:00
github-actions[bot] c5601d8d7d Changeset version bump (#8903)
* changeset version bump

* Updating CHANGELOG.md format

* Add banner updates for release

* Update changelog for release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
2026-01-29 19:21:01 -08:00
Ara a281aea0e7 feat(models): increase context window for stealth/giga-potato model (#8965)
Update contextWindow from 128K to 224K tokens for the stealth/giga-potato
model to reflect updated model capabilities.
2026-01-29 18:58:20 -08:00
Ara e8bc6b9794 refactor: new FeatureSettingsSection UI (#8931)
* feat: enable experimental features by default and update settings UI

- Change ts-proto env from 'node' to 'both' for browser compatibility
- Enable multiRootEnabled, enableParallelToolCalling, and skillsEnabled by default
- Disable strictPlanModeEnabled by default
- Add @radix-ui/react-collapsible and @radix-ui/react-slider dependencies
- Remove experimental feature toggles from settings UI for cleaner interface

* Fixing wording

* Fixing wording

* Fixing wording

* Fixing wording

* Fixing wording

* fix: properly handle yolo mode UI when remotely locked

- Use remote config value for yolo state instead of forcing false
- Disable the yolo toggle when locked by remote configuration
- Add visual indicator and tooltip explaining organization management

* Fixing wording

* Fixing wording

* Fixing wording

* Fixing wording
2026-01-29 16:07:13 -08:00
Yuri Chukhlib e018199fef Fix: LiteLLM thinking configuration not showing for models (#8342) (#8592)
* Fix: LiteLLM thinking configuration not showing for models (#8342)

* fix: add supportsReasoning to LiteLLM proto serialization

The model ID key fix alone wasn't sufficient - supportsReasoning was
being lost during the proto serialization cycle when saving/loading
model info. This adds the field to all relevant conversion functions.

---------

Co-authored-by: ClineXDiego <diego@cline.bot>
Co-authored-by: Robin Newhouse <robin@cline.bot>
2026-01-29 16:05:12 -08:00
Mariam Jabara 35ecf3e551 feat(prompts): Add Trinity Large variant for better tool-calling support (#8952)
* Add Trinity model variant with prompt optimizations

* test: Add Trinity model to snapshot test cases

* adding changeset
2026-01-29 15:53:59 -08:00
Ara ee028033c7 feat: add stealth/giga-potato test model to Cline (#8956)
* feat: add stealth/giga-potato test model to OpenRouter

Add a new stealth model "stealth/giga-potato" for testing purposes:
- Define model info in CLINE_STEALTH_MODELS with 128k context window
- Add to freeModels list in OpenRouterModelPicker for UI display
- Model supports images and prompt caching with zero pricing

* Fixing wording
2026-01-29 14:47:13 -08:00
Robin Newhouse 7adfcabfa0 feat(cli): add Vercel AI Gateway + Cline API key auth (#8917)
Add two new CLI auth providers for headless setups and map their
configuration fields. Fix auth menu/provider status to use the
workspace-backed auth instance so the configured provider displays
correctly.
2026-01-29 12:34:04 -08:00
Tomás Barreiro 51201b00be Add custom Metrics and Logs endpoints headers (#8937) 2026-01-29 11:36:00 -08:00
Tomás Barreiro 0d43d014eb Update package lock (#8941)
* Update package lock

* update package-lock
2026-01-29 08:06:32 -08:00
Tomás Barreiro df7d5062bc Fix OTEL issues (#8932) 2026-01-29 05:12:01 +01:00
Juan Pablo Flores 98aec6c9d3 feat(moonshot): update temperature setting and add new model configuration for kimi-k2.5 (#8925) 2026-01-28 16:58:33 -08:00
Tomás Barreiro 5a65b12a43 Add the Cline User Agent to all inference providers (#8872)
* Add the Cline User Agent to all inference providers

* Pass all options when using `createOpenAIClient`

* Revert async changes

* Fix other changes
2026-01-28 15:01:55 -08:00
CandiedUniverse 741bea5af8 feat(hooks): Run hooks from cwd of the workspace repo root. [CLINE-1212] (#8913)
* feat(hooks): Run hooks from cwd of the workspace repo root.

* feat(hooks): npm run changeset

* feat(hooks): Make hooks execute in their respective repo's root dir.

* feat(hooks): Improvements as per Cline's code review feedback.
2026-01-28 13:19:06 -08:00
Ara ad1e35a425 Revert "chore: extract storage migrations to extension layer (#8843)" (#8922)
This reverts commit 2e2239b138.
2026-01-28 13:09:28 -08:00
Bee 2e2239b138 chore: extract storage migrations to extension layer (#8843)
* chore: extract storage migrations to extension layer

Extracts VS Code specific storage migrations from common initialization into a dedicated function. This isolates the logic to the extension layer, making it clear that these steps are not applicable to other clients.

* invoke performStorageMigrations in vs code activation event

* fix check
2026-01-28 10:55:12 -08:00
Jose R. Perez 03aac0cd80 feat: add social icons to new version modal (#8898)
* feat:  add social icons to new version modal

* feat: change set

* feat: missing file
2026-01-28 10:53:43 -08:00
Saoud Rizwan 22cc73f614 chore: add *.tsbuildinfo to .gitignore 2026-01-28 10:01:25 -08:00
github-actions[bot] 06b05ddfe9 Changeset version bump (#8895)
* changeset version bump

* Updating CHANGELOG.md format

* release(3.55.0): Version bump and update WhatsNewModal

* feat(settings): Support linking to recommended or free model picker.

* Send to cline provider

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-27 19:02:59 -08:00
Jose R. Perez 2670a4a171 feat: updated welcome card content and added ability to close each card (#8900)
* feat: updated welcome card content and added ability to close each card

* feat: change set

* feat: fix
2026-01-27 18:35:18 -08:00
Renee Huang 1699c9a63a wording for Codex login (#8835) 2026-01-27 18:29:34 -08:00
Tomás Barreiro 808dd42ae9 Lock the LiteLLM api key input when it's remotely configured (#8899) 2026-01-28 02:34:06 +01:00
Ara 71af56f493 feat: add Arcee AI Trinity Large Preview to free models (#8897)
Add arcee-ai/trinity-large-preview:free as a new free model option:
- Add to onboarding models with 131k context window and score of 88
- Include in OpenRouterModelPicker free models list
- Update filter to preserve Trinity Large models like Minimax models
2026-01-27 15:51:39 -08:00
Juan Pablo Flores 1167b4f3a6 feat(rules): Conditional rules docs (#8874)
* docs(rules): Initial thoughts on docs for conditional rules.

* docs: restructure Cline Rules documentation into nested structure

Reorganize Cline Rules documentation by:
- Creating a "Cline Rules" group with overview and conditional-rules pages
- Moving conditional-rules.mdx into features/cline-rules/ subdirectory
- Adding URL redirects for backward compatibility
- Streamlining conditional-rules content for clarity and conciseness
- Adding cross-reference link to the overview page

This improves documentation navigation by grouping related rule concepts together and makes the content more accessible with clearer, more concise explanations.

* docs(cline-rules): consolidate rule file format documentation

Reorganize and expand the documentation for supported rule file formats:

- Add new "Supported Rule Files" section with comprehensive table
- Document cross-tool compatibility (Cursor, Windsurf, AGENTS.md)
- Clarify file priority and loading behavior
- Remove separate AGENTS.md section and integrate into unified table

This improves discoverability by showing all supported formats in one
place and makes it clearer how Cline works with rules from different AI
coding tools.

* docs(rules): remove context management note from overview

---------

Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
2026-01-27 14:17:29 -08:00
WangXiaolong e8d6370b0c feat(deepseek): add native tool calling support and reasoning_content handling (#7888)
* feat(deepseek): add native tool calling support and reasoning_content passback

- Add DeepSeek to isNextGenModelProvider list to enable native tool calling
- Add isDeepSeekModelFamily function for model identification
- Add addReasoningContent function for DeepSeek Reasoner's reasoning_content field
  - Pass back reasoning_content during tool calling within the same turn
  - Clear reasoning_content when starting a new conversation turn
- Compliant with DeepSeek API documentation for thinking mode with tool calling

* Update src/core/api/transform/r1-format.ts

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

* Update comments for user message handling logic

Clarify reasoning for handling user messages in comments.

* Update src/core/api/transform/r1-format.ts

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

* fix: format code for consistency in isNextGenModelFamily function

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
2026-01-27 13:24:20 -08:00
Toby White e243376a39 feat: add MCP prompts support (#8066)
* feat: add MCP prompts support

Implement support for MCP prompts as defined in the MCP spec (2025-06-18):
- Add McpPrompt and McpPromptArgument types to shared types
- Update proto definitions with prompt messages
- Update McpHub to fetch prompts list and get individual prompts
- Add prompts to system prompt component for AI awareness
- Add McpPromptRow UI component for displaying prompts
- Update ServerRow with Prompts tab showing available prompts
- Add slash command integration (/mcp:<server>:<prompt>)
- Update regex patterns to support colons in command names

MCP prompts are user-controlled templates that can be invoked via
slash commands to inject contextual messages into the conversation.

* style: alphabetize imports in mcp-server-conversion.ts

Reorder imports to follow project convention of alphabetical ordering.

* feat: add MCP prompts to slash command autocomplete

Wire up mcpServers to SlashCommandMenu so MCP prompt commands appear
in the autocomplete dropdown with their own "MCP Prompts" section.

* test: add unit tests for MCP prompt slash commands

- Add webview slash-commands.test.ts testing getMcpPromptCommands,
  getMatchingSlashCommands, and validateSlashCommand with MCP servers
- Add backend slash-commands tests for formatMcpPromptResponse and
  parseSlashCommands MCP handling
- Export formatMcpPromptResponse for testability
- Add "mcp_prompt" to telemetry captureSlashCommandUsed types

* test: update snapshots and fix backend tests for MCP prompts

- Update system prompt snapshots to include MCP prompts section
- Remove backend tests requiring StateManager initialization
  (tests for unknown server, no fetcher, fetcher errors)
- Core MCP prompt functionality is covered by remaining tests

* fix: change test status to valid 'connecting' value

* chore: remove commented debug line from prompts fetching

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use Logger instead of console.error for lint compliance

* fix: wire up mcpPromptFetcher callback to parseSlashCommands

The MCP prompt slash commands were not working because the
mcpPromptFetcher callback was never passed to parseSlashCommands.
This adds the callback that wraps mcpHub.getPrompt() to actually
fetch and inject prompt content when using /mcp:server:prompt.

* fix: resolve MCP prompts keyboard navigation and edge cases

- Add mcpServers param to keyboard handler's getMatchingSlashCommands calls
  to fix arrow key navigation and Enter/Tab selection for MCP prompts
- Add null check for connection.client in McpHub.getPrompt()
- Add debug logging when MCP prompt fetch returns null
- Fix regex in shouldShowSlashCommandsMenu to include colons for MCP format

* chore: add changeset for MCP prompts feature

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robin Newhouse <robin@cline.bot>
2026-01-27 13:12:02 -08:00
Tomás Barreiro 4f1be9d512 Replace POSTHOG_TELEMETRY_ENABLED with CLINE_TELEMETRY_DISABLED (#8818)
* Replace `POSTHOG_TELEMETRY_ENABLED` with `CLINE_TELEMETRY_DISABLED`

* Update cli/pkg/hostbridge/env.go

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-27 18:17:06 +01:00
Ara 94d36ce719 feat(ui): reduce font size for thinking content (#8892)
Add text-xs class to reasoning content in ThinkingRow component
for improved visual hierarchy and readability.
2026-01-27 08:28:39 -08:00
Bee 5df7498f03 refactor: simplify ThinkingRow expansion state management (#8735)
* refactor: simplify ThinkingRow expansion state management

Remove the responseStarted prop and complex logic that conditionally controlled ThinkingRow visibility during streaming. Simplify to allow ThinkingRow to remain expandable throughout the entire streaming lifecycle instead of forcing it expanded during reasoning and then collapsing after response starts.

Changes:
- Remove ApiReqState type and responseStarted tracking
- Eliminate showStreamingThinking and showCollapsedThinking logic
- Use consistent isExpanded state based only on user toggle
- Always show ThinkingRow title

* remove unused responseStarted

* feat(ui): update thinking UI with improved expand/collapse controls

Changes:
- Replace "Thinking..." with "Working..." status text in non-plan mode
- Switch from ChevronRight to ChevronUp/Down icons for better UX
- Redesign thinking section header with cleaner layout
- Remove preview text when collapsed, show only "Thinking" label
- Add consistent border styling to thinking content
- Implement per-tool thinking expand/collapse state management
- Update icon sizing and styling for better visual consistency

This improves the user experience by making the thinking/reasoning sections more intuitive to expand and collapse, with clearer visual indicators and a more polished appearance.

* add blur

* feat: chevron fix, reasoning change, slight style change

* feat: spacing issues

* keep thinking row expanded during stream

* Reasoning -> Thoughts

* feat: Inline reading of files vs having reading then read list items seperately

* feat: remove extra reading state

* feat: removed reasoning from file expandable file state

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2026-01-27 07:53:03 -08:00
Juan Pablo Flores c2b87252ac Fixes Cannot restore checkpoint more than once #8866 (#8873) 2026-01-27 07:46:43 -08:00
github-actions[bot] be353bb3da v3.54.0 Release Notes (#8840)
- Native tool calls support for Ollama provider
- Sonnet 4.5 is now the default Amazon Bedrock model id

- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.

- Removed Devstral-2512 free from the free models list
- Removed deprecated zai-glm-4.6 model from Cerebras provider

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-27 07:44:49 -08:00
Ara 0a7791de1f feat: remove Mistral Devstral 2512 from free models list (#8889)
Remove mistralai/devstral-2512:free from:
- Onboarding models configuration
- Free models picker in settings
- OpenRouter model filter exception list

The Devstral model is no longer included as a free tier option.
2026-01-27 07:16:55 -08:00
Robin Newhouse 7cca102e14 fix: apply_patch tool now works with OCA provider's gpt5 model ID (#8875) 2026-01-26 22:54:21 -08:00
Bee 4f591de6a9 feat: Adds support for native tool calls to Ollama provider (#8871)
* feat: add support for tool calls in Ollama API

Enhanced OllamaHandler to support tool calls by adding a 'tools' parameter to createMessage. Implements processing of tool call deltas using ToolCallProcessor, enabling handling of function calls made by the model. Added necessary imports for ChatCompletionTool and ToolCallProcessor types.

* add changeset
2026-01-26 17:50:52 -08:00
Tomás Barreiro 60436b3ddf Do not call feature_flag_called event if the value hasn't changed (#8867)
* Do not call feature_flag_called event if the value hasn't changed

* Send the feature flag called on startup
2026-01-26 10:51:33 -08:00
Saoud Rizwan df5954052d feat: disable extended thinking by default (#8863) 2026-01-26 10:21:21 -08:00
Igor Tceglevskii a66a2784c3 feat: disable PostHog services in self-hosted mode (#8842)
- Skip PostHog client initialization when running in self-hosted mode
- Return no-op config from ErrorProviderFactory and FeatureFlagsProviderFactory
- Add comprehensive tests for self-hosted mode PostHog disabling behavior

This ensures no telemetry or analytics data is sent when users run
the extension in a self-hosted environment.
2026-01-26 07:51:02 -08:00
Bee 47031cea25 feat: add debugLog RPC for host bridge logging (#8841)
* feat: add appendOutputLog RPC for host bridge logging

Add new appendOutputLog RPC endpoint to EnvService proto definition
and refactor VSCode output channel creation to use a dedicated factory
function. This enables structured logging through the host bridge
service instead of direct Logger calls.

* rename appendOutputLog to debugLog and add subscriber pattern

- Rename `appendOutputLog` RPC to `debugLog` with documentation
- Refactor Logger to use subscriber pattern instead of single output
- Update HostProvider to use env.debugLog directly for logging
- Remove redundant logger callback from setupHostProvider

* feat: add multi-subscriber support for Logger output

- Rename Logger.setOutput to Logger.subscribe to better reflect behavior
- Subscribe both output channel and debug logger to receive log messages
- Enable logging to multiple destinations simultaneously

* update mock
2026-01-23 18:18:24 -08:00
Robin Newhouse e29740479e fix: skip diff error UI handling during streaming to prevent flickering (#8788)
* fix: skip diff error UI handling during streaming to prevent flickering

During streaming, handlePartialBlock is called repeatedly, and if the diff
application fails (e.g., search string not found), all the error handling code
was running on every chunk. This caused:
- consecutiveMistakeCount to rapidly increment
- diff_error messages to be added/removed repeatedly
- revertChanges/reset to be called repeatedly
- rapid flickering of the diff viewer

Now we return early from the catch block when block.partial is true, skipping
all error UI handling. The error is only processed once on the final block.

* chore: add changeset for diff error suppression

* test: add unit tests for partial block streaming behavior

Adds tests verifying that error handling is skipped during streaming
(block.partial=true) to prevent counter rapid increment and UI flickering.

* chore: remove unused errorPushedForCallIds tracking

This mechanism was replaced by the simpler block.partial check for
skipping error handling during streaming. Remove the dead code.
2026-01-23 16:55:32 -08:00
Ara 74f607ff8e chore(release): bump version to 3.53.1 (#8839)
- Fix bug in responses API
- Update changeset package name from "cline" to "claude-dev"
- Update version in package.json and package-lock.json
2026-01-23 15:46:59 -08:00
Robin Newhouse 0edf6d777b fix: prevent infinite retry loops when replace_in_file fails repeatedly (#8787)
* fix: prevent infinite retry loops when replace_in_file fails repeatedly

The consecutiveMistakeCount was being reset to 0 at the START of each
WriteToFileToolHandler execution, before the tooManyMistakes check could
see accumulated failures. This allowed the model to retry failing
replace_in_file operations indefinitely, causing context explosion.

Changes:
- Move counter reset from before operation to after successful saveChanges()
- Add consecutiveMistakeCount++ in the diff error catch block
- Fix typo: "his thought process" → "Cline's thought process"

* chore: add changeset for retry loop prevention

* test: add unit tests for consecutiveMistakeCount behavior

Verify the fix for infinite retry loops by testing that:
- Counter is NOT reset at the start of operations
- Counter IS reset only after successful saveChanges()
- Counter IS incremented on diff errors
- Repeated failures accumulate so tooManyMistakes can trigger
2026-01-23 15:37:01 -08:00
Robin Newhouse de630c64d4 fix: throttle diff view updates during streaming (#8785)
* fix: throttle diff view updates during streaming

Skip redundant rapid updates to reduce performance issues in large
streams (e.g., notebooks) and reset throttle state on cleanup.

* chore: add changeset for diff throttling fix

* test: add unit tests for diff view update throttling

Add comprehensive tests for the throttling behavior introduced in the
streaming diff updates fix. Tests cover empty content, unchanged content,
time-based throttling, final update bypass, and state reset.
2026-01-23 15:36:47 -08:00
Bee 3ff63562c8 chore: migrate host logging to shared Logger service (#8820)
* chore: migrate host logging to shared Logger service

- Replace HostProvider.logToChannel usage with Logger.log/error
  in controller, webview, and checkpoint migration code
- Remove redundant, low-value log statements from Cline API
  methods to reduce noise
- Centralize logging through shared Logger service for more
  consistent, structured logging and easier maintenance
- Remove redundant , low-value log statements from StateManager where
  we logged error that would be throw and get logged again

* Update src/integrations/checkpoints/CheckpointMigration.ts

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

* fix

* update tests

* update tests

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-23 15:27:11 -08:00
Saoud Rizwan 4d6f908fbd fix: add null check when filtering tools by type in Responses API providers (#8837)
Users reported seeing this error with the OpenAI Codex provider:
{"message":"Cannot read properties of undefined (reading 'type')","modelId":"gpt-5.2-codex"}

The issue occurs when filtering tools before sending to the Responses API.
The filter accessed .type without checking if the tool element was defined:

  tools.filter((tool) => tool.type === "function")

If the tools array contains any undefined elements, this throws. Fixed by
adding optional chaining:

  tools.filter((tool) => tool?.type === "function")

Applied the same fix to all three providers using the Responses API:
- openai-codex.ts (ChatGPT Plus/Pro subscriptions)
- openai-native.ts (OpenAI API with Responses format)
- oca.ts (OpenAI-compatible API with Responses format)
2026-01-23 14:48:51 -08:00
Igor Tceglevskii 6521fdcc94 disable telemetry for self-hosted environments (#8790) 2026-01-23 14:16:50 -08:00
Igor Tceglevskii 1393eace27 Endpoint configuration file (#8645) 2026-01-23 13:40:31 -08:00
github-actions[bot] eebb99c1e3 Changeset version bump (#8800)
* changeset version bump

* Updating CHANGELOG.md format

* update changelog and banner for release

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-23 13:12:29 -08:00
Ara 9ed44f7a83 feat(cerebras): use model-specific temperature configuration (#8833)
- Extract model retrieval to avoid multiple function calls
- Use temperature from model.info with fallback to 0 instead of hardcoded value
- Allows temperature to be configured per model rather than using a fixed value

This change enables more flexible temperature configuration for different Cerebras models while maintaining backward compatibility with a default value of 0.
2026-01-23 12:50:57 -08:00
Ara 6a95cc5f19 feat: add default temperature to Cerebras model configuration (#8832)
Set default temperature value of 0.9 for Cerebras model in the model
configuration. This establishes a consistent default sampling temperature
for the model's response generation behavior.
2026-01-23 12:42:26 -08:00
er-ri 1d0637f39c fix: add support for haiku 4.5 in JP_SUPPORTED_CRIS_MODELS and enable global endpoint support (#8298) 2026-01-23 11:46:02 -08:00
AJ Juaire f2c16bae7a Make the default bedrock model Sonnet 4.5 (#8830) 2026-01-23 11:45:49 -08:00
Ara 204f15ce1c Remove free period on grok (#8831) 2026-01-23 11:27:42 -08:00
Robin Newhouse 8118e11596 fix(extract-text): strip notebook outputs to reduce context size (#8784)
* fix(extract-text): strip notebook outputs to reduce context size

* chore: add changeset for notebook outputs fix
2026-01-22 17:50:31 -08:00
Bee f7b593df35 chore: remove noisy log when checking file outside workspace (#8814)
* chore: remove noisy log when checking file outside workspace

Removes a `Logger.error` call in `ifFileExistsRelativePath` that triggered whenever a file path was checked without an active workspace. This log was creating excessive noise during long conversations where many files were mentioned but no workspace was open.

* update test
2026-01-22 17:01:22 -08:00
Saoud Rizwan 2e0358a7a1 fix: disable browser tool by default (#8815)
The browser tool conflicts with the new websearch tool. Disabling it by
default provides a better out-of-box experience.
2026-01-22 16:58:29 -08:00
Bee 0fbc10f807 chore: remove unhelpful and noisy log statements - part 1 (#8813)
* chore: remove unhelpful and noisy log statements - part 1

Removes excessive debug and info logs across several services to reduce console noise, specifically:
- Deletes `[DEBUG]` logs for request registration, subscription setup/cleanup, and event dispatching in the gRPC controller and UI handlers.
- Removes verbose file cleanup logs in `ClineTempManager` and process termination logs in `AudioRecordingService`.
- Simplifies the success log in `refreshOpenRouterModels` by removing the large JSON payload dump.
- Upgrades the log level from `debug` to `error` for request cleanup failures in `GrpcRequestRegistry` to ensure exceptions are properly highlighted.

* removes subscription logs
2026-01-22 16:44:05 -08:00
Bee c093ca1760 refactor: replace console with Logger service (#8741)
* chore: add grit rule to enforce Logger service over console calls

Add a new Grit linting rule that detects direct console method usage
(log, debug, error, warn, info) and prompts developers to use the
Logger service instead for consistent logging practices.

The rule is configured in biome.jsonc to apply to most source files
while excluding test files, webview-ui, evals, standalone, e2e tests,
and scripts where direct console usage may be acceptable.

* support variadic args

* wip: migrate console to Logger

* migrate rest of console logger

* Switch to Logger

* Migrations

* shared

* use shared

* revert format change

* Update tests to stub Logger instead of console

* verbose in dev mode
2026-01-22 13:16:37 -08:00
Seb Duerr dc85299a73 Remove deprecated zai-glm-4.6 model from Cerebras provider (#8717) 2026-01-22 12:58:07 -08:00
ClineXDiego 4533ed3ea8 fix: support drag & drop files from SSH remote workspaces (#8804)
Add vscode-remote: scheme to the valid URI filter for drag & drop operations.
This allows files from SSH Remote workspaces to be dropped into the chat.

Fixes #7606
2026-01-22 12:37:11 -08:00
Max 66d7664d1b improve cline command permission parsing logic (#8544)
- cline command permission flag can now parse subshells correctly and
validate that subshells don't contain disallowed commands.

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-01-22 11:11:53 -08:00
dependabot[bot] 50aeb7098f chore(deps): bump undici (#8783)
Bumps [undici](https://github.com/nodejs/undici) to 6.23.0 and updates ancestor dependency . These dependencies need to be updated together.


Updates `undici` from 6.22.0 to 6.23.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.23.0)

Updates `undici` from 7.16.0 to 7.19.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.22.0...v6.23.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.23.0
  dependency-type: indirect
- dependency-name: undici
  dependency-version: 7.19.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ClineXDiego <diego@cline.bot>
2026-01-22 10:50:49 -08:00
github-actions[bot] c2e6eafb45 v3.52.0 Release Notes (#8633)
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
- Grok models are now moving out of free tier and into paid plans.
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.

- Bugs in DiffViewProvider for file editing
- Ollama's recommended models to use correct identifiers

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-22 10:48:32 -08:00
Juan Pablo Flores 0220628c39 Adds Open AI Codex docs (#8791)
* feat(docs): add OpenAI Codex provider setup instructions and update model selection guidance

* fix(docs): improve clarity and formatting in OpenAI Codex documentation

* Update docs/provider-config/openai-codex.mdx

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

* Update docs/provider-config/openai-codex.mdx

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tony Loehr <turingxo@gmail.com>
2026-01-22 09:30:12 -08:00
CandiedUniverse 5052220195 feat(hooks): Make hooks always enabled and remove its feature setting. [CLINE-1179] (#8777)
* feat(hooks): Standardize on calling getHooksEnabledSafe().

* feat(hooks): Hard-code getHookEnabledSafe() to return true unless on Windows.

* feat(hooks): Remove hooks setting from the CLI.

* feat(hooks): Remove hooks toggle from the Feature Settings UI.

* feat(hooks): Remove hooksEnabled toggles from settings/task APIs.

* feat(hooks): Stop using hooksEnabled setting.

* feat(hooks): npm run changeset

* feat(hooks): Simplify getHooksEnabledSafe() function signature.

* feat(hooks): Remove hooks setting migration.

feat(hooks): Remove hooksEnabled from updateSettingsCli() conversion.

feat(hooks): Use 'reserved' for removed fields in UpdateSettingsRequest protobuf.
2026-01-22 09:26:16 -08:00
CandiedUniverse abf3081e56 Rules: Wire up conditional rules functionality [ENG-1470] (#8669)
* feat(rules): Write technical design / implementation plan doc.

* update frontmatter plan

* feat(rules): Initial implementation based on plan doc.

* feat(rules): Add tool-call path harvesting for path-scoped Cline Rules.

* chore(rules): exclude internal paths-frontmatter plan doc from PR

* fix(rules): use latest user message for paths frontmatter context

* feat(rules): Implement conditional_rules_applied say type.

* feat(rules): changes as per Cline's code review feedback

* feat(rules): npm run changeset

* feat(rules): Changes as per ellipsis-dev feedback.

* feat(rules): Changes as per code review feedback (i.e. don't bloat the task context).

* feat(rules): Fix failing unit tests.
2026-01-22 09:25:59 -08:00
Michael Gutin 125cb78a31 Switch background element now has a 3:1 contrast ration with thumb and rule row background in vs code light and dark themes (#8747) 2026-01-22 12:16:51 -05:00
CandiedUniverse 17539228ea feat(cli): Add note about ctrl+c to exit the CLI. [CLINE-1162] (#8796)
* feat(cli): Add note about ctrl+c to exit the CLI.

* feat(cli): npm run changeset
2026-01-22 09:01:57 -08:00
Ara bdb7cc36fa feat(DiffEditRow): add button to open file in editor (#8564)
* refactor(diff): return result object with line tracking metadata

Change constructNewFileContent to return an object containing newContent
and line number information instead of just the string content. Add
charIndexToLineNumber helper function to support tracking where changes
occur in the file.

Update all callers and tests to access the newContent property from
the result object.

* minor fix

* fix: hide line numbers when not available from backend

* chore: add changeset

* feat: add startLineNumbers support to ApplyPatchHandler

* fix: split V4A @@ chunks into separate Patch objects for proper line numbers

* fix(DiffEditRow): preserve +/- prefix in diff line display for backwards compatibility

* fix line numbers
2026-01-21 21:38:45 -08:00
Tony Loehr d3e1065707 added jupyter docs (#8742)
* added jupyter docs

* fix jupyter docs and add gifs

* fix jupyter docs 2

* fix jupyter gif placement
2026-01-21 16:15:26 -08:00
839 changed files with 390266 additions and 38359 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add claude 4.5 haiku
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Replace the LiteLLM model list with a selector
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: finalize document content during approval flow
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Add GitHub Actions workflow to build CLI from any commit for testing
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
fix(cli): prevent hang when spawned without TTY
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Add Claude Opus 4.6 model support
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Expose --version in cline cli command
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Supports rendering markdown table in chat view.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: prevent duplicate diff errors when parallel tool calling is enabled
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Log Persistence errors to PostHog
-13
View File
@@ -1,13 +0,0 @@
---
"claude-dev": patch
---
feat: add OpenAI Codex (ChatGPT Plus/Pro) provider
Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
Models available:
- gpt-5.2-codex (default)
- gpt-5.1-codex-max
- gpt-5.1-codex-mini
- gpt-5.2
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Fix CLI crashing in CI environments and with stdin redirection (e.g., `cline "prompt" < /dev/null`). Now checks both stdin and stdout TTY status before using Ink, and only errors on empty stdin when no prompt is provided.
@@ -1,9 +0,0 @@
---
"claude-dev": patch
---
Fix two bugs in DiffViewProvider file editing:
1. **Line boundary validation**: Add `safelyTruncateDocument()` to prevent out-of-bounds line errors on JetBrains hosts (fixes #8423, #8429). The gRPC protocol strictly validates line numbers, causing "truncateDocument INTERNAL: Wrong line" errors when `truncateDocument()` was called with a line number >= document line count.
2. **Content concatenation on final update**: When replacing content without a trailing newline, the old content at line N+1 was concatenated to the new content. Fixed by extending the replacement range to cover the entire document on final update.
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix JetBrains sign-in regression by adding fallback for openExternal RPC
-9
View File
@@ -1,9 +0,0 @@
---
"claude-dev": patch
---
docs: fix outdated Ollama model names in documentation
Updated recommended Ollama models to use correct identifiers:
- Changed qwen3-coder-30b to qwen2.5-coder:32b
- Changed devstral-small to codellama:34b-code
+7
View File
@@ -0,0 +1,7 @@
---
"cline": patch
---
fix: use vscode.env.openExternal for auth in remote environments
Fixes OAuth authentication in VS Code Server and remote environments by routing browser URL opening through VS Code's native openExternal API instead of the npm 'open' package.
@@ -0,0 +1,7 @@
---
"cline": patch
---
fix: use vscode.env.asExternalUri for auth callback URLs only in VS Code Web
Fixes OAuth callback redirect in VS Code Web (`code serve-web`) environments by using `vscode.env.asExternalUri()` to resolve the callback URI. This is gated behind a `vscode.env.uiKind === UIKind.Web` check so regular desktop VS Code continues to use the `vscode://` URI directly, avoiding unintended transformations from `asExternalUri`.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
OpenAI GPT-5 Codex models are now using Apply Patch tool for diff edits.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix issue where tool call ids are invalid when switching between models using the chat completion format and the responses api format.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Updating script documentation and removing unnecessary continue on error
-10
View File
@@ -1,10 +0,0 @@
---
"claude-dev": patch
---
fix: improve Jupyter notebook diff view and reduce LLM context for notebook edits
- Restore switchToSpecializedEditor() for Jupyter notebook diff views that was accidentally removed during rebase
- Open .ipynb files in Jupyter notebook editor after save instead of leaving stale diff view
- Strip notebook outputs from content sent to LLM, reducing context by 95% (196KB → 9KB)
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: add chat output on skill use
+5
View File
@@ -0,0 +1,5 @@
---
"cline": patch
---
Fix Bedrock model id
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Adding telemetry for background exec terminal
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
This pull request introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness. The feature allows users to seamlessly work with Jupyter notebooks using Cline's AI capabilities while preserving the notebook's JSON structure.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: removes retry message from UI after retry succeeds
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Support native tool calling for LM Studio and Ollama provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fixing integration tests from testing framework
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
add claude 4.5 opus into sap provider.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Limite Vertex and LiteLLM options when they're remote configured
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix crash when the Context Menu has a type but no options
+5
View File
@@ -0,0 +1,5 @@
---
"cline": minor
---
Add Generate API Key on Hicap Provider selection
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Throttle the remote config fetch
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Improve history view filter menu
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add git worktree management UI for running parallel Cline sessions
+33
View File
@@ -0,0 +1,33 @@
# CLI Development
The CLI lives in `cli/` and uses React Ink for terminal UI.
- If needed, look at `cli/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
- Never use `dimColor` with gray (e.g. `<Text color="gray" dimColor>`) - it's too hard to read. Use `color="gray"` for secondary text and normal foreground (no color) for primary text.
- When thinking about how to handle state or messages from core, look at webview for how it communicates with the vs code extension.
- When updating the webview, consider and suggest to the user to update the CLI TUI since we want to provide a similar experience to our terminal users as we do our vs code extension users.
## Adding New API Providers
When adding a new API provider to the extension, you must also update the CLI:
1. **Update `cli/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
```typescript
import { newProviderDefaultModelId, newProviderModels } from "@/shared/api"
export const providerModels = {
// ...existing providers
"new-provider": { models: newProviderModels, defaultId: newProviderDefaultModelId },
}
```
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli/src/utils/provider-config.ts`:
```typescript
import { applyProviderConfig } from "../utils/provider-config"
// After successful auth:
await applyProviderConfig({ providerId: "new-provider", controller })
```
This handles setting provider, default model, API key mapping, state persistence, and rebuilding the API handler.
3. **Provider-specific auth**: If the provider uses OAuth (like `openai-codex`), add handling in `SettingsPanelContent.tsx`'s `handleProviderSelect` callback. See the existing Codex OAuth flow as a reference.
-7
View File
@@ -16,13 +16,6 @@
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
# ============================================================================
# TELEMETRY PROVIDER CONTROL
# ============================================================================
# Control which telemetry providers are active
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
# Set to false to disable Telemetry completely
# ============================================================================
# OPENTELEMETRY (Optional - for advanced telemetry)
# ============================================================================
+2 -3
View File
@@ -1,3 +1,2 @@
/docs/
/.github/ @saoudrizwan @garoth @sjf
/README.md @saoudrizwan @nickbaumann98
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
/README.md @saoudrizwan @juanpflores
+19 -1
View File
@@ -44,6 +44,12 @@ jobs:
with:
fetch-depth: 0
- name: Print HEAD commit
run: |
echo "HEAD is at: $(git rev-parse HEAD)"
echo "Short: $(git rev-parse --short HEAD)"
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
- name: Get PR number
id: pr
run: |
@@ -229,7 +235,13 @@ jobs:
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
Start with a warm thank you for their contribution. Be conversational, not robotic.
Start by noting the commit hash you reviewed:
```bash
git rev-parse --short HEAD
```
Include this at the top of your comment: "Reviewed at commit: <short hash>"
Then thank them for their contribution. Be conversational, not robotic.
Include what's relevant:
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author's intent, why they made the changes, how they implemented it, and what files/systems are affected. Don't just summarize - explain.
@@ -237,6 +249,12 @@ jobs:
- Your review findings (issues to address, suggestions, etc.)
- Clear next steps for the author
At the very bottom of your comment, append this exact footer:
```text
---
Generated by Claude PR Review
```
Include a "For Maintainers" section with:
- Anything else useful to help the maintainer resolve this PR
- Related issues/PRs with context on why they're relevant
+21 -2
View File
@@ -48,6 +48,12 @@ jobs:
with:
fetch-depth: 0
- name: Print HEAD commit
run: |
echo "HEAD is at: $(git rev-parse HEAD)"
echo "Short: $(git rev-parse --short HEAD)"
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -56,7 +62,8 @@ jobs:
- name: Install and Verify Cline CLI
run: |
npx cline version # verify installation
npm install -g cline
cline version # verify installation
- name: Configure Cline with Anthropic
run: |
@@ -269,7 +276,13 @@ jobs:
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
Start with a warm thank you for their contribution. Be conversational, not robotic.
Start by noting the commit hash you reviewed:
```bash
git rev-parse --short HEAD
```
Include this at the top of your comment: "Reviewed at commit: <short hash>"
Then thank them for their contribution. Be conversational, not robotic.
Include what'\''s relevant:
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
@@ -277,6 +290,12 @@ jobs:
- Your review findings (issues to address, suggestions, etc.)
- Clear next steps for the author
At the very bottom of your comment, append this exact footer:
```text
---
Generated by Cline PR Code Review
```
Include a '\''For Maintainers'\'' section with:
- Anything else useful to help the maintainer resolve this PR
- Related issues/PRs with context on why they'\''re relevant
+5 -2
View File
@@ -80,13 +80,16 @@ jobs:
playwright-browsers-${{ runner.os }}-
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install vsce
run: npm install -g @vscode/vsce
- name: Install xvfb on Linux
if: matrix.runner == 'ubuntu'
run: sudo apt-get update && sudo apt-get install -y xvfb
+21 -48
View File
@@ -9,9 +9,9 @@ on:
type: string
permissions:
contents: read
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
contents: write # Required for pushing tags
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
@@ -30,14 +30,8 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: cli/go.sum
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
@@ -47,45 +41,21 @@ jobs:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
- name: Generate Protos
run: npm run protos
- name: Read release version
id: version
run: |
# Read version from cli/package.json (stable version)
# Read version from cli/package.json
VERSION=$(node -p "require('./cli/package.json').version")
echo "Release version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Download ripgrep binaries
run: npm run download-ripgrep
- name: Clean previous builds
run: rm -rf dist-standalone
- name: Generate Protos (First Pass)
run: npm run protos && npm run protos-go
- name: Compile CLI
run: npm run compile-cli
- name: Compile CLI for all platforms
run: npm run compile-cli-all-platforms
- name: Build standalone NPM package
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
@@ -97,20 +67,16 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: npm run compile-standalone-npm
- name: Generate Protos (Second Pass - Bug Workaround)
run: npm run protos && npm run protos-go
run: node scripts/package-npm.mjs
- name: Verify build output
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
@@ -122,6 +88,13 @@ jobs:
cd dist-standalone
npm publish --tag latest --access public
- name: Tag release
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v${{ steps.version.outputs.version }}-cli"
git push origin "v${{ steps.version.outputs.version }}-cli"
- name: Summary
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
+19 -60
View File
@@ -2,13 +2,13 @@ name: Publish NPM Nightly
on:
schedule:
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
permissions:
contents: read
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
@@ -39,15 +39,8 @@ jobs:
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Setup Go
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: cli/go.sum
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
@@ -58,36 +51,27 @@ jobs:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Install root dependencies
if: steps.check_commits.outputs.skip != 'true' && steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install webview-ui dependencies
if: steps.check_commits.outputs.skip != 'true' && steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
- name: Generate Protos
if: steps.check_commits.outputs.skip != 'true'
run: npm run protos
- name: Generate nightly version with timestamp
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
# Read base version from cli/package.json (e.g., "1.0.9")
# Read base version from cli/package.json (e.g., "2.0.0")
BASE_VERSION=$(node -p "require('./cli/package.json').version")
# Generate timestamp (Unix epoch seconds)
TIMESTAMP=$(date +%s)
# Create unique nightly version: 1.0.9-nightly.1736365200
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: $BASE_VERSION"
echo "Generated nightly version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
@@ -102,31 +86,11 @@ jobs:
pkg.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
"
echo "Using version ${{ steps.version.outputs.version }} for build"
cat cli/package.json | grep '"version"'
- name: Download ripgrep binaries
if: steps.check_commits.outputs.skip != 'true'
run: npm run download-ripgrep
- name: Clean previous builds
if: steps.check_commits.outputs.skip != 'true'
run: rm -rf dist-standalone
- name: Generate Protos (First Pass)
if: steps.check_commits.outputs.skip != 'true'
run: npm run protos && npm run protos-go
- name: Compile CLI
if: steps.check_commits.outputs.skip != 'true'
run: npm run compile-cli
- name: Compile CLI for all platforms
if: steps.check_commits.outputs.skip != 'true'
run: npm run compile-cli-all-platforms
- name: Build standalone NPM package
- name: Build and package CLI
if: steps.check_commits.outputs.skip != 'true'
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
@@ -138,22 +102,17 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: npm run compile-standalone-npm
- name: Generate Protos (Second Pass - Bug Workaround)
if: steps.check_commits.outputs.skip != 'true'
run: npm run protos && npm run protos-go
run: node scripts/package-npm.mjs
- name: Verify build output
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
+215
View File
@@ -0,0 +1,215 @@
# Build and Pack CLI
#
# Builds a CLI tarball from any branch/commit and publishes it as a GitHub Release.
# Requires write access to the repository (maintainers/collaborators only).
#
# Security: Split into two jobs to isolate untrusted build code from write tokens.
# The build job runs arbitrary ref code with zero permissions. The release job
# only runs trusted GitHub Actions with write scope.
#
# Usage (helper script, auto-detects current branch):
# ./scripts/build-cli-artifact.sh
# ./scripts/build-cli-artifact.sh feature/my-changes
# ./scripts/build-cli-artifact.sh feature/my-changes 1234 # comments on PR
#
# Usage (gh CLI directly):
# gh workflow run pack-cli.yml -f ref=main
# gh workflow run pack-cli.yml -f ref=abc123 -f pr_number=1234
#
# Install the built CLI (no auth required):
# npm install -g https://github.com/cline/cline/releases/download/cli-build-<sha>/cline-<ver>.tgz
#
# Find releases:
# gh release list --limit 10
name: Build and Pack CLI
permissions:
contents: read
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch, tag, or commit SHA to build (leave empty for default branch)'
required: false
type: string
pr_number:
description: 'PR number to comment on with install instructions (optional)'
required: false
type: number
jobs:
# ── Build job: runs untrusted ref code with ZERO permissions ──
build:
name: Build CLI
runs-on: ubuntu-latest
permissions: {}
outputs:
commit_sha: ${{ steps.commit.outputs.sha }}
tarball: ${{ steps.pack.outputs.tarball }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Get commit SHA
id: commit
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "Building from commit: $COMMIT_SHA"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install dependencies
run: npm ci --include=optional
- name: Generate Protos
run: npm run protos
- name: Build standalone package
run: node scripts/package-npm.mjs
- name: Create Tarball
id: pack
run: |
cd dist-standalone
TARBALL=$(npm pack)
echo "tarball=$TARBALL" >> $GITHUB_OUTPUT
echo "Created tarball: $TARBALL"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cli-tarball
path: dist-standalone/*.tgz
# ── Release job: only trusted Actions code, with write permissions ──
release:
name: Release CLI
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: cli-tarball
path: dist-standalone
- name: Create GitHub Release
id: create_release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const commit = '${{ needs.build.outputs.commit_sha }}';
const tarball = '${{ needs.build.outputs.tarball }}';
// Delete existing release/tag if re-running for the same commit
const tagName = `cli-build-${commit}`;
try {
const existing = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tagName
});
await github.rest.repos.deleteRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: existing.data.id
});
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${tagName}`
});
core.info(`Deleted existing release for ${tagName}`);
} catch (e) {
// Release doesn't exist yet, that's fine
}
// Create a release
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `CLI Build (${commit})`,
body: `Automated CLI build from commit ${commit}\n\nInstall with:\n\`\`\`bash\nnpm install -g https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}\n\`\`\``,
draft: false,
prerelease: true
});
// Upload the tarball as a release asset
const tarballPath = path.join('dist-standalone', tarball);
const tarballData = fs.readFileSync(tarballPath);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.data.id,
name: tarball,
data: tarballData
});
const downloadUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}`;
core.setOutput('release_url', release.data.html_url);
core.setOutput('download_url', downloadUrl);
- name: Comment on PR with download instructions
if: inputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const commit = '${{ needs.build.outputs.commit_sha }}';
const releaseUrl = '${{ steps.create_release.outputs.release_url }}';
const downloadUrl = '${{ steps.create_release.outputs.download_url }}';
const prNumber = ${{ inputs.pr_number || 0 }};
if (!prNumber) return;
const comment = `## 📦 CLI Build Ready
A CLI build has been created for commit \`${commit}\`.
### Install Directly from URL (No Authentication Required!)
\`\`\`bash
npm install -g ${downloadUrl}
\`\`\`
### Alternative: Download and Install
\`\`\`bash
curl -L ${downloadUrl} -o cline.tgz
npm install -g ./cline.tgz
\`\`\`
📦 [View Release](${releaseUrl})
`;
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Summary
run: |
echo "✅ CLI build complete!"
echo ""
echo "📦 Release: ${{ steps.create_release.outputs.release_url }}"
echo "🔗 Download URL: ${{ steps.create_release.outputs.download_url }}"
echo ""
echo "Install from anywhere (no authentication required):"
echo " npm install -g ${{ steps.create_release.outputs.download_url }}"
-2
View File
@@ -55,11 +55,9 @@ jobs:
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
- name: Install Publishing Tools
+25 -63
View File
@@ -28,27 +28,17 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Run Quality Checks (Parallel)
@@ -73,27 +63,17 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Set up NPM on Windows
@@ -135,6 +115,11 @@ jobs:
cd webview-ui
npm run test:coverage
- name: CLI Tests
id: cli_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: cd cli && npm run test:run
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
# Only upload artifacts on Linux - We only need coverage from one OS
@@ -156,35 +141,18 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache testing-platform dependencies
- name: Cache testing-platform dependencies
uses: actions/cache@v4
id: testing-platform-cache
with:
path: testing-platform/node_modules
key: ${{ runner.os }}-npm-testing-platform-${{ hashFiles('testing-platform/package-lock.json') }}
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
testing-platform/package-lock.json
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Download ripgrep binaries
@@ -194,17 +162,11 @@ jobs:
run: npm run compile-standalone
- name: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
run: cd testing-platform && npm ci
- name: Running testing platform integration spec tests
continue-on-error: true
timeout-minutes: 7
# Temporarily wrapping the test command to always return a neutral exit code.
# This prevents the job from showing as failed and avoids distracting developers
# until the integration tests are ready to be enforced.
run: |
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
+2
View File
@@ -38,6 +38,7 @@ coverage-unit
src/generated/
src/shared/proto/
webview-ui/src/services/grpc-client.ts
*.tsbuildinfo
# E2E Tests
test-results
@@ -46,3 +47,4 @@ test-results
/pkg
.secrets
*.tsbuildinfo
+101 -2
View File
@@ -1,10 +1,109 @@
# Changelog
## [3.57.1]
### Fixed
- Fixed Opus 4.6 for bedrock provider
## [3.57.0]
### Added
- Cline CLI 2.0 now available. Install with `npm install -g cline`
- Anthopic Opus 4.6
- Minimax-2.1 and Kimi-k2.5 now available for free for a limited time promo
- Codex-5.3 through ChatGPT subscription
### Fixed
- Fix read file tool to support reading large files
- Fix decimal input crash in OpenAI Compatible price fields (#8129)
- Fix build complete handlers when updating the api config
- Fixed missing provider from list
- Fixed Favorite Icon / Star from getting clipped in the task history view
### Changed
- Make skills always enabled and remove feature toggle setting
## [3.56.0]
### Added
- __CLI authentication:__ Added Vercel AI Gateway and Cline API key provider support for headless CI/automation workflows
- __New model:__ Added Kimi-K2.5 model to Moonshot provider (262K context, image support, prompt caching)
- __Prompt variant:__ Added Trinity Large prompt variant for improved tool-calling support
- __OpenTelemetry:__ Added support for custom headers on metrics and logs endpoints
- __Social links:__ Added community icons (X, Discord, GitHub, Reddit, LinkedIn) to the What's New modal
### Fixed
- __LiteLLM:__ Fixed thinking configuration not appearing for reasoning-capable models
- __OpenTelemetry:__ Fixed endpoint path handling (no longer incorrectly appends `/v1/logs` or `/v1/metrics`) and ensured logs are sent regardless of VSCode telemetry settings
- __CLI auth:__ Fixed `cline auth` displaying incorrect provider information after configuration
### Changed
- __Hooks:__ Hook scripts now run from the workspace repository root instead of filesystem root
- __Default settings:__ Enabled multi-root workspaces, parallel tool calling, and skills by default; disabled strict plan mode by default
- __Settings UI:__ Refreshed feature settings section with collapsible design
## [3.55.0]
- Add new model: Arcee Trinity Large Preview
- Add new model: Moonshot Kimi K2.5
- Add MCP prompts support - prompts from connected MCP servers now appear in slash command autocomplete as `/mcp:<server>:<prompt>`
## [3.54.0]
### Added
- Native tool calls support for Ollama provider
- Sonnet 4.5 is now the default Amazon Bedrock model id
### Fixed
- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.
### Changed
- Removed Mistral's Devstral-2512 free from the free models list
- Removed deprecated zai-glm-4.6 model from Cerebras provider
## [3.53.1]
### Fixed
- Bug in responses API
## [3.53.0]
### Fixed
- Removed grok model from free tier
## [3.52.0]
### Added
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
- Grok models are now moving out of free tier and into paid plans.
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
### Fixed
- Bugs in DiffViewProvider for file editing
- Ollama's recommended models to use correct identifiers
## [3.51.0]
### Added
- Adding OpenAI gpt-5.2-codex model to the model picker
- Adding OpenAI gpt-5.2-codex model to the model picker
## [3.50.0]
@@ -1737,4 +1836,4 @@ Add Opus 4.1 through Claude Code
## [0.0.6]
- Initial release
- Initial release
+1
View File
@@ -1,2 +1,3 @@
@.clinerules/general.md
@.clinerules/network.md
@.clinerules/cli.md
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 Cline Bot Inc.
Copyright 2026 Cline Bot Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+1 -1
View File
@@ -148,4 +148,4 @@ Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), g
## License
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.317 4.15557C18.7873 3.45369 17.147 2.93658 15.4319 2.6404C15.4007 2.63469 15.3695 2.64897 15.3534 2.67754C15.1424 3.05276 14.9087 3.54225 14.7451 3.927C12.9004 3.65083 11.0652 3.65083 9.25832 3.927C9.09465 3.5337 8.85248 3.05276 8.64057 2.67754C8.62449 2.64992 8.59328 2.63564 8.56205 2.6404C6.84791 2.93563 5.20756 3.45275 3.67693 4.15557C3.66368 4.16129 3.65233 4.17082 3.64479 4.18319C0.533392 8.83155 -0.31895 13.3657 0.0991801 17.8436C0.101072 17.8655 0.11337 17.8864 0.130398 17.8997C2.18321 19.4073 4.17171 20.3225 6.12328 20.9291C6.15451 20.9386 6.18761 20.9272 6.20748 20.9015C6.66913 20.2711 7.08064 19.6063 7.43348 18.9073C7.4543 18.8664 7.43442 18.8178 7.39186 18.8016C6.73913 18.554 6.1176 18.2521 5.51973 17.9093C5.47244 17.8816 5.46865 17.814 5.51216 17.7816C5.63797 17.6873 5.76382 17.5893 5.88396 17.4902C5.90569 17.4721 5.93598 17.4683 5.96153 17.4797C9.88928 19.273 14.1415 19.273 18.023 17.4797C18.0485 17.4674 18.0788 17.4712 18.1015 17.4893C18.2216 17.5883 18.3475 17.6873 18.4742 17.7816C18.5177 17.814 18.5149 17.8816 18.4676 17.9093C17.8697 18.2588 17.2482 18.554 16.5945 18.8006C16.552 18.8168 16.533 18.8664 16.5538 18.9073C16.9143 19.6054 17.3258 20.2701 17.7789 20.9005C17.7978 20.9272 17.8319 20.9386 17.8631 20.9291C19.8241 20.3225 21.8126 19.4073 23.8654 17.8997C23.8834 17.8864 23.8948 17.8664 23.8967 17.8445C24.3971 12.6676 23.0585 8.17064 20.3482 4.18414C20.3416 4.17082 20.3303 4.16129 20.317 4.15557ZM8.02002 15.117C6.8375 15.117 5.86313 14.0313 5.86313 12.6981C5.86313 11.3648 6.8186 10.2791 8.02002 10.2791C9.23087 10.2791 10.1958 11.3743 10.1769 12.6981C10.1769 14.0313 9.22141 15.117 8.02002 15.117ZM15.9947 15.117C14.8123 15.117 13.8379 14.0313 13.8379 12.6981C13.8379 11.3648 14.7933 10.2791 15.9947 10.2791C17.2056 10.2791 18.1705 11.3743 18.1516 12.6981C18.1516 14.0313 17.2056 15.117 15.9947 15.117Z" fill="#FAFAFA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.477 2 2 6.477 2 12C2 16.418 4.865 20.166 8.84 21.49C9.34 21.58 9.52 21.27 9.52 21C9.52 20.77 9.51 20.14 9.51 19.31C6.73 19.91 6.14 17.97 6.14 17.97C5.68 16.81 5.03 16.5 5.03 16.5C4.12 15.88 5.1 15.9 5.1 15.9C6.1 15.97 6.63 16.93 6.63 16.93C7.5 18.45 8.97 18 9.54 17.76C9.63 17.11 9.89 16.67 10.17 16.42C7.95 16.17 5.62 15.31 5.62 11.5C5.62 10.39 6 9.5 6.65 8.79C6.55 8.54 6.2 7.5 6.75 6.15C6.75 6.15 7.59 5.88 9.5 7.17C10.29 6.95 11.15 6.84 12 6.84C12.85 6.84 13.71 6.95 14.5 7.17C16.41 5.88 17.25 6.15 17.25 6.15C17.8 7.5 17.45 8.54 17.35 8.79C18 9.5 18.38 10.39 18.38 11.5C18.38 15.32 16.04 16.16 13.81 16.41C14.17 16.72 14.5 17.33 14.5 18.26C14.5 19.6 14.49 20.68 14.49 21C14.49 21.27 14.67 21.59 15.17 21.49C19.14 20.16 22 16.42 22 12C22 6.477 17.523 2 12 2Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 902 B

+10
View File
@@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2001_1428)">
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5562 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2937 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516Z" fill="#FAFAFA"/>
</g>
<defs>
<clipPath id="clip0_2001_1428">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 989 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.0512 4.07466C15.3113 5.17727 16.301 5.99866 17.4829 5.99866C18.8627 5.99866 19.9813 4.87965 19.9813 3.49933C19.9813 2.11902 18.8627 1 17.4829 1C16.2764 1 15.2703 1.85537 15.036 2.99314C13.0155 3.20991 11.4378 4.92417 11.4378 7.00167C11.4378 7.00636 11.4378 7.00988 11.4378 7.01456C9.24041 7.10713 7.23397 7.73284 5.641 8.72062C5.04949 8.26247 4.30688 7.98945 3.50102 7.98945C1.5672 7.98945 0 9.55725 0 11.4918C0 12.8955 0.824597 14.1048 2.01581 14.6637C2.13177 18.7297 6.56047 22 12.0082 22C17.4559 22 21.8905 18.7261 22.0006 14.6567C23.1824 14.0942 24 12.8885 24 11.493C24 9.55842 22.4328 7.99063 20.499 7.99063C19.6966 7.99063 18.9575 8.2613 18.3672 8.71594C16.7602 7.72113 14.7315 7.09541 12.5119 7.01222C12.5119 7.0087 12.5119 7.00636 12.5119 7.00285C12.5119 5.51473 13.6176 4.27971 15.0512 4.077V4.07466ZM5.50044 13.7146C5.559 12.4444 6.40234 11.4695 7.38272 11.4695C8.3631 11.4695 9.11274 12.4995 9.05417 13.7697C8.99561 15.0398 8.26354 15.5015 7.28199 15.5015C6.30044 15.5015 5.44187 14.9848 5.50044 13.7146ZM16.6348 11.4695C17.6164 11.4695 18.4597 12.4444 18.5171 13.7146C18.5757 14.9848 17.716 15.5015 16.7356 15.5015C15.7552 15.5015 15.022 15.041 14.9634 13.7697C14.9048 12.4995 15.6533 11.4695 16.6348 11.4695ZM15.4682 16.6533C15.6521 16.6721 15.7693 16.8631 15.6978 17.0341C15.0946 18.4766 13.6703 19.4901 12.0082 19.4901C10.3461 19.4901 8.92299 18.4766 8.31859 17.0341C8.24714 16.8631 8.36427 16.6721 8.54817 16.6533C9.62577 16.5444 10.7912 16.4846 12.0082 16.4846C13.2252 16.4846 14.3895 16.5444 15.4682 16.6533Z" fill="#FAFAFA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3263 1.90393H21.6998L14.3297 10.3274L23 21.7899H16.2112L10.894 14.838L4.80995 21.7899H1.43443L9.31743 12.78L1 1.90393H7.96111L12.7674 8.25826L18.3263 1.90393ZM17.1423 19.7707H19.0116L6.94539 3.81706H4.93946L17.1423 19.7707Z" fill="#FAFAFA"/>
</svg>

After

Width:  |  Height:  |  Size: 358 B

+87 -50
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -28,58 +28,59 @@
"rules": {
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "off",
"useExhaustiveDependencies": "info",
"noUndeclaredVariables": "off",
"noEmptyPattern": "off",
"noEmptyPattern": "info",
"useJsxKeyInIterable": "off",
"noInnerDeclarations": "off",
"useHookAtTopLevel": "off",
"useYield": "off",
"useHookAtTopLevel": "info",
"useYield": "info",
"noConstructorReturn": "off",
"noInvalidPositionAtImportRule": "off",
"noSwitchDeclarations": "off",
"noUnusedImports": "error"
},
"a11y": "off",
"a11y": "info",
"style": {
"useNodejsImportProtocol": "off",
"useImportType": "off",
"useBlockStatements": "warn",
"useBlockStatements": "off",
"useNamingConvention": "off",
"useThrowOnlyError": "info",
"useConsistentArrayType": "off",
"noParameterAssign": "off",
"useAsConstAssertion": "off",
"useDefaultParameterLast": "off",
"noNonNullAssertion": "off",
"noNonNullAssertion": "info",
"useEnumInitializers": "off",
"useSelfClosingElements": "off",
"useSelfClosingElements": "info",
"useSingleVarDeclarator": "off",
"useNumberNamespace": "off",
"noInferrableTypes": "off",
"useTemplate": "off",
"noUselessElse": "off"
"useNumberNamespace": "info",
"noInferrableTypes": "info",
"useTemplate": "info",
"noUselessElse": "info"
},
"suspicious": {
"noDoubleEquals": "warn",
"noImplicitAnyLet": "info",
"noThenProperty": "off",
"noAsyncPromiseExecutor": "off",
"noAsyncPromiseExecutor": "info",
"noImportAssign": "off",
"noExplicitAny": "off",
"noControlCharactersInRegex": "off",
"noExplicitAny": "info",
"noControlCharactersInRegex": "warn",
"noShadowRestrictedNames": "off",
"noArrayIndexKey": "info",
"noAssignInExpressions": "info"
"noAssignInExpressions": "info",
"useIterableCallbackReturn": "info"
},
"complexity": {
"noUselessConstructor": "off",
"useOptionalChain": "off",
"noBannedTypes": "off",
"useLiteralKeys": "off",
"noUselessCatch": "off",
"noUselessSwitchCase": "off",
"noStaticOnlyClass": "off"
"noUselessConstructor": "info",
"useOptionalChain": "info",
"noBannedTypes": "warn",
"useLiteralKeys": "info",
"noUselessCatch": "info",
"noUselessSwitchCase": "info",
"noStaticOnlyClass": "info"
},
"security": {
"noDangerouslySetInnerHtml": "info"
@@ -94,6 +95,11 @@
"lineEnding": "lf",
"formatWithErrors": true
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
@@ -112,19 +118,21 @@
}
},
"files": {
"ignoreUnknown": true,
"includes": [
"**",
"!**/dist",
"!**/dist-*",
"!**/out",
"!**/evals",
"!**/playwright",
"!**/test-results",
"!**/node_modules",
"!**/webview-ui/build",
"!**/generated",
"!**/proto",
"!**/tests/specs"
// explicitly force files to be ignored by the scanner with !!
"!!**/dist",
"!!**/dist-*",
"!!**/out",
"!!**/evals",
"!!**/playwright",
"!!**/test-results",
"!!**/node_modules",
"!!**/webview-ui/build",
"!!**/generated",
"!!**/proto",
"!!**/tests/specs"
]
},
"plugins": [
@@ -134,29 +142,58 @@
{
"includes": [
"**",
"!**/hosts/vscode/**",
"!**/test/**",
"!**/*.test.ts",
"!src/dev/**",
"!src/extension.ts",
"!src/integrations/git/commit-message-generator.ts",
"!src/integrations/terminal/**",
"!src/core/controller/ui/openWalkthrough.ts"
"!!**/dist",
"!!**/hosts/vscode/**",
"!!**/test/**",
"!!**/*.test.ts",
"!!src/dev/**",
"!!src/extension.ts",
"!!src/integrations/git/commit-message-generator.ts",
"!!src/integrations/terminal/**",
"!!src/core/controller/ui/openWalkthrough.ts"
],
"plugins": [
"src/dev/grit/vscode-api.grit"
]
},
{
// Do not use console logging directly, use the Logger service instead.
"plugins": [
"src/dev/grit/console-log.grit"
],
"includes": [
"**",
"!src/core/storage/state-migrations.ts",
"!src/core/storage/FileContextTracker.ts",
"!src/core/context/context-tracking/FileContextTracker.ts",
"!src/common.ts",
"!src/services/logging/distinctId.ts",
"!src/core/storage/utils/state-helpers.ts",
"!src/extension.ts"
"!!**/esbuild.*",
"!!**/*.mts",
"!!**/webview-ui/**",
"!!**/evals/**",
"!!**/standalone/**",
"!!**/cli/**",
"!!**/e2e/**",
"!!**/test/**",
"!!**/__tests__/**",
"!!**/*.test.ts",
"!!**/*.stories.ts",
"!!src/dev/**",
"!!**/*.mjs",
"!!**/*.js",
"!!**/scripts/**",
"!!**/*.tsx",
"!!**/testing-platform/**",
// ACP mode must redirect console to stderr - this is intentional
"!!cli/src/acp/index.ts"
]
},
{
"includes": [
"**",
"!!src/core/storage/state-migrations.ts",
"!!src/core/storage/FileContextTracker.ts",
"!!src/core/context/context-tracking/FileContextTracker.ts",
"!!src/common.ts",
"!!src/services/logging/distinctId.ts",
"!!src/core/storage/utils/state-helpers.ts",
"!!src/extension.ts"
],
"plugins": [
"src/dev/grit/use-cache-service.grit"
-2
View File
@@ -1,2 +0,0 @@
cline-core-debug.log
bin/*
+365
View File
@@ -0,0 +1,365 @@
# Cline CLI
The official CLI for Cline. Run Cline tasks directly from the terminal with the same underlying functionality as the VS Code extension.
## Features
- **Reuses Core Codebase**: Shares the same Controller, Task, and API handling as the VS Code extension
- **Terminal Output**: Displays Cline messages directly in your terminal with colored output
- **Task History**: Access your task history from the command line
- **Configurable**: Use custom configuration directories and working directories
- **Image Support**: Attach images to your prompts using file paths or inline references
## Prerequisites
- Node.js 20.x or later
- npm or yarn
- The parent Cline project dependencies installed
## Installation
From the repository root:
```bash
# Install all dependencies first
npm run install:all
# Ensure protos are generated
npm run protos
# Build and link the CLI globally
npm run cli:link
```
## Usage
### Interactive Mode (Default)
When you run `cline` without any command, it launches an interactive welcome prompt:
```bash
# Launch interactive mode
cline
# Or run a task directly
cline "Create a hello world function in Python"
# With options
cline -v --thinking "Analyze this codebase"
```
### Commands
#### `task` (alias: `t`)
Run a new task with a prompt.
```bash
cline task "Create a hello world function in Python"
cline t "Create a hello world function"
```
**Options:**
| Option | Description |
|--------|-------------|
| `-a, --act` | Run in act mode |
| `-p, --plan` | Run in plan mode |
| `-y, --yolo` | Enable yolo mode (auto-approve actions) |
| `-m, --model <model>` | Model to use for the task |
| `-i, --images <paths...>` | Image file paths to include with the task |
| `-v, --verbose` | Show verbose output including reasoning |
| `-c, --cwd <path>` | Working directory for the task |
| `--config <path>` | Path to Cline configuration directory |
| `-t, --thinking` | Enable extended thinking (1024 token budget) |
**Examples:**
```bash
# Run in plan mode with verbose output
cline task -p -v "Design a REST API"
# Use a specific model with yolo mode
cline task -m claude-sonnet-4-5-20250929 -y "Refactor this function"
# Include images with your prompt
cline task -i screenshot.png diagram.jpg "Fix the UI based on these images"
# Or use inline image references in the prompt
cline task "Fix the layout shown in @./screenshot.png"
# Enable extended thinking for complex tasks
cline task -t "Architect a microservices system"
# Specify working directory
cline task -c /path/to/project "Add unit tests"
```
#### `history` (alias: `h`)
List task history with pagination support.
```bash
cline history
cline h
```
**Options:**
| Option | Description |
|--------|-------------|
| `-n, --limit <number>` | Number of tasks to show (default: 10) |
| `-p, --page <number>` | Page number, 1-based (default: 1) |
| `--config <path>` | Path to Cline configuration directory |
**Examples:**
```bash
# Show last 10 tasks (default)
cline history
# Show 20 tasks
cline history -n 20
# Show page 2 with 5 tasks per page
cline history -n 5 -p 2
```
#### `config`
Show current configuration including global and workspace state.
```bash
cline config
```
**Options:**
| Option | Description |
|--------|-------------|
| `--config <path>` | Path to Cline configuration directory |
#### `auth`
Authenticate a provider and configure what model is used.
```bash
cline auth
```
**Options:**
| Option | Description |
|--------|-------------|
| `-p, --provider <id>` | Provider ID for quick setup (e.g., openai-native, anthropic) |
| `-k, --apikey <key>` | API key for the provider |
| `-m, --modelid <id>` | Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929) |
| `-b, --baseurl <url>` | Base URL (optional, only for openai provider) |
| `-v, --verbose` | Show verbose output |
| `-c, --cwd <path>` | Working directory for the task |
| `--config <path>` | Path to Cline configuration directory |
**Examples:**
```bash
# Interactive authentication
cline auth
# Quick setup with provider and API key
cline auth -p anthropic -k sk-ant-xxxxx
# Full quick setup with model
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
# OpenAI-compatible provider with custom base URL
cline auth -p openai -k your-api-key -b https://api.example.com/v1
```
### Global Options
These options are available for the default command (running a task directly):
| Option | Description |
|--------|-------------|
| `-v, --verbose` | Show verbose output |
| `-c, --cwd <path>` | Working directory |
| `--config <path>` | Configuration directory |
| `--thinking` | Enable extended thinking (1024 token budget) |
## Development
### Quick Start
```bash
# 1. Install all dependencies (root, webview-ui, cli)
npm run install:all
# 2. Build and link globally so you can run `cline` from anywhere
npm run cli:link
# 3. Test it
cline --help
```
### Scripts
Run these from the repository root:
| Script | Description |
|--------|-------------|
| `npm run install:all` | Install deps for root, webview-ui, and cli |
| `npm run cli:build` | Generate protos and build CLI |
| `npm run cli:build:production` | Production build (minified) |
| `npm run cli:link` | Build and `npm link` so you can run `cline` from anywhere |
| `npm run cli:unlink` | Remove the global `cline` symlink |
| `npm run cli:dev` | Link + watch mode for development |
| `npm run cli:watch` | Watch mode only (no initial build) |
| `npm run cli:test` | Run CLI tests |
### Development Workflow
1. Run `npm run cli:dev` - this links the CLI globally and starts watch mode
2. Make changes to files in `cli/src/`
3. The build automatically rebuilds on save
4. Test your changes by running `cline` in another terminal
5. When done, run `npm run cli:unlink` to clean up
### Proto Generation
The CLI uses proto-generated types for message passing (same as the VS Code extension). If you modify any `.proto` files, run:
```bash
npm run protos
```
This generates TypeScript types in `src/generated/` that both the CLI and extension use.
## Publish
#### 1. Publish to npm
```bash
npm publish
```
#### 2. Update the Homebrew formula
```bash
npm run update-brew-formula
```
#### 3. Test the formula locally
```bash
# Create a local tap
brew tap-new cline/local
cp ./cli/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
# Build from Source
brew install --build-from-source cline/local/cline
# Install from your local tap
brew install cline/local/cline
# Clean up when done
brew untap cline/local
```
#### 4. If using a tap, commit and push
```bash
git add cline.rb
git commit -m "Update cline to v2.0.0"
git push
```
## Architecture
### How It Works
The CLI directly imports and reuses the core Cline TypeScript codebase (the same code that powers the VS Code extension). This means feature parity is easy to maintain - when core gets updated, the CLI automatically benefits.
```
┌─────────────────────────────────────────────────────────┐
│ CLI (cli/) │
│ - React Ink terminal UI │
│ - Command parsing (commander) │
│ - Terminal-specific adapters │
└─────────────────────────────────────────────────────────┘
│ direct imports
┌─────────────────────────────────────────────────────────┐
│ Core (src/core/) │
│ - Controller: task lifecycle, state management │
│ - Task: AI API calls, tool execution │
│ - StateManager: persistent storage │
│ - Proto types: message definitions │
└─────────────────────────────────────────────────────────┘
```
Unlike a client-server architecture, the CLI runs everything in a single Node.js process. The "host bridge" pattern provides terminal-appropriate implementations for things the VS Code extension would handle differently (clipboard, file dialogs, etc.).
### Key Files
| File | Purpose |
|------|---------|
| `src/index.ts` | Entry point, command definitions |
| `src/components/App.tsx` | Main React Ink app |
| `src/components/ChatView.tsx` | Task conversation UI |
| `src/controllers/CliWebviewProvider.ts` | Bridges core messages to terminal output |
| `src/vscode-context.ts` | Mock VS Code extension context for core compatibility |
| `src/vscode-shim.ts` | Shims for VS Code APIs that core depends on |
| `src/constants/colors.ts` | Terminal color definitions |
### React Ink
The CLI uses [React Ink](https://github.com/vadimdemedes/ink) for its terminal UI. This lets us build the interface with React components that render to the terminal. Key patterns:
- Components in `src/components/` render terminal UI
- Hooks in `src/hooks/` manage terminal-specific state (size, scrolling)
- The `useStateSubscriber` hook subscribes to core state changes
## Configuration
The CLI stores its data in `~/.cline/data/` by default:
- `globalState.json`: Global settings and state
- `secrets.json`: API keys and secrets
- `workspace/`: Workspace-specific state
- `tasks/`: Task history and conversation data
Override with the `--config` option or `CLINE_DIR` environment variable.
## Troubleshooting
### Build Errors
If you encounter build errors:
```bash
# Make sure all deps are installed
npm run install:all
# Regenerate proto types
npm run protos
# Then rebuild
npm run cli:build
```
### "command not found: cline"
The CLI isn't linked globally. Run:
```bash
npm run cli:link
```
### Changes Not Reflected
If your code changes aren't showing up:
1. Make sure watch mode is running (`npm run cli:dev`)
2. Check for TypeScript errors in the watch output
3. Try unlinking and relinking: `npm run cli:unlink && npm run cli:link`
### Import Errors from Core
The CLI imports from `@core/`, `@shared/`, etc. These paths are defined in the root `tsconfig.json`. If you see import errors, make sure you're building from the repo root, not from inside `cli/`.
+58 -49
View File
@@ -1,73 +1,82 @@
# Cline CLI
# Cline
```
/_____/\ /_/\ /_______/\/__/\ /__/\ /_____/\
\:::__\/ \:\ \ \__.::._\/\::\_\\ \ \\::::_\/_
\:\ \ __\:\ \ \::\ \ \:. `-\ \ \\:\/___/\
\:\ \/_/\\:\ \____ _\::\ \__\:. _ \ \\::___\/_
\:\_\ \ \\:\/___/\/__\::\__/\\. \`-\ \ \\:\____/\
\_____\/ \_____\/\________\/ \__\/ \__\/ \_____\/
```
<p align="center">
<img src="https://github.com/user-attachments/assets/7123f9d1-afeb-48d5-93fa-e750dec0ebba" width="70%" />
</p>
Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more.
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://www.npmjs.com/package/cline" target="_blank"><strong>NPM</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
</td>
<td align="center">
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
</td>
<td align="center">
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
</td>
<td align="center">
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
</td>
</tbody>
</table>
</div>
## Installation
Meet Cline, an AI assistant that lives in your terminal.
Install Cline globally using npm:
Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support.
```bash
npm install -g cline
```
npm i -g cline
## Usage
```bash
# cd into your project and run:
cline
```
This will start the Cline CLI interface where you can interact with the autonomous coding agent.
> Move your mouse around under the Cline icon for a surprise!
## Features
---
- **Autonomous Coding**: AI-powered code generation, editing, and refactoring
- **File Operations**: Create, read, update, and delete files and directories
- **Command Execution**: Run shell commands and scripts
- **Browser Automation**: Interact with web pages and applications
- **Multi-Model Support**: Works with Anthropic Claude, OpenAI GPT, and other AI models
- **MCP Integration**: Extensible through Model Context Protocol servers
- **Project Understanding**: Analyzes codebases to provide context-aware assistance
<img align="right" width="340" src="https://github.com/user-attachments/assets/ceb74224-08aa-4b8b-a3e7-b438ac3d160a">
## Requirements
### Use any API and Model
- Node.js 18.0.0 or higher
- Supported platforms: macOS, Linux. Windows soon
- Supported architectures: x64, arm64
Cline supports API providers like ChatGPT, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using a Cline Account, you'll always have access to the newest models as soon as they're available.
## Configuration
<!-- Transparent pixel to create line break after floating image -->
Cline can be configured through:
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
- Environment variables
- Configuration files
- Command-line arguments
<img align="left" width="370" src="https://github.com/user-attachments/assets/cad091f6-6c0f-4e4b-97ea-a1ff67e39b9b">
See the [main documentation](https://cline.bot) for detailed configuration options.
### Stay in Control with Human-in-the-Loop
## Links
Cline asks for your approval before running commands, editing files, or taking any action. Review each step and approve or reject as you go—or enable auto-approve to let Cline work autonomously to completion.
- **Website**: [https://cline.bot](https://cline.bot)
- **Documentation**: [https://docs.cline.bot](https://docs.cline.bot)
- **GitHub**: [https://github.com/cline/cline](https://github.com/cline/cline)
- **VSCode Extension**: Available in the VSCode Marketplace
- **JetBrains Extension**: Available in the JetBrains Marketplace
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/cad091f6-6c0f-4e4b-97ea-a1ff67e39b9b"><br>
<img align="right" width="400" src="https://github.com/user-attachments/assets/4f264a0c-3802-49a7-8e5e-13d97beb659e">
### Plan & Act Modes
Toggle to Plan Mode to discuss implementation and architecture with Cline. He'll ask clarifying questions, explore your codebase, and present a plan for you to align on. Once you're satisfied, switch to Act Mode and let Cline execute the plan.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/4f264a0c-3802-49a7-8e5e-13d97beb659e"><br>
## Enterprise
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
## License
Apache-2.0 - see [LICENSE](https://github.com/cline/cline/blob/main/LICENSE) for details.
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
## Support
- Report issues: [GitHub Issues](https://github.com/cline/cline/issues)
- Community: [GitHub Discussions](https://github.com/cline/cline/discussions)
- Documentation: [docs.cline.bot](https://docs.cline.bot)
- Cline CLI Architecture: [architecture.md](./architecture.md)
-292
View File
@@ -1,292 +0,0 @@
# Cline CLI Architecture
The CLI is a **standalone terminal interface** for the Cline AI coding assistant, written in Go. It provides the same autonomous coding capabilities as the VS Code extension but runs entirely in the terminal.
## High-Level Architecture
```
┌─────────────────────────────────────────────────────────────────────────┐
│ User Terminal │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ cline (Go binary) │
│ cmd/cline/main.go │
│ • Cobra CLI commands (task, auth, config, instance, etc.) │
│ • Interactive input via Bubble Tea │
│ • Streaming output with markdown rendering │
└─────────────────────────────────────────────────────────────────────────┘
│ gRPC (50052) │ starts subprocess
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ cline-core │◄────────────────►│ cline-host │
│ (Node.js) │ gRPC (51052) │ (Go binary) │
│ │ │ cmd/cline-host/main.go│
│ • AI/LLM orchestration │ │ │
│ • Tool execution │ │ • Workspace paths │
│ • Task state mgmt │ │ • File diff editing │
│ • Message handling │ │ • Clipboard access │
└─────────────────────────┘ │ • Environment info │
│ └─────────────────────────┘
│ SQLite (self-registration)
┌─────────────────────────────────────────────────────────────────────────┐
│ ~/.cline/data/locks/locks.db │
│ (Instance registry - core self-registers on startup) │
└─────────────────────────────────────────────────────────────────────────┘
```
## Entry Points (`cmd/`)
### `cmd/cline/main.go` - Main CLI
Cobra-based CLI with commands:
- **Root**: `cline [prompt]` - Start a task directly
- **task**: Create, send, view, list, pause, restore tasks
- **auth**: Authentication setup and provider configuration
- **config**: Read/write settings
- **instance**: Manage running Cline instances
- **logs**: View and clean log files
- **doctor**: System health check
### `cmd/cline-host/main.go` - Host Bridge Service
Separate gRPC server providing host environment operations to cline-core:
- Workspace paths
- File diff editing
- Clipboard access
- Shutdown coordination
---
## `pkg/cli/` Subsystems
### 1. `auth/` - Authentication System
Handles authentication with Cline service and BYO (Bring Your Own) API providers.
| File | Purpose |
| ------------------------- | ------------------------------------------------------------------------ |
| `auth_cline_provider.go` | OAuth login flow - opens browser, subscribes to auth callback stream |
| `auth_menu.go` | Interactive menu showing auth options based on current state |
| `auth_subscription.go` | gRPC stream subscription for auth status updates |
| `wizard_byo.go` | Interactive wizard for configuring BYO providers |
| `wizard_byo_bedrock.go` | AWS Bedrock-specific credential setup |
| `wizard_byo_oca.go` | Oracle Code Assist setup |
| `providers_list.go` | Retrieves configured providers from core state |
| `providers_byo.go` | Provider selection UI and field configuration |
| `models_*.go` | Model listing (static lists + dynamic fetch from OpenRouter/OpenAI/Ollama) |
**Flow**: User runs `cline auth` → Menu shows options → For BYO: wizard guides through provider/key/model selection → Config saved via gRPC to core.
---
### 2. `clerror/` - Error Handling
Parses and classifies API errors from the Cline service.
**Error Types:**
- `ErrorTypeAuth` - 401, bad API key
- `ErrorTypeBalance` - Insufficient credits
- `ErrorTypeRateLimit` - 429, quota exceeded
- `ErrorTypeNetwork` - Connection issues
- `ErrorTypeUnknown` - Catch-all
Extracts billing details (balance, spent, buy credits URL) from error responses.
---
### 3. `config/` - Configuration Management
| File | Purpose |
| --------------------- | -------------------------------------------------------------------------- |
| `manager.go` | gRPC interface for reading/writing settings via `UpdateSettingsCli` RPC |
| `settings_renderer.go`| Pretty-prints config values, censors sensitive fields (keys, secrets) |
Supports dot-notation paths: `cline config get auto-approval-settings.actions.read-files`
---
### 4. `display/` - Terminal Display System
The most complex subsystem - handles all visual output.
| File | Purpose |
| ----------------------- | -------------------------------------------------------------------- |
| `renderer.go` | Central coordinator with lipgloss styles, color methods, markdown delegation |
| `streaming.go` | Real-time streaming display with deduplication |
| `segment_streamer.go` | Streaming segments (header + body) with context-aware headers |
| `typewriter.go` | Character-by-character animation with variable delays |
| `markdown_renderer.go` | Glamour wrapper for terminal markdown rendering |
| `tool_renderer.go` | Tool operation formatting ("Cline is editing `file.ts`") |
| `tool_result_parser.go` | Parses structured tool results (file lists, search results) |
| `banner.go` | Session startup banner with version/model/workspace |
| `deduplicator.go` | MD5-based deduplication with 2-second window |
| `system_renderer.go` | Rich error/warning boxes for balance errors, auth failures |
| `ansi.go` | TTY detection, line clearing with escape codes |
---
### 5. `global/` - Global State Management
| File | Purpose |
| ------------------ | -------------------------------------------------------------------------- |
| `global.go` | Global config (paths, verbosity, output format), initialization |
| `registry.go` | Instance discovery via SQLite, health checking, default instance management|
| `cline-clients.go` | Starts cline-core + cline-host processes, port allocation, cleanup |
**Instance lifecycle:**
1. Find available port pair
2. Start `cline-host` on port+1000
3. Start `cline-core` on port
4. Wait for core to self-register in SQLite
5. Set as default if first instance
---
### 6. `handlers/` - Message Handlers
Routes incoming messages from cline-core to appropriate renderers.
| File | Purpose |
| ------------------ | --------------------------------------------------------------------- |
| `handler.go` | Handler registry with priority-based routing |
| `ask_handlers.go` | Approval requests: tool, command, followup, api_req_failed, etc. |
| `say_handlers.go` | Status messages: text, reasoning, command_output, tool, checkpoint, etc. |
Uses `DisplayContext` providing renderer access, state, and context flags (isLast, isPartial, isStreamingMode).
---
### 7. `output/` - Output Coordination
| File | Purpose |
| --------------------- | ----------------------------------------------------------------------- |
| `coordinator.go` | Coordinates streaming output with interactive input (saves/restores input state) |
| `input_model.go` | Bubble Tea model for rich input (message, approval, feedback types) |
| `slash_completion.go` | Autocomplete dropdown for slash commands |
**Key pattern:** When output needs to print while input is visible, the coordinator saves input state, clears the form, prints, then restores input.
---
### 8. `slash/` - Slash Command Registry
Central registry for commands like `/plan`, `/act`, `/cancel`:
- **CLI-local commands**: Handled directly by CLI
- **Backend commands**: Fetched from core via gRPC, filtered by `CliCompatible` flag
---
### 9. `sqlite/` - Instance Locking
Manages the distributed locking system:
- **Instance locks**: Track running Cline instances by address
- **File locks**: Coordinate file access across instances
- SQLite database created by cline-core, CLI reads/writes for discovery
---
### 10. `task/` - Task Management
| File | Purpose |
| ----------------------- | -------------------------------------------------------------------- |
| `manager.go` | Core orchestrator: create, cancel, resume, restore tasks; stream handling |
| `stream_coordinator.go` | Deduplication and turn management for dual streams |
| `input_handler.go` | Interactive input during follow mode (polling, approval detection) |
| `history_handler.go` | Direct disk access to `taskHistory.json` |
| `settings_parser.go` | Parse settings from CLI flags |
| `follow_options.go` | Configuration for follow behavior |
**Streaming:** Task manager subscribes to two gRPC streams:
1. `SubscribeToState` - Full state updates
2. `SubscribeToPartialMessage` - Streaming AI responses
---
### 11. `terminal/` - Terminal Handling
Enhanced keyboard protocol support and terminal configuration:
- Enables modifyOtherKeys and Kitty keyboard protocol
- Detects terminal type (VS Code, iTerm, Ghostty, Kitty, etc.)
- Auto-configures shift+enter keybindings for various terminals
---
### 12. `types/` - Type Definitions
| File | Purpose |
| -------------- | ----------------------------------------------------------------- |
| `messages.go` | `ClineMessage`, `AskType`, `SayType`, `ToolType` enums, proto conversion |
| `state.go` | `ConversationState` with thread-safe message access |
| `history.go` | `HistoryItem` matching taskHistory.json format |
---
### 13. `updater/` - Auto-Update
Background auto-update checking:
- 24-hour check interval (cached)
- Queries npm registry for newer versions
- Supports `latest` and `nightly` channels
- Runs `npm install -g cline` to update
---
## `pkg/common/` - Shared Types
| File | Purpose |
| --------------- | ------------------------------------------------------------ |
| `constants.go` | `SETTINGS_SUBFOLDER`, `DEFAULT_CLINE_CORE_PORT` |
| `schema.go` | SQL queries for instance/file locks |
| `types.go` | `CoreInstanceInfo`, `LockRow`, `DefaultCoreInstance` |
| `utils.go` | Port checking, health checks, address normalization, retry logic |
---
## `pkg/generated/` - Auto-Generated
| File | Purpose |
| --------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `providers.go` | Provider definitions (Anthropic, OpenAI, Bedrock, etc.) with field metadata and model specs - generated from TypeScript sources |
| `field_overrides.go` | Manual overrides for field filtering |
---
## `pkg/hostbridge/` - CLI-to-Core Bridge
This is the **reverse bridge** allowing cline-core to request host environment operations:
| File | Purpose |
| ----------------------- | ---------------------------------------------------- |
| `grpc_server.go` | Main server registering all services |
| `simple_workspace.go` | Workspace service: returns CWD as workspace path |
| `diff.go` | In-memory file diff editing with line-based operations |
| `env.go` | Clipboard access, version info, shutdown coordination |
| `window.go` | UI stubs (no-ops or console output) |
**Why this exists:** The same cline-core logic runs in VS Code and CLI. In VS Code, the "host" is the extension with editor APIs. In CLI, hostbridge emulates these capabilities with terminal-appropriate implementations.
---
## Key Design Decisions
1. **Two-process model:** `cline` CLI manages instances; `cline-core` is the actual AI engine (Node.js). This allows reusing the same core as the VS Code extension.
2. **Self-registration via SQLite:** `cline-core` registers itself in a SQLite database on startup. The CLI discovers instances by reading this database, enabling multi-instance support.
3. **Host bridge abstraction:** The `cline-host` process provides platform-specific operations (clipboard, workspace paths) via gRPC, allowing `cline-core` to remain host-agnostic.
4. **Streaming-first UI:** The CLI uses gRPC streaming to display AI responses in real-time with typewriter-style rendering.
5. **Dual stream handling:** Task manager subscribes to both state updates and partial messages, using deduplication to prevent duplicate rendering.
+21
View File
@@ -0,0 +1,21 @@
# IMPORTANT: `npm run postpublish` to update this file after publishing a new version of the package
class Cline < Formula
desc "Autonomous coding agent CLI - capable of creating/editing files, running commands, and more"
homepage "https://cline.bot"
url "https://registry.npmjs.org/cline/-/cline-2.0.0.tgz" # GET from https://registry.npmjs.org/cline/latest tarball URL
sha256 "65bae90401191aeeabfbbc0b315e816aea96742043ba85b90671bf5e19d0761e"
license "Apache-2.0"
depends_on "node@20"
depends_on "ripgrep"
def install
system "npm", "install", *std_npm_args(prefix: false)
bin.install_symlink Dir["#{libexec}/bin/*"]
end
test do
# Test that the binary exists and is executable
assert_match version.to_s, shell_output("#{bin}/cline --version")
end
end
-73
View File
@@ -1,73 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/cline/cli/pkg/hostbridge"
)
var (
port int
verbose bool
workspaces []string
)
func main() {
rootCmd := &cobra.Command{
Use: "cline-host",
Short: "Cline Host Bridge Service",
Long: `A simple host bridge service that provides host operations for Cline Core.`,
RunE: runServer,
}
rootCmd.Flags().IntVarP(&port, "port", "p", 51052, "port to listen on")
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose logging")
rootCmd.Flags().StringSliceVar(&workspaces, "workspace", nil, "workspace paths")
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runServer(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Create gRPC hostbridge service
service := hostbridge.NewGrpcServer(port, verbose, workspaces)
// Handle graceful shutdown
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
if verbose {
log.Println("Shutting down hostbridge server...")
}
cancel()
}()
// Start server
if verbose {
log.Printf("Starting Cline Host Bridge on port %d", port)
}
// Run the service
if err := service.Start(ctx); err != nil {
return fmt.Errorf("failed to run service: %w", err)
}
return nil
}
-385
View File
@@ -1,385 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"slices"
"strings"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli"
"github.com/cline/cli/pkg/cli/auth"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/output"
"github.com/cline/cli/pkg/cli/slash"
"github.com/cline/cli/pkg/common"
"github.com/cline/grpc-go/cline"
"github.com/spf13/cobra"
)
var (
coreAddress string
verbose bool
outputFormat string
// Task creation flags (for root command)
images []string
files []string
mode string
settings []string
yolo bool
oneshot bool
workspaces []string
)
func main() {
rootCmd := &cobra.Command{
Use: "cline [prompt]",
Short: "Cline CLI - AI-powered coding assistant",
Version: global.CliVersion,
Long: `A command-line interface for interacting with Cline AI coding assistant.
Start a new task by providing a prompt:
cline "Create a new Python script that prints hello world"
Or pipe a prompt via stdin:
echo "Create a todo app" | cline
cat prompt.txt | cline --yolo
Or run with no arguments to enter interactive mode:
cline
This CLI also provides task management, configuration, and monitoring capabilities.
For detailed documentation including all commands, options, and examples,
see the manual page: man cline`,
Args: cobra.ArbitraryArgs,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if outputFormat != "rich" && outputFormat != "json" && outputFormat != "plain" {
return fmt.Errorf("invalid output format '%s': must be one of 'rich', 'json', or 'plain'", outputFormat)
}
return global.InitializeGlobalConfig(&global.GlobalConfig{
Verbose: verbose,
OutputFormat: outputFormat,
CoreAddress: coreAddress,
})
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Validate workspace paths exist
if err := common.ValidateDirsExist(workspaces); err != nil {
return err
}
// Build the full workspace list: cwd first, then additional workspaces
allWorkspaces, err := buildWorkspaceList(workspaces)
if err != nil {
return fmt.Errorf("failed to build workspace list: %w", err)
}
// If --address flag not provided, start instance BEFORE getting prompt
if !cmd.Flags().Changed("address") {
if global.Config.Verbose {
fmt.Println("Starting new Cline instance...")
}
instance, err := global.Instances.StartNewInstance(ctx, allWorkspaces...)
if err != nil {
return fmt.Errorf("failed to start new instance: %w", err)
}
global.Config.CoreAddress = instance.CoreAddress
if global.Config.Verbose {
fmt.Printf("Started instance at %s\n\n", global.Config.CoreAddress)
}
// Set up cleanup on exit
defer func() {
if global.Config.Verbose {
fmt.Println("\nCleaning up instance...")
}
registry := global.Instances.GetRegistry()
if err := global.KillInstanceByAddress(context.Background(), registry, global.Config.CoreAddress); err != nil {
if global.Config.Verbose {
fmt.Printf("Warning: Failed to clean up instance: %v\n", err)
}
}
}()
}
// Check if user has credentials configured
if !isUserReadyToUse(ctx) {
// Create renderer for welcome messages
renderer := display.NewRenderer(global.Config.OutputFormat)
fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
// Check if user cancelled - exit cleanly
if err == huh.ErrUserAborted {
return nil
}
return fmt.Errorf("auth setup failed: %w", err)
}
// Re-check after auth wizard
if !isUserReadyToUse(ctx) {
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
}
fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
}
// Get content from both args and stdin
prompt, err := getContentFromStdinAndArgs(args)
if err != nil {
return fmt.Errorf("failed to read prompt: %w", err)
}
// If no prompt (or just a mode switch with no message), show interactive input
// Loop to allow mode switches without a message
bannerShown := false
for prompt == "" {
// Pass the mode flag and workspaces to banner so it shows correct info
prompt, err = promptForInitialTask(ctx, mode, allWorkspaces, !bannerShown)
bannerShown = true
if err != nil {
// Check if user cancelled - exit cleanly without error
if err == huh.ErrUserAborted {
return nil
}
return err
}
// Check if user entered a mode switch command
if newMode, remaining, isModeSwitch := slash.ParseModeSwitch(prompt); isModeSwitch {
mode = newMode
prompt = remaining
// If just a mode switch with no message, continue loop to re-prompt
if prompt == "" {
renderer := display.NewRenderer(global.Config.OutputFormat)
if mode == "act" {
fmt.Printf("\n%s\n\n", renderer.Success("Switched to act mode"))
} else {
fmt.Printf("\n%s\n\n", renderer.Success("Switched to plan mode"))
}
continue
}
}
if prompt == "" {
return fmt.Errorf("prompt required")
}
}
// If oneshot mode, force plan mode and yolo
if oneshot {
mode = "plan"
yolo = true
}
return cli.CreateAndFollowTask(ctx, prompt, cli.TaskOptions{
Images: images,
Files: files,
Mode: mode,
Settings: settings,
Yolo: yolo,
Address: global.Config.CoreAddress,
Verbose: verbose,
Workspaces: allWorkspaces,
})
},
}
rootCmd.SetVersionTemplate(cli.VersionString())
rootCmd.PersistentFlags().StringVar(&coreAddress, "address", fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT), "Cline Core gRPC address")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "rich", "output format (rich|json|plain)")
// Task creation flags (only apply when using root command with prompt)
rootCmd.Flags().StringSliceVarP(&images, "image", "i", nil, "attach image files")
rootCmd.Flags().StringSliceVarP(&files, "file", "f", nil, "attach files")
rootCmd.Flags().StringVarP(&mode, "mode", "m", "plan", "mode (act|plan) - defaults to plan")
rootCmd.Flags().StringSliceVarP(&settings, "setting", "s", nil, "task settings (key=value format)")
rootCmd.Flags().BoolVarP(&yolo, "yolo", "y", false, "enable yolo mode (non-interactive)")
rootCmd.Flags().BoolVar(&yolo, "no-interactive", false, "enable yolo mode (non-interactive)")
rootCmd.Flags().BoolVarP(&oneshot, "oneshot", "o", false, "full autonomous mode")
rootCmd.Flags().StringSliceVarP(&workspaces, "workspace", "w", nil, "additional workspace paths (can be specified multiple times)")
rootCmd.AddCommand(cli.NewTaskCommand())
rootCmd.AddCommand(cli.NewInstanceCommand())
rootCmd.AddCommand(cli.NewConfigCommand())
rootCmd.AddCommand(cli.NewVersionCommand())
rootCmd.AddCommand(cli.NewAuthCommand())
rootCmd.AddCommand(cli.NewLogsCommand())
// rootCmd.AddCommand(cli.NewDoctorCommand()) // Disabled for now
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
os.Exit(1)
}
}
func promptForInitialTask(ctx context.Context, modeFlag string, workspaces []string, showBanner bool) (string, error) {
// Show session banner before the initial input (only on first prompt)
if showBanner {
showSessionBanner(ctx, modeFlag, workspaces)
}
prompt, err := output.PromptForInitialTask(
"Start a new Cline task",
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
modeFlag,
slash.NewRegistry(ctx),
)
if err != nil {
if err == output.ErrUserAborted {
return "", huh.ErrUserAborted
}
return "", err
}
return prompt, nil
}
// showSessionBanner displays session info before initial prompt
func showSessionBanner(ctx context.Context, modeFlag string, workspaces []string) {
bannerInfo := display.BannerInfo{
Version: global.CliVersion,
Mode: modeFlag, // Use the mode from command flag, not state
}
// If mode is empty, default to "plan"
if bannerInfo.Mode == "" {
bannerInfo.Mode = "plan"
}
bannerInfo.Workdirs = workspaces
// Get provider/model using auth functions (same logic as auth menu)
if providerList, err := auth.GetProviderConfigurations(ctx); err == nil {
// Show provider/model for the mode we'll be using
var providerDisplay *auth.ProviderDisplay
if bannerInfo.Mode == "plan" && providerList.PlanProvider != nil {
providerDisplay = providerList.PlanProvider
} else if bannerInfo.Mode == "act" && providerList.ActProvider != nil {
providerDisplay = providerList.ActProvider
}
if providerDisplay != nil {
bannerInfo.Provider = auth.GetProviderIDForEnum(providerDisplay.Provider)
bannerInfo.ModelID = providerDisplay.ModelID
}
}
// Render and display banner
banner := display.RenderSessionBanner(bannerInfo)
fmt.Println(banner)
fmt.Println() // Extra spacing before form
}
// isUserReadyToUse checks if the user has completed initial setup
// Returns true if welcomeViewCompleted flag is set OR user is authenticated
// Matches extension logic: welcomeViewCompleted = Boolean(globalState.welcomeViewCompleted || user?.uid)
func isUserReadyToUse(ctx context.Context) bool {
grpcClient, err := global.GetClientForAddress(ctx, global.Config.CoreAddress)
if err != nil {
return false
}
state, err := grpcClient.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return false
}
stateMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(state.StateJson), &stateMap); err != nil {
return false
}
if welcomeCompleted, ok := stateMap["welcomeViewCompleted"].(bool); ok && welcomeCompleted {
return true
}
// Check 2: Is user authenticated? (matches extension's || user?.uid check)
if userInfo, ok := stateMap["userInfo"].(map[string]interface{}); ok {
if uid, ok := userInfo["uid"].(string); ok && uid != "" {
return true
}
}
return false
}
// getContentFromStdinAndArgs reads content from both command line args and stdin, and combines them
func getContentFromStdinAndArgs(args []string) (string, error) {
var content strings.Builder
// Add command line args first (if any)
if len(args) > 0 {
content.WriteString(strings.Join(args, " "))
}
// Check if stdin has data
stat, err := os.Stdin.Stat()
if err != nil {
return "", fmt.Errorf("failed to stat stdin: %w", err)
}
// Check if data is being piped to stdin
if (stat.Mode() & os.ModeCharDevice) == 0 {
// Only try to read if there's actually data available
if stat.Size() > 0 {
stdinBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("failed to read from stdin: %w", err)
}
stdinContent := strings.TrimSpace(string(stdinBytes))
if stdinContent != "" {
if content.Len() > 0 {
content.WriteString(" ")
}
content.WriteString(stdinContent)
}
}
}
return content.String(), nil
}
// buildWorkspaceList builds the full workspace list with cwd as the first entry
func buildWorkspaceList(additionalWorkspaces []string) ([]string, error) {
cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to get current working directory: %w", err)
}
// Start with cwd
workspaces := []string{cwd}
// Add additional workspaces, avoiding duplicates
for _, ws := range additionalWorkspaces {
// Normalize the path
absPath, err := common.AbsPath(ws)
if err != nil {
return nil, fmt.Errorf("failed to resolve workspace path %s: %w", ws, err)
}
// Skip if it's the same as cwd
if absPath == cwd {
continue
}
// Check for duplicates
isDuplicate := slices.Contains(workspaces, absPath)
if !isDuplicate {
workspaces = append(workspaces, absPath)
}
}
return workspaces, nil
}
-154
View File
@@ -1,154 +0,0 @@
package e2e
import (
"context"
"encoding/json"
"os"
"path/filepath"
"syscall"
"testing"
"github.com/cline/cli/pkg/common"
)
// 2. Multi-instance start: default_instance remains the first started.
func TestMultiInstanceDefaultUnchanged(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start first instance and wait healthy
_ = mustRunCLI(ctx, t, "instance", "new")
out1 := listInstancesJSON(ctx, t)
if len(out1.CoreInstances) != 1 {
t.Fatalf("expected 1 instance, got %d", len(out1.CoreInstances))
}
firstAddr := out1.CoreInstances[0].CoreAddress
waitForAddressHealthy(t, firstAddr, defaultTimeout)
// Start second instance
_ = mustRunCLI(ctx, t, "instance", "new")
out2 := listInstancesJSON(ctx, t)
if len(out2.CoreInstances) < 2 {
t.Fatalf("expected at least 2 instances, got %d", len(out2.CoreInstances))
}
// Default should remain the first started address
if out2.DefaultInstance != firstAddr {
t.Fatalf("default changed; expected %s, got %s", firstAddr, out2.DefaultInstance)
}
}
// 6. Default.json update after removal of current default
func TestDefaultJsonUpdateAfterRemoval(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start two instances
_ = mustRunCLI(ctx, t, "instance", "new")
_ = mustRunCLI(ctx, t, "instance", "new")
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) < 2 {
t.Fatalf("expected at least 2 instances, got %d", len(out.CoreInstances))
}
// Choose second as new default
target := out.CoreInstances[1]
waitForAddressHealthy(t, target.CoreAddress, defaultTimeout)
// Set as default
_ = mustRunCLI(ctx, t, "instance", "use", target.CoreAddress)
// Verify default switched
out = listInstancesJSON(ctx, t)
if out.DefaultInstance != target.CoreAddress {
t.Fatalf("default_instance not updated to %s (got %s)", target.CoreAddress, out.DefaultInstance)
}
// Kill the default instance using runtime PID discovery
corePID := getCorePID(t, target.CoreAddress)
if corePID <= 0 {
t.Fatalf("could not find PID for core process at %s", target.CoreAddress)
}
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.CoreAddress)
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
t.Fatalf("kill pid %d: %v", corePID, err)
}
// Wait for removal
waitForAddressRemoved(t, target.CoreAddress, longTimeout)
// Clean up dangling host process (SIGKILL leaves these behind by design)
t.Logf("Cleaning up dangling host process on port %d", target.HostPort())
findAndKillHostProcess(t, target.HostPort())
// Ensure default_instance updated to another available instance (or removed if none remain)
out = listInstancesJSON(ctx, t)
// If there are instances left, default_instance must be one of them
if len(out.CoreInstances) > 0 {
found := false
for _, it := range out.CoreInstances {
if out.DefaultInstance == it.CoreAddress {
found = true
break
}
}
if !found {
t.Fatalf("default_instance %s not set to an existing instance after removal", out.DefaultInstance)
}
} else {
// No instances remain; cli-default-instance.json should be removed
clineDir := getClineDir(t)
defPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
if _, err := os.Stat(defPath); err == nil {
t.Fatalf("expected cli-default-instance.json removed when no instances remain")
}
}
// Also verify cli-default-instance.json on disk reflects the in-memory default (if any)
clineDir := getClineDir(t)
defPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
if len(out.CoreInstances) > 0 {
raw, err := os.ReadFile(defPath)
if err != nil {
t.Fatalf("read cli-default-instance.json: %v", err)
}
var tmp struct {
DefaultInstance string `json:"default_instance"`
}
if err := json.Unmarshal(raw, &tmp); err != nil {
t.Fatalf("unmarshal cli-default-instance.json: %v", err)
}
if tmp.DefaultInstance != out.DefaultInstance {
t.Fatalf("cli-default-instance.json mismatch: file=%s list=%s", tmp.DefaultInstance, out.DefaultInstance)
}
}
}
// 11. SQLite database missing (edge): list succeeds and returns empty set
func TestRegistryDirMissingEdge(t *testing.T) {
clineDir := setTempClineDir(t)
// Remove the settings directory entirely (which contains locks.db)
settingsDir := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER)
if err := os.RemoveAll(settingsDir); err != nil {
t.Fatalf("RemoveAll(%s): %v", common.SETTINGS_SUBFOLDER, err)
}
// Listing should succeed and return empty results
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) != 0 {
t.Fatalf("expected 0 instances after removing %s dir, got %d", common.SETTINGS_SUBFOLDER, len(out.CoreInstances))
}
// Ensure cli-default-instance.json not present
defPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
if _, err := os.Stat(defPath); err == nil {
t.Fatalf("expected no cli-default-instance.json after removing %s dir", common.SETTINGS_SUBFOLDER)
}
}
-378
View File
@@ -1,378 +0,0 @@
package e2e
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/common"
"github.com/cline/grpc-go/cline"
)
const (
defaultTimeout = 30 * time.Second
longTimeout = 60 * time.Second
pollInterval = 250 * time.Millisecond
instancesBinRel = "../bin/cline"
)
func repoAwareBinPath(t *testing.T) string {
// Tests live in repoRoot/cli/e2e. Binary is at repoRoot/cli/bin/cline
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd error: %v", err)
}
// cli/e2e -> cli/bin/cline
p := filepath.Clean(filepath.Join(wd, instancesBinRel))
if _, err := os.Stat(p); err != nil {
t.Fatalf("CLI binary not found at %s; run `npm run compile-cli` first: %v", p, err)
}
return p
}
func setTempClineDir(t *testing.T) string {
t.Helper()
dir := t.TempDir()
clineDir := filepath.Join(dir, ".cline")
if err := os.MkdirAll(clineDir, 0o755); err != nil {
t.Fatalf("mkdir clineDir: %v", err)
}
t.Setenv("CLINE_DIR", clineDir)
return clineDir
}
func runCLI(ctx context.Context, t *testing.T, args ...string) (string, string, int) {
t.Helper()
bin := repoAwareBinPath(t)
// Ensure CLI uses the same CLINE_DIR as the tests by passing --config=<CLINE_DIR>
// (InitializeGlobalConfig uses ConfigPath as the base directory for registry.)
if clineDir := os.Getenv("CLINE_DIR"); clineDir != "" && !contains(args, "--config") {
// Prepend persistent flag so Cobra sees it regardless of subcommand position
args = append([]string{"--config", clineDir}, args...)
}
cmd := exec.CommandContext(ctx, bin, args...)
// Run CLI from repo root so relative paths inside CLI (./cli/bin/...) resolve
if wd, err := os.Getwd(); err == nil {
repoRoot := filepath.Clean(filepath.Join(wd, "..", ".."))
cmd.Dir = repoRoot
}
// propagate env including CLINE_DIR
cmd.Env = os.Environ()
outB, errB := &strings.Builder{}, &strings.Builder{}
cmd.Stdout = outB
cmd.Stderr = errB
err := cmd.Run()
exit := 0
if err != nil {
// Extract exit code if possible
if ee, ok := err.(*exec.ExitError); ok {
exit = ee.ExitCode()
} else {
exit = -1
}
}
return outB.String(), errB.String(), exit
}
func mustRunCLI(ctx context.Context, t *testing.T, args ...string) string {
t.Helper()
out, errOut, exit := runCLI(ctx, t, args...)
if exit != 0 {
t.Fatalf("cline %v failed (exit=%d)\nstdout:\n%s\nstderr:\n%s", args, exit, out, errOut)
}
return out
}
func listInstancesJSON(ctx context.Context, t *testing.T) common.InstancesOutput {
t.Helper()
// Trigger CLI to perform cleanup/health by invoking list (table output is ignored)
_ = mustRunCLI(ctx, t, "instance", "list")
// Read from SQLite locks database to build structured output
clineDir := getClineDir(t)
// Load default instance from settings file
defaultInstance := readDefaultInstanceFromSettings(t, clineDir)
// Load instances from SQLite
instances := readInstancesFromSQLite(t, clineDir)
return common.InstancesOutput{
DefaultInstance: defaultInstance,
CoreInstances: instances,
}
}
func hasAddress(in common.InstancesOutput, addr string) bool {
for _, it := range in.CoreInstances {
if it.CoreAddress == addr {
return true
}
}
return false
}
func getByAddress(in common.InstancesOutput, addr string) (common.CoreInstanceInfo, bool) {
for _, it := range in.CoreInstances {
if it.CoreAddress == addr {
return it, true
}
}
return common.CoreInstanceInfo{}, false
}
func waitFor(t *testing.T, timeout time.Duration, cond func() (bool, string)) {
t.Helper()
deadline := time.Now().Add(timeout)
for {
ok, msg := cond()
if ok {
return
}
if time.Now().After(deadline) {
t.Fatalf("waitFor timeout: %s", msg)
}
time.Sleep(pollInterval)
}
}
func waitForAddressHealthy(t *testing.T, addr string, timeout time.Duration) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
t.Logf("Waiting for gRPC health check on %s...", addr)
waitFor(t, timeout, func() (bool, string) {
if common.IsInstanceHealthy(ctx, addr) {
return true, ""
}
return false, fmt.Sprintf("gRPC health check failed for %s", addr)
})
t.Logf("gRPC health check passed for %s", addr)
}
func waitForAddressRemoved(t *testing.T, addr string, timeout time.Duration) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
waitFor(t, timeout, func() (bool, string) {
out := listInstancesJSON(ctx, t)
if hasAddress(out, addr) {
return false, fmt.Sprintf("address %s still present", addr)
}
return true, ""
})
}
func findFreePort(t *testing.T) int {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen 127.0.0.1:0: %v", err)
}
defer l.Close()
_, portStr, _ := net.SplitHostPort(l.Addr().String())
var port int
fmt.Sscanf(portStr, "%d", &port)
return port
}
func getClineDir(t *testing.T) string {
t.Helper()
clineDir := os.Getenv("CLINE_DIR")
if clineDir == "" {
t.Fatalf("CLINE_DIR not set")
}
return clineDir
}
// isPortInUse checks if a port is currently in use by any process
func isPortInUse(port int) bool {
conn, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return true // Port is in use
}
conn.Close()
return false // Port is free
}
// waitForPortClosed waits for a port to become free (no process listening)
func waitForPortClosed(t *testing.T, port int, timeout time.Duration) {
t.Helper()
waitFor(t, timeout, func() (bool, string) {
if isPortInUse(port) {
return false, fmt.Sprintf("port %d still in use", port)
}
return true, ""
})
}
// waitForPortsClosed waits for both core and host ports to become free
func waitForPortsClosed(t *testing.T, corePort, hostPort int, timeout time.Duration) {
t.Helper()
waitFor(t, timeout, func() (bool, string) {
if isPortInUse(corePort) {
return false, fmt.Sprintf("core port %d still in use", corePort)
}
if isPortInUse(hostPort) {
return false, fmt.Sprintf("host port %d still in use", hostPort)
}
return true, ""
})
}
// findAndKillHostProcess finds and kills any process listening on the host port
// This is used to clean up dangling host processes after SIGKILL tests
func findAndKillHostProcess(t *testing.T, hostPort int) {
t.Helper()
// Use lsof to find process listening on the host port
cmd := exec.Command("lsof", "-ti", fmt.Sprintf(":%d", hostPort))
output, err := cmd.Output()
if err != nil {
// No process found on port - that's fine
return
}
pidStr := strings.TrimSpace(string(output))
if pidStr == "" {
return
}
var pid int
if _, err := fmt.Sscanf(pidStr, "%d", &pid); err != nil {
t.Logf("Warning: could not parse PID from lsof output: %s", pidStr)
return
}
if pid > 0 {
t.Logf("Cleaning up dangling host process PID %d on port %d", pid, hostPort)
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
t.Logf("Warning: failed to kill dangling host process %d: %v", pid, err)
}
}
}
// getPIDByPort returns the PID of the process listening on the specified port (fallback method)
func getPIDByPort(t *testing.T, port int) int {
t.Helper()
cmd := exec.Command("lsof", "-ti", fmt.Sprintf(":%d", port))
output, err := cmd.Output()
if err != nil {
return 0 // Process not found
}
pidStr := strings.TrimSpace(string(output))
if pidStr == "" {
return 0
}
pid, err := strconv.Atoi(pidStr)
if err != nil {
t.Logf("Warning: could not parse PID from lsof output: %s", pidStr)
return 0
}
return pid
}
// getCorePIDViaRPC returns the PID of the cline-core process using RPC (preferred method)
func getCorePIDViaRPC(t *testing.T, address string) int {
t.Helper()
// Initialize global config to access registry
clineDir := os.Getenv("CLINE_DIR")
if clineDir == "" {
t.Logf("Warning: CLINE_DIR not set, falling back to lsof")
return getCorePIDViaLsof(t, address)
}
cfg := &global.GlobalConfig{
ConfigPath: clineDir,
}
if err := global.InitializeGlobalConfig(cfg); err != nil {
t.Logf("Warning: failed to initialize global config, falling back to lsof: %v", err)
return getCorePIDViaLsof(t, address)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Get client for the address
client, err := global.Instances.GetRegistry().GetClient(ctx, address)
if err != nil {
t.Logf("Warning: failed to get client for %s, falling back to lsof: %v", address, err)
return getCorePIDViaLsof(t, address)
}
// Call GetProcessInfo RPC
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
if err != nil {
t.Logf("Warning: GetProcessInfo RPC failed for %s, falling back to lsof: %v", address, err)
return getCorePIDViaLsof(t, address)
}
return int(processInfo.ProcessId)
}
// getCorePIDViaLsof returns the PID using lsof (fallback method)
func getCorePIDViaLsof(t *testing.T, address string) int {
t.Helper()
_, portStr, err := net.SplitHostPort(address)
if err != nil {
t.Logf("Warning: invalid address format %s", address)
return 0
}
port, err := strconv.Atoi(portStr)
if err != nil {
t.Logf("Warning: invalid port in address %s", address)
return 0
}
return getPIDByPort(t, port)
}
// getCorePID returns the PID of the cline-core process for the given address
// Uses RPC first, falls back to lsof if RPC fails
func getCorePID(t *testing.T, address string) int {
t.Helper()
// Try RPC first (preferred method)
if pid := getCorePIDViaRPC(t, address); pid > 0 {
return pid
}
// Fall back to lsof if RPC fails
return getCorePIDViaLsof(t, address)
}
// getHostPID returns the PID of the cline-host process for the given host port
func getHostPID(t *testing.T, hostPort int) int {
t.Helper()
return getPIDByPort(t, hostPort)
}
// contains reports whether slice has the target string.
func contains(slice []string, target string) bool {
for _, s := range slice {
if s == target {
return true
}
}
return false
}
-47
View File
@@ -1,47 +0,0 @@
package e2e
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
// TestMain validates required artifacts exist before running E2E tests.
// It does NOT build artifacts. Build manually via:
//
// npm run compile-standalone
// npm run compile-cli
func TestMain(m *testing.M) {
// Determine repo root from cli/e2e
wd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "getwd: %v\n", err)
os.Exit(2)
}
repoRoot := filepath.Clean(filepath.Join(wd, "..", ".."))
cliBin := filepath.Join(repoRoot, "cli", "bin", "cline")
coreJS := filepath.Join(repoRoot, "dist-standalone", "cline-core.js")
missing := []string{}
if _, err := os.Stat(cliBin); err != nil {
missing = append(missing, cliBin)
}
if _, err := os.Stat(coreJS); err != nil {
missing = append(missing, coreJS)
}
if len(missing) > 0 {
if testing.Short() {
// Optional quality-of-life: allow skipping with -short when artifacts are absent
fmt.Fprintf(os.Stderr, "[e2e] skipping (-short) due to missing artifacts:\n %s\n", strings.Join(missing, "\n "))
os.Exit(0)
}
fmt.Fprintf(os.Stderr, "Missing required build artifacts for E2E tests:\n %s\n\nPlease build them first:\n npm run compile-standalone\n npm run compile-cli\n", strings.Join(missing, "\n "))
os.Exit(2)
}
os.Exit(m.Run())
}
-120
View File
@@ -1,120 +0,0 @@
package e2e
import (
"context"
"fmt"
"os"
"path/filepath"
"syscall"
"testing"
"github.com/cline/cli/pkg/common"
)
// 9. Mixed localhost vs 127.0.0.1 addresses coexist and are both healthy
func TestMixedLocalhostVs127Coexist(t *testing.T) {
clineDir := setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start one instance
_ = mustRunCLI(ctx, t, "instance", "new")
// Get the running instance and its port/PID
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) == 0 {
t.Fatalf("expected at least 1 instance")
}
inst := out.CoreInstances[0]
waitForAddressHealthy(t, inst.CoreAddress, defaultTimeout)
// Manually add a SQLite entry for the same port but 127.0.0.1 host
addr127 := fmt.Sprintf("127.0.0.1:%d", inst.CorePort())
dbPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "locks.db")
if err := insertRemoteInstanceIntoSQLite(t, dbPath, addr127, inst.CorePort(), inst.HostPort()); err != nil {
t.Fatalf("insert 127 alias entry: %v", err)
}
// Verify both addresses appear and are healthy
waitForAddressHealthy(t, inst.CoreAddress, defaultTimeout)
waitForAddressHealthy(t, addr127, defaultTimeout)
out = listInstancesJSON(ctx, t)
if !hasAddress(out, inst.CoreAddress) || !hasAddress(out, addr127) {
t.Fatalf("expected both %s and %s present", inst.CoreAddress, addr127)
}
}
// 10. Start-stop stress: loop starting then killing instances; ensure no leftovers
func TestStartStopStress(t *testing.T) {
_ = setTempClineDir(t)
for i := 0; i < 3; i++ { // keep small for CI time
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Snapshot current addresses
before := listInstancesJSON(ctx, t)
beforeSet := map[string]struct{}{}
for _, it := range before.CoreInstances {
beforeSet[it.CoreAddress] = struct{}{}
}
// Start a new instance
_ = mustRunCLI(ctx, t, "instance", "new")
// Find the new instance address
var newAddr string
waitFor(t, defaultTimeout, func() (bool, string) {
after := listInstancesJSON(ctx, t)
for _, it := range after.CoreInstances {
if _, ok := beforeSet[it.CoreAddress]; !ok {
newAddr = it.CoreAddress
return true, ""
}
}
return false, "new instance address not detected yet"
})
// Wait healthy
waitForAddressHealthy(t, newAddr, defaultTimeout)
// Get PID using runtime discovery and kill it
after := listInstancesJSON(ctx, t)
info, ok := getByAddress(after, newAddr)
if !ok {
t.Fatalf("new instance %s missing", newAddr)
}
// Get PID using runtime discovery
corePID := getCorePID(t, info.CoreAddress)
if corePID <= 0 {
t.Fatalf("could not find PID for new instance at %s", info.CoreAddress)
}
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.CoreAddress, corePID, i)
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
t.Fatalf("kill pid %d: %v", corePID, err)
}
// Wait removed from SQLite database
waitForAddressRemoved(t, newAddr, longTimeout)
// Verify instance is removed from SQLite database
clineDir := os.Getenv("CLINE_DIR")
if clineDir != "" {
dbPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "locks.db")
if verifyInstanceExistsInSQLite(t, dbPath, newAddr) {
t.Fatalf("expected instance removed from SQLite database: %s", newAddr)
}
}
// Clean up dangling host process (SIGKILL leaves these behind by design)
t.Logf("Cleaning up dangling host process on port %d for iteration %d", info.HostPort(), i)
findAndKillHostProcess(t, info.HostPort())
// Verify both ports are now free
waitForPortsClosed(t, info.CorePort(), info.HostPort(), defaultTimeout)
}
}
-161
View File
@@ -1,161 +0,0 @@
package e2e
import (
"database/sql"
"encoding/json"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/cline/cli/pkg/common"
_ "github.com/glebarez/go-sqlite"
"google.golang.org/grpc/health/grpc_health_v1"
)
// readInstancesFromSQLite reads instances directly from the SQLite database for testing
func readInstancesFromSQLite(t *testing.T, clineDir string) []common.CoreInstanceInfo {
t.Helper()
dbPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "locks.db")
// Check if database exists
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return []common.CoreInstanceInfo{}
}
db, err := sql.Open("sqlite", dbPath)
if err != nil {
t.Logf("Warning: Failed to open SQLite database: %v", err)
return []common.CoreInstanceInfo{}
}
defer db.Close()
// Query instance locks
query := common.SelectInstanceLockHoldersAscSQL
rows, err := db.Query(query)
if err != nil {
t.Logf("Warning: Failed to query instance locks: %v", err)
return []common.CoreInstanceInfo{}
}
defer rows.Close()
var instances []common.CoreInstanceInfo
for rows.Next() {
var heldBy, lockTarget string
var lockedAt int64
err := rows.Scan(&heldBy, &lockTarget, &lockedAt)
if err != nil {
t.Logf("Warning: Failed to scan lock row: %v", err)
continue
}
// Create InstanceInfo
info := common.CoreInstanceInfo{
CoreAddress: heldBy,
HostServiceAddress: lockTarget,
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN, // Will be updated by health check
LastSeen: time.Unix(lockedAt/1000, 0), // Convert from milliseconds
}
instances = append(instances, info)
}
return instances
}
// readDefaultInstanceFromSettings reads the default instance from the settings file
func readDefaultInstanceFromSettings(t *testing.T, clineDir string) string {
t.Helper()
settingsPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
return ""
}
t.Logf("Warning: Failed to read default instance file: %v", err)
return ""
}
var tmp struct {
DefaultInstance string `json:"default_instance"`
}
if err := json.Unmarshal(data, &tmp); err != nil {
t.Logf("Warning: Failed to parse default instance file: %v", err)
return ""
}
return tmp.DefaultInstance
}
// insertRemoteInstanceIntoSQLite inserts a remote instance entry directly into SQLite for testing
func insertRemoteInstanceIntoSQLite(t *testing.T, dbPath, address string, corePort, hostPort int) error {
t.Helper()
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return err
}
defer db.Close()
// Initialize database schema for testing
createTableSQL := `
CREATE TABLE IF NOT EXISTS locks (
id INTEGER PRIMARY KEY,
held_by TEXT NOT NULL,
lock_type TEXT NOT NULL CHECK (lock_type IN ('file', 'instance', 'folder')),
lock_target TEXT NOT NULL,
locked_at INTEGER NOT NULL,
UNIQUE(lock_type, lock_target)
);
`
createIndexesSQL := `
CREATE INDEX IF NOT EXISTS idx_locks_held_by ON locks(held_by);
CREATE INDEX IF NOT EXISTS idx_locks_type ON locks(lock_type);
CREATE INDEX IF NOT EXISTS idx_locks_target ON locks(lock_target);
`
if _, err := db.Exec(createTableSQL); err != nil {
return err
}
if _, err := db.Exec(createIndexesSQL); err != nil {
return err
}
// Insert the remote instance
hostAddress := "remote.example.com:0"
if hostPort != 0 {
hostAddress = "remote.example.com:" + strconv.Itoa(hostPort)
}
insertSQL := `INSERT INTO locks (held_by, lock_type, lock_target, locked_at) VALUES (?, 'instance', ?, ?)`
_, err = db.Exec(insertSQL, address, hostAddress, time.Now().Unix()*1000)
return err
}
// verifyInstanceExistsInSQLite checks if an instance exists in the SQLite database
func verifyInstanceExistsInSQLite(t *testing.T, dbPath, address string) bool {
t.Helper()
db, err := sql.Open("sqlite", dbPath)
if err != nil {
t.Logf("Failed to open database: %v", err)
return false
}
defer db.Close()
query := `SELECT COUNT(*) FROM locks WHERE held_by = ? AND lock_type = 'instance'`
var count int
err = db.QueryRow(query, address).Scan(&count)
if err != nil {
t.Logf("Failed to query database: %v", err)
return false
}
return count > 0
}
-178
View File
@@ -1,178 +0,0 @@
package e2e
import (
"context"
"fmt"
"syscall"
"testing"
)
// TestStartAndList verifies self-registration and default.json semantics in a fresh CLINE_DIR.
func TestStartAndList(t *testing.T) {
clineDir := setTempClineDir(t)
t.Logf("Using temp CLINE_DIR: %s", clineDir)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
t.Logf("Starting new instance...")
// Start a new instance
startOutput := mustRunCLI(ctx, t, "instance", "new")
t.Logf("Instance start output: %s", startOutput)
t.Logf("Listing instances to check registration...")
// It should appear healthy in list JSON and be the default.
out := listInstancesJSON(ctx, t)
t.Logf("Found %d instances after start", len(out.CoreInstances))
if len(out.CoreInstances) != 1 {
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
}
addr := out.CoreInstances[0].CoreAddress
t.Logf("Instance address: %s, status: %s", addr, out.CoreInstances[0].Status)
t.Logf("Waiting for address %s to become healthy...", addr)
waitForAddressHealthy(t, addr, defaultTimeout)
t.Logf("Address %s is now healthy", addr)
t.Logf("Checking default instance configuration...")
// Default should be set to the new instance.
out = listInstancesJSON(ctx, t)
t.Logf("Default instance: %s", out.DefaultInstance)
if out.DefaultInstance == "" {
t.Fatalf("default_instance not set")
}
if out.DefaultInstance != out.CoreInstances[0].CoreAddress {
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].CoreAddress, out.DefaultInstance)
}
t.Logf("TestStartAndList completed successfully")
}
// TestTaskNewDefault ensures tasks route to default instance.
func TestTaskNewDefault(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start one instance and wait for healthy
_ = mustRunCLI(ctx, t, "instance", "new")
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) != 1 {
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
}
addr := out.CoreInstances[0].CoreAddress
waitForAddressHealthy(t, addr, defaultTimeout)
// Create a new task at default (success is sufficient)
_ = mustRunCLI(ctx, t, "task", "new", "hello world")
}
// TestExplicitAddressAutoStart verifies that giving an explicit address auto-starts an instance and routes the task.
func TestExplicitAddressAutoStart(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Find a free port and use explicit address. This should auto-start an instance.
port := findFreePort(t)
addr := "localhost:" + itoa(port)
// Run a task at explicit address (auto-start path)
_ = mustRunCLI(ctx, t, "task", "new", "--address", "localhost:"+itoa(port), "explicit address task")
// Verify the instance is present and healthy
waitForAddressHealthy(t, addr, defaultTimeout)
}
// TestCrashCleanup verifies that after SIGKILL of a local core, the cleanup removes the registry entry.
// Also tests graceful shutdown (SIGTERM) vs crash cleanup and ensures no dangling host processes.
func TestCrashCleanup(t *testing.T) {
_ = setTempClineDir(t)
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
defer cancel()
// Start two instances for testing both graceful and crash scenarios
_ = mustRunCLI(ctx, t, "instance", "new")
_ = mustRunCLI(ctx, t, "instance", "new")
out := listInstancesJSON(ctx, t)
if len(out.CoreInstances) < 2 {
t.Fatalf("expected at least 2 instances, got %d", len(out.CoreInstances))
}
// Test 1: Graceful shutdown (SIGTERM) - should clean up both processes
gracefulTarget := out.CoreInstances[0]
waitForAddressHealthy(t, gracefulTarget.CoreAddress, defaultTimeout)
// Get PID using runtime discovery
gracefulPID := getCorePID(t, gracefulTarget.CoreAddress)
if gracefulPID <= 0 {
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.CoreAddress)
}
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.CoreAddress, gracefulPID)
if err := syscall.Kill(gracefulPID, syscall.SIGTERM); err != nil {
t.Fatalf("kill SIGTERM pid %d: %v", gracefulPID, err)
}
// Wait for registry cleanup
waitForAddressRemoved(t, gracefulTarget.CoreAddress, longTimeout)
// Verify both core and host ports are freed (no dangling processes)
waitForPortsClosed(t, gracefulTarget.CorePort(), gracefulTarget.HostPort(), defaultTimeout)
// Verify the instance is removed from SQLite (no file to check anymore)
// The waitForAddressRemoved already confirms the instance is gone from the registry
// Test 2: Crash cleanup (SIGKILL) - creates dangling host process that we must clean up
crashTarget := out.CoreInstances[1]
waitForAddressHealthy(t, crashTarget.CoreAddress, defaultTimeout)
// Get PID using runtime discovery
crashPID := getCorePID(t, crashTarget.CoreAddress)
if crashPID <= 0 {
t.Fatalf("could not find PID for crash target at %s", crashTarget.CoreAddress)
}
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.CoreAddress, crashPID)
if err := syscall.Kill(crashPID, syscall.SIGKILL); err != nil {
t.Fatalf("kill SIGKILL pid %d: %v", crashPID, err)
}
// Wait for registry cleanup
waitForAddressRemoved(t, crashTarget.CoreAddress, longTimeout)
// Verify the instance is removed from SQLite (no file to check anymore)
// The waitForAddressRemoved already confirms the instance is gone from the registry
// Clean up dangling host process (SIGKILL leaves these behind by design)
t.Logf("Cleaning up dangling host process %s", crashTarget.HostServiceAddress)
findAndKillHostProcess(t, crashTarget.HostPort())
// Verify both ports are now free
waitForPortsClosed(t, crashTarget.CorePort(), crashTarget.HostPort(), defaultTimeout)
}
// itoa is a small helper for readability
func itoa(i int) string {
return strconvItoa(i)
}
// minimal inline int->string to avoid extra imports in helpers
func strconvItoa(i int) string {
// simple fast path
return fmtInt(i)
}
func fmtInt(i int) string {
// allocate small buffer; ints here are short
return (func(n int) string {
return fmt.Sprintf("%d", n)
})(i)
}
+273
View File
@@ -0,0 +1,273 @@
import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
import dotenv from "dotenv"
import * as esbuild from "esbuild"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.resolve(__dirname, "..")
// Load .env from repo root
dotenv.config({ path: path.join(rootDir, ".env") })
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
/**
* Plugin to resolve path aliases from the parent project
*/
const aliasResolverPlugin: esbuild.Plugin = {
name: "alias-resolver",
setup(build) {
const aliases = {
"@": path.resolve(rootDir, "src"),
"@core": path.resolve(rootDir, "src/core"),
"@integrations": path.resolve(rootDir, "src/integrations"),
"@services": path.resolve(rootDir, "src/services"),
"@shared": path.resolve(rootDir, "src/shared"),
"@utils": path.resolve(rootDir, "src/utils"),
"@packages": path.resolve(rootDir, "src/packages"),
"@hosts": path.resolve(rootDir, "src/hosts"),
"@generated": path.resolve(rootDir, "src/generated"),
"@api": path.resolve(rootDir, "src/core/api"),
}
// For each alias entry, create a resolver
Object.entries(aliases).forEach(([alias, aliasPath]) => {
const aliasRegex = new RegExp(`^${alias}($|/.*)`)
build.onResolve({ filter: aliasRegex }, (args) => {
const importPath = args.path.replace(alias, aliasPath)
// First, check if the path exists as is
if (fs.existsSync(importPath)) {
const stats = fs.statSync(importPath)
if (stats.isDirectory()) {
// If it's a directory, try to find index files
const extensions = [".ts", ".tsx", ".js", ".jsx"]
for (const ext of extensions) {
const indexFile = path.join(importPath, `index${ext}`)
if (fs.existsSync(indexFile)) {
return { path: indexFile }
}
}
} else {
// It's a file that exists, so return it
return { path: importPath }
}
}
// If the path doesn't exist, try appending extensions
const extensions = [".ts", ".tsx", ".js", ".jsx"]
for (const ext of extensions) {
const pathWithExtension = `${importPath}${ext}`
if (fs.existsSync(pathWithExtension)) {
return { path: pathWithExtension }
}
}
// Handle .js -> .ts extension mapping (common in ESM TypeScript projects)
if (importPath.endsWith(".js")) {
const tsPath = importPath.replace(/\.js$/, ".ts")
if (fs.existsSync(tsPath)) {
return { path: tsPath }
}
const tsxPath = importPath.replace(/\.js$/, ".tsx")
if (fs.existsSync(tsxPath)) {
return { path: tsxPath }
}
}
// If nothing worked, return the original path and let esbuild handle the error
return { path: importPath }
})
})
},
}
/**
* Plugin to redirect vscode imports to our shim
*/
const vscodeStubPlugin: esbuild.Plugin = {
name: "vscode-stub",
setup(build) {
// Redirect 'vscode' imports to our shim
build.onResolve({ filter: /^vscode$/ }, () => {
return { path: path.join(__dirname, "src", "vscode-shim.ts") }
})
},
}
const esbuildProblemMatcherPlugin: esbuild.Plugin = {
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[cli esbuild] Build started...")
})
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
if (location) {
console.error(` ${location.file}:${location.line}:${location.column}:`)
}
})
console.log("[cli esbuild] Build finished")
})
},
}
// Plugin to stub out optional devtools module
const stubOptionalModulesPlugin: esbuild.Plugin = {
name: "stub-optional-modules",
setup(build) {
build.onResolve({ filter: /^react-devtools-core$/ }, () => {
return { path: path.join(__dirname, "src", "stub-devtools.js"), external: false }
})
},
}
const copyWasmFiles: esbuild.Plugin = {
name: "copy-wasm-files",
setup(build) {
build.onEnd(() => {
const destDir = path.join(__dirname, "dist")
// Ensure dist directory exists
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
// tree sitter
const sourceDir = path.join(rootDir, "node_modules", "web-tree-sitter")
// Copy tree-sitter.wasm
const treeSitterWasm = path.join(sourceDir, "tree-sitter.wasm")
if (fs.existsSync(treeSitterWasm)) {
fs.copyFileSync(treeSitterWasm, path.join(destDir, "tree-sitter.wasm"))
}
// Copy language-specific WASM files
const languageWasmDir = path.join(rootDir, "node_modules", "tree-sitter-wasms", "out")
const languages = [
"typescript",
"tsx",
"python",
"rust",
"javascript",
"go",
"cpp",
"c",
"c_sharp",
"ruby",
"java",
"php",
"swift",
"kotlin",
]
if (fs.existsSync(languageWasmDir)) {
languages.forEach((lang) => {
const filename = `tree-sitter-${lang}.wasm`
const sourcePath = path.join(languageWasmDir, filename)
if (fs.existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, path.join(destDir, filename))
}
})
}
})
},
}
const buildEnvVars: Record<string, string> = {
"process.env.IS_STANDALONE": JSON.stringify("true"),
"process.env.IS_CLI": JSON.stringify("true"),
}
const buildTimeEnvs = [
"TELEMETRY_SERVICE_API_KEY",
"ERROR_SERVICE_API_KEY",
"POSTHOG_TELEMETRY_ENABLED",
"OTEL_TELEMETRY_ENABLED",
"OTEL_LOGS_EXPORTER",
"OTEL_METRICS_EXPORTER",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_METRIC_EXPORT_INTERVAL",
"CLINE_ENVIRONMENT",
]
buildTimeEnvs.forEach((envVar) => {
if (process.env[envVar]) {
console.log(`[cli esbuild] ${envVar} env var is set`)
buildEnvVars[`process.env.${envVar}`] = JSON.stringify(process.env[envVar])
}
})
if (production) {
buildEnvVars["process.env.IS_DEV"] = "false"
}
const config: esbuild.BuildOptions = {
entryPoints: [path.join(__dirname, "src", "index.ts")],
bundle: true,
minify: production,
sourcemap: !production,
logLevel: "silent",
define: buildEnvVars,
tsconfig: path.join(__dirname, "tsconfig.json"),
plugins: [copyWasmFiles, aliasResolverPlugin, vscodeStubPlugin, stubOptionalModulesPlugin, esbuildProblemMatcherPlugin],
format: "esm",
sourcesContent: false,
platform: "node",
target: "node20",
outfile: path.join(__dirname, "dist", "cli.mjs"),
// These modules need to load files from the module directory at runtime
external: [
"@grpc/reflection",
"grpc-health-check",
"better-sqlite3",
"ink",
"ink-spinner",
"ink-picture",
"react",
"aws4fetch",
"pino",
"pino-roll",
"@vscode/ripgrep", // Uses __dirname to locate the binary
],
supported: { "top-level-await": true },
banner: {
js: `#!/usr/bin/env node
// Suppress all Node.js warnings (deprecation, experimental, etc.)
process.emitWarning = () => {};
import { createRequire as _createRequire } from 'module';
import { fileURLToPath as _fileURLToPath } from 'url';
import { dirname as _dirname } from 'path';
const require = _createRequire(import.meta.url);
const __filename = _fileURLToPath(import.meta.url);
const __dirname = _dirname(__filename);`,
},
}
async function main() {
const ctx = await esbuild.context(config)
if (watch) {
await ctx.watch()
console.log("[cli] Watching for changes...")
} else {
await ctx.rebuild()
await ctx.dispose()
// Make the output executable
const outfile = path.join(__dirname, "dist", "cli.mjs")
if (fs.existsSync(outfile)) {
fs.chmodSync(outfile, "755")
}
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
-64
View File
@@ -1,64 +0,0 @@
module github.com/cline/cli
go 1.24.0
require (
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/huh v0.7.1-0.20251005153135-a01a1e304532
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/cline/grpc-go v0.0.0
github.com/glebarez/go-sqlite v1.22.0
github.com/muesli/termenv v0.16.0
github.com/spf13/cobra v1.8.0
golang.org/x/term v0.32.0
google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.6
)
replace github.com/cline/grpc-go => ../src/generated/grpc-go
require (
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
)
-162
View File
@@ -1,162 +0,0 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/huh v0.7.1-0.20251005153135-a01a1e304532 h1:+xmbw70JXxmsOqvm1PEIAqFnqI/Hy2RYqrK7CtPmsNY=
github.com/charmbracelet/huh v0.7.1-0.20251005153135-a01a1e304532/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
+342 -267
View File
@@ -1,318 +1,389 @@
.\" Automatically generated by Pandoc 3.8.2
.\" Automatically generated by Pandoc 3.8.3
.\"
.TH "CLINE" "1" "January 2025" "Cline CLI 1.0" "User Commands"
.TH "CLINE" "1" "January 2026" "Cline CLI 2.0" "User Commands"
.SH NAME
cline \- orchestrate and interact with Cline AI coding agents
cline \- AI coding assistant in your terminal
.SH SYNOPSIS
\f[B]cline\f[R] [\f[I]prompt\f[R]] [\f[I]options\f[R]]
.PP
\f[B]cline\f[R] \f[I]command\f[R] [\f[I]subcommand\f[R]]
[\f[I]options\f[R]] [\f[I]arguments\f[R]]
\f[B]cline\f[R] \f[I]command\f[R] [\f[I]options\f[R]]
[\f[I]arguments\f[R]]
.SH DESCRIPTION
Try: cat README.md | cline \(lqSummarize this for me:\(rq
\f[B]cline\f[R] is a command\-line interface for the Cline AI coding
assistant.
It provides the same powerful AI capabilities as the VS Code extension,
directly in your terminal.
.PP
\f[B]cline\f[R] is a command\-line interface for orchestrating multiple
Cline AI coding agents.
Cline is an autonomous AI agent who can read, write, and execute code
Cline is an autonomous AI agent that can read, write, and execute code
across your projects.
He operates through a client\-server architecture where \f[B]Cline
Core\f[R] runs as a standalone service, and the CLI acts as a scriptable
interface for managing tasks, instances, and agent interactions.
He can create and edit files, run terminal commands, use a headless
browser, and more\(emall while asking for your approval before taking
actions.
.PP
The CLI is designed for both interactive use and automation, making it
ideal for CI/CD pipelines, parallel task execution, and terminal\-based
workflows.
Multiple frontends (CLI, VSCode, JetBrains) can attach to the same Cline
Core instance, enabling seamless task handoff between environments.
The CLI supports both interactive mode (with a rich terminal UI) and
plain text mode (for piped input and scripted workflows).
.SH MODES OF OPERATION
.TP
\f[B]Instant Task Mode\f[R]
The simplest invocation: \f[B]cline \(lqprompt here\(rq\f[R] immediately
spawns an instance, creates a task, and enters chat mode.
This is equivalent to running \f[B]cline instance new && cline task new
&& cline task chat\f[R] in sequence.
.TP
\f[B]Subcommand Mode\f[R]
Advanced usage with explicit control: \f[B]cline <command> [subcommand]
[options]\f[R] provides fine\-grained control over instances, tasks,
authentication, and configuration.
\f[B]Interactive Mode\f[R] : When you run \f[B]cline\f[R] without
arguments, it launches an interactive welcome prompt with a rich
terminal UI.
You can type your task, view conversation history, and interact with
Cline in real\-time.
.PP
\f[B]Task Mode\f[R] : Run \f[B]cline \(lqprompt\(rq\f[R] or \f[B]cline
task \(lqprompt\(rq\f[R] to immediately start a task.
If stdin is a TTY, you\(cqll see the interactive UI.
If stdin is piped or output is redirected, the CLI automatically
switches to plain text mode.
.PP
\f[B]Plain Text Mode\f[R] : Activated automatically when stdin is piped,
output is redirected, or \f[B]\-\-json\f[R]/\f[B]\-\-yolo\f[R] flags are
used.
Outputs clean text without the Ink UI, suitable for scripting and CI/CD
pipelines.
.SH AGENT BEHAVIOR
Cline operates in two primary modes:
.TP
\f[B]ACT MODE\f[R]
Cline actively uses tools to accomplish tasks.
.PP
\f[B]ACT MODE\f[R] : Cline actively uses tools to accomplish tasks.
He can read files, write code, execute commands, use a headless browser,
and more.
This is the default mode for task execution.
.TP
\f[B]PLAN MODE\f[R]
Cline gathers information and creates a detailed plan before
implementation.
.PP
\f[B]PLAN MODE\f[R] : Cline gathers information and creates a detailed
plan before implementation.
He explores the codebase, asks clarifying questions, and presents a
strategy for user approval before switching to ACT MODE.
.SH INSTANT TASK OPTIONS
When using the instant task syntax \f[B]cline \(lqprompt\(rq\f[R] the
following options are available:
.TP
\f[B]\-o\f[R], \f[B]\-\-oneshot\f[R]
Full autonomous mode.
Cline completes the task and stops following after completion.
Example: cline \-o \(lqwhat\(cqs 6 + 8?\(rq
.TP
\f[B]\-s\f[R], \f[B]\-\-setting\f[R] \f[I]setting\f[R] \f[I]value\f[R]
Override a setting for this task
.TP
\f[B]\-y\f[R], \f[B]\-\-no\-interactive\f[R], \f[B]\-\-yolo\f[R]
Enable fully autonomous mode.
Disables all interactivity:
.RS
.IP \(bu 2
ask_followup_question tool is disabled
.IP \(bu 2
attempt_completion happens automatically
.IP \(bu 2
execute_command runs in non\-blocking mode with timeout
.IP \(bu 2
PLAN MODE automatically switches to ACT MODE
.RE
.TP
\f[B]\-m\f[R], \f[B]\-\-mode\f[R] \f[I]mode\f[R]
Starting mode.
Options: \f[B]act\f[R] (default), \f[B]plan\f[R]
.SH GLOBAL OPTIONS
These options apply to all subcommands:
.TP
\f[B]\-F\f[R], \f[B]\-\-output\-format\f[R] \f[I]format\f[R]
Output format.
Options: \f[B]rich\f[R] (default), \f[B]json\f[R], \f[B]plain\f[R]
.TP
\f[B]\-h\f[R], \f[B]\-\-help\f[R]
Display help information for the command.
.TP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R]
Enable verbose output for debugging.
.SH COMMANDS
.SS Authentication
\f[B]cline auth\f[R] [\f[I]provider\f[R]] [\f[I]key\f[R]]
.TP
\f[B]cline a\f[R] [\f[I]provider\f[R]] [\f[I]key\f[R]]
Configure authentication for AI model providers.
Launches an interactive wizard if no arguments provided.
If provider is specified without a key, prompts for the key or launches
the appropriate OAuth flow.
.SS Instance Management
Cline Core instances are independent agent processes that can run in the
background.
Multiple instances can run simultaneously, enabling parallel task
execution.
.SS task (alias: t)
Run a new task with a prompt.
.PP
\f[B]cline instance\f[R]
.TP
\f[B]cline i\f[R]
Display instance management help.
\f[B]cline task\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]]
.PP
\f[B]cline instance new\f[R] [\f[B]\-d\f[R]|\f[B]\-\-default\f[R]]
.TP
\f[B]cline i n\f[R] [\f[B]\-d\f[R]|\f[B]\-\-default\f[R]]
Spawn a new Cline Core instance.
Use \f[B]\-\-default\f[R] to set it as the default instance for
subsequent commands.
.PP
\f[B]cline instance list\f[R]
.TP
\f[B]cline i l\f[R]
List all running Cline Core instances with their addresses and status.
.PP
\f[B]cline instance default\f[R] \f[I]address\f[R]
.TP
\f[B]cline i d\f[R] \f[I]address\f[R]
Set the default instance to avoid specifying \f[B]\-\-address\f[R] in
task commands.
.PP
\f[B]cline instance kill\f[R] \f[I]address\f[R]
[\f[B]\-a\f[R]|\f[B]\-\-all\f[R]]
.TP
\f[B]cline i k\f[R] \f[I]address\f[R] [\f[B]\-a\f[R]|\f[B]\-\-all\f[R]]
Terminate a Cline Core instance.
Use \f[B]\-\-all\f[R] to kill all running instances.
.SS Task Management
Tasks represent individual work items that Cline executes.
Tasks maintain conversation history, checkpoints, and settings.
.PP
\f[B]cline task\f[R] [\f[B]\-a\f[R]|\f[B]\-\-address\f[R]
\f[I]ADDR\f[R]]
.TP
\f[B]cline t\f[R] [\f[B]\-a\f[R]|\f[B]\-\-address\f[R] \f[I]ADDR\f[R]]
Display task management help.
The \f[B]\-\-address\f[R] flag specifies which Cline Core instance to
use (e.g., localhost:50052).
.PP
\f[B]cline task new\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]]
.TP
\f[B]cline t n\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]]
Create a new task in the default or specified instance.
\f[B]cline t\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]] : Create and run
a new task.
Options:
.RS
.TP
\f[B]\-s\f[R], \f[B]\-\-setting\f[R] \f[I]setting\f[R] \f[I]value\f[R]
Set task\-specific settings
.TP
\f[B]\-y\f[R], \f[B]\-\-no\-interactive\f[R], \f[B]\-\-yolo\f[R]
Enable autonomous mode
.TP
\f[B]\-m\f[R], \f[B]\-\-mode\f[R] \f[I]mode\f[R]
Starting mode (act or plan)
.RE
.PP
\f[B]cline task open\f[R] \f[I]task\-id\f[R] [\f[I]options\f[R]]
.TP
\f[B]cline t o\f[R] \f[I]task\-id\f[R] [\f[I]options\f[R]]
Resume a previous task from history.
Accepts the same options as \f[B]task new\f[R].
\f[B]\-a\f[R], \f[B]\-\-act\f[R] : Run in act mode (default)
.PP
\f[B]cline task list\f[R]
.TP
\f[B]cline t l\f[R]
List all tasks in history with their id and snippet
\f[B]\-p\f[R], \f[B]\-\-plan\f[R] : Run in plan mode
.PP
\f[B]cline task chat\f[R]
.TP
\f[B]cline t c\f[R]
Enter interactive chat mode for the current task.
Allows back\-and\-forth conversation with Cline.
\f[B]\-y\f[R], \f[B]\-\-yolo\f[R] : Enable yolo/yes mode (auto\-approve
all actions, output in plain mode, exit process automatically when task
complete)
.PP
\f[B]cline task send\f[R] [\f[I]message\f[R]] [\f[I]options\f[R]]
.TP
\f[B]cline t s\f[R] [\f[I]message\f[R]] [\f[I]options\f[R]]
Send a message to Cline.
If no message is provided, reads from stdin.
\f[B]\-m\f[R], \f[B]\-\-model\f[R] \f[I]model\f[R] : Model to use for
the task
.PP
\f[B]\-i\f[R], \f[B]\-\-images\f[R] \f[I]paths\&...\f[R] : Image file
paths to include with the task
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output including
reasoning
.PP
\f[B]\-c\f[R], \f[B]\-\-cwd\f[R] \f[I]path\f[R] : Working directory for
the task
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.PP
\f[B]\-\-thinking\f[R] : Enable extended thinking (1024 token budget)
.PP
\f[B]\-\-json\f[R] : Output messages as JSON instead of styled text
.PP
\f[B]\-T\f[R], \f[B]\-\-taskId\f[R] \f[I]id\f[R] : Resume an existing
task by ID.
The prompt argument becomes an optional follow\-up message.
.SS history (alias: h)
List task history with pagination.
.PP
\f[B]cline history\f[R] [\f[I]options\f[R]]
.PP
\f[B]cline h\f[R] [\f[I]options\f[R]] : Display previous tasks.
Options:
.RS
.TP
\f[B]\-a\f[R], \f[B]\-\-approve\f[R]
Approve Cline\(cqs proposed action
.TP
\f[B]\-d\f[R], \f[B]\-\-deny\f[R]
Deny Cline\(cqs proposed action
.TP
\f[B]\-f\f[R], \f[B]\-\-file\f[R] \f[I]FILE\f[R]
Attach a file to the message
.TP
\f[B]\-y\f[R], \f[B]\-\-no\-interactive\f[R], \f[B]\-\-yolo\f[R]
Enable autonomous mode
.TP
\f[B]\-m\f[R], \f[B]\-\-mode\f[R] \f[I]mode\f[R]
Switch mode (act or plan)
.RE
.PP
\f[B]cline task view\f[R] [\f[B]\-f\f[R]|\f[B]\-\-follow\f[R]]
[\f[B]\-c\f[R]|\f[B]\-\-follow\-complete\f[R]]
.TP
\f[B]cline t v\f[R] [\f[B]\-f\f[R]|\f[B]\-\-follow\f[R]] [\f[B]\-c\f[R]|\f[B]\-\-follow\-complete\f[R]]
Display the current conversation.
Use \f[B]\-\-follow\f[R] to stream updates in real\-time, or
\f[B]\-\-follow\-complete\f[R] to follow until task completion.
\f[B]\-n\f[R], \f[B]\-\-limit\f[R] \f[I]number\f[R] : Number of tasks to
show (default: 10)
.PP
\f[B]cline task restore\f[R] \f[I]checkpoint\f[R]
.TP
\f[B]cline t r\f[R] \f[I]checkpoint\f[R]
Restore the task to a previous checkpoint state.
\f[B]\-p\f[R], \f[B]\-\-page\f[R] \f[I]number\f[R] : Page number,
1\-based (default: 1)
.PP
\f[B]cline task pause\f[R]
.TP
\f[B]cline t p\f[R]
Pause task execution.
.SS Configuration
Configuration can be set globally.
Override these global settings for a task using the
\f[B]\-\-setting\f[R] flag
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.SS config
Show current configuration.
.PP
\f[B]cline config\f[R]
\f[B]cline config\f[R] [\f[I]options\f[R]] : Display global and
workspace state.
Options:
.PP
\f[B]cline c\f[R]
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.SS auth
Authenticate a provider and configure the model.
.PP
\f[B]cline config set\f[R] \f[I]key\f[R] \f[I]value\f[R]
.TP
\f[B]cline c s\f[R] \f[I]key\f[R] \f[I]value\f[R]
Set a configuration variable.
\f[B]cline auth\f[R] [\f[I]options\f[R]] : Launch interactive
authentication wizard, or use quick setup flags.
Options:
.PP
\f[B]cline config get\f[R] \f[I]key\f[R]
.TP
\f[B]cline c g\f[R] \f[I]key\f[R]
Read a configuration variable.
\f[B]\-p\f[R], \f[B]\-\-provider\f[R] \f[I]id\f[R] : Provider ID for
quick setup (e.g., openai\-native, anthropic, openrouter)
.PP
\f[B]cline config list\f[R]
.TP
\f[B]cline c l\f[R]
List all configuration variables and their values.
.SH TASK SETTINGS
Task settings are persisted in the \f[I]\(ti/.cline/x/tasks\f[R]
directory.
When resuming a task with \f[B]cline task open\f[R], task settings are
automatically restored.
\f[B]\-k\f[R], \f[B]\-\-apikey\f[R] \f[I]key\f[R] : API key for the
provider
.PP
Common settings include:
.TP
\f[B]yolo\f[R]
Enable autonomous mode (true/false)
.TP
\f[B]mode\f[R]
Starting mode (act/plan)
.SH NOTES & EXAMPLES
The \f[B]cline task send\f[R] and \f[B]cline task new\f[R] commands
support reading from stdin, enabling powerful pipeline compositions:
\f[B]\-m\f[R], \f[B]\-\-modelid\f[R] \f[I]id\f[R] : Model ID to
configure (e.g., gpt\-4o, claude\-sonnet\-4\-5\-20250929)
.PP
\f[B]\-b\f[R], \f[B]\-\-baseurl\f[R] \f[I]url\f[R] : Base URL (optional,
for OpenAI\-compatible providers)
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output
.PP
\f[B]\-c\f[R], \f[B]\-\-cwd\f[R] \f[I]path\f[R] : Working directory
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.SS update
Check for updates and install if available.
.PP
\f[B]cline update\f[R] [\f[I]options\f[R]] : Check npm for newer
versions.
Options:
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output
.SS version
Show the CLI version number.
.PP
\f[B]cline version\f[R]
.SS dev
Developer tools and utilities.
.PP
\f[B]cline dev log\f[R] : Open the log file for debugging.
.SH DEFAULT COMMAND OPTIONS
When running \f[B]cline\f[R] with just a prompt (no subcommand), these
options are available:
.PP
\f[B]\-a\f[R], \f[B]\-\-act\f[R] : Run in act mode (default)
.PP
\f[B]\-p\f[R], \f[B]\-\-plan\f[R] : Run in plan mode
.PP
\f[B]\-y\f[R], \f[B]\-\-yolo\f[R] : Enable yolo mode (auto\-approve all
actions).
Also forces plain text output mode.
.PP
\f[B]\-m\f[R], \f[B]\-\-model\f[R] \f[I]model\f[R] : Model to use for
the task
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output
.PP
\f[B]\-c\f[R], \f[B]\-\-cwd\f[R] \f[I]path\f[R] : Working directory
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Configuration directory
.PP
\f[B]\-\-thinking\f[R] : Enable extended thinking (1024 token budget)
.PP
\f[B]\-\-json\f[R] : Output messages as JSON instead of styled text.
Forces plain text mode.
.PP
\f[B]\-T\f[R], \f[B]\-\-taskId\f[R] \f[I]id\f[R] : Resume an existing
task by ID instead of starting a new one.
The prompt becomes an optional follow\-up message.
.SH JSON OUTPUT FORMAT
When using \f[B]\-\-json\f[R], each message is output as a JSON object
with these fields:
.PP
\f[B]Required fields:\f[R]
.IP \(bu 2
\f[B]type\f[R]: \(lqask\(rq or \(lqsay\(rq
.IP \(bu 2
\f[B]text\f[R]: message text
.IP \(bu 2
\f[B]ts\f[R]: Unix epoch timestamp in milliseconds
.PP
\f[B]Optional fields:\f[R]
.IP \(bu 2
\f[B]reasoning\f[R]: reasoning text
.IP \(bu 2
\f[B]say\f[R]: say subtype (when type is \(lqsay\(rq)
.IP \(bu 2
\f[B]ask\f[R]: ask subtype (when type is \(lqask\(rq)
.IP \(bu 2
\f[B]partial\f[R]: streaming flag
.IP \(bu 2
\f[B]images\f[R]: list of image URIs
.IP \(bu 2
\f[B]files\f[R]: list of file paths
.SH EXAMPLES
.SS Basic Usage
.IP
.EX
cat requirements.txt \f[B]|\f[R] cline task send
echo \(dqRefactor this code\(dq \f[B]|\f[R] cline \-y
\f[I]# Launch interactive mode\f[R]
cline
\f[I]# Run a task directly\f[R]
cline \(dqCreate a hello world function in Python\(dq
\f[I]# Run with verbose output and extended thinking\f[R]
cline \-v \-\-thinking \(dqAnalyze this codebase architecture\(dq
.EE
.SS Instance Management
Manage multiple Cline instances:
.SS Mode Selection
.IP
.EX
\f[I]# Start a new instance and make it default\f[R]
cline instance new \-\-default
\f[I]# Run in plan mode (gather info before acting)\f[R]
cline \-p \(dqDesign a REST API for user management\(dq
\f[I]# List all running instances\f[R]
cline instance list
\f[I]# Run in act mode with auto\-approval (yolo)\f[R]
cline \-y \(dqFix the typo in README.md\(dq
.EE
.SS Using Specific Models
.IP
.EX
\f[I]# Use a specific model\f[R]
cline \-m claude\-sonnet\-4\-5\-20250929 \(dqRefactor this function\(dq
\f[I]# Kill a specific instance\f[R]
cline instance kill localhost:50052
\f[I]# Quick auth setup with model\f[R]
cline auth \-p anthropic \-k sk\-ant\-xxxxx \-m claude\-sonnet\-4\-5\-20250929
.EE
.SS Including Images
.IP
.EX
\f[I]# Include images with explicit flag\f[R]
cline task \-i screenshot.png diagram.jpg \(dqFix the UI based on these images\(dq
\f[I]# Kill all CLI instances\f[R]
cline instance kill \-\-all\-cli
\f[I]# Or use inline image references in the prompt\f[R]
cline \(dqFix the layout shown in \(at./screenshot.png\(dq
.EE
.SS Piped Input
.IP
.EX
\f[I]# Pipe file contents to Cline\f[R]
cat README.md \f[B]|\f[R] cline \(dqSummarize this document\(dq
\f[I]# Pipe with additional prompt\f[R]
echo \(dqfunction add(a, b) { return a + b }\(dq \f[B]|\f[R] cline \(dqAdd TypeScript types to this\(dq
\f[I]# Combine piped input with a prompt\f[R]
git diff \f[B]|\f[R] cline \(dqReview these changes and suggest improvements\(dq
.EE
.SS Scripting and Automation
.IP
.EX
\f[I]# JSON output for parsing\f[R]
cline \-\-json \(dqWhat files are in this directory?\(dq \f[B]|\f[R] jq \(aq.text\(aq
\f[I]# Yolo mode for automated workflows (auto\-approves all actions), forces plain text output\f[R]
cline \-y \(dqRun the test suite and fix any failures\(dq
.EE
.SS Task History
Work with task history:
.IP
.EX
\f[I]# List previous tasks\f[R]
cline task list
\f[I]# List recent tasks\f[R]
cline history
\f[I]# Resume a previous task\f[R]
cline task open 1760501486669
\f[I]# View conversation history\f[R]
cline task view
\f[I]# Start interactive chat with this task\f[R]
cline task chat
\f[I]# Show more tasks with pagination\f[R]
cline history \-n 20 \-p 2
.EE
.SH ARCHITECTURE
Cline operates on a three\-layer architecture:
.TP
\f[B]Presentation Layer\f[R]
User interfaces (CLI, VSCode, JetBrains) that connect to Cline Core via
gRPC
.TP
\f[B]Cline Core\f[R]
The autonomous agent service handling task management, AI model
integration, state management, tool orchestration, and real\-time
streaming updates
.TP
\f[B]Host Provider Layer\f[R]
Environment\-specific integrations (VSCode APIs, JetBrains APIs, shell
APIs) that Cline Core uses to interact with the host system
.SS Resuming Tasks
.IP
.EX
\f[I]# Resume a task by ID (get IDs from cline history)\f[R]
cline \-T abc123def
\f[I]# Resume a task with a follow\-up message\f[R]
cline \-T abc123def \(dqNow add unit tests for the changes\(dq
\f[I]# Resume in plan mode to review before continuing\f[R]
cline \-T abc123def \-p \(dqWhat\(aqs left to do?\(dq
\f[I]# Resume with yolo mode for automated continuation\f[R]
cline \-T abc123def \-y \(dqContinue with the implementation\(dq
.EE
.SS Authentication
.IP
.EX
\f[I]# Interactive authentication wizard\f[R]
cline auth
\f[I]# Quick setup for Anthropic\f[R]
cline auth \-p anthropic \-k sk\-ant\-api\-xxxxx
\f[I]# Quick setup for OpenAI\f[R]
cline auth \-p openai\-native \-k sk\-xxxxx \-m gpt\-4o
\f[I]# OpenAI\-compatible provider with custom base URL\f[R]
cline auth \-p openai \-k your\-api\-key \-b https://api.example.com/v1
.EE
.SH ENVIRONMENT
\f[B]CLINE_DIR\f[R] : Override the default configuration directory.
When set, Cline stores all data in this directory instead of
\f[CR]\(ti/.cline/data/\f[R].
.PP
\f[B]CLINE_COMMAND_PERMISSIONS\f[R] : JSON configuration for restricting
which shell commands Cline can execute.
When set, commands are validated against allow/deny patternks before
execution.
When not set, all commands are allowed.
.PP
Format:
\f[CR]{\(dqallow\(dq: [\(dqpattern1\(dq, \(dqpattern2\(dq], \(dqdeny\(dq: [\(dqpattern3\(dq], \(dqallowRedirects\(dq: true}\f[R]
.PP
\f[B]Fields:\f[R]
.IP \(bu 2
\f[B]allow\f[R] (array of strings): Glob patterns for allowed commands.
If specified, only matching commands are permitted.
Uses \f[CR]*\f[R] to match any characters and \f[CR]?\f[R] to match a
single character.
Setting allow on anything will deny all others.
.IP \(bu 2
\f[B]deny\f[R] (array of strings): Glob patterns for denied commands.
Deny rules take precedence over allow rules.
.IP \(bu 2
\f[B]allowRedirects\f[R] (boolean): Whether to allow shell redirects
(\f[CR]>\f[R], \f[CR]>>\f[R], \f[CR]<\f[R], etc.).
Defaults to false.
.PP
\f[B]Rule evaluation:\f[R]
.IP "1." 3
Check for dangerous characters (backticks outside single quotes,
unquoted newlines)
.IP "2." 3
Parse command into segments split by operators (\f[CR]&&\f[R],
\f[CR]||\f[R], \f[CR]|\f[R], \f[CR];\f[R])
.IP "3." 3
If redirects detected and \f[CR]allowRedirects\f[R] is not true, command
is denied
.IP "4." 3
Each segment is validated against deny rules first, then allow rules
.IP "5." 3
Subshell contents (\f[CR]$(...)\f[R] and \f[CR](...)\f[R]) are
recursively validated
.IP "6." 3
All segments must pass for the command to be allowed
.PP
\f[B]Examples:\f[R]
.IP
.EX
\f[I]# Allow only npm and git commands.\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqnpm *\(dq, \(dqgit *\(dq]}\(aq
\f[I]# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqnpm *\(dq, \(dqgit *\(dq, \(dqnode *\(dq], \(dqdeny\(dq: [\(dqrm \-rf *\(dq, \(dqsudo *\(dq]}\(aq
\f[I]# Allow file operations with redirects\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqcat *\(dq, \(dqecho *\(dq], \(dqallowRedirects\(dq: true}\(aq
.EE
.SH CONFIGURATION FILES
.IP
.EX
\(ti/.cline/
├── data/ # Default configuration directory
│ ├── globalState.json # Global settings and state
│ ├── secrets.json # API keys and secrets (stored securely)
│ ├── workspace/ # Workspace\-specific state
│ └── tasks/ # Task history and conversation data
└── log/ # Log files for debugging
.EE
.PP
View logs with \f[CR]cline dev log\f[R].
.SH BUGS
Report bugs at: \c
.UR https://github.com/cline/cline/issues
@@ -325,7 +396,11 @@ For real\-time help, join the Discord community at: \c
Full documentation: \c
.UR https://docs.cline.bot
.UE \c
.PP
VS Code extension: \c
.UR https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev
.UE \c
.SH AUTHORS
Cline is developed by the Cline Bot Inc.\ and the open source community.
Cline is developed by Cline Bot Inc.\ and the open source community.
.SH COPYRIGHT
Copyright © 2025 Cline Bot Inc.\ Licensed under the Apache License 2.0.
+222 -220
View File
@@ -2,342 +2,342 @@
title: CLINE
section: 1
header: User Commands
footer: Cline CLI 1.0
date: January 2025
footer: Cline CLI 2.0
date: January 2026
---
# NAME
cline - orchestrate and interact with Cline AI coding agents
cline - AI coding assistant in your terminal
# SYNOPSIS
**cline** [*prompt*] [*options*]
**cline** *command* [*subcommand*] [*options*] [*arguments*]
**cline** *command* [*options*] [*arguments*]
# DESCRIPTION
Try: cat README.md | cline "Summarize this for me:"
**cline** is a command-line interface for the Cline AI coding assistant. It provides the same powerful AI capabilities as the VS Code extension, directly in your terminal.
**cline** is a command-line interface for orchestrating multiple Cline AI coding agents. Cline is an autonomous AI agent who can read, write, and execute code across your projects. He operates through a client-server architecture where **Cline Core** runs as a standalone service, and the CLI acts as a scriptable interface for managing tasks, instances, and agent interactions.
Cline is an autonomous AI agent that can read, write, and execute code across your projects. He can create and edit files, run terminal commands, use a headless browser, and more—all while asking for your approval before taking actions.
The CLI is designed for both interactive use and automation, making it ideal for CI/CD pipelines, parallel task execution, and terminal-based workflows. Multiple frontends (CLI, VSCode, JetBrains) can attach to the same Cline Core instance, enabling seamless task handoff between environments.
The CLI supports both interactive mode (with a rich terminal UI) and plain text mode (for piped input and scripted workflows).
# MODES OF OPERATION
**Instant Task Mode**
**Interactive Mode** : When you run **cline** without arguments, it launches an interactive welcome prompt with a rich terminal UI. You can type your task, view conversation history, and interact with Cline in real-time.
: The simplest invocation: **cline "prompt here"** immediately spawns an instance, creates a task, and enters chat mode. This is equivalent to running **cline instance new && cline task new && cline task chat** in sequence.
**Task Mode** : Run **cline "prompt"** or **cline task "prompt"** to immediately start a task. If stdin is a TTY, you'll see the interactive UI. If stdin is piped or output is redirected, the CLI automatically switches to plain text mode.
**Subcommand Mode**
: Advanced usage with explicit control: **cline \<command\> [subcommand] [options]** provides fine-grained control over instances, tasks, authentication, and configuration.
**Plain Text Mode** : Activated automatically when stdin is piped, output is redirected, or **\--json**/**\--yolo** flags are used. Outputs clean text without the Ink UI, suitable for scripting and CI/CD pipelines.
# AGENT BEHAVIOR
Cline operates in two primary modes:
**ACT MODE**
**ACT MODE** : Cline actively uses tools to accomplish tasks. He can read files, write code, execute commands, use a headless browser, and more. This is the default mode for task execution.
: Cline actively uses tools to accomplish tasks. He can read files, write code, execute commands, use a headless browser, and more. This is the default mode for task execution.
**PLAN MODE**
: Cline gathers information and creates a detailed plan before implementation. He explores the codebase, asks clarifying questions, and presents a strategy for user approval before switching to ACT MODE.
# INSTANT TASK OPTIONS
When using the instant task syntax **cline "prompt"** the following options are available:
**-o**, **\--oneshot**
: Full autonomous mode. Cline completes the task and stops following after completion. Example: cline -o "what's 6 + 8?"
**-s**, **\--setting** *setting* *value*
: Override a setting for this task
**-y**, **\--no-interactive**, **\--yolo**
: Enable fully autonomous mode. Disables all interactivity:
- ask_followup_question tool is disabled
- attempt_completion happens automatically
- execute_command runs in non-blocking mode with timeout
- PLAN MODE automatically switches to ACT MODE
**-m**, **\--mode** *mode*
: Starting mode. Options: **act** (default), **plan**
**-w**, **\--workspace** *path*
: Additional workspace paths. Can be specified multiple times to include multiple directories. The current working directory is always included as the first workspace. Example: cline -w /path/to/other/project "refactor shared code"
# GLOBAL OPTIONS
These options apply to all subcommands:
**-F**, **\--output-format** *format*
: Output format. Options: **rich** (default), **json**, **plain**
When you use **-F json**, the CLI prints each client message as JSON.
Each message is a **ClineMessage** object.
Required fields:
- **type**: "ask" or "say"
- **text**: message text
- **ts**: Unix epoch timestamp in milliseconds
Optional fields (omitted when empty):
- **reasoning**: reasoning text
- **say**: say subtype (present when type is "say")
- **ask**: ask subtype (present when type is "ask")
- **partial**: streaming flag
- **images**: list of image URIs
- **files**: list of file paths
- **lastCheckpointHash**: git checkpoint hash
- **isCheckpointCheckedOut**: checkpoint checkout flag
- **isOperationOutsideWorkspace**: workspace safety flag
**-h**, **\--help**
: Display help information for the command.
**-v**, **\--verbose**
: Enable verbose output for debugging.
**PLAN MODE** : Cline gathers information and creates a detailed plan before implementation. He explores the codebase, asks clarifying questions, and presents a strategy for user approval before switching to ACT MODE.
# COMMANDS
## Authentication
## task (alias: t)
**cline auth** [*provider*] [*key*]
Run a new task with a prompt.
**cline a** [*provider*] [*key*]
**cline task** *prompt* [*options*]
: Configure authentication for AI model providers. Launches an interactive wizard if no arguments provided. If provider is specified without a key, prompts for the key or launches the appropriate OAuth flow.
**cline t** *prompt* [*options*] : Create and run a new task. Options:
## Instance Management
**-a**, **\--act** : Run in act mode (default)
Cline Core instances are independent agent processes that can run in the background. Multiple instances can run simultaneously, enabling parallel task execution.
**-p**, **\--plan** : Run in plan mode
**cline instance**
**-y**, **\--yolo** : Enable yolo/yes mode (auto-approve all actions, output in plain mode, exit process automatically when task complete)
**cline i**
**-m**, **\--model** *model* : Model to use for the task
: Display instance management help.
**-i**, **\--images** *paths...* : Image file paths to include with the task
**cline instance new** [**-d**|**\--default**]
**-v**, **\--verbose** : Show verbose output including reasoning
**cline i n** [**-d**|**\--default**]
**-c**, **\--cwd** *path* : Working directory for the task
: Spawn a new Cline Core instance. Use **\--default** to set it as the default instance for subsequent commands.
**\--config** *path* : Path to Cline configuration directory
**cline instance list**
**\--thinking** : Enable extended thinking (1024 token budget)
**cline i l**
**\--json** : Output messages as JSON instead of styled text
: List all running Cline Core instances with their addresses and status.
**-T**, **\--taskId** *id* : Resume an existing task by ID. The prompt argument becomes an optional follow-up message.
**cline instance default** *address*
## history (alias: h)
**cline i d** *address*
List task history with pagination.
: Set the default instance to avoid specifying **\--address** in task commands.
**cline history** [*options*]
**cline instance kill** *address* [**-a**|**\--all**]
**cline h** [*options*] : Display previous tasks. Options:
**cline i k** *address* [**-a**|**\--all**]
**-n**, **\--limit** *number* : Number of tasks to show (default: 10)
: Terminate a Cline Core instance. Use **\--all** to kill all running instances.
**-p**, **\--page** *number* : Page number, 1-based (default: 1)
## Task Management
**\--config** *path* : Path to Cline configuration directory
Tasks represent individual work items that Cline executes. Tasks maintain conversation history, checkpoints, and settings.
## config
**cline task** [**-a**|**\--address** *ADDR*]
Show current configuration.
**cline t** [**-a**|**\--address** *ADDR*]
**cline config** [*options*] : Display global and workspace state. Options:
: Display task management help. The **\--address** flag specifies which Cline Core instance to use (e.g., localhost:50052).
**\--config** *path* : Path to Cline configuration directory
**cline task new** *prompt* [*options*]
## auth
**cline t n** *prompt* [*options*]
Authenticate a provider and configure the model.
: Create a new task in the default or specified instance. Options:
**cline auth** [*options*] : Launch interactive authentication wizard, or use quick setup flags. Options:
**-s**, **\--setting** *setting* *value*
: Set task-specific settings
**-p**, **\--provider** *id* : Provider ID for quick setup (e.g., openai-native, anthropic, openrouter)
**-y**, **\--no-interactive**, **\--yolo**
: Enable autonomous mode
**-k**, **\--apikey** *key* : API key for the provider
**-m**, **\--mode** *mode*
: Starting mode (act or plan)
**-m**, **\--modelid** *id* : Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)
**cline task open** *task-id* [*options*]
**-b**, **\--baseurl** *url* : Base URL (optional, for OpenAI-compatible providers)
**cline t o** *task-id* [*options*]
**-v**, **\--verbose** : Show verbose output
: Resume a previous task from history. Accepts the same options as **task new**.
**-c**, **\--cwd** *path* : Working directory
**cline task list**
**\--config** *path* : Path to Cline configuration directory
**cline t l**
## update
: List all tasks in history with their id and snippet
Check for updates and install if available.
**cline task chat**
**cline update** [*options*] : Check npm for newer versions. Options:
**cline t c**
**-v**, **\--verbose** : Show verbose output
: Enter interactive chat mode for the current task. Allows back-and-forth conversation with Cline.
## version
**cline task send** [*message*] [*options*]
Show the CLI version number.
**cline t s** [*message*] [*options*]
**cline version**
: Send a message to Cline. If no message is provided, reads from stdin. Options:
## dev
**-a**, **\--approve**
: Approve Cline's proposed action
Developer tools and utilities.
**-d**, **\--deny**
: Deny Cline's proposed action
**cline dev log** : Open the log file for debugging.
**-f**, **\--file** *FILE*
: Attach a file to the message
# DEFAULT COMMAND OPTIONS
**-y**, **\--no-interactive**, **\--yolo**
: Enable autonomous mode
When running **cline** with just a prompt (no subcommand), these options are available:
**-m**, **\--mode** *mode*
: Switch mode (act or plan)
**-a**, **\--act** : Run in act mode (default)
**cline task view** [**-f**|**\--follow**] [**-c**|**\--follow-complete**]
**-p**, **\--plan** : Run in plan mode
**cline t v** [**-f**|**\--follow**] [**-c**|**\--follow-complete**]
**-y**, **\--yolo** : Enable yolo mode (auto-approve all actions). Also forces plain text output mode.
: Display the current conversation. Use **\--follow** to stream updates in real-time, or **\--follow-complete** to follow until task completion.
**-m**, **\--model** *model* : Model to use for the task
**cline task restore** *checkpoint*
**-v**, **\--verbose** : Show verbose output
**cline t r** *checkpoint*
**-c**, **\--cwd** *path* : Working directory
: Restore the task to a previous checkpoint state.
**\--config** *path* : Configuration directory
**cline task pause**
**\--thinking** : Enable extended thinking (1024 token budget)
**cline t p**
**\--json** : Output messages as JSON instead of styled text. Forces plain text mode.
: Pause task execution.
**-T**, **\--taskId** *id* : Resume an existing task by ID instead of starting a new one. The prompt becomes an optional follow-up message.
## Configuration
# JSON OUTPUT FORMAT
Configuration can be set globally. Override these global settings for a task using the **\--setting** flag
When using **\--json**, each message is output as a JSON object with these fields:
**cline config**
**Required fields:**
**cline c**
- **type**: "ask" or "say"
- **text**: message text
- **ts**: Unix epoch timestamp in milliseconds
**cline config set** *key* *value*
**Optional fields:**
**cline c s** *key* *value*
- **reasoning**: reasoning text
- **say**: say subtype (when type is "say")
- **ask**: ask subtype (when type is "ask")
- **partial**: streaming flag
- **images**: list of image URIs
- **files**: list of file paths
: Set a configuration variable.
# EXAMPLES
**cline config get** *key*
**cline c g** *key*
: Read a configuration variable.
**cline config list**
**cline c l**
: List all configuration variables and their values.
# TASK SETTINGS
Task settings are persisted in the *~/.cline/x/tasks* directory. When resuming a task with **cline task open**, task settings are automatically restored.
Common settings include:
**yolo**
: Enable autonomous mode (true/false)
**mode**
: Starting mode (act/plan)
# NOTES & EXAMPLES
The **cline task send** and **cline task new** commands support reading from stdin, enabling powerful pipeline compositions:
## Basic Usage
```bash
cat requirements.txt | cline task send
echo "Refactor this code" | cline -y
# Launch interactive mode
cline
# Run a task directly
cline "Create a hello world function in Python"
# Run with verbose output and extended thinking
cline -v --thinking "Analyze this codebase architecture"
```
## Instance Management
Manage multiple Cline instances:
## Mode Selection
```bash
# Start a new instance and make it default
cline instance new --default
# Run in plan mode (gather info before acting)
cline -p "Design a REST API for user management"
# List all running instances
cline instance list
# Run in act mode with auto-approval (yolo)
cline -y "Fix the typo in README.md"
```
# Kill a specific instance
cline instance kill localhost:50052
## Using Specific Models
# Kill all CLI instances
cline instance kill --all-cli
```bash
# Use a specific model
cline -m claude-sonnet-4-5-20250929 "Refactor this function"
# Quick auth setup with model
cline auth -p anthropic -k sk-ant-xxxxx -m claude-sonnet-4-5-20250929
```
## Including Images
```bash
# Include images with explicit flag
cline task -i screenshot.png diagram.jpg "Fix the UI based on these images"
# Or use inline image references in the prompt
cline "Fix the layout shown in @./screenshot.png"
```
## Piped Input
```bash
# Pipe file contents to Cline
cat README.md | cline "Summarize this document"
# Pipe with additional prompt
echo "function add(a, b) { return a + b }" | cline "Add TypeScript types to this"
# Combine piped input with a prompt
git diff | cline "Review these changes and suggest improvements"
```
## Scripting and Automation
```bash
# JSON output for parsing
cline --json "What files are in this directory?" | jq '.text'
# Yolo mode for automated workflows (auto-approves all actions), forces plain text output
cline -y "Run the test suite and fix any failures"
```
## Task History
Work with task history:
```bash
# List previous tasks
cline task list
# List recent tasks
cline history
# Resume a previous task
cline task open 1760501486669
# View conversation history
cline task view
# Start interactive chat with this task
cline task chat
# Show more tasks with pagination
cline history -n 20 -p 2
```
# ARCHITECTURE
## Resuming Tasks
Cline operates on a three-layer architecture:
```bash
# Resume a task by ID (get IDs from cline history)
cline -T abc123def
**Presentation Layer**
# Resume a task with a follow-up message
cline -T abc123def "Now add unit tests for the changes"
: User interfaces (CLI, VSCode, JetBrains) that connect to Cline Core via gRPC
# Resume in plan mode to review before continuing
cline -T abc123def -p "What's left to do?"
**Cline Core**
# Resume with yolo mode for automated continuation
cline -T abc123def -y "Continue with the implementation"
```
: The autonomous agent service handling task management, AI model integration, state management, tool orchestration, and real-time streaming updates
## Authentication
**Host Provider Layer**
```bash
# Interactive authentication wizard
cline auth
# Quick setup for Anthropic
cline auth -p anthropic -k sk-ant-api-xxxxx
# Quick setup for OpenAI
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
# OpenAI-compatible provider with custom base URL
cline auth -p openai -k your-api-key -b https://api.example.com/v1
```
# ENVIRONMENT
**CLINE_DIR** : Override the default configuration directory. When set, Cline stores all data in this directory instead of `~/.cline/data/`.
**CLINE_COMMAND_PERMISSIONS** : JSON configuration for restricting which shell commands Cline can execute. When set, commands are validated against allow/deny patternks before execution. When not set, all commands are allowed.
Format: `{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}`
**Fields:**
- **allow** (array of strings): Glob patterns for allowed commands. If specified, only matching commands are permitted. Uses `*` to match any characters and `?` to match a single character. Setting allow on anything will deny all others.
- **deny** (array of strings): Glob patterns for denied commands. Deny rules take precedence over allow rules.
- **allowRedirects** (boolean): Whether to allow shell redirects (`>`, `>>`, `<`, etc.). Defaults to false.
**Rule evaluation:**
1. Check for dangerous characters (backticks outside single quotes, unquoted newlines)
2. Parse command into segments split by operators (`&&`, `||`, `|`, `;`)
3. If redirects detected and `allowRedirects` is not true, command is denied
4. Each segment is validated against deny rules first, then allow rules
5. Subshell contents (`$(...)` and `(...)`) are recursively validated
6. All segments must pass for the command to be allowed
**Examples:**
```bash
# Allow only npm and git commands.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
# Allow file operations with redirects
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
```
# CONFIGURATION FILES
```
~/.cline/
├── data/ # Default configuration directory
│ ├── globalState.json # Global settings and state
│ ├── secrets.json # API keys and secrets (stored securely)
│ ├── workspace/ # Workspace-specific state
│ └── tasks/ # Task history and conversation data
└── log/ # Log files for debugging
```
View logs with `cline dev log`.
: Environment-specific integrations (VSCode APIs, JetBrains APIs, shell APIs) that Cline Core uses to interact with the host system
# BUGS
@@ -349,9 +349,11 @@ For real-time help, join the Discord community at: <https://discord.gg/cline>
Full documentation: <https://docs.cline.bot>
VS Code extension: <https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev>
# AUTHORS
Cline is developed by the Cline Bot Inc. and the open source community.
Cline is developed by Cline Bot Inc. and the open source community.
# COPYRIGHT
+53 -30
View File
@@ -1,27 +1,39 @@
{
"name": "cline",
"version": "1.0.9",
"version": "2.0.5",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "cline-core.js",
"main": "dist/cli.mjs",
"bin": {
"cline": "./bin/cline",
"cline-host": "./bin/cline-host"
"cline": "./dist/cli.mjs"
},
"man": "./man/cline.1",
"scripts": {
"postinstall": "node postinstall.js"
},
"bundleDependencies": [
"@grpc/grpc-js",
"@grpc/reflection",
"better-sqlite3",
"grpc-health-check",
"open",
"vscode-uri"
"os": [
"darwin",
"linux",
"win32"
],
"cpu": [
"x64",
"arm64"
],
"man": "./man/cline.1",
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"package:brew": "npx tsx ./scripts/update-brew-formula.mts",
"package": "npm pack --pack-destination ./dist",
"build": "npm run typecheck && npx tsx esbuild.mts",
"build:production": "npm run typecheck && npx tsx esbuild.mts --production",
"watch": "npx tsx esbuild.mts --watch",
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
"clean": "rimraf dist",
"typecheck": "npx tsc --noEmit",
"link": "npm run build && npm link",
"unlink": "npm unlink -g cline",
"test": "vitest",
"test:run": "vitest run"
},
"keywords": [
"cline",
"claude",
@@ -49,20 +61,31 @@
"bugs": {
"url": "https://github.com/cline/cline/issues"
},
"dependencies": {
"@grpc/grpc-js": "^1.13.3",
"@grpc/reflection": "^1.0.4",
"better-sqlite3": "^12.2.0",
"grpc-health-check": "^2.0.2",
"open": "^10.1.2",
"vscode-uri": "^3.1.0"
"devDependencies": {
"@types/node": "20.x",
"@types/prompts": "^2.4.9",
"@types/react": "^19.2.9",
"dotenv": "^16.4.5",
"esbuild": "^0.25.0",
"ink-testing-library": "^4.0.0",
"rimraf": "^6.0.1",
"typescript": "^5.4.5",
"vitest": "^4.0.17"
},
"os": [
"darwin",
"linux"
],
"cpu": [
"x64",
"arm64"
]
"dependencies": {
"@agentclientprotocol/sdk": "^0.13.1",
"@vscode/ripgrep": "^1.15.9",
"aws4fetch": "^1.0.20",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"ink": "npm:@jrichman/ink@6.4.7",
"ink-picture": "^1.3.3",
"ink-spinner": "^5.0.0",
"ora": "^8.0.1",
"nanoid": "^5.1.6",
"pino": "^10.0.0",
"pino-roll": "^4.0.0",
"prompts": "^2.4.2",
"react": "^19.2.3"
}
}
-43
View File
@@ -1,43 +0,0 @@
package cli
import (
"github.com/cline/cli/pkg/cli/auth"
"github.com/spf13/cobra"
)
func NewAuthCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Authenticate a provider and configure what model is used",
Long: `Authenticate a provider and configure what model is used
Interactive Mode:
Run without flags to open an interactive menu where you can:
- Sign in to your Cline account
- Configure other LLM providers (Anthropic, OpenAI, etc.)
- Select and switch between AI models
- Manage provider settings
Quick Setup Mode:
Use flags to quickly configure a BYO provider non-interactively:
Examples:
cline auth --provider openai-native --apikey sk-xxx --modelid gpt-5
cline auth -p anthropic -k sk-ant-xxx -m claude-sonnet-4-5-20250929
cline auth -p openai-compatible -k xxx -m gpt-4 -b https://api.example.com/v1
Supported providers: openai-native, openai, anthropic, gemini, openrouter, xai, cerebras, ollama
Note: Bedrock provider requires interactive setup due to complex auth fields`,
RunE: func(cmd *cobra.Command, args []string) error {
return auth.RunAuthFlow(cmd.Context(), args)
},
}
// Add flags for quick setup mode
cmd.Flags().StringVarP(&auth.QuickProvider, "provider", "p", "", "Provider ID for quick setup (e.g., openai-native, anthropic)")
cmd.Flags().StringVarP(&auth.QuickAPIKey, "apikey", "k", "", "API key for the provider")
cmd.Flags().StringVarP(&auth.QuickModelID, "modelid", "m", "", "Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)")
cmd.Flags().StringVarP(&auth.QuickBaseURL, "baseurl", "b", "", "Base URL (optional, only for openai provider)")
return cmd
}
-285
View File
@@ -1,285 +0,0 @@
package auth
import (
"context"
"fmt"
"time"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
var isSessionAuthenticated bool
// Cline provider specific code
func HandleClineAuth(ctx context.Context) error {
verboseLog("Authenticating with Cline...")
// Check if already authenticated
if IsAuthenticated(ctx) {
return signOutDialog(ctx)
}
// Perform sign in
if err := signIn(ctx); err != nil {
return err
}
fmt.Println()
verboseLog("✓ You are signed in!")
// Configure default Cline model after successful authentication
if err := configureDefaultClineModel(ctx); err != nil {
fmt.Printf("Warning: Could not configure default Cline model: %v\n", err)
fmt.Println("You can configure a model later with 'cline auth' and selecting 'Change Cline model'")
}
// Return to main auth menu after successful authentication
return HandleAuthMenuNoArgs(ctx)
}
func signOut(ctx context.Context) error {
client, err := global.GetDefaultClient(ctx)
if err != nil {
return err
}
if _, err = client.Account.AccountLogoutClicked(ctx, &cline.EmptyRequest{}); err != nil {
return err
}
isSessionAuthenticated = false
fmt.Println("You have been signed out of Cline.")
return nil
}
func signOutDialog(ctx context.Context) error {
var confirm bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("You are already signed in to Cline.").
Description("Would you like to sign out?").
Value(&confirm),
),
)
if err := form.Run(); err != nil {
return nil
}
if confirm {
if err := signOut(ctx); err != nil {
fmt.Printf("Failed to sign out: %v\n", err)
return err
}
}
return HandleAuthMenuNoArgs(ctx)
}
func signIn(ctx context.Context) error {
if IsAuthenticated(ctx) {
return nil
}
// Subscribe to auth updates before initiating login
verboseLog("Subscribing to auth status updates...")
listener, err := NewAuthStatusListener(ctx)
if err != nil {
verboseLog("Failed to subscribe to auth updates: %v", err)
return fmt.Errorf("failed to subscribe to auth updates: %w", err)
}
defer listener.Stop()
if err := listener.Start(); err != nil {
verboseLog("Failed to start auth listener: %v", err)
return fmt.Errorf("failed to start auth listener: %w", err)
}
// Initiate login (opens browser with callback URL from cline-core's AuthHandler)
verboseLog("Initiating login...")
client, err := global.GetDefaultClient(ctx)
if err != nil {
verboseLog("Failed to obtain client: %v", err)
return fmt.Errorf("failed to obtain client: %w", err)
}
response, err := client.Account.AccountLoginClicked(ctx, &cline.EmptyRequest{})
if err != nil {
verboseLog("Failed to initiate login: %v", err)
return fmt.Errorf("failed to initiate login: %w", err)
}
fmt.Println("\n Opening browser for authentication...")
if response != nil && response.Value != "" {
fmt.Printf(" If the browser doesn't open automatically, visit this URL:\n %s\n\n", response.Value)
}
fmt.Println(" Waiting for you to complete authentication in your browser...")
fmt.Println(" (This may take a few moments. Timeout: 5 minutes)")
// Wait for auth status update confirming success
verboseLog("Waiting for authentication to complete...")
if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
verboseLog("Authentication failed or timed out: %v", err)
fmt.Println("\n Authentication failed or timed out.")
fmt.Println(" Please try again with 'cline auth'")
return err
}
// Only NOW set the session flag after confirmed authentication
isSessionAuthenticated = true
verboseLog("Login successful")
return nil
}
func IsAuthenticated(ctx context.Context) bool {
if isSessionAuthenticated {
verboseLog("Session is already authenticated")
return true
}
verboseLog("Verifying authentication with server...")
client, err := global.GetDefaultClient(ctx)
if err != nil {
verboseLog("Failed to get client for auth check: %v", err)
return false
}
_, err = client.Account.GetUserCredits(ctx, &cline.EmptyRequest{})
if err == nil {
// Update session variable for future fast-path checks
verboseLog("Server verification successful, updating session flag")
isSessionAuthenticated = true
return true
}
verboseLog("Server verification failed: %v", err)
return false
}
// HandleChangeClineModel allows Cline-authenticated users to change their Cline model selection. Hidden when not authenticated.
func HandleChangeClineModel(ctx context.Context) error {
// Ensure user is authenticated
if !IsAuthenticated(ctx) {
return fmt.Errorf("you must be authenticated with Cline to change models. Run 'cline auth' to sign in")
}
// Get task manager
manager, err := createTaskManager(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Launch Cline model selection
return SelectClineModel(ctx, manager)
}
// configureDefaultClineModel configures the default Cline model after authentication
func configureDefaultClineModel(ctx context.Context) error {
verboseLog("Configuring default Cline model...")
// Create task manager
manager, err := task.NewManagerForDefault(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Set default Cline model
return SetDefaultClineModel(ctx, manager)
}
// HandleSelectOrganization allows Cline-authenticated users to select which organization to use
func HandleSelectOrganization(ctx context.Context) error {
// Ensure user is authenticated
if !IsAuthenticated(ctx) {
return fmt.Errorf("you must be authenticated with Cline to select an organization. Run 'cline auth' to sign in")
}
// Get client
client, err := global.GetDefaultClient(ctx)
if err != nil {
return fmt.Errorf("failed to get client: %w", err)
}
// Fetch user organizations
orgsResponse, err := client.Account.GetUserOrganizations(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to fetch organizations: %w", err)
}
organizations := orgsResponse.GetOrganizations()
if len(organizations) == 0 {
fmt.Println("You don't have any organizations yet.")
fmt.Println("Visit https://app.cline.bot/dashboard to create an organization.")
return HandleAuthMenuNoArgs(ctx)
}
// Build options list: Personal + Organizations
var options []huh.Option[string]
options = append(options, huh.NewOption("Personal", "personal"))
for _, org := range organizations {
displayName := org.Name
// Show active indicator
if org.Active {
displayName = fmt.Sprintf("%s (active)", displayName)
}
options = append(options, huh.NewOption(displayName, org.OrganizationId))
}
options = append(options, huh.NewOption("(Cancel)", "cancel"))
// Show selection menu
var selected string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select which account to use").
Options(options...).
Value(&selected),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("failed to select organization: %w", err)
}
if selected == "cancel" {
return HandleAuthMenuNoArgs(ctx)
}
// Set the organization
var orgId *string
if selected != "personal" {
orgId = &selected
}
req := &cline.UserOrganizationUpdateRequest{
OrganizationId: orgId,
}
if _, err := client.Account.SetUserOrganization(ctx, req); err != nil {
return fmt.Errorf("failed to set organization: %w", err)
}
if selected == "personal" {
fmt.Println("✓ Switched to personal account")
} else {
// Find the org name to display
var orgName string
for _, org := range organizations {
if org.OrganizationId == selected {
orgName = org.Name
break
}
}
fmt.Printf("✓ Switched to organization: %s\n", orgName)
}
return HandleAuthMenuNoArgs(ctx)
}
-321
View File
@@ -1,321 +0,0 @@
package auth
import (
"context"
"fmt"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/display"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// contextKey is a distinct type for context keys to avoid collisions
type contextKey string
const authInstanceAddressKey contextKey = "authInstanceAddress"
// AuthAction represents the type of authentication action
type AuthAction string
const (
AuthActionClineLogin AuthAction = "cline_login"
AuthActionBYOSetup AuthAction = "provider_setup"
AuthActionChangeClineModel AuthAction = "change_cline_model"
AuthActionSelectOrganization AuthAction = "select_organization"
AuthActionSelectProvider AuthAction = "select_provider"
AuthActionExit AuthAction = "exit_wizard"
)
// Cline Auth Menu
// Example Layout
//
// ┃ Cline Account: <authenticated/not authenticated>
// ┃ Active Provider: <provider name or none configured>
// ┃ Active Model: <model name or none configured>
// ┃
// ┃ What would you like to do?
// ┃ Change Cline model (only if authenticated) - hidden if not authenticated
// ┃ Authenticate with Cline account / Sign out of Cline - changes based on auth status
// ┃ Select active provider (Cline or BYO) - always shown. Used to switch between Cline and BYO providers
// ┃ Configure BYO API providers - always shown. Launches provider setup wizard
// ┃ Exit authorization wizard - always shown. Exits the auth menu
// RunAuthFlow is the entry point for the entire auth flow with instance management
// It spawns a fresh instance for auth operations and cleans it up when done
func RunAuthFlow(ctx context.Context, args []string) error {
// Spawn a fresh instance for auth operations
instanceInfo, err := global.Instances.StartNewInstance(ctx)
if err != nil {
return fmt.Errorf("failed to start auth instance: %w", err)
}
// Cleanup when done (success, error, or panic)
defer func() {
verboseLog("Shutting down auth instance at %s", instanceInfo.CoreAddress)
if err := global.KillInstanceByAddress(context.Background(), global.Instances.GetRegistry(), instanceInfo.CoreAddress); err != nil {
verboseLog("Warning: Failed to kill auth instance: %v", err)
}
}()
// Store instance address in context for all auth handlers to use
authCtx := context.WithValue(ctx, authInstanceAddressKey, instanceInfo.CoreAddress)
// Route to existing auth flow
return HandleAuthCommand(authCtx, args)
}
// Main entry point for handling the `cline auth` command
// HandleAuthCommand routes the auth command based on the number of arguments
func HandleAuthCommand(ctx context.Context, args []string) error {
// Check if flags are provided for quick setup
if QuickProvider != "" || QuickAPIKey != "" || QuickModelID != "" || QuickBaseURL != "" {
if QuickProvider == "" || QuickAPIKey == "" || QuickModelID == "" {
return fmt.Errorf("quick setup requires --provider, --apikey, and --modelid flags. Use 'cline auth --help' for more information")
}
return QuickSetupFromFlags(ctx, QuickProvider, QuickAPIKey, QuickModelID, QuickBaseURL)
}
switch len(args) {
case 0:
// No args: Show uth wizard
return HandleAuthMenuNoArgs(ctx)
case 1, 2, 3, 4:
fmt.Println("Invalid positional arguments. Correct usage:")
fmt.Println(" cline auth --provider <provider> --apikey <key> --modelid <model> --baseurl <optional>")
return nil
default:
return fmt.Errorf("too many arguments. Use flags for quick setup: --provider, --apikey, --modelid --baseurl(optional)")
}
}
// getAuthInstanceAddress retrieves the auth instance address from context
// Returns empty string if not found (falls back to default behavior)
func getAuthInstanceAddress(ctx context.Context) string {
if addr, ok := ctx.Value(authInstanceAddressKey).(string); ok {
return addr
}
return ""
}
// HandleAuthMenuNoArgs prepares the auth menu when no arguments are provided
func HandleAuthMenuNoArgs(ctx context.Context) error {
// Check if Cline is authenticated
isClineAuth := IsAuthenticated(ctx)
// Get current provider config for display
var currentProvider string
var currentModel string
if providerList, err := GetProviderConfigurations(ctx); err == nil {
if providerList.ActProvider != nil {
currentProvider = GetProviderDisplayName(providerList.ActProvider.Provider)
currentModel = providerList.ActProvider.ModelID
}
}
// Fetch organizations if authenticated
var hasOrganizations bool
if isClineAuth {
if client, err := global.GetDefaultClient(ctx); err == nil {
if orgsResponse, err := client.Account.GetUserOrganizations(ctx, &cline.EmptyRequest{}); err == nil {
hasOrganizations = len(orgsResponse.GetOrganizations()) > 0
}
}
}
action, err := ShowAuthMenuWithStatus(isClineAuth, hasOrganizations, currentProvider, currentModel)
if err != nil {
// Check if user cancelled - propagate for clean exit
if err == huh.ErrUserAborted {
return huh.ErrUserAborted
}
return err
}
switch action {
case AuthActionClineLogin:
return HandleClineAuth(ctx)
case AuthActionBYOSetup:
return HandleAPIProviderSetup(ctx)
case AuthActionChangeClineModel:
return HandleChangeClineModel(ctx)
case AuthActionSelectOrganization:
return HandleSelectOrganization(ctx)
case AuthActionSelectProvider:
return HandleSelectProvider(ctx)
case AuthActionExit:
return nil
default:
return fmt.Errorf("invalid action")
}
}
// ShowAuthMenuWithStatus displays the main auth menu with Cline + provider status
func ShowAuthMenuWithStatus(isClineAuthenticated bool, hasOrganizations bool, currentProvider, currentModel string) (AuthAction, error) {
var action AuthAction
var options []huh.Option[AuthAction]
// Build menu options based on authentication status
if isClineAuthenticated {
options = []huh.Option[AuthAction]{
huh.NewOption("Change Cline model", AuthActionChangeClineModel),
}
// Add organization selection if user has organizations
if hasOrganizations {
options = append(options, huh.NewOption("Select organization", AuthActionSelectOrganization))
}
options = append(options,
huh.NewOption("Sign out of Cline", AuthActionClineLogin),
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
huh.NewOption("Exit authorization wizard", AuthActionExit),
)
} else {
options = []huh.Option[AuthAction]{
huh.NewOption("Authenticate with Cline account", AuthActionClineLogin),
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
huh.NewOption("Exit authorization wizard", AuthActionExit),
}
}
// Determine menu title based on status
var title string
renderer := display.NewRenderer(global.Config.OutputFormat)
// Always show Cline authentication status
if isClineAuthenticated {
title = fmt.Sprintf("Cline Account: %s Authenticated\n", renderer.Green("✓"))
} else {
title = fmt.Sprintf("Cline Account: %s Not authenticated\n", renderer.Red("✗"))
}
// Show active provider and model if configured (regardless of Cline auth status)
if currentProvider != "" && currentModel != "" {
title += fmt.Sprintf("Active Provider: %s\nActive Model: %s\n",
renderer.White(currentProvider),
renderer.White(currentModel))
}
// Always end with a huh?
title += "\nWhat would you like to do?"
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[AuthAction]().
Title(title).
Options(options...).
Value(&action),
),
)
if err := form.Run(); err != nil {
// Check if user cancelled with Control-C
if err == huh.ErrUserAborted {
// Return the error to allow deferred cleanup to run
return "", huh.ErrUserAborted
}
return "", fmt.Errorf("failed to get menu choice: %w", err)
}
return action, nil
}
// HandleAPIProviderSetup launches the API provider configuration wizard
func HandleAPIProviderSetup(ctx context.Context) error {
wizard, err := NewProviderWizard(ctx)
if err != nil {
return fmt.Errorf("failed to create provider wizard: %w", err)
}
return wizard.Run()
}
// HandleSelectProvider allows users to switch between Cline provider and BYO providers
func HandleSelectProvider(ctx context.Context) error {
// Get task manager
manager, err := createTaskManager(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Detect all providers with valid configurations (is an API key present)
availableProviders, err := DetectAllConfiguredProviders(ctx, manager)
if err != nil {
return fmt.Errorf("failed to detect configured providers: %w", err)
}
// Build list of available providers
var providerOptions []huh.Option[string]
var providerMapping = make(map[string]cline.ApiProvider)
// Add each configured provider to the selection menu
for _, provider := range availableProviders {
providerName := GetProviderDisplayName(provider)
providerKey := fmt.Sprintf("provider_%d", provider)
providerOptions = append(providerOptions, huh.NewOption(providerName, providerKey))
providerMapping[providerKey] = provider
}
if len(providerOptions) == 0 {
fmt.Println("No providers available. Please configure a provider first.")
return HandleAuthMenuNoArgs(ctx)
}
providerOptions = append(providerOptions, huh.NewOption("(Cancel)", "cancel"))
// Show selection menu
var selected string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select which provider to use").
Options(providerOptions...).
Value(&selected),
),
)
if err := form.Run(); err != nil {
// Check if user cancelled with Control-C
if err == huh.ErrUserAborted {
return huh.ErrUserAborted
}
return fmt.Errorf("failed to select provider: %w", err)
}
if selected == "cancel" {
return HandleAuthMenuNoArgs(ctx)
}
// Get the selected provider
selectedProvider := providerMapping[selected]
// Apply the selected provider
if selectedProvider == cline.ApiProvider_CLINE {
// Configure Cline as the active provider
return SelectClineModel(ctx, manager)
} else {
// Switch to the selected BYO provider
return SwitchToBYOProvider(ctx, manager, selectedProvider)
}
}
// createTaskManager is a helper to create a task manager (avoids import cycles)
// Uses the auth instance address from context if available, otherwise falls back to default
func createTaskManager(ctx context.Context) (*task.Manager, error) {
authAddr := getAuthInstanceAddress(ctx)
if authAddr != "" {
return task.NewManagerForAddress(ctx, authAddr)
}
return task.NewManagerForDefault(ctx)
}
func verboseLog(format string, args ...interface{}) {
if global.Config != nil && global.Config.Verbose {
fmt.Printf("[VERBOSE] "+format+"\n", args...)
}
}
-130
View File
@@ -1,130 +0,0 @@
package auth
import (
"context"
"fmt"
"io"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/cline"
)
// AuthStatusListener manages subscription to auth status updates
type AuthStatusListener struct {
stream cline.AccountService_SubscribeToAuthStatusUpdateClient
updatesCh chan *cline.AuthState
errCh chan error
ctx context.Context
cancel context.CancelFunc
}
// NewAuthStatusListener creates a new auth status listener
func NewAuthStatusListener(parentCtx context.Context) (*AuthStatusListener, error) {
client, err := global.GetDefaultClient(parentCtx)
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
// Create cancellable context
ctx, cancel := context.WithCancel(parentCtx)
// Subscribe to auth status updates
stream, err := client.Account.SubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
if err != nil {
cancel()
return nil, fmt.Errorf("failed to subscribe to auth updates: %w", err)
}
return &AuthStatusListener{
stream: stream,
updatesCh: make(chan *cline.AuthState, 10),
errCh: make(chan error, 1),
ctx: ctx,
cancel: cancel,
}, nil
}
// Start begins listening to the auth status update stream
func (l *AuthStatusListener) Start() error {
verboseLog("Starting auth status listener...")
go l.readStream()
return nil
}
// readStream reads from the gRPC stream and forwards messages to channels
func (l *AuthStatusListener) readStream() {
defer close(l.updatesCh)
defer close(l.errCh)
for {
select {
case <-l.ctx.Done():
verboseLog("Auth listener context cancelled")
return
default:
state, err := l.stream.Recv()
if err != nil {
if err == io.EOF {
verboseLog("Auth status stream closed")
return
}
verboseLog("Error reading from auth status stream: %v", err)
select {
case l.errCh <- err:
case <-l.ctx.Done():
}
return
}
verboseLog("Received auth state update: user=%v", state.User != nil)
select {
case l.updatesCh <- state:
case <-l.ctx.Done():
return
}
}
}
}
// WaitForAuthentication blocks until authentication succeeds or timeout occurs
func (l *AuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
verboseLog("Waiting for authentication (timeout: %v)...", timeout)
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case <-timer.C:
return fmt.Errorf("authentication timeout after %v - please try again", timeout)
case <-l.ctx.Done():
return fmt.Errorf("authentication cancelled")
case err := <-l.errCh:
return fmt.Errorf("authentication stream error: %w", err)
case state := <-l.updatesCh:
if isAuthenticated(state) {
verboseLog("Authentication successful!")
return nil
}
verboseLog("Received auth update but not authenticated yet...")
}
}
}
// Stop closes the stream and cleans up resources
func (l *AuthStatusListener) Stop() {
verboseLog("Stopping auth status listener...")
l.cancel()
}
// isAuthenticated checks if AuthState indicates successful authentication
func isAuthenticated(state *cline.AuthState) bool {
return state != nil && state.User != nil
}
-247
View File
@@ -1,247 +0,0 @@
package auth
import (
"context"
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// Package-level variables for command-line flags
var (
QuickProvider string // Provider ID (e.g., "openai", "anthropic")
QuickAPIKey string // API key for the provider
QuickModelID string // Model ID to configure
QuickBaseURL string // Base URL (optional, for openai compatible only)
)
// QuickSetupFromFlags performs quick setup using command-line flags
// Returns error if validation fails or configuration cannot be applied
func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL string) error {
// Validate all input parameters
providerEnum, err := validateQuickSetupInputs(provider, apiKey, modelID, baseURL)
if err != nil {
return err
}
// Create task manager for state operations
manager, err := createTaskManager(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Validate and fetch model information if needed
finalModelID, modelInfo, err := validateAndFetchModel(ctx, manager, providerEnum, modelID, apiKey)
if err != nil {
return fmt.Errorf("model validation failed: %w", err)
}
// For Ollama, baseURL is stored in the API key field
finalAPIKey := apiKey
finalBaseURL := baseURL
if providerEnum == cline.ApiProvider_OLLAMA {
if baseURL != "" {
finalAPIKey = baseURL
finalBaseURL = ""
} else if apiKey != "" {
// User provided API key for Ollama - treat it as baseURL
finalAPIKey = apiKey
finalBaseURL = ""
} else {
// Use default Ollama baseURL
finalAPIKey = "http://localhost:11434"
finalBaseURL = ""
}
}
// Configure the provider using existing AddProviderPartial function
if err := AddProviderPartial(ctx, manager, providerEnum, finalModelID, finalAPIKey, finalBaseURL, modelInfo); err != nil {
return fmt.Errorf("failed to configure provider: %w", err)
}
// Set the provider as active for both Plan and Act modes
if err := UpdateProviderPartial(ctx, manager, providerEnum, ProviderUpdatesPartial{}, true); err != nil {
return fmt.Errorf("failed to set provider as active: %w", err)
}
// Mark welcome view as completed
if err := markWelcomeViewCompleted(ctx, manager); err != nil {
// Non-fatal error, just log it
if global.Config.Verbose {
fmt.Printf("[DEBUG] Warning: failed to mark welcome view as completed: %v\n", err)
}
}
// Flush pending state changes to disk immediately
// This ensures all configuration changes are persisted before the instance terminates
if _, err := manager.GetClient().State.FlushPendingState(ctx, &cline.EmptyRequest{}); err != nil {
return fmt.Errorf("failed to flush pending state: %w", err)
}
// Success message
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
fmt.Printf(" Model: %s\n", finalModelID)
if providerEnum == cline.ApiProvider_OLLAMA {
fmt.Printf(" Base URL: %s\n", finalAPIKey)
} else {
fmt.Println(" API Key: Configured")
}
if finalBaseURL != "" {
fmt.Printf(" Custom Base URL: %s\n", finalBaseURL)
}
fmt.Println("\nYou can now use Cline with this provider.")
fmt.Println("Run 'cline start' to begin a new task.")
return nil
}
// validateQuickSetupInputs validates all input parameters for quick setup
// Returns the validated provider enum or an error if validation fails
func validateQuickSetupInputs(provider, apiKey, modelID, baseURL string) (cline.ApiProvider, error) {
// Validate required parameters
if provider == "" {
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("provider is required. Use --provider or -p flag")
}
if strings.TrimSpace(apiKey) == "" && provider != "ollama" {
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("API key is required for %s provider. Use --apikey or -k flag", provider)
}
if strings.TrimSpace(modelID) == "" {
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("model ID is required. Use --modelid or -m flag")
}
// Validate and map provider string to enum
providerEnum, err := validateQuickSetupProvider(provider)
if err != nil {
return cline.ApiProvider_ANTHROPIC, err
}
// Validate that baseURL is only provided for OpenAI-compatible providers
if err := validateBaseURL(baseURL, providerEnum); err != nil {
return cline.ApiProvider_ANTHROPIC, err
}
return providerEnum, nil
}
// validateBaseURL checks if the user's input includes a baseURL for a provider other than OpenAI (compatible)
// Returns error if baseURL is provided for unsupported providers
func validateBaseURL(baseURL string, providerEnum cline.ApiProvider) error {
if providerEnum != cline.ApiProvider_OPENAI {
if baseURL != "" {
return fmt.Errorf("base URL is only supported for OpenAI and OpenAI-compatible providers")
}
}
return nil
}
// validateQuickSetupProvider validates the provider ID and returns the enum value
// Returns error if provider is invalid or not supported for quick setup
func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
// Normalize provider ID (trim whitespace, lowercase)
normalizedID := strings.TrimSpace(strings.ToLower(providerID))
// Explicitly block Bedrock
if normalizedID == "bedrock" {
return cline.ApiProvider_BEDROCK, fmt.Errorf("bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup: cline auth")
}
// Map provider string to enum using existing function
provider, ok := mapProviderStringToEnum(normalizedID)
if !ok {
// Provider not found - provide helpful error message
supportedProviders := []string{
"openai-native", "openai", "anthropic", "gemini",
"openrouter", "xai", "cerebras", "ollama",
}
return cline.ApiProvider_ANTHROPIC, fmt.Errorf(
"invalid provider '%s'. Supported providers: %s",
providerID,
strings.Join(supportedProviders, ", "),
)
}
// Validate against supported quick setup providers
supportedProviders := map[cline.ApiProvider]bool{
cline.ApiProvider_OPENAI_NATIVE: true,
cline.ApiProvider_OPENAI: true,
cline.ApiProvider_ANTHROPIC: true,
cline.ApiProvider_GEMINI: true,
cline.ApiProvider_OPENROUTER: true,
cline.ApiProvider_XAI: true,
cline.ApiProvider_CEREBRAS: true,
cline.ApiProvider_OLLAMA: true,
cline.ApiProvider_NOUSRESEARCH: true,
}
if !supportedProviders[provider] {
return provider, fmt.Errorf(
"provider '%s' is not supported for quick setup. Please use interactive setup: cline auth",
providerID,
)
}
return provider, nil
}
// validateAndFetchModel validates the model ID or fetches from provider if needed
// Returns the final model ID and optional model info
// For providers with static models, validates against the list
// For providers with dynamic models, fetches the list if possible
func validateAndFetchModel(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID, apiKey string) (string, interface{}, error) {
// Normalize model ID
modelID = strings.TrimSpace(modelID)
if modelID == "" {
return "", nil, fmt.Errorf("model ID cannot be empty")
}
// For most providers, we trust the user's input since we can't easily validate without making API calls
// The actual validation will happen when the model is used
switch provider {
case cline.ApiProvider_OPENROUTER:
// OpenRouter supports model info fetching, but it requires an API call
// For quick setup, we'll trust the user's input and return nil for model info
// The actual model info will be fetched when needed
if global.Config.Verbose {
fmt.Printf("[DEBUG] OpenRouter model ID: %s (will be validated on first use)\n", modelID)
}
return modelID, nil, nil
case cline.ApiProvider_OLLAMA:
// Ollama models can be validated by fetching the list, but this requires the server to be running
// For quick setup, we'll trust the user's input
if global.Config.Verbose {
fmt.Printf("[DEBUG] Ollama model ID: %s (will be validated when server is accessible)\n", modelID)
}
return modelID, nil, nil
default:
// For other providers (Anthropic, OpenAI, Gemini, XAI, Cerebras), trust user input
// Model validation will occur when the model is actually used
if global.Config.Verbose {
fmt.Printf("[DEBUG] %s model ID: %s (will be validated on first use)\n", GetProviderDisplayName(provider), modelID)
}
return modelID, nil, nil
}
}
// markWelcomeViewCompleted marks the welcome view as completed in the state
// This prevents the welcome view from showing up after quick setup
func markWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
// Use the State service to update the welcome view flag
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
if err != nil {
return fmt.Errorf("failed to mark welcome view as completed: %w", err)
}
if global.Config.Verbose {
fmt.Println("[DEBUG] Marked welcome view as completed")
}
return nil
}
-141
View File
@@ -1,141 +0,0 @@
package auth
import (
"context"
"fmt"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// DefaultClineModelID is the default model ID for Cline provider.
// Cline uses OpenRouter-compatible model IDs.
const DefaultClineModelID = "anthropic/claude-sonnet-4.5"
// FetchClineModels fetches available Cline models from Cline Core.
// Note: Cline provider uses OpenRouter-compatible API and model format.
// The models are fetched using the same method as OpenRouter.
func FetchClineModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
if global.Config.Verbose {
fmt.Println("Fetching Cline models (using OpenRouter-compatible API)")
}
// Cline uses OpenRouter model fetching
models, err := FetchOpenRouterModels(ctx, manager)
if err != nil {
return nil, fmt.Errorf("failed to fetch Cline models: %w", err)
}
return models, nil
}
// GetClineModelInfo retrieves information for a specific Cline model.
func GetClineModelInfo(modelID string, models map[string]*cline.OpenRouterModelInfo) (*cline.OpenRouterModelInfo, error) {
modelInfo, exists := models[modelID]
if !exists {
return nil, fmt.Errorf("model %s not found", modelID)
}
return modelInfo, nil
}
// SetDefaultClineModel configures the default Cline model after authentication.
// This is called automatically after successful Cline sign-in.
func SetDefaultClineModel(ctx context.Context, manager *task.Manager) error {
// Fetch available models
models, err := FetchClineModels(ctx, manager)
if err != nil {
// If we can't fetch models, we'll use the default without model info
fmt.Printf("Warning: Could not fetch Cline models: %v\n", err)
fmt.Printf("Using default model: %s\n", DefaultClineModelID)
return applyDefaultClineModel(ctx, manager, nil)
}
// Check if default model is available
modelInfo, err := GetClineModelInfo(DefaultClineModelID, models)
if err != nil {
fmt.Printf("Warning: Default model not found: %v\n", err)
// Try to use any available model
for modelID := range models {
fmt.Printf("Using available model: %s\n", modelID)
return applyClineModelConfiguration(ctx, manager, modelID, models[modelID])
}
return fmt.Errorf("no usable Cline models found")
}
if err := applyClineModelConfiguration(ctx, manager, DefaultClineModelID, modelInfo); err != nil {
return err
}
if err := setWelcomeViewCompletedWithManager(ctx, manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
return nil
}
// SelectClineModel presents a menu to select a Cline model and applies the configuration.
func SelectClineModel(ctx context.Context, manager *task.Manager) error {
// Fetch models (uses OpenRouter-compatible format)
models, err := FetchClineModels(ctx, manager)
if err != nil {
return fmt.Errorf("failed to fetch Cline models: %w", err)
}
// Convert to interface map for generic utilities
modelMap := ConvertOpenRouterModelsToInterface(models)
// Get model IDs as a sorted list
modelIDs := ConvertModelsMapToSlice(modelMap)
// Display selection menu
selectedModelID, err := DisplayModelSelectionMenu(modelIDs, "Cline")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Get the selected model info
modelInfo := models[selectedModelID]
// Apply the configuration
if err := applyClineModelConfiguration(ctx, manager, selectedModelID, modelInfo); err != nil {
return err
}
fmt.Println()
// Return to main auth menu after model selection
return HandleAuthMenuNoArgs(ctx)
}
// applyClineModelConfiguration applies a Cline model configuration to both Act and Plan modes using UpdateProviderPartial.
// Cline uses OpenRouter-compatible model format.
func applyClineModelConfiguration(ctx context.Context, manager *task.Manager, modelID string, modelInfo *cline.OpenRouterModelInfo) error {
provider := cline.ApiProvider_CLINE
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
return UpdateProviderPartial(ctx, manager, provider, updates, true)
}
func applyDefaultClineModel(ctx context.Context, manager *task.Manager, modelInfo *cline.OpenRouterModelInfo) error {
if err := applyClineModelConfiguration(ctx, manager, DefaultClineModelID, modelInfo); err != nil {
return err
}
if err := setWelcomeViewCompletedWithManager(ctx, manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
return nil
}
func setWelcomeViewCompletedWithManager(ctx context.Context, manager *task.Manager) error {
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
return err
}
-156
View File
@@ -1,156 +0,0 @@
package auth
import (
"context"
"fmt"
"os"
"sort"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"golang.org/x/term"
)
// FetchOpenRouterModels fetches available OpenRouter models from Cline Core
func FetchOpenRouterModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
resp, err := manager.GetClient().Models.RefreshOpenRouterModelsRpc(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to fetch OpenRouter models: %w", err)
}
return resp.Models, nil
}
// FetchOcaModels fetches available Oca models from Cline Core
func FetchOcaModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OcaModelInfo, error) {
resp, err := manager.GetClient().Models.RefreshOcaModels(ctx, &cline.StringRequest{})
if err != nil {
return nil, fmt.Errorf("failed to fetch Oca models: %w", err)
}
return resp.Models, nil
}
// ConvertOpenRouterModelsToInterface converts OpenRouter model map to generic interface map.
// This allows OpenRouter and Cline models to be used with the generic fetching utilities.
func ConvertOpenRouterModelsToInterface(models map[string]*cline.OpenRouterModelInfo) map[string]interface{} {
result := make(map[string]interface{}, len(models))
for k, v := range models {
result[k] = v
}
return result
}
// FetchOpenAiModels fetches available OpenAI models from Cline Core
// Takes the API key and returns a list of model IDs
func FetchOpenAiModels(ctx context.Context, manager *task.Manager, baseURL, apiKey string) ([]string, error) {
req := &cline.OpenAiModelsRequest{
BaseUrl: baseURL,
ApiKey: apiKey,
}
resp, err := manager.GetClient().Models.RefreshOpenAiModels(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to fetch OpenAI models: %w", err)
}
return resp.Values, nil
}
// FetchOllamaModels fetches available Ollama models from Cline Core
// Takes the base URL (empty string for default) and returns a list of model IDs
func FetchOllamaModels(ctx context.Context, manager *task.Manager, baseURL string) ([]string, error) {
req := &cline.StringRequest{
Value: baseURL,
}
resp, err := manager.GetClient().Models.GetOllamaModels(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to fetch Ollama models: %w", err)
}
return resp.Values, nil
}
// DisplayModelSelectionMenu shows an interactive menu for selecting a model from a list.
// Models are displayed alphabetically. Uses model ID as the option value to avoid
// index-based bugs when list order changes.
// Returns the selected model ID.
func DisplayModelSelectionMenu(models []string, providerName string) (string, error) {
if len(models) == 0 {
return "", fmt.Errorf("no models available for selection")
}
// Use model ID as the value (not index) to avoid positional coupling bugs
var selectedModel string
options := make([]huh.Option[string], len(models))
for i, model := range models {
options[i] = huh.NewOption(model, model)
}
title := fmt.Sprintf("Select a %s model", providerName)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title(title).
Options(options...).
Height(calculateSelectHeight()).
Filtering(true).
Value(&selectedModel),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to select model: %w", err)
}
return selectedModel, nil
}
// ConvertModelsMapToSlice converts a map of models to a sorted slice of model IDs.
// This is useful for displaying models in a consistent order in UI components.
func ConvertModelsMapToSlice(models map[string]interface{}) []string {
result := make([]string, 0, len(models))
for modelID := range models {
result = append(result, modelID)
}
// Sort alphabetically for consistent display
sort.Strings(result)
return result
}
// ConvertOcaModelsToInterface converts Oca model map to generic interface map.
// This allows Oca and Cline models to be used with the generic fetching utilities.
func ConvertOcaModelsToInterface(models map[string]*cline.OcaModelInfo) map[string]interface{} {
result := make(map[string]interface{}, len(models))
for k, v := range models {
result[k] = v
}
return result
}
// getTerminalHeight returns the terminal height (rows)
func getTerminalHeight() int {
_, height, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || height <= 0 {
return 25 // safe fallback for non-TTY or errors
}
return height
}
// calculateSelectHeight computes appropriate height for Select component
// Reserves space for title, search UI, and margins
func calculateSelectHeight() int {
height := getTerminalHeight()
// Reserve ~10 rows for UI chrome (title, search, margins)
visibleRows := height - 10
// Clamp between 8 (minimum usable) and 25 (maximum before unwieldy)
if visibleRows < 8 {
return 8
}
if visibleRows > 25 {
return 25
}
return visibleRows
}
-69
View File
@@ -1,69 +0,0 @@
package auth
import (
"fmt"
"sort"
"github.com/cline/cli/pkg/generated"
"github.com/cline/grpc-go/cline"
)
// SupportsStaticModelList returns true if the provider has a predefined static model list
func SupportsStaticModelList(provider cline.ApiProvider) bool {
providerID := GetProviderIDForEnum(provider)
if providerID == "" {
return false
}
// Check if this provider has static models defined
def, err := generated.GetProviderDefinition(providerID)
if err != nil {
return false
}
// Return true if provider has models and isn't dynamic-only
// (Dynamic providers like OpenRouter/OpenAI/Ollama fetch from API)
return len(def.Models) > 0 && !def.HasDynamicModels
}
// FetchStaticModels retrieves the static model list for a provider from generated definitions
// Returns a sorted list of model IDs and a map of model IDs to their info
func FetchStaticModels(provider cline.ApiProvider) ([]string, map[string]generated.ModelInfo, error) {
providerID := GetProviderIDForEnum(provider)
if providerID == "" {
return nil, nil, fmt.Errorf("unknown provider enum: %v", provider)
}
def, err := generated.GetProviderDefinition(providerID)
if err != nil {
return nil, nil, fmt.Errorf("failed to get provider definition: %w", err)
}
if len(def.Models) == 0 {
return nil, nil, fmt.Errorf("no models defined for provider %s", providerID)
}
// Extract model IDs and sort them
modelIDs := make([]string, 0, len(def.Models))
for modelID := range def.Models {
modelIDs = append(modelIDs, modelID)
}
sort.Strings(modelIDs)
return modelIDs, def.Models, nil
}
// GetDefaultModelForProvider returns the default model ID for a provider if one is defined
func GetDefaultModelForProvider(provider cline.ApiProvider) string {
providerID := GetProviderIDForEnum(provider)
if providerID == "" {
return ""
}
def, err := generated.GetProviderDefinition(providerID)
if err != nil {
return ""
}
return def.DefaultModelID
}
-184
View File
@@ -1,184 +0,0 @@
package auth
import (
"fmt"
"github.com/charmbracelet/huh"
"github.com/cline/grpc-go/cline"
)
// BYOProviderOption represents a selectable BYO (bring-your-own) provider option
type BYOProviderOption struct {
Name string
Provider cline.ApiProvider
}
// GetBYOProviderList returns the list of supported BYO providers for CLI configuration.
// This list excludes Cline provider which is handled separately.
func GetBYOProviderList() []BYOProviderOption {
return []BYOProviderOption{
{Name: "Anthropic", Provider: cline.ApiProvider_ANTHROPIC},
{Name: "OpenAI Compatible", Provider: cline.ApiProvider_OPENAI},
{Name: "OpenAI (Official)", Provider: cline.ApiProvider_OPENAI_NATIVE},
{Name: "OpenRouter", Provider: cline.ApiProvider_OPENROUTER},
{Name: "X AI (Grok)", Provider: cline.ApiProvider_XAI},
{Name: "AWS Bedrock", Provider: cline.ApiProvider_BEDROCK},
{Name: "Google Gemini", Provider: cline.ApiProvider_GEMINI},
{Name: "Ollama", Provider: cline.ApiProvider_OLLAMA},
{Name: "Cerebras", Provider: cline.ApiProvider_CEREBRAS},
{Name: "NousResearch", Provider: cline.ApiProvider_NOUSRESEARCH},
{Name: "Oracle Code Assist", Provider: cline.ApiProvider_OCA},
}
}
// SelectBYOProvider displays a menu for selecting a BYO provider.
func SelectBYOProvider() (cline.ApiProvider, error) {
providers := GetBYOProviderList()
var selectedIndex int
options := make([]huh.Option[int], len(providers)+1)
for i, provider := range providers {
options[i] = huh.NewOption(provider.Name, i)
}
options[len(providers)] = huh.NewOption("(Cancel)", -1)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[int]().
Title("Select an API provider").
Options(options...).
Value(&selectedIndex),
),
)
if err := form.Run(); err != nil {
return 0, fmt.Errorf("failed to select provider: %w", err)
}
if selectedIndex == -1 {
return 0, fmt.Errorf("provider selection cancelled")
}
return providers[selectedIndex].Provider, nil
}
// SupportsBYOModelFetching returns true if the provider supports fetching models dynamically
// from a remote API, or if it has a static list of predefined models.
// This is used to determine whether to show a model list before prompting for manual entry.
func SupportsBYOModelFetching(provider cline.ApiProvider) bool {
switch provider {
case cline.ApiProvider_OPENROUTER:
return true
case cline.ApiProvider_OPENAI:
return true
case cline.ApiProvider_OLLAMA:
return true
case cline.ApiProvider_OCA:
return true
}
return SupportsStaticModelList(provider)
}
// GetBYOProviderPlaceholder returns a placeholder model ID for manual entry based on provider.
func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return "e.g., claude-sonnet-4-5-20250929"
case cline.ApiProvider_OPENAI:
return "e.g., openai/gpt-oss-120b"
case cline.ApiProvider_OPENAI_NATIVE:
return "e.g., gpt-5-2025-08-07"
case cline.ApiProvider_OPENROUTER:
return "e.g., google/gemini-2.0-flash-exp:free"
case cline.ApiProvider_XAI:
return "e.g., grok-code-fast-1"
case cline.ApiProvider_BEDROCK:
return "e.g., anthropic.claude-sonnet-4-5-20250929-v1:0"
case cline.ApiProvider_GEMINI:
return "e.g., gemini-2.5-pro"
case cline.ApiProvider_OLLAMA:
return "e.g., qwen3-coder:30b"
case cline.ApiProvider_CEREBRAS:
return "e.g., gpt-oss-120b"
case cline.ApiProvider_NOUSRESEARCH:
return "e.g., Hermes-4-405B"
case cline.ApiProvider_OCA:
return "e.g., oca/llama4"
default:
return "Enter model ID"
}
}
// GetBYOAPIKeyFieldConfig returns field configuration for API key input based on provider.
type APIKeyFieldConfig struct {
Title string
EchoMode huh.EchoMode
IsRequired bool
}
// GetBYOAPIKeyFieldConfig returns the configuration for the API key field based on provider.
func GetBYOAPIKeyFieldConfig(provider cline.ApiProvider) APIKeyFieldConfig {
if provider == cline.ApiProvider_OLLAMA {
return APIKeyFieldConfig{
Title: "Base URL (optional, press Enter for default)",
EchoMode: huh.EchoModeNormal,
IsRequired: false,
}
}
return APIKeyFieldConfig{
Title: "API Key",
EchoMode: huh.EchoModePassword,
IsRequired: true,
}
}
// PromptForAPIKey prompts the user to enter an API key (or base URL for Ollama).
// For OpenAI (Compatible) provider, also prompts for an optional base URL.
func PromptForAPIKey(provider cline.ApiProvider) (string, string, error) {
var apiKey string
config := GetBYOAPIKeyFieldConfig(provider)
apiKeyField := huh.NewInput().
Title(config.Title).
EchoMode(config.EchoMode).
Value(&apiKey)
if config.IsRequired {
apiKeyField = apiKeyField.Validate(func(s string) error {
if s == "" {
return fmt.Errorf("API key cannot be empty")
}
return nil
})
}
form := huh.NewForm(huh.NewGroup(apiKeyField))
if err := form.Run(); err != nil {
return "", "", fmt.Errorf("failed to get API key: %w", err)
}
// For OpenAI (Compatible) provider, prompt for base URL
if provider == cline.ApiProvider_OPENAI {
var baseURL string
baseURLForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Base URL (optional, for OpenAI-compatible providers)").
Placeholder("e.g., https://api.example.com/v1").
Value(&baseURL).
Description("Press Enter to skip if using standard OpenAI API"),
),
)
if err := baseURLForm.Run(); err != nil {
return "", "", fmt.Errorf("failed to get base URL: %w", err)
}
return apiKey, baseURL, nil
}
return apiKey, "", nil
}
-527
View File
@@ -1,527 +0,0 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// ProviderDisplay represents a configured provider for display purposes
type ProviderDisplay struct {
Mode string // "Plan" or "Act"
Provider cline.ApiProvider // Provider enum
ModelID string // Model identifier
HasAPIKey bool // Whether an API key is configured (never show actual key)
BaseURL string // Base URL for providers like Ollama (can be shown publicly)
}
// ProviderListResult holds the parsed provider configuration from state
type ProviderListResult struct {
PlanProvider *ProviderDisplay
ActProvider *ProviderDisplay
apiConfig map[string]interface{} // Store the raw apiConfig for scanning all providers
}
// GetProviderConfigurations retrieves and parses provider configurations from Cline Core state
func GetProviderConfigurations(ctx context.Context) (*ProviderListResult, error) {
if global.Config.Verbose {
fmt.Println("[DEBUG] Retrieving provider configurations from Cline Core")
}
// Get latest state from Cline Core
grpcClient, err := global.GetDefaultClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get provider configs due to unable to get gRPC client: %w", err)
}
state, err := grpcClient.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
stateJSON := state.StateJson
if global.Config.Verbose {
fmt.Printf("[DEBUG] Retrieved state, parsing JSON (length: %d)\n", len(stateJSON))
}
// Parse state_json as map[string]interface{}
var stateData map[string]any
if err := json.Unmarshal([]byte(stateJSON), &stateData); err != nil {
return nil, fmt.Errorf("failed to parse state JSON: %w", err)
}
if global.Config.Verbose {
fmt.Printf("[DEBUG] Parsed state data with %d keys\n", len(stateData))
}
// Extract apiConfiguration object from state
apiConfig, ok := stateData["apiConfiguration"].(map[string]any)
if !ok {
if global.Config.Verbose {
fmt.Println("[DEBUG] No apiConfiguration found in state")
}
return &ProviderListResult{
apiConfig: make(map[string]interface{}),
}, nil
}
if global.Config.Verbose {
fmt.Printf("[DEBUG] Found apiConfiguration with %d keys\n", len(apiConfig))
}
// Extract plan mode configuration
planProvider := extractProviderFromState(apiConfig, "plan")
if global.Config.Verbose && planProvider != nil {
fmt.Printf("[DEBUG] Plan mode: provider=%v, model=%s\n", planProvider.Provider, planProvider.ModelID)
}
// Extract act mode configuration
actProvider := extractProviderFromState(apiConfig, "act")
if global.Config.Verbose && actProvider != nil {
fmt.Printf("[DEBUG] Act mode: provider=%v, model=%s\n", actProvider.Provider, actProvider.ModelID)
}
return &ProviderListResult{
PlanProvider: planProvider,
ActProvider: actProvider,
apiConfig: apiConfig,
}, nil
}
// GetAllReadyProviders returns all providers that have both a model and API key configured
func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
if r.apiConfig == nil {
return []*ProviderDisplay{}
}
var readyProviders []*ProviderDisplay
seenProviders := make(map[cline.ApiProvider]bool)
// Check all possible providers
allProviders := []cline.ApiProvider{
cline.ApiProvider_CLINE,
cline.ApiProvider_ANTHROPIC,
cline.ApiProvider_OPENAI,
cline.ApiProvider_OPENAI_NATIVE,
cline.ApiProvider_OPENROUTER,
cline.ApiProvider_XAI,
cline.ApiProvider_BEDROCK,
cline.ApiProvider_GEMINI,
cline.ApiProvider_OLLAMA,
cline.ApiProvider_CEREBRAS,
cline.ApiProvider_NOUSRESEARCH,
cline.ApiProvider_OCA,
cline.ApiProvider_HICAP,
}
// Check each provider to see if it's ready to use
// We use "plan" mode to check, since both plan and act should have the same providers configured
for _, provider := range allProviders {
// Skip if we've already seen this provider
if seenProviders[provider] {
continue
}
// Check if this provider has a model configured
modelID := getProviderSpecificModelID(r.apiConfig, "plan", provider)
// Determine if credentials exist
hasCreds := checkCredentialsExists(r.apiConfig, provider)
// Determine readiness: OCA uses auth state presence; others need creds and model
if provider == cline.ApiProvider_OCA {
state, _ := GetLatestOCAState(context.Background(), 2*time.Second)
if state == nil || state.User == nil {
continue
}
} else {
// Provider is not ready unless it has credentials AND a model configured
if !hasCreds || modelID == "" {
continue
}
}
// Get base URL for Ollama
baseURL := ""
if provider == cline.ApiProvider_OLLAMA {
if url, ok := r.apiConfig["ollamaBaseUrl"].(string); ok {
baseURL = url
}
}
// This provider is ready to use
readyProviders = append(readyProviders, &ProviderDisplay{
Mode: "Ready",
Provider: provider,
ModelID: modelID,
HasAPIKey: checkCredentialsExists(r.apiConfig, provider),
BaseURL: baseURL,
})
seenProviders[provider] = true
}
return readyProviders
}
// extractProviderFromState extracts provider configuration for specific plan/act mode
func extractProviderFromState(stateData map[string]interface{}, mode string) *ProviderDisplay {
// Build key names based on mode
providerKey := mode + "ModeApiProvider"
// Extract provider string from state
providerStr, ok := stateData[providerKey].(string)
if !ok || providerStr == "" {
if global.Config.Verbose {
fmt.Printf("[DEBUG] No provider configured for %s mode\n", mode)
}
return nil
}
// Map provider string to enum
provider, ok := mapProviderStringToEnum(providerStr)
if !ok {
if global.Config.Verbose {
fmt.Printf("[DEBUG] Unknown provider type: %s\n", providerStr)
}
return nil
}
// Get provider-specific model ID
modelID := getProviderSpecificModelID(stateData, mode, provider)
// Check if API key exists
hasCredentials := checkCredentialsExists(stateData, provider)
// Get base URL for Ollama (can be shown publicly)
baseURL := ""
if provider == cline.ApiProvider_OLLAMA {
if url, ok := stateData["ollamaBaseUrl"].(string); ok {
baseURL = url
}
}
return &ProviderDisplay{
Mode: capitalizeMode(mode),
Provider: provider,
ModelID: modelID,
HasAPIKey: hasCredentials,
BaseURL: baseURL,
}
}
// mapProviderStringToEnum converts provider string from state to ApiProvider enum
// Returns (provider, ok) where ok is false if the provider is unknown
func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
normalizedStr := strings.ToLower(providerStr)
// Map string values to enum values
switch normalizedStr {
case "anthropic":
return cline.ApiProvider_ANTHROPIC, true
case "openai", "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
return cline.ApiProvider_OPENAI, true
case "openai-native": // This is the native, official Open AI provider
return cline.ApiProvider_OPENAI_NATIVE, true
case "openrouter":
return cline.ApiProvider_OPENROUTER, true
case "xai":
return cline.ApiProvider_XAI, true
case "bedrock":
return cline.ApiProvider_BEDROCK, true
case "gemini":
return cline.ApiProvider_GEMINI, true
case "ollama":
return cline.ApiProvider_OLLAMA, true
case "cerebras":
return cline.ApiProvider_CEREBRAS, true
case "cline":
return cline.ApiProvider_CLINE, true
case "oca":
return cline.ApiProvider_OCA, true
case "hicap":
return cline.ApiProvider_HICAP, true
case "nousResearch":
return cline.ApiProvider_NOUSRESEARCH, true
default:
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
}
}
// GetProviderIDForEnum converts a provider enum to the provider ID string
// This is the inverse of mapProviderStringToEnum and is used for provider definitions
func GetProviderIDForEnum(provider cline.ApiProvider) string {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return "anthropic"
case cline.ApiProvider_OPENAI:
return "openai-compatible"
case cline.ApiProvider_OPENAI_NATIVE:
return "openai-native"
case cline.ApiProvider_OPENROUTER:
return "openrouter"
case cline.ApiProvider_XAI:
return "xai"
case cline.ApiProvider_BEDROCK:
return "bedrock"
case cline.ApiProvider_GEMINI:
return "gemini"
case cline.ApiProvider_OLLAMA:
return "ollama"
case cline.ApiProvider_CEREBRAS:
return "cerebras"
case cline.ApiProvider_CLINE:
return "cline"
case cline.ApiProvider_OCA:
return "oca"
case cline.ApiProvider_HICAP:
return "hicap"
case cline.ApiProvider_NOUSRESEARCH:
return "nousResearch"
default:
return ""
}
}
// getProviderSpecificModelID gets the provider-specific model ID field from state
func getProviderSpecificModelID(stateData map[string]interface{}, mode string, provider cline.ApiProvider) string {
modelKey, err := GetModelIDFieldName(provider, mode)
if err != nil {
if global.Config.Verbose {
fmt.Printf("[DEBUG] Error getting model ID field name: %v\n", err)
}
return ""
}
if global.Config.Verbose {
fmt.Printf("[DEBUG] Looking for model ID in key: %s\n", modelKey)
}
// Extract model ID from state
modelID, _ := stateData[modelKey].(string)
return modelID
}
// checkCredentialsExists checks if API key field exists in state (never retrieve actual key)
func checkCredentialsExists(stateData map[string]interface{}, provider cline.ApiProvider) bool {
// Get field mapping from centralized function
fields, err := GetProviderFields(provider)
if err != nil {
return false
}
// Check if the key exists and is not empty
if value, ok := stateData[fields.APIKeyField]; ok {
if str, ok := value.(string); ok && str != "" {
return true
}
}
if value, ok := stateData[fields.UseProfileField]; ok {
if hasProfileField, ok := value.(bool); ok && hasProfileField {
return true
}
}
return false
}
// capitalizeMode capitalizes the mode string for display
func capitalizeMode(mode string) string {
if len(mode) == 0 {
return mode
}
return strings.ToUpper(mode[:1]) + mode[1:]
}
// GetProviderDisplayName returns a user-friendly name for the provider
func GetProviderDisplayName(provider cline.ApiProvider) string {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return "Anthropic"
case cline.ApiProvider_OPENAI:
return "OpenAI Compatible"
case cline.ApiProvider_OPENAI_NATIVE:
return "OpenAI (Official)"
case cline.ApiProvider_OPENROUTER:
return "OpenRouter"
case cline.ApiProvider_XAI:
return "X AI (Grok)"
case cline.ApiProvider_BEDROCK:
return "AWS Bedrock"
case cline.ApiProvider_GEMINI:
return "Google Gemini"
case cline.ApiProvider_OLLAMA:
return "Ollama"
case cline.ApiProvider_CEREBRAS:
return "Cerebras"
case cline.ApiProvider_CLINE:
return "Cline (Official)"
case cline.ApiProvider_OCA:
return "Oracle Code Assist"
case cline.ApiProvider_HICAP:
return "Hicap"
case cline.ApiProvider_NOUSRESEARCH:
return "NousResearch"
default:
return "Unknown"
}
}
// FormatProviderList formats the complete provider list for console display
// This now shows ALL providers that have both a model and API key configured
func FormatProviderList(result *ProviderListResult) string {
var output strings.Builder
output.WriteString("\n=== Configured API Providers ===\n\n")
// Get the currently active provider
var activeProvider cline.ApiProvider
var activeProviderSet bool
if result.ActProvider != nil {
activeProvider = result.ActProvider.Provider
activeProviderSet = true
}
// Get all ready-to-use providers (those with both API key and model configured)
readyProviders := result.GetAllReadyProviders()
if len(readyProviders) == 0 {
output.WriteString(" No providers ready to use.\n")
output.WriteString(" A provider is ready when it has both a model and API key configured.\n")
output.WriteString(" Use 'Configure a new provider' to configure one.\n\n")
} else {
//output.WriteString(fmt.Sprintf(" %d provider(s) ready to use:\n\n", len(readyProviders)))
for _, display := range readyProviders {
// Check if this is the active provider
isActive := activeProviderSet && display.Provider == activeProvider
if isActive {
output.WriteString(fmt.Sprintf(" ✓ %s (ACTIVE)\n", GetProviderDisplayName(display.Provider)))
} else {
output.WriteString(fmt.Sprintf(" • %s\n", GetProviderDisplayName(display.Provider)))
}
output.WriteString(fmt.Sprintf(" Model: %s\n", display.ModelID))
// Show status based on provider type
if display.Provider == cline.ApiProvider_OLLAMA {
if display.BaseURL != "" {
output.WriteString(fmt.Sprintf(" Base URL: %s\n", display.BaseURL))
} else {
output.WriteString(" Base URL: (default)\n")
}
} else if display.Provider == cline.ApiProvider_CLINE || display.Provider == cline.ApiProvider_OCA {
output.WriteString(" Status: Authenticated\n")
} else {
output.WriteString(" API Key: Configured\n")
}
output.WriteString("\n")
}
}
output.WriteString("================================\n")
return output.String()
}
// DetectAllConfiguredProviders scans the state to find all providers that have API keys configured.
// This allows switching between multiple providers even when only one is currently active.
func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([]cline.ApiProvider, error) {
verboseLog("[DEBUG] Detecting all configured providers...")
// Get latest state from Cline Core
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
stateJSON := state.StateJson
// Parse state_json as map[string]interface{}
var stateData map[string]any
if err := json.Unmarshal([]byte(stateJSON), &stateData); err != nil {
return nil, fmt.Errorf("failed to parse state JSON: %w", err)
}
// Extract apiConfiguration object from state
apiConfig, ok := stateData["apiConfiguration"].(map[string]any)
if !ok {
verboseLog("[DEBUG] No apiConfiguration found in state")
verboseLog("[DEBUG] Available keys in stateData: %v", getMapKeys(stateData))
return []cline.ApiProvider{}, nil
}
verboseLog("[DEBUG] apiConfiguration keys: %v", getMapKeys(apiConfig))
var configuredProviders []cline.ApiProvider
// Check for Cline provider (uses authentication instead of API key)
if IsAuthenticated(ctx) {
configuredProviders = append(configuredProviders, cline.ApiProvider_CLINE)
verboseLog("[DEBUG] Cline provider is authenticated")
}
// Check OCA provider via global auth subscription (state presence)
if state, _ := GetLatestOCAState(context.Background(), 2*time.Second); state != nil && state.User != nil {
configuredProviders = append(configuredProviders, cline.ApiProvider_OCA)
verboseLog("[DEBUG] OCA provider has active auth state")
}
// Check each BYO provider for API key presence
providersToCheck := []struct {
provider cline.ApiProvider
keyFields []string
}{
{cline.ApiProvider_ANTHROPIC, []string{"apiKey"}},
{cline.ApiProvider_OPENAI, []string{"openAiApiKey"}},
{cline.ApiProvider_OPENAI_NATIVE, []string{"openAiNativeApiKey"}},
{cline.ApiProvider_OPENROUTER, []string{"openRouterApiKey"}},
{cline.ApiProvider_XAI, []string{"xaiApiKey"}},
{cline.ApiProvider_BEDROCK, []string{"awsAccessKey", "awsUseProfile"}},
{cline.ApiProvider_GEMINI, []string{"geminiApiKey"}},
{cline.ApiProvider_OLLAMA, []string{"ollamaBaseUrl"}}, // Ollama uses baseUrl instead of API key
{cline.ApiProvider_CEREBRAS, []string{"cerebrasApiKey"}},
{cline.ApiProvider_HICAP, []string{"hicapApiKey"}},
{cline.ApiProvider_NOUSRESEARCH, []string{"nousResearchApiKey"}},
}
for _, providerCheck := range providersToCheck {
verboseLog("[DEBUG] Checking for %s key: %s", GetProviderDisplayName(providerCheck.provider), providerCheck.keyFields)
for _, keyField := range providerCheck.keyFields {
if value, ok := apiConfig[keyField]; ok {
verboseLog("[DEBUG] Found key, value type: %T, is empty: %v", value, value == "")
if str, ok := value.(string); ok && str != "" {
configuredProviders = append(configuredProviders, providerCheck.provider)
verboseLog("[DEBUG] ✓ Provider %s is configured", GetProviderDisplayName(providerCheck.provider))
break
}
} else {
verboseLog("[DEBUG] Key %s not found", keyField)
}
}
}
verboseLog("[DEBUG] Total configured providers: %d", len(configuredProviders))
for _, p := range configuredProviders {
verboseLog("[DEBUG] - %s", GetProviderDisplayName(p))
}
return configuredProviders, nil
}
// getMapKeys returns the keys of a map for debugging
func getMapKeys(m map[string]interface{}) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
@@ -1,655 +0,0 @@
package auth
import (
"context"
"fmt"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// updateApiConfigurationPartial is a helper that calls the gRPC method with optional verbose logging.
// This replaces the Manager.updateApiConfigurationPartial method to keep auth-specific code in the auth package.
func updateApiConfigurationPartial(ctx context.Context, manager *task.Manager, request *cline.UpdateApiConfigurationPartialRequest) error {
if global.Config.Verbose {
fmt.Println("[DEBUG] Updating API configuration (partial)")
if request.UpdateMask != nil && len(request.UpdateMask.Paths) > 0 {
fmt.Printf("[DEBUG] Field mask paths: %v\n", request.UpdateMask.Paths)
}
if request.ApiConfiguration != nil {
apiConfig := request.ApiConfiguration
if apiConfig.PlanModeApiProvider != nil {
fmt.Printf("[DEBUG] Plan mode provider: %s\n", *apiConfig.PlanModeApiProvider)
}
if apiConfig.ActModeApiProvider != nil {
fmt.Printf("[DEBUG] Act mode provider: %s\n", *apiConfig.ActModeApiProvider)
}
}
}
// Call the Models service to update API configuration
_, err := manager.GetClient().Models.UpdateApiConfigurationPartial(ctx, request)
if err != nil {
return fmt.Errorf("failed to update API configuration (partial): %w", err)
}
if global.Config.Verbose {
fmt.Println("[DEBUG] API configuration updated successfully (partial)")
}
return nil
}
// ProviderFields defines all the field names associated with a specific provider
type ProviderFields struct {
APIKeyField string // API key field name (e.g., "apiKey", "openAiApiKey")
BaseURLField string // Base URL field name (optional, empty if not applicable)
PlanModeModelIDField string // Plan mode model ID field (e.g., "planModeApiModelId")
ActModeModelIDField string // Act mode model ID field (e.g., "actModeApiModelId")
PlanModeModelInfoField string // Plan mode model info field (optional, empty if not applicable)
ActModeModelInfoField string // Act mode model info field (optional, empty if not applicable)
// Provider-specific additional model ID fields
PlanModeProviderSpecificModelIDField string // e.g., "planModeOpenRouterModelId"
ActModeProviderSpecificModelIDField string // e.g., "actModeOpenRouterModelId"
UseProfileField string // e.g., "awsUseProfile" (for bedrock) (optional, empty if not applicable)
}
// GetProviderFields returns the field mapping for a given provider
func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
switch provider {
case cline.ApiProvider_ANTHROPIC:
return ProviderFields{
APIKeyField: "apiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_OPENAI:
return ProviderFields{
APIKeyField: "openAiApiKey",
BaseURLField: "openAiBaseUrl",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeOpenAiModelId",
ActModeProviderSpecificModelIDField: "actModeOpenAiModelId",
}, nil
case cline.ApiProvider_OPENROUTER:
return ProviderFields{
APIKeyField: "openRouterApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeOpenRouterModelInfo",
ActModeModelInfoField: "actModeOpenRouterModelInfo",
PlanModeProviderSpecificModelIDField: "planModeOpenRouterModelId",
ActModeProviderSpecificModelIDField: "actModeOpenRouterModelId",
}, nil
case cline.ApiProvider_XAI:
return ProviderFields{
APIKeyField: "xaiApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_BEDROCK:
return ProviderFields{
UseProfileField: "awsUseProfile",
APIKeyField: "awsAccessKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeAwsBedrockCustomModelBaseId",
ActModeProviderSpecificModelIDField: "actModeAwsBedrockCustomModelBaseId",
}, nil
case cline.ApiProvider_GEMINI:
return ProviderFields{
APIKeyField: "geminiApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_OPENAI_NATIVE:
return ProviderFields{
APIKeyField: "openAiNativeApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_OLLAMA:
return ProviderFields{
APIKeyField: "ollamaBaseUrl",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeOllamaModelId",
ActModeProviderSpecificModelIDField: "actModeOllamaModelId",
}, nil
case cline.ApiProvider_CEREBRAS:
return ProviderFields{
APIKeyField: "cerebrasApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
}, nil
case cline.ApiProvider_CLINE:
return ProviderFields{
APIKeyField: "clineApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeOpenRouterModelInfo",
ActModeModelInfoField: "actModeOpenRouterModelInfo",
PlanModeProviderSpecificModelIDField: "planModeOpenRouterModelId",
ActModeProviderSpecificModelIDField: "actModeOpenRouterModelId",
}, nil
case cline.ApiProvider_OCA:
return ProviderFields{
APIKeyField: "ocaApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeOcaModelInfo",
ActModeModelInfoField: "actModeOcaModelInfo",
PlanModeProviderSpecificModelIDField: "planModeOcaModelId",
ActModeProviderSpecificModelIDField: "actModeOcaModelId",
}, nil
case cline.ApiProvider_HICAP:
return ProviderFields{
APIKeyField: "hicapApiKey",
PlanModeModelInfoField: "planModeHicapModelInfo",
ActModeModelInfoField: "actModeHicapModelInfo",
PlanModeProviderSpecificModelIDField: "planModeHicapModelId",
ActModeProviderSpecificModelIDField: "actModeHicapModelId",
}, nil
case cline.ApiProvider_NOUSRESEARCH:
return ProviderFields{
APIKeyField: "nousResearchApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeNousResearchModelId",
ActModeProviderSpecificModelIDField: "actModeNousResearchModelId",
}, nil
default:
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
}
}
// ProviderUpdatesPartial defines optional fields for partial provider updates
// Uses pointers to distinguish between "not provided" and "set to empty"
type ProviderUpdatesPartial struct {
ModelID *string // New model ID (optional)
APIKey *string // New API key (optional)
ModelInfo interface{} // New model info (optional, provider-specific)
BaseURL *string // New base URL (optional, e.g., for OCA, Ollama)
RefreshToken *string // New refresh token (optional, e.g., for OCA)
Mode *string // New mode (optional, e.g., "internal" or "external" for OCA)
}
// GetModelIDFieldName returns the appropriate model ID field name for a provider and mode.
// This helper centralizes the logic for determining whether to use provider-specific
// or generic model ID fields.
func GetModelIDFieldName(provider cline.ApiProvider, mode string) (string, error) {
fields, err := GetProviderFields(provider)
if err != nil {
return "", err
}
if mode == "plan" {
// Use provider-specific field if available, otherwise use generic field
if fields.PlanModeProviderSpecificModelIDField != "" {
return fields.PlanModeProviderSpecificModelIDField, nil
}
return fields.PlanModeModelIDField, nil
}
// Act mode
if fields.ActModeProviderSpecificModelIDField != "" {
return fields.ActModeProviderSpecificModelIDField, nil
}
return fields.ActModeModelIDField, nil
}
// buildProviderFieldMask builds a list of camelCase field paths for the field mask.
// When includeProviderEnums is true, the provider enum fields are included (for setting active provider).
// When false, only the data fields are included (for configuring without activating).
func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeModelID bool, includeModelInfo bool, includeBaseURL bool, includeProviderEnums bool) []string {
var fieldPaths []string
// Include provider enums if requested (used when setting active provider)
if includeProviderEnums {
fieldPaths = append(fieldPaths, "planModeApiProvider", "actModeApiProvider")
}
// Add API key field if requested
if includeAPIKey {
fieldPaths = append(fieldPaths, fields.APIKeyField)
// Special case: Bedrock also needs secret key
if fields.APIKeyField == "awsAccessKey" {
fieldPaths = append(fieldPaths, "awsSecretKey")
}
}
// Add base URL field if requested and applicable
if includeBaseURL && fields.BaseURLField != "" {
fieldPaths = append(fieldPaths, fields.BaseURLField)
}
// Add model ID fields if requested
if includeModelID {
// Only include provider-specific fields if they exist, otherwise use generic fields
if fields.PlanModeProviderSpecificModelIDField != "" {
// Provider has specific fields - use ONLY those
fieldPaths = append(fieldPaths, fields.PlanModeProviderSpecificModelIDField)
fieldPaths = append(fieldPaths, fields.ActModeProviderSpecificModelIDField)
} else {
// Provider uses generic fields - update those
fieldPaths = append(fieldPaths, fields.PlanModeModelIDField)
fieldPaths = append(fieldPaths, fields.ActModeModelIDField)
}
}
// Add model info fields if requested and applicable
if includeModelInfo && fields.PlanModeModelInfoField != "" {
fieldPaths = append(fieldPaths, fields.PlanModeModelInfoField)
fieldPaths = append(fieldPaths, fields.ActModeModelInfoField)
}
return fieldPaths
}
// setAPIKeyField sets the appropriate API key field in the config based on the field name
func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "apiKey":
apiConfig.ApiKey = value
case "openAiApiKey":
apiConfig.OpenAiApiKey = value
case "openAiNativeApiKey":
apiConfig.OpenAiNativeApiKey = value
case "openRouterApiKey":
apiConfig.OpenRouterApiKey = value
case "xaiApiKey":
apiConfig.XaiApiKey = value
case "awsAccessKey":
apiConfig.AwsAccessKey = value
case "geminiApiKey":
apiConfig.GeminiApiKey = value
case "ollamaBaseUrl":
apiConfig.OllamaBaseUrl = value
case "cerebrasApiKey":
apiConfig.CerebrasApiKey = value
case "clineApiKey":
apiConfig.ClineApiKey = value
case "ocaApiKey":
apiConfig.OcaApiKey = value
case "hicapApiKey":
apiConfig.HicapApiKey = value
case "nousResearchApiKey":
apiConfig.NousResearchApiKey = value
}
}
// setProviderSpecificModelID sets the appropriate provider-specific model ID fields when possible
func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "planModeOpenAiModelId":
apiConfig.PlanModeOpenAiModelId = value
apiConfig.ActModeOpenAiModelId = value
case "planModeOpenRouterModelId":
apiConfig.PlanModeOpenRouterModelId = value
apiConfig.ActModeOpenRouterModelId = value
case "planModeOllamaModelId":
apiConfig.PlanModeOllamaModelId = value
apiConfig.ActModeOllamaModelId = value
case "planModeAwsBedrockCustomModelBaseId":
apiConfig.PlanModeAwsBedrockCustomModelBaseId = value
apiConfig.ActModeAwsBedrockCustomModelBaseId = value
case "planModeOcaModelId":
apiConfig.PlanModeOcaModelId = value
apiConfig.ActModeOcaModelId = value
case "planModeHicapModelId":
apiConfig.PlanModeHicapModelId = value
apiConfig.ActModeHicapModelId = value
case "planModeNousResearchModelId":
apiConfig.PlanModeNousResearchModelId = value
apiConfig.ActModeNousResearchModelId = value
}
}
// AddProviderPartial configures a new provider with all necessary fields using partial updates.
func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID string, apiKey string, baseURL string, modelInfo interface{}) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
return err
}
// Build a ModelsApiConfiguration with only the relevant provider fields set
apiConfig := &cline.ModelsApiConfiguration{}
// Set API key field
if apiKey != "" || fields.APIKeyField != "ollamaBaseUrl" {
setAPIKeyField(apiConfig, fields.APIKeyField, proto.String(apiKey))
}
// Set base URL field if provided and applicable
includeBaseURL := false
if baseURL != "" && fields.BaseURLField != "" {
setBaseURLField(apiConfig, fields.BaseURLField, proto.String(baseURL))
includeBaseURL = true
}
// Set model ID fields
apiConfig.PlanModeApiModelId = proto.String(modelID)
apiConfig.ActModeApiModelId = proto.String(modelID)
// Set provider-specific model ID fields if applicable
if fields.PlanModeProviderSpecificModelIDField != "" {
setProviderSpecificModelID(apiConfig, fields.PlanModeProviderSpecificModelIDField, proto.String(modelID))
}
// Set model info if applicable and provided
if fields.PlanModeModelInfoField != "" && modelInfo != nil {
if openRouterInfo, ok := modelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
} else if ocaInfo, ok := modelInfo.(*cline.OcaModelInfo); ok {
apiConfig.PlanModeOcaModelInfo = ocaInfo
apiConfig.ActModeOcaModelInfo = ocaInfo
}
}
// Build field mask including all fields we're setting (without provider enums)
includeModelInfo := fields.PlanModeModelInfoField != "" && modelInfo != nil
fieldPaths := buildProviderFieldMask(fields, true, true, includeModelInfo, includeBaseURL, false)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to update API configuration: %w", err)
}
return nil
}
// UpdateProviderPartial updates specific fields for an existing provider using partial updates.
// If setAsActive is true, this will also set the provider as the active provider for both Plan and Act modes.
func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, updates ProviderUpdatesPartial, setAsActive bool) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
return err
}
// Build a ModelsApiConfiguration with only the fields being updated
apiConfig := &cline.ModelsApiConfiguration{}
// Set provider enum for BOTH Plan and Act modes if setAsActive is true
if setAsActive {
apiConfig.PlanModeApiProvider = &provider
apiConfig.ActModeApiProvider = &provider
}
// Track what we're updating for field mask
includeAPIKey := updates.APIKey != nil
includeModelID := updates.ModelID != nil
includeModelInfo := updates.ModelInfo != nil && fields.PlanModeModelInfoField != ""
// Update API key if provided
if updates.APIKey != nil {
setAPIKeyField(apiConfig, fields.APIKeyField, updates.APIKey)
}
// Update model ID if provided
if updates.ModelID != nil {
// Only set provider-specific fields if they exist, otherwise use generic fields
if fields.PlanModeProviderSpecificModelIDField != "" {
setProviderSpecificModelID(apiConfig, fields.PlanModeProviderSpecificModelIDField, updates.ModelID)
} else {
// Provider uses generic fields - set those
apiConfig.PlanModeApiModelId = updates.ModelID
apiConfig.ActModeApiModelId = updates.ModelID
}
}
// Update model info if provided
if updates.ModelInfo != nil && fields.PlanModeModelInfoField != "" {
if openRouterInfo, ok := updates.ModelInfo.(*cline.OpenRouterModelInfo); ok {
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
} else if ocaInfo, ok := updates.ModelInfo.(*cline.OcaModelInfo); ok {
apiConfig.PlanModeOcaModelInfo = ocaInfo
apiConfig.ActModeOcaModelInfo = ocaInfo
}
}
// Build field mask for only the fields being updated
fieldPaths := buildProviderFieldMask(fields, includeAPIKey, includeModelID, includeModelInfo, false, setAsActive)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to update API configuration: %w", err)
}
return nil
}
// RemoveProviderPartial removes a provider by clearing its API key using partial updates
func RemoveProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
return err
}
// Build an EMPTY ModelsApiConfiguration (or one with empty API key field)
// Fields in the mask without values will be cleared
apiConfig := &cline.ModelsApiConfiguration{}
// Build field mask with only the API key field(s)
// For Bedrock, include both access key and secret key
fieldPaths := []string{fields.APIKeyField}
if provider == cline.ApiProvider_BEDROCK {
fieldPaths = append(fieldPaths, "awsSecretKey")
}
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update (clearing API key by including in mask without value)
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to update API configuration: %w", err)
}
return nil
}
// setBaseURLField sets the appropriate base URL field in the config based on the field name
func setBaseURLField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "ocaBaseUrl":
apiConfig.OcaBaseUrl = value
case "ollamaBaseUrl":
apiConfig.OllamaBaseUrl = value
case "openAiBaseUrl":
apiConfig.OpenAiBaseUrl = value
case "geminiBaseUrl":
apiConfig.GeminiBaseUrl = value
case "liteLlmBaseUrl":
apiConfig.LiteLlmBaseUrl = value
case "anthropicBaseUrl":
apiConfig.AnthropicBaseUrl = value
case "requestyBaseUrl":
apiConfig.RequestyBaseUrl = value
case "lmStudioBaseUrl":
apiConfig.LmStudioBaseUrl = value
case "oca":
apiConfig.OcaBaseUrl = value
}
}
// setRefreshTokenField sets the appropriate refresh token field in the config
func setRefreshTokenField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "ocaRefreshToken":
apiConfig.OcaRefreshToken = value
}
}
// setModeField sets the appropriate mode field in the config
func setModeField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
switch fieldName {
case "ocaMode":
apiConfig.OcaMode = value
}
}
// BedrockOptionalFields holds optional configuration fields for AWS Bedrock
type BedrockOptionalFields struct {
SessionToken *string // Optional: AWS session token for temporary credentials
Region *string // Optional: AWS region
UseCrossRegionInference *bool // Optional: Enable cross-region inference
UseGlobalInference *bool // Optional: Use global inference endpoint
UsePromptCache *bool // Optional: Enable prompt caching
Authentication *string // Optional: Authentication method
UseProfile *bool // Optional: Use AWS profile
Profile *string // Optional: AWS profile name
Endpoint *string // Optional: Custom endpoint URL
}
// OcaOptionalFields holds optional configuration fields for Oracle Code Assist
type OcaOptionalFields struct {
BaseURL *string // Optional: Base URL
Mode *string // Optional: Mode ("internal" or "external")
}
// setBedrockOptionalFields sets optional Bedrock-specific fields in the API configuration
func setBedrockOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *BedrockOptionalFields) {
if fields == nil {
return
}
if fields.SessionToken != nil {
apiConfig.AwsSessionToken = fields.SessionToken
}
if fields.Region != nil {
apiConfig.AwsRegion = fields.Region
}
if fields.UseCrossRegionInference != nil {
apiConfig.AwsUseCrossRegionInference = fields.UseCrossRegionInference
}
if fields.UseGlobalInference != nil {
apiConfig.AwsUseGlobalInference = fields.UseGlobalInference
}
if fields.UsePromptCache != nil {
apiConfig.AwsBedrockUsePromptCache = fields.UsePromptCache
}
if fields.Authentication != nil {
apiConfig.AwsAuthentication = fields.Authentication
}
if fields.UseProfile != nil {
apiConfig.AwsUseProfile = fields.UseProfile
}
if fields.Profile != nil {
apiConfig.AwsProfile = fields.Profile
}
if fields.Endpoint != nil {
apiConfig.AwsBedrockEndpoint = fields.Endpoint
}
}
// setOcaOptionalFields sets optional Oca-specific fields in the API configuration
func setOcaOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *OcaOptionalFields) {
if fields == nil {
return
}
if fields.Mode != nil {
apiConfig.OcaMode = fields.Mode
}
if fields.BaseURL != nil {
apiConfig.OcaBaseUrl = fields.BaseURL
}
}
// buildBedrockOptionalFieldMask builds field mask paths for Bedrock optional fields that have values
func buildBedrockOptionalFieldMask(fields *BedrockOptionalFields) []string {
if fields == nil {
return nil
}
var fieldPaths []string
if fields.SessionToken != nil {
fieldPaths = append(fieldPaths, "awsSessionToken")
}
if fields.Region != nil {
fieldPaths = append(fieldPaths, "awsRegion")
}
if fields.UseCrossRegionInference != nil {
fieldPaths = append(fieldPaths, "awsUseCrossRegionInference")
}
if fields.UseGlobalInference != nil {
fieldPaths = append(fieldPaths, "awsUseGlobalInference")
}
if fields.UsePromptCache != nil {
fieldPaths = append(fieldPaths, "awsBedrockUsePromptCache")
}
if fields.Authentication != nil {
fieldPaths = append(fieldPaths, "awsAuthentication")
}
if fields.UseProfile != nil {
fieldPaths = append(fieldPaths, "awsUseProfile")
}
if fields.Profile != nil {
fieldPaths = append(fieldPaths, "awsProfile")
}
if fields.Endpoint != nil {
fieldPaths = append(fieldPaths, "awsBedrockEndpoint")
}
return fieldPaths
}
// buildOcaOptionalFieldMask builds field mask paths for Bedrock optional fields that have values
func buildOcaOptionalFieldMask(fields *OcaOptionalFields) []string {
if fields == nil {
return nil
}
var fieldPaths []string
if fields.Mode != nil {
fieldPaths = append(fieldPaths, "ocaMode")
}
if fields.BaseURL != nil {
fieldPaths = append(fieldPaths, "ocaBaseUrl")
}
return fieldPaths
}
-763
View File
@@ -1,763 +0,0 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// ProviderWizard handles the interactive provider configuration process
type ProviderWizard struct {
ctx context.Context
manager *task.Manager
}
// NewProviderWizard prepares a new provider configuration wizard
func NewProviderWizard(ctx context.Context) (*ProviderWizard, error) {
// Create task manager using auth instance from context
manager, err := createTaskManager(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create task manager: %w", err)
}
return &ProviderWizard{
ctx: ctx,
manager: manager,
}, nil
}
// showMainMenu displays the main provider configuration menu
func (pw *ProviderWizard) showMainMenu() (string, error) {
var action string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("What would you like to do?").
Options(
huh.NewOption("Add or change an API provider", "add"),
huh.NewOption("Change model for API provider", "change-model"),
huh.NewOption("Remove a provider", "remove"),
huh.NewOption("List configured providers", "list"),
huh.NewOption("Return to main auth menu", "back"),
).
Value(&action),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to get menu choice: %w", err)
}
return action, nil
}
// Run runs the provider configuration wizard
func (pw *ProviderWizard) Run() error {
for {
action, err := pw.showMainMenu()
if err != nil {
return err
}
switch action {
case "add":
if err := pw.handleAddProvider(); err != nil {
return err
}
case "change-model":
if err := pw.handleChangeModel(); err != nil {
return err
}
case "remove":
if err := pw.handleRemoveProvider(); err != nil {
return err
}
case "list":
if err := pw.handleListProviders(); err != nil {
return err
}
case "back":
// Return to main auth menu
return HandleAuthMenuNoArgs(pw.ctx)
}
fmt.Println()
}
}
// "Add a new provider" > handleAddProvider
func (pw *ProviderWizard) handleAddProvider() error {
// Step 1: Select provider
provider, err := SelectBYOProvider()
if err != nil {
if strings.Contains(err.Error(), "cancelled") {
return nil
}
return fmt.Errorf("provider selection failed: %w", err)
}
// Step 2: Special handling for Bedrock provider
if provider == cline.ApiProvider_BEDROCK {
return pw.handleAddBedrockProvider()
}
// Step 2b: Special handling for OCA provider
if provider == cline.ApiProvider_OCA {
return pw.handleAddOcaProvider()
}
// Step 3: Get API key first (for non-Bedrock providers)
apiKey, baseURL, err := PromptForAPIKey(provider)
if err != nil {
return fmt.Errorf("failed to get API key: %w", err)
}
// Step 4: Try to fetch models and let user select (with fallback to manual entry for providers that don't support fetch)
modelID, modelInfo, err := pw.selectModel(provider, apiKey)
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 5: Apply configuration using AddProviderPartial
if err := AddProviderPartial(pw.ctx, pw.manager, provider, modelID, apiKey, baseURL, modelInfo); err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}
if err := setWelcomeViewCompleted(pw.ctx, pw.manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
fmt.Println("✓ Provider configured successfully!")
return nil
}
// handleAddBedrockProvider handles the special case of adding Bedrock provider with its multi-field form
func (pw *ProviderWizard) handleAddBedrockProvider() error {
// Step 1: Get Bedrock configuration (all credentials and optional fields)
config, err := PromptForBedrockConfig(pw.ctx, pw.manager)
if err != nil {
if strings.Contains(err.Error(), "user declined profile authentication") {
return nil
}
return fmt.Errorf("failed to get Bedrock configuration: %w", err)
}
// Step 2: Select model
modelID, modelInfo, err := pw.selectModel(cline.ApiProvider_BEDROCK, "")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 3: Apply Bedrock configuration
if err := ApplyBedrockConfig(pw.ctx, pw.manager, config, modelID, modelInfo); err != nil {
return fmt.Errorf("failed to save Bedrock configuration: %w", err)
}
if err := setWelcomeViewCompleted(pw.ctx, pw.manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
fmt.Println("✓ Bedrock provider configured successfully!")
return nil
}
// handleAddOcaProvider handles adding Oracle Code Assist provider with optional settings and auth
func (pw *ProviderWizard) handleAddOcaProvider() error {
// Step 1: Get OCA configuration (base URL and mode)
config, err := PromptForOcaConfig(pw.ctx, pw.manager)
if err != nil {
if strings.Contains(err.Error(), "user aborted") || strings.Contains(err.Error(), "cancelled") {
return nil
}
return fmt.Errorf("failed to get OCA configuration: %w", err)
}
// Apply OCA configuration (base URL and mode)
if err := ApplyOcaConfig(pw.ctx, pw.manager, config); err != nil {
return fmt.Errorf("failed to save OCA configuration: %w", err)
}
// Step 2: Ensure OCA authentication
if err := ensureOcaAuthenticated(pw.ctx); err != nil {
return fmt.Errorf("failed to authenticate with OCA: %w", err)
}
// Step 3: Select model
modelID, modelInfo, err := pw.selectModel(cline.ApiProvider_OCA, "")
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 4: Apply the OCA model configuration and set as active
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
if err := UpdateProviderPartial(pw.ctx, pw.manager, cline.ApiProvider_OCA, updates, true); err != nil {
return fmt.Errorf("failed to save OCA configuration: %w", err)
}
if err := setWelcomeViewCompleted(pw.ctx, pw.manager); err != nil {
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
}
fmt.Println("✓ OCA provider configured successfully!")
return nil
}
// handleListProviders retrieves and displays configured providers
func (pw *ProviderWizard) handleListProviders() error {
result, err := GetProviderConfigurations(pw.ctx)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
output := FormatProviderList(result)
fmt.Println(output)
return nil
}
// selectModel attempts to fetch available models and let user select, or falls back to manual entry
func (pw *ProviderWizard) selectModel(provider cline.ApiProvider, apiKey string) (string, interface{}, error) {
// For providers that support model fetching, try to fetch and display models
canFetchModels := pw.supportsModelFetching(provider)
if canFetchModels {
fmt.Println("Fetching available models...")
models, modelInfoMap, err := pw.fetchModelsForProvider(provider, apiKey)
if err != nil {
fmt.Println("\n⚠ Unable to fetch model list from the provider. Please enter the model ID manually instead.")
if global.Config.Verbose {
fmt.Printf(" Error details: %v\n", err)
}
return pw.manualModelEntry(provider)
}
if len(models) == 0 {
fmt.Println("\n⚠ No models found from the provider. Please enter the model ID manually instead.")
return pw.manualModelEntry(provider)
}
// Let user select from available models (includes manual entry option)
modelID, err := pw.selectFromAvailableModels(models)
if err != nil {
return "", nil, fmt.Errorf("model selection failed: %w", err)
}
// Check if user chose manual entry
const manualEntryKey = "__MANUAL_ENTRY__"
if modelID == manualEntryKey {
return pw.manualModelEntry(provider)
}
// Get the model info for the selected model
var modelInfo interface{}
if modelInfoMap != nil {
modelInfo = modelInfoMap[modelID]
}
return modelID, modelInfo, nil
}
// For providers without model fetching support, use manual entry
return pw.manualModelEntry(provider)
}
// supportsModelFetching returns true if the provider supports fetching models
func (pw *ProviderWizard) supportsModelFetching(provider cline.ApiProvider) bool {
return SupportsBYOModelFetching(provider)
}
// fetchModelsForProvider fetches models for a given provider
// Supports both dynamic API fetching (OpenRouter, OpenAI, Ollama) and static model lists (Anthropic, Bedrock, Gemini, X AI)
func (pw *ProviderWizard) fetchModelsForProvider(provider cline.ApiProvider, apiKey string) ([]string, map[string]interface{}, error) {
// Try dynamic/remote model fetching first
switch provider {
case cline.ApiProvider_OPENROUTER:
models, err := FetchOpenRouterModels(pw.ctx, pw.manager)
if err != nil {
return nil, nil, err
}
interfaceMap := ConvertOpenRouterModelsToInterface(models)
return ConvertModelsMapToSlice(interfaceMap), interfaceMap, nil
case cline.ApiProvider_OPENAI:
// For OpenAI, we need to pass the base URL and API key
baseURL := "https://api.openai.com/v1" // Default OpenAI API base URL
modelIDs, err := FetchOpenAiModels(pw.ctx, pw.manager, baseURL, apiKey)
if err != nil {
return nil, nil, err
}
// OpenAI returns just model IDs without additional info, so modelInfo map is nil
return modelIDs, nil, nil
case cline.ApiProvider_OLLAMA:
// For Ollama, apiKey actually contains the base URL (or empty for default)
baseURL := apiKey // The "API key" field for Ollama is actually the base URL
modelIDs, err := FetchOllamaModels(pw.ctx, pw.manager, baseURL)
if err != nil {
return nil, nil, err
}
// Ollama returns just model IDs without additional info, so modelInfo map is nil
return modelIDs, nil, nil
case cline.ApiProvider_OCA:
// OCA supports dynamic model fetching
models, err := FetchOcaModels(pw.ctx, pw.manager)
if err != nil {
return nil, nil, err
}
interfaceMap := ConvertOcaModelsToInterface(models)
return ConvertModelsMapToSlice(interfaceMap), interfaceMap, nil
}
// Fall back to static models for providers that don't support dynamic fetching
if SupportsStaticModelList(provider) {
modelIDs, _, err := FetchStaticModels(provider)
if err != nil {
return nil, nil, err
}
// Static models don't have detailed info maps for now, so modelInfo map is nil
return modelIDs, nil, nil
}
return nil, nil, fmt.Errorf("model fetching not supported for provider: %v", provider)
}
// selectFromAvailableModels displays available models and lets user select one.
// Includes an option to enter a model ID manually in case the desired model isn't listed.
func (pw *ProviderWizard) selectFromAvailableModels(models []string) (string, error) {
if len(models) == 0 {
return "", fmt.Errorf("no models available")
}
// Add a special "manual entry" option at the end
const manualEntryKey = "__MANUAL_ENTRY__"
// Use model ID as the value (not index)
var selectedModel string
options := make([]huh.Option[string], len(models)+1)
for i, model := range models {
options[i] = huh.NewOption(model, model)
}
// Add manual entry option at the end
options[len(models)] = huh.NewOption("Enter model ID manually...", manualEntryKey)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select a model").
Options(options...).
Height(calculateSelectHeight()).
Filtering(true).
Value(&selectedModel),
),
)
if err := form.Run(); err != nil {
return "", fmt.Errorf("failed to select model: %w", err)
}
// If user selected manual entry, return special key to trigger manual input
if selectedModel == manualEntryKey {
return manualEntryKey, nil
}
return selectedModel, nil
}
// manualModelEntry prompts user to manually enter a model ID.
// Returns the model ID and an error. The modelInfo is always nil for manual entry.
func (pw *ProviderWizard) manualModelEntry(provider cline.ApiProvider) (string, interface{}, error) {
var modelID string
modelPlaceholder := GetBYOProviderPlaceholder(provider)
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Model ID").
Placeholder(modelPlaceholder).
Value(&modelID).
Validate(func(s string) error {
// Trim whitespace and validate
trimmed := strings.TrimSpace(s)
if trimmed == "" {
return fmt.Errorf("model ID cannot be empty")
}
return nil
}),
),
)
if err := form.Run(); err != nil {
return "", nil, fmt.Errorf("failed to get model ID: %w", err)
}
// Trim whitespace from the final value
modelID = strings.TrimSpace(modelID)
// modelInfo is always nil for manual entry
return modelID, nil, nil
}
// handleChangeModel allows changing the model for any configured provider
func (pw *ProviderWizard) handleChangeModel() error {
// Step 1: Get current provider configurations
result, err := GetProviderConfigurations(pw.ctx)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
// Step 2: Get all configured providers with models
readyProviders := result.GetAllReadyProviders()
// Filter out Cline provider (it has its own model changer in the main menu)
var configurableProviders []*ProviderDisplay
for _, provider := range readyProviders {
if provider.Provider != cline.ApiProvider_CLINE {
configurableProviders = append(configurableProviders, provider)
}
}
// Step 3: Check if there are any configurable providers
if len(configurableProviders) == 0 {
fmt.Println("\nNo configurable providers found.")
fmt.Println("Note: Cline provider has its own model selection in the main menu.")
return nil
}
// Step 4: Let user select which provider to change the model for
var selectedIndex int
options := make([]huh.Option[int], len(configurableProviders)+1)
for i, providerDisplay := range configurableProviders {
displayName := fmt.Sprintf("%s (current: %s)",
GetProviderDisplayName(providerDisplay.Provider),
providerDisplay.ModelID)
options[i] = huh.NewOption(displayName, i)
}
options[len(configurableProviders)] = huh.NewOption("(Cancel)", -1)
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[int]().
Title("Select provider to change model for").
Options(options...).
Value(&selectedIndex),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("failed to select provider: %w", err)
}
if selectedIndex == -1 {
return nil
}
selectedProvider := configurableProviders[selectedIndex]
provider := selectedProvider.Provider
fmt.Printf("\nChanging model for %s\n", GetProviderDisplayName(provider))
fmt.Printf("Current model: %s\n\n", selectedProvider.ModelID)
// Step 5: Retrieve API key if needed for model fetching
var apiKey string
if pw.supportsModelFetching(provider) {
// For providers that support fetching, we need to retrieve the API key from state
state, err := pw.manager.GetClient().State.GetLatestState(pw.ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get state: %w", err)
}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return fmt.Errorf("failed to parse state JSON: %w", err)
}
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
return fmt.Errorf("no API configuration found in state")
}
apiKey = getProviderAPIKeyFromState(apiConfig, provider)
if apiKey == "" {
return fmt.Errorf("no API key found for provider %s", GetProviderDisplayName(provider))
}
}
modelID, modelInfo, err := pw.selectModel(provider, apiKey)
if err != nil {
return fmt.Errorf("model selection failed: %w", err)
}
// Step 6: Apply the model change (for both Plan and Act modes)
if err := pw.applyModelChange(provider, modelID, modelInfo); err != nil {
return fmt.Errorf("failed to apply model change: %w", err)
}
fmt.Printf("✓ Model changed successfully to: %s\n", modelID)
fmt.Println(" (Applied to both Plan and Act modes)")
return nil
}
// applyModelChange applies a model change for both Plan and Act modes using UpdateProviderPartial
func (pw *ProviderWizard) applyModelChange(provider cline.ApiProvider, modelID string, modelInfo interface{}) error {
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
return UpdateProviderPartial(pw.ctx, pw.manager, provider, updates, true)
}
// SwitchToBYOProvider switches to a BYO provider that's already configured.
// It retrieves the existing model configuration and sets it as the active provider for both Plan and Act modes.
func SwitchToBYOProvider(ctx context.Context, manager *task.Manager, provider cline.ApiProvider) error {
// Get the current state to retrieve the model ID and model info for this provider
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get state: %w", err)
}
// Parse state JSON
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return fmt.Errorf("failed to parse state JSON: %w", err)
}
// Extract apiConfiguration
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
if !ok {
return fmt.Errorf("no API configuration found in state")
}
// Get the model ID for the selected provider
modelID := getProviderModelIDFromState(apiConfig, provider)
if modelID == "" {
return fmt.Errorf("no model configured for provider %s", GetProviderDisplayName(provider))
}
// Get model info if available (for OpenRouter/Cline)
var modelInfo interface{}
if provider == cline.ApiProvider_OPENROUTER || provider == cline.ApiProvider_CLINE {
if modelInfoData, ok := apiConfig["planModeOpenRouterModelInfo"].(map[string]interface{}); ok {
modelInfo = convertMapToOpenRouterModelInfo(modelInfoData)
}
}
// Use UpdateProviderPartial to switch to this provider
updates := ProviderUpdatesPartial{
ModelID: &modelID,
ModelInfo: modelInfo,
}
if err := UpdateProviderPartial(ctx, manager, provider, updates, true); err != nil {
return fmt.Errorf("failed to switch provider: %w", err)
}
verboseLog("✓ Switched to %s\n", GetProviderDisplayName(provider))
verboseLog(" Using model: %s\n", modelID)
return HandleAuthMenuNoArgs(ctx)
}
// getProviderModelIDFromState retrieves the model ID for a specific provider from state
func getProviderModelIDFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
modelKey, err := GetModelIDFieldName(provider, "plan")
if err != nil {
return ""
}
if modelID, ok := stateData[modelKey].(string); ok {
return modelID
}
return ""
}
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
func getProviderAPIKeyFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
// OCA uses account authentication, not API keys. Consider it "present" if authenticated.
if provider == cline.ApiProvider_OCA {
if state, _ := GetLatestOCAState(context.TODO(), 2*time.Second); state != nil && state.User != nil {
// Return a sentinel non-empty string so upstream checks pass.
return "OCA_AUTH_VERIFIED"
}
return ""
}
fields, err := GetProviderFields(provider)
if err != nil {
return ""
}
if apiKey, ok := stateData[fields.APIKeyField].(string); ok {
return apiKey
}
return ""
}
// convertMapToOpenRouterModelInfo converts a map to OpenRouterModelInfo
func convertMapToOpenRouterModelInfo(data map[string]interface{}) *cline.OpenRouterModelInfo {
info := &cline.OpenRouterModelInfo{}
if val, ok := data["description"].(string); ok {
info.Description = &val
}
if val, ok := data["contextWindow"].(float64); ok {
contextWindow := int64(val)
info.ContextWindow = &contextWindow
}
if val, ok := data["maxTokens"].(float64); ok {
maxTokens := int64(val)
info.MaxTokens = &maxTokens
}
if val, ok := data["inputPrice"].(float64); ok {
info.InputPrice = &val
}
if val, ok := data["outputPrice"].(float64); ok {
info.OutputPrice = &val
}
if val, ok := data["cacheWritesPrice"].(float64); ok {
info.CacheWritesPrice = &val
}
if val, ok := data["cacheReadsPrice"].(float64); ok {
info.CacheReadsPrice = &val
}
if val, ok := data["supportsImages"].(bool); ok {
info.SupportsImages = &val
}
if val, ok := data["supportsPromptCache"].(bool); ok {
info.SupportsPromptCache = val
}
return info
}
// handleRemoveProvider allows removing a configured provider by clearing its API key
func (pw *ProviderWizard) handleRemoveProvider() error {
// Step 1: Get current provider configurations
result, err := GetProviderConfigurations(pw.ctx)
if err != nil {
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
}
// Step 2: Get all ready providers
readyProviders := result.GetAllReadyProviders()
// Filter out Cline provider (uses account auth, not API keys)
var removableProviders []*ProviderDisplay
for _, provider := range readyProviders {
if provider.Provider != cline.ApiProvider_CLINE {
removableProviders = append(removableProviders, provider)
}
}
// Step 3: Check if there are providers to remove
if len(removableProviders) == 0 {
fmt.Println("\nNo providers available to remove.")
fmt.Println("Note: Cline provider cannot be removed via this menu.")
return nil
}
// Step 4: Display selection menu
var selectedIndex int
options := make([]huh.Option[int], len(removableProviders))
for i, provider := range removableProviders {
// Mark active provider
displayName := GetProviderDisplayName(provider.Provider)
if result.ActProvider != nil && provider.Provider == result.ActProvider.Provider {
displayName += " (ACTIVE)"
}
options[i] = huh.NewOption(displayName, i)
}
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[int]().
Title("Select provider to remove").
Options(options...).
Value(&selectedIndex),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("failed to select provider: %w", err)
}
selectedProvider := removableProviders[selectedIndex]
// Step 5: Check if trying to remove the active provider
if result.ActProvider != nil && selectedProvider.Provider == result.ActProvider.Provider {
fmt.Printf("\nCannot remove %s because it is currently active.\n", GetProviderDisplayName(selectedProvider.Provider))
fmt.Println("Please switch to a different provider first, then try again.")
return nil
}
// Step 6: Confirm removal
var confirm bool
confirmForm := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(fmt.Sprintf("Are you sure you want to remove %s?", GetProviderDisplayName(selectedProvider.Provider))).
Description("This will clear the API key but preserve the model configuration.").
Value(&confirm),
),
)
if err := confirmForm.Run(); err != nil {
return fmt.Errorf("failed to get confirmation: %w", err)
}
if !confirm {
fmt.Println("Removal cancelled.")
return nil
}
// Step 7: If removing OCA, sign out first
if selectedProvider.Provider == cline.ApiProvider_OCA {
if err := signOutOca(pw.ctx); err != nil {
fmt.Printf("Warning: Failed to sign out of OCA: %v\n", err)
} else {
fmt.Println("Signed out of OCA.")
}
}
// Step 8: Clear the API key for the selected provider
if err := pw.clearProviderAPIKey(selectedProvider.Provider); err != nil {
return fmt.Errorf("failed to remove provider: %w", err)
}
fmt.Printf("\n✓ %s removed successfully\n", GetProviderDisplayName(selectedProvider.Provider))
return nil
}
// clearProviderAPIKey clears the API key field for a specific provider using RemoveProviderPartial
func (pw *ProviderWizard) clearProviderAPIKey(provider cline.ApiProvider) error {
return RemoveProviderPartial(pw.ctx, pw.manager, provider)
}
func signOutOca(ctx context.Context) error {
client, err := global.GetDefaultClient(ctx)
if err != nil {
return err
}
_, err = client.Ocaaccount.OcaAccountLogoutClicked(ctx, &cline.EmptyRequest{})
return err
}
func setWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
return err
}
-200
View File
@@ -1,200 +0,0 @@
package auth
import (
"context"
"fmt"
"strings"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// BedrockConfig holds all AWS Bedrock-specific configuration fields
type BedrockConfig struct {
// Profile authentication fields
UseProfile bool // Always true for successful config
Profile string // Optional: AWS profile name (empty = default)
Region string // Required: AWS region
Endpoint string // Optional: Custom VPC endpoint URL
// Optional features
UseCrossRegionInference bool // Optional: Enable cross-region inference
UseGlobalInference bool // Optional: Use global inference endpoint
UsePromptCache bool // Optional: Enable prompt caching
// Authentication method (always "profile")
Authentication string // Always set to "profile"
// Legacy fields (no longer used in profile-only flow)
AccessKey string // No longer used
SecretKey string // No longer used
SessionToken string // No longer used
}
// PromptForBedrockConfig displays a profile-first authentication form for Bedrock configuration
func PromptForBedrockConfig(ctx context.Context, manager *task.Manager) (*BedrockConfig, error) {
config := &BedrockConfig{}
// First, ask if user wants to use AWS profile authentication
var useProfile bool
profileQuestion := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Do you want to use an AWS profile for authentication?").
Description("AWS profiles are managed via 'aws configure'").
Value(&useProfile).
Affirmative("Yes").
Negative("No").
Inline(true),
),
)
if err := profileQuestion.Run(); err != nil {
return nil, fmt.Errorf("failed to get authentication method: %w", err)
}
// If user declines profile authentication, show message and return error
if !useProfile {
fmt.Println("\nAWS profile authentication is currently the only supported method in the CLI.")
fmt.Println("Please configure an AWS profile using 'aws configure' and try again.")
return nil, fmt.Errorf("user declined profile authentication")
}
// User wants profile auth - collect profile configuration
config.UseProfile = true
config.Authentication = "profile"
// Collect profile name, region, and optional settings
configForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("AWS Profile Name (optional, press Enter for default profile)").
Value(&config.Profile).
Description("Leave empty to use default AWS profile"),
huh.NewInput().
Title("AWS Region (required, e.g., us-east-1)").
Value(&config.Region).
Validate(func(s string) error {
if strings.TrimSpace(s) == "" {
return fmt.Errorf("AWS Region is required")
}
return nil
}),
huh.NewInput().
Title("Custom VPC Endpoint URL (optional)").
Value(&config.Endpoint).
Description("Press Enter to skip"),
huh.NewConfirm().
Title("Enable Prompt Cache? ").
Value(&config.UsePromptCache).
Affirmative("Yes").
Negative("No").
Inline(true),
huh.NewConfirm().
Title("Enable Cross-Region Inference? ").
Value(&config.UseCrossRegionInference).
Affirmative("Yes").
Negative("No").
Inline(true),
huh.NewConfirm().
Title("Use Global Inference Endpoint? ").
Value(&config.UseGlobalInference).
Affirmative("Yes").
Negative("No").
Inline(true),
),
)
if err := configForm.Run(); err != nil {
return nil, fmt.Errorf("failed to get Bedrock configuration: %w", err)
}
// Trim whitespace from string fields
config.Profile = strings.TrimSpace(config.Profile)
config.Region = strings.TrimSpace(config.Region)
config.Endpoint = strings.TrimSpace(config.Endpoint)
return config, nil
}
// ApplyBedrockConfig applies Bedrock configuration using partial updates (profile-only)
func ApplyBedrockConfig(ctx context.Context, manager *task.Manager, config *BedrockConfig, modelID string, modelInfo interface{}) error {
// Build the API configuration with all Bedrock fields
apiConfig := &cline.ModelsApiConfiguration{}
// Set provider for both Plan and Act modes
bedrockProvider := cline.ApiProvider_BEDROCK
apiConfig.PlanModeApiProvider = &bedrockProvider
apiConfig.ActModeApiProvider = &bedrockProvider
// Set model ID field - this is the primary model ID used by Cline Core
apiConfig.PlanModeApiModelId = proto.String(modelID)
apiConfig.ActModeApiModelId = proto.String(modelID)
apiConfig.PlanModeAwsBedrockCustomModelBaseId = proto.String(modelID)
apiConfig.ActModeAwsBedrockCustomModelBaseId = proto.String(modelID)
// Set profile authentication fields (always required)
optionalFields := &BedrockOptionalFields{}
optionalFields.Authentication = proto.String("profile")
optionalFields.UseProfile = proto.Bool(true)
optionalFields.Region = proto.String(config.Region)
// Set profile name (can be empty for default profile)
if config.Profile != "" {
optionalFields.Profile = proto.String(config.Profile)
}
// Set optional fields if provided
if config.Endpoint != "" {
optionalFields.Endpoint = proto.String(config.Endpoint)
}
if config.UseCrossRegionInference {
optionalFields.UseCrossRegionInference = proto.Bool(true)
}
if config.UseGlobalInference {
optionalFields.UseGlobalInference = proto.Bool(true)
}
if config.UsePromptCache {
optionalFields.UsePromptCache = proto.Bool(true)
}
// Apply all fields to the config
setBedrockOptionalFields(apiConfig, optionalFields)
// Build field mask including all fields we're setting (excluding access keys)
fieldPaths := []string{
"planModeApiProvider",
"actModeApiProvider",
"planModeApiModelId",
"actModeApiModelId",
"planModeAwsBedrockCustomModelBaseId",
"actModeAwsBedrockCustomModelBaseId",
}
// Add profile authentication field paths
optionalPaths := buildBedrockOptionalFieldMask(optionalFields)
fieldPaths = append(fieldPaths, optionalPaths...)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to apply Bedrock configuration: %w", err)
}
return nil
}
-366
View File
@@ -1,366 +0,0 @@
package auth
import (
"context"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// OcaConfig holds Oracle Code Assist (OCA) configuration fields
type OcaConfig struct {
BaseURL string
Mode string
}
// PromptForOcaConfig displays a form for OCA configuration (base URL and mode)
func PromptForOcaConfig(ctx context.Context, manager *task.Manager) (*OcaConfig, error) {
config := &OcaConfig{}
var mode string
// Collect optional settings
configForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Base URL").
Value(&config.BaseURL).
Description("Leave empty to use default Base URL"),
huh.NewSelect[string]().
Title("Choose OCA mode (used for authentication)").
Description("Select 'Internal' to use Cline's internal OCA, or 'External' for your own OCA instance").
Options(
huh.NewOption("Internal", "internal"),
huh.NewOption("External", "external"),
).
Value(&mode),
),
)
if err := configForm.Run(); err != nil {
return nil, fmt.Errorf("failed to get OCA configuration: %w", err)
}
// Trim whitespace from string fields
config.BaseURL = strings.TrimSpace(config.BaseURL)
config.Mode = strings.TrimSpace(mode)
return config, nil
}
// ApplyOcaConfig applies OCA configuration using partial updates
func ApplyOcaConfig(ctx context.Context, manager *task.Manager, config *OcaConfig) error {
// Build the API configuration with all OCA fields
apiConfig := &cline.ModelsApiConfiguration{}
// Set profile authentication fields (always required)
optionalFields := &OcaOptionalFields{}
// Set profile name (can be empty for default profile)
if config.BaseURL != "" {
optionalFields.BaseURL = proto.String(config.BaseURL)
}
// Set optional fields if provided
if config.Mode != "" {
optionalFields.Mode = proto.String(config.Mode)
}
// Apply all fields to the config
setOcaOptionalFields(apiConfig, optionalFields)
// Add profile authentication field paths
optionalPaths := buildOcaOptionalFieldMask(optionalFields)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: optionalPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to apply OCA configuration: %w", err)
}
return nil
}
// ===========================
// OCA Auth Listener Singleton
// ===========================
type ocaAuthStream interface {
Recv() (*cline.OcaAuthState, error)
}
// OcaAuthStatusListener manages subscription to OCA auth status updates
type OcaAuthStatusListener struct {
stream ocaAuthStream
updatesCh chan *cline.OcaAuthState
errCh chan error
ctx context.Context
cancel context.CancelFunc
mu sync.RWMutex
lastState *cline.OcaAuthState
firstEventCh chan struct{}
firstEventOnce sync.Once
}
// NewOcaAuthStatusListener creates a new OCA auth status listener
func NewOcaAuthStatusListener(parentCtx context.Context) (*OcaAuthStatusListener, error) {
client, err := global.GetDefaultClient(parentCtx)
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
// Keep the listener alive independently of short-lived caller contexts
ctx, cancel := context.WithCancel(context.Background())
// Subscribe to OCA auth status updates
stream, err := client.Ocaaccount.OcaSubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
if err != nil {
cancel()
return nil, fmt.Errorf("failed to subscribe to OCA auth updates: %w", err)
}
return &OcaAuthStatusListener{
stream: stream,
updatesCh: make(chan *cline.OcaAuthState, 10),
errCh: make(chan error, 1),
ctx: ctx,
cancel: cancel,
firstEventCh: make(chan struct{}),
}, nil
}
// Start begins listening to the auth status update stream
func (l *OcaAuthStatusListener) Start() error {
go l.readStream()
return nil
}
func (l *OcaAuthStatusListener) readStream() {
defer close(l.updatesCh)
defer close(l.errCh)
for {
select {
case <-l.ctx.Done():
return
default:
state, err := l.stream.Recv()
if err != nil {
// Propagate error and exit
if err == io.EOF {
// Treat as error to notify waiters
err = fmt.Errorf("OCA auth status stream closed")
}
select {
case l.errCh <- err:
case <-l.ctx.Done():
}
return
}
l.mu.Lock()
l.lastState = state
l.mu.Unlock()
// Notify first event waiters
l.firstEventOnce.Do(func() { close(l.firstEventCh) })
select {
case l.updatesCh <- state:
case <-l.ctx.Done():
return
}
}
}
}
// WaitForFirstEvent blocks until the first event is received or timeout occurs
func (l *OcaAuthStatusListener) WaitForFirstEvent(timeout time.Duration) error {
// Fast-path if already have a state
l.mu.RLock()
ready := l.lastState != nil
l.mu.RUnlock()
if ready {
return nil
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-l.firstEventCh:
return nil
case <-timer.C:
return fmt.Errorf("timeout waiting for initial OCA auth event")
case <-l.ctx.Done():
return fmt.Errorf("OCA auth listener cancelled")
}
}
// IsAuthenticated returns true if the last known OCA auth state is authenticated
func (l *OcaAuthStatusListener) IsAuthenticated() bool {
l.mu.RLock()
defer l.mu.RUnlock()
return isOCAStateAuthenticated(l.lastState)
}
// WaitForAuthentication waits until OCA authentication succeeds or timeout occurs
func (l *OcaAuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
// If already authenticated, return immediately
if l.IsAuthenticated() {
return nil
}
for {
select {
case <-timer.C:
return fmt.Errorf("OCA authentication timeout after %v - please try again", timeout)
case <-l.ctx.Done():
return fmt.Errorf("OCA authentication cancelled")
case err := <-l.errCh:
return fmt.Errorf("OCA authentication stream error: %w", err)
case state := <-l.updatesCh:
if isOCAStateAuthenticated(state) {
return nil
}
}
}
}
// Stop closes the stream and cleans up resources
func (l *OcaAuthStatusListener) Stop() {
l.cancel()
}
func isOCAStateAuthenticated(state *cline.OcaAuthState) bool {
return state != nil && state.User != nil
}
// Singleton holder
var (
ocaListener *OcaAuthStatusListener
ocaListenerOnce sync.Once
ocaListenerErr error
)
// GetOcaAuthListener returns the OCA auth listener singleton
func GetOcaAuthListener(ctx context.Context) (*OcaAuthStatusListener, error) {
// Allow optional ctx: if nil, use context.TODO(). If already initialized, return singleton.
if ctx == nil {
ctx = context.TODO()
}
ocaListenerOnce.Do(func() {
l, err := NewOcaAuthStatusListener(ctx)
if err != nil {
ocaListenerErr = err
return
}
if err := l.Start(); err != nil {
ocaListenerErr = err
return
}
ocaListener = l
})
return ocaListener, ocaListenerErr
}
// IsOCAAuthenticated returns true if the global OCA auth status is authenticated.
// It attempts a brief wait for the first event to avoid stale reads.
func IsOCAAuthenticated(ctx context.Context) bool {
l, err := GetOcaAuthListener(ctx)
if err != nil {
return false
}
_ = l.WaitForFirstEvent(1 * time.Second) // best-effort
return l.IsAuthenticated()
}
// LatestState returns the last received OCA auth state (may be nil)
func (l *OcaAuthStatusListener) LatestState() *cline.OcaAuthState {
l.mu.RLock()
defer l.mu.RUnlock()
return l.lastState
}
// GetLatestOCAState returns the latest known OCA auth state, optionally waiting for the first event
func GetLatestOCAState(ctx context.Context, timeout time.Duration) (*cline.OcaAuthState, error) {
l, err := GetOcaAuthListener(ctx)
if err != nil {
return nil, err
}
if timeout > 0 {
if err := l.WaitForFirstEvent(timeout); err != nil {
return nil, err
}
}
return l.LatestState(), nil
}
// ensureOcaAuthenticated initiates OCA login (if needed) and waits for success using the singleton listener
func ensureOcaAuthenticated(ctx context.Context) error {
// Ensure listener exists
listener, err := GetOcaAuthListener(ctx)
if err != nil {
return fmt.Errorf("failed to initialize OCA auth listener: %w", err)
}
// Briefly wait for first event to know current state
_ = listener.WaitForFirstEvent(1 * time.Second)
// If already authenticated, nothing to do
if listener.IsAuthenticated() {
fmt.Println("✓ OCA authentication already active.")
return nil
}
// Create gRPC client for initiating login
client, err := global.GetDefaultClient(ctx)
if err != nil {
return fmt.Errorf("failed to obtain client: %w", err)
}
// Start login and wait for authentication
waitCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
// Initiate login (opens the browser with a callback URL from Cline Core)
response, err := client.Ocaaccount.OcaAccountLoginClicked(waitCtx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to initiate OCA login: %w", err)
}
fmt.Println("\nOpening browser for OCA authentication...")
if response != nil && response.Value != "" {
fmt.Printf("If the browser doesn't open automatically, visit this URL:\n%s\n\n", response.Value)
}
fmt.Println("Waiting for you to complete OCA authentication in your browser...")
fmt.Println("(This may take a few moments. Timeout: 5 minutes)")
// Block until authenticated or timeout
if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
return err
}
fmt.Println("✓ OCA authentication successful!")
return nil
}
-187
View File
@@ -1,187 +0,0 @@
package clerror
import (
"encoding/json"
"fmt"
"strings"
)
// ClineErrorType represents the category of error
type ClineErrorType string
const (
ErrorTypeAuth ClineErrorType = "auth"
ErrorTypeNetwork ClineErrorType = "network"
ErrorTypeRateLimit ClineErrorType = "rateLimit"
ErrorTypeBalance ClineErrorType = "balance"
ErrorTypeUnknown ClineErrorType = "unknown"
)
// ClineError represents a parsed error from Cline API
type ClineError struct {
Message string `json:"message"`
Status int `json:"status"`
RequestID string `json:"request_id"`
Code interface{} `json:"code"` // Can be string or int
ModelID string `json:"modelId"`
ProviderID string `json:"providerId"`
Details map[string]interface{} `json:"details"`
}
// GetCodeString returns the code as a string regardless of its type
func (e *ClineError) GetCodeString() string {
if e == nil || e.Code == nil {
return ""
}
switch v := e.Code.(type) {
case string:
return v
case float64:
return fmt.Sprintf("%.0f", v)
case int:
return fmt.Sprintf("%d", v)
default:
return fmt.Sprintf("%v", v)
}
}
// Rate limit patterns from webview
var rateLimitPatterns = []string{
"status code 429",
"rate limit",
"too many requests",
"quota exceeded",
"resource exhausted",
}
// ParseClineError parses a JSON error string into a ClineError
func ParseClineError(errorJSON string) (*ClineError, error) {
if errorJSON == "" {
return nil, nil
}
var err ClineError
if parseErr := json.Unmarshal([]byte(errorJSON), &err); parseErr != nil {
// If JSON parsing fails, create a simple error with the message
return &ClineError{
Message: errorJSON,
}, nil
}
return &err, nil
}
// GetErrorType determines the type of error based on code, status, and message
func (e *ClineError) GetErrorType() ClineErrorType {
if e == nil {
return ErrorTypeUnknown
}
// Check balance error first (most specific)
codeStr := e.GetCodeString()
if codeStr == "insufficient_credits" {
return ErrorTypeBalance
}
// Check auth errors
if codeStr == "ERR_BAD_REQUEST" || e.Status == 401 {
return ErrorTypeAuth
}
// Check for auth message
if strings.Contains(e.Message, "Authentication required") ||
strings.Contains(e.Message, "Invalid API key") ||
strings.Contains(e.Message, "Unauthorized") {
return ErrorTypeAuth
}
// Check rate limit patterns
messageLower := strings.ToLower(e.Message)
for _, pattern := range rateLimitPatterns {
if strings.Contains(messageLower, pattern) {
return ErrorTypeRateLimit
}
}
return ErrorTypeUnknown
}
// IsBalanceError returns true if this is a balance/credits error
func (e *ClineError) IsBalanceError() bool {
return e.GetErrorType() == ErrorTypeBalance
}
// IsAuthError returns true if this is an authentication error
func (e *ClineError) IsAuthError() bool {
return e.GetErrorType() == ErrorTypeAuth
}
// IsRateLimitError returns true if this is a rate limit error
func (e *ClineError) IsRateLimitError() bool {
return e.GetErrorType() == ErrorTypeRateLimit
}
// GetCurrentBalance returns the current balance if available
func (e *ClineError) GetCurrentBalance() *float64 {
if e == nil || e.Details == nil {
return nil
}
if balance, ok := e.Details["current_balance"].(float64); ok {
return &balance
}
return nil
}
// GetBuyCreditsURL returns the URL to buy credits if available
func (e *ClineError) GetBuyCreditsURL() string {
if e == nil || e.Details == nil {
return ""
}
if url, ok := e.Details["buy_credits_url"].(string); ok {
return url
}
return ""
}
// GetTotalSpent returns the total spent amount if available
func (e *ClineError) GetTotalSpent() *float64 {
if e == nil || e.Details == nil {
return nil
}
if spent, ok := e.Details["total_spent"].(float64); ok {
return &spent
}
return nil
}
// GetTotalPromotions returns the total promotions amount if available
func (e *ClineError) GetTotalPromotions() *float64 {
if e == nil || e.Details == nil {
return nil
}
if promos, ok := e.Details["total_promotions"].(float64); ok {
return &promos
}
return nil
}
// GetDetailMessage returns the detail message from error.details if available
func (e *ClineError) GetDetailMessage() string {
if e == nil || e.Details == nil {
return ""
}
if msg, ok := e.Details["message"].(string); ok {
return msg
}
return ""
}
-152
View File
@@ -1,152 +0,0 @@
package cli
import (
"context"
"fmt"
"github.com/cline/cli/pkg/cli/config"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/spf13/cobra"
)
var configManager *config.Manager
func ensureConfigManager(ctx context.Context, address string) error {
if configManager == nil || (address != "" && configManager.GetCurrentInstance() != address) {
var err error
var instanceAddress string
if address != "" {
// Ensure instance exists at the specified address
if err := ensureInstanceAtAddress(ctx, address); err != nil {
return fmt.Errorf("failed to ensure instance at address %s: %w", address, err)
}
configManager, err = config.NewManager(ctx, address)
instanceAddress = address
} else {
// Ensure default instance exists
if err := global.EnsureDefaultInstance(ctx); err != nil {
return fmt.Errorf("failed to ensure default instance: %w", err)
}
configManager, err = config.NewManager(ctx, "")
if err == nil {
instanceAddress = configManager.GetCurrentInstance()
}
}
if err != nil {
return fmt.Errorf("failed to create config manager: %w", err)
}
// Always set the instance we're using as the default
registry := global.Instances.GetRegistry()
if err := registry.SetDefaultInstance(instanceAddress); err != nil {
// Log warning but don't fail - this is not critical
fmt.Printf("Warning: failed to set default instance: %v\n", err)
}
}
return nil
}
func NewConfigCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Aliases: []string{"c"},
Short: "Manage Cline configuration",
Long: `Set and manage global Cline configuration variables.`,
}
cmd.AddCommand(newConfigListCommand())
cmd.AddCommand(newConfigGetCommand())
cmd.AddCommand(setCommand())
return cmd
}
func newConfigGetCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "get <key>",
Aliases: []string{"g"},
Short: "Get a specific configuration value",
Long: `Get the value of a specific configuration setting. Supports nested keys using dot notation (e.g., auto-approval-settings.actions.read-files).`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
key := args[0]
// Ensure config manager
if err := ensureConfigManager(ctx, address); err != nil {
return err
}
// Get the setting
return configManager.GetSetting(ctx, key)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func newConfigListCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all configuration settings",
Long: `List all configuration settings from the Cline instance.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Ensure config manager
if err := ensureConfigManager(ctx, address); err != nil {
return err
}
// List settings
return configManager.ListSettings(ctx)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
func setCommand() *cobra.Command {
var address string
cmd := &cobra.Command{
Use: "set <key=value> [key=value...]",
Aliases: []string{"s"},
Short: "Set configuration variables",
Long: `Set one or more global configuration variables using key=value format.
This command merges the provided settings with existing values, preserving
unspecified fields. Only the fields you explicitly set will be updated.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Parse using existing task parser
settings, secrets, err := task.ParseTaskSettings(args)
if err != nil {
return fmt.Errorf("failed to parse settings: %w", err)
}
// Ensure config manager
if err := ensureConfigManager(ctx, address); err != nil {
return err
}
// Update settings (server-side merge handles preserving existing values)
return configManager.UpdateSettings(ctx, settings, secrets)
},
}
cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use")
return cmd
}
-208
View File
@@ -1,208 +0,0 @@
package config
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/grpc-go/client"
"github.com/cline/grpc-go/cline"
)
type Manager struct {
client *client.ClineClient
clientAddress string
}
func NewManager(ctx context.Context, address string) (*Manager, error) {
var c *client.ClineClient
var err error
if address != "" {
c, err = global.GetClientForAddress(ctx, address)
} else {
c, err = global.GetDefaultClient(ctx)
}
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
// Get the actual address being used
clientAddress := address
if address == "" && global.Instances != nil {
clientAddress = global.Instances.GetRegistry().GetDefaultInstance()
}
return &Manager{
client: c,
clientAddress: clientAddress,
}, nil
}
// GetCurrentInstance returns the address of the current instance
func (m *Manager) GetCurrentInstance() string {
return m.clientAddress
}
func (m *Manager) UpdateSettings(ctx context.Context, settings *cline.Settings, secrets *cline.Secrets) error {
request := &cline.UpdateSettingsRequestCli{
Metadata: &cline.Metadata{},
Settings: settings,
Secrets: secrets,
}
// Call the updateSettingsCli RPC
_, err := m.client.State.UpdateSettingsCli(ctx, request)
if err != nil {
return fmt.Errorf("failed to update settings: %w", err)
}
fmt.Println("Settings updated successfully")
fmt.Printf("Instance: %s\n", m.clientAddress)
return nil
}
func (m *Manager) GetState(ctx context.Context) (map[string]interface{}, error) {
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}
var stateData map[string]interface{}
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
return nil, fmt.Errorf("failed to parse state: %w", err)
}
return stateData, nil
}
func (m *Manager) ListSettings(ctx context.Context) error {
// Get state
stateData, err := m.GetState(ctx)
if err != nil {
return err
}
// Subset of fields we will print the values for
settingsFields := []string{
"apiConfiguration",
"telemetrySetting",
"planActSeparateModelsSetting",
"enableCheckpointsSetting",
"shellIntegrationTimeout",
"terminalReuseEnabled",
"mcpResponsesCollapsed",
"mcpDisplayMode",
"terminalOutputLineLimit",
"mode",
"preferredLanguage",
"openaiReasoningEffort",
"strictPlanModeEnabled",
"focusChainSettings",
"useAutoCondense",
"customPrompt",
"browserSettings",
"defaultTerminalProfile",
"yoloModeToggled",
"dictationSettings",
"autoCondenseThreshold",
"autoApprovalSettings",
"hooksEnabled",
}
// Render each field using the renderer
for _, field := range settingsFields {
if value, ok := stateData[field]; ok {
if err := RenderField(field, value, true); err != nil {
fmt.Printf("Error rendering %s: %v\n", field, err)
}
fmt.Println()
}
}
return nil
}
func (m *Manager) GetSetting(ctx context.Context, key string) error {
// Get state
stateData, err := m.GetState(ctx)
if err != nil {
return err
}
// Convert kebab-case to camelCase path
parts := kebabToCamelPath(key)
rootField := parts[0]
// Get the value
value, found := getNestedValue(stateData, parts)
if !found {
return fmt.Errorf("setting '%s' not found", key)
}
// Render the value
if len(parts) == 1 {
// Top-level field: use RenderField for nice formatting
return RenderField(rootField, value, false)
} else {
// Nested field: simple print
fmt.Printf("%s: %s\n", key, formatValue(value, rootField, true))
}
return nil
}
// kebabToCamelPath converts a kebab-case path to camelCase
// e.g., "auto-approval-settings.actions.read-files" -> "autoApprovalSettings.actions.readFiles"
func kebabToCamelPath(path string) []string {
parts := strings.Split(path, ".")
for i, part := range parts {
parts[i] = kebabToCamel(part)
}
return parts
}
// kebabToCamel converts a single kebab-case string to camelCase
// e.g., "auto-approval-settings" -> "autoApprovalSettings"
func kebabToCamel(s string) string {
if s == "" {
return s
}
parts := strings.Split(s, "-")
if len(parts) == 1 {
return s
}
// First part stays lowercase, rest are capitalized
result := parts[0]
for i := 1; i < len(parts); i++ {
if parts[i] != "" {
result += strings.ToUpper(parts[i][:1]) + parts[i][1:]
}
}
return result
}
// getNestedValue retrieves a value from a nested map using dot notation
// e.g., "autoApprovalSettings.actions.readFiles"
func getNestedValue(data map[string]interface{}, parts []string) (interface{}, bool) {
current := interface{}(data)
for _, part := range parts {
// Try to access as map
if m, ok := current.(map[string]interface{}); ok {
if val, exists := m[part]; exists {
current = val
continue
}
return nil, false
}
return nil, false
}
return current, true
}
-197
View File
@@ -1,197 +0,0 @@
package config
import (
"fmt"
"strings"
)
// sensitiveKeywords defines field name patterns that should be censored
var sensitiveKeywords = []string{"key", "secret", "password", "cline-account-id"}
// camelToKebab converts camelCase to kebab-case
// e.g., "autoApprovalSettings" -> "auto-approval-settings"
func camelToKebab(s string) string {
if s == "" {
return s
}
var result []rune
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
result = append(result, '-')
}
result = append(result, r|32) // Convert to lowercase (works for A-Z)
}
return string(result)
}
// isSensitiveField checks if a field name contains sensitive keywords
func isSensitiveField(fieldName string) bool {
if fieldName == "" {
return false
}
lowerName := strings.ToLower(fieldName)
for _, keyword := range sensitiveKeywords {
if strings.Contains(lowerName, keyword) {
return true
}
}
return false
}
// formatValue formats a value for display, handling empty strings and censoring sensitive fields
func formatValue(val interface{}, fieldName string, censor bool) string {
// Handle empty strings specifically
if str, ok := val.(string); ok && str == "" {
return "''"
}
if censor && isSensitiveField(fieldName) {
valStr := fmt.Sprintf("%v", val)
if valStr != "" && valStr != "''" {
return "********"
}
}
return fmt.Sprintf("%v", val)
}
// RenderField renders a single config field with proper formatting
func RenderField(key string, value interface{}, censor bool) error {
switch key {
// Nested objects - render with header + nested fields
case "apiConfiguration":
return renderApiConfiguration(value, censor)
case "browserSettings":
return renderBrowserSettings(value, censor)
case "focusChainSettings":
return renderFocusChainSettings(value, censor)
case "dictationSettings":
return renderDictationSettings(value, censor)
case "autoApprovalSettings":
return renderAutoApprovalSettings(value, censor)
// Simple values - just print key: value
case "mode", "telemetrySetting", "preferredLanguage", "customPrompt",
"defaultTerminalProfile", "mcpDisplayMode", "openaiReasoningEffort",
"planActSeparateModelsSetting", "enableCheckpointsSetting",
"terminalReuseEnabled", "mcpResponsesCollapsed", "strictPlanModeEnabled",
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
"terminalOutputLineLimit", "autoCondenseThreshold", "hooksEnabled":
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
return nil
default:
return fmt.Errorf("unknown config field: %s", key)
}
}
// renderApiConfiguration renders the API configuration object
func renderApiConfiguration(value interface{}, censor bool) error {
fmt.Println("api-configuration:")
configMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid api-configuration format")
}
// Print each field directly
for key, val := range configMap {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
return nil
}
// renderBrowserSettings renders browser settings
func renderBrowserSettings(value interface{}, censor bool) error {
fmt.Println("browser-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid browser-settings format")
}
// Handle nested viewport if present
if viewport, ok := settingsMap["viewport"].(map[string]interface{}); ok {
fmt.Println(" viewport:")
for key, val := range viewport {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
}
// Print other fields
for key, val := range settingsMap {
if key != "viewport" {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
}
return nil
}
// renderFocusChainSettings renders focus chain settings
func renderFocusChainSettings(value interface{}, censor bool) error {
fmt.Println("focus-chain-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid focus-chain-settings format")
}
for key, val := range settingsMap {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
return nil
}
// renderDictationSettings renders dictation settings
func renderDictationSettings(value interface{}, censor bool) error {
fmt.Println("dictation-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid dictation-settings format")
}
for key, val := range settingsMap {
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
return nil
}
// renderAutoApprovalSettings renders auto approval settings
func renderAutoApprovalSettings(value interface{}, censor bool) error {
fmt.Println("auto-approval-settings:")
settingsMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid auto-approval-settings format")
}
// Print top-level fields (skip version, handle actions specially)
for key, val := range settingsMap {
if key == "version" {
continue // Skip version
}
if key == "actions" {
// Handle nested actions with double indentation
fmt.Println(" actions:")
if actionsMap, ok := val.(map[string]interface{}); ok {
for actionKey, actionVal := range actionsMap {
fmt.Printf(" %s: %s\n", camelToKebab(actionKey), formatValue(actionVal, actionKey, censor))
}
}
} else {
// Print other fields normally (enabled, enableNotifications, favorites)
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
}
return nil
}
-27
View File
@@ -1,27 +0,0 @@
package display
import (
"fmt"
"os"
"golang.org/x/term"
)
func isTTY() bool {
return term.IsTerminal(int(os.Stdout.Fd()))
}
func ClearLine() {
if !isTTY() {
return
}
fmt.Print("\r\033[K")
}
// ClearToEnd clears from cursor to end of screen
func ClearToEnd() {
if !isTTY() {
return
}
fmt.Print("\033[J")
}
-99
View File
@@ -1,99 +0,0 @@
package display
import (
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/cline/cli/pkg/common"
)
// BannerInfo contains information to display in the session banner
type BannerInfo struct {
Version string
Provider string
ModelID string
Workdirs []string // workspace directories
Mode string
}
// RenderSessionBanner renders a nice banner showing version, model, and workspace info
func RenderSessionBanner(info BannerInfo) string {
// Bright white for title
titleStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("15")). // Bright white
Bold(true)
// Dim gray for regular text (same as huh placeholder)
dimStyle := lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"})
// Border color matches mode
borderColor := lipgloss.Color("3") // Yellow for plan
if info.Mode == "act" {
borderColor = lipgloss.Color("39") // Blue for act
}
boxStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(borderColor).
Padding(1, 4)
var lines []string
// Format version with "v" prefix if it starts with a number
versionStr := info.Version
if len(versionStr) > 0 && versionStr[0] >= '0' && versionStr[0] <= '9' {
versionStr = "v" + versionStr
}
// First line: "cline cli vX.X.X" on left, "plan mode" on right
leftSide := titleStyle.Render("cline cli preview") + " " + dimStyle.Render(versionStr)
if info.Mode != "" {
modeColor := lipgloss.Color("3") // Yellow for plan
if info.Mode == "act" {
modeColor = lipgloss.Color("39") // Blue for act
}
modeStyle := lipgloss.NewStyle().Foreground(modeColor).Bold(true)
rightSide := modeStyle.Render(info.Mode + " mode")
// Calculate spacing to push mode to the right
// Assume a reasonable width (we'll adjust based on content)
lineWidth := 50
leftWidth := lipgloss.Width(leftSide)
rightWidth := lipgloss.Width(rightSide)
spacing := lineWidth - leftWidth - rightWidth
if spacing > 0 {
titleLine := leftSide + strings.Repeat(" ", spacing) + rightSide
lines = append(lines, titleLine)
} else {
// If too narrow, just put them on same line with a space
lines = append(lines, leftSide+" "+rightSide)
}
} else {
// No mode, just show title
lines = append(lines, leftSide)
}
// Model line - dim gray
if info.Provider != "" && info.ModelID != "" {
lines = append(lines, dimStyle.Render(info.Provider+"/"+common.ShortenPath(info.ModelID, 30)))
}
for _, wd := range info.Workdirs {
lines = append(lines, dimStyle.Render(common.ShortenPath(wd, 45)))
}
// Checkpoint warning for multi-root workspaces
if len(info.Workdirs) > 1 {
warningStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("3")). // Yellow warning color
Italic(true)
lines = append(lines, "")
lines = append(lines, warningStyle.Render("⚠ Checkpoints disabled for multi-root workspaces"))
}
content := lipgloss.JoinVertical(lipgloss.Left, lines...)
return boxStyle.Render(content)
}
-95
View File
@@ -1,95 +0,0 @@
package display
import (
"crypto/md5"
"fmt"
"sync"
"time"
"github.com/cline/cli/pkg/cli/types"
)
// MessageDeduplicator handles message deduplication to prevent duplicate displays
type MessageDeduplicator struct {
mu sync.RWMutex
seenMessages map[string]time.Time
maxAge time.Duration
cleanupTicker *time.Ticker
}
// NewMessageDeduplicator creates a new message deduplicator
func NewMessageDeduplicator() *MessageDeduplicator {
d := &MessageDeduplicator{
seenMessages: make(map[string]time.Time),
maxAge: 5 * time.Minute, // Keep messages for 5 minutes
cleanupTicker: time.NewTicker(1 * time.Minute), // Cleanup every minute
}
// Start cleanup goroutine
go d.cleanup()
return d
}
// IsDuplicate checks if a message is a duplicate
func (d *MessageDeduplicator) IsDuplicate(msg *types.ClineMessage) bool {
d.mu.Lock()
defer d.mu.Unlock()
// Create a hash of the message content
hash := d.hashMessage(msg)
// Check if we've seen this message recently
if lastSeen, exists := d.seenMessages[hash]; exists {
// If we've seen it within the last few seconds, it's a duplicate
if time.Since(lastSeen) < 2*time.Second {
return true
}
}
// Mark this message as seen
d.seenMessages[hash] = time.Now()
return false
}
// hashMessage creates a hash of the message for deduplication
func (d *MessageDeduplicator) hashMessage(msg *types.ClineMessage) string {
// Create a hash based on message content, type, and timestamp
content := fmt.Sprintf("%s|%s|%s|%d",
string(msg.Type),
msg.Say,
msg.Ask,
msg.Timestamp)
// For partial messages, include the text content in the hash
if msg.Partial {
content += "|" + msg.Text
}
hash := md5.Sum([]byte(content))
return fmt.Sprintf("%x", hash)
}
// cleanup removes old entries from the seen messages map
func (d *MessageDeduplicator) cleanup() {
for range d.cleanupTicker.C {
d.mu.Lock()
now := time.Now()
// Remove entries older than maxAge
for hash, timestamp := range d.seenMessages {
if now.Sub(timestamp) > d.maxAge {
delete(d.seenMessages, hash)
}
}
d.mu.Unlock()
}
}
// Stop stops the cleanup goroutine
func (d *MessageDeduplicator) Stop() {
if d.cleanupTicker != nil {
d.cleanupTicker.Stop()
}
}
-144
View File
@@ -1,144 +0,0 @@
package display
import (
"fmt"
"strings"
"github.com/cline/cli/pkg/cli/types"
)
// HookRenderer renders hook status messages in a CLI-native style.
//
// Goals:
// - Match ToolRenderers markdown look
// - Keep executions ungrouped
// - Render status + high-signal metadata (script paths, error summary)
//
// Note: hook stdout/stderr currently arrives as separate `hook_output_stream` messages.
// The CLI suppresses those by default and prints them only in --verbose mode.
// Future work could group streamed output under the corresponding hook block.
//
// It returns markdown (or rendered markdown when enabled); callers should print the
// returned string.
type HookRenderer struct {
mdRenderer *MarkdownRenderer
outputFormat string
}
func NewHookRenderer(mdRenderer *MarkdownRenderer, outputFormat string) *HookRenderer {
return &HookRenderer{mdRenderer: mdRenderer, outputFormat: outputFormat}
}
func (hr *HookRenderer) RenderHookStatus(h types.HookMessage) string {
statusText := strings.TrimSpace(h.Status)
if statusText == "" {
statusText = "unknown"
}
// Header: aligned with ToolRenderers phrasing so transcripts scan consistently.
// Example: "### Cline hook completed: PreToolUse (tool: read_file) (exit 0)"
var headerBuilder strings.Builder
headerBuilder.WriteString(fmt.Sprintf("### Cline hook %s: %s", statusText, h.HookName))
if h.ToolName != "" {
headerBuilder.WriteString(" ")
headerBuilder.WriteString(fmt.Sprintf("(tool: %s)", h.ToolName))
}
if statusText == "failed" && h.ExitCode != 0 {
headerBuilder.WriteString(" ")
headerBuilder.WriteString(fmt.Sprintf("(exit %d)", h.ExitCode))
}
header := headerBuilder.String()
var lines []string
lines = append(lines, header)
// Pending tool info (PreToolUse): show one high-signal line directly under the header.
if h.PendingToolInfo != nil {
if pending := hr.formatPendingToolInfo(h.PendingToolInfo); pending != "" {
lines = append(lines, fmt.Sprintf("- Pending: %s", pending))
}
}
// Script paths: one per line.
paths := make([]string, 0, len(h.ScriptPaths))
for _, p := range h.ScriptPaths {
p = strings.TrimSpace(p)
if p != "" {
paths = append(paths, p)
}
}
if len(paths) == 0 {
// Fallback when no script paths are provided.
lines = append(lines, "- *(no hook scripts found)*")
} else {
for _, p := range paths {
lines = append(lines, fmt.Sprintf("- Running hook: `%s`", p))
}
}
// On failure, show a minimal summary (full stderr reserved for verbose).
if statusText == "failed" && h.Error != nil {
if msg := strings.TrimSpace(h.Error.Message); msg != "" {
lines = append(lines, fmt.Sprintf("- Error: %s", msg))
}
// If we have a specific script path, include it as a hint.
if sp := strings.TrimSpace(h.Error.ScriptPath); sp != "" {
lines = append(lines, fmt.Sprintf("- Script: `%s`", sp))
}
}
markdown := strings.Join(lines, "\n")
return hr.renderMarkdown(markdown)
}
func (hr *HookRenderer) formatPendingToolInfo(info *types.ToolInfo) string {
if info == nil {
return ""
}
tool := strings.TrimSpace(info.Tool)
if tool == "" {
return ""
}
// Keep this intentionally compact and readable.
// Format: "<tool> <identifier>" where identifier is the most relevant param.
var ident string
switch {
case strings.TrimSpace(info.Path) != "":
ident = strings.TrimSpace(info.Path)
case strings.TrimSpace(info.Command) != "":
ident = strings.TrimSpace(info.Command)
case strings.TrimSpace(info.Url) != "":
ident = strings.TrimSpace(info.Url)
case strings.TrimSpace(info.McpTool) != "" && strings.TrimSpace(info.McpServer) != "":
ident = fmt.Sprintf("%s %s", strings.TrimSpace(info.McpServer), strings.TrimSpace(info.McpTool))
case strings.TrimSpace(info.ResourceUri) != "":
ident = strings.TrimSpace(info.ResourceUri)
case strings.TrimSpace(info.Regex) != "":
ident = strings.TrimSpace(info.Regex)
default:
ident = ""
}
if ident != "" {
return fmt.Sprintf("%s %s", tool, ident)
}
return tool
}
func (hr *HookRenderer) renderMarkdown(markdown string) string {
// Align with ToolRenderer: in plain mode or non-TTY, return markdown as-is.
if hr.outputFormat == "plain" || !isTTY() {
return markdown
}
if hr.mdRenderer == nil {
return markdown
}
rendered, err := hr.mdRenderer.Render(markdown)
if err != nil {
return markdown
}
return rendered
}
-69
View File
@@ -1,69 +0,0 @@
package display
import (
"strings"
"testing"
"github.com/cline/cli/pkg/cli/types"
)
func TestHookRenderer_RenderHookStatus_FailedShowsErrorAndScript(t *testing.T) {
hr := NewHookRenderer(nil, "plain")
msg := hr.RenderHookStatus(types.HookMessage{
HookName: "PreToolUse",
ToolName: "execute_command",
Status: "failed",
ExitCode: 2,
ScriptPaths: []string{"repo/.clinerules/hooks/PreToolUse"},
Error: &types.HookError{
Message: "boom",
ScriptPath: "repo/.clinerules/hooks/PreToolUse",
},
})
if !strings.Contains(msg, "### Cline hook failed: PreToolUse") {
t.Fatalf("expected header in rendered output, got: %q", msg)
}
if !strings.Contains(msg, "- Error: boom") {
t.Fatalf("expected error line in rendered output, got: %q", msg)
}
if !strings.Contains(msg, "- Script: `repo/.clinerules/hooks/PreToolUse`") {
t.Fatalf("expected script line in rendered output, got: %q", msg)
}
}
func TestHookRenderer_RenderHookStatus_PendingToolInfoAppearsDirectlyUnderHeader(t *testing.T) {
hr := NewHookRenderer(nil, "plain")
msg := hr.RenderHookStatus(types.HookMessage{
HookName: "PreToolUse",
ToolName: "write_to_file",
Status: "running",
PendingToolInfo: &types.ToolInfo{
Tool: "write_to_file",
Path: "src/foo.ts",
},
ScriptPaths: []string{"repo/.clinerules/hooks/PreToolUse"},
})
header := "### Cline hook running: PreToolUse"
pending := "- Pending: write_to_file src/foo.ts"
runningHook := "- Running hook: `repo/.clinerules/hooks/PreToolUse`"
headerIdx := strings.Index(msg, header)
if headerIdx == -1 {
t.Fatalf("expected header %q in output, got: %q", header, msg)
}
pendingIdx := strings.Index(msg, pending)
if pendingIdx == -1 {
t.Fatalf("expected pending line %q in output, got: %q", pending, msg)
}
runningIdx := strings.Index(msg, runningHook)
if runningIdx == -1 {
t.Fatalf("expected running hook line %q in output, got: %q", runningHook, msg)
}
if !(headerIdx < pendingIdx && pendingIdx < runningIdx) {
t.Fatalf("expected header < pending < runningHook ordering, got indexes header=%d pending=%d running=%d\nfull=%q", headerIdx, pendingIdx, runningIdx, msg)
}
}
-139
View File
@@ -1,139 +0,0 @@
package display
import (
"os"
"strconv"
"strings"
"fmt"
"github.com/charmbracelet/glamour"
"golang.org/x/term"
)
type MarkdownRenderer struct {
renderer *glamour.TermRenderer
width int
}
// i went back and forth on whether or not to enable word wrap
// setting line width to 0 enables the terminal to handle wrapping
// setting it to a terminal width enables glamour's word wrap
// the thing is, glamour's nice indentation looks really good, and
// won't work without glamour's word wrap - if you use the terminal's
// word wrap, the indentation looks weird so you have to turn it off
// and everything will be right next to the left margin
// but if you DO use glamours word wrap, it also means if you resize the terminal,
// it will scuff everything. but given that this is the case for the input anyway,
// i figure we just make things as beautiful as possible
// and if you resize the terminal, you'll learn real quick.
// anyway, you can set this to true or false to experiment
const USETERMINALWORDWRAP = true
// seems like a reliable way to check for terminals
// for now i'm keeping everything as auto
// eventually we can define a custom glamour style for ghostty / iterm
// https://github.com/charmbracelet/glamour/blob/master/styles/README.md)
func detectTerminalTheme() string {
switch os.Getenv("TERM_PROGRAM") {
case "iTerm.app", "Ghostty":
return "dark"
}
if os.Getenv("GHOSTTY_VERSION") != "" {
return "dark"
}
return "dark"
}
func glamourStyleJSON(terminalWrap bool) string {
const tmpl = `{
"document": {
"block_prefix": "\n",
"block_suffix": "\n",
"color": "252",
"margin": %s
},
"code_block": {
"margin": 0
}
}`
if terminalWrap {
return fmt.Sprintf(tmpl, "0")
}
return fmt.Sprintf(tmpl, "2")
}
func NewMarkdownRenderer() (*MarkdownRenderer, error) {
var wordWrap int
if USETERMINALWORDWRAP {
// terminal handles wrapping -> disable glamour wrap
wordWrap = 0
} else {
// glamour handles wrapping -> set to current width
wordWrap = terminalWidthOr(0)
}
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle(detectTerminalTheme()), // Load full auto style first
glamour.WithStylesFromJSONBytes([]byte(glamourStyleJSON(USETERMINALWORDWRAP))), // Then override just margins
glamour.WithWordWrap(wordWrap),
glamour.WithPreservedNewLines(),
)
if err != nil {
return nil, err
}
return &MarkdownRenderer{
renderer: r,
width: 0, // Unlimited width
}, nil
}
// terminalWidthOr returns the terminal width or the provided fallback.
// It first tries term.GetSize, then falls back to $COLUMNS if set.
func terminalWidthOr(fallback int) int {
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
return w
}
if cols := os.Getenv("COLUMNS"); cols != "" {
if n, err := strconv.Atoi(cols); err == nil && n > 0 {
return n
}
}
return fallback
}
// NewMarkdownRendererWithWidth creates a markdown renderer with a specific width.
// Useful for tables and other content that should fit within terminal bounds.
func NewMarkdownRendererWithWidth(width int) (*MarkdownRenderer, error) {
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle(detectTerminalTheme()),
glamour.WithStylesFromJSONBytes([]byte(glamourStyleJSON(false))),
glamour.WithWordWrap(width),
glamour.WithPreservedNewLines(),
)
if err != nil {
return nil, err
}
return &MarkdownRenderer{renderer: r, width: width}, nil
}
// NewMarkdownRendererForTerminal creates a markdown renderer using the actual terminal width.
// Falls back to 120 if terminal width cannot be determined.
func NewMarkdownRendererForTerminal() (*MarkdownRenderer, error) {
width := terminalWidthOr(120)
return NewMarkdownRendererWithWidth(width)
}
func (mr *MarkdownRenderer) Render(markdown string) (string, error) {
rendered, err := mr.renderer.Render(markdown)
if err != nil {
return "", err
}
return strings.TrimLeft(strings.TrimRight(rendered, "\n"), "\n"), nil
}

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