mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 095ee24288 | |||
| 523dd9ef7d | |||
| 6d8fb8507b | |||
| edc93f35f1 | |||
| 401358374f | |||
| f8bcad16a5 | |||
| 26391c94e9 | |||
| 462438ece5 | |||
| 92521ed279 | |||
| 08aa81f798 | |||
| e025995177 | |||
| ee361ef3ae | |||
| c8ef342c19 | |||
| 7c8701799d | |||
| fbcf63ad71 | |||
| fffe626b3a | |||
| 2dccd6f6f6 | |||
| 39971128cb | |||
| b725490582 | |||
| 8f78645154 | |||
| 0ef1c0bf47 | |||
| 4c07df370b | |||
| f440f3a5dd | |||
| 3ce1ad3504 | |||
| 00bc38d4e0 | |||
| a1f2601fe0 | |||
| 8e3689a5d6 | |||
| a64f46a5f4 | |||
| 507483b2c3 | |||
| 42ce100143 | |||
| 7be4e6c6d3 | |||
| 09b91a1ea5 | |||
| 7127a2ffa7 | |||
| d6987d4578 | |||
| 7b59cbcb5c | |||
| 0e26ba46d0 | |||
| 3b313ae41f | |||
| 7e74f35f23 | |||
| 59c05f4b84 | |||
| 5d6424682f | |||
| b1a8db252a | |||
| 5ae47fb90b | |||
| 28c2697ae1 | |||
| cf01317885 | |||
| 7d5eebe192 | |||
| 91a3636356 | |||
| 5d02eea9cd | |||
| 45b2786dbf | |||
| 4924192b64 | |||
| 28c548b3ee | |||
| 9b9035ea4a | |||
| e4b39aeb22 | |||
| f7c54e964f | |||
| d116ac5dcf | |||
| ac22d5d81a | |||
| b57aefb5a1 | |||
| 11da3ee89e | |||
| e4e63912dd | |||
| f17d523b2e | |||
| edbba8b7f6 | |||
| 7b09999fdf | |||
| 01240744a2 | |||
| 2944416758 | |||
| b5b503dd50 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Replace the LiteLLM model list with a selector
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
fix(cli): prevent hang when spawned without TTY
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Add Claude Opus 4.6 model support
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix decimal input crash in OpenAI Compatible price fields (#8129)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Supports rendering markdown table in chat view.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: build complete handlers when upadting the api config
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Fix Bedrock model id
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed missing provider from list
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(skills): Make skills always enabled and remove feature toggle setting
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed Favorite Icon / Star from getting clipped in the task history view
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": minor
|
||||
---
|
||||
|
||||
Add Generate API Key on Hicap Provider selection
|
||||
+2
-3
@@ -1,3 +1,2 @@
|
||||
/docs/
|
||||
/.github/ @saoudrizwan @garoth @sjf
|
||||
/README.md @saoudrizwan @nickbaumann98
|
||||
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
|
||||
/README.md @saoudrizwan @juanpflores
|
||||
|
||||
@@ -62,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: |
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write # Required for pushing tags
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
@@ -88,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'"
|
||||
|
||||
@@ -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
|
||||
|
||||
+18
-50
@@ -28,20 +28,10 @@ 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
|
||||
run: npm ci
|
||||
@@ -73,20 +63,10 @@ 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
|
||||
run: npm ci
|
||||
@@ -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,28 +141,11 @@ 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
|
||||
run: npm ci
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
+76
-67
@@ -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,19 +28,19 @@
|
||||
"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",
|
||||
@@ -51,35 +51,36 @@
|
||||
"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,14 +142,15 @@
|
||||
{
|
||||
"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"
|
||||
@@ -154,37 +163,37 @@
|
||||
],
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/esbuild.*",
|
||||
"!**/*.mts",
|
||||
"!**/webview-ui/**",
|
||||
"!**/evals/**",
|
||||
"!**/standalone/**",
|
||||
"!**/cli/**",
|
||||
"!**/e2e/**",
|
||||
"!**/test/**",
|
||||
"!**/__tests__/**",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.stories.ts",
|
||||
"!src/dev/**",
|
||||
"!**/*.mjs",
|
||||
"!**/*.js",
|
||||
"!**/scripts/**",
|
||||
"!**/*.tsx",
|
||||
"!**/testing-platform/**",
|
||||
"!!**/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"
|
||||
"!!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"
|
||||
"!!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"
|
||||
|
||||
+35
-13
@@ -88,6 +88,10 @@ directory
|
||||
\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
|
||||
@@ -179,6 +183,10 @@ the task
|
||||
.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:
|
||||
@@ -274,6 +282,21 @@ cline history
|
||||
\f[I]# Show more tasks with pagination\f[R]
|
||||
cline history \-n 20 \-p 2
|
||||
.EE
|
||||
.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
|
||||
@@ -348,20 +371,19 @@ export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqnpm *\(dq, \(dqgit *\(
|
||||
\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 FILES
|
||||
\f[B]\(ti/.cline/data/\f[R] : Default configuration directory
|
||||
containing:
|
||||
.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
|
||||
\f[B]globalState.json\f[R] : Global settings and state
|
||||
.PP
|
||||
\f[B]secrets.json\f[R] : API keys and secrets (stored securely)
|
||||
.PP
|
||||
\f[B]workspace/\f[R] : Workspace\-specific state
|
||||
.PP
|
||||
\f[B]tasks/\f[R] : Task history and conversation data
|
||||
.PP
|
||||
\f[B]\(ti/.cline/log/\f[R] : Log files for debugging.
|
||||
View with \f[CR]cline dev log\f[R].
|
||||
View logs with \f[CR]cline dev log\f[R].
|
||||
.SH BUGS
|
||||
Report bugs at: \c
|
||||
.UR https://github.com/cline/cline/issues
|
||||
|
||||
@@ -70,6 +70,8 @@ Run a new task with a prompt.
|
||||
|
||||
**\--json** : Output messages as JSON instead of styled text
|
||||
|
||||
**-T**, **\--taskId** *id* : Resume an existing task by ID. The prompt argument becomes an optional follow-up message.
|
||||
|
||||
## history (alias: h)
|
||||
|
||||
List task history with pagination.
|
||||
@@ -154,6 +156,8 @@ When running **cline** with just a prompt (no subcommand), these options are ava
|
||||
|
||||
**\--json** : Output messages as JSON instead of styled text. Forces plain text mode.
|
||||
|
||||
**-T**, **\--taskId** *id* : Resume an existing task by ID instead of starting a new one. The prompt becomes an optional follow-up message.
|
||||
|
||||
# JSON OUTPUT FORMAT
|
||||
|
||||
When using **\--json**, each message is output as a JSON object with these fields:
|
||||
@@ -251,6 +255,22 @@ cline history
|
||||
cline history -n 20 -p 2
|
||||
```
|
||||
|
||||
## Resuming Tasks
|
||||
|
||||
```bash
|
||||
# Resume a task by ID (get IDs from cline history)
|
||||
cline -T abc123def
|
||||
|
||||
# Resume a task with a follow-up message
|
||||
cline -T abc123def "Now add unit tests for the changes"
|
||||
|
||||
# Resume in plan mode to review before continuing
|
||||
cline -T abc123def -p "What's left to do?"
|
||||
|
||||
# Resume with yolo mode for automated continuation
|
||||
cline -T abc123def -y "Continue with the implementation"
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
```bash
|
||||
|
||||
Generated
-2950
File diff suppressed because it is too large
Load Diff
+14
-5
@@ -1,25 +1,34 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.5",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "dist/cli.mjs",
|
||||
"bin": {
|
||||
"cline": "./dist/cli.mjs"
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
],
|
||||
"man": "./man/cline.1",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "npm run build:production",
|
||||
"package:brew": "npx tsx ./scripts/update-brew-formula.mts",
|
||||
"package": "npm pack --pack-destination ./dist",
|
||||
"build": "npx tsx esbuild.mts",
|
||||
"build:production": "npx tsx esbuild.mts --production",
|
||||
"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": "tsc --noEmit",
|
||||
"typecheck": "npx tsc --noEmit",
|
||||
"link": "npm run build && npm link",
|
||||
"unlink": "npm unlink -g cline",
|
||||
"test": "vitest",
|
||||
|
||||
@@ -172,6 +172,16 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
Logger.debug("[ACPEnvServiceClient] shutdown called (stub)")
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
|
||||
async openExternal(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
|
||||
const url = request.value || ""
|
||||
if (url) {
|
||||
Logger.debug(`[ACPEnvServiceClient] openExternal: ${url}`)
|
||||
const { openUrlInBrowser } = await import("../utils/browser")
|
||||
await openUrlInBrowser(url)
|
||||
}
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -337,9 +337,7 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
// Use provider-specific model ID key (e.g., cline uses actModeOpenRouterModelId)
|
||||
const modelKey = currentProvider ? getProviderModelIdKey(currentProvider, mode) : null
|
||||
const currentModelId = modelKey
|
||||
? (stateManager.getGlobalSettingsKey(modelKey as string) as string | undefined)
|
||||
: undefined
|
||||
const currentModelId = modelKey ? stateManager.getGlobalSettingsKey(modelKey) : undefined
|
||||
|
||||
// Build the current model ID in provider/model format
|
||||
const currentFullModelId =
|
||||
@@ -1160,8 +1158,8 @@ export class ClineAgent implements acp.Agent {
|
||||
|
||||
if (currentProvider === "cline") {
|
||||
// For Cline provider, check if we have stored auth data
|
||||
const authData = await secretStorage.get("cline:clineAccountId")
|
||||
return !!authData
|
||||
const values = await Promise.all(["clineApiKey", "clineAccountId"].map((key) => secretStorage.get(key)))
|
||||
return values.some(Boolean)
|
||||
}
|
||||
|
||||
// For OpenAI Codex provider, check OAuth credentials
|
||||
|
||||
@@ -34,7 +34,7 @@ type AsciiMotionCliProps = {
|
||||
autoPlay?: boolean;
|
||||
loop?: boolean;
|
||||
onReady?: (api: PlaybackAPI) => void;
|
||||
onScroll?: () => void; // Called when user scrolls (scroll wheel)
|
||||
onInteraction?: () => void; // Called when user scrolls, clicks, or drags
|
||||
};
|
||||
|
||||
const FRAMES: FrameData[] = [
|
||||
@@ -333364,7 +333364,7 @@ const FRAME_BOTTOM_RIGHT = 128;
|
||||
|
||||
export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
hasDarkBackground = true,
|
||||
onScroll,
|
||||
onInteraction,
|
||||
}) => {
|
||||
const [frameIndex, setFrameIndex] = useState(0);
|
||||
const [targetFrame, setTargetFrame] = useState(0);
|
||||
@@ -333390,13 +333390,13 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
// Stop animation on terminal resize to prevent visual glitches
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
onScroll?.();
|
||||
onInteraction?.();
|
||||
};
|
||||
process.stdout.on("resize", handleResize);
|
||||
return () => {
|
||||
process.stdout.off("resize", handleResize);
|
||||
};
|
||||
}, [onScroll]);
|
||||
}, [onInteraction]);
|
||||
|
||||
// Mouse tracking - gracefully handle environments without tty support
|
||||
useEffect(() => {
|
||||
@@ -333417,13 +333417,19 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
const handleData = (data: Buffer) => {
|
||||
const str = data.toString();
|
||||
|
||||
// Parse mouse events: \x1b[<button;x;yM
|
||||
// Parse mouse events: \x1b[<button;x;yM (M=press, m=release)
|
||||
const mouseMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
||||
if (mouseMatch) {
|
||||
const button = parseInt(mouseMatch[1], 10);
|
||||
// Button 64 = scroll up, 65 = scroll down
|
||||
if (button === 64 || button === 65) {
|
||||
onScroll?.();
|
||||
const isPress = mouseMatch[4] === "M";
|
||||
// Button 64/65 = scroll up/down
|
||||
// Button 0-2 = left/middle/right click (on press)
|
||||
// Button 32-34 = drag with left/middle/right button held
|
||||
const isScroll = button === 64 || button === 65;
|
||||
const isClick = isPress && button >= 0 && button <= 2;
|
||||
const isDrag = button >= 32 && button <= 34;
|
||||
if (isScroll || isClick || isDrag) {
|
||||
onInteraction?.();
|
||||
}
|
||||
// Throttle cursor updates to ~20fps to reduce re-renders
|
||||
const now = Date.now();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Handles different types of user interactions (text input, confirmations, choices)
|
||||
*/
|
||||
|
||||
import type { ClineAsk } from "@shared/ExtensionMessage"
|
||||
import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { Box, Text, useApp, useInput } from "ink"
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
@@ -11,7 +11,6 @@ import { useTaskController } from "../context/TaskContext"
|
||||
import { useLastCompletedAskMessage } from "../hooks/useStateSubscriber"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { jsonParseSafe } from "../utils/parser"
|
||||
import { getCliMessagePrefixIcon } from "./MessageRow"
|
||||
|
||||
interface AskPromptProps {
|
||||
onRespond?: (response: string) => void
|
||||
@@ -372,3 +371,73 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get emoji icon for message type
|
||||
*/
|
||||
function getCliMessagePrefixIcon(message: ClineMessage): string {
|
||||
if (message.type === "ask") {
|
||||
switch (message.ask) {
|
||||
case "followup":
|
||||
return "❓"
|
||||
case "command":
|
||||
case "command_output":
|
||||
return "⚙️"
|
||||
case "tool":
|
||||
return "🔧"
|
||||
case "completion_result":
|
||||
return "✅"
|
||||
case "api_req_failed":
|
||||
return "❌"
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
return "▶️"
|
||||
case "browser_action_launch":
|
||||
return "🌐"
|
||||
case "use_mcp_server":
|
||||
return "🔌"
|
||||
case "plan_mode_respond":
|
||||
return "📋"
|
||||
default:
|
||||
return "❔"
|
||||
}
|
||||
} else {
|
||||
switch (message.say) {
|
||||
case "task":
|
||||
return "📋"
|
||||
case "error":
|
||||
return "❌"
|
||||
case "text":
|
||||
return "💬"
|
||||
case "reasoning":
|
||||
return "🧠"
|
||||
case "completion_result":
|
||||
return "✅"
|
||||
case "user_feedback":
|
||||
return "👤"
|
||||
case "command":
|
||||
case "command_output":
|
||||
return "⚙️"
|
||||
case "tool":
|
||||
return "🔧"
|
||||
case "browser_action":
|
||||
case "browser_action_launch":
|
||||
case "browser_action_result":
|
||||
return "🌐"
|
||||
case "mcp_server_request_started":
|
||||
case "mcp_server_response":
|
||||
return "🔌"
|
||||
case "api_req_started":
|
||||
case "api_req_finished":
|
||||
return "🔄"
|
||||
case "checkpoint_created":
|
||||
return "💾"
|
||||
case "info":
|
||||
return "ℹ️"
|
||||
case "generate_explanation":
|
||||
return "📝"
|
||||
default:
|
||||
return " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,21 @@
|
||||
|
||||
import { Box, Text, useApp, useInput } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
// biome-ignore lint/style/useImportType: React is used as a value by JSX (jsx: "react" in tsconfig)
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import type { ApiProvider } from "@/shared/api"
|
||||
import { openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage"
|
||||
import { liteLlmDefaultModelId, openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { getAllFeaturedModels } from "../constants/featured-models"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useOcaAuth } from "../hooks/useOcaAuth"
|
||||
import { useScrollableList } from "../hooks/useScrollableList"
|
||||
import { type DetectedSources, detectImportSources, type ImportSource } from "../utils/import-configs"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
|
||||
import { useValidProviders } from "../utils/providers"
|
||||
import { ApiKeyInput } from "./ApiKeyInput"
|
||||
import { StaticRobotFrame } from "./AsciiMotionCli"
|
||||
import { type BedrockConfig, BedrockSetup } from "./BedrockSetup"
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
} from "./FeaturedModelPicker"
|
||||
import { ImportView } from "./ImportView"
|
||||
import { getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
|
||||
import { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder } from "./ProviderPicker"
|
||||
import { getProviderLabel } from "./ProviderPicker"
|
||||
|
||||
type AuthStep =
|
||||
| "menu"
|
||||
@@ -42,14 +43,12 @@ type AuthStep =
|
||||
| "success"
|
||||
| "error"
|
||||
| "cline_auth"
|
||||
| "oca_auth"
|
||||
| "cline_model"
|
||||
| "openai_codex_auth"
|
||||
| "bedrock"
|
||||
| "import"
|
||||
|
||||
// Featured models loaded from shared constants
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
|
||||
interface AuthViewProps {
|
||||
controller: any
|
||||
onComplete?: () => void
|
||||
@@ -148,6 +147,9 @@ const TextInput: React.FC<{
|
||||
|
||||
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome }) => {
|
||||
const { exit } = useApp()
|
||||
|
||||
const providers = useValidProviders()
|
||||
|
||||
const [step, setStep] = useState<AuthStep>("menu")
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>(
|
||||
StateManager.get().getApiConfiguration().actModeApiProvider ||
|
||||
@@ -166,11 +168,29 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const [importSource, setImportSource] = useState<ImportSource | null>(null)
|
||||
const [bedrockConfig, setBedrockConfig] = useState<BedrockConfig | null>(null)
|
||||
|
||||
// Use providers.json order, filtered to exclude CLI-incompatible providers
|
||||
const sortedProviders = useMemo(() => {
|
||||
return getProviderOrder().filter((p) => !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
// OCA auth hook - enabled when step is oca_auth
|
||||
const handleOcaAuthSuccess = useCallback(async () => {
|
||||
await applyProviderConfig({ providerId: "oca", controller })
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
await stateManager.flushPendingState()
|
||||
setSelectedProvider("oca")
|
||||
setModelId(liteLlmDefaultModelId)
|
||||
setStep("success")
|
||||
}, [controller])
|
||||
|
||||
const handleOcaAuthError = useCallback((error: Error) => {
|
||||
setErrorMessage(error.message)
|
||||
setStep("error")
|
||||
}, [])
|
||||
|
||||
const { startAuth: initiateOcaAuth } = useOcaAuth({
|
||||
controller,
|
||||
enabled: step === "oca_auth",
|
||||
onSuccess: handleOcaAuthSuccess,
|
||||
onError: handleOcaAuthError,
|
||||
})
|
||||
|
||||
// Main menu items - conditionally include import options
|
||||
const mainMenuItems: SelectItem[] = useMemo(() => {
|
||||
const items: SelectItem[] = [{ label: "Sign in with Cline", value: "cline_auth" }]
|
||||
@@ -196,15 +216,13 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const providerItems: SelectItem[] = useMemo(() => {
|
||||
const search = providerSearch.toLowerCase()
|
||||
const filtered = providerSearch
|
||||
? sortedProviders.filter(
|
||||
(p) => p.toLowerCase().includes(search) || getProviderLabel(p).toLowerCase().includes(search),
|
||||
)
|
||||
: sortedProviders
|
||||
? providers.filter((p) => p.toLowerCase().includes(search) || getProviderLabel(p).toLowerCase().includes(search))
|
||||
: providers
|
||||
return filtered.map((p: string) => ({
|
||||
label: getProviderLabel(p),
|
||||
value: p,
|
||||
}))
|
||||
}, [sortedProviders, providerSearch])
|
||||
}, [providers, providerSearch])
|
||||
|
||||
// Use shared scrollable list hook for provider windowing
|
||||
const TOTAL_PROVIDER_ROWS = 8
|
||||
@@ -251,22 +269,8 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}
|
||||
|
||||
if (authState.user && authState.user.email) {
|
||||
// Auth succeeded - save configuration and transition to success
|
||||
const stateManager = StateManager.get()
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
// Use provider-specific model ID key (cline uses OpenRouterModelId)
|
||||
const modelIdKey = getProviderModelIdKey("cline" as ApiProvider, mode as "act" | "plan")
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: "cline",
|
||||
[providerKey]: "cline",
|
||||
}
|
||||
if (modelIdKey) {
|
||||
config[modelIdKey] = openRouterDefaultModelId
|
||||
}
|
||||
stateManager.setApiConfiguration(config)
|
||||
stateManager.flushPendingState()
|
||||
|
||||
// Auth succeeded - save configuration and transition to model selection
|
||||
await applyProviderConfig({ providerId: "cline", controller })
|
||||
setSelectedProvider("cline")
|
||||
setModelId(openRouterDefaultModelId)
|
||||
setStep("cline_model")
|
||||
@@ -295,23 +299,10 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
await openAiCodexOAuthManager.waitForCallback()
|
||||
|
||||
// Success - save configuration
|
||||
await applyProviderConfig({ providerId: "openai-codex", controller })
|
||||
const stateManager = StateManager.get()
|
||||
const mode = stateManager.getGlobalSettingsKey("mode") || "act"
|
||||
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
// Use provider-specific model ID key (openai-codex uses generic apiModelId)
|
||||
const modelIdKey = getProviderModelIdKey("openai-codex" as ApiProvider, mode as "act" | "plan")
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: "openai-codex",
|
||||
planModeApiProvider: "openai-codex",
|
||||
[providerKey]: "openai-codex",
|
||||
}
|
||||
if (modelIdKey) {
|
||||
config[modelIdKey] = openAiCodexDefaultModelId
|
||||
}
|
||||
stateManager.setApiConfiguration(config)
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
setSelectedProvider("openai-codex")
|
||||
setModelId(openAiCodexDefaultModelId)
|
||||
setStep("success")
|
||||
@@ -334,6 +325,12 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
}
|
||||
}, [controller])
|
||||
|
||||
const startOcaAuth = useCallback(() => {
|
||||
setStep("oca_auth")
|
||||
setAuthStatus("Starting authentication...")
|
||||
initiateOcaAuth()
|
||||
}, [initiateOcaAuth])
|
||||
|
||||
const handleMainMenuSelect = useCallback(
|
||||
(value: string) => {
|
||||
if (value === "exit") {
|
||||
@@ -360,8 +357,8 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const handleProviderSelect = useCallback(
|
||||
(value: string) => {
|
||||
setSelectedProvider(value)
|
||||
if (value === "cline") {
|
||||
startClineAuth()
|
||||
if (value === "oca") {
|
||||
startOcaAuth()
|
||||
} else if (value === "openai-codex") {
|
||||
setStep("openai_codex_auth")
|
||||
startOpenAiCodexAuth()
|
||||
@@ -371,7 +368,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setStep("apikey")
|
||||
}
|
||||
},
|
||||
[startClineAuth, startOpenAiCodexAuth],
|
||||
[startOcaAuth, startOpenAiCodexAuth],
|
||||
)
|
||||
|
||||
const handleApiKeySubmit = useCallback(
|
||||
@@ -391,52 +388,23 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const saveConfiguration = useCallback(
|
||||
async (model: string, base: string) => {
|
||||
try {
|
||||
const stateManager = StateManager.get()
|
||||
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
const actModelKey = getProviderModelIdKey(selectedProvider as ApiProvider, "act")
|
||||
const planModelKey = getProviderModelIdKey(selectedProvider as ApiProvider, "plan")
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: selectedProvider,
|
||||
planModeApiProvider: selectedProvider,
|
||||
apiProvider: selectedProvider,
|
||||
}
|
||||
if (actModelKey) config[actModelKey] = model
|
||||
if (planModelKey) config[planModelKey] = model
|
||||
|
||||
// For cline/openrouter, also set model info (required for getModel() to return correct model)
|
||||
if (selectedProvider === "cline" || selectedProvider === "openrouter") {
|
||||
const openRouterModels = await controller?.readOpenRouterModels()
|
||||
const modelInfo = openRouterModels?.[model]
|
||||
if (modelInfo) {
|
||||
stateManager.setGlobalState("actModeOpenRouterModelInfo", modelInfo)
|
||||
stateManager.setGlobalState("planModeOpenRouterModelInfo", modelInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Add API key or Bedrock-specific config
|
||||
if (selectedProvider === "bedrock" && bedrockConfig) {
|
||||
const bedrockFields: Record<string, unknown> = {
|
||||
awsAuthentication: bedrockConfig.awsAuthentication,
|
||||
awsRegion: bedrockConfig.awsRegion,
|
||||
awsUseCrossRegionInference: bedrockConfig.awsUseCrossRegionInference,
|
||||
}
|
||||
if (bedrockConfig.awsProfile !== undefined) bedrockFields.awsProfile = bedrockConfig.awsProfile
|
||||
if (bedrockConfig.awsAccessKey) bedrockFields.awsAccessKey = bedrockConfig.awsAccessKey
|
||||
if (bedrockConfig.awsSecretKey) bedrockFields.awsSecretKey = bedrockConfig.awsSecretKey
|
||||
if (bedrockConfig.awsSessionToken) bedrockFields.awsSessionToken = bedrockConfig.awsSessionToken
|
||||
Object.assign(config, bedrockFields)
|
||||
} else if (apiKey) {
|
||||
const keyField = ProviderToApiKeyMap[selectedProvider as keyof typeof ProviderToApiKeyMap]
|
||||
if (keyField) {
|
||||
const fields = Array.isArray(keyField) ? keyField : [keyField]
|
||||
config[fields[0]] = apiKey
|
||||
}
|
||||
await applyBedrockConfig({
|
||||
bedrockConfig,
|
||||
modelId: model,
|
||||
controller,
|
||||
})
|
||||
} else {
|
||||
await applyProviderConfig({
|
||||
providerId: selectedProvider,
|
||||
apiKey,
|
||||
modelId: model,
|
||||
baseUrl: base,
|
||||
controller,
|
||||
})
|
||||
}
|
||||
|
||||
if (base) {
|
||||
config.openAiBaseUrl = base
|
||||
}
|
||||
stateManager.setApiConfiguration(config)
|
||||
const stateManager = StateManager.get()
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
@@ -566,6 +534,9 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
setBaseUrl("")
|
||||
setStep("modelid")
|
||||
break
|
||||
case "oca_auth":
|
||||
setStep("provider")
|
||||
break
|
||||
case "cline_auth":
|
||||
setStep("menu")
|
||||
break
|
||||
@@ -704,6 +675,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
</Box>
|
||||
)
|
||||
|
||||
case "oca_auth":
|
||||
case "cline_auth":
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
@@ -793,6 +765,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
"modelid",
|
||||
"baseurl",
|
||||
"cline_auth",
|
||||
"oca_auth",
|
||||
"cline_model",
|
||||
"openai_codex_auth",
|
||||
"bedrock",
|
||||
@@ -885,7 +858,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
{index === menuIndex ? "❯ " : " "}
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.value === "cline_auth" && <Text color="yellow"> (try Kimi K2.5 free!)</Text>}
|
||||
{item.value === "cline_auth" && <Text color="yellow"> (try Opus 4.6!)</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@shared/ClineAccount"
|
||||
import { COMMAND_OUTPUT_STRING } from "@shared/combineCommandSequences"
|
||||
import type { ClineAskUseMcpServer, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { Box, Text } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import React from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
@@ -126,10 +127,20 @@ interface ChatMessageProps {
|
||||
* For this to work properly, parent containers must have width="100%"
|
||||
* so flexGrow={1} on the content box has a reference width to fill.
|
||||
*/
|
||||
const DotRow: React.FC<{ children: React.ReactNode; color?: string }> = ({ children, color }) => (
|
||||
const DotRow: React.FC<{ children: React.ReactNode; color?: string; flashing?: boolean }> = ({
|
||||
children,
|
||||
color,
|
||||
flashing = false,
|
||||
}) => (
|
||||
<Box flexDirection="row">
|
||||
<Box width={2}>
|
||||
<Text color={color}>⏺</Text>
|
||||
{flashing ? (
|
||||
<Text color={color}>
|
||||
<Spinner type="toggle8" />
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={color}>⏺</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexGrow={1}>{children}</Box>
|
||||
</Box>
|
||||
@@ -223,8 +234,8 @@ function formatToolResult(result: string, maxLines: number = 5): string[] {
|
||||
return displayLines
|
||||
}
|
||||
|
||||
export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
const { type, ask, say, text } = message
|
||||
export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStreaming }) => {
|
||||
const { type, ask, say, text, partial } = message
|
||||
const toolColor = mode === "plan" ? "yellow" : COLORS.primaryBlue
|
||||
const { columns: terminalWidth } = useTerminalSize()
|
||||
|
||||
@@ -280,11 +291,11 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
if (isFileEditTool(toolInfo.toolName) && filePath && toolInfo.args.content) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<ToolCallText args={toolInfo.args} isAsk={isToolAsk} mode={mode} toolName={toolInfo.toolName} />
|
||||
</DotRow>
|
||||
<Box marginLeft={2}>
|
||||
<DiffView content={toolInfo.args.content} filePath={filePath as string | undefined} />
|
||||
<DiffView content={toolInfo.args.content as string} filePath={filePath as string | undefined} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
@@ -299,7 +310,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<ToolCallText args={toolInfo.args} isAsk={isToolAsk} mode={mode} toolName={toolInfo.toolName} />
|
||||
</DotRow>
|
||||
{contentLines.length > 0 && (
|
||||
@@ -318,7 +329,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
if (isToolSay) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text color={toolColor}>{truncate(text, 100)}</Text>
|
||||
</DotRow>
|
||||
</Box>
|
||||
@@ -340,7 +351,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text>
|
||||
<Text color={toolColor}>{label}</Text>
|
||||
<Text>{truncate(command, 120)}</Text>
|
||||
@@ -379,12 +390,12 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
if ((type === "ask" && ask === "use_mcp_server") || say === "use_mcp_server") {
|
||||
const isAsk = type === "ask"
|
||||
const parsed = text
|
||||
? jsonParseSafe<ClineAskUseMcpServer>(text, {
|
||||
type: undefined as ClineAskUseMcpServer["type"] | undefined,
|
||||
? jsonParseSafe<Partial<ClineAskUseMcpServer> & { serverName: string }>(text, {
|
||||
type: undefined,
|
||||
serverName: "unknown server",
|
||||
toolName: undefined as string | undefined,
|
||||
arguments: undefined as string | undefined,
|
||||
uri: undefined as string | undefined,
|
||||
toolName: undefined,
|
||||
arguments: undefined,
|
||||
uri: undefined,
|
||||
})
|
||||
: undefined
|
||||
|
||||
@@ -410,7 +421,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text>
|
||||
<Text color={toolColor}>{actionLabel}</Text>
|
||||
<Text>{`: ${serverName}`}</Text>
|
||||
@@ -440,7 +451,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
const lines = formatToolResult(text, 8)
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text color={toolColor}>MCP response</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" marginLeft={2} width="100%">
|
||||
@@ -581,7 +592,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
if (say === "browser_action" || say === "browser_action_launch") {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text>
|
||||
<Text color={toolColor}>Cline used the browser</Text>
|
||||
{text && (
|
||||
@@ -600,7 +611,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
if (say === "mcp_server_request_started") {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text>
|
||||
<Text color={toolColor}>Cline is using an MCP tool</Text>
|
||||
{text && (
|
||||
@@ -728,7 +739,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
if (type === "ask" && ask === "condense" && text) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={COLORS.primaryBlue}>
|
||||
<DotRow color={COLORS.primaryBlue} flashing={partial === true && isStreaming}>
|
||||
<Text bold color={COLORS.primaryBlue}>
|
||||
Cline wants to condense your conversation:
|
||||
</Text>
|
||||
@@ -744,7 +755,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
if (type === "ask" && ask === "summarize_task" && text) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={COLORS.primaryBlue}>
|
||||
<DotRow color={COLORS.primaryBlue} flashing={partial === true && isStreaming}>
|
||||
<Text bold color={COLORS.primaryBlue}>
|
||||
Cline wants to summarize the task:
|
||||
</Text>
|
||||
@@ -760,7 +771,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode }) => {
|
||||
if (type === "ask" && ask === "report_bug" && text) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={COLORS.primaryBlue}>
|
||||
<DotRow color={COLORS.primaryBlue} flashing={partial === true && isStreaming}>
|
||||
<Text bold color={COLORS.primaryBlue}>
|
||||
Cline wants to create a Github issue:
|
||||
</Text>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Tests for ChatView component exit and cleanup behavior
|
||||
*
|
||||
* These tests verify that when the user exits (via Ctrl+C or other means),
|
||||
* These tests verify that when the user exits (via shutdown event or other means),
|
||||
* the input field is properly hidden before the app terminates.
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { ChatView } from "./ChatView"
|
||||
|
||||
// Helper to wait for async state updates
|
||||
// Using 60ms since handleExit has a 50ms setTimeout
|
||||
const delay = (ms: number = 60) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
// Type for our exit mock function
|
||||
@@ -175,12 +174,21 @@ vi.mock("@shared/getApiMetrics", () => ({
|
||||
totalTokensOut: 0,
|
||||
totalCost: 0,
|
||||
})),
|
||||
getLastApiReqTotalTokens: vi.fn(() => 0),
|
||||
}))
|
||||
|
||||
vi.mock("child_process", () => ({
|
||||
exec: vi.fn(),
|
||||
execSync: vi.fn(() => "main"),
|
||||
}))
|
||||
|
||||
// Mock telemetry service to prevent HostProvider errors in shutdown handler
|
||||
vi.mock("@/services/telemetry", () => ({
|
||||
telemetryService: {
|
||||
captureHostEvent: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Helper to create a typed mock for onExit
|
||||
const createExitMock = (): ExitMockFn => vi.fn() as ExitMockFn
|
||||
|
||||
@@ -213,33 +221,6 @@ describe("ChatView Exit and Cleanup", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Ctrl+C exit handling", () => {
|
||||
it("should hide input but keep footer, then call onExit", async () => {
|
||||
const { lastFrame, stdin } = render(<ChatView onExit={mockOnExit} />)
|
||||
|
||||
// Verify UI visible before Ctrl+C
|
||||
expect(lastFrame()).toContain("Input:")
|
||||
expect(lastFrame()).toContain("@ for files")
|
||||
|
||||
// Simulate Ctrl+C
|
||||
stdin.write("\x03")
|
||||
|
||||
// onExit should not be called immediately
|
||||
expect(mockOnExit).not.toHaveBeenCalled()
|
||||
|
||||
// Wait for state update and callback
|
||||
await delay()
|
||||
|
||||
// Input should be hidden, but footer should remain
|
||||
const frameAfter = lastFrame()
|
||||
expect(frameAfter).not.toContain("Input:")
|
||||
expect(frameAfter).toContain("@ for files")
|
||||
|
||||
// onExit should have been called
|
||||
expect(mockOnExit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Shutdown event handling", () => {
|
||||
it("should subscribe on mount and unsubscribe on unmount", () => {
|
||||
const { unmount } = render(<ChatView onExit={mockOnExit} />)
|
||||
@@ -249,39 +230,59 @@ describe("ChatView Exit and Cleanup", () => {
|
||||
expect(shutdownMockState.listeners.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should hide UI when shutdown event fires", async () => {
|
||||
it("should hide input when shutdown event fires", async () => {
|
||||
const { lastFrame } = render(<ChatView onExit={mockOnExit} />)
|
||||
|
||||
// Input should be visible initially
|
||||
expect(lastFrame()).toContain("Input:")
|
||||
|
||||
// Fire shutdown event (simulates Ctrl+C)
|
||||
shutdownMockState.fire()
|
||||
await delay()
|
||||
|
||||
// Input should be hidden after shutdown
|
||||
expect(lastFrame()).not.toContain("Input:")
|
||||
})
|
||||
|
||||
it("should preserve footer when shutdown event fires", async () => {
|
||||
const { lastFrame } = render(<ChatView onExit={mockOnExit} />)
|
||||
|
||||
// Footer should be visible initially
|
||||
expect(lastFrame()).toContain("@ for files")
|
||||
|
||||
// Fire shutdown event
|
||||
shutdownMockState.fire()
|
||||
await delay()
|
||||
|
||||
// Footer should still be present (only input is hidden)
|
||||
expect(lastFrame()).toContain("@ for files")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle exit when onExit prop is undefined", async () => {
|
||||
const { lastFrame, stdin } = render(<ChatView />)
|
||||
it("should handle shutdown event when onExit prop is undefined", async () => {
|
||||
const { lastFrame } = render(<ChatView />)
|
||||
|
||||
stdin.write("\x03")
|
||||
// Fire shutdown event
|
||||
shutdownMockState.fire()
|
||||
await delay()
|
||||
|
||||
// Should not throw, UI should still hide
|
||||
expect(lastFrame()).not.toContain("Input:")
|
||||
})
|
||||
|
||||
it("should handle multiple Ctrl+C presses gracefully", async () => {
|
||||
const { stdin } = render(<ChatView onExit={mockOnExit} />)
|
||||
it("should handle multiple shutdown events gracefully", async () => {
|
||||
const { lastFrame } = render(<ChatView onExit={mockOnExit} />)
|
||||
|
||||
stdin.write("\x03")
|
||||
stdin.write("\x03")
|
||||
stdin.write("\x03")
|
||||
// Fire multiple shutdown events
|
||||
shutdownMockState.fire()
|
||||
shutdownMockState.fire()
|
||||
shutdownMockState.fire()
|
||||
|
||||
await delay()
|
||||
|
||||
expect(mockOnExit).toHaveBeenCalled()
|
||||
// UI should still hide properly
|
||||
expect(lastFrame()).not.toContain("Input:")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -294,14 +295,15 @@ describe("ChatView UI State During Exit", () => {
|
||||
|
||||
it("should preserve static content and footer, only hide input during exit", async () => {
|
||||
const onExit = createExitMock()
|
||||
const { lastFrame, stdin } = render(<ChatView onExit={onExit} />)
|
||||
const { lastFrame } = render(<ChatView onExit={onExit} />)
|
||||
|
||||
// Footer contains auto-approve toggle
|
||||
expect(lastFrame()).toContain("Auto-approve")
|
||||
expect(lastFrame()).toContain("What can I do for you?")
|
||||
expect(lastFrame()).toContain("Input:")
|
||||
|
||||
stdin.write("\x03")
|
||||
// Fire shutdown event
|
||||
shutdownMockState.fire()
|
||||
await delay()
|
||||
|
||||
const frameAfter = lastFrame()
|
||||
|
||||
@@ -103,15 +103,17 @@
|
||||
|
||||
import type { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import { combineHookSequences } from "@shared/combineHookSequences"
|
||||
import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { getApiMetrics, getLastApiReqTotalTokens } from "@shared/getApiMetrics"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
|
||||
import type { SlashCommandInfo } from "@shared/proto/cline/slash"
|
||||
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
|
||||
import { getProviderModelIdKey } from "@shared/storage"
|
||||
import { getProviderDefaultModelId, getProviderModelIdKey } from "@shared/storage"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { execSync } from "child_process"
|
||||
import { Box, Static, Text, useApp, useInput } from "ink"
|
||||
// biome-ignore lint/style/useImportType: JSX requires React as a value (jsx: "react" in tsconfig)
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { getAvailableSlashCommands } from "@/core/controller/slash/getAvailableSlashCommands"
|
||||
import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
|
||||
@@ -136,6 +138,7 @@ import {
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { jsonParseSafe, parseImagesFromInput } from "../utils/parser"
|
||||
import { extractSlashQuery, filterCommands, insertSlashCommand, sortCommandsWorkflowsFirst } from "../utils/slash-commands"
|
||||
import { waitFor } from "../utils/timeout"
|
||||
import { isFileEditTool, parseToolFromMessage } from "../utils/tools"
|
||||
import { shutdownEvent } from "../vscode-shim"
|
||||
import { ActionButtons, type ButtonActionType, getButtonConfig, getVisibleButtons } from "./ActionButtons"
|
||||
@@ -208,9 +211,9 @@ function getGitDiffStats(cwd?: string): GitDiffStats | null {
|
||||
const delMatch = output.match(/(\d+) deletion/)
|
||||
|
||||
return {
|
||||
files: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
||||
additions: addMatch ? parseInt(addMatch[1], 10) : 0,
|
||||
deletions: delMatch ? parseInt(delMatch[1], 10) : 0,
|
||||
files: filesMatch ? Number.parseInt(filesMatch[1], 10) : 0,
|
||||
additions: addMatch ? Number.parseInt(addMatch[1], 10) : 0,
|
||||
deletions: delMatch ? Number.parseInt(delMatch[1], 10) : 0,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
@@ -221,7 +224,7 @@ function getGitDiffStats(cwd?: string): GitDiffStats | null {
|
||||
* Create a progress bar for context window usage
|
||||
* Returns { filled, empty } strings to allow different coloring
|
||||
*/
|
||||
function createContextBar(used: number, total: number, width: number = 8): { filled: string; empty: string } {
|
||||
function createContextBar(used: number, total: number, width = 8): { filled: string; empty: string } {
|
||||
const ratio = Math.min(used / total, 1)
|
||||
// Use ceil so any usage > 0 shows at least one bar
|
||||
const filledCount = used > 0 ? Math.max(1, Math.ceil(ratio * width)) : 0
|
||||
@@ -311,7 +314,7 @@ function parseAskOptions(text: string): string[] {
|
||||
*/
|
||||
function expandPastedTexts(text: string, pastedTexts: Map<number, string>): string {
|
||||
return text.replace(/\[Pasted text #(\d+) \+\d+ lines\]/g, (match, num) => {
|
||||
const content = pastedTexts.get(parseInt(num, 10))
|
||||
const content = pastedTexts.get(Number.parseInt(num, 10))
|
||||
return content ?? match
|
||||
})
|
||||
}
|
||||
@@ -348,9 +351,11 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
insertText: insertTextAtCursor,
|
||||
} = useTextInput()
|
||||
|
||||
// Ref for text input (used by useHomeEndKeys)
|
||||
// Refs for text input and cursor position (used by useHomeEndKeys and to avoid stale closures in useInput)
|
||||
const textInputRef = useRef(textInput)
|
||||
textInputRef.current = textInput
|
||||
const cursorPosRef = useRef(cursorPos)
|
||||
cursorPosRef.current = cursorPos
|
||||
|
||||
const [fileResults, setFileResults] = useState<FileSearchResult[]>([])
|
||||
const [selectedIndex, setSelectedIndex] = useState(0) // For file menu
|
||||
@@ -423,7 +428,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return stateManager.getGlobalSettingsKey("mode") || "act"
|
||||
})
|
||||
|
||||
const [yolo, setYolo] = useState<boolean>(() => StateManager.get().getGlobalSettingsKey("yoloModeToggled") ?? false)
|
||||
const [yolo, _setYolo] = useState<boolean>(() => StateManager.get().getGlobalSettingsKey("yoloModeToggled") ?? false)
|
||||
const [autoApproveAll, setAutoApproveAll] = useState<boolean>(
|
||||
() => StateManager.get().getGlobalSettingsKey("autoApproveAllToggled") ?? false,
|
||||
)
|
||||
@@ -451,11 +456,12 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// Get model ID based on current mode and provider
|
||||
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
// Re-read when activePanel changes (settings panel closes) to pick up changes
|
||||
// Falls back to provider's default model if no model has been explicitly set
|
||||
const modelId = useMemo(() => {
|
||||
if (!provider) return ""
|
||||
const stateManager = StateManager.get()
|
||||
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
|
||||
return (stateManager.getGlobalSettingsKey(modelKey as string) as string) || ""
|
||||
return (stateManager.getGlobalSettingsKey(modelKey) as string) || getProviderDefaultModelId(provider as ApiProvider) || ""
|
||||
}, [mode, provider, activePanel])
|
||||
|
||||
const toggleMode = useCallback(async () => {
|
||||
@@ -604,8 +610,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return true
|
||||
})
|
||||
|
||||
// Combine command messages with their output (like webview does)
|
||||
return combineCommandSequences(filtered)
|
||||
// Combine hook messages with their output, then command messages (like webview does)
|
||||
// CLI always has hooks enabled, so we always apply combineHookSequences
|
||||
const withHooks = combineHookSequences(filtered)
|
||||
return combineCommandSequences(withHooks)
|
||||
}, [messages])
|
||||
|
||||
// Detect task switches by watching first message timestamp change.
|
||||
@@ -881,6 +889,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
)
|
||||
|
||||
// Auto-submit initial prompt if provided
|
||||
// When taskId is also provided, this sends the prompt to resume the existing task
|
||||
// When no taskId, this creates a new task with the prompt
|
||||
useEffect(() => {
|
||||
const autoSubmit = async () => {
|
||||
if (!initialPrompt && (!initialImages || initialImages.length === 0)) {
|
||||
@@ -901,8 +911,32 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (initialPrompt) {
|
||||
setTerminalTitle(initialPrompt)
|
||||
}
|
||||
// initialImages are already data URLs from index.ts processing
|
||||
await ctrl.initTask(initialPrompt || "", initialImages && initialImages.length > 0 ? initialImages : undefined)
|
||||
|
||||
if (taskId) {
|
||||
// Resuming an existing task with a prompt - wait for task to load first
|
||||
// The task loading happens in the other useEffect via showTaskWithId
|
||||
// We need to wait for it to complete before sending the resume message
|
||||
const task = await waitFor(() => ctrl.task, 5000)
|
||||
|
||||
if (task) {
|
||||
// Send the prompt as a message to resume the task
|
||||
await task.handleWebviewAskResponse("messageResponse", initialPrompt || "")
|
||||
} else {
|
||||
// Task failed to load, fall back to creating new task
|
||||
Logger.error(`Failed to load task ${taskId} for resume, creating new task instead`)
|
||||
await ctrl.initTask(
|
||||
initialPrompt || "",
|
||||
initialImages && initialImages.length > 0 ? initialImages : undefined,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// New task - use initTask
|
||||
// initialImages are already data URLs from index.ts processing
|
||||
await ctrl.initTask(
|
||||
initialPrompt || "",
|
||||
initialImages && initialImages.length > 0 ? initialImages : undefined,
|
||||
)
|
||||
}
|
||||
} catch (_error) {
|
||||
onError?.()
|
||||
}
|
||||
@@ -998,11 +1032,11 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// 3. Handle Option+arrow via key.meta (backup - Ink sometimes parses these instead of passing raw sequence)
|
||||
if (key.meta) {
|
||||
if (key.leftArrow) {
|
||||
setCursorPos(findWordStart(textInput, cursorPos))
|
||||
setCursorPos(findWordStart(textInputRef.current, cursorPosRef.current))
|
||||
return
|
||||
}
|
||||
if (key.rightArrow) {
|
||||
setCursorPos(findWordEnd(textInput, cursorPos))
|
||||
setCursorPos(findWordEnd(textInputRef.current, cursorPosRef.current))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1188,7 +1222,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (hasPrimary && buttonConfig.primaryAction) {
|
||||
handleButtonAction(buttonConfig.primaryAction, true)
|
||||
return
|
||||
} else if (hasSecondary && !hasPrimary && buttonConfig.secondaryAction) {
|
||||
}
|
||||
if (hasSecondary && !hasPrimary && buttonConfig.secondaryAction) {
|
||||
handleButtonAction(buttonConfig.secondaryAction, false)
|
||||
return
|
||||
}
|
||||
@@ -1209,7 +1244,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
// Number selection for options (only when no text typed yet)
|
||||
if (askType === "options") {
|
||||
const num = parseInt(input, 10)
|
||||
const num = Number.parseInt(input, 10)
|
||||
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= askOptions.length) {
|
||||
const selectedOption = askOptions[num - 1]
|
||||
sendAskResponse("messageResponse", selectedOption)
|
||||
@@ -1251,10 +1286,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
pasteUpdateTimeoutRef.current = setTimeout(() => {
|
||||
const newPlaceholder = `[Pasted text #${pasteNum} +${activePasteLinesRef.current} lines]`
|
||||
setTextInput((prev) => {
|
||||
const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`)
|
||||
return prev.replace(pattern, newPlaceholder)
|
||||
})
|
||||
const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`)
|
||||
const newText = textInputRef.current.replace(pattern, newPlaceholder)
|
||||
textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works
|
||||
setTextInput(newText)
|
||||
// Update cursor to be right after the placeholder
|
||||
setCursorPos(activePasteStartPosRef.current + newPlaceholder.length)
|
||||
Logger.info(`Paste #${pasteNum} complete: ${activePasteLinesRef.current} lines`)
|
||||
@@ -1267,7 +1302,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
pasteCounterRef.current += 1
|
||||
const pasteNum = pasteCounterRef.current
|
||||
activePasteNumRef.current = pasteNum
|
||||
activePasteStartPosRef.current = cursorPos // Track where placeholder starts
|
||||
const currentCursorPos = cursorPosRef.current // Use ref to avoid stale closure
|
||||
activePasteStartPosRef.current = currentCursorPos // Track where placeholder starts
|
||||
// Count line breaks in the pasted content (handle both \n and \r)
|
||||
const extraLines = input.match(/[\r\n]/g)?.length || 0
|
||||
activePasteLinesRef.current = extraLines // Track total lines
|
||||
@@ -1279,8 +1315,11 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return next
|
||||
})
|
||||
|
||||
setTextInput((prev) => prev.slice(0, cursorPos) + placeholder + prev.slice(cursorPos))
|
||||
setCursorPos(cursorPos + placeholder.length)
|
||||
const newText =
|
||||
textInputRef.current.slice(0, currentCursorPos) + placeholder + textInputRef.current.slice(currentCursorPos)
|
||||
textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works
|
||||
setTextInput(newText)
|
||||
setCursorPos(currentCursorPos + placeholder.length)
|
||||
return // Exit early - don't also add the raw input via normal handling below
|
||||
}
|
||||
|
||||
@@ -1309,15 +1348,15 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return
|
||||
}
|
||||
if (key.rightArrow && !inSlashMenu && !inFileMenu) {
|
||||
setCursorPos((pos) => Math.min(textInput.length, pos + 1))
|
||||
setCursorPos((pos) => Math.min(textInputRef.current.length, pos + 1))
|
||||
return
|
||||
}
|
||||
if (key.upArrow && !inSlashMenu && !inFileMenu) {
|
||||
setCursorPos(moveCursorUp(textInput, cursorPos))
|
||||
setCursorPos(moveCursorUp(textInputRef.current, cursorPosRef.current))
|
||||
return
|
||||
}
|
||||
if (key.downArrow && !inSlashMenu && !inFileMenu) {
|
||||
setCursorPos(moveCursorDown(textInput, cursorPos))
|
||||
setCursorPos(moveCursorDown(textInputRef.current, cursorPosRef.current))
|
||||
return
|
||||
}
|
||||
// Normal input (single char or short paste)
|
||||
@@ -1383,10 +1422,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
|
||||
{/* Dynamic region - only current streaming message + input */}
|
||||
<Box flexDirection="column" width="100%">
|
||||
{/* Animated robot and welcome text - only shown before messages start and user hasn't scrolled */}
|
||||
{/* Animated robot and welcome text - only shown before messages start and user hasn't interacted */}
|
||||
{isWelcomeState && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<AsciiMotionCli onScroll={() => setUserScrolled(true)} />
|
||||
<AsciiMotionCli onInteraction={() => setUserScrolled(true)} />
|
||||
<Text> </Text>
|
||||
<Text bold color="white">
|
||||
{centerText("What can I do for you?")}
|
||||
|
||||
@@ -27,12 +27,12 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{title && (
|
||||
<>
|
||||
<Text>
|
||||
<Text bold color={COLORS.primaryBlue}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text> </Text>
|
||||
</>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{featuredModels.map((model, i) => {
|
||||
@@ -45,15 +45,15 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
<Text bold color={isSelected ? COLORS.primaryBlue : "white"}>
|
||||
{model.name}
|
||||
</Text>
|
||||
{model.label && (
|
||||
<>
|
||||
{model.labels.map((label) => (
|
||||
<Text key={label}>
|
||||
<Text> </Text>
|
||||
<Text backgroundColor={model.label === "FREE" ? "gray" : COLORS.primaryBlue} color="black">
|
||||
<Text backgroundColor={label === "FREE" ? "gray" : COLORS.primaryBlue} color="black">
|
||||
{" "}
|
||||
{model.label}{" "}
|
||||
{label}{" "}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="gray">{model.description}</Text>
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
import { Box, Text, useInput } from "ink"
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { ApiProvider } from "@/shared/api"
|
||||
import { getProviderModelIdKey } from "@/shared/storage"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import {
|
||||
@@ -18,6 +16,7 @@ import {
|
||||
importFromCodex,
|
||||
importFromOpenCode,
|
||||
} from "../utils/import-configs"
|
||||
import { applyProviderConfig } from "../utils/provider-config"
|
||||
|
||||
type ImportStep = "select" | "confirm" | "saving" | "error"
|
||||
|
||||
@@ -61,25 +60,12 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
|
||||
return
|
||||
}
|
||||
|
||||
await applyProviderConfig({
|
||||
providerId: selectedKey.provider,
|
||||
apiKey: selectedKey.key,
|
||||
modelId: selectedKey.modelId,
|
||||
})
|
||||
const stateManager = StateManager.get()
|
||||
const config: Record<string, string> = {
|
||||
actModeApiProvider: selectedKey.provider,
|
||||
planModeApiProvider: selectedKey.provider,
|
||||
apiProvider: selectedKey.provider,
|
||||
}
|
||||
|
||||
// Set API key
|
||||
config[selectedKey.keyField] = selectedKey.key
|
||||
|
||||
// Set model ID if available (use provider-specific keys)
|
||||
if (selectedKey.modelId) {
|
||||
const actModelKey = getProviderModelIdKey(selectedKey.provider as ApiProvider, "act")
|
||||
const planModelKey = getProviderModelIdKey(selectedKey.provider as ApiProvider, "plan")
|
||||
if (actModelKey) config[actModelKey] = selectedKey.modelId
|
||||
if (planModelKey) config[planModelKey] = selectedKey.modelId
|
||||
}
|
||||
|
||||
stateManager.setApiConfiguration(config)
|
||||
stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
|
||||
@@ -20,11 +20,13 @@ interface PanelProps {
|
||||
tabs?: PanelTab[]
|
||||
/** Current tab key - required when tabs are provided */
|
||||
currentTab?: string
|
||||
/** Whether currently in a subpage (shows "Esc to go back" and hides arrow key hint) */
|
||||
isSubpage?: boolean
|
||||
/** Panel content */
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const Panel: React.FC<PanelProps> = ({ label, tabs, currentTab, children }) => {
|
||||
export const Panel: React.FC<PanelProps> = ({ label, tabs, currentTab, isSubpage, children }) => {
|
||||
const { columns } = useTerminalSize()
|
||||
const currentTabIndex = currentTab && tabs ? tabs.findIndex((t) => t.key === currentTab) : 0
|
||||
|
||||
@@ -35,7 +37,7 @@ export const Panel: React.FC<PanelProps> = ({ label, tabs, currentTab, children
|
||||
<Text bold color={COLORS.primaryBlue}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text color="gray"> (Esc to close)</Text>
|
||||
<Text color="gray"> (Esc to {isSubpage ? "go back" : "close"})</Text>
|
||||
</Box>
|
||||
|
||||
{/* Tab bar if tabs are provided */}
|
||||
@@ -53,7 +55,7 @@ export const Panel: React.FC<PanelProps> = ({ label, tabs, currentTab, children
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
<Text color="gray"> (←/→)</Text>
|
||||
{!isSubpage && <Text color="gray"> (←/→)</Text>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { ApiConfiguration } from "@/shared/api"
|
||||
import { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder } from "../utils/providers"
|
||||
import { SearchableList, SearchableListItem } from "./SearchableList"
|
||||
import { getProviderLabel, useValidProviders } from "../utils/providers"
|
||||
import { SearchableList, type SearchableListItem } from "./SearchableList"
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder }
|
||||
export { getProviderLabel }
|
||||
|
||||
/**
|
||||
* Check if a provider is configured (has required credentials/settings)
|
||||
@@ -18,8 +18,8 @@ export { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder }
|
||||
function isProviderConfigured(providerId: string, config: ApiConfiguration): boolean {
|
||||
switch (providerId) {
|
||||
case "cline":
|
||||
// Check if user has Cline account auth data stored
|
||||
return !!(config as Record<string, unknown>)["cline:clineAccountId"]
|
||||
// Check if user has Cline API key or Cline account auth data stored
|
||||
return !!(config.clineApiKey ?? config["cline:clineAccountId"])
|
||||
case "anthropic":
|
||||
return !!config.apiKey
|
||||
case "openrouter":
|
||||
@@ -125,17 +125,16 @@ interface ProviderPickerProps {
|
||||
export const ProviderPicker: React.FC<ProviderPickerProps> = ({ onSelect, isActive = true }) => {
|
||||
// Get API configuration to check which providers are configured
|
||||
const apiConfig = StateManager.get().getApiConfiguration()
|
||||
const sorted = useValidProviders()
|
||||
|
||||
// Use providers.json order, filtered to exclude CLI-incompatible providers
|
||||
const items: SearchableListItem[] = useMemo(() => {
|
||||
const sorted = getProviderOrder().filter((p: string) => !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
|
||||
return sorted.map((providerId: string) => ({
|
||||
id: providerId,
|
||||
label: getProviderLabel(providerId),
|
||||
suffix: isProviderConfigured(providerId, apiConfig) ? "(Configured)" : undefined,
|
||||
}))
|
||||
}, [apiConfig])
|
||||
}, [apiConfig, sorted])
|
||||
|
||||
return <SearchableList isActive={isActive} items={items} onSelect={(item) => onSelect(item.id)} />
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import type { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import type { ApiProvider } from "@shared/api"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@shared/storage"
|
||||
import type { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { getProviderModelIdKey, isSettingsKey, ProviderToApiKeyMap } from "@shared/storage"
|
||||
import type { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { Box, Text, useInput } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
@@ -21,8 +21,9 @@ import { openExternal } from "@/utils/env"
|
||||
import { version as CLI_VERSION } from "../../package.json"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useOcaAuth } from "../hooks/useOcaAuth"
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { applyProviderConfig } from "../utils/provider-config"
|
||||
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
|
||||
import { ApiKeyInput } from "./ApiKeyInput"
|
||||
import { type BedrockConfig, BedrockSetup } from "./BedrockSetup"
|
||||
import { Checkbox } from "./Checkbox"
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
isBrowseAllSelected,
|
||||
} from "./FeaturedModelPicker"
|
||||
import { LanguagePicker } from "./LanguagePicker"
|
||||
import { getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
|
||||
import { hasModelPicker, ModelPicker } from "./ModelPicker"
|
||||
import { OrganizationPicker } from "./OrganizationPicker"
|
||||
import { Panel, PanelTab } from "./Panel"
|
||||
import { getProviderLabel, ProviderPicker } from "./ProviderPicker"
|
||||
@@ -68,41 +69,35 @@ const TABS: PanelTab[] = [
|
||||
// Settings configuration for simple boolean toggles
|
||||
const FEATURE_SETTINGS = {
|
||||
autoCondense: {
|
||||
stateKey: "useAutoCondense" as const,
|
||||
stateKey: "useAutoCondense",
|
||||
default: false,
|
||||
label: "Auto-condense",
|
||||
description: "Automatically summarize long conversations",
|
||||
},
|
||||
webTools: {
|
||||
stateKey: "clineWebToolsEnabled" as const,
|
||||
stateKey: "clineWebToolsEnabled",
|
||||
default: true,
|
||||
label: "Web tools",
|
||||
description: "Enable web search and fetch tools",
|
||||
},
|
||||
strictPlanMode: {
|
||||
stateKey: "strictPlanModeEnabled" as const,
|
||||
stateKey: "strictPlanModeEnabled",
|
||||
default: true,
|
||||
label: "Strict plan mode",
|
||||
description: "Require explicit mode switching",
|
||||
},
|
||||
nativeToolCall: {
|
||||
stateKey: "nativeToolCallEnabled" as const,
|
||||
stateKey: "nativeToolCallEnabled",
|
||||
default: true,
|
||||
label: "Native tool call",
|
||||
description: "Use model's native tool calling API",
|
||||
},
|
||||
parallelToolCalling: {
|
||||
stateKey: "enableParallelToolCalling" as const,
|
||||
stateKey: "enableParallelToolCalling",
|
||||
default: false,
|
||||
label: "Parallel tool calling",
|
||||
description: "Allow multiple tools in a single response",
|
||||
},
|
||||
skillsEnabled: {
|
||||
stateKey: "skillsEnabled" as const,
|
||||
default: false,
|
||||
label: "Skills",
|
||||
description: "Enable reusable agent instructions",
|
||||
},
|
||||
} as const
|
||||
|
||||
type FeatureKey = keyof typeof FEATURE_SETTINGS
|
||||
@@ -150,7 +145,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
const [features, setFeatures] = useState<Record<FeatureKey, boolean>>(() => {
|
||||
const initial: Record<string, boolean> = {}
|
||||
for (const [key, config] of Object.entries(FEATURE_SETTINGS)) {
|
||||
initial[key] = stateManager.getGlobalSettingsKey(config.stateKey) ?? config.default
|
||||
if (isSettingsKey(config.stateKey)) {
|
||||
initial[key] = stateManager.getGlobalSettingsKey(config.stateKey)
|
||||
} else {
|
||||
initial[key] = stateManager.getGlobalStateKey(config.stateKey)
|
||||
}
|
||||
}
|
||||
return initial as Record<FeatureKey, boolean>
|
||||
})
|
||||
@@ -201,6 +200,23 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
const [modelRefreshKey, setModelRefreshKey] = useState(0)
|
||||
const refreshModelIds = useCallback(() => setModelRefreshKey((k) => k + 1), [])
|
||||
|
||||
// OCA auth hook
|
||||
const handleOcaAuthSuccess = useCallback(async () => {
|
||||
await applyProviderConfig({ providerId: "oca", controller })
|
||||
setProvider("oca")
|
||||
refreshModelIds()
|
||||
}, [controller, refreshModelIds])
|
||||
|
||||
const {
|
||||
isWaiting: isWaitingForOcaAuth,
|
||||
startAuth: startOcaAuth,
|
||||
cancelAuth: cancelOcaAuth,
|
||||
isAuthenticated: isOcaAuthenticated,
|
||||
} = useOcaAuth({
|
||||
controller,
|
||||
onSuccess: handleOcaAuthSuccess,
|
||||
})
|
||||
|
||||
// Read model IDs from state (re-reads when refreshKey changes)
|
||||
const { actModelId, planModelId } = useMemo(() => {
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
@@ -209,11 +225,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
if (!actProvider && !planProvider) {
|
||||
return { actModelId: "", planModelId: "" }
|
||||
}
|
||||
const actKey = actProvider ? getProviderModelIdKey(actProvider as ApiProvider, "act") : null
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
|
||||
const actKey = actProvider ? getProviderModelIdKey(actProvider, "act") : null
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider, "plan") : null
|
||||
return {
|
||||
actModelId: actKey ? (stateManager.getGlobalSettingsKey(actKey as string) as string) || "" : "",
|
||||
planModelId: planKey ? (stateManager.getGlobalSettingsKey(planKey as string) as string) || "" : "",
|
||||
actModelId: actKey ? (stateManager.getGlobalSettingsKey(actKey) as string) || "" : "",
|
||||
planModelId: planKey ? (stateManager.getGlobalSettingsKey(planKey) as string) || "" : "",
|
||||
}
|
||||
}, [modelRefreshKey, stateManager])
|
||||
|
||||
@@ -477,7 +493,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
key: parentKey,
|
||||
label: parentLabel,
|
||||
type: "checkbox",
|
||||
value: actions[parentKey as keyof typeof actions],
|
||||
value: actions[parentKey as keyof typeof actions] ?? false,
|
||||
description: parentDesc,
|
||||
})
|
||||
if (actions[parentKey as keyof typeof actions]) {
|
||||
@@ -721,9 +737,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
const actProvider = apiConfig.actModeApiProvider
|
||||
const planProvider = apiConfig.planModeApiProvider || actProvider
|
||||
if (actProvider) {
|
||||
const actKey = getProviderModelIdKey(actProvider as ApiProvider, "act")
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
|
||||
const actModel = stateManager.getGlobalSettingsKey(actKey as string)
|
||||
const actKey = getProviderModelIdKey(actProvider, "act")
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider, "plan") : null
|
||||
const actModel = stateManager.getGlobalSettingsKey(actKey)
|
||||
if (planKey) stateManager.setGlobalState(planKey, actModel)
|
||||
}
|
||||
}
|
||||
@@ -823,11 +839,11 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
: actProvider || planProvider
|
||||
if (!providerForSelection) return
|
||||
// Use provider-specific model ID keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
const actKey = actProvider ? getProviderModelIdKey(actProvider as ApiProvider, "act") : null
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
|
||||
const actKey = actProvider ? getProviderModelIdKey(actProvider, "act") : null
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider, "plan") : null
|
||||
|
||||
// For cline/openrouter providers, also set model info (like webview does)
|
||||
let modelInfo
|
||||
let modelInfo: ModelInfo | undefined
|
||||
if (providerForSelection === "cline" || providerForSelection === "openrouter") {
|
||||
const openRouterModels = await controller?.readOpenRouterModels()
|
||||
modelInfo = openRouterModels?.[modelId]
|
||||
@@ -914,7 +930,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
}, [controller])
|
||||
|
||||
const handleProviderSelect = useCallback(
|
||||
(providerId: string) => {
|
||||
async (providerId: string) => {
|
||||
// Special handling for Cline - uses OAuth (but skip if already logged in)
|
||||
if (providerId === "cline") {
|
||||
setIsPickingProvider(false)
|
||||
@@ -922,7 +938,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
const authInfo = AuthService.getInstance(controller).getInfo()
|
||||
if (authInfo?.user?.email) {
|
||||
// Already logged in - just set the provider
|
||||
applyProviderConfig({ providerId: "cline", controller })
|
||||
await applyProviderConfig({ providerId: "cline", controller })
|
||||
setProvider("cline")
|
||||
refreshModelIds()
|
||||
} else {
|
||||
@@ -939,6 +955,22 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
// Special handling for OCA - uses OAuth (but skip if already logged in)
|
||||
if (providerId === "oca") {
|
||||
setIsPickingProvider(false)
|
||||
// Check if already logged in
|
||||
if (isOcaAuthenticated) {
|
||||
// Already logged in - just set the provider
|
||||
await applyProviderConfig({ providerId: "oca", controller })
|
||||
setProvider("oca")
|
||||
refreshModelIds()
|
||||
} else {
|
||||
// Not logged in - trigger OAuth
|
||||
startOcaAuth()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Special handling for Bedrock - needs multi-field configuration
|
||||
if (providerId === "bedrock") {
|
||||
setPendingProvider(providerId)
|
||||
@@ -948,7 +980,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
}
|
||||
|
||||
// Check if this provider needs an API key
|
||||
const keyField = ProviderToApiKeyMap[providerId as keyof typeof ProviderToApiKeyMap]
|
||||
const keyField = ProviderToApiKeyMap[providerId as ApiProvider]
|
||||
if (keyField) {
|
||||
// Provider needs an API key - go to API key entry mode
|
||||
// Pre-fill with existing key if configured
|
||||
@@ -961,13 +993,13 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
setIsEnteringApiKey(true)
|
||||
} else {
|
||||
// Provider doesn't need an API key (rare) - just set it
|
||||
applyProviderConfig({ providerId, controller })
|
||||
await applyProviderConfig({ providerId, controller })
|
||||
setProvider(providerId)
|
||||
refreshModelIds()
|
||||
setIsPickingProvider(false)
|
||||
}
|
||||
},
|
||||
[stateManager, startCodexAuth, handleClineLogin, controller, refreshModelIds],
|
||||
[stateManager, startCodexAuth, handleClineLogin, startOcaAuth, isOcaAuthenticated, controller, refreshModelIds],
|
||||
)
|
||||
|
||||
// Handle API key submission after provider selection
|
||||
@@ -990,47 +1022,16 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
// Handle Bedrock configuration complete
|
||||
const handleBedrockComplete = useCallback(
|
||||
(bedrockConfig: BedrockConfig) => {
|
||||
const config: Record<string, unknown> = {
|
||||
actModeApiProvider: "bedrock",
|
||||
planModeApiProvider: "bedrock",
|
||||
apiProvider: "bedrock",
|
||||
awsAuthentication: bedrockConfig.awsAuthentication,
|
||||
awsRegion: bedrockConfig.awsRegion,
|
||||
awsUseCrossRegionInference: bedrockConfig.awsUseCrossRegionInference,
|
||||
}
|
||||
|
||||
const defaultModelId = getDefaultModelId("bedrock")
|
||||
if (defaultModelId) {
|
||||
// Use provider-specific model ID keys
|
||||
const actModelKey = getProviderModelIdKey("bedrock" as ApiProvider, "act")
|
||||
const planModelKey = getProviderModelIdKey("bedrock" as ApiProvider, "plan")
|
||||
if (actModelKey) config[actModelKey] = defaultModelId
|
||||
if (planModelKey) config[planModelKey] = defaultModelId
|
||||
}
|
||||
|
||||
if (bedrockConfig.awsProfile !== undefined) config.awsProfile = bedrockConfig.awsProfile
|
||||
if (bedrockConfig.awsAccessKey) config.awsAccessKey = bedrockConfig.awsAccessKey
|
||||
if (bedrockConfig.awsSecretKey) config.awsSecretKey = bedrockConfig.awsSecretKey
|
||||
if (bedrockConfig.awsSessionToken) config.awsSessionToken = bedrockConfig.awsSessionToken
|
||||
|
||||
stateManager.setApiConfiguration(config as Record<string, string>)
|
||||
|
||||
// Close Bedrock config first, then flush state async
|
||||
// Update UI state first for responsiveness
|
||||
setProvider("bedrock")
|
||||
refreshModelIds()
|
||||
setIsConfiguringBedrock(false)
|
||||
setPendingProvider(null)
|
||||
|
||||
// Flush state and rebuild API handler in background
|
||||
stateManager.flushPendingState().then(() => {
|
||||
if (controller?.task) {
|
||||
const currentMode = stateManager.getGlobalSettingsKey("mode")
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
|
||||
}
|
||||
})
|
||||
// Apply config and rebuild API handler in background
|
||||
applyBedrockConfig({ bedrockConfig, controller })
|
||||
},
|
||||
[stateManager, controller],
|
||||
[controller, refreshModelIds],
|
||||
)
|
||||
|
||||
// Handle saving edited value
|
||||
@@ -1046,8 +1047,8 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
const actProvider = apiConfig.actModeApiProvider
|
||||
const planProvider = apiConfig.planModeApiProvider || actProvider
|
||||
if (!actProvider && !planProvider) break
|
||||
const actKey = actProvider ? getProviderModelIdKey(actProvider as ApiProvider, "act") : null
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider as ApiProvider, "plan") : null
|
||||
const actKey = actProvider ? getProviderModelIdKey(actProvider, "act") : null
|
||||
const planKey = planProvider ? getProviderModelIdKey(planProvider, "plan") : null
|
||||
|
||||
if (separateModels) {
|
||||
// Only update the selected mode's model
|
||||
@@ -1204,6 +1205,14 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
// OCA OAuth waiting mode - escape to cancel
|
||||
if (isWaitingForOcaAuth) {
|
||||
if (key.escape) {
|
||||
cancelOcaAuth()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
if (key.escape) {
|
||||
setIsEditing(false)
|
||||
@@ -1424,6 +1433,25 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (isWaitingForOcaAuth) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Text color={COLORS.primaryBlue}>
|
||||
<Spinner type="dots" />
|
||||
</Text>
|
||||
<Text color="white"> Waiting for OCA sign-in...</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray">Complete sign-in in your browser.</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray">Esc to cancel</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Account tab - loading state
|
||||
if (currentTab === "account" && isAccountLoading) {
|
||||
return (
|
||||
@@ -1559,8 +1587,23 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
// Determine if we're in a subpage (picker, editor, or waiting state)
|
||||
const isSubpage =
|
||||
isPickingProvider ||
|
||||
isPickingModel ||
|
||||
isPickingFeaturedModel ||
|
||||
isPickingLanguage ||
|
||||
isEnteringApiKey ||
|
||||
isConfiguringBedrock ||
|
||||
isWaitingForCodexAuth ||
|
||||
!!codexAuthError ||
|
||||
isPickingOrganization ||
|
||||
isWaitingForClineAuth ||
|
||||
isWaitingForOcaAuth ||
|
||||
isEditing
|
||||
|
||||
return (
|
||||
<Panel currentTab={currentTab} label="Settings" tabs={TABS}>
|
||||
<Panel currentTab={currentTab} isSubpage={isSubpage} label="Settings" tabs={TABS}>
|
||||
{renderContent()}
|
||||
</Panel>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Box, Text, useInput } from "ink"
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { ApiProvider } from "@/shared/api"
|
||||
import { getProviderDefaultModelId, getProviderModelIdKey, Mode } from "@/shared/storage"
|
||||
import { getProviderDefaultModelId, getProviderModelIdKey, Mode, SettingsKey } from "@/shared/storage"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import {
|
||||
checkAndWarnRipgrepMissing,
|
||||
@@ -79,7 +79,10 @@ export const WelcomeView: React.FC<WelcomeViewProps> = ({ onSubmit, onExit, cont
|
||||
const modelId = useMemo(() => {
|
||||
const stateManager = StateManager.get()
|
||||
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
|
||||
return (stateManager.getGlobalSettingsKey(modelKey as string) as string) || getProviderDefaultModelId(provider)
|
||||
return (
|
||||
(stateManager.getGlobalSettingsKey(modelKey as SettingsKey) as string) ||
|
||||
getProviderDefaultModelId(provider as ApiProvider)
|
||||
)
|
||||
}, [mode, provider])
|
||||
|
||||
const toggleMode = useCallback(() => {
|
||||
|
||||
@@ -7,48 +7,54 @@ export interface FeaturedModel {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
label: string
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
export const FEATURED_MODELS = {
|
||||
recommended: [
|
||||
{
|
||||
id: "anthropic/claude-opus-4.5",
|
||||
name: "Claude Opus 4.5",
|
||||
id: "anthropic/claude-opus-4.6",
|
||||
name: "Claude Opus 4.6",
|
||||
description: "State-of-the-art for complex coding",
|
||||
label: "Best",
|
||||
labels: ["BEST"],
|
||||
},
|
||||
{
|
||||
id: "openai/gpt-5.2-codex",
|
||||
name: "GPT 5.2 Codex",
|
||||
description: "OpenAI's latest with strong coding abilities",
|
||||
label: "New",
|
||||
labels: ["NEW"],
|
||||
},
|
||||
{
|
||||
id: "google/gemini-3-pro-preview",
|
||||
name: "Gemini 3 Pro",
|
||||
description: "1M context window for large codebases",
|
||||
label: "Trending",
|
||||
labels: ["TRENDING"],
|
||||
},
|
||||
] as FeaturedModel[],
|
||||
free: [
|
||||
{
|
||||
id: "minimax/minimax-m2.1",
|
||||
name: "MiniMax M2.1",
|
||||
description: "Exceptional Multi-Programming Language Capabilities",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "moonshotai/kimi-k2.5",
|
||||
name: "Kimi K2.5",
|
||||
description: "State-of-the-art model topping benchmarks",
|
||||
label: "FREE",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "kwaipilot/kat-coder-pro",
|
||||
name: "KAT Coder Pro",
|
||||
description: "Advanced agentic coding model",
|
||||
label: "FREE",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "arcee-ai/trinity-large-preview:free",
|
||||
name: "Trinity Large Preview",
|
||||
description: "US built open source coding model",
|
||||
label: "FREE",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
] as FeaturedModel[],
|
||||
}
|
||||
|
||||
@@ -142,6 +142,17 @@ export class CliEnvServiceClient implements EnvServiceClientInterface {
|
||||
printInfo("Shutting down...")
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
|
||||
async openExternal(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
|
||||
const url = request.value || ""
|
||||
if (url) {
|
||||
printInfo(`🌐 Opening: ${url}`)
|
||||
// Dynamically import 'open' to open URL in default browser
|
||||
const { default: open } = await import("open")
|
||||
await open(url)
|
||||
}
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Hook for OCA OAuth authentication flow in the CLI.
|
||||
* Handles starting auth, subscribing to status updates, and notifying on success.
|
||||
*/
|
||||
|
||||
import type { OcaAuthState } from "@shared/proto/cline/oca_account"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import type { Controller } from "@/core/controller"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
|
||||
interface UseOcaAuthOptions {
|
||||
controller: Controller | undefined
|
||||
/** If provided, controls when subscription is active (for external state management like AuthView's step) */
|
||||
enabled?: boolean
|
||||
onSuccess?: () => void | Promise<void>
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
interface UseOcaAuthResult {
|
||||
/** Whether we're waiting for auth to complete (only relevant when not using `enabled` prop) */
|
||||
isWaiting: boolean
|
||||
/** Start the OAuth flow - opens browser */
|
||||
startAuth: () => void
|
||||
/** Cancel waiting for auth */
|
||||
cancelAuth: () => void
|
||||
/** The authenticated user, if any */
|
||||
user: OcaAuthState["user"] | null
|
||||
/** Whether user is currently authenticated */
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
|
||||
export function useOcaAuth({ controller, enabled, onSuccess, onError }: UseOcaAuthOptions): UseOcaAuthResult {
|
||||
const [isWaiting, setIsWaiting] = useState(false)
|
||||
const [user, setUser] = useState<OcaAuthState["user"] | null>(null)
|
||||
const onSuccessRef = useRef(onSuccess)
|
||||
onSuccessRef.current = onSuccess
|
||||
|
||||
// Determine if subscription should be active
|
||||
// If `enabled` is provided, use it; otherwise use internal `isWaiting` state
|
||||
const isSubscriptionActive = enabled !== undefined ? enabled : isWaiting
|
||||
|
||||
const startAuth = useCallback(() => {
|
||||
if (!controller) {
|
||||
return
|
||||
}
|
||||
setIsWaiting(true)
|
||||
OcaAuthService.initialize(controller)
|
||||
OcaAuthService.getInstance()
|
||||
.createAuthRequest()
|
||||
.catch((error) => {
|
||||
setIsWaiting(false)
|
||||
onError?.(error instanceof Error ? error : new Error(String(error)))
|
||||
})
|
||||
}, [controller, onError])
|
||||
|
||||
const cancelAuth = useCallback(() => {
|
||||
setIsWaiting(false)
|
||||
}, [])
|
||||
|
||||
// Check if already authenticated
|
||||
const isAuthenticated = !!user?.uid
|
||||
|
||||
// Subscribe to auth status updates when active
|
||||
useEffect(() => {
|
||||
if (!isSubscriptionActive || !controller) {
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const responseHandler = async (authState: OcaAuthState) => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
if (authState.user?.uid) {
|
||||
setUser(authState.user)
|
||||
setIsWaiting(false)
|
||||
await onSuccessRef.current?.()
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure OcaAuthService is initialized before subscribing
|
||||
OcaAuthService.initialize(controller)
|
||||
OcaAuthService.getInstance().subscribeToAuthStatusUpdate({}, responseHandler, `cli-oca-auth-${Date.now()}`)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [isSubscriptionActive, controller])
|
||||
|
||||
return { isWaiting, startAuth, cancelAuth, user, isAuthenticated }
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export interface UseTextInputReturn {
|
||||
cursorPos: number
|
||||
|
||||
// Text manipulation
|
||||
setText: (text: string) => void
|
||||
setText: (text: string | ((prev: string) => string)) => void
|
||||
insertText: (text: string) => void
|
||||
setCursorPos: (pos: number | ((prev: number) => number)) => void
|
||||
|
||||
@@ -102,9 +102,15 @@ export function useTextInput(): UseTextInputReturn {
|
||||
cursorRef.current = cursorPos
|
||||
|
||||
// Text manipulation
|
||||
const setText = useCallback((newText: string) => {
|
||||
setTextState(newText)
|
||||
setCursorPosState(newText.length)
|
||||
const setText = useCallback((newText: string | ((prev: string) => string)) => {
|
||||
setTextState((prev) => {
|
||||
const resolved = typeof newText === "function" ? newText(prev) : newText
|
||||
// Only update cursor to end if setting a direct value (not functional update)
|
||||
if (typeof newText !== "function") {
|
||||
setCursorPosState(resolved.length)
|
||||
}
|
||||
return resolved
|
||||
})
|
||||
}, [])
|
||||
|
||||
const insertText = useCallback((insertedText: string) => {
|
||||
|
||||
+261
-122
@@ -19,6 +19,7 @@ import { BannerService } from "@/services/banner/BannerService"
|
||||
import { ErrorService } from "@/services/error/ErrorService"
|
||||
import { initializeDistinctId } from "@/services/logging/distinctId"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { HistoryItem } from "@/shared/HistoryItem"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Session } from "@/shared/services/Session"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage"
|
||||
@@ -36,11 +37,166 @@ import { CLINE_CLI_DIR, getCliBinaryPath } from "./utils/path"
|
||||
import { readStdinIfPiped } from "./utils/piped"
|
||||
import { runPlainTextTask } from "./utils/plain-text-task"
|
||||
import { applyProviderConfig } from "./utils/provider-config"
|
||||
import { selectOutputMode } from "./utils/mode-selection"
|
||||
import { getValidCliProviders, isValidCliProvider } from "./utils/providers"
|
||||
import { autoUpdateOnStartup, checkForUpdates } from "./utils/update"
|
||||
import { initializeCliContext } from "./vscode-context"
|
||||
import { CLI_LOG_FILE, shutdownEvent, window } from "./vscode-shim"
|
||||
|
||||
/**
|
||||
* Common options shared between runTask and resumeTask
|
||||
*/
|
||||
interface TaskOptions {
|
||||
act?: boolean
|
||||
plan?: boolean
|
||||
model?: string
|
||||
verbose?: boolean
|
||||
cwd?: string
|
||||
config?: string
|
||||
thinking?: boolean
|
||||
yolo?: boolean
|
||||
timeout?: string
|
||||
json?: boolean
|
||||
stdinWasPiped?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply task-related options (mode, model, thinking, yolo) to StateManager.
|
||||
* Shared between runTask and resumeTask to avoid duplication.
|
||||
*/
|
||||
function applyTaskOptions(options: TaskOptions): void {
|
||||
// Apply mode flag
|
||||
if (options.plan) {
|
||||
StateManager.get().setGlobalState("mode", "plan")
|
||||
telemetryService.captureHostEvent("mode_flag", "plan")
|
||||
} else if (options.act) {
|
||||
StateManager.get().setGlobalState("mode", "act")
|
||||
telemetryService.captureHostEvent("mode_flag", "act")
|
||||
}
|
||||
|
||||
// Apply model override if specified
|
||||
if (options.model) {
|
||||
const selectedMode = (StateManager.get().getGlobalSettingsKey("mode") || "act") as "act" | "plan"
|
||||
const providerKey = selectedMode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = StateManager.get().getGlobalSettingsKey(providerKey) as ApiProvider
|
||||
const modelKey = getProviderModelIdKey(currentProvider, selectedMode)
|
||||
if (modelKey) {
|
||||
StateManager.get().setGlobalState(modelKey, options.model)
|
||||
}
|
||||
telemetryService.captureHostEvent("model_flag", options.model)
|
||||
}
|
||||
|
||||
// Set thinking budget based on --thinking flag
|
||||
const thinkingBudget = options.thinking ? 1024 : 0
|
||||
const currentMode = StateManager.get().getGlobalSettingsKey("mode") || "act"
|
||||
const thinkingKey = currentMode === "act" ? "actModeThinkingBudgetTokens" : "planModeThinkingBudgetTokens"
|
||||
StateManager.get().setGlobalState(thinkingKey, thinkingBudget)
|
||||
if (options.thinking) {
|
||||
telemetryService.captureHostEvent("thinking_flag", "true")
|
||||
}
|
||||
|
||||
// Set yolo mode based on --yolo flag
|
||||
if (options.yolo) {
|
||||
StateManager.get().setGlobalState("yoloModeToggled", true)
|
||||
telemetryService.captureHostEvent("yolo_flag", "true")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mode selection result using the extracted, testable selectOutputMode function.
|
||||
* This wrapper provides the current process TTY state.
|
||||
*/
|
||||
function getModeSelection(options: TaskOptions) {
|
||||
return selectOutputMode({
|
||||
stdoutIsTTY: process.stdout.isTTY === true,
|
||||
stdinIsTTY: process.stdin.isTTY === true,
|
||||
stdinWasPiped: options.stdinWasPiped ?? false,
|
||||
json: options.json,
|
||||
yolo: options.yolo,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if plain text mode should be used based on options and environment.
|
||||
*/
|
||||
function shouldUsePlainTextMode(options: TaskOptions): boolean {
|
||||
return getModeSelection(options).usePlainTextMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reason for using plain text mode (for telemetry).
|
||||
*/
|
||||
function getPlainTextModeReason(options: TaskOptions): string {
|
||||
return getModeSelection(options).reason
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task in plain text mode (no Ink UI).
|
||||
* Handles auth check, task execution, cleanup, and exit.
|
||||
*/
|
||||
async function runTaskInPlainTextMode(
|
||||
ctx: CliContext,
|
||||
options: TaskOptions,
|
||||
taskConfig: {
|
||||
prompt?: string
|
||||
taskId?: string
|
||||
imageDataUrls?: string[]
|
||||
},
|
||||
): Promise<never> {
|
||||
// Set flag so shutdown handler knows not to clear Ink UI lines
|
||||
isPlainTextMode = true
|
||||
|
||||
// Check if auth is configured before attempting to run the task
|
||||
// In plain text mode we can't show the interactive auth flow
|
||||
const hasAuth = await isAuthConfigured()
|
||||
if (!hasAuth) {
|
||||
printWarning("Not authenticated. Please run 'cline auth' first to configure your API credentials.")
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
exit(1)
|
||||
}
|
||||
|
||||
const reason = getPlainTextModeReason(options)
|
||||
telemetryService.captureHostEvent("plain_text_mode", reason)
|
||||
|
||||
// Plain text mode: no Ink rendering, just clean text output
|
||||
const success = await runPlainTextTask({
|
||||
controller: ctx.controller,
|
||||
prompt: taskConfig.prompt,
|
||||
taskId: taskConfig.taskId,
|
||||
imageDataUrls: taskConfig.imageDataUrls,
|
||||
verbose: options.verbose,
|
||||
jsonOutput: options.json,
|
||||
timeoutSeconds: options.timeout ? parseInt(options.timeout, 10) : undefined,
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
|
||||
// Ensure stdout is fully drained before exiting - critical for piping
|
||||
await drainStdout()
|
||||
exit(success ? 0 : 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the standard cleanup function for Ink apps.
|
||||
*/
|
||||
function createInkCleanup(ctx: CliContext, onTaskError?: () => boolean): () => Promise<void> {
|
||||
return async () => {
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
if (onTaskError?.()) {
|
||||
printWarning("Task ended with errors.")
|
||||
exit(1)
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Track active context for graceful shutdown
|
||||
let activeContext: CliContext | null = null
|
||||
let isShuttingDown = false
|
||||
@@ -228,24 +384,7 @@ async function runInkApp(element: React.ReactElement, cleanup: () => Promise<voi
|
||||
/**
|
||||
* Run a task with the given prompt - uses welcome view for consistent behavior
|
||||
*/
|
||||
async function runTask(
|
||||
prompt: string,
|
||||
options: {
|
||||
act?: boolean
|
||||
plan?: boolean
|
||||
model?: string
|
||||
verbose?: boolean
|
||||
cwd?: string
|
||||
config?: string
|
||||
thinking?: boolean
|
||||
yolo?: boolean
|
||||
timeout?: string
|
||||
images?: string[]
|
||||
json?: boolean
|
||||
stdinWasPiped?: boolean
|
||||
},
|
||||
existingContext?: CliContext,
|
||||
) {
|
||||
async function runTask(prompt: string, options: TaskOptions & { images?: string[] }, existingContext?: CliContext) {
|
||||
const ctx = existingContext || (await initializeCli({ ...options, enableAuth: true }))
|
||||
|
||||
// Parse images from the prompt text (e.g., @/path/to/image.png)
|
||||
@@ -262,101 +401,23 @@ async function runTask(
|
||||
// Task without prompt starts in interactive mode
|
||||
telemetryService.captureHostEvent("task_command", prompt ? "task" : "interactive")
|
||||
|
||||
if (options.plan) {
|
||||
StateManager.get().setGlobalState("mode", "plan")
|
||||
telemetryService.captureHostEvent("mode_flag", "plan")
|
||||
} else if (options.act) {
|
||||
StateManager.get().setGlobalState("mode", "act")
|
||||
telemetryService.captureHostEvent("mode_flag", "act")
|
||||
}
|
||||
|
||||
if (options.model) {
|
||||
const selectedMode = (StateManager.get().getGlobalSettingsKey("mode") || "act") as "act" | "plan"
|
||||
|
||||
// Get the current provider for the selected mode
|
||||
const providerKey = selectedMode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = StateManager.get().getGlobalSettingsKey(providerKey) as ApiProvider
|
||||
|
||||
// Update model ID using provider-specific key (e.g., cline uses actModeOpenRouterModelId)
|
||||
const modelKey = getProviderModelIdKey(currentProvider, selectedMode)
|
||||
if (modelKey) {
|
||||
StateManager.get().setGlobalState(modelKey, options.model)
|
||||
}
|
||||
telemetryService.captureHostEvent("model_flag", options.model)
|
||||
}
|
||||
|
||||
// Set thinking budget based on --thinking flag
|
||||
const thinkingBudget = options.thinking ? 1024 : 0
|
||||
const currentMode = StateManager.get().getGlobalSettingsKey("mode") || "act"
|
||||
const thinkingKey = currentMode === "act" ? "actModeThinkingBudgetTokens" : "planModeThinkingBudgetTokens"
|
||||
StateManager.get().setGlobalState(thinkingKey, thinkingBudget)
|
||||
if (options.thinking) {
|
||||
telemetryService.captureHostEvent("thinking_flag", "true")
|
||||
}
|
||||
|
||||
// Set yolo mode based on --yolo flag
|
||||
if (options.yolo) {
|
||||
StateManager.get().setGlobalState("yoloModeToggled", true)
|
||||
telemetryService.captureHostEvent("yolo_flag", "true")
|
||||
}
|
||||
|
||||
// Apply shared task options (mode, model, thinking, yolo)
|
||||
applyTaskOptions(options)
|
||||
await StateManager.get().flushPendingState()
|
||||
|
||||
// Detect if output is a TTY (interactive terminal) or redirected to a file/pipe
|
||||
const isTTY = process.stdout.isTTY === true
|
||||
|
||||
// Use plain text mode when output is redirected, stdin was piped, JSON mode is enabled, or --yolo flag is used
|
||||
// Ink requires raw mode on stdin which isn't available when stdin is piped
|
||||
// Note: we use the stdinWasPiped flag passed from the caller because process.stdin.isTTY
|
||||
// may not be reliable after stdin has been consumed by readStdinIfPiped()
|
||||
if (!isTTY || options.stdinWasPiped || options.json || options.yolo) {
|
||||
// Set flag so shutdown handler knows not to clear Ink UI lines
|
||||
isPlainTextMode = true
|
||||
|
||||
// Check if auth is configured before attempting to run the task
|
||||
// In plain text mode we can't show the interactive auth flow
|
||||
const hasAuth = await isAuthConfigured()
|
||||
if (!hasAuth) {
|
||||
printWarning("Not authenticated. Please run 'cline auth' first to configure your API credentials.")
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
exit(1)
|
||||
}
|
||||
|
||||
const reason = options.yolo
|
||||
? "yolo_flag"
|
||||
: options.json
|
||||
? "json"
|
||||
: options.stdinWasPiped
|
||||
? "piped_stdin"
|
||||
: "redirected_output"
|
||||
telemetryService.captureHostEvent("plain_text_mode", reason)
|
||||
// Plain text mode: no Ink rendering, just clean text output
|
||||
const success = await runPlainTextTask({
|
||||
controller: ctx.controller,
|
||||
if (shouldUsePlainTextMode(options)) {
|
||||
return runTaskInPlainTextMode(ctx, options, {
|
||||
prompt: taskPrompt,
|
||||
imageDataUrls: imageDataUrls.length > 0 ? imageDataUrls : undefined,
|
||||
verbose: options.verbose,
|
||||
jsonOutput: options.json,
|
||||
timeoutSeconds: options.timeout ? parseInt(options.timeout, 10) : undefined,
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
|
||||
// Ensure stdout is fully drained before exiting - critical for piping
|
||||
await drainStdout()
|
||||
exit(success ? 0 : 1)
|
||||
}
|
||||
|
||||
let taskError = false
|
||||
|
||||
// Render the welcome view with optional initial prompt/images
|
||||
// Interactive mode: Render the welcome view with optional initial prompt/images
|
||||
// If prompt provided (cline task "prompt"), ChatView will auto-submit
|
||||
// If no prompt (cline interactive), user will type it in
|
||||
let taskError = false
|
||||
|
||||
await runInkApp(
|
||||
React.createElement(App, {
|
||||
view: "welcome",
|
||||
@@ -373,16 +434,7 @@ async function runTask(
|
||||
exit(0)
|
||||
},
|
||||
}),
|
||||
async () => {
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
if (taskError) {
|
||||
printWarning("Task ended with errors.")
|
||||
exit(1)
|
||||
}
|
||||
exit(0)
|
||||
},
|
||||
createInkCleanup(ctx, () => taskError),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -438,9 +490,6 @@ async function showConfig(options: { config?: string }) {
|
||||
// Dynamically import the wrapper to avoid circular dependencies
|
||||
const { ConfigViewWrapper } = await import("./components/ConfigViewWrapper")
|
||||
|
||||
// Check feature flags
|
||||
const skillsEnabled = stateManager.getGlobalSettingsKey("skillsEnabled") ?? false
|
||||
|
||||
telemetryService.captureHostEvent("config_command", "executed")
|
||||
|
||||
await runInkApp(
|
||||
@@ -450,7 +499,7 @@ async function showConfig(options: { config?: string }) {
|
||||
globalState: stateManager.getAllGlobalStateEntries(),
|
||||
workspaceState: stateManager.getAllWorkspaceStateEntries(),
|
||||
hooksEnabled: true,
|
||||
skillsEnabled,
|
||||
skillsEnabled: true,
|
||||
isRawModeSupported: checkRawModeSupport(),
|
||||
}),
|
||||
async () => {
|
||||
@@ -599,7 +648,13 @@ program
|
||||
.option("--config <path>", "Path to Cline configuration directory")
|
||||
.option("--thinking", "Enable extended thinking (1024 token budget)")
|
||||
.option("--json", "Output messages as JSON instead of styled text")
|
||||
.action((prompt, options) => runTask(prompt, options))
|
||||
.option("-T, --taskId <id>", "Resume an existing task by ID")
|
||||
.action((prompt, options) => {
|
||||
if (options.taskId) {
|
||||
return resumeTask(options.taskId, { ...options, initialPrompt: prompt })
|
||||
}
|
||||
return runTask(prompt, options)
|
||||
})
|
||||
|
||||
program
|
||||
.command("history")
|
||||
@@ -686,7 +741,7 @@ async function checkAnyProviderConfigured(): Promise<boolean> {
|
||||
const config = stateManager.getApiConfiguration() as Record<string, unknown>
|
||||
|
||||
// Check Cline account (stored as "cline:clineAccountId" in secrets, loaded into config)
|
||||
if (config["cline:clineAccountId"]) return true
|
||||
if (config["clineApiKey"] || config["cline:clineAccountId"]) return true
|
||||
|
||||
// Check OpenAI Codex OAuth (stored in SECRETS_KEYS, loaded into config)
|
||||
if (config["openai-codex-oauth-credentials"]) return true
|
||||
@@ -712,6 +767,69 @@ async function checkAnyProviderConfigured(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a task exists in history
|
||||
* @returns The task history item if found, null otherwise
|
||||
*/
|
||||
function findTaskInHistory(taskId: string): HistoryItem | null {
|
||||
const taskHistory = StateManager.get().getGlobalStateKey("taskHistory") || []
|
||||
return taskHistory.find((item) => item.id === taskId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume an existing task by ID
|
||||
* Loads the task and optionally prefills the input with a prompt
|
||||
*/
|
||||
async function resumeTask(taskId: string, options: TaskOptions & { initialPrompt?: string }) {
|
||||
const ctx = await initializeCli({ ...options, enableAuth: true })
|
||||
|
||||
// Validate task exists
|
||||
const historyItem = findTaskInHistory(taskId)
|
||||
if (!historyItem) {
|
||||
printWarning(`Task not found: ${taskId}`)
|
||||
printInfo("Use 'cline history' to see available tasks.")
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
exit(1)
|
||||
}
|
||||
|
||||
telemetryService.captureHostEvent("resume_task_command", options.initialPrompt ? "with_prompt" : "interactive")
|
||||
|
||||
// Apply shared task options (mode, model, thinking, yolo)
|
||||
applyTaskOptions(options)
|
||||
await StateManager.get().flushPendingState()
|
||||
|
||||
// Use plain text mode for non-interactive scenarios
|
||||
if (shouldUsePlainTextMode(options)) {
|
||||
return runTaskInPlainTextMode(ctx, options, {
|
||||
prompt: options.initialPrompt,
|
||||
taskId: taskId,
|
||||
})
|
||||
}
|
||||
|
||||
// Interactive mode: render the task view with the existing task
|
||||
let taskError = false
|
||||
|
||||
await runInkApp(
|
||||
React.createElement(App, {
|
||||
view: "task",
|
||||
taskId: taskId,
|
||||
verbose: options.verbose,
|
||||
controller: ctx.controller,
|
||||
isRawModeSupported: checkRawModeSupport(),
|
||||
initialPrompt: options.initialPrompt || undefined,
|
||||
onError: () => {
|
||||
taskError = true
|
||||
},
|
||||
onWelcomeExit: () => {
|
||||
exit(0)
|
||||
},
|
||||
}),
|
||||
createInkCleanup(ctx, () => taskError),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show welcome prompt and wait for user input
|
||||
* If auth is not configured, show auth flow first
|
||||
@@ -761,6 +879,7 @@ program
|
||||
.option("--thinking", "Enable extended thinking (1024 token budget)")
|
||||
.option("--json", "Output messages as JSON instead of styled text")
|
||||
.option("--acp", "Run in ACP (Agent Client Protocol) mode for editor integration")
|
||||
.option("-T, --taskId <id>", "Resume an existing task by ID")
|
||||
.action(async (prompt, options) => {
|
||||
// Check for ACP mode first - this takes precedence over everything else
|
||||
if (options.acp) {
|
||||
@@ -775,8 +894,18 @@ program
|
||||
// Always check for piped stdin content
|
||||
const stdinInput = await readStdinIfPiped()
|
||||
|
||||
// Error if stdin was piped but empty (e.g., `echo "" | cline`)
|
||||
if (stdinInput === "") {
|
||||
// Track whether stdin was actually piped (even if empty) vs not piped (null)
|
||||
// stdinInput === null means stdin wasn't piped (TTY or not FIFO/file)
|
||||
// stdinInput === "" means stdin was piped but empty
|
||||
// stdinInput has content means stdin was piped with data
|
||||
const stdinWasPiped = stdinInput !== null
|
||||
|
||||
// Error if stdin was piped but empty AND no prompt was provided
|
||||
// This handles:
|
||||
// - `echo "" | cline` -> error (empty stdin, no prompt)
|
||||
// - `cline "prompt"` in GitHub Actions -> OK (empty stdin ignored, has prompt)
|
||||
// - `cat file | cline "explain"` -> OK (has stdin AND prompt)
|
||||
if (stdinInput === "" && !prompt) {
|
||||
printWarning("Empty input received from stdin. Please provide content to process.")
|
||||
exit(1)
|
||||
}
|
||||
@@ -799,9 +928,19 @@ program
|
||||
}
|
||||
}
|
||||
|
||||
// Handle --taskId flag to resume an existing task
|
||||
if (options.taskId) {
|
||||
await resumeTask(options.taskId, {
|
||||
...options,
|
||||
initialPrompt: effectivePrompt,
|
||||
stdinWasPiped,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (effectivePrompt) {
|
||||
// Pass stdinWasPiped flag so runTask knows to use plain text mode
|
||||
await runTask(effectivePrompt, { ...options, stdinWasPiped: !!stdinInput })
|
||||
await runTask(effectivePrompt, { ...options, stdinWasPiped })
|
||||
} else {
|
||||
// Show welcome prompt if no prompt given
|
||||
await showWelcome(options)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* Uses dynamic import of the 'open' package to open URLs.
|
||||
*
|
||||
* @param url - The URL to open in the browser
|
||||
*/
|
||||
export async function openUrlInBrowser(url: string): Promise<void> {
|
||||
const { default: open } = await import("open")
|
||||
await open(url)
|
||||
}
|
||||
@@ -13,6 +13,6 @@ import { Fzf } from "fzf"
|
||||
*/
|
||||
export function fuzzyFilter<T>(items: readonly T[], query: string, selector: (item: T) => string): T[] {
|
||||
if (!query) return [...items]
|
||||
const fzf = new Fzf(items, { selector })
|
||||
return fzf.find(query).map((result) => result.item)
|
||||
const fzf = new Fzf(items as any, { selector } as any)
|
||||
return fzf.find(query).map((result) => result.item) as T[]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { selectOutputMode } from "./mode-selection"
|
||||
|
||||
describe("selectOutputMode", () => {
|
||||
describe("interactive mode (Ink)", () => {
|
||||
it("should use interactive mode when both stdin and stdout are TTY", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(false)
|
||||
expect(result.reason).toBe("interactive")
|
||||
})
|
||||
})
|
||||
|
||||
describe("yolo flag", () => {
|
||||
it("should use plain text mode when --yolo flag is set", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
yolo: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("yolo_flag")
|
||||
})
|
||||
|
||||
it("should prioritize yolo over other flags", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: true,
|
||||
json: true,
|
||||
yolo: true,
|
||||
})
|
||||
expect(result.reason).toBe("yolo_flag")
|
||||
})
|
||||
})
|
||||
|
||||
describe("json flag", () => {
|
||||
it("should use plain text mode when --json flag is set", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
json: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("json")
|
||||
})
|
||||
})
|
||||
|
||||
describe("piped stdin", () => {
|
||||
it("should use plain text mode when stdin was piped (echo x | cline)", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false, // piped stdin is not a TTY
|
||||
stdinWasPiped: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("piped_stdin")
|
||||
})
|
||||
|
||||
it("should use plain text mode when stdin was piped but empty (echo '' | cline 'prompt')", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: true, // empty pipe still counts as piped
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("piped_stdin")
|
||||
})
|
||||
})
|
||||
|
||||
describe("stdin redirected (< /dev/null)", () => {
|
||||
it("should use plain text mode when stdin is redirected from /dev/null", () => {
|
||||
// cline "prompt" < /dev/null
|
||||
// stdin is not a TTY, but also not a FIFO/file, so stdinWasPiped=false
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false, // redirected, not a TTY
|
||||
stdinWasPiped: false, // /dev/null is a character device, not FIFO
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("stdin_redirected")
|
||||
})
|
||||
})
|
||||
|
||||
describe("stdout redirected", () => {
|
||||
it("should use plain text mode when stdout is redirected to file", () => {
|
||||
// cline "prompt" > output.txt
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("stdout_redirected")
|
||||
})
|
||||
|
||||
it("should use plain text mode when stdout is piped", () => {
|
||||
// cline "prompt" | grep something
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("stdout_redirected")
|
||||
})
|
||||
})
|
||||
|
||||
describe("GitHub Actions scenarios", () => {
|
||||
it("should use plain text mode in GitHub Actions (stdin is empty FIFO)", () => {
|
||||
// In GitHub Actions: stdin is an empty FIFO pipe
|
||||
// stdinIsTTY=false, stdinWasPiped=true (FIFO detected)
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true, // GitHub Actions stdout is TTY-like
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: true, // empty FIFO still counts as piped
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
|
||||
it("should use plain text mode with --yolo in CI", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: false,
|
||||
yolo: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("yolo_flag")
|
||||
})
|
||||
})
|
||||
|
||||
describe("real-world scenarios", () => {
|
||||
it("cline (no args, interactive terminal)", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(false)
|
||||
})
|
||||
|
||||
it('cline "prompt" (prompt arg, interactive terminal)', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(false)
|
||||
})
|
||||
|
||||
it('cat file | cline "explain"', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
|
||||
it('cline --yolo "prompt"', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
yolo: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
|
||||
it('cline "prompt" < /dev/null', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
|
||||
it('cline "prompt" > output.log', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Mode selection logic for CLI - determines whether to use Ink (interactive) or plain text mode
|
||||
*
|
||||
* This is extracted as a pure function for testability. The decision tree:
|
||||
* - Plain text mode when output is redirected (stdout not TTY)
|
||||
* - Plain text mode when input is redirected (stdin not TTY) - Ink requires raw mode
|
||||
* - Plain text mode when stdin was piped (e.g., echo "x" | cline)
|
||||
* - Plain text mode when --json flag is used
|
||||
* - Plain text mode when --yolo flag is used
|
||||
* - Otherwise: Interactive Ink mode
|
||||
*/
|
||||
|
||||
export interface ModeSelectionInput {
|
||||
/** Is stdout connected to a TTY (interactive terminal)? */
|
||||
stdoutIsTTY: boolean
|
||||
/** Is stdin connected to a TTY (interactive terminal)? */
|
||||
stdinIsTTY: boolean
|
||||
/** Was stdin piped (FIFO or file), even if empty? */
|
||||
stdinWasPiped: boolean
|
||||
/** --json flag for machine-readable output */
|
||||
json?: boolean
|
||||
/** --yolo flag for auto-approve mode */
|
||||
yolo?: boolean
|
||||
}
|
||||
|
||||
export interface ModeSelectionResult {
|
||||
/** Use plain text mode instead of Ink */
|
||||
usePlainTextMode: boolean
|
||||
/** Reason for the mode selection (for telemetry/debugging) */
|
||||
reason: "interactive" | "yolo_flag" | "json" | "piped_stdin" | "stdin_redirected" | "stdout_redirected"
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether to use plain text mode or interactive Ink mode
|
||||
*
|
||||
* @param input - Environment and option flags
|
||||
* @returns Mode selection result with reason
|
||||
*/
|
||||
export function selectOutputMode(input: ModeSelectionInput): ModeSelectionResult {
|
||||
// Priority order matters - check most specific flags first
|
||||
|
||||
if (input.yolo) {
|
||||
return { usePlainTextMode: true, reason: "yolo_flag" }
|
||||
}
|
||||
|
||||
if (input.json) {
|
||||
return { usePlainTextMode: true, reason: "json" }
|
||||
}
|
||||
|
||||
if (input.stdinWasPiped) {
|
||||
return { usePlainTextMode: true, reason: "piped_stdin" }
|
||||
}
|
||||
|
||||
if (!input.stdinIsTTY) {
|
||||
return { usePlainTextMode: true, reason: "stdin_redirected" }
|
||||
}
|
||||
|
||||
if (!input.stdoutIsTTY) {
|
||||
return { usePlainTextMode: true, reason: "stdout_redirected" }
|
||||
}
|
||||
|
||||
return { usePlainTextMode: false, reason: "interactive" }
|
||||
}
|
||||
@@ -38,9 +38,9 @@ export async function fetchOpenRouterModels(): Promise<string[]> {
|
||||
throw new Error(`Failed to fetch: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const data = (await response.json()) as { data?: OpenRouterModel[] }
|
||||
if (data?.data) {
|
||||
const models = (data.data as OpenRouterModel[]).map((m) => m.id).sort((a, b) => a.localeCompare(b))
|
||||
const models = data.data.map((m) => m.id).sort((a, b) => a.localeCompare(b))
|
||||
cachedModels = models
|
||||
return models
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { execFileSync } from "node:child_process"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
// @ts-expect-error - @vscode/ripgrep has no type declarations
|
||||
import { rgPath } from "@vscode/ripgrep"
|
||||
|
||||
const data = process.env.CLINE_DATA_DIR ?? path.join(os.homedir(), ".cline", "data")
|
||||
|
||||
+125
-31
@@ -1,54 +1,145 @@
|
||||
import { EventEmitter } from "node:events"
|
||||
import * as fs from "node:fs"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { readStdinIfPiped } from "./piped"
|
||||
|
||||
// Mock the fs module
|
||||
vi.mock("node:fs", () => ({
|
||||
readFileSync: vi.fn(),
|
||||
}))
|
||||
// Mock fs.fstatSync to simulate pipe detection
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await vi.importActual("node:fs")
|
||||
return {
|
||||
...actual,
|
||||
fstatSync: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe("readStdinIfPiped", () => {
|
||||
const mockReadFileSync = fs.readFileSync as ReturnType<typeof vi.fn>
|
||||
let originalIsTTY: boolean | undefined
|
||||
let originalStdin: typeof process.stdin
|
||||
let mockStdin: EventEmitter & {
|
||||
isTTY?: boolean
|
||||
setEncoding: ReturnType<typeof vi.fn>
|
||||
resume: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
originalIsTTY = process.stdin.isTTY
|
||||
originalStdin = process.stdin
|
||||
|
||||
// Create a mock stdin
|
||||
mockStdin = Object.assign(new EventEmitter(), {
|
||||
isTTY: undefined as boolean | undefined,
|
||||
setEncoding: vi.fn(),
|
||||
resume: vi.fn(),
|
||||
})
|
||||
|
||||
// Default: simulate a real pipe (FIFO)
|
||||
vi.mocked(fs.fstatSync).mockReturnValue({
|
||||
isFIFO: () => true,
|
||||
isFile: () => false,
|
||||
} as fs.Stats)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
// Restore original isTTY value
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value: originalIsTTY,
|
||||
// Restore original stdin
|
||||
Object.defineProperty(process, "stdin", {
|
||||
value: originalStdin,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
})
|
||||
|
||||
function setTTY(value: boolean | undefined) {
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
value,
|
||||
mockStdin.isTTY = value
|
||||
Object.defineProperty(process, "stdin", {
|
||||
value: mockStdin,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
function emitData(data: string) {
|
||||
mockStdin.emit("data", data)
|
||||
}
|
||||
|
||||
function emitEnd() {
|
||||
mockStdin.emit("end")
|
||||
}
|
||||
|
||||
function emitError(error: Error) {
|
||||
mockStdin.emit("error", error)
|
||||
}
|
||||
|
||||
describe("TTY detection", () => {
|
||||
it("should return null when stdin is a TTY (interactive terminal)", async () => {
|
||||
setTTY(true)
|
||||
const result = await readStdinIfPiped()
|
||||
expect(result).toBeNull()
|
||||
expect(mockReadFileSync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should attempt to read when stdin is not a TTY (piped input)", async () => {
|
||||
setTTY(false)
|
||||
mockReadFileSync.mockReturnValue("")
|
||||
|
||||
const promise = readStdinIfPiped()
|
||||
emitEnd()
|
||||
const result = await promise
|
||||
|
||||
expect(result).toBe("")
|
||||
expect(mockStdin.setEncoding).toHaveBeenCalledWith("utf8")
|
||||
expect(mockStdin.resume).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("stdin type detection (fstat)", () => {
|
||||
it("should return null when stdin is not a FIFO or file (spawned without TTY)", async () => {
|
||||
setTTY(false)
|
||||
// Simulate a character device or socket (not a pipe)
|
||||
vi.mocked(fs.fstatSync).mockReturnValue({
|
||||
isFIFO: () => false,
|
||||
isFile: () => false,
|
||||
} as fs.Stats)
|
||||
|
||||
const result = await readStdinIfPiped()
|
||||
expect(result).toBeNull()
|
||||
expect(mockReadFileSync).toHaveBeenCalledWith(0, "utf8")
|
||||
})
|
||||
|
||||
it("should return null when fstatSync throws (detached stdin)", async () => {
|
||||
setTTY(false)
|
||||
vi.mocked(fs.fstatSync).mockImplementation(() => {
|
||||
throw new Error("EBADF: bad file descriptor")
|
||||
})
|
||||
|
||||
const result = await readStdinIfPiped()
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("should read from stdin when it is a FIFO (pipe)", async () => {
|
||||
setTTY(false)
|
||||
vi.mocked(fs.fstatSync).mockReturnValue({
|
||||
isFIFO: () => true,
|
||||
isFile: () => false,
|
||||
} as fs.Stats)
|
||||
|
||||
const promise = readStdinIfPiped()
|
||||
emitData("piped content")
|
||||
emitEnd()
|
||||
const result = await promise
|
||||
|
||||
expect(result).toBe("piped content")
|
||||
})
|
||||
|
||||
it("should read from stdin when it is a regular file (redirected)", async () => {
|
||||
setTTY(false)
|
||||
vi.mocked(fs.fstatSync).mockReturnValue({
|
||||
isFIFO: () => false,
|
||||
isFile: () => true,
|
||||
} as fs.Stats)
|
||||
|
||||
const promise = readStdinIfPiped()
|
||||
emitData("file content")
|
||||
emitEnd()
|
||||
const result = await promise
|
||||
|
||||
expect(result).toBe("file content")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,14 +167,14 @@ describe("readStdinIfPiped", () => {
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: null,
|
||||
description: "should return null for empty input",
|
||||
expected: "",
|
||||
description: "should return empty string for empty input",
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n \t \n ",
|
||||
expected: null,
|
||||
description: "should return null for whitespace-only input",
|
||||
expected: "",
|
||||
description: "should return empty string for whitespace-only input",
|
||||
},
|
||||
{
|
||||
name: "leading and trailing whitespace",
|
||||
@@ -127,25 +218,25 @@ describe("readStdinIfPiped", () => {
|
||||
it(`${name}${description ? ` - ${description}` : ""}`, async () => {
|
||||
setTTY(false)
|
||||
const data = Array.isArray(input) ? input.join("\n") : input
|
||||
mockReadFileSync.mockReturnValue(data)
|
||||
|
||||
const result = await readStdinIfPiped()
|
||||
const promise = readStdinIfPiped()
|
||||
emitData(data)
|
||||
emitEnd()
|
||||
const result = await promise
|
||||
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should return null on fs.readFileSync error and fall back to async", async () => {
|
||||
it("should return null on stdin error", async () => {
|
||||
setTTY(false)
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error("EAGAIN: resource temporarily unavailable")
|
||||
})
|
||||
|
||||
// The async fallback will timeout since we can't easily mock process.stdin events
|
||||
// But we can verify it doesn't throw
|
||||
const result = await readStdinIfPiped()
|
||||
// Result will be null because async path times out with no data
|
||||
const promise = readStdinIfPiped()
|
||||
emitError(new Error("EAGAIN: resource temporarily unavailable"))
|
||||
const result = await promise
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -188,9 +279,12 @@ describe("readStdinIfPiped", () => {
|
||||
useCases.forEach(({ name, input, expected }) => {
|
||||
it(`should handle ${name}`, async () => {
|
||||
setTTY(false)
|
||||
mockReadFileSync.mockReturnValue(input)
|
||||
|
||||
const result = await readStdinIfPiped()
|
||||
const promise = readStdinIfPiped()
|
||||
emitData(input)
|
||||
emitEnd()
|
||||
const result = await promise
|
||||
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
+18
-1
@@ -1,3 +1,5 @@
|
||||
import * as fs from "node:fs"
|
||||
|
||||
/**
|
||||
* Read piped input from stdin (non-blocking)
|
||||
*
|
||||
@@ -9,11 +11,26 @@
|
||||
* for EOF which signals that the previous command has finished writing.
|
||||
*/
|
||||
export async function readStdinIfPiped(): Promise<string | null> {
|
||||
// Check if stdin is a TTY (interactive) or piped
|
||||
// Check if stdin is a TTY (interactive) - no piped input
|
||||
if (process.stdin.isTTY) {
|
||||
return null
|
||||
}
|
||||
|
||||
// When spawned as a child process without TTY (e.g., from spawn()), stdin.isTTY
|
||||
// is false but there's no actual piped input. Check if stdin is a real pipe/file
|
||||
// by testing if we can get stats on fd 0. A real pipe will have stats, while
|
||||
// a detached stdin may throw or have unusual properties.
|
||||
try {
|
||||
const stats = fs.fstatSync(0)
|
||||
// If it's not a FIFO (pipe) or regular file, treat as no input
|
||||
if (!stats.isFIFO() && !stats.isFile()) {
|
||||
return null
|
||||
}
|
||||
} catch {
|
||||
// If we can't stat stdin, treat as no input
|
||||
return null
|
||||
}
|
||||
|
||||
// Use async approach - more reliable for piped input from other commands
|
||||
// The synchronous readFileSync(0) can fail with EAGAIN when the pipe
|
||||
// isn't ready yet (common when piping from another cline command)
|
||||
|
||||
@@ -12,18 +12,23 @@
|
||||
// Console output is intentional here for plain text mode
|
||||
|
||||
import type { ClineMessage, ExtensionState } from "@shared/ExtensionMessage"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import type { Controller } from "@/core/controller"
|
||||
import { getRequestRegistry } from "@/core/controller/grpc-handler"
|
||||
import { subscribeToState } from "@/core/controller/state/subscribeToState"
|
||||
import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
|
||||
|
||||
export interface PlainTextTaskOptions {
|
||||
controller: Controller
|
||||
prompt: string
|
||||
/** Prompt for new task or message to send to resumed task */
|
||||
prompt?: string
|
||||
imageDataUrls?: string[]
|
||||
verbose?: boolean
|
||||
jsonOutput?: boolean
|
||||
/** Timeout in seconds (default: 600 = 10 minutes) */
|
||||
timeoutSeconds?: number
|
||||
/** Task ID to resume an existing task */
|
||||
taskId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,9 +44,9 @@ export interface PlainTextTaskOptions {
|
||||
export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<boolean> {
|
||||
const { controller, prompt, imageDataUrls, verbose, jsonOutput } = options
|
||||
|
||||
let completionResolve: () => void
|
||||
let completionResolve: (reason?: any) => void
|
||||
let completionReject: (reason?: any) => void
|
||||
const completionPromise = new Promise<void>((res, rej) => {
|
||||
const completionPromise = new Promise<string>((res, rej) => {
|
||||
completionResolve = res
|
||||
completionReject = rej
|
||||
})
|
||||
@@ -50,6 +55,13 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
|
||||
// Track which messages have been processed (by timestamp)
|
||||
const processedMessages = new Map<number, string>()
|
||||
|
||||
const isViewTaskOnly = Boolean(options.taskId) && !prompt
|
||||
|
||||
// When resuming a task, we need to ignore completion_result messages that existed
|
||||
// before we sent our new prompt. This timestamp marks the cutoff - only completion
|
||||
// results AFTER this time should trigger task completion.
|
||||
const completionCutoffTs = Date.now()
|
||||
|
||||
// Helper to process a message and track completion state
|
||||
const processMessage = (message: ClineMessage) => {
|
||||
const ts = message.ts || 0
|
||||
@@ -67,8 +79,12 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
|
||||
processedMessages.set(ts, message.text ?? "")
|
||||
|
||||
// Check for completion (only on non-partial messages)
|
||||
// When resuming a task, only consider completion_result messages that appeared
|
||||
// AFTER we sent our resume message (ts > completionCutoffTs)
|
||||
if (message.say === "completion_result" || message.ask === "completion_result") {
|
||||
completionResolve()
|
||||
if (isViewTaskOnly || ts > completionCutoffTs) {
|
||||
completionResolve()
|
||||
}
|
||||
} else if (message.say === "error" || message.ask === "api_req_failed") {
|
||||
completionReject(message.text ?? "message.say error || message.ask api_req_failed")
|
||||
}
|
||||
@@ -99,8 +115,27 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
|
||||
)
|
||||
|
||||
try {
|
||||
// Start the task
|
||||
await controller.initTask(prompt, imageDataUrls)
|
||||
// Either resume an existing task or start a new one
|
||||
if (options.taskId) {
|
||||
// Load the existing task
|
||||
await showTaskWithId(controller, StringRequest.create({ value: options.taskId }))
|
||||
|
||||
// If a prompt was provided, send it as a message to the resumed task
|
||||
if (prompt && controller.task) {
|
||||
// Wait a moment for the task to fully load
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Send the prompt as a response to any pending ask, or as a new message
|
||||
await controller.task.handleWebviewAskResponse("messageResponse", prompt)
|
||||
}
|
||||
} else if (prompt) {
|
||||
// Start a new task with the prompt
|
||||
await controller.initTask(prompt, imageDataUrls)
|
||||
} else {
|
||||
throw new Error("Either taskId or prompt must be provided")
|
||||
}
|
||||
|
||||
// Normal mode: wait for task completion
|
||||
const timeoutMs = (options.timeoutSeconds ?? 600) * 1000 // default 10 minutes
|
||||
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs))
|
||||
await Promise.race([completionPromise, timeoutPromise])
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getProviderModelIdKey, ProviderToApiKeyMap } from "@shared/storage"
|
||||
import { buildApiHandler } from "@/core/api"
|
||||
import type { Controller } from "@/core/controller"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { BedrockConfig } from "../components/BedrockSetup"
|
||||
import { getDefaultModelId } from "../components/ModelPicker"
|
||||
|
||||
export interface ApplyProviderConfigOptions {
|
||||
@@ -75,3 +76,52 @@ export async function applyProviderConfig(options: ApplyProviderConfigOptions):
|
||||
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApplyBedrockConfigOptions {
|
||||
bedrockConfig: BedrockConfig
|
||||
modelId?: string
|
||||
controller?: Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Bedrock provider configuration to state
|
||||
* Handles AWS-specific fields (authentication, region, credentials)
|
||||
*/
|
||||
export async function applyBedrockConfig(options: ApplyBedrockConfigOptions): Promise<void> {
|
||||
const { bedrockConfig, modelId, controller } = options
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
const config: Record<string, unknown> = {
|
||||
actModeApiProvider: "bedrock",
|
||||
planModeApiProvider: "bedrock",
|
||||
awsAuthentication: bedrockConfig.awsAuthentication,
|
||||
awsRegion: bedrockConfig.awsRegion,
|
||||
awsUseCrossRegionInference: bedrockConfig.awsUseCrossRegionInference,
|
||||
}
|
||||
|
||||
// Add model ID
|
||||
const finalModelId = modelId || getDefaultModelId("bedrock")
|
||||
if (finalModelId) {
|
||||
const actModelKey = getProviderModelIdKey("bedrock" as ApiProvider, "act")
|
||||
const planModelKey = getProviderModelIdKey("bedrock" as ApiProvider, "plan")
|
||||
if (actModelKey) config[actModelKey] = finalModelId
|
||||
if (planModelKey) config[planModelKey] = finalModelId
|
||||
}
|
||||
|
||||
// Add optional AWS credentials
|
||||
if (bedrockConfig.awsProfile !== undefined) config.awsProfile = bedrockConfig.awsProfile
|
||||
if (bedrockConfig.awsAccessKey) config.awsAccessKey = bedrockConfig.awsAccessKey
|
||||
if (bedrockConfig.awsSecretKey) config.awsSecretKey = bedrockConfig.awsSecretKey
|
||||
if (bedrockConfig.awsSessionToken) config.awsSessionToken = bedrockConfig.awsSessionToken
|
||||
|
||||
// Save via StateManager
|
||||
stateManager.setApiConfiguration(config as Record<string, string>)
|
||||
await stateManager.flushPendingState()
|
||||
|
||||
// Rebuild API handler on active task if one exists
|
||||
if (controller?.task) {
|
||||
const currentMode = stateManager.getGlobalSettingsKey("mode")
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
* Used by both UI components and CLI commands
|
||||
*/
|
||||
|
||||
import { useMemo } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import providersData from "@/shared/providers/providers.json"
|
||||
import type { RemoteConfigFields } from "@/shared/storage/state-keys"
|
||||
|
||||
// Create a lookup map from provider value to display label
|
||||
const providerLabels: Record<string, string> = Object.fromEntries(
|
||||
@@ -17,7 +20,7 @@ const providerOrder: string[] = providersData.list.map((p: { value: string }) =>
|
||||
* Providers that are not supported in CLI.
|
||||
* - vscode-lm: Requires VS Code's Language Model API (see ENG-1490 for OAuth-based support)
|
||||
*/
|
||||
export const CLI_EXCLUDED_PROVIDERS = new Set<string>(["vscode-lm"])
|
||||
const CLI_EXCLUDED_PROVIDERS = new Set<string>(["vscode-lm"])
|
||||
|
||||
/**
|
||||
* Get the display label for a provider ID
|
||||
@@ -29,7 +32,7 @@ export function getProviderLabel(providerId: string): string {
|
||||
/**
|
||||
* Get the ordered list of all provider IDs (from providers.json)
|
||||
*/
|
||||
export function getProviderOrder(): string[] {
|
||||
function getProviderOrder(): string[] {
|
||||
return providerOrder
|
||||
}
|
||||
|
||||
@@ -46,3 +49,19 @@ export function getValidCliProviders(): string[] {
|
||||
export function isValidCliProvider(providerId: string): boolean {
|
||||
return providerOrder.includes(providerId) && !CLI_EXCLUDED_PROVIDERS.has(providerId)
|
||||
}
|
||||
|
||||
const getValidProviders = (remoteConfig: Partial<RemoteConfigFields> | undefined) => {
|
||||
if (remoteConfig?.remoteConfiguredProviders?.length) {
|
||||
return remoteConfig.remoteConfiguredProviders
|
||||
}
|
||||
|
||||
return getProviderOrder().filter((p: string) => !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
}
|
||||
|
||||
export const useValidProviders = () => {
|
||||
const remoteConfig = StateManager.get().getRemoteConfigSettings()
|
||||
|
||||
return useMemo(() => {
|
||||
return getValidProviders(remoteConfig)
|
||||
}, [remoteConfig])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Wait for a condition to become truthy, with a timeout.
|
||||
* Uses Promise.race for clean timeout handling instead of polling.
|
||||
*
|
||||
* @param condition - Function that returns the value to check (truthy = done)
|
||||
* @param timeoutMs - Maximum time to wait in milliseconds
|
||||
* @param pollIntervalMs - How often to check the condition (default: 100ms)
|
||||
* @returns The truthy value if condition is met, or undefined if timeout
|
||||
*/
|
||||
export async function waitFor<T>(
|
||||
condition: () => T | undefined | null,
|
||||
timeoutMs: number,
|
||||
pollIntervalMs: number = 100,
|
||||
): Promise<T | undefined> {
|
||||
// Check immediately first
|
||||
const immediate = condition()
|
||||
if (immediate) {
|
||||
return immediate
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const intervalId = setInterval(() => {
|
||||
const result = condition()
|
||||
if (result) {
|
||||
clearInterval(intervalId)
|
||||
clearTimeout(timeoutId)
|
||||
resolve(result)
|
||||
}
|
||||
}, pollIntervalMs)
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
clearInterval(intervalId)
|
||||
resolve(undefined)
|
||||
}, timeoutMs)
|
||||
})
|
||||
}
|
||||
+2
-1
@@ -7,7 +7,8 @@
|
||||
"jsx": "react",
|
||||
"jsxFactory": "React.createElement",
|
||||
"lib": [
|
||||
"es2022"
|
||||
"es2022",
|
||||
"DOM"
|
||||
],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "Bundler",
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
title: "ACP: Editor Integrations"
|
||||
description: "Use Cline in JetBrains, Neovim, Zed, and other editors via the Agent Client Protocol"
|
||||
---
|
||||
|
||||
|
||||
Cline CLI supports the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/), an open standard that enables AI coding agents to work across different editors and IDEs. This means you can use the full Cline agent—with all its capabilities including Skills, Hooks, and MCP integrations—in your preferred development environment.
|
||||
|
||||
## Why ACP?
|
||||
|
||||
- **Editor flexibility**: Use Cline in JetBrains, Neovim, Zed, or any ACP-compatible editor
|
||||
- **No feature compromises**: Full access to Cline's capabilities regardless of editor
|
||||
- **Team consistency**: Same AI assistant across different developer workflows
|
||||
- **Open standard**: Built on Zed's open Agent Client Protocol specification
|
||||
|
||||
## JetBrains IDEs
|
||||
|
||||
[JetBrains](https://www.jetbrains.com) IDEs include IntelliJ IDEA, PyCharm, WebStorm, and more. They offer built-in AI Assistant with ACP support.
|
||||
|
||||
<Note>
|
||||
**Recommended: Native JetBrains Plugin**
|
||||
|
||||
For the best JetBrains experience, install the [native Cline plugin](/getting-started/installing-cline#jetbrains-ides) from the JetBrains Marketplace. It provides full IDE integration and the complete Cline experience.
|
||||
|
||||
The ACP setup below is an alternative way to use Cline CLI features in JetBrains IDEs.
|
||||
</Note>
|
||||
|
||||
Alternatively, you can run Cline CLI in IntelliJ IDEA, PyCharm, WebStorm, and all other JetBrains IDEs through their built-in AI Assistant with ACP support.
|
||||
|
||||
<video
|
||||
src="https://storage.googleapis.com/cline_public_images/cline-acp-jetbrains.mp4"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
style={{ width: "100%", borderRadius: "8px", marginTop: "16px", marginBottom: "16px" }}
|
||||
/>
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Install Cline CLI** (if not already installed):
|
||||
```bash
|
||||
npm i -g cline
|
||||
```
|
||||
|
||||
2. **Authenticate with Cline**:
|
||||
```bash
|
||||
cline auth
|
||||
```
|
||||
|
||||
3. **Configure JetBrains AI Assistant**:
|
||||
- Open your JetBrains IDE
|
||||
- Navigate to `Settings | Tools | AI Assistant | Agents`
|
||||
- Click "Add Custom Agent"
|
||||
- This opens/creates `~/.jetbrains/acp.json`
|
||||
|
||||
4. **Add Cline to `acp.json`**:
|
||||
```json
|
||||
{
|
||||
"agent_servers": {
|
||||
"Cline": {
|
||||
"command": "cline",
|
||||
"args": ["--acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **Use Cline**:
|
||||
- Open the AI Chat tool window
|
||||
- Select "Cline" from the agent dropdown
|
||||
- Start coding with Cline in your JetBrains IDE!
|
||||
|
||||
<Tip>
|
||||
JetBrains AI Assistant can expose its built-in MCP server to Cline, giving Cline access to IDE-specific tools and context.
|
||||
</Tip>
|
||||
|
||||
## Neovim
|
||||
|
||||
[Neovim](https://neovim.io) is a hyperextensible Vim-based text editor loved by developers for its speed and flexibility. Use Cline in Neovim through the [agentic.nvim](https://github.com/carlos-algms/agentic.nvim) or [avante.nvim](https://github.com/yetone/avante.nvim) plugins, which provide ACP integration.
|
||||
|
||||
<video
|
||||
src="https://storage.googleapis.com/cline_public_images/cline-acp-neovim-avante.mp4"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
style={{ width: "100%", borderRadius: "8px", marginTop: "16px", marginBottom: "16px" }}
|
||||
/>
|
||||
|
||||
### Setup with agentic.nvim
|
||||
|
||||
1. **Install Cline CLI** (if not already installed):
|
||||
```bash
|
||||
npm i -g cline
|
||||
```
|
||||
|
||||
2. **Authenticate with Cline**:
|
||||
```bash
|
||||
cline auth
|
||||
```
|
||||
|
||||
3. **Install agentic.nvim** using lazy.nvim:
|
||||
```lua
|
||||
{
|
||||
"carlos-algms/agentic.nvim",
|
||||
opts = {
|
||||
provider = "cline-acp",
|
||||
acp_providers = {
|
||||
["cline-acp"] = {
|
||||
command = "cline",
|
||||
args = {"--acp"},
|
||||
},
|
||||
},
|
||||
},
|
||||
keys = {
|
||||
{"<C-\\>", function() require("agentic").toggle() end, mode={"n","v","i"}, desc="Toggle Cline Chat"},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
4. **Use Cline**:
|
||||
- Press `<C-\>` to toggle Cline chat
|
||||
- Start coding with Cline in Neovim!
|
||||
|
||||
### Setup with avante.nvim
|
||||
|
||||
Follow the [avante.nvim documentation](https://github.com/yetone/avante.nvim) for configuring external ACP agents and point it to `cline --acp`.
|
||||
|
||||
## Zed
|
||||
|
||||
[Zed](https://zed.dev) is a high-performance, multiplayer code editor built from the ground up for speed and collaboration. Zed's team created the Agent Client Protocol, making Cline a natural fit for this editor.
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Install Cline CLI** (if not already installed):
|
||||
```bash
|
||||
npm i -g cline
|
||||
```
|
||||
|
||||
2. **Authenticate with Cline**:
|
||||
```bash
|
||||
cline auth
|
||||
```
|
||||
|
||||
3. **Configure Zed**:
|
||||
- Open Zed settings (`Cmd/Ctrl + ,`)
|
||||
- Add Cline to your `settings.json`:
|
||||
```json
|
||||
{
|
||||
"agent_servers": {
|
||||
"Cline": {
|
||||
"type": "custom",
|
||||
"command": "cline",
|
||||
"args": ["--acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Use Cline**:
|
||||
- Open the AI assistant panel
|
||||
- Select "Cline" from the agent dropdown
|
||||
- Start coding with Cline in Zed!
|
||||
|
||||
## Other Editors
|
||||
|
||||
Any editor that supports the Agent Client Protocol can run Cline. Check your editor's documentation for ACP configuration instructions, then point it to:
|
||||
|
||||
```bash
|
||||
cline --acp
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent not appearing
|
||||
|
||||
- Ensure Cline CLI is installed globally: `npm i -g cline`
|
||||
- Verify authentication: `cline auth`
|
||||
- Check that `cline --acp` runs without errors
|
||||
- Restart your editor after configuration changes
|
||||
|
||||
### Permission errors
|
||||
|
||||
If Cline can't access files or run commands:
|
||||
- Check that your editor's ACP integration passes the correct working directory
|
||||
- Verify file permissions in your project
|
||||
- Ensure Cline has approval settings configured correctly
|
||||
|
||||
### Connection issues
|
||||
|
||||
- Make sure no other Cline instance is using the same configuration directory
|
||||
- Check editor logs for ACP-related errors
|
||||
- Try running `cline --acp` manually to test the connection
|
||||
|
||||
## Learn More
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="CLI Overview" icon="terminal" href="/cline-cli/overview">
|
||||
Learn about Cline CLI's core capabilities and use cases.
|
||||
</Card>
|
||||
|
||||
<Card title="Three Core Flows" icon="route" href="/cline-cli/three-core-flows">
|
||||
Master interactive mode, headless automation, and multi-instance workflows.
|
||||
</Card>
|
||||
|
||||
<Card title="Skills" icon="graduation-cap" href="/features/skills">
|
||||
Understand how Cline's Skills work across all editors via ACP.
|
||||
</Card>
|
||||
|
||||
<Card title="Hooks" icon="link" href="/features/hooks/index">
|
||||
Learn how to enforce policies with Hooks in any editor.
|
||||
</Card>
|
||||
</Columns>
|
||||
@@ -0,0 +1,482 @@
|
||||
---
|
||||
title: "CLI Reference (Deprecated)"
|
||||
description: "Command reference for Cline CLI versions earlier than 2.0.0 (deprecated). For the latest commands and options, see the current Cline CLI reference."
|
||||
---
|
||||
|
||||
Complete command reference for Cline CLI. Use this for detailed documentation on all commands, options, and configuration.
|
||||
|
||||
For quick help in your terminal:
|
||||
|
||||
```bash
|
||||
cline --help # Show all commands
|
||||
cline task --help # Show task-specific commands
|
||||
man cline # View the full manual page
|
||||
```
|
||||
|
||||
## Manual Page
|
||||
|
||||
The complete manual page for the Cline CLI:
|
||||
|
||||
```
|
||||
CLINE(1) User Commands CLINE(1)
|
||||
|
||||
NAME
|
||||
cline - orchestrate and interact with Cline AI coding agents
|
||||
|
||||
SYNOPSIS
|
||||
cline [prompt] [options]
|
||||
|
||||
cline command [subcommand] [options] [arguments]
|
||||
|
||||
DESCRIPTION
|
||||
Try: cat README.md | cline "Summarize this for me:"
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
MODES OF OPERATION
|
||||
Instant Task Mode
|
||||
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.
|
||||
|
||||
Subcommand Mode
|
||||
Advanced usage with explicit control: cline <command>
|
||||
[subcommand] [options] provides fine-grained control over
|
||||
instances, tasks, authentication, and configuration.
|
||||
|
||||
AGENT BEHAVIOR
|
||||
Cline operates in two primary modes:
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
-h, --help
|
||||
Display help information for the command.
|
||||
|
||||
-v, --verbose
|
||||
Enable verbose output for debugging.
|
||||
|
||||
COMMANDS
|
||||
Authentication
|
||||
cline auth [provider] [key]
|
||||
|
||||
cline a [provider] [key]
|
||||
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.
|
||||
|
||||
Instance Management
|
||||
Cline Core instances are independent agent processes that can run in
|
||||
the background. Multiple instances can run simultaneously, enabling
|
||||
parallel task execution.
|
||||
|
||||
cline instance
|
||||
|
||||
cline i
|
||||
Display instance management help.
|
||||
|
||||
cline instance new [-d|--default]
|
||||
|
||||
cline i n [-d|--default]
|
||||
Spawn a new Cline Core instance. Use --default to set it as
|
||||
the default instance for subsequent commands.
|
||||
|
||||
cline instance list
|
||||
|
||||
cline i l
|
||||
List all running Cline Core instances with their addresses and
|
||||
status.
|
||||
|
||||
cline instance default address
|
||||
|
||||
cline i d address
|
||||
Set the default instance to avoid specifying --address in task
|
||||
commands.
|
||||
|
||||
cline instance kill address [-a|--all]
|
||||
|
||||
cline i k address [-a|--all]
|
||||
Terminate a Cline Core instance. Use --all to kill all running
|
||||
instances.
|
||||
|
||||
Task Management
|
||||
Tasks represent individual work items that Cline executes. Tasks
|
||||
maintain conversation history, checkpoints, and settings.
|
||||
|
||||
cline task [-a|--address ADDR]
|
||||
|
||||
cline t [-a|--address ADDR]
|
||||
Display task management help. The --address flag specifies
|
||||
which Cline Core instance to use (e.g., localhost:50052).
|
||||
|
||||
cline task new prompt [options]
|
||||
|
||||
cline t n prompt [options]
|
||||
Create a new task in the default or specified instance.
|
||||
Options:
|
||||
|
||||
-s, --setting setting value
|
||||
Set task-specific settings
|
||||
|
||||
-y, --no-interactive, --yolo
|
||||
Enable autonomous mode
|
||||
|
||||
-m, --mode mode
|
||||
Starting mode (act or plan)
|
||||
|
||||
cline task open task-id [options]
|
||||
|
||||
cline t o task-id [options]
|
||||
Resume a previous task from history. Accepts the same options
|
||||
as task new.
|
||||
|
||||
cline task list
|
||||
|
||||
cline t l
|
||||
List all tasks in history with their id and snippet
|
||||
|
||||
cline task chat
|
||||
|
||||
cline t c
|
||||
Enter interactive chat mode for the current task. Allows
|
||||
back-and-forth conversation with Cline.
|
||||
|
||||
cline task send [message] [options]
|
||||
|
||||
cline t s [message] [options]
|
||||
Send a message to Cline. If no message is provided, reads from
|
||||
stdin. Options:
|
||||
|
||||
-a, --approve
|
||||
Approve Cline's proposed action
|
||||
|
||||
-d, --deny
|
||||
Deny Cline's proposed action
|
||||
|
||||
-f, --file FILE
|
||||
Attach a file to the message
|
||||
|
||||
-y, --no-interactive, --yolo
|
||||
Enable autonomous mode
|
||||
|
||||
-m, --mode mode
|
||||
Switch mode (act or plan)
|
||||
|
||||
cline task view [-f|--follow] [-c|--follow-complete]
|
||||
|
||||
cline t v [-f|--follow] [-c|--follow-complete]
|
||||
Display the current conversation. Use --follow to stream
|
||||
updates in real-time, or --follow-complete to follow until task
|
||||
completion.
|
||||
|
||||
cline task restore checkpoint
|
||||
|
||||
cline t r checkpoint
|
||||
Restore the task to a previous checkpoint state.
|
||||
|
||||
cline task pause
|
||||
|
||||
cline t p
|
||||
Pause task execution.
|
||||
|
||||
Configuration
|
||||
Configuration can be set globally. Override these global settings for
|
||||
a task using the --setting flag
|
||||
|
||||
cline config
|
||||
|
||||
cline c
|
||||
|
||||
cline config set key value
|
||||
|
||||
cline c s key value
|
||||
Set a configuration variable.
|
||||
|
||||
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.
|
||||
|
||||
Context Window Configuration
|
||||
For local model providers, you can configure the context window size:
|
||||
|
||||
Ollama
|
||||
cline config s ollama-api-options-ctx-num=32768
|
||||
|
||||
LM Studio
|
||||
cline config s lm-studio-max-tokens=32768
|
||||
|
||||
For other providers (Anthropic, OpenRouter, etc.), the context window
|
||||
is defined per model in the model metadata and is not user-settable.
|
||||
Cline uses each model's built-in context limits automatically.
|
||||
|
||||
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)
|
||||
|
||||
hooks_enabled
|
||||
Enable or disable hooks for the task (true/false)
|
||||
|
||||
HOOKS INTEGRATION
|
||||
Hooks let you inject custom logic into Cline's workflow at key moments.
|
||||
They can validate operations before they execute, monitor tool usage,
|
||||
and shape AI decisions. This allows you to integrate hooks into
|
||||
automated workflows, CI/CD pipelines, and headless task execution.
|
||||
|
||||
Enable hooks for a task:
|
||||
|
||||
cline "prompt" -s hooks_enabled=true
|
||||
|
||||
Configure hooks globally:
|
||||
|
||||
cline config set hooks-enabled=true
|
||||
cline config get hooks-enabled
|
||||
|
||||
Note: Hooks in the CLI are only supported on macOS and Linux.
|
||||
|
||||
For complete hooks documentation, see:
|
||||
<https://docs.cline.bot/features/hooks/index>
|
||||
|
||||
NOTES & EXAMPLES
|
||||
The cline task send and cline task new commands support reading from
|
||||
stdin, enabling powerful pipeline compositions:
|
||||
|
||||
cat requirements.txt | cline task send
|
||||
echo "Refactor this code" | cline -y
|
||||
|
||||
Instance Management
|
||||
Manage multiple Cline instances:
|
||||
|
||||
# Start a new instance and make it default
|
||||
cline instance new --default
|
||||
|
||||
# List all running instances
|
||||
cline instance list
|
||||
|
||||
# Kill a specific instance
|
||||
cline instance kill localhost:50052
|
||||
|
||||
# Kill all CLI instances
|
||||
cline instance kill --all-cli
|
||||
|
||||
Task History
|
||||
Work with task history:
|
||||
|
||||
# List previous tasks
|
||||
cline task list
|
||||
|
||||
# Resume a previous task
|
||||
cline task open 1760501486669
|
||||
|
||||
# View conversation history
|
||||
cline task view
|
||||
|
||||
# Start interactive chat with this task
|
||||
cline task chat
|
||||
|
||||
ARCHITECTURE
|
||||
Cline operates on a three-layer architecture:
|
||||
|
||||
Presentation Layer
|
||||
User interfaces (CLI, VSCode, JetBrains) that connect to Cline
|
||||
Core via gRPC
|
||||
|
||||
Cline Core
|
||||
The autonomous agent service handling task management, AI model
|
||||
integration, state management, tool orchestration, and real-time
|
||||
streaming updates
|
||||
|
||||
Host Provider Layer
|
||||
Environment-specific integrations (VSCode APIs, JetBrains APIs,
|
||||
shell APIs) that Cline Core uses to interact with the host
|
||||
system
|
||||
|
||||
BUGS
|
||||
Report bugs at: <https://github.com/cline/cline/issues>
|
||||
|
||||
For real-time help, join the Discord community at:
|
||||
<https://discord.gg/cline>
|
||||
|
||||
SEE ALSO
|
||||
Full documentation: <https://docs.cline.bot>
|
||||
|
||||
AUTHORS
|
||||
Cline is developed by the Cline Bot Inc. and the open source community.
|
||||
|
||||
COPYRIGHT
|
||||
Copyright © 2025 Cline Bot Inc. Licensed under the Apache License 2.0.
|
||||
```
|
||||
|
||||
## JSON output (-F json)
|
||||
|
||||
When you run a command with `-F json` (or `--output-format json`), Cline prints each client message as JSON.
|
||||
|
||||
### ClineMessage schema
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `type` | `"ask" or "say"` | Yes | Top-level message category. |
|
||||
| `text` | `string` | Yes | Human-readable message content. |
|
||||
| `ts` | `number` | Yes | Unix epoch timestamp in milliseconds. |
|
||||
| `reasoning` | `string` | No | Omitted when empty. |
|
||||
| `say` | `string` | No | Omitted when empty. Present when `type` is `"say"`. |
|
||||
| `ask` | `string` | No | Omitted when empty. Present when `type` is `"ask"`. |
|
||||
| `partial` | `boolean` | No | Omitted when false. `true` for streaming updates. |
|
||||
| `images` | `string[]` | No | Omitted when empty. Image URIs when included with a message. |
|
||||
| `files` | `string[]` | No | Omitted when empty. File paths when attached to a message. |
|
||||
| `lastCheckpointHash` | `string` | No | Omitted when empty. Git checkpoint hash when available. |
|
||||
| `isCheckpointCheckedOut` | `boolean` | No | Omitted when false. `true` if Cline checked out a checkpoint. |
|
||||
| `isOperationOutsideWorkspace` | `boolean` | No | Omitted when false. `true` if an operation happened outside the workspace. |
|
||||
|
||||
<Note>
|
||||
Most fields are optional and omitted when empty. If you parse this output, treat missing fields as “not present”, not as empty strings.
|
||||
</Note>
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "say",
|
||||
"text": "Cline is about to run a command.",
|
||||
"ts": 1760501486669,
|
||||
"say": "command",
|
||||
"partial": false
|
||||
}
|
||||
```
|
||||
|
||||
### Shell Completion
|
||||
|
||||
Generate autocompletion scripts for various shells:
|
||||
|
||||
#### Bash
|
||||
|
||||
```bash
|
||||
# Generate bash completion
|
||||
cline completion bash > /etc/bash_completion.d/cline
|
||||
|
||||
# Or for user-level installation
|
||||
cline completion bash > ~/.local/share/bash-completion/completions/cline
|
||||
```
|
||||
|
||||
#### Zsh
|
||||
|
||||
```bash
|
||||
# Generate zsh completion
|
||||
cline completion zsh > "${fpath[1]}/_cline"
|
||||
|
||||
# Or add to your .zshrc
|
||||
echo 'source <(cline completion zsh)' >> ~/.zshrc
|
||||
```
|
||||
|
||||
#### Fish
|
||||
|
||||
```bash
|
||||
# Generate fish completion
|
||||
cline completion fish > ~/.config/fish/completions/cline.fish
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
```powershell
|
||||
# Generate PowerShell completion
|
||||
cline completion powershell > cline.ps1
|
||||
|
||||
# Add to your PowerShell profile
|
||||
Add-Content $PROFILE "cline completion powershell | Out-String | Invoke-Expression"
|
||||
```
|
||||
|
||||
### Version Command
|
||||
|
||||
```bash
|
||||
# Show version information
|
||||
cline version
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### CLINE_DIR
|
||||
|
||||
Override the default Cline directory location:
|
||||
|
||||
```bash
|
||||
# Override default Cline directory
|
||||
export CLINE_DIR=/custom/path
|
||||
|
||||
# Default: ~/.cline
|
||||
```
|
||||
|
||||
This directory is used for:
|
||||
- Instance registry database
|
||||
- Configuration files
|
||||
- Task history
|
||||
- Checkpoints
|
||||
+375
-431
@@ -1,482 +1,426 @@
|
||||
---
|
||||
title: "CLI Reference"
|
||||
description: "Complete command reference for Cline CLI including configuration, instance management, and task commands"
|
||||
description: "Complete command reference for Cline CLI including all commands, flags, and configuration options"
|
||||
---
|
||||
|
||||
Complete command reference for Cline CLI. Use this for detailed documentation on all commands, options, and configuration.
|
||||
# CLI Reference
|
||||
|
||||
For quick help in your terminal:
|
||||
This page documents all available commands, flags, and configuration options for Cline CLI. For quick help in your terminal, use:
|
||||
|
||||
```bash
|
||||
cline --help # Show all commands
|
||||
cline task --help # Show task-specific commands
|
||||
man cline # View the full manual page
|
||||
cline task --help # Show task command options
|
||||
cline auth --help # Show auth command options
|
||||
man cline # View the full manual page (if installed)
|
||||
```
|
||||
|
||||
## Manual Page
|
||||
## Synopsis
|
||||
|
||||
The complete manual page for the Cline CLI:
|
||||
|
||||
```
|
||||
CLINE(1) User Commands CLINE(1)
|
||||
|
||||
NAME
|
||||
cline - orchestrate and interact with Cline AI coding agents
|
||||
|
||||
SYNOPSIS
|
||||
cline [prompt] [options]
|
||||
|
||||
cline command [subcommand] [options] [arguments]
|
||||
|
||||
DESCRIPTION
|
||||
Try: cat README.md | cline "Summarize this for me:"
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
MODES OF OPERATION
|
||||
Instant Task Mode
|
||||
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.
|
||||
|
||||
Subcommand Mode
|
||||
Advanced usage with explicit control: cline <command>
|
||||
[subcommand] [options] provides fine-grained control over
|
||||
instances, tasks, authentication, and configuration.
|
||||
|
||||
AGENT BEHAVIOR
|
||||
Cline operates in two primary modes:
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
-h, --help
|
||||
Display help information for the command.
|
||||
|
||||
-v, --verbose
|
||||
Enable verbose output for debugging.
|
||||
|
||||
COMMANDS
|
||||
Authentication
|
||||
cline auth [provider] [key]
|
||||
|
||||
cline a [provider] [key]
|
||||
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.
|
||||
|
||||
Instance Management
|
||||
Cline Core instances are independent agent processes that can run in
|
||||
the background. Multiple instances can run simultaneously, enabling
|
||||
parallel task execution.
|
||||
|
||||
cline instance
|
||||
|
||||
cline i
|
||||
Display instance management help.
|
||||
|
||||
cline instance new [-d|--default]
|
||||
|
||||
cline i n [-d|--default]
|
||||
Spawn a new Cline Core instance. Use --default to set it as
|
||||
the default instance for subsequent commands.
|
||||
|
||||
cline instance list
|
||||
|
||||
cline i l
|
||||
List all running Cline Core instances with their addresses and
|
||||
status.
|
||||
|
||||
cline instance default address
|
||||
|
||||
cline i d address
|
||||
Set the default instance to avoid specifying --address in task
|
||||
commands.
|
||||
|
||||
cline instance kill address [-a|--all]
|
||||
|
||||
cline i k address [-a|--all]
|
||||
Terminate a Cline Core instance. Use --all to kill all running
|
||||
instances.
|
||||
|
||||
Task Management
|
||||
Tasks represent individual work items that Cline executes. Tasks
|
||||
maintain conversation history, checkpoints, and settings.
|
||||
|
||||
cline task [-a|--address ADDR]
|
||||
|
||||
cline t [-a|--address ADDR]
|
||||
Display task management help. The --address flag specifies
|
||||
which Cline Core instance to use (e.g., localhost:50052).
|
||||
|
||||
cline task new prompt [options]
|
||||
|
||||
cline t n prompt [options]
|
||||
Create a new task in the default or specified instance.
|
||||
Options:
|
||||
|
||||
-s, --setting setting value
|
||||
Set task-specific settings
|
||||
|
||||
-y, --no-interactive, --yolo
|
||||
Enable autonomous mode
|
||||
|
||||
-m, --mode mode
|
||||
Starting mode (act or plan)
|
||||
|
||||
cline task open task-id [options]
|
||||
|
||||
cline t o task-id [options]
|
||||
Resume a previous task from history. Accepts the same options
|
||||
as task new.
|
||||
|
||||
cline task list
|
||||
|
||||
cline t l
|
||||
List all tasks in history with their id and snippet
|
||||
|
||||
cline task chat
|
||||
|
||||
cline t c
|
||||
Enter interactive chat mode for the current task. Allows
|
||||
back-and-forth conversation with Cline.
|
||||
|
||||
cline task send [message] [options]
|
||||
|
||||
cline t s [message] [options]
|
||||
Send a message to Cline. If no message is provided, reads from
|
||||
stdin. Options:
|
||||
|
||||
-a, --approve
|
||||
Approve Cline's proposed action
|
||||
|
||||
-d, --deny
|
||||
Deny Cline's proposed action
|
||||
|
||||
-f, --file FILE
|
||||
Attach a file to the message
|
||||
|
||||
-y, --no-interactive, --yolo
|
||||
Enable autonomous mode
|
||||
|
||||
-m, --mode mode
|
||||
Switch mode (act or plan)
|
||||
|
||||
cline task view [-f|--follow] [-c|--follow-complete]
|
||||
|
||||
cline t v [-f|--follow] [-c|--follow-complete]
|
||||
Display the current conversation. Use --follow to stream
|
||||
updates in real-time, or --follow-complete to follow until task
|
||||
completion.
|
||||
|
||||
cline task restore checkpoint
|
||||
|
||||
cline t r checkpoint
|
||||
Restore the task to a previous checkpoint state.
|
||||
|
||||
cline task pause
|
||||
|
||||
cline t p
|
||||
Pause task execution.
|
||||
|
||||
Configuration
|
||||
Configuration can be set globally. Override these global settings for
|
||||
a task using the --setting flag
|
||||
|
||||
cline config
|
||||
|
||||
cline c
|
||||
|
||||
cline config set key value
|
||||
|
||||
cline c s key value
|
||||
Set a configuration variable.
|
||||
|
||||
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.
|
||||
|
||||
Context Window Configuration
|
||||
For local model providers, you can configure the context window size:
|
||||
|
||||
Ollama
|
||||
cline config s ollama-api-options-ctx-num=32768
|
||||
|
||||
LM Studio
|
||||
cline config s lm-studio-max-tokens=32768
|
||||
|
||||
For other providers (Anthropic, OpenRouter, etc.), the context window
|
||||
is defined per model in the model metadata and is not user-settable.
|
||||
Cline uses each model's built-in context limits automatically.
|
||||
|
||||
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)
|
||||
|
||||
hooks_enabled
|
||||
Enable or disable hooks for the task (true/false)
|
||||
|
||||
HOOKS INTEGRATION
|
||||
Hooks let you inject custom logic into Cline's workflow at key moments.
|
||||
They can validate operations before they execute, monitor tool usage,
|
||||
and shape AI decisions. This allows you to integrate hooks into
|
||||
automated workflows, CI/CD pipelines, and headless task execution.
|
||||
|
||||
Enable hooks for a task:
|
||||
|
||||
cline "prompt" -s hooks_enabled=true
|
||||
|
||||
Configure hooks globally:
|
||||
|
||||
cline config set hooks-enabled=true
|
||||
cline config get hooks-enabled
|
||||
|
||||
Note: Hooks in the CLI are only supported on macOS and Linux.
|
||||
|
||||
For complete hooks documentation, see:
|
||||
<https://docs.cline.bot/features/hooks/index>
|
||||
|
||||
NOTES & EXAMPLES
|
||||
The cline task send and cline task new commands support reading from
|
||||
stdin, enabling powerful pipeline compositions:
|
||||
|
||||
cat requirements.txt | cline task send
|
||||
echo "Refactor this code" | cline -y
|
||||
|
||||
Instance Management
|
||||
Manage multiple Cline instances:
|
||||
|
||||
# Start a new instance and make it default
|
||||
cline instance new --default
|
||||
|
||||
# List all running instances
|
||||
cline instance list
|
||||
|
||||
# Kill a specific instance
|
||||
cline instance kill localhost:50052
|
||||
|
||||
# Kill all CLI instances
|
||||
cline instance kill --all-cli
|
||||
|
||||
Task History
|
||||
Work with task history:
|
||||
|
||||
# List previous tasks
|
||||
cline task list
|
||||
|
||||
# Resume a previous task
|
||||
cline task open 1760501486669
|
||||
|
||||
# View conversation history
|
||||
cline task view
|
||||
|
||||
# Start interactive chat with this task
|
||||
cline task chat
|
||||
|
||||
ARCHITECTURE
|
||||
Cline operates on a three-layer architecture:
|
||||
|
||||
Presentation Layer
|
||||
User interfaces (CLI, VSCode, JetBrains) that connect to Cline
|
||||
Core via gRPC
|
||||
|
||||
Cline Core
|
||||
The autonomous agent service handling task management, AI model
|
||||
integration, state management, tool orchestration, and real-time
|
||||
streaming updates
|
||||
|
||||
Host Provider Layer
|
||||
Environment-specific integrations (VSCode APIs, JetBrains APIs,
|
||||
shell APIs) that Cline Core uses to interact with the host
|
||||
system
|
||||
|
||||
BUGS
|
||||
Report bugs at: <https://github.com/cline/cline/issues>
|
||||
|
||||
For real-time help, join the Discord community at:
|
||||
<https://discord.gg/cline>
|
||||
|
||||
SEE ALSO
|
||||
Full documentation: <https://docs.cline.bot>
|
||||
|
||||
AUTHORS
|
||||
Cline is developed by the Cline Bot Inc. and the open source community.
|
||||
|
||||
COPYRIGHT
|
||||
Copyright © 2025 Cline Bot Inc. Licensed under the Apache License 2.0.
|
||||
```bash
|
||||
cline [prompt] [options]
|
||||
cline <command> [options] [arguments]
|
||||
```
|
||||
|
||||
## JSON output (-F json)
|
||||
## Global Options
|
||||
|
||||
When you run a command with `-F json` (or `--output-format json`), Cline prints each client message as JSON.
|
||||
These options work with any command:
|
||||
|
||||
### ClineMessage schema
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--config <path>` | Use a custom configuration directory instead of `~/.cline/data/` |
|
||||
| `-c, --cwd <path>` | Set the working directory for the task |
|
||||
| `-v, --verbose` | Show detailed output including model reasoning |
|
||||
| `--help` | Show help for the command |
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `type` | `"ask" or "say"` | Yes | Top-level message category. |
|
||||
| `text` | `string` | Yes | Human-readable message content. |
|
||||
| `ts` | `number` | Yes | Unix epoch timestamp in milliseconds. |
|
||||
| `reasoning` | `string` | No | Omitted when empty. |
|
||||
| `say` | `string` | No | Omitted when empty. Present when `type` is `"say"`. |
|
||||
| `ask` | `string` | No | Omitted when empty. Present when `type` is `"ask"`. |
|
||||
| `partial` | `boolean` | No | Omitted when false. `true` for streaming updates. |
|
||||
| `images` | `string[]` | No | Omitted when empty. Image URIs when included with a message. |
|
||||
| `files` | `string[]` | No | Omitted when empty. File paths when attached to a message. |
|
||||
| `lastCheckpointHash` | `string` | No | Omitted when empty. Git checkpoint hash when available. |
|
||||
| `isCheckpointCheckedOut` | `boolean` | No | Omitted when false. `true` if Cline checked out a checkpoint. |
|
||||
| `isOperationOutsideWorkspace` | `boolean` | No | Omitted when false. `true` if an operation happened outside the workspace. |
|
||||
## Modes of Operation
|
||||
|
||||
<Note>
|
||||
Most fields are optional and omitted when empty. If you parse this output, treat missing fields as “not present”, not as empty strings.
|
||||
</Note>
|
||||
Cline CLI automatically detects the best output mode based on how you invoke it:
|
||||
|
||||
### Example
|
||||
| Mode | When Activated | Description |
|
||||
|------|----------------|-------------|
|
||||
| **Interactive** | `cline` with no args, TTY connected | Rich terminal UI with real-time streaming, keyboard shortcuts, and visual feedback. |
|
||||
| **Task** | `cline "prompt"` with TTY connected | Interactive UI starts immediately with your task. |
|
||||
| **Plain Text** | stdin piped, stdout redirected, or `--yolo`/`--json` flags | Clean text output without UI, suitable for scripting and CI/CD. |
|
||||
|
||||
## Agent Behavior
|
||||
|
||||
Cline operates in two primary modes that control how it approaches tasks:
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| **Act Mode** (default) | Cline actively uses tools to accomplish tasks. It can read files, write code, execute commands, use a headless browser, and more. |
|
||||
| **Plan Mode** | Cline gathers information and creates a detailed plan before implementation. It explores the codebase, asks clarifying questions, and presents a strategy for your approval before switching to Act Mode. |
|
||||
|
||||
Use `-a, --act` or `-p, --plan` flags to explicitly set the mode.
|
||||
|
||||
## Commands
|
||||
|
||||
### cline (default)
|
||||
|
||||
Run Cline without a subcommand to start a task or enter interactive mode.
|
||||
|
||||
```bash
|
||||
# Interactive mode (no arguments)
|
||||
cline
|
||||
|
||||
# Start a task directly
|
||||
cline "your prompt here"
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-a, --act` | Start in Act mode (default). Cline executes actions directly. |
|
||||
| `-p, --plan` | Start in Plan mode. Cline analyzes and creates a strategy before acting. |
|
||||
| `-y, --yolo` | YOLO mode: auto-approve all actions, use plain text output, exit when complete. Ideal for CI/CD. |
|
||||
| `-m, --model <id>` | Use a specific model (e.g., `claude-sonnet-4-5-20250929`, `gpt-4o`). |
|
||||
| `-i, --images <paths...>` | Include image files with the prompt. |
|
||||
| `--thinking` | Enable extended thinking with a 1024 token budget. |
|
||||
| `--json` | Output messages as JSON (one object per line). Forces plain text mode. |
|
||||
| `--timeout <seconds>` | Maximum execution time before the task is stopped. |
|
||||
|
||||
**Mode Behavior:**
|
||||
|
||||
| Invocation | Output Mode | Why |
|
||||
|------------|-------------|-----|
|
||||
| `cline` | Interactive UI | No arguments, TTY connected |
|
||||
| `cline "prompt"` | Interactive UI | TTY connected |
|
||||
| `cline -y "prompt"` | Plain text | YOLO flag forces plain text |
|
||||
| `cline --json "prompt"` | JSON | JSON flag forces plain text |
|
||||
| `cat file \| cline "prompt"` | Plain text | stdin is piped |
|
||||
| `cline "prompt" > out.txt` | Plain text | stdout is redirected |
|
||||
|
||||
---
|
||||
|
||||
### cline task (alias: t)
|
||||
|
||||
Run a task with a prompt. This is equivalent to `cline "prompt"`.
|
||||
|
||||
```bash
|
||||
cline task "Create a REST API endpoint"
|
||||
cline t "Fix the bug in utils.js"
|
||||
```
|
||||
|
||||
**Options:** Same as the default command above.
|
||||
|
||||
---
|
||||
|
||||
### cline auth
|
||||
|
||||
Configure authentication with an AI provider.
|
||||
|
||||
```bash
|
||||
# Interactive wizard
|
||||
cline auth
|
||||
|
||||
# Quick setup with flags
|
||||
cline auth -p anthropic -k sk-ant-api-xxxxx -m claude-sonnet-4-5-20250929
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-p, --provider <id>` | Provider ID. See [Supported Providers](#supported-providers) below. |
|
||||
| `-k, --apikey <key>` | API key for the provider. |
|
||||
| `-m, --modelid <id>` | Model ID to use (e.g., `claude-sonnet-4-5-20250929`, `gpt-4o`). |
|
||||
| `-b, --baseurl <url>` | Base URL for OpenAI-compatible providers. |
|
||||
|
||||
**Supported Providers:**
|
||||
|
||||
| Provider ID | Description |
|
||||
|-------------|-------------|
|
||||
| `anthropic` | Anthropic Claude (direct API) |
|
||||
| `openai-native` | OpenAI GPT models |
|
||||
| `openai-codex` | ChatGPT subscription via OAuth |
|
||||
| `openrouter` | OpenRouter (access multiple providers) |
|
||||
| `bedrock` | AWS Bedrock |
|
||||
| `gemini` | Google Gemini |
|
||||
| `xai` | X AI (Grok) |
|
||||
| `cerebras` | Cerebras (fast inference) |
|
||||
| `deepseek` | DeepSeek |
|
||||
| `ollama` | Ollama (local models) |
|
||||
| `lmstudio` | LM Studio (local models) |
|
||||
| `openai` | OpenAI-compatible API (custom base URL) |
|
||||
|
||||
---
|
||||
|
||||
### cline history (alias: h)
|
||||
|
||||
Browse task history with pagination.
|
||||
|
||||
```bash
|
||||
# Show recent tasks (default: 10)
|
||||
cline history
|
||||
|
||||
# Show more tasks
|
||||
cline history -n 20
|
||||
|
||||
# Paginate through history
|
||||
cline history -n 10 -p 2
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-n, --limit <number>` | Number of tasks to show (default: 10) |
|
||||
| `-p, --page <number>` | Page number, 1-based (default: 1) |
|
||||
|
||||
---
|
||||
|
||||
### cline config
|
||||
|
||||
View and manage configuration settings.
|
||||
|
||||
```bash
|
||||
cline config
|
||||
```
|
||||
|
||||
Opens an interactive configuration view with tabs for:
|
||||
- **Settings** - Global and workspace-specific settings
|
||||
- **Rules** - `.clinerules` files and imported rules
|
||||
- **Workflows** - Available workflows (appear as slash commands)
|
||||
- **Hooks** - Configured hook scripts
|
||||
- **Skills** - Enabled skills
|
||||
|
||||
---
|
||||
|
||||
### cline update
|
||||
|
||||
Check for updates and install the latest version.
|
||||
|
||||
```bash
|
||||
cline update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### cline version
|
||||
|
||||
Show the installed CLI version.
|
||||
|
||||
```bash
|
||||
cline version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### cline dev
|
||||
|
||||
Developer tools for debugging.
|
||||
|
||||
```bash
|
||||
# Open the log file
|
||||
cline dev log
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### CLINE_DIR
|
||||
|
||||
Override the default configuration directory:
|
||||
|
||||
```bash
|
||||
export CLINE_DIR=/path/to/custom/config
|
||||
cline "your task"
|
||||
```
|
||||
|
||||
When set, all Cline data (settings, secrets, task history) is stored in this directory instead of `~/.cline/data/`.
|
||||
|
||||
**Use cases:**
|
||||
- Running isolated Cline instances with different settings
|
||||
- CI/CD environments with custom state directories
|
||||
- Testing configuration changes without affecting your main setup
|
||||
|
||||
### CLINE_COMMAND_PERMISSIONS
|
||||
|
||||
Restrict which shell commands Cline can execute:
|
||||
|
||||
```bash
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"], "deny": ["rm -rf *"]}'
|
||||
```
|
||||
|
||||
**Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"allow": ["pattern1", "pattern2"],
|
||||
"deny": ["pattern3"],
|
||||
"allowRedirects": true
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `allow` | `string[]` | Glob patterns for allowed commands. If set, **only** matching commands are permitted. |
|
||||
| `deny` | `string[]` | Glob patterns for denied commands. Deny rules **always take precedence** over allow rules. |
|
||||
| `allowRedirects` | `boolean` | Whether to allow shell redirects (`>`, `>>`, `<`). Default: `false`. |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Allow only npm and git commands (deny everything else)
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
|
||||
|
||||
# Allow dev commands but explicitly deny dangerous ones
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
|
||||
|
||||
# Allow file reading with redirects
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
|
||||
```
|
||||
|
||||
**How commands are evaluated:**
|
||||
|
||||
1. Check for dangerous characters (backticks outside single quotes, unquoted newlines)
|
||||
2. Parse command into segments split by operators (`&&`, `||`, `|`, `;`)
|
||||
3. If redirects are 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
|
||||
|
||||
## JSON Output Format
|
||||
|
||||
When using `--json`, each message is output as a JSON object (one per line):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "say",
|
||||
"text": "Cline is about to run a command.",
|
||||
"text": "I'll create the file now.",
|
||||
"ts": 1760501486669,
|
||||
"say": "command",
|
||||
"partial": false
|
||||
"say": "text"
|
||||
}
|
||||
```
|
||||
|
||||
### Shell Completion
|
||||
**Required fields:**
|
||||
|
||||
Generate autocompletion scripts for various shells:
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `type` | `"ask"` \| `"say"` | Message category |
|
||||
| `text` | `string` | Human-readable message content |
|
||||
| `ts` | `number` | Unix timestamp in milliseconds |
|
||||
|
||||
#### Bash
|
||||
**Optional fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `say` | `string` | Subtype when `type` is `"say"` (e.g., `"text"`, `"tool"`) |
|
||||
| `ask` | `string` | Subtype when `type` is `"ask"` (e.g., `"tool"`, `"followup"`) |
|
||||
| `reasoning` | `string` | Model reasoning (omitted when empty) |
|
||||
| `partial` | `boolean` | `true` while streaming (omitted when complete) |
|
||||
| `images` | `string[]` | Image URIs (omitted when empty) |
|
||||
| `files` | `string[]` | File paths (omitted when empty) |
|
||||
|
||||
## Configuration Files
|
||||
|
||||
Cline stores all data in `~/.cline/` by default:
|
||||
|
||||
```
|
||||
~/.cline/
|
||||
├── data/ # Configuration directory
|
||||
│ ├── globalState.json # Global settings
|
||||
│ ├── secrets.json # API keys (stored securely)
|
||||
│ ├── workspace/ # Workspace-specific state
|
||||
│ └── tasks/ # Task history and conversations
|
||||
└── log/ # Debug logs (view with cline dev log)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Interactive Development
|
||||
|
||||
```bash
|
||||
# Generate bash completion
|
||||
cline completion bash > /etc/bash_completion.d/cline
|
||||
# Start interactive mode
|
||||
cline
|
||||
|
||||
# Or for user-level installation
|
||||
cline completion bash > ~/.local/share/bash-completion/completions/cline
|
||||
# Start with a task and use interactive UI
|
||||
cline "Help me refactor this codebase"
|
||||
```
|
||||
|
||||
#### Zsh
|
||||
### Direct Task Execution
|
||||
|
||||
```bash
|
||||
# Generate zsh completion
|
||||
cline completion zsh > "${fpath[1]}/_cline"
|
||||
# Run a task directly
|
||||
cline "Add error handling to utils.js"
|
||||
|
||||
# Or add to your .zshrc
|
||||
echo 'source <(cline completion zsh)' >> ~/.zshrc
|
||||
# Start in Plan mode to review strategy first
|
||||
cline -p "Design a caching layer for the API"
|
||||
|
||||
# Use a specific model
|
||||
cline -m gpt-4o "Explain this code"
|
||||
```
|
||||
|
||||
#### Fish
|
||||
### Piped Input
|
||||
|
||||
```bash
|
||||
# Generate fish completion
|
||||
cline completion fish > ~/.config/fish/completions/cline.fish
|
||||
# Pipe file contents
|
||||
cat README.md | cline "Summarize this document"
|
||||
|
||||
# Review git changes
|
||||
git diff | cline "Review these changes"
|
||||
|
||||
# Analyze test output
|
||||
npm test 2>&1 | cline "Fix any failing tests"
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
```powershell
|
||||
# Generate PowerShell completion
|
||||
cline completion powershell > cline.ps1
|
||||
|
||||
# Add to your PowerShell profile
|
||||
Add-Content $PROFILE "cline completion powershell | Out-String | Invoke-Expression"
|
||||
```
|
||||
|
||||
### Version Command
|
||||
### Automation and CI/CD
|
||||
|
||||
```bash
|
||||
# Show version information
|
||||
cline version
|
||||
# YOLO mode for automated workflows
|
||||
cline -y "Run tests and fix failures"
|
||||
|
||||
# JSON output for scripting
|
||||
cline --json "List all TODO comments" | jq '.text'
|
||||
|
||||
# With timeout
|
||||
cline -y --timeout 600 "Run the full test suite"
|
||||
|
||||
# Chain commands
|
||||
git diff | cline -y "explain" | cline -y "write a commit message"
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### CLINE_DIR
|
||||
|
||||
Override the default Cline directory location:
|
||||
### Authentication
|
||||
|
||||
```bash
|
||||
# Override default Cline directory
|
||||
export CLINE_DIR=/custom/path
|
||||
# Interactive wizard
|
||||
cline auth
|
||||
|
||||
# Default: ~/.cline
|
||||
# Quick setup: Anthropic
|
||||
cline auth -p anthropic -k sk-ant-api-xxxxx -m claude-sonnet-4-5-20250929
|
||||
|
||||
# Quick setup: OpenAI
|
||||
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
|
||||
|
||||
# Quick setup: OpenRouter
|
||||
cline auth -p openrouter -k sk-or-xxxxx
|
||||
|
||||
# OpenAI-compatible with custom URL
|
||||
cline auth -p openai -k your-key -b https://api.example.com/v1
|
||||
```
|
||||
|
||||
This directory is used for:
|
||||
- Instance registry database
|
||||
- Configuration files
|
||||
- Task history
|
||||
- Checkpoints
|
||||
## Support
|
||||
|
||||
- **Report bugs:** https://github.com/cline/cline/issues
|
||||
- **Discord community:** https://discord.gg/cline
|
||||
- **Documentation:** https://docs.cline.bot
|
||||
|
||||
## See Also
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="Installation & Setup" icon="download" href="/cline-cli/installation">
|
||||
Install Cline CLI and configure authentication.
|
||||
</Card>
|
||||
|
||||
<Card title="Interactive Mode" icon="terminal" href="/cline-cli/interactive-mode">
|
||||
Keyboard shortcuts, slash commands, and file mentions.
|
||||
</Card>
|
||||
|
||||
<Card title="CLI Workflows" icon="route" href="/cline-cli/three-core-flows">
|
||||
Interactive mode, direct execution, and automation patterns.
|
||||
</Card>
|
||||
|
||||
<Card title="Configuration" icon="gear" href="/cline-cli/configuration">
|
||||
Environment variables and advanced settings.
|
||||
</Card>
|
||||
</Columns>
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
description: "Manage Cline CLI settings with cline config, environment variables, and configuration files"
|
||||
---
|
||||
|
||||
Cline CLI provides multiple ways to configure settings, from the interactive `cline config` command to environment variables for automation.
|
||||
|
||||
## The Config Command
|
||||
|
||||
Launch the configuration interface:
|
||||
|
||||
```bash
|
||||
cline config
|
||||
```
|
||||
|
||||
This opens an interactive view with tabs for different configuration categories.
|
||||
|
||||
## Configuration Tabs
|
||||
|
||||
Navigate between tabs using arrow keys.
|
||||
|
||||
### Settings Tab
|
||||
|
||||
View and edit global and workspace-specific settings:
|
||||
|
||||
- **Global State**: Settings that apply across all workspaces
|
||||
- **Workspace State**: Settings specific to the current directory
|
||||
|
||||
### Rules Tab
|
||||
|
||||
Manage Cline rules that guide AI behavior:
|
||||
|
||||
- **`.clinerules` files**: Project-specific rules in your workspace
|
||||
- **Cursor rules**: Import rules from Cursor editor format
|
||||
- **Windsurf rules**: Import rules from Windsurf editor format
|
||||
|
||||
Rules help Cline understand your project's conventions, coding standards, and preferences.
|
||||
|
||||
### Workflows Tab
|
||||
|
||||
View and manage [workflows](/features/slash-commands/workflows/index):
|
||||
|
||||
- List available workflows
|
||||
- View workflow definitions
|
||||
- Workflows appear as slash commands in interactive mode
|
||||
|
||||
### Hooks Tab
|
||||
|
||||
Configure [hooks](/features/hooks/index) for custom logic integration:
|
||||
|
||||
- Enable/disable hooks globally
|
||||
- View configured hook scripts
|
||||
- Hooks run at key points in Cline's workflow
|
||||
|
||||
<Note>
|
||||
Hooks must be enabled via settings. Use `cline config` to toggle `hooks-enabled`.
|
||||
</Note>
|
||||
|
||||
### Skills Tab
|
||||
|
||||
Manage [skills](/features/skills) that extend Cline's capabilities:
|
||||
|
||||
- View available skills
|
||||
- Enable/disable specific skills
|
||||
- Skills provide specialized instructions for specific tasks
|
||||
|
||||
## Configuration Directory
|
||||
|
||||
Cline stores configuration in `~/.cline/data/`:
|
||||
|
||||
```
|
||||
~/.cline/
|
||||
├── data/ # Configuration directory
|
||||
│ ├── globalState.json # Global settings
|
||||
│ ├── secrets.json # API keys (encrypted)
|
||||
│ ├── workspace/ # Workspace-specific state
|
||||
│ └── tasks/ # Task history and data
|
||||
└── log/ # Log files
|
||||
```
|
||||
|
||||
### Viewing Logs
|
||||
|
||||
For debugging, view the log file:
|
||||
|
||||
```bash
|
||||
cline dev log
|
||||
```
|
||||
|
||||
This opens the log file in your default editor.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### CLINE_DIR
|
||||
|
||||
Override the default configuration directory:
|
||||
|
||||
```bash
|
||||
export CLINE_DIR=/custom/path/to/cline
|
||||
cline "your task"
|
||||
```
|
||||
|
||||
When set, all Cline data is stored in this directory instead of `~/.cline/data/`.
|
||||
|
||||
**Use cases:**
|
||||
- Running multiple isolated Cline configurations
|
||||
- Team-shared configurations
|
||||
- CI/CD with custom state directories
|
||||
|
||||
### CLINE_COMMAND_PERMISSIONS
|
||||
|
||||
Restrict which shell commands Cline can execute:
|
||||
|
||||
```bash
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"], "deny": ["rm -rf *"]}'
|
||||
```
|
||||
|
||||
**Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"allow": ["pattern1", "pattern2"],
|
||||
"deny": ["pattern3"],
|
||||
"allowRedirects": true
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `allow` | `string[]` | Glob patterns for allowed commands. If set, only matching commands are permitted. |
|
||||
| `deny` | `string[]` | Glob patterns for denied commands. Deny rules take precedence over allow. |
|
||||
| `allowRedirects` | `boolean` | Whether to allow shell redirects (`>`, `>>`, `<`). Default: `false` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Allow only npm and git commands
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
|
||||
|
||||
# Allow dev commands but deny dangerous ones
|
||||
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}'
|
||||
```
|
||||
|
||||
<Warning>
|
||||
When `allow` is set, all commands not matching the allow patterns are denied. Use this for security-sensitive environments.
|
||||
</Warning>
|
||||
|
||||
## Using --config Flag
|
||||
|
||||
Run Cline with a custom configuration directory:
|
||||
|
||||
```bash
|
||||
cline --config /path/to/custom/config "your task"
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
- Running isolated Cline instances
|
||||
- Testing different configurations
|
||||
- Separating work and personal setups
|
||||
|
||||
**Example: Multiple configurations**
|
||||
|
||||
```bash
|
||||
# Work configuration
|
||||
cline --config ~/.cline-work "review this PR"
|
||||
|
||||
# Personal projects
|
||||
cline --config ~/.cline-personal "help me with this side project"
|
||||
```
|
||||
|
||||
## Configuration for Local Providers
|
||||
|
||||
### Ollama
|
||||
|
||||
Configure context window size for Ollama:
|
||||
|
||||
```bash
|
||||
# In settings or via config
|
||||
cline config
|
||||
# Navigate to Settings tab, find ollama-api-options-ctx-num
|
||||
```
|
||||
|
||||
Or set via environment:
|
||||
|
||||
```bash
|
||||
# Set context window to 32K tokens
|
||||
cline -m ollama/llama3 "your task"
|
||||
```
|
||||
|
||||
### LM Studio
|
||||
|
||||
Configure max tokens for LM Studio:
|
||||
|
||||
```bash
|
||||
cline config
|
||||
# Navigate to Settings tab, find lm-studio-max-tokens
|
||||
```
|
||||
|
||||
## Importing Configuration
|
||||
|
||||
### From VS Code Extension
|
||||
|
||||
If you use the Cline VS Code extension, the CLI automatically detects and can share some settings. However, the CLI maintains its own configuration for terminal-specific features.
|
||||
|
||||
### From Other CLI Tools
|
||||
|
||||
See [Installation & Setup](/cline-cli/installation#option-3-import-from-existing-tools) for importing configurations from:
|
||||
- Codex CLI
|
||||
- OpenCode
|
||||
|
||||
## Configuration Best Practices
|
||||
|
||||
### For Development
|
||||
|
||||
Use the default configuration with workspace-specific rules:
|
||||
|
||||
```bash
|
||||
# Add project-specific rules
|
||||
echo "Use TypeScript strict mode" > .clinerules/typescript.md
|
||||
```
|
||||
|
||||
### For CI/CD
|
||||
|
||||
Use environment variables and `--yolo` mode:
|
||||
|
||||
```bash
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm test", "npm run build"]}'
|
||||
cline -y "run tests and fix any failures"
|
||||
```
|
||||
|
||||
### For Teams
|
||||
|
||||
Share configuration via version control:
|
||||
|
||||
```bash
|
||||
# Commit .clinerules/ to your repo
|
||||
git add .clinerules/
|
||||
git commit -m "Add Cline rules for team"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Configuration Not Persisting
|
||||
|
||||
1. Check write permissions on `~/.cline/data/`
|
||||
2. Ensure `CLINE_DIR` isn't set to a read-only location
|
||||
3. Verify the config directory exists
|
||||
|
||||
### Environment Variables Not Working
|
||||
|
||||
1. Ensure variables are exported: `export CLINE_DIR=/path`
|
||||
2. Check for typos in variable names
|
||||
3. Verify JSON syntax for `CLINE_COMMAND_PERMISSIONS`
|
||||
|
||||
### Reset Configuration
|
||||
|
||||
To start fresh, remove the configuration directory:
|
||||
|
||||
```bash
|
||||
rm -rf ~/.cline/data/
|
||||
cline auth # Re-authenticate
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="CLI Workflows" icon="route" href="/cline-cli/three-core-flows">
|
||||
Learn about interactive mode, direct execution, and automation patterns.
|
||||
</Card>
|
||||
|
||||
<Card title="CLI Reference" icon="terminal" href="/cline-cli/cli-reference">
|
||||
Complete command documentation with all flags and options.
|
||||
</Card>
|
||||
</Columns>
|
||||
+239
-15
@@ -1,50 +1,274 @@
|
||||
---
|
||||
title: "Installation"
|
||||
description: "Install Cline CLI and authenticate with your account"
|
||||
title: "Installation & Setup"
|
||||
description: "Install Cline CLI on macOS, Linux, or Windows and configure your AI provider"
|
||||
---
|
||||
|
||||
Cline CLI brings the full power of Cline to your terminal. In just a few minutes, you can install the CLI, authenticate with your preferred AI provider, and start running tasks from any directory on your machine.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Cline CLI requires Node.js version 20 or higher. We recommend using Node.js 22 for the best experience.
|
||||
Cline CLI requires **Node.js version 20 or higher**. We recommend Node.js 22 for the best experience.
|
||||
|
||||
To check your Node.js version:
|
||||
Check your Node.js version:
|
||||
|
||||
```bash
|
||||
node --version
|
||||
```
|
||||
|
||||
## Installation
|
||||
If you need to install or update Node.js, visit [nodejs.org](https://nodejs.org) or use a version manager like [nvm](https://github.com/nvm-sh/nvm).
|
||||
|
||||
## Install Cline CLI
|
||||
|
||||
Install globally via npm:
|
||||
|
||||
```bash
|
||||
npm install -g cline
|
||||
```
|
||||
|
||||
After installation, authenticate with your Cline account:
|
||||
Verify the installation:
|
||||
|
||||
```bash
|
||||
cline version
|
||||
```
|
||||
|
||||
<Tip>
|
||||
To install a specific version, use `npm install -g cline@2.0.0`. Check [npm](https://www.npmjs.com/package/cline) for available versions.
|
||||
</Tip>
|
||||
|
||||
## Authenticate
|
||||
|
||||
After installation, run the authentication wizard:
|
||||
|
||||
```bash
|
||||
cline auth
|
||||
```
|
||||
|
||||
This starts an authentication wizard to sign you in and configure your preferred AI model provider.
|
||||
This launches an interactive wizard with multiple options. Choose the method that works best for your workflow.
|
||||
|
||||
### Option 1: Sign in with Cline (Recommended)
|
||||
|
||||
Select **"Sign in with Cline"** to authenticate with your Cline account via OAuth. Your browser opens automatically to complete sign-in.
|
||||
|
||||
### Option 2: Sign in with ChatGPT Subscription
|
||||
|
||||
If you have a ChatGPT Plus or Pro subscription, select **"Sign in with ChatGPT Subscription"**. This uses OpenAI's Codex OAuth to authenticate with your existing subscription.
|
||||
|
||||
### Option 3: Import from Existing Tools
|
||||
|
||||
Already using another AI coding CLI? Cline can import your existing configuration:
|
||||
|
||||
- **Import from Codex CLI** - Imports credentials from `~/.codex/auth.json`
|
||||
- **Import from OpenCode** - Imports configuration from `~/.local/share/opencode/auth.json`
|
||||
|
||||
### Option 4: Bring Your Own API Key
|
||||
|
||||
Select **"Bring your own API key"** to manually configure any supported provider. Or skip the wizard entirely with flags:
|
||||
|
||||
```bash
|
||||
# Anthropic (Claude)
|
||||
cline auth -p anthropic -k sk-ant-api-xxxxx -m claude-sonnet-4-5-20250929
|
||||
|
||||
# OpenAI
|
||||
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
|
||||
|
||||
# OpenRouter
|
||||
cline auth -p openrouter -k sk-or-xxxxx -m anthropic/claude-sonnet-4-5-20250929
|
||||
|
||||
# OpenAI-compatible provider with custom base URL
|
||||
cline auth -p openai -k your-api-key -b https://api.example.com/v1
|
||||
```
|
||||
|
||||
**Quick Setup Flags:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-p, --provider <id>` | Provider ID (e.g., `anthropic`, `openai-native`, `openrouter`) |
|
||||
| `-k, --apikey <key>` | Your API key |
|
||||
| `-m, --modelid <id>` | Model ID (e.g., `claude-sonnet-4-5-20250929`, `gpt-4o`) |
|
||||
| `-b, --baseurl <url>` | Base URL for OpenAI-compatible providers |
|
||||
|
||||
<Tip>
|
||||
Flags are especially useful for scripting, CI/CD environments, or setting up multiple machines.
|
||||
</Tip>
|
||||
|
||||
### Supported Providers
|
||||
|
||||
| Provider | Provider ID | Notes |
|
||||
|----------|-------------|-------|
|
||||
| Anthropic | `anthropic` | Direct Claude API access |
|
||||
| OpenAI | `openai-native` | GPT-4o, GPT-4, etc. |
|
||||
| OpenAI Codex | `openai-codex` | ChatGPT subscription OAuth |
|
||||
| OpenRouter | `openrouter` | Access multiple providers |
|
||||
| AWS Bedrock | `bedrock` | Claude via AWS |
|
||||
| Google Gemini | `gemini` | Gemini Pro, etc. |
|
||||
| X AI (Grok) | `xai` | Grok models |
|
||||
| Cerebras | `cerebras` | Fast inference |
|
||||
| DeepSeek | `deepseek` | DeepSeek models |
|
||||
| Ollama | `ollama` | Local models |
|
||||
| LM Studio | `lmstudio` | Local models |
|
||||
| OpenAI Compatible | `openai` | Any OpenAI-compatible API |
|
||||
|
||||
## Verify Your Setup
|
||||
|
||||
Confirm everything is working with a simple test:
|
||||
|
||||
```bash
|
||||
cline "What is 2 + 2?"
|
||||
```
|
||||
|
||||
If Cline responds with an answer, your installation and authentication are complete.
|
||||
|
||||
Check your current configuration:
|
||||
|
||||
```bash
|
||||
cline config
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Get started with Cline in seconds:
|
||||
Now you're ready to use Cline. Choose how you want to work:
|
||||
|
||||
### Interactive Mode
|
||||
|
||||
Launch the interactive CLI for development:
|
||||
|
||||
```bash
|
||||
cline
|
||||
```
|
||||
|
||||
That's it! Running `cline` in any directory starts an interactive session where you can chat with the AI agent. Type your task, review the plan, and type `/act` when ready to execute.
|
||||
You'll see the Cline welcome screen. Type your task and press Enter. Use:
|
||||
- `Tab` to toggle between Plan and Act modes
|
||||
- `Shift+Tab` to enable auto-approve
|
||||
- `/help` for available commands
|
||||
|
||||
For even faster execution without interaction:
|
||||
[Learn more about interactive mode →](/cline-cli/interactive-mode)
|
||||
|
||||
### Direct Task Execution
|
||||
|
||||
Run a task directly from your shell:
|
||||
|
||||
```bash
|
||||
cline "Add unit tests to utils.js"
|
||||
cline "Add error handling to utils.js"
|
||||
```
|
||||
|
||||
This runs Cline with a single command, perfect for quick tasks or automation.
|
||||
For non-interactive execution (perfect for scripts and CI/CD):
|
||||
|
||||
<Tip>
|
||||
New to Cline CLI? Start with interactive mode (`cline`) to see how it works. Once comfortable, explore [the three core flows](/cline-cli/three-core-flows) for advanced usage patterns.
|
||||
</Tip>
|
||||
```bash
|
||||
cline -y "Run tests and fix any failures"
|
||||
```
|
||||
|
||||
[Learn more about CLI workflows →](/cline-cli/three-core-flows)
|
||||
|
||||
## Switching Providers
|
||||
|
||||
To change your configured provider at any time:
|
||||
|
||||
```bash
|
||||
cline auth
|
||||
```
|
||||
|
||||
You can also use the settings panel in interactive mode:
|
||||
|
||||
```bash
|
||||
cline
|
||||
# Then type: /settings
|
||||
# Navigate to the API tab
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
Check for updates and install the latest version:
|
||||
|
||||
```bash
|
||||
cline update
|
||||
```
|
||||
|
||||
Or update manually via npm:
|
||||
|
||||
```bash
|
||||
npm update -g cline
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Command Not Found
|
||||
|
||||
If `cline` is not found after installation:
|
||||
|
||||
1. Ensure npm global bin is in your PATH:
|
||||
```bash
|
||||
npm bin -g
|
||||
```
|
||||
|
||||
2. Add the path to your shell configuration (`.bashrc`, `.zshrc`, etc.):
|
||||
```bash
|
||||
export PATH="$PATH:$(npm bin -g)"
|
||||
```
|
||||
|
||||
3. Restart your terminal or source your shell config.
|
||||
|
||||
### Permission Errors
|
||||
|
||||
If you get permission errors during installation:
|
||||
|
||||
```bash
|
||||
# Option 1: Use a Node version manager (recommended)
|
||||
# nvm, fnm, or volta handle permissions automatically
|
||||
|
||||
# Option 2: Fix npm permissions
|
||||
# See: https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally
|
||||
```
|
||||
|
||||
### OAuth Flow Issues
|
||||
|
||||
If the browser doesn't open automatically during OAuth:
|
||||
1. Copy the URL from the terminal
|
||||
2. Paste it in your browser manually
|
||||
3. Complete the sign-in flow
|
||||
4. Return to the terminal
|
||||
|
||||
### API Key Validation
|
||||
|
||||
If your API key is rejected:
|
||||
1. Verify the key is correct and hasn't expired
|
||||
2. Check that you've selected the correct provider
|
||||
3. Ensure your API account has the necessary permissions
|
||||
|
||||
**Provider-specific tips:**
|
||||
- **Anthropic**: Keys start with `sk-ant-`
|
||||
- **OpenAI**: Keys start with `sk-`
|
||||
- **AWS Bedrock**: Requires AWS credentials configured separately. See [AWS Bedrock documentation](/provider-config/aws-bedrock/api-key).
|
||||
|
||||
## Uninstallation
|
||||
|
||||
To remove Cline CLI:
|
||||
|
||||
```bash
|
||||
npm uninstall -g cline
|
||||
```
|
||||
|
||||
To also remove configuration data:
|
||||
|
||||
```bash
|
||||
rm -rf ~/.cline
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="Interactive Mode" icon="terminal" href="/cline-cli/interactive-mode">
|
||||
Master the interactive CLI with shortcuts and slash commands.
|
||||
</Card>
|
||||
|
||||
<Card title="CLI Workflows" icon="route" href="/cline-cli/three-core-flows">
|
||||
Learn interactive mode, direct execution, and automation patterns.
|
||||
</Card>
|
||||
|
||||
<Card title="Configuration" icon="gear" href="/cline-cli/configuration">
|
||||
Configure settings, rules, workflows, and environment variables.
|
||||
</Card>
|
||||
|
||||
<Card title="CLI Reference" icon="book" href="/cline-cli/cli-reference">
|
||||
Complete command documentation with all flags and options.
|
||||
</Card>
|
||||
</Columns>
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
---
|
||||
title: "Interactive Mode"
|
||||
description: "Master the interactive CLI with keyboard shortcuts, slash commands, and file mentions"
|
||||
---
|
||||
|
||||
Interactive mode is the primary way to work with Cline CLI when you want a collaborative, conversational experience. Unlike headless mode (which runs a single task and exits), interactive mode keeps a session open where you can have back-and-forth conversations with Cline, refine your requests, and guide the AI as it works.
|
||||
|
||||
## Why Use Interactive Mode?
|
||||
|
||||
Interactive mode is ideal when you:
|
||||
|
||||
- **Don't know exactly what you need yet** - Explore a codebase, ask questions, and let Cline help you understand the architecture before making changes
|
||||
- **Want to review before acting** - Toggle Plan mode to see Cline's strategy, then switch to Act mode when you're ready
|
||||
- **Need iterative refinement** - Build on previous responses, ask follow-up questions, and guide Cline to the right solution
|
||||
- **Prefer human oversight** - Review each action, approve file changes, and maintain control over what Cline does
|
||||
- **Working on complex tasks** - Multi-step refactoring, debugging sessions, or feature development that requires judgment calls
|
||||
|
||||
For automated workflows, scripts, or CI/CD pipelines, see [headless mode](/cline-cli/overview#headless-mode-non-interactive) instead.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using interactive mode, you need to have Cline CLI installed and authenticated. If you haven't done this yet, follow the [Installation & Setup guide](/cline-cli/installation) first.
|
||||
|
||||
## Launching Interactive Mode
|
||||
|
||||
Start interactive mode by running `cline` without any arguments:
|
||||
|
||||
```bash
|
||||
cline
|
||||
```
|
||||
|
||||
You'll see an animated welcome screen with the Cline robot. Start typing your task in the input field at the bottom of the screen.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
Keyboard shortcuts are the primary way to navigate and control the interactive CLI. Since there's no mouse interaction in the terminal, learning these shortcuts will help you work efficiently and switch between modes, manage input, and control your session without breaking your flow.
|
||||
|
||||
### Mode Controls
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Tab` | Toggle between Plan and Act mode |
|
||||
| `Shift+Tab` | Toggle auto-approve all actions |
|
||||
| `Esc` | Exit or cancel current operation |
|
||||
|
||||
### Input Controls
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Enter` | Submit your message |
|
||||
| `↑` / `↓` | Navigate message history |
|
||||
| `Home` / `End` | Move cursor to start/end of line |
|
||||
| `Ctrl+A` | Move cursor to beginning |
|
||||
| `Ctrl+E` | Move cursor to end |
|
||||
| `Ctrl+W` | Delete word before cursor |
|
||||
| `Ctrl+U` | Delete entire line |
|
||||
|
||||
### Session Controls
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Ctrl+C` | Exit with session summary |
|
||||
|
||||
## File Mentions with @
|
||||
|
||||
Reference files from your workspace by typing `@` followed by the filename:
|
||||
|
||||
```
|
||||
@src/utils.ts can you add error handling to this file?
|
||||
```
|
||||
|
||||
As you type after `@`, Cline shows a fuzzy search dropdown of matching files. Use arrow keys to navigate and `Enter` to select.
|
||||
|
||||
<Tip>
|
||||
File search uses ripgrep for fast, fuzzy matching. You can type partial paths like `@utils` to find `src/utils/helpers.ts`.
|
||||
</Tip>
|
||||
|
||||
### Multiple File Mentions
|
||||
|
||||
Include multiple files in a single message:
|
||||
|
||||
```
|
||||
Compare @src/old-api.ts with @src/new-api.ts and list the breaking changes
|
||||
```
|
||||
|
||||
## Slash Commands
|
||||
|
||||
Type `/` to see available commands. Slash commands provide quick access to settings, history, and workflows.
|
||||
|
||||
### Built-in Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/settings` | Open the settings panel |
|
||||
| `/models` | Quick model switching |
|
||||
| `/history` | Browse and resume previous tasks |
|
||||
| `/clear` | Start a fresh task (clears current conversation) |
|
||||
| `/help` | Show help and available commands |
|
||||
| `/exit` | Exit the CLI |
|
||||
|
||||
### Workflow Commands
|
||||
|
||||
If you have [workflows](/features/slash-commands/workflows/index) configured, they appear as additional slash commands. For example, if you have a workflow named `code-review`, you can invoke it with:
|
||||
|
||||
```
|
||||
/code-review
|
||||
```
|
||||
|
||||
## Settings Panel
|
||||
|
||||
Access the settings panel with `/settings`. Navigate between tabs using arrow keys.
|
||||
|
||||
| Tab | Description | Settings |
|
||||
|-----|-------------|----------|
|
||||
| **API** | Configure your AI provider and model | Provider selection, model choice, extended thinking toggle, thinking budget |
|
||||
| **Auto-approve** | Control which actions Cline can perform without prompting | Read files, write files, execute commands, browser actions, MCP tools |
|
||||
| **Features** | Toggle Cline capabilities | Hooks, skills, auto-compact, sound notifications |
|
||||
| **Account** | Manage your Cline account | View account status, sign in/out, manage subscription |
|
||||
| **Other** | Additional preferences | Theme preferences, debug options |
|
||||
|
||||
## Plan and Act Modes
|
||||
|
||||
Cline operates in two modes, toggled with `Tab`. These modes work the same way in the CLI as they do in the VS Code extension. For a deeper explanation of how Plan and Act modes work, see the [Plan and Act documentation](/features/plan-and-act).
|
||||
|
||||
### Plan Mode
|
||||
|
||||
In Plan mode, Cline analyzes your request and creates a strategy before making changes. Use this when:
|
||||
- Exploring a new codebase
|
||||
- Working on complex refactoring
|
||||
- You want to review the approach first
|
||||
|
||||
### Act Mode
|
||||
|
||||
In Act mode, Cline executes tasks directly. Use this when:
|
||||
- You're confident in the task
|
||||
- Making straightforward changes
|
||||
- Running quick operations
|
||||
|
||||
<Tip>
|
||||
Press `Tab` anytime to switch modes. Starting in Plan mode and switching to Act after reviewing is a common workflow.
|
||||
</Tip>
|
||||
|
||||
## Auto-approve Toggle
|
||||
|
||||
Press `Shift+Tab` to toggle auto-approve for all actions. This removes the approval prompts that appear before each action, letting Cline work continuously without interruption.
|
||||
|
||||
### When to Enable Auto-approve
|
||||
|
||||
Auto-approve is useful when:
|
||||
- **You trust the task** - Well-defined tasks where you're confident in the outcome
|
||||
- **Speed matters** - Long-running tasks where constant approvals slow you down
|
||||
- **You're watching anyway** - You can see Cline's work in real-time and can interrupt if needed
|
||||
- **Iterating quickly** - Rapid prototyping where you want to see results fast
|
||||
|
||||
### What Gets Auto-approved
|
||||
|
||||
When enabled, these actions happen without prompting:
|
||||
- File reads
|
||||
- File writes
|
||||
- Command execution
|
||||
- Browser actions
|
||||
- MCP tool calls
|
||||
|
||||
You can also configure granular auto-approve settings (e.g., auto-approve reads but not writes) via `/settings` → Auto-approve tab, or see the [Auto-approve documentation](/features/auto-approve) for more details.
|
||||
|
||||
<Warning>
|
||||
Auto-approve gives Cline full autonomy. Use on a clean git branch so you can easily revert changes if needed. You can always press `Ctrl+C` to stop Cline immediately.
|
||||
</Warning>
|
||||
|
||||
## Session Summary
|
||||
|
||||
When you exit with `Ctrl+C`, Cline displays a session summary showing:
|
||||
- Tasks completed
|
||||
- Files modified
|
||||
- Commands executed
|
||||
- Token usage
|
||||
|
||||
This helps you track what was accomplished during your session.
|
||||
|
||||
## Running Multiple Instances
|
||||
|
||||
By default, all CLI instances share the same settings and state. However, you may want to run isolated instances with separate configurations for scenarios like:
|
||||
|
||||
- **Different models for different tasks** - Use a fast, cheap model for quick questions in one terminal and a more capable model for complex refactoring in another
|
||||
- **Separate work and personal projects** - Keep API keys, rules, and task history isolated between contexts
|
||||
- **Testing configuration changes** - Experiment with new settings without affecting your main setup
|
||||
- **Team vs. individual settings** - Use shared team configuration for work projects and personal preferences for side projects
|
||||
|
||||
To run isolated instances, use the `--config` flag with different directories:
|
||||
|
||||
```bash
|
||||
# Work instance with team configuration
|
||||
cline --config ~/.cline-work
|
||||
|
||||
# Personal instance with different model/provider
|
||||
cline --config ~/.cline-personal
|
||||
|
||||
# Experimental instance for testing new settings
|
||||
cline --config ~/.cline-test
|
||||
```
|
||||
|
||||
Each config directory maintains its own provider settings, API keys, task history, and preferences.
|
||||
|
||||
<Tip>
|
||||
Use terminal multiplexers like tmux or split terminals to run multiple Cline instances in parallel, each working on different parts of your project with different models or settings.
|
||||
</Tip>
|
||||
|
||||
## Tips for Effective Usage
|
||||
|
||||
### Start with Context
|
||||
|
||||
Give Cline context about what you're working on:
|
||||
|
||||
```
|
||||
I'm building a REST API with Express. The routes are in @src/routes/ and models in @src/models/. Help me add user authentication.
|
||||
```
|
||||
|
||||
### Use Plan Mode for Exploration
|
||||
|
||||
When you're unsure about the best approach:
|
||||
|
||||
```
|
||||
[Tab to Plan mode]
|
||||
How should I structure the database schema for a multi-tenant SaaS app?
|
||||
```
|
||||
|
||||
### Iterate with Follow-ups
|
||||
|
||||
The interactive CLI maintains conversation context. Build on previous messages:
|
||||
|
||||
```
|
||||
> Add a login endpoint
|
||||
[Cline creates the endpoint]
|
||||
|
||||
> Now add rate limiting to it
|
||||
[Cline modifies the same endpoint]
|
||||
|
||||
> Add tests for both features
|
||||
[Cline creates test files]
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="CLI Workflows" icon="route" href="/cline-cli/three-core-flows">
|
||||
Learn about interactive mode, direct execution, and automation patterns.
|
||||
</Card>
|
||||
|
||||
<Card title="Configuration" icon="gear" href="/cline-cli/configuration">
|
||||
Explore `cline config` and advanced configuration options.
|
||||
</Card>
|
||||
</Columns>
|
||||
+201
-57
@@ -1,84 +1,228 @@
|
||||
---
|
||||
title: "Overview"
|
||||
description: "Install the CLI, run your first task, and learn to automate code reviews and integrate AI agents into your development workflow"
|
||||
description: "Run Cline AI coding agents directly in your terminal with an interactive CLI or automated workflows"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
**Preview Release - macOS and Linux Only**
|
||||
|
||||
Cline CLI is currently in preview and only available for macOS and Linux users. Windows support is coming soon.
|
||||
</Warning>
|
||||
|
||||
## What is Cline CLI?
|
||||
|
||||
Cline CLI runs AI coding agents directly in your terminal. Pipe git diffs for automated code reviews in CI/CD, run multiple instances simultaneously for parallel development, or integrate Cline into your existing shell workflows.
|
||||
Cline CLI brings the full power of Cline to your terminal. Whether you prefer an interactive experience or automated workflows for CI/CD pipelines, the CLI adapts to your needs.
|
||||
|
||||
The CLI tracks instances across your system and outputs in formats designed for both humans and scripts—JSON, plain text, or rich terminal output.
|
||||
The CLI supports macOS, Linux, and Windows, and works with all the same AI providers as the VS Code extension.
|
||||
|
||||
<Tip>
|
||||
Ready to get started? Check out the [installation guide](/cline-cli/installation) to install Cline CLI and run your first task.
|
||||
</Tip>
|
||||
|
||||
## Supported Model Providers
|
||||
## Two Ways to Use Cline CLI
|
||||
|
||||
Cline CLI supports multiple AI model providers, giving you flexibility in choosing the best model for your needs:
|
||||
The CLI operates in two distinct modes, automatically selecting the appropriate one based on how you invoke it:
|
||||
|
||||
- **Anthropic**
|
||||
- **OpenAI**
|
||||
- **OpenAI Compatible**
|
||||
- **OpenRouter**
|
||||
- **X AI (Grok)**
|
||||
- **AWS Bedrock**
|
||||
- **Google Gemini**
|
||||
- **Ollama**
|
||||
- **Cerebras**
|
||||
### Interactive Mode
|
||||
|
||||
During installation, you'll authenticate and configure your preferred provider using the `cline auth` command.
|
||||
Interactive mode is designed for **hands-on development sessions** where you want to collaborate with Cline in real-time. It provides a rich terminal interface that feels like chatting with an AI assistant.
|
||||
|
||||
## What you can build with this
|
||||
|
||||
**Automated code maintenance**
|
||||
- Schedule daily runs to identify and fix linting issues across your codebase
|
||||
- Create tasks that scan for security vulnerabilities and automatically patch them
|
||||
- Build scripts that update deprecated dependencies and run tests
|
||||
|
||||
**Multi-instance development**
|
||||
- Run separate Cline instances for frontend and backend simultaneously
|
||||
- Spawn instances for different feature branches, each with isolated state
|
||||
- Create parallel review processes for multiple PRs
|
||||
|
||||
**Custom workflows**
|
||||
- Build shell scripts that combine Cline with git hooks for pre-commit analysis
|
||||
- Create custom commands that pipe complex data structures through Cline for processing
|
||||
- Integrate with your existing toolchain (jq, grep, awk) for sophisticated automation
|
||||
|
||||
**CI/CD integration**
|
||||
- Add Cline to GitHub Actions for automatic code review on every PR
|
||||
- Create GitLab pipelines that generate migration scripts from schema changes
|
||||
- Build Jenkins jobs that use Cline to analyze test failures and suggest fixes
|
||||
|
||||
## Hooks integration
|
||||
|
||||
[Hooks](/features/hooks/index) let you inject custom logic into Cline's workflow to validate operations and enforce policies. You can enable hooks when running tasks from the command line:
|
||||
**When it activates:** Running `cline` without arguments, or when stdin is a TTY (terminal).
|
||||
|
||||
```bash
|
||||
# Enable hooks for a task
|
||||
cline "What does this repo do?" -s hooks_enabled=true
|
||||
|
||||
# Configure hooks globally via CLI
|
||||
cline config set hooks-enabled=true
|
||||
cline
|
||||
```
|
||||
|
||||
This allows you to integrate hooks into automated workflows, CI/CD pipelines, and headless task execution for consistent enforcement across all environments.
|
||||
Key features:
|
||||
|
||||
## Learn more
|
||||
- **Real-time conversation** - Type messages, see Cline's responses, and iterate on tasks
|
||||
- **Visual feedback** - Animated welcome screen, syntax-highlighted code, and progress indicators
|
||||
- **File mentions** with `@` - Reference workspace files with fuzzy search autocomplete
|
||||
- **Slash commands** with `/` - Quick access to `/settings`, `/history`, `/models`, and workflows
|
||||
- **Keyboard shortcuts** - `Tab` to toggle Plan/Act, `Shift+Tab` for auto-approve all
|
||||
- **Session summaries** - See tasks completed, files modified, and token usage on exit
|
||||
- **Settings panel** - Configure providers, models, and features without leaving the CLI
|
||||
|
||||
Interactive mode keeps you in control. You review Cline's plan, approve or modify actions, and guide the conversation.
|
||||
|
||||
[Learn more about interactive mode →](/cline-cli/interactive-mode)
|
||||
|
||||
### Headless Mode (Non-Interactive)
|
||||
|
||||
Headless mode is designed for **automation, scripting, and CI/CD pipelines** where human interaction isn't possible or desired.
|
||||
|
||||
**When it activates:** Using the `-y`/`--yolo` flag, `--json` flag, piping input/output, or when stdin is not a TTY.
|
||||
|
||||
```bash
|
||||
# Headless with auto-approval (YOLO mode)
|
||||
cline -y "Run tests and fix any failures"
|
||||
|
||||
# Headless with JSON output for parsing
|
||||
cline --json "List all TODO comments" | jq '.text'
|
||||
|
||||
# Headless via piped input
|
||||
cat README.md | cline "Summarize this document"
|
||||
|
||||
# Chain multiple headless commands
|
||||
git diff | cline -y "explain these changes" | cline -y "write a commit message"
|
||||
```
|
||||
|
||||
Key features:
|
||||
|
||||
- **No visual interface** - Clean text or JSON output suitable for scripting
|
||||
- **Automatic execution** - With `-y`, Cline approves all actions and runs autonomously
|
||||
- **Process control** - Exits automatically when the task completes
|
||||
- **Piped workflows** - Read from stdin, write to stdout, chain with other commands
|
||||
- **Machine-readable output** - Use `--json` to get structured output for parsing
|
||||
|
||||
<Warning>
|
||||
Headless mode with `-y` gives Cline full autonomy. Run on a clean git branch so you can easily revert changes if needed.
|
||||
</Warning>
|
||||
|
||||
### Mode Detection Summary
|
||||
|
||||
Cline automatically detects which mode to use based on your invocation. This table shows how different command patterns trigger each mode, helping you predict behavior in scripts and interactive sessions.
|
||||
|
||||
| Invocation | Mode | Reason |
|
||||
|------------|------|--------|
|
||||
| `cline` | Interactive | No arguments, TTY connected |
|
||||
| `cline "task"` | Interactive | TTY connected |
|
||||
| `cline -y "task"` | Headless | YOLO flag forces headless |
|
||||
| `cline --json "task"` | Headless | JSON flag forces headless |
|
||||
| `cat file \| cline "task"` | Headless | stdin is piped |
|
||||
| `cline "task" > output.txt` | Headless | stdout is redirected |
|
||||
|
||||
[Learn more about CLI workflows →](/cline-cli/three-core-flows)
|
||||
|
||||
## Supported Model Providers
|
||||
|
||||
Cline CLI supports all providers available in the VS Code extension:
|
||||
|
||||
- **Anthropic** (Claude)
|
||||
- **OpenAI** (GPT-4o, GPT-4)
|
||||
- **OpenAI Codex** (ChatGPT subscription)
|
||||
- **OpenRouter**
|
||||
- **AWS Bedrock**
|
||||
- **Google Gemini**
|
||||
- **X AI (Grok)**
|
||||
- **Cerebras**
|
||||
- **DeepSeek**
|
||||
- **Ollama** (local models)
|
||||
- **LM Studio** (local models)
|
||||
- **OpenAI Compatible** (any compatible API)
|
||||
|
||||
During setup, authenticate with `cline auth` to configure your preferred provider. [See setup guide →](/cline-cli/installation#authenticate)
|
||||
|
||||
## What You Can Build
|
||||
|
||||
### Automated Code Maintenance
|
||||
|
||||
Keep your codebase healthy with automated fixes. Cline scans for issues and applies corrections across multiple files.
|
||||
|
||||
```bash
|
||||
cline -y "Fix all ESLint errors in src/"
|
||||
```
|
||||
Finds and fixes linting violations throughout your source directory.
|
||||
|
||||
```bash
|
||||
cline -y "Update all deprecated React lifecycle methods"
|
||||
```
|
||||
Migrates legacy code patterns to modern equivalents (e.g., `componentWillMount` → `useEffect`).
|
||||
|
||||
```bash
|
||||
cline -y "Update dependencies with known vulnerabilities"
|
||||
```
|
||||
Identifies outdated packages with security issues and updates them to safe versions.
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
Integrate Cline into your continuous integration pipelines for automated code review and documentation.
|
||||
|
||||
```bash
|
||||
git diff origin/main | cline -y "Review these changes for issues"
|
||||
```
|
||||
Pipes your PR diff to Cline for automated code review, catching bugs and style issues before merge.
|
||||
|
||||
```bash
|
||||
git log --oneline v1.0..v1.1 | cline -y "Write release notes"
|
||||
```
|
||||
Generates human-readable release notes from your commit history between two tags.
|
||||
|
||||
```bash
|
||||
cline -y "Run tests and fix failures" --timeout 600
|
||||
```
|
||||
Executes your test suite, analyzes failures, and attempts fixes with a 10-minute timeout.
|
||||
|
||||
### Development Workflows
|
||||
|
||||
From quick edits to complex refactors, Cline adapts to your workflow.
|
||||
|
||||
```bash
|
||||
cline
|
||||
```
|
||||
Launches interactive mode for exploratory development and back-and-forth collaboration.
|
||||
|
||||
```bash
|
||||
cline "Refactor this function to use async/await"
|
||||
```
|
||||
Executes a focused task directly from the command line with approval prompts at key steps.
|
||||
|
||||
```bash
|
||||
cline "Based on @src/api.ts, add error handling to all endpoints"
|
||||
```
|
||||
Uses file mentions (`@`) to give Cline context about specific files in your workspace.
|
||||
|
||||
### Custom Shell Pipelines
|
||||
|
||||
Chain Cline with other CLI tools to build powerful automation workflows.
|
||||
|
||||
```bash
|
||||
gh pr diff 123 | cline -y "Review this PR"
|
||||
```
|
||||
Fetches a GitHub PR diff and pipes it directly to Cline for review.
|
||||
|
||||
```bash
|
||||
cline --json "List all TODO comments" | jq '.text'
|
||||
```
|
||||
Outputs structured JSON that you can process with tools like `jq` for scripting.
|
||||
|
||||
```bash
|
||||
git diff | cline -y "explain" | cline -y "write a haiku about these changes"
|
||||
```
|
||||
Chains multiple Cline invocations together for creative multi-step workflows.
|
||||
|
||||
## Features at a Glance
|
||||
|
||||
| Feature | Interactive Mode | Non-Interactive Mode |
|
||||
|---------|------------------|----------------------|
|
||||
| Interactive chat | ✓ | - |
|
||||
| File mentions (@) | ✓ | ✓ (inline) |
|
||||
| Slash commands (/) | ✓ | - |
|
||||
| Settings panel | ✓ | `cline config` |
|
||||
| Plan/Act toggle | ✓ (Tab) | `-p` / `-a` flags |
|
||||
| Auto-approve | ✓ (Shift+Tab) | `-y` flag |
|
||||
| Session summary | ✓ | - |
|
||||
| JSON output | - | `--json` |
|
||||
| Piped input | - | ✓ |
|
||||
|
||||
## Learn More
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="Installation" icon="download" href="/cline-cli/installation">
|
||||
Install Cline CLI and authenticate with your account to get started.
|
||||
<Card title="Installation & Setup" icon="download" href="/cline-cli/installation">
|
||||
Install Cline CLI and authenticate with your preferred provider.
|
||||
</Card>
|
||||
|
||||
<Card title="Three Core Flows" icon="route" href="/cline-cli/three-core-flows">
|
||||
Master the three ways to use Cline CLI: interactive mode, headless automation, and multi-instance parallelization.
|
||||
<Card title="Interactive Mode" icon="terminal" href="/cline-cli/interactive-mode">
|
||||
Master the interactive CLI with keyboard shortcuts and slash commands.
|
||||
</Card>
|
||||
|
||||
<Card title="CLI Workflows" icon="route" href="/cline-cli/three-core-flows">
|
||||
Learn interactive mode, direct execution, and automation patterns.
|
||||
</Card>
|
||||
|
||||
<Card title="Configuration" icon="gear" href="/cline-cli/configuration">
|
||||
Configure settings, rules, workflows, and environment variables.
|
||||
</Card>
|
||||
|
||||
<Card title="Use in Other Editors" icon="code" href="/cline-cli/acp-editor-integrations">
|
||||
Run Cline as an ACP agent in JetBrains, Neovim, Zed, and more.
|
||||
</Card>
|
||||
|
||||
<Card title="Use in Other Editors" icon="code" href="/cline-cli/acp-editor-integrations">
|
||||
Run Cline as an ACP agent in JetBrains, Neovim, Zed, and more.
|
||||
</Card>
|
||||
</Columns>
|
||||
|
||||
@@ -88,33 +88,24 @@ jobs:
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Node v20 is needed for Cline CLI on GitHub Actions Linux
|
||||
# Node v20+ is needed for Cline CLI on GitHub Actions Linux
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Setup Cline CLI
|
||||
- name: Install Cline CLI
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
run: |
|
||||
# Install the Cline CLI
|
||||
sudo npm install -g cline
|
||||
run: npm install -g cline
|
||||
|
||||
- name: Create Cline Instance
|
||||
- name: Configure Cline Authentication
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
env:
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
CLINE_DIR: ${{ runner.temp }}/cline
|
||||
run: |
|
||||
# Create instance and capture output
|
||||
INSTANCE_OUTPUT=$(cline instance new 2>&1)
|
||||
|
||||
# Parse address from output (format: " Address: 127.0.0.1:36733")
|
||||
CLINE_ADDRESS=$(echo "$INSTANCE_OUTPUT" | grep "Address:" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]+')
|
||||
echo "CLINE_ADDRESS=$CLINE_ADDRESS" >> $GITHUB_ENV
|
||||
|
||||
# Configure API key
|
||||
cline config set open-router-api-key=$OPENROUTER_API_KEY --address $CLINE_ADDRESS -v
|
||||
# Configure API key using the auth command
|
||||
cline auth --provider openrouter --apikey "${{ secrets.OPENROUTER_API_KEY }}"
|
||||
|
||||
- name: Download analyze script
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
@@ -135,7 +126,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
RESULT=$(./analyze-issue.sh "${ISSUE_URL}" "Analyze this issue. The user asked: ${COMMENT}" "$CLINE_ADDRESS")
|
||||
RESULT=$(./analyze-issue.sh "${ISSUE_URL}" "Analyze this issue. The user asked: ${COMMENT}")
|
||||
|
||||
{
|
||||
echo 'result<<EOF'
|
||||
@@ -180,8 +171,7 @@ This tells the workflow where to download the analysis script from your reposito
|
||||
</Warning>
|
||||
|
||||
The workflow will look for new or updated issues, check for `@cline` mentions, and then
|
||||
start up an instance of the Cline CLI to dig into the issue, providing feedback
|
||||
as a reply to the issue.
|
||||
start up the Cline CLI to dig into the issue, providing feedback as a reply to the issue.
|
||||
|
||||
### 2. Configure API Keys
|
||||
|
||||
@@ -246,19 +236,16 @@ if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <github-issue-url> [prompt] [address]"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?'"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?' 127.0.0.1:46529"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Gather the args
|
||||
ISSUE_URL="$1"
|
||||
PROMPT="${2:-What is the root cause of this issue?}"
|
||||
if [ -n "$3" ]; then
|
||||
ADDRESS="--address $3"
|
||||
fi
|
||||
|
||||
# Ask Cline for its analysis, showing only the summary
|
||||
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
|
||||
cline -y "$PROMPT: $ISSUE_URL" --mode act -F json | \
|
||||
sed -n '/^{/,$p' | \
|
||||
jq -r 'select(.say == "completion_result") | .text' | \
|
||||
sed 's/\\n/\n/g'
|
||||
@@ -311,12 +298,10 @@ The workflow (`cline-responder.yml`):
|
||||
1. **Triggers** on issue comments (created or edited)
|
||||
2. **Detects** `@cline` mentions (case-insensitive)
|
||||
3. **Installs** Cline CLI globally using npm
|
||||
4. **Creates** a Cline instance using `cline instance new`
|
||||
5. **Configures** authentication using `cline config set open-router-api-key=...
|
||||
--address ...`
|
||||
4. **Configures** authentication using `cline config set open-router-api-key=...`
|
||||
6. **Downloads** the reusable `analyze-issue.sh` script from the
|
||||
`github-issue-rca` sample
|
||||
7. **Runs** analysis with the instance address
|
||||
7. **Runs** analysis in Cline CLI
|
||||
8. **Posts** the analysis result as a comment
|
||||
|
||||
## Related Samples
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: "GitHub PR Review"
|
||||
description: "Automatically review Pull Requests with AI using Cline CLI in GitHub Actions."
|
||||
---
|
||||
|
||||
# GitHub PR Review Sample
|
||||
|
||||
Automate code review for every Pull Request. Detailed analysis, security checks, and code suggestions provided by Cline running autonomously in GitHub Actions.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/cli-pr-review.png" alt="Cline PR Review Comment" width="600" />
|
||||
</Frame>
|
||||
|
||||
## The Workflow
|
||||
|
||||
When a PR is opened or marked ready for review, this workflow:
|
||||
1. **Checks out** the code.
|
||||
2. **Installs** Node.js and Cline CLI.
|
||||
3. **Configures** authentication (e.g., Anthropic, OpenAI).
|
||||
4. **Runs Cline** with a comprehensive system prompt to analyze the diff, context, and related issues using GitHub CLI (`gh`).
|
||||
5. **Posts** a detailed review comment with inline code suggestions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **GitHub repository** with Actions enabled.
|
||||
- **AI Provider API Key** (e.g., Anthropic, OpenRouter) added as a repository secret (e.g., `ANTHROPIC_API_KEY`).
|
||||
- **GitHub Token** (automatically provided by Actions as `GITHUB_TOKEN`).
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Create the Workflow File
|
||||
|
||||
Create a file named `.github/workflows/cline-pr-review.yml` in your repository:
|
||||
|
||||
```yaml
|
||||
name: Cline PR Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: "PR number to review"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cline-pr-review:
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Cline CLI
|
||||
run: npm install -g cline
|
||||
|
||||
- name: Configure Cline Authentication
|
||||
# Replace 'anthropic' with your provider of choice (openai, openrouter, etc.)
|
||||
# and ensure the corresponding secret is set in your repo settings.
|
||||
run: |
|
||||
cline auth --provider anthropic \
|
||||
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||
--modelid claude-opus-4-5-20251101
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Review PR with Cline
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr.outputs.number }}
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
# Restrict Cline to only safe, read-only GitHub CLI commands
|
||||
CLINE_COMMAND_PERMISSIONS: |
|
||||
{
|
||||
"allow": [
|
||||
"gh pr diff *",
|
||||
"gh pr view *",
|
||||
"gh pr checks *",
|
||||
"gh pr list *",
|
||||
"gh issue list *",
|
||||
"gh issue view *",
|
||||
"git log *",
|
||||
"gh pr comment ${{ steps.pr.outputs.number }} *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
|
||||
]
|
||||
}
|
||||
run: |
|
||||
cline --yolo 'You are a GitHub PR reviewer for this repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
|
||||
|
||||
PR: #'"${PR_NUMBER}"'
|
||||
|
||||
## Gather context
|
||||
Use `gh` commands to fetch the PR diff, details, and checks.
|
||||
|
||||
```bash
|
||||
# Get full PR details
|
||||
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
|
||||
|
||||
# Get the diff
|
||||
gh pr diff '"${PR_NUMBER}"'
|
||||
|
||||
# Check CI status
|
||||
gh pr checks '"${PR_NUMBER}"'
|
||||
```
|
||||
|
||||
## Deep code review
|
||||
Analyze the code changes. Look for:
|
||||
- Logic errors and edge cases
|
||||
- Security vulnerabilities
|
||||
- Performance issues
|
||||
- adherence to patterns in the codebase
|
||||
|
||||
## Submit Review
|
||||
Post a single comprehensive comment summarizing your review.
|
||||
|
||||
If you have specific code suggestions, use the GitHub API to post inline comments:
|
||||
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f event="COMMENT" \
|
||||
-f body="" \
|
||||
-F comments='[{"path": "src/file.ts", "line": 10, "body": "Suggestion: ..."}]'
|
||||
```
|
||||
|
||||
Start your main comment with "Reviewed by Cline".'
|
||||
```
|
||||
|
||||
### 2. Configure Secrets
|
||||
|
||||
1. Go to your repository settings -> **Secrets and variables** -> **Actions**.
|
||||
2. Add a **New repository secret**.
|
||||
3. Name: `ANTHROPIC_API_KEY` (or match the key used in your workflow).
|
||||
4. Value: Your actual API key.
|
||||
|
||||
## Key Components Explained
|
||||
|
||||
### Permissions
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
```
|
||||
We grant `pull-requests: write` so Cline can post comments and inline reviews. `contents: read` ensures it can analyze the code but **cannot push changes directly**, providing a security boundary.
|
||||
|
||||
### Authentication
|
||||
```bash
|
||||
cline auth --provider anthropic --apikey "..."
|
||||
```
|
||||
The `auth` command configures Cline in the CI environment without interactive prompts. You can switch providers (e.g., `openai`, `openrouter`) by changing the flags.
|
||||
|
||||
### Autonomous Mode (`--yolo`)
|
||||
```bash
|
||||
cline --yolo '...'
|
||||
```
|
||||
The `--yolo` flag tells Cline to run autonomously, executing commands without waiting for user approval. This is essential for CI/CD workflows.
|
||||
|
||||
### Command Permissions
|
||||
We explicitly restrict what commands Cline can run using `CLINE_COMMAND_PERMISSIONS`. This ensures Cline can only use `gh` and `git` commands relevant to reviewing, preventing any accidental or malicious system modifications.
|
||||
|
||||
## Customizing the Reviewer
|
||||
|
||||
The "System Prompt" passed to Cline in the final step is fully customizable. You can modify it to:
|
||||
- Enforce specific style guides.
|
||||
- Focus on security vs. performance.
|
||||
- Ask for specific types of feedback (e.g., "Roast my code" vs. "Be gentle").
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: "Model Orchestration"
|
||||
description: "Use multiple AI models strategically: optimize costs, reduce bias, and leverage model-specific strengths in your workflows"
|
||||
---
|
||||
|
||||
# Model Orchestration
|
||||
|
||||
Cline CLI's `--config` and `--thinking` flags enable sophisticated multi-model workflows. Instead of using a single model for all tasks, you can route different work to different models based on cost, capability, and specialization.
|
||||
|
||||
## Why Orchestrate Multiple Models?
|
||||
|
||||
**Cost Optimization**
|
||||
- Use fast, cheap models (Haiku, Gemini Flash) for simple tasks like summarization
|
||||
- Reserve expensive models (Opus, O1) for complex reasoning and planning
|
||||
- Reduce API costs by 10-100x on routine operations
|
||||
|
||||
**Bias Reduction**
|
||||
- Different models catch different issues in code reviews
|
||||
- Cross-validate solutions with multiple AI perspectives
|
||||
- Reduce blind spots from single-model thinking
|
||||
|
||||
**Specialization**
|
||||
- Some models excel at code (Codex, DeepSeek)
|
||||
- Others are better at documentation (GPT-4, Claude)
|
||||
- Security analysis benefits from multiple viewpoints
|
||||
|
||||
## Pattern 1: CI/CD Code Review
|
||||
|
||||
See our production GitHub Actions workflow that uses Cline CLI for automated PR reviews: [cline-pr-review.yml](https://github.com/cline/cline/blob/main/.github/workflows/cline-pr-review.yml)
|
||||
|
||||
**Key capabilities demonstrated:**
|
||||
- **Automated inline suggestions**: Creates GitHub suggestion blocks that authors can commit with one click
|
||||
- **SME identification**: Analyzes git history to find subject matter experts for each file
|
||||
- **Related issue discovery**: Searches for context from past issues and PRs
|
||||
- **Security-first permissions**: Read-only codebase access, can only post reviews
|
||||
- **Deep code analysis**: Understands intent, compares approaches, identifies edge cases
|
||||
|
||||
The workflow runs on every PR and provides maintainers with comprehensive context to make faster, more informed decisions.
|
||||
|
||||
## Pattern 2: Task Phase Optimization
|
||||
|
||||
Use different models for different phases of work. Route simple tasks to cheap models, complex reasoning to premium models.
|
||||
|
||||
### Example: Issue Analysis Pipeline
|
||||
|
||||
```bash
|
||||
# Get latest issue content
|
||||
ISSUE_CONTENT=$(gh issue view $(gh issue list -L 1 | awk '{print $1}'))
|
||||
|
||||
# Phase 1: Quick summary with cheap model
|
||||
SUMMARY=$(echo "$ISSUE_CONTENT" | cline -y --config ~/.cline-haiku \
|
||||
"summarize this issue in 2-3 sentences")
|
||||
|
||||
# Phase 2: Detailed plan with expensive model + thinking
|
||||
PLAN=$(echo "$SUMMARY" | cline -y --thinking --config ~/.cline-opus \
|
||||
"create detailed implementation plan with edge cases")
|
||||
|
||||
# Phase 3: Execute with mid-tier model
|
||||
echo "$PLAN" | cline -y --config ~/.cline-sonnet \
|
||||
"implement the plan from above"
|
||||
```
|
||||
|
||||
<Note>
|
||||
Each cline invocation needs to complete before passing output to the next phase. Use shell variables to store intermediate results rather than piping cline commands directly.
|
||||
</Note>
|
||||
|
||||
**Cost impact:**
|
||||
- Haiku: $0.80 per million input tokens
|
||||
- Opus: $15 per million input tokens
|
||||
- Sonnet: $3 per million input tokens
|
||||
|
||||
This pattern uses Opus only when needed for complex reasoning, saving ~10x on API costs compared to using Opus for everything.
|
||||
|
||||
### Setting Up Model Configs
|
||||
|
||||
Create separate configuration directories for each model:
|
||||
|
||||
```bash
|
||||
# Create config directories
|
||||
mkdir -p ~/.cline-haiku ~/.cline-sonnet ~/.cline-opus
|
||||
|
||||
# Configure each with different models
|
||||
cline --config ~/.cline-haiku auth anthropic --modelid claude-haiku-4-20250514
|
||||
cline --config ~/.cline-sonnet auth anthropic --modelid claude-sonnet-4-20250514
|
||||
cline --config ~/.cline-opus auth anthropic --modelid claude-opus-4-5-20251101
|
||||
|
||||
# Or use different providers entirely
|
||||
cline --config ~/.cline-gemini auth gemini --modelid gemini-2.0-flash-exp
|
||||
cline --config ~/.cline-codex auth openai-codex --modelid gpt-5-latest
|
||||
```
|
||||
|
||||
Now you can switch models per-task with `--config`:
|
||||
|
||||
```bash
|
||||
cline --config ~/.cline-haiku "quick task"
|
||||
cline --config ~/.cline-opus "complex reasoning task"
|
||||
```
|
||||
|
||||
## Pattern 3: Multi-Model Review & Consensus
|
||||
|
||||
Get multiple AI perspectives on the same change, then synthesize their feedback.
|
||||
|
||||
### Example: Diff Review Pipeline
|
||||
|
||||
```bash
|
||||
# Get the latest commit
|
||||
DIFF=$(git show)
|
||||
|
||||
# Review 1: Gemini's perspective
|
||||
echo "$DIFF" | cline -y --config ~/.cline-gemini \
|
||||
"review this diff and write your analysis to gemini-review.md"
|
||||
|
||||
# Review 2: Codex's perspective
|
||||
echo "$DIFF" | cline -y --config ~/.cline-codex \
|
||||
"review this diff and write your analysis to codex-review.md"
|
||||
|
||||
# Review 3: Opus's perspective
|
||||
echo "$DIFF" | cline -y --config ~/.cline-opus \
|
||||
"review this diff and write your analysis to opus-review.md"
|
||||
|
||||
# Synthesize all reviews into a consensus
|
||||
cat gemini-review.md codex-review.md opus-review.md | cline -y \
|
||||
"summarize these 3 reviews and identify: 1) issues all models agree on, 2) issues only one model caught, 3) your final recommendation"
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- **Redundancy**: Issues caught by all 3 models are high-confidence
|
||||
- **Coverage**: Each model has blind spots; together they cover more ground
|
||||
- **Prioritization**: Consensus issues should be fixed first
|
||||
- **Learning**: See which model types catch which issue types
|
||||
|
||||
### Advanced: Parallel Reviews
|
||||
|
||||
Run reviews in parallel for faster feedback:
|
||||
|
||||
```bash
|
||||
# Run all reviews simultaneously
|
||||
git show | cline -y --config ~/.cline-gemini "review and save to gemini-review.md" &
|
||||
git show | cline -y --config ~/.cline-codex "review and save to codex-review.md" &
|
||||
git show | cline -y --config ~/.cline-opus "review and save to opus-review.md" &
|
||||
|
||||
# Wait for all to complete
|
||||
wait
|
||||
|
||||
# Synthesize
|
||||
cat *-review.md | cline -y "create consensus review"
|
||||
```
|
||||
|
||||
<Note>
|
||||
Parallel execution requires managing multiple Cline instances. See [Multi-instance workflows](/cline-cli/three-core-flows#3-multi-instance-run-parallel-agents) for details.
|
||||
</Note>
|
||||
|
||||
## Extended Thinking for Complex Tasks
|
||||
|
||||
Use the `--thinking` flag when Cline needs to analyze multiple approaches:
|
||||
|
||||
```bash
|
||||
# Without thinking: Fast but may miss nuances
|
||||
cline -y "refactor this codebase"
|
||||
|
||||
# With thinking: Slower but more thorough
|
||||
cline -y --thinking \
|
||||
"refactor this codebase - consider: performance, maintainability, backward compatibility"
|
||||
```
|
||||
|
||||
The `--thinking` flag allocates 1024 tokens for internal reasoning before Cline responds. Best for:
|
||||
- Architectural decisions
|
||||
- Security analysis
|
||||
- Complex refactoring
|
||||
- Multi-step planning
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Profile your workload**: Track which tasks are simple vs. complex
|
||||
2. **Match models to tasks**: Use fast models for summaries, powerful models for reasoning
|
||||
3. **Automate switching**: Script model selection based on task type
|
||||
4. **Monitor costs**: Different models have 10-100x price differences
|
||||
5. **Validate important decisions**: Use multi-model consensus for critical changes
|
||||
|
||||
## Production Examples
|
||||
|
||||
### Cost-Optimized PR Review
|
||||
|
||||
```bash
|
||||
# Haiku: Quick summary and issue identification
|
||||
gh pr view $PR | cline -y --config ~/.cline-haiku \
|
||||
"list all issues to fix, output as JSON"
|
||||
|
||||
# Opus with thinking: Deep analysis only if issues found
|
||||
if [ -s issues.json ]; then
|
||||
cline -y --thinking --config ~/.cline-opus \
|
||||
"analyze these issues and recommend fixes"
|
||||
fi
|
||||
```
|
||||
|
||||
### Security-Focused Multi-Model Scan
|
||||
|
||||
```bash
|
||||
# Different models have different security perspectives
|
||||
git diff main | cline -y --config ~/.cline-gemini "security review" > gemini-sec.md &
|
||||
git diff main | cline -y --config ~/.cline-opus "security review" > opus-sec.md &
|
||||
git diff main | cline -y --config ~/.cline-codex "security review" > codex-sec.md &
|
||||
wait
|
||||
|
||||
# High-priority: Issues all 3 models found
|
||||
cat *-sec.md | cline -y "find security issues all 3 reviews mentioned"
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="CLI Reference" icon="terminal" href="/cline-cli/cli-reference">
|
||||
Complete documentation for --config and --thinking flags
|
||||
</Card>
|
||||
|
||||
<Card title="Three Core Flows" icon="route" href="/cline-cli/three-core-flows">
|
||||
Learn about interactive mode, headless automation, and multi-instance workflows
|
||||
</Card>
|
||||
|
||||
<Card title="Model Selection Guide" icon="brain" href="/core-features/model-selection-guide">
|
||||
Compare models and choose the right one for your needs
|
||||
</Card>
|
||||
|
||||
<Card title="CI/CD Integration" icon="github" href="/cline-cli/samples/github-integration">
|
||||
Automate GitHub workflows with Cline CLI
|
||||
</Card>
|
||||
</Columns>
|
||||
@@ -8,6 +8,22 @@ This section provides sample implementations that demonstrate various Cline CLI
|
||||
## Available Samples
|
||||
|
||||
<CardGroup cols={1}>
|
||||
<Card
|
||||
title="Model Orchestration"
|
||||
icon="layer-group"
|
||||
href="/cline-cli/samples/model-orchestration"
|
||||
>
|
||||
Use multiple AI models strategically with --config and --thinking flags. Optimize costs by routing simple tasks to cheap models and complex reasoning to premium models. Includes patterns for CI/CD code review, task phase optimization, and multi-model consensus.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Worktree Workflows"
|
||||
icon="code-branch"
|
||||
href="/cline-cli/samples/worktree-workflows"
|
||||
>
|
||||
Use Git worktrees with the --cwd flag to run parallel tasks, test different approaches, and pipe context between isolated environments. Includes patterns for parallel execution, cross-worktree piping, and combining with model orchestration.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="GitHub Root Cause Analysis"
|
||||
icon="magnifying-glass-chart"
|
||||
@@ -23,6 +39,14 @@ This section provides sample implementations that demonstrate various Cline CLI
|
||||
>
|
||||
Automatically respond to GitHub issues by mentioning @cline in comments. Uses Cline CLI in GitHub Actions to create an AI-powered issue assistant that analyzes and responds autonomously.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="GitHub PR Review (Actions)"
|
||||
icon="code-pull-request"
|
||||
href="/cline-cli/samples/github-pr-review"
|
||||
>
|
||||
Automatically review Pull Requests with AI. Configures Cline in GitHub Actions to analyze diffs, check for security issues, and post detailed reviews with inline code suggestions.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Additional Resources
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
---
|
||||
title: "Worktree Workflows"
|
||||
description: "Use Git worktrees with Cline CLI to run parallel tasks, test different approaches, and pipe context between isolated environments"
|
||||
---
|
||||
|
||||
# Worktree Workflows
|
||||
|
||||
Git worktrees let you have multiple branches checked out simultaneously in different folders. Combined with Cline CLI's `--cwd` flag, this enables powerful parallel development workflows and isolated experimentation.
|
||||
|
||||
<Tip>
|
||||
New to Git worktrees? See our comprehensive [Worktrees guide](/features/worktrees) for the full concept explanation, VS Code integration, and best practices.
|
||||
</Tip>
|
||||
|
||||
## Quick Worktree Setup
|
||||
|
||||
If you haven't used Git worktrees before, here's the essentials:
|
||||
|
||||
```bash
|
||||
# Create a new worktree in ~/worktree-a on branch feature-a
|
||||
git worktree add ~/worktree-a -b feature-a
|
||||
|
||||
# Create another worktree for a different feature
|
||||
git worktree add ~/worktree-b -b feature-b
|
||||
|
||||
# List all worktrees
|
||||
git worktree list
|
||||
|
||||
# Remove a worktree when done
|
||||
git worktree remove ~/worktree-a
|
||||
```
|
||||
|
||||
Each worktree is a separate folder with its own branch checked out. They all share the same Git history and `.git` directory, but have independent working directories.
|
||||
|
||||
## The `--cwd` Flag
|
||||
|
||||
The `-c, --cwd <path>` flag tells Cline to run in a specific directory without changing your current location:
|
||||
|
||||
```bash
|
||||
# Run Cline in a different directory
|
||||
cline --cwd ~/worktree-a -y "refactor the authentication code"
|
||||
|
||||
# Short form
|
||||
cline -c ~/worktree-b -y "add unit tests"
|
||||
```
|
||||
|
||||
This is the key to worktree workflows—you can run multiple Cline instances in different worktrees simultaneously from a single terminal.
|
||||
|
||||
## Pattern 1: Parallel Task Execution
|
||||
|
||||
Run different tasks in parallel across multiple worktrees. Each task works on a separate branch in complete isolation.
|
||||
|
||||
### Example: Parallel Feature Development
|
||||
|
||||
```bash
|
||||
# Terminal 1: Update docs in worktree-a
|
||||
cline --cwd ~/worktree-a -y "read the last 10 changes using git show and update our README with them" &
|
||||
|
||||
# Terminal 2: TypeScript migration in worktree-b
|
||||
cline --cwd ~/worktree-b -y "update the index.js to use typescript" &
|
||||
|
||||
# Terminal 3: Refactoring in worktree-c
|
||||
cline --cwd ~/worktree-c -y "refactor the cli/ folder to be more modular" &
|
||||
|
||||
# Wait for all to complete
|
||||
wait
|
||||
```
|
||||
|
||||
The `&` runs each command in the background, allowing all three to execute simultaneously.
|
||||
|
||||
### When to Use Parallel Execution
|
||||
|
||||
**Perfect for:**
|
||||
- Multiple independent features
|
||||
- Bulk refactoring across different modules
|
||||
- Running tests in one worktree while developing in another
|
||||
- Trying multiple approaches to the same problem
|
||||
|
||||
**Not ideal for:**
|
||||
- Tasks that modify the same files (merge conflicts likely)
|
||||
- Tasks that depend on each other's results
|
||||
- When you need to monitor progress closely
|
||||
|
||||
## Pattern 2: Cross-Worktree Context Piping
|
||||
|
||||
Pipe output from one worktree as input to another. Use when a task in one worktree needs context from attempts in another worktree.
|
||||
|
||||
### Example: Learning from Failures
|
||||
|
||||
```bash
|
||||
# Try approach A in worktree-a, capture only the failure summary
|
||||
cline --cwd ~/worktree-a -y \
|
||||
"edit the index.ts to be better and then npm run. if it fails, output ONLY the failure summary. nothing else but the failure summary" \
|
||||
| cline --cwd ~/worktree-b -y \
|
||||
"i've tried to edit the index.ts in a different worktree but it failed. use a different approach for this work tree"
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
1. First Cline instance runs in `worktree-a`, attempts a change, tests it
|
||||
2. If it fails, outputs just the failure summary
|
||||
3. That summary is piped to a second Cline instance in `worktree-b`
|
||||
4. Second instance sees the failure and tries a different approach
|
||||
|
||||
### When to Use Context Piping
|
||||
|
||||
**Perfect for:**
|
||||
- A/B testing different solutions
|
||||
- Learning from failed attempts
|
||||
- Iterative refinement (try → analyze → try differently)
|
||||
- Comparing outputs across approaches
|
||||
|
||||
**Not ideal for:**
|
||||
- Simple tasks that don't need cross-context
|
||||
- When both worktrees would succeed independently
|
||||
- Real-time collaboration (use parallel execution instead)
|
||||
|
||||
## Combining with Other CLI Features
|
||||
|
||||
### Different Models Per Worktree
|
||||
|
||||
Use `--config` to run different models in different worktrees:
|
||||
|
||||
```bash
|
||||
# Cheap model for simple docs update
|
||||
cline --cwd ~/worktree-docs --config ~/.cline-haiku -y \
|
||||
"update README with latest changes"
|
||||
|
||||
# Expensive model for complex refactoring
|
||||
cline --cwd ~/worktree-refactor --config ~/.cline-opus --thinking -y \
|
||||
"refactor authentication system for better security"
|
||||
```
|
||||
|
||||
This optimizes costs while maintaining quality where it matters.
|
||||
|
||||
### Task Isolation
|
||||
|
||||
Keep long-running worktree sessions isolated by running each task against a different worktree path:
|
||||
|
||||
```bash
|
||||
# Run tasks in dedicated worktrees
|
||||
cline --cwd ~/worktree-a -y "long-running task"
|
||||
cline --cwd ~/worktree-b -y "another task"
|
||||
```
|
||||
|
||||
Each worktree has its own Git branch and working directory, so task history and changes stay separated without needing instance management.
|
||||
|
||||
### With YOLO Mode
|
||||
|
||||
The `-y` (YOLO) flag is essential for worktree workflows:
|
||||
|
||||
```bash
|
||||
# Without -y: Opens interactive chat (blocks other tasks)
|
||||
cline --cwd ~/worktree-a "refactor code"
|
||||
|
||||
# With -y: Runs autonomously (doesn't block)
|
||||
cline --cwd ~/worktree-a -y "refactor code" &
|
||||
```
|
||||
|
||||
For parallel execution, always use `-y` to avoid blocking on user approval.
|
||||
|
||||
## Real-World Workflow Example
|
||||
|
||||
Here's a complete workflow showing how these patterns work together:
|
||||
|
||||
```bash
|
||||
# Setup: Create three worktrees
|
||||
git worktree add ~/cline-worktrees/feature-auth -b feature/authentication
|
||||
git worktree add ~/cline-worktrees/feature-api -b feature/api-endpoints
|
||||
git worktree add ~/cline-worktrees/fix-tests -b fix/failing-tests
|
||||
|
||||
# Pattern 1: Run parallel independent tasks
|
||||
cline -c ~/cline-worktrees/feature-auth -y --config ~/.cline-sonnet \
|
||||
"implement JWT authentication" &
|
||||
|
||||
cline -c ~/cline-worktrees/feature-api -y --config ~/.cline-sonnet \
|
||||
"create REST API endpoints for user management" &
|
||||
|
||||
cline -c ~/cline-worktrees/fix-tests -y --config ~/.cline-haiku \
|
||||
"fix all failing unit tests" &
|
||||
|
||||
wait
|
||||
echo "All parallel tasks complete!"
|
||||
|
||||
# Pattern 2: Use piping for iterative refinement
|
||||
cline -c ~/cline-worktrees/feature-auth -y \
|
||||
"test the authentication with curl. output only errors if any" \
|
||||
| cline -c ~/cline-worktrees/feature-auth -y \
|
||||
"fix the authentication issues described in the input"
|
||||
|
||||
# Merge successful changes back
|
||||
cd ~/cline-worktrees/feature-auth
|
||||
git checkout main
|
||||
git merge feature/authentication
|
||||
|
||||
# Cleanup
|
||||
git worktree remove ~/cline-worktrees/feature-auth
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Worktree Organization">
|
||||
- **Use a dedicated folder**: Create `~/cline-worktrees/` for all worktrees
|
||||
- **Meaningful branch names**: Use `feature/`, `fix/`, `refactor/` prefixes
|
||||
- **Clean up regularly**: Remove worktrees after merging branches
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Task Isolation">
|
||||
- **Independent features only**: Don't parallelize tasks that touch the same files
|
||||
- **Test in isolation**: Each worktree should have its own test run
|
||||
- **Separate configs**: Use `.worktreeinclude` to copy `node_modules` and build artifacts
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Resource Management">
|
||||
- **Monitor disk space**: Each worktree is a full checkout
|
||||
- **Limit parallel tasks**: Running too many simultaneously can slow your system
|
||||
- **Use background jobs wisely**: Track with `jobs` command, kill with `kill %1`, etc.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Error Handling">
|
||||
- **Check exit codes**: Use `|| echo "Task failed"` to catch errors
|
||||
- **Log outputs**: Redirect to files for debugging: `> worktree-a.log 2>&1`
|
||||
- **Graceful cleanup**: Always remove worktrees after tasks complete
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title=""Branch already checked out" error">
|
||||
Git doesn't allow the same branch in multiple worktrees. Solutions:
|
||||
- Use different branch names for each worktree
|
||||
- Remove the existing worktree first: `git worktree remove <path>`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Tasks not running in parallel">
|
||||
Make sure you're using:
|
||||
- `&` at the end of each command to background it
|
||||
- `-y` flag so Cline doesn't wait for approval
|
||||
- Different worktrees (not the same path)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Pipe not working as expected">
|
||||
Verify:
|
||||
- First command outputs to stdout (not stderr)
|
||||
- Second command reads from stdin (use `--` separator if needed)
|
||||
- Both commands use correct `--cwd` paths
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Changes not appearing in worktree">
|
||||
Check:
|
||||
- You're in the right worktree: `git worktree list`
|
||||
- Files aren't gitignored
|
||||
- You committed/staged changes if needed
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Related Documentation
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="Worktrees Overview" icon="code-branch" href="/features/worktrees">
|
||||
Complete guide to Git worktrees, VS Code integration, and .worktreeinclude
|
||||
</Card>
|
||||
|
||||
<Card title="Model Orchestration" icon="layer-group" href="/cline-cli/samples/model-orchestration">
|
||||
Use different models strategically with --config and --thinking flags
|
||||
</Card>
|
||||
|
||||
<Card title="CLI Reference" icon="terminal" href="/cline-cli/cli-reference">
|
||||
Complete documentation for --cwd and all other CLI flags
|
||||
</Card>
|
||||
|
||||
<Card title="Three Core Flows" icon="route" href="/cline-cli/three-core-flows">
|
||||
Learn about interactive mode, task mode, and plain text workflows
|
||||
</Card>
|
||||
</Columns>
|
||||
@@ -1,158 +1,292 @@
|
||||
---
|
||||
title: "Three Core Flows"
|
||||
description: "Learn the three ways to use Cline CLI: interactive mode, headless automation, and multi-instance parallelization"
|
||||
title: "CLI Workflows"
|
||||
description: "Learn the three ways to use Cline CLI: interactive mode, direct task execution, and automation"
|
||||
---
|
||||
|
||||
Two concepts to understand:
|
||||
Cline CLI supports three primary workflows, each optimized for different use cases. Choose the approach that best fits your needs.
|
||||
|
||||
**Task** - A single job for Cline to complete ("add tests to utils.js"). You describe what you want, Cline plans how to do it, then executes the plan. Tasks run on instances.
|
||||
<Note>
|
||||
**Migrating from an older CLI version?** Instance commands (`cline instance new/list/kill`) have been removed in Cline CLI 2.0. The new architecture is simpler. Just run `cline` for interactive mode or `cline "task"` for direct execution.
|
||||
</Note>
|
||||
|
||||
**Instance** - An independent Cline workspace. Each instance runs one task at a time. Create multiple instances to run multiple tasks that work on different parts of your project in parallel.
|
||||
## 1. Interactive Mode
|
||||
|
||||
## 1. Interactive mode: Plan first, then act
|
||||
The interactive CLI provides the richest experience for interactive development.
|
||||
|
||||
Start here to see how Cline works. Interactive mode opens a chat session where you can review plans before execution.
|
||||
### Getting Started
|
||||
|
||||
```bash
|
||||
cline
|
||||
```
|
||||
|
||||
Cline opens an interactive session in your current directory. Type your task as a message. Cline enters Plan mode and proposes a step-by-step strategy.
|
||||
This launches an interactive session in your current directory. Type your task, and Cline will analyze and execute it.
|
||||
|
||||
Review or edit the plan in chat. When you're ready, switch to execution:
|
||||
### Key Features
|
||||
|
||||
```bash
|
||||
/act
|
||||
**Plan/Act Mode Toggle** - Press `Tab` to switch between modes:
|
||||
- **Plan Mode**: Cline analyzes your request and presents a strategy
|
||||
- **Act Mode**: Cline executes actions directly
|
||||
|
||||
**Auto-approve Toggle** - Press `Shift+Tab` to enable automatic approval for all actions.
|
||||
|
||||
**Slash Commands** - Type `/` for quick access to:
|
||||
- `/settings` - Configure providers, models, and features
|
||||
- `/models` - Quick model switching
|
||||
- `/history` - Browse and resume previous tasks
|
||||
- `/clear` - Start a fresh task
|
||||
- `/help` - Show available commands
|
||||
|
||||
**File Mentions** - Type `@` to reference workspace files:
|
||||
```
|
||||
@src/utils.ts add error handling to this file
|
||||
```
|
||||
|
||||
Cline executes the approved steps—reading files, writing code, running commands. You maintain control throughout the process.
|
||||
**Session Summary** - When you exit with `Ctrl+C`, Cline displays a summary of your session including tasks completed, files modified, and token usage.
|
||||
|
||||
## 2. Headless single-shot: Complete a task without chat
|
||||
### When to Use Interactive Mode
|
||||
|
||||
Use this for automation where you want a one-liner that just does the work.
|
||||
- Exploring a new codebase
|
||||
- Complex refactoring that requires back-and-forth
|
||||
- Learning how Cline approaches problems
|
||||
- Tasks where you want to review before executing
|
||||
|
||||
[Full Interactive Mode Guide →](/cline-cli/interactive-mode)
|
||||
|
||||
## 2. Direct Task Execution
|
||||
|
||||
Execute tasks directly from the command line without entering interactive mode.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
cline instance new --default
|
||||
cline task new -y "Generate unit tests for all Go files"
|
||||
cline "Add unit tests to utils.js"
|
||||
```
|
||||
|
||||
With the `-y` (YOLO) flag, Cline plans and executes autonomously without interactive chat. Perfect for CI, cron jobs, or scripts.
|
||||
Cline analyzes your task, creates a plan, and executes it. You'll be prompted for approval at key decision points.
|
||||
|
||||
Examples:
|
||||
### Piping Context
|
||||
|
||||
Pipe file contents or command output into Cline:
|
||||
|
||||
```bash
|
||||
# Create a complete feature
|
||||
cline task new -y "Create a REST API for user authentication"
|
||||
# Explain a file
|
||||
cat README.md | cline "Summarize this document"
|
||||
|
||||
# Generate documentation
|
||||
cline task new -y "Add JSDoc comments to all functions in src/"
|
||||
# Review git changes
|
||||
git diff | cline "Review these changes and suggest improvements"
|
||||
|
||||
# Refactor code
|
||||
cline task new -y "Convert all var declarations to const/let"
|
||||
# Analyze command output
|
||||
npm test 2>&1 | cline "Analyze these test failures and fix them"
|
||||
```
|
||||
|
||||
Monitor your task with:
|
||||
### Chaining Cline Commands
|
||||
|
||||
Pipe Cline's output into another Cline instance for creative workflows:
|
||||
|
||||
```bash
|
||||
# View task status
|
||||
cline task view
|
||||
# Explain changes, then write a commit message
|
||||
git diff | cline -y "explain these changes" | cline -y "write a commit message for this"
|
||||
|
||||
# Follow task progress in real-time
|
||||
cline task view --follow
|
||||
# Generate code, then write tests
|
||||
cline -y "create a fibonacci function" | cline -y "write unit tests for this code"
|
||||
|
||||
# Fun: Generate a poem about your code
|
||||
git diff | cline -y "explain" | cline -y "write a haiku about this"
|
||||
```
|
||||
|
||||
Press Ctrl+C to exit the view.
|
||||
### Including Images
|
||||
|
||||
<Note>
|
||||
Run YOLO mode with care on a directory or a clean Git branch. You get speed in exchange for oversight, so be ready to revert if needed.
|
||||
</Note>
|
||||
|
||||
## 3. Multi-instance: Run parallel agents
|
||||
|
||||
Multiple instances let you parallelize work on the same project without colliding contexts. Run frontend, backend, and infrastructure tasks simultaneously.
|
||||
|
||||
Create your first instance:
|
||||
Attach images to your task:
|
||||
|
||||
```bash
|
||||
cline instance new
|
||||
cline task -i screenshot.png "Fix the layout issue shown in this screenshot"
|
||||
|
||||
# Or reference inline
|
||||
cline "Fix the UI shown in @./design-mockup.png"
|
||||
```
|
||||
|
||||
This returns an instance address you'll use to target tasks. Attach a task to this instance:
|
||||
### Mode Selection
|
||||
|
||||
```bash
|
||||
# Frontend work on first instance
|
||||
cline task new -y "Build React components"
|
||||
# Start in Plan mode (analyze before acting)
|
||||
cline -p "Design a REST API for user management"
|
||||
|
||||
# Start in Act mode (default)
|
||||
cline -a "Fix the typo in README.md"
|
||||
```
|
||||
|
||||
Create a second instance and set it as default in one command:
|
||||
### When to Use Direct Execution
|
||||
|
||||
- Quick, well-defined tasks
|
||||
- Tasks with sufficient context in the prompt
|
||||
- Scripting and shell workflows
|
||||
- When you don't need interactive conversation
|
||||
|
||||
## 3. Automation & CI/CD
|
||||
|
||||
For fully autonomous operation in scripts, CI/CD pipelines, and automated workflows.
|
||||
|
||||
### YOLO Mode (Yes Mode)
|
||||
|
||||
The `-y` or `--yolo` flag enables fully autonomous operation:
|
||||
|
||||
```bash
|
||||
cline instance new --default
|
||||
cline -y "Run the test suite and fix any failures"
|
||||
```
|
||||
|
||||
Now you can create tasks without specifying the address—they automatically use the default instance:
|
||||
In YOLO mode:
|
||||
- All actions are auto-approved
|
||||
- Output is plain text (non-interactive)
|
||||
- Process exits automatically when complete
|
||||
- Perfect for CI/CD and scripts
|
||||
|
||||
<Warning>
|
||||
Run YOLO mode on a clean git branch or directory. You get speed in exchange for oversight, so be ready to revert if needed.
|
||||
</Warning>
|
||||
|
||||
### JSON Output
|
||||
|
||||
Use `--json` for machine-readable output:
|
||||
|
||||
```bash
|
||||
# Backend work on the new default instance
|
||||
cline task new -y "Implement API endpoints"
|
||||
cline --json "List all TODO comments in the codebase" | jq '.text'
|
||||
```
|
||||
|
||||
List all running instances:
|
||||
JSON output follows the same format as task files in `~/.cline/data/tasks/<id>/ui_messages.json`.
|
||||
|
||||
**JSON Message Schema:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `type` | `"ask"` or `"say"` | Message category |
|
||||
| `text` | `string` | Message content |
|
||||
| `ts` | `number` | Unix timestamp (ms) |
|
||||
| `reasoning` | `string` | (Optional) Model reasoning |
|
||||
| `partial` | `boolean` | (Optional) Streaming flag |
|
||||
|
||||
### Timeout Control
|
||||
|
||||
Set a maximum execution time:
|
||||
|
||||
```bash
|
||||
cline instances list
|
||||
cline -y --timeout 600 "Run full test suite"
|
||||
```
|
||||
|
||||
Stop all instances when done:
|
||||
### Environment Variables
|
||||
|
||||
Control Cline behavior via environment variables:
|
||||
|
||||
**CLINE_DIR** - Custom configuration directory:
|
||||
```bash
|
||||
export CLINE_DIR=/path/to/config
|
||||
cline -y "your task"
|
||||
```
|
||||
|
||||
**CLINE_COMMAND_PERMISSIONS** - Restrict allowed commands:
|
||||
```bash
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"], "deny": ["rm -rf *"]}'
|
||||
cline -y "your task"
|
||||
```
|
||||
|
||||
See [Configuration](/cline-cli/configuration#environment-variables) for full documentation.
|
||||
|
||||
### GitHub Actions Example
|
||||
|
||||
Automate PR reviews with Cline:
|
||||
|
||||
```yaml
|
||||
name: AI Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install Cline
|
||||
run: npm install -g cline
|
||||
|
||||
- name: Configure Cline
|
||||
run: cline auth -p anthropic -k ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Review PR
|
||||
run: |
|
||||
git diff origin/main...HEAD | cline -y "Review this PR for:
|
||||
- Potential bugs
|
||||
- Security issues
|
||||
- Performance concerns
|
||||
- Code style violations
|
||||
|
||||
Provide a summary of findings."
|
||||
```
|
||||
|
||||
### Shell Script Example
|
||||
|
||||
Create a code review script:
|
||||
|
||||
```bash
|
||||
cline instances kill -a
|
||||
#!/bin/bash
|
||||
# review.sh - AI-powered code review
|
||||
|
||||
set -e
|
||||
|
||||
# Get the diff
|
||||
DIFF=$(git diff HEAD~1)
|
||||
|
||||
if [ -z "$DIFF" ]; then
|
||||
echo "No changes to review"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run Cline review
|
||||
echo "$DIFF" | cline -y --json "Review this code diff for issues" | jq -r '.text'
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Keep track of instance addresses returned by `cline instance new`. When scripting multiple agents, store these IDs and direct your tasks to the appropriate instance.
|
||||
</Tip>
|
||||
### When to Use Automation Mode
|
||||
|
||||
## Configuring context window for local providers
|
||||
- CI/CD pipelines
|
||||
- Scheduled maintenance tasks
|
||||
- Batch processing
|
||||
- Any workflow requiring non-interactive execution
|
||||
|
||||
For Ollama and LM Studio, you can configure the model context window via CLI:
|
||||
## Choosing the Right Flow
|
||||
|
||||
```bash
|
||||
# For Ollama
|
||||
cline config s ollama-api-options-ctx-num=32768
|
||||
| Use Case | Recommended Flow |
|
||||
|----------|------------------|
|
||||
| Exploring a new codebase | Interactive Mode |
|
||||
| Complex refactoring | Interactive Mode (Plan first) |
|
||||
| Quick file edits | Direct Execution |
|
||||
| Code review | Direct Execution with pipe |
|
||||
| CI/CD integration | Automation (`-y` flag) |
|
||||
| Scheduled tasks | Automation (`-y` flag) |
|
||||
| Learning Cline | Interactive Mode |
|
||||
|
||||
# For LM Studio
|
||||
cline config s lm-studio-max-tokens=32768
|
||||
```
|
||||
|
||||
For other providers (Anthropic, OpenRouter, etc.), the context window is defined per model in the model metadata and is not user-configurable—Cline uses each model's built-in context limits automatically.
|
||||
|
||||
## Choosing the right flow
|
||||
|
||||
- **Interactive mode**: Best for exploring new problems, learning how Cline works, or when you want to review plans before execution
|
||||
- **Headless single-shot**: Perfect for automation, CI/CD, and tasks where you trust Cline to execute without supervision
|
||||
- **Multi-instance**: Use when you need to parallelize work or maintain separate contexts for different parts of your project
|
||||
|
||||
<Tip>
|
||||
For in-depth commands and flags, check out the [CLI reference](/cline-cli/cli-reference) page for complete documentation on all available options.
|
||||
</Tip>
|
||||
|
||||
## Next steps
|
||||
## Next Steps
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="CLI reference" icon="terminal" href="/cline-cli/cli-reference">
|
||||
Complete command documentation including configuration, instance management, and task commands.
|
||||
<Card title="Interactive Mode" icon="terminal" href="/cline-cli/interactive-mode">
|
||||
Master keyboard shortcuts, slash commands, and file mentions.
|
||||
</Card>
|
||||
|
||||
<Card title="Plan and Act" icon="brain" href="/features/plan-and-act">
|
||||
Deep dive into Plan and Act modes, including when to use each and how to switch between them.
|
||||
<Card title="CLI Reference" icon="book" href="/cline-cli/cli-reference">
|
||||
Complete command documentation with all flags and options.
|
||||
</Card>
|
||||
|
||||
<Card title="YOLO mode" icon="zap" href="/features/yolo-mode">
|
||||
Understand how YOLO mode works and when to use full automation versus manual approval.
|
||||
<Card title="Configuration" icon="gear" href="/cline-cli/configuration">
|
||||
Environment variables, rules, and advanced settings.
|
||||
</Card>
|
||||
|
||||
<Card title="Task management" icon="clipboard-check" href="/features/tasks/task-management">
|
||||
Learn how Cline tracks and manages tasks, including saving and restoring state from checkpoints.
|
||||
<Card title="YOLO Mode" icon="zap" href="/features/yolo-mode">
|
||||
Deep dive into autonomous execution and safety considerations.
|
||||
</Card>
|
||||
</Columns>
|
||||
|
||||
+11
-1
@@ -87,13 +87,19 @@
|
||||
"pages": [
|
||||
"cline-cli/overview",
|
||||
"cline-cli/installation",
|
||||
"cline-cli/interactive-mode",
|
||||
"cline-cli/configuration",
|
||||
"cline-cli/three-core-flows",
|
||||
"cline-cli/acp-editor-integrations",
|
||||
{
|
||||
"group": "CLI Samples",
|
||||
"pages": [
|
||||
"cline-cli/samples/overview",
|
||||
"cline-cli/samples/model-orchestration",
|
||||
"cline-cli/samples/worktree-workflows",
|
||||
"cline-cli/samples/github-issue-rca",
|
||||
"cline-cli/samples/github-integration"
|
||||
"cline-cli/samples/github-integration",
|
||||
"cline-cli/samples/github-pr-review"
|
||||
]
|
||||
},
|
||||
"cline-cli/cli-reference"
|
||||
@@ -438,6 +444,10 @@
|
||||
{
|
||||
"source": "/features/conditional-rules",
|
||||
"destination": "/features/cline-rules/conditional-rules"
|
||||
},
|
||||
{
|
||||
"source": "/cline-cli/authentication",
|
||||
"destination": "/cline-cli/installation"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
|
||||
+18
-14
@@ -5,13 +5,6 @@ sidebarTitle: "Worktrees"
|
||||
|
||||
Worktrees let you work on multiple branches simultaneously, each in its own folder. This enables Cline to work on tasks in parallel across separate VS Code windows, or lets Cline work independently while you continue coding in your main workspace.
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/worktrees-overview.png"
|
||||
alt="Worktrees view showing multiple linked worktrees"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
## What Are Git Worktrees?
|
||||
|
||||
A Git worktree is a linked copy of your repository in a separate folder, checked out to a specific branch. All worktrees share the same Git history and `.git` directory, but each has its own working directory with different code checked out.
|
||||
@@ -123,13 +116,6 @@ When you're done working in a worktree and ready to merge your changes back to t
|
||||
3. Choose whether to delete the worktree after merging
|
||||
4. Click **Merge**
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/worktrees-merge.png"
|
||||
alt="Merge worktree modal"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### Handling Merge Conflicts
|
||||
|
||||
If your branch has conflicts with the main branch, Cline will detect them and show you the conflicting files. You have two options:
|
||||
@@ -226,6 +212,24 @@ Worktrees are not available in certain workspace configurations:
|
||||
|
||||
The Worktrees view will display a message explaining the limitation if either of these applies to your workspace.
|
||||
|
||||
## Using Worktrees with Cline CLI
|
||||
|
||||
Cline CLI's `--cwd` flag unlocks powerful command-line worktree workflows:
|
||||
|
||||
- **Parallel execution**: Run multiple Cline instances simultaneously in different worktrees
|
||||
- **Context piping**: Pipe output from one worktree as input to another for iterative refinement
|
||||
- **Combined with other features**: Use with `--config` for different models per worktree, or `--thinking` for deep analysis
|
||||
|
||||
Example:
|
||||
```bash
|
||||
# Run parallel tasks in different worktrees
|
||||
cline --cwd ~/worktree-a -y "refactor authentication" &
|
||||
cline --cwd ~/worktree-b -y "add unit tests" &
|
||||
wait
|
||||
```
|
||||
|
||||
For complete CLI worktree patterns and examples, see [Worktree Workflows](/cline-cli/samples/worktree-workflows).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
# Implementation Plan: Cline CLI Documentation Update
|
||||
|
||||
[Overview]
|
||||
Update the Cline CLI documentation to reflect the new CLI 2.0 architecture that removes instances, adds a rich TUI experience, and introduces streamlined authentication options.
|
||||
|
||||
The Cline CLI 2.0 has undergone significant changes. The previous architecture used explicit instance management (`cline instance new`, `cline instance list`, etc.) which has been completely removed. The new architecture simplifies the user experience:
|
||||
|
||||
1. **TUI Mode**: Running `cline` without arguments launches a full-featured terminal UI built with React Ink, featuring an animated robot, file mentions (@), slash commands (/), session summaries, and inline settings panels. This provides a "Claude Code-like" experience.
|
||||
|
||||
2. **CLI Mode**: Running `cline "prompt"` executes tasks directly. With `--yolo` flag, it runs non-interactively with output to stdout, making it ideal for CI/CD, piping, and bash scripts.
|
||||
|
||||
3. **Authentication**: Multiple options including Cline account OAuth, ChatGPT subscription OAuth (via Codex), import from existing CLI tools (Codex CLI, OpenCode), and BYO API keys. Supports all providers from the VS Code extension (superset).
|
||||
|
||||
The documentation must clearly separate these two user journeys (TUI interactive vs CLI automation) while documenting deprecated features for users migrating from older versions.
|
||||
|
||||
**Note:** The CLI is now generally available (no longer preview) and supports macOS, Linux, and Windows.
|
||||
|
||||
[Types]
|
||||
No code type changes required - this is a documentation-only update.
|
||||
|
||||
This implementation plan only covers documentation files (`.mdx` files in `docs/cline-cli/`). No TypeScript interfaces, types, or code modifications are needed.
|
||||
|
||||
[Files]
|
||||
Update existing files and create new documentation pages for comprehensive coverage.
|
||||
|
||||
**Files to UPDATE (in-place):**
|
||||
- `docs/cline-cli/overview.mdx` - Remove instance references, reframe around TUI vs CLI modes
|
||||
- `docs/cline-cli/installation.mdx` - Expand with prerequisites, post-install steps, authentication
|
||||
- `docs/cline-cli/three-core-flows.mdx` - Complete rewrite to remove instances, replace with TUI/CLI/Automation flows
|
||||
- `docs/cline-cli/cli-reference.mdx` - Replace outdated man page content with current man page from `cli/man/cline.1.md`
|
||||
|
||||
**Files to CREATE:**
|
||||
- `docs/cline-cli/tui-guide.mdx` - New comprehensive guide for the TUI experience
|
||||
- `docs/cline-cli/authentication.mdx` - New guide covering all auth options
|
||||
- `docs/cline-cli/configuration.mdx` - New guide for `cline config` and settings management
|
||||
|
||||
**Files to MODIFY:**
|
||||
- `docs/docs.json` - Add new pages to navigation under CLI group
|
||||
|
||||
[Functions]
|
||||
No function changes required - documentation only.
|
||||
|
||||
This is a documentation update with no code changes to functions, methods, or handlers.
|
||||
|
||||
[Classes]
|
||||
No class changes required - documentation only.
|
||||
|
||||
This is a documentation update with no code changes to classes or components.
|
||||
|
||||
[Dependencies]
|
||||
No dependency changes required.
|
||||
|
||||
This is a documentation update with no package changes.
|
||||
|
||||
[Testing]
|
||||
Documentation should be verified for accuracy by cross-referencing with source code.
|
||||
|
||||
**Verification steps:**
|
||||
1. Cross-reference all documented features against `cli/src/index.ts` entry point
|
||||
2. Verify keyboard shortcuts against `cli/src/components/ChatView.tsx`
|
||||
3. Verify auth options against `cli/src/components/AuthView.tsx`
|
||||
4. Verify slash commands against `cli/src/components/HelpPanelContent.tsx`
|
||||
5. Verify config options against `cli/src/components/ConfigView.tsx` and `SettingsPanelContent.tsx`
|
||||
6. Verify import sources against `cli/src/utils/import-configs.ts`
|
||||
7. Run `npm run docs:dev` (if available) to preview documentation locally
|
||||
|
||||
**Content accuracy checks:**
|
||||
- [ ] All keyboard shortcuts match source code
|
||||
- [ ] All command flags match `cli/src/index.ts`
|
||||
- [ ] Auth provider list matches `AuthView.tsx`
|
||||
- [ ] Import sources correctly documented (Codex CLI, OpenCode - NOT "Claude Code")
|
||||
- [ ] Deprecated features clearly marked
|
||||
|
||||
[Implementation Order]
|
||||
Execute documentation updates in dependency order to ensure consistency.
|
||||
|
||||
1. **Update `docs/docs.json`** - Add new page entries to navigation first so links work
|
||||
2. **Create `docs/cline-cli/authentication.mdx`** - Auth is foundational, other docs reference it
|
||||
3. **Create `docs/cline-cli/tui-guide.mdx`** - Core new content for TUI users
|
||||
4. **Create `docs/cline-cli/configuration.mdx`** - Config management guide
|
||||
5. **Update `docs/cline-cli/overview.mdx`** - Reframe overview with new architecture
|
||||
6. **Update `docs/cline-cli/installation.mdx`** - Expand installation guide
|
||||
7. **Update `docs/cline-cli/three-core-flows.mdx`** - Rewrite as TUI/CLI/Automation flows
|
||||
8. **Update `docs/cline-cli/cli-reference.mdx`** - Replace with current man page content
|
||||
9. **Verify all cross-references and links work correctly**
|
||||
|
||||
---
|
||||
|
||||
## Detailed File Specifications
|
||||
|
||||
### 1. `docs/docs.json` (UPDATE)
|
||||
|
||||
Add new pages to the CLI navigation group:
|
||||
|
||||
```json
|
||||
{
|
||||
"group": "CLI",
|
||||
"pages": [
|
||||
"cline-cli/overview",
|
||||
"cline-cli/installation",
|
||||
"cline-cli/authentication",
|
||||
"cline-cli/tui-guide",
|
||||
"cline-cli/configuration",
|
||||
"cline-cli/three-core-flows",
|
||||
{
|
||||
"group": "CLI Samples",
|
||||
"pages": [
|
||||
"cline-cli/samples/overview",
|
||||
"cline-cli/samples/github-issue-rca",
|
||||
"cline-cli/samples/github-integration"
|
||||
]
|
||||
},
|
||||
"cline-cli/cli-reference"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `docs/cline-cli/authentication.mdx` (CREATE)
|
||||
|
||||
**Purpose:** Comprehensive guide to all authentication options
|
||||
|
||||
**Sections:**
|
||||
- Quick start (sign in with Cline - recommended)
|
||||
- Sign in with ChatGPT subscription (OpenAI Codex OAuth)
|
||||
- Import from existing CLI tools:
|
||||
- Import from Codex CLI (`~/.codex/auth.json`)
|
||||
- Import from OpenCode (`~/.local/share/opencode/auth.json`)
|
||||
- Bring your own API keys (manual provider configuration)
|
||||
- Supported providers list with examples
|
||||
- Switching providers (`cline auth`)
|
||||
- Quick setup flags (`cline auth -p <provider> -k <key> -m <model>`)
|
||||
|
||||
**Key corrections from user input:**
|
||||
- User said "import from Claude Code" - INCORRECT. Actual sources are:
|
||||
- Codex CLI (OpenAI's CLI tool)
|
||||
- OpenCode
|
||||
- Document the actual import sources from `cli/src/utils/import-configs.ts`
|
||||
|
||||
### 3. `docs/cline-cli/tui-guide.mdx` (CREATE)
|
||||
|
||||
**Purpose:** Guide to the interactive terminal UI experience
|
||||
|
||||
**Sections:**
|
||||
- Launching the TUI (`cline` without arguments)
|
||||
- The welcome screen and robot animation
|
||||
- Input field and message display
|
||||
- Keyboard shortcuts:
|
||||
- `Tab` - Toggle Plan/Act mode
|
||||
- `Shift+Tab` - Toggle auto-approve all
|
||||
- `Enter` - Submit message
|
||||
- `Esc` - Exit/cancel
|
||||
- `↑/↓` - Navigate history
|
||||
- `Home/End` - Cursor movement
|
||||
- `Ctrl+A/E/W/U` - Text editing
|
||||
- File mentions with `@`:
|
||||
- Type `@` to search workspace files
|
||||
- Uses ripgrep for fast searching
|
||||
- Slash commands with `/`:
|
||||
- `/settings` - Open settings panel
|
||||
- `/models` - Quick model switching
|
||||
- `/history` - Browse task history
|
||||
- `/clear` - Start fresh task
|
||||
- `/help` - Show help
|
||||
- `/exit` - Exit CLI
|
||||
- Workflow commands
|
||||
- Settings panel (`/settings`):
|
||||
- API tab (provider, model, thinking)
|
||||
- Auto-approve tab
|
||||
- Features tab
|
||||
- Account tab
|
||||
- Other tab
|
||||
- Session summary on exit
|
||||
- Running multiple instances with `--config`:
|
||||
- Default: settings shared across all instances
|
||||
- Use `cline --config /path/to/config` for isolated configs
|
||||
- Recommend tmux/terminal multiplexing for parallel work
|
||||
|
||||
### 4. `docs/cline-cli/configuration.mdx` (CREATE)
|
||||
|
||||
**Purpose:** Guide to `cline config` command and settings management
|
||||
|
||||
**Sections:**
|
||||
- Running `cline config`
|
||||
- Configuration tabs:
|
||||
- Settings (global state, workspace state)
|
||||
- Rules (`.clinerules` files, Cursor rules, Windsurf rules)
|
||||
- Workflows
|
||||
- Hooks (if enabled)
|
||||
- Skills (if enabled)
|
||||
- Keyboard navigation in config view
|
||||
- Editing configuration values
|
||||
- Configuration directory structure (`~/.cline/data/`)
|
||||
- Environment variables (`CLINE_DIR`, `CLINE_COMMAND_PERMISSIONS`)
|
||||
- Using `--config` flag for separate configurations
|
||||
|
||||
### 5. `docs/cline-cli/overview.mdx` (UPDATE)
|
||||
|
||||
**Changes:**
|
||||
- Remove all references to instances (`cline instance new/list/kill`)
|
||||
- Reframe around two modes: TUI (interactive) and CLI (automation)
|
||||
- Update "What you can build" section to remove multi-instance examples
|
||||
- Add section about new TUI features
|
||||
- Link to new authentication and TUI guide pages
|
||||
- Note deprecation of instance commands
|
||||
|
||||
**New structure:**
|
||||
1. What is Cline CLI?
|
||||
2. Two ways to use Cline CLI:
|
||||
- TUI Mode (interactive development)
|
||||
- CLI Mode (automation and scripting)
|
||||
3. Supported Model Providers
|
||||
4. What you can build
|
||||
5. Learn more (links)
|
||||
|
||||
### 6. `docs/cline-cli/installation.mdx` (UPDATE)
|
||||
|
||||
**Changes:**
|
||||
- Remove "Preview Release - macOS and Linux Only" warning (CLI is now GA and supports Windows)
|
||||
- Add note that CLI supports macOS, Linux, and Windows
|
||||
- Add Node.js version requirement (20+, recommend 22)
|
||||
- Add version specification (`npm install -g cline@2.0.0`)
|
||||
- Add more detail on post-install authentication
|
||||
- Link to new authentication guide
|
||||
- Add troubleshooting tips
|
||||
- Add verification steps
|
||||
|
||||
**New structure:**
|
||||
1. Prerequisites (Node.js version)
|
||||
2. Installation: `npm install -g cline` (or `npm install -g cline@2.0.0`)
|
||||
3. Authentication (`cline auth` - link to auth guide)
|
||||
4. Quick Start (two paths: TUI and CLI)
|
||||
5. Next Steps (links to guides)
|
||||
|
||||
### 7. `docs/cline-cli/three-core-flows.mdx` (UPDATE - Major Rewrite)
|
||||
|
||||
**Complete rewrite removing all instance references.**
|
||||
|
||||
**New title suggestion:** "CLI Workflows" or "Getting Started Workflows"
|
||||
|
||||
**New structure:**
|
||||
1. **Interactive TUI Mode** (replaces old "Interactive mode")
|
||||
- Launch with `cline`
|
||||
- Plan/Act mode toggle (Tab key)
|
||||
- Using slash commands and file mentions
|
||||
- Auto-approve toggle (Shift+Tab)
|
||||
- Session summary on exit (Ctrl+C)
|
||||
2. **Direct Task Execution** (replaces old "Headless single-shot")
|
||||
- `cline "prompt"` syntax
|
||||
- Piping context (`cat file | cline "explain"`)
|
||||
- Piping cline into cline: `git diff | cline -y "explain" | cline -y "write poem"`
|
||||
- Image attachments
|
||||
3. **Automation & CI/CD** (replaces old "Multi-instance")
|
||||
- `--yolo` / `-y` flag for non-interactive mode (also called "yes mode")
|
||||
- `--json` output for parsing (same format as `~/.cline/data/tasks/<id>/ui_messages.json`)
|
||||
- `--timeout` for long-running tasks
|
||||
- Environment variables:
|
||||
- `CLINE_DIR` - custom config directory
|
||||
- `CLINE_COMMAND_PERMISSIONS` - restrict allowed shell commands
|
||||
- Example GitHub Actions workflow for PR review
|
||||
|
||||
**Creative use cases from engineer demo:**
|
||||
- Chain cline commands: `git diff | cline -y "explain" | cline -y "write a poem about this"`
|
||||
- GitHub PR review workflow with `gh` CLI integration
|
||||
|
||||
**Deprecation notice:**
|
||||
Add a callout at the top noting that instance commands (`cline instance new/list/kill`) have been removed in favor of the simpler architecture.
|
||||
|
||||
### 8. `docs/cline-cli/cli-reference.mdx` (UPDATE)
|
||||
|
||||
**Changes:**
|
||||
- Replace the outdated embedded man page with content from `cli/man/cline.1.md`
|
||||
- The current man page in the docs references old instance commands
|
||||
- The actual man page (`cli/man/cline.1.md`) has correct, updated content
|
||||
- Convert man page markdown format to mdx documentation format
|
||||
- Add JSON output schema section
|
||||
- Add environment variables section
|
||||
- Remove all instance command references
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Additional Features from Engineer Demo
|
||||
|
||||
### Man Page
|
||||
- `man cline` - View in-depth documentation in terminal
|
||||
|
||||
### Dev Tools
|
||||
- `cline dev log` - Opens log file for debugging
|
||||
- `cline update` - Check for and install updates
|
||||
|
||||
### JSON Output Format
|
||||
- Same format as saved task files: `~/.cline/data/tasks/<id>/ui_messages.json`
|
||||
- Useful for programmatic use cases
|
||||
- Pipe through `jq` for easier parsing
|
||||
- Example: `cline --json "prompt" | jq '.text'`
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After implementation, verify these user requirements are documented:
|
||||
|
||||
- [x] New TUI experience explained
|
||||
- [x] NPM installation covered
|
||||
- [x] Authorization options:
|
||||
- [x] Sign in with Cline
|
||||
- [x] Sign in with ChatGPT Subscription (Codex OAuth)
|
||||
- [x] Import from Codex CLI (CORRECTED from "Claude Code")
|
||||
- [x] Import from OpenCode
|
||||
- [x] Bring your own API keys
|
||||
- [x] Bedrock support mentioned
|
||||
- [x] `cline auth` for changing providers
|
||||
- [x] Basic CLI usage:
|
||||
- [x] `cline "task"` syntax
|
||||
- [x] Piping context
|
||||
- [x] `--yolo` / `-y` for CI/CD (also called "yes mode")
|
||||
- [x] TUI features:
|
||||
- [x] `cline` alone launches TUI
|
||||
- [x] Tab to toggle Plan/Act mode
|
||||
- [x] Shift+Tab for auto-approve all
|
||||
- [x] Session summary on exit (Ctrl+C)
|
||||
- [x] `--config` for separate configs
|
||||
- [x] Instance deprecation noted
|
||||
- [x] `cline config` for rules, workflows, hooks, skills
|
||||
- [x] @ file mentions with autocomplete (fuzzy search)
|
||||
- [x] / slash commands with autocomplete
|
||||
- [x] `/settings` documented
|
||||
- [x] `/models` documented
|
||||
- [x] `/history` documented
|
||||
- [x] Workflows generate slash commands
|
||||
- [x] /settings panel sections documented (arrow keys to navigate tabs)
|
||||
- [x] Environment variables:
|
||||
- [x] `CLINE_DIR` documented
|
||||
- [x] `CLINE_COMMAND_PERMISSIONS` documented (security measure)
|
||||
- [x] Dev tools:
|
||||
- [x] `cline dev log` documented
|
||||
- [x] `cline update` documented
|
||||
- [x] `man cline` documented
|
||||
- [x] JSON output format documented
|
||||
- [x] Piping cline into cline documented
|
||||
- [x] GitHub Actions PR review example included
|
||||
Generated
+6323
-4324
File diff suppressed because it is too large
Load Diff
+8
-7
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.56.2",
|
||||
"version": "3.57.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"workspaces": [
|
||||
"cli"
|
||||
@@ -393,7 +393,7 @@
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"postcompile-standalone-npm": "node scripts/package-npm.mjs",
|
||||
"dev": "npm run protos && npm run watch",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch": "npx npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.mjs --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
|
||||
@@ -406,16 +406,16 @@
|
||||
"clean:all": "npm run clean:build && npm run clean:deps",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit",
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit && cd ../cli && npx tsc --noEmit",
|
||||
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
|
||||
"lint:proto": "bash ./scripts/proto-lint.sh",
|
||||
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
|
||||
"format:fix": "biome check --changed --no-errors-on-unmatched --files-ignore-unknown=true --write",
|
||||
"fix:all": "biome check --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe",
|
||||
"ci:check-all": "npm-run-all -p check-types lint format",
|
||||
"ci:check-all": "npx npm-run-all -p check-types lint format",
|
||||
"ci:build": "npm run protos && npm run build:webview && node esbuild.mjs && npm run compile-tests",
|
||||
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
"test": "npx npm-run-all test:unit test:integration",
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
@@ -426,7 +426,7 @@
|
||||
"test:e2e": "playwright install && npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:optimal": "npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:ui": "npx tsx scripts/interactive-playwright.ts",
|
||||
"install:all": "npm install && cd webview-ui && npm install && cd ../cli && npm install && cd ..",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
"test:webview": "cd webview-ui && npm run test",
|
||||
@@ -453,7 +453,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.1.4",
|
||||
"@biomejs/biome": "^2.3.14",
|
||||
"@bufbuild/buf": "^1.54.0",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
@@ -465,6 +465,7 @@
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/picomatch": "^4.0.2",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/should": "^11.2.0",
|
||||
|
||||
@@ -41,6 +41,8 @@ service AccountService {
|
||||
rpc openrouterAuthClicked(EmptyRequest) returns (Empty);
|
||||
|
||||
rpc requestyAuthClicked(StringRequest) returns (Empty);
|
||||
|
||||
rpc hicapAuthClicked(EmptyRequest) returns (Empty);
|
||||
|
||||
// Returns a link the webview can use to redirect back to the user's IDE.
|
||||
rpc getRedirectUrl(EmptyRequest) returns (String);
|
||||
|
||||
@@ -36,6 +36,11 @@ service EnvService {
|
||||
|
||||
// Logs a debug message to the host environment's log/output console.
|
||||
rpc debugLog(cline.StringRequest) returns (cline.Empty);
|
||||
|
||||
// Opens an external URL in the default browser.
|
||||
// In remote environments (VS Code Server, SSH, etc.), this routes the URL
|
||||
// to the user's local machine to open in their local browser.
|
||||
rpc openExternal(cline.StringRequest) returns (cline.Empty);
|
||||
}
|
||||
|
||||
message GetHostVersionResponse {
|
||||
|
||||
@@ -130,7 +130,7 @@ async function generateVscodeProtobusServers(protobusServices) {
|
||||
// Create output file
|
||||
const output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
import * as serviceTypes from "src/generated/hosts/vscode/protobus-service-types"
|
||||
import * as serviceTypes from "@generated/hosts/vscode/protobus-service-types"
|
||||
|
||||
${imports.join("\n")}
|
||||
${servers.join("\n")}
|
||||
|
||||
+8
-39
@@ -27,7 +27,7 @@ async function main() {
|
||||
await cleanBuildDir()
|
||||
await buildTypeScriptCli()
|
||||
await copyCliDist()
|
||||
await createNpmPackageJson()
|
||||
await copyPackageJson()
|
||||
await copyReadme()
|
||||
await createNpmIgnoreFile()
|
||||
|
||||
@@ -90,45 +90,14 @@ async function copyCliDist() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create package.json for NPM publication
|
||||
* Reads from cli/package.json and modifies for publication
|
||||
* Copy package.json from cli/ directory
|
||||
*/
|
||||
async function createNpmPackageJson() {
|
||||
console.log("Creating NPM package.json...")
|
||||
|
||||
const sourcePackageJson = path.join(CLI_DIR, "package.json")
|
||||
|
||||
if (!fs.existsSync(sourcePackageJson)) {
|
||||
console.error(`Error: package.json not found at ${sourcePackageJson}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(sourcePackageJson, "utf8"))
|
||||
|
||||
// Modify for NPM publication
|
||||
const npmPkg = {
|
||||
name: "cline", // Change from @cline/cli to cline for NPM
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
main: pkg.main,
|
||||
bin: pkg.bin,
|
||||
type: pkg.type,
|
||||
engines: pkg.engines,
|
||||
keywords: pkg.keywords,
|
||||
author: pkg.author,
|
||||
license: pkg.license,
|
||||
repository: pkg.repository,
|
||||
homepage: pkg.homepage,
|
||||
bugs: pkg.bugs,
|
||||
dependencies: pkg.dependencies,
|
||||
os: ["darwin", "linux"],
|
||||
cpu: ["x64", "arm64"],
|
||||
}
|
||||
|
||||
const destPackageJson = path.join(BUILD_DIR, "package.json")
|
||||
fs.writeFileSync(destPackageJson, JSON.stringify(npmPkg, null, "\t"))
|
||||
|
||||
console.log(`✓ package.json created (name: cline, version: ${pkg.version})`)
|
||||
async function copyPackageJson() {
|
||||
console.log("Copying package.json...")
|
||||
const source = path.join(CLI_DIR, "package.json")
|
||||
const dest = path.join(BUILD_DIR, "package.json")
|
||||
await cpr(source, dest)
|
||||
console.log(`✓ package.json copied`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -259,6 +259,7 @@ function createHandlerForProvider(
|
||||
return new ClineHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
clineAccountId: options.clineAccountId,
|
||||
clineApiKey: options.clineApiKey,
|
||||
ulid: options.ulid,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
|
||||
@@ -8,15 +8,15 @@ import {
|
||||
InvokeModelWithResponseStreamCommand,
|
||||
} from "@aws-sdk/client-bedrock-runtime"
|
||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||
import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { type BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_SUFFIX, type ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI, calculateApiCostQwen } from "@utils/cost"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import type { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import type { ApiStream } from "../transform/stream"
|
||||
|
||||
export interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
@@ -110,9 +110,11 @@ interface ProviderChainOptions {
|
||||
profile?: string
|
||||
}
|
||||
|
||||
// a special jp inference profile was created for sonnet 4.5 & haiku 4.5
|
||||
// a special jp inference profile was created for opus 4.6, sonnet 4.5 & haiku 4.5
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html
|
||||
const JP_SUPPORTED_CRIS_MODELS = [
|
||||
"anthropic.claude-opus-4-6-v1",
|
||||
"anthropic.claude-opus-4-6-v1:1m",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0:1m",
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import { type ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import axios from "axios"
|
||||
import OpenAI from "openai"
|
||||
@@ -8,15 +8,15 @@ import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { buildClineExtraHeaders } from "@/services/EnvUtils"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import type { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import type { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
import type { OpenRouterErrorResponse } from "./types"
|
||||
|
||||
interface ClineHandlerOptions extends CommonApiHandlerOptions {
|
||||
ulid?: string
|
||||
@@ -27,6 +27,7 @@ interface ClineHandlerOptions extends CommonApiHandlerOptions {
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
clineAccountId?: string
|
||||
clineApiKey?: string
|
||||
geminiThinkingLevel?: string
|
||||
}
|
||||
|
||||
@@ -48,7 +49,7 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<OpenAI> {
|
||||
const clineAccountAuthToken = await this._authService.getAuthToken()
|
||||
const clineAccountAuthToken = this.options.clineApiKey || (await this._authService.getAuthToken())
|
||||
if (!clineAccountAuthToken) {
|
||||
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
|
||||
}
|
||||
@@ -108,7 +109,7 @@ export class ClineHandler implements ApiHandler {
|
||||
this.lastGenerationId = undefined
|
||||
this.lastRequestId = undefined
|
||||
|
||||
let didOutputUsage: boolean = false
|
||||
let didOutputUsage = false
|
||||
|
||||
const stream = await createOpenRouterStream(
|
||||
client,
|
||||
@@ -148,11 +149,8 @@ export class ClineHandler implements ApiHandler {
|
||||
const error = choiceWithError.error
|
||||
Logger.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
} else {
|
||||
throw new Error(
|
||||
"Cline Mid-Stream Error: Stream terminated with error status but no error details provided",
|
||||
)
|
||||
}
|
||||
throw new Error("Cline Mid-Stream Error: Stream terminated with error status but no error details provided")
|
||||
}
|
||||
|
||||
const delta = choice?.delta
|
||||
@@ -187,7 +185,7 @@ export class ClineHandler implements ApiHandler {
|
||||
if (
|
||||
"reasoning_details" in delta &&
|
||||
delta.reasoning_details &&
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
delta?.reasoning_details?.length && // exists and non-0
|
||||
!shouldSkipReasoningForModel(this.options.openRouterModelId)
|
||||
) {
|
||||
@@ -199,10 +197,12 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
const modelId = this.getModel().id
|
||||
const isFreeModel = ["kwaipilot/kat-coder-pro", "moonshotai/kimi-k2.5"].includes(modelId)
|
||||
const isFreeModel = ["kwaipilot/kat-coder-pro", "moonshotai/kimi-k2.5", "minimax/minimax-m2.1"].includes(
|
||||
modelId,
|
||||
)
|
||||
|
||||
if (isFreeModel) {
|
||||
totalCost = 0
|
||||
|
||||
@@ -77,6 +77,7 @@ export class RequestyHandler implements ApiHandler {
|
||||
? { thinking: { type: "enabled", budget_tokens: thinkingBudget } }
|
||||
: { thinking: { type: "disabled" } }
|
||||
const thinkingArgs =
|
||||
model.id.includes("claude-opus-4-6") ||
|
||||
model.id.includes("claude-3-7-sonnet") ||
|
||||
model.id.includes("claude-sonnet-4") ||
|
||||
model.id.includes("claude-opus-4") ||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
OPENROUTER_PROVIDER_PREFERENCES,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
openRouterClaudeOpus461mModelId,
|
||||
} from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
@@ -30,8 +31,11 @@ export async function createOpenRouterStream(
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
|
||||
if (isClaudeSonnet1m) {
|
||||
const isClaude1m =
|
||||
model.id === openRouterClaudeSonnet41mModelId ||
|
||||
model.id === openRouterClaudeSonnet451mModelId ||
|
||||
model.id === openRouterClaudeOpus461mModelId
|
||||
if (isClaude1m) {
|
||||
// remove the custom :1m suffix, to create the model id openrouter API expects
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
}
|
||||
@@ -43,6 +47,7 @@ export async function createOpenRouterStream(
|
||||
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
// handles direct model.id match logic
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-opus-4.6":
|
||||
case "anthropic/claude-haiku-4.5":
|
||||
case "anthropic/claude-4.5-haiku":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
@@ -110,6 +115,7 @@ export async function createOpenRouterStream(
|
||||
// (models usually default to max tokens allowed)
|
||||
let maxTokens: number | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-opus-4.6":
|
||||
case "anthropic/claude-haiku-4.5":
|
||||
case "anthropic/claude-4.5-haiku":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
@@ -155,6 +161,7 @@ export async function createOpenRouterStream(
|
||||
|
||||
let reasoning: { max_tokens: number } | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-opus-4.6":
|
||||
case "anthropic/claude-haiku-4.5":
|
||||
case "anthropic/claude-4.5-haiku":
|
||||
case "anthropic/claude-sonnet-4.5":
|
||||
@@ -210,7 +217,7 @@ export async function createOpenRouterStream(
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...(openRouterProviderSorting && !providerPreferences ? { provider: { sort: openRouterProviderSorting } } : {}),
|
||||
...(providerPreferences ? { provider: providerPreferences } : {}),
|
||||
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
|
||||
...(isClaude1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
...(model.id.includes("gemini-3") && geminiThinkingLevel
|
||||
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ModelInfo,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
openRouterClaudeOpus461mModelId,
|
||||
} from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
@@ -28,8 +29,11 @@ export async function createVercelAIGatewayStream(
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
|
||||
if (isClaudeSonnet1m) {
|
||||
const isClaude1m =
|
||||
model.id === openRouterClaudeSonnet41mModelId ||
|
||||
model.id === openRouterClaudeSonnet451mModelId ||
|
||||
model.id === openRouterClaudeOpus461mModelId
|
||||
if (isClaude1m) {
|
||||
// remove the custom :1m suffix, to create the model id the API expects
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Initiates Hicap auth
|
||||
*/
|
||||
export async function hicapAuthClicked(_: Controller, __: EmptyRequest): Promise<Empty> {
|
||||
const callbackUri = await HostProvider.get().getCallbackUrl()
|
||||
const authUri = `https://dashboard.hicap.ai/setup?application=cline&callback_url=${callbackUri}/hicap`
|
||||
|
||||
await openExternal(authUri)
|
||||
|
||||
return {}
|
||||
}
|
||||
@@ -23,9 +23,9 @@ import fs from "fs/promises"
|
||||
import open from "open"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import type { FolderLockWithRetryResult } from "src/core/locks/types"
|
||||
import type * as vscode from "vscode"
|
||||
import { ClineEnv } from "@/config"
|
||||
import type { FolderLockWithRetryResult } from "@/core/locks/types"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
@@ -759,6 +759,30 @@ export class Controller {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Hicap
|
||||
async handleHicapCallback(code: string) {
|
||||
const apiKey: string = code
|
||||
|
||||
const hicap: ApiProvider = "hicap"
|
||||
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
|
||||
// Update API configuration through cache service
|
||||
const currentApiConfiguration = this.stateManager.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...currentApiConfiguration,
|
||||
planModeApiProvider: hicap,
|
||||
actModeApiProvider: hicap,
|
||||
hicapApiKey: apiKey,
|
||||
}
|
||||
this.stateManager.setApiConfiguration(updatedConfig)
|
||||
|
||||
await this.postStateToWebview()
|
||||
this.accountService
|
||||
if (this.task) {
|
||||
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
|
||||
}
|
||||
}
|
||||
|
||||
// Task history
|
||||
|
||||
async getTaskWithId(id: string): Promise<{
|
||||
|
||||
@@ -5,17 +5,46 @@ import { ANTHROPIC_MAX_THINKING_BUDGET, ModelInfo } from "@shared/api"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { parsePrice } from "@utils/model-utils"
|
||||
import axios from "axios"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { basetenModels } from "../../../shared/api"
|
||||
import { Controller } from ".."
|
||||
|
||||
// Track pending refresh promise to prevent duplicate concurrent fetches
|
||||
let pendingRefresh: Promise<Record<string, ModelInfo>> | null = null
|
||||
|
||||
/**
|
||||
* Core function: Refreshes the Baseten models and returns application types
|
||||
* @param controller The controller instance
|
||||
* @returns Record of model ID to ModelInfo (application types)
|
||||
*/
|
||||
export async function refreshBasetenModels(controller: Controller): Promise<Record<string, ModelInfo>> {
|
||||
// Check in-memory cache first
|
||||
const cache = StateManager.get().getModelsCache("baseten")
|
||||
if (cache) {
|
||||
return cache
|
||||
}
|
||||
|
||||
// If a fetch is already in progress, return the same promise
|
||||
if (pendingRefresh) {
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
// Start new fetch and track the promise
|
||||
pendingRefresh = (async () => {
|
||||
try {
|
||||
return await fetchAndCacheModels(controller)
|
||||
} finally {
|
||||
// Clear pending promise when done (success or error)
|
||||
pendingRefresh = null
|
||||
}
|
||||
})()
|
||||
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
async function fetchAndCacheModels(controller: Controller): Promise<Record<string, ModelInfo>> {
|
||||
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.basetenModels)
|
||||
|
||||
// Get the Baseten API key from the controller's state
|
||||
@@ -153,6 +182,9 @@ export async function refreshBasetenModels(controller: Controller): Promise<Reco
|
||||
}
|
||||
}
|
||||
|
||||
// Store in StateManager's in-memory cache
|
||||
StateManager.get().setModelsCache("baseten", typedModels)
|
||||
|
||||
return typedModels
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,47 @@ import { fileExistsAtPath } from "@utils/fs"
|
||||
import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { groqModels } from "../../../shared/api"
|
||||
import { Controller } from ".."
|
||||
|
||||
// Track pending refresh promise to prevent duplicate concurrent fetches
|
||||
let pendingRefresh: Promise<Record<string, ModelInfo>> | null = null
|
||||
|
||||
/**
|
||||
* Core function: Refreshes the Groq models and returns application types
|
||||
* @param controller The controller instance
|
||||
* @returns Record of model ID to ModelInfo (application types)
|
||||
*/
|
||||
export async function refreshGroqModels(controller: Controller): Promise<Record<string, ModelInfo>> {
|
||||
// Check in-memory cache first
|
||||
const cache = StateManager.get().getModelsCache("groq")
|
||||
if (cache) {
|
||||
return cache
|
||||
}
|
||||
|
||||
// If a fetch is already in progress, return the same promise
|
||||
if (pendingRefresh) {
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
// Start new fetch and track the promise
|
||||
pendingRefresh = (async () => {
|
||||
try {
|
||||
return await fetchAndCacheModels(controller)
|
||||
} finally {
|
||||
// Clear pending promise when done (success or error)
|
||||
pendingRefresh = null
|
||||
}
|
||||
})()
|
||||
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
async function fetchAndCacheModels(controller: Controller): Promise<Record<string, ModelInfo>> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.groqModels)
|
||||
|
||||
const groqApiKey = controller.stateManager.getSecretKey("groqApiKey")
|
||||
@@ -83,11 +112,12 @@ export async function refreshGroqModels(controller: Controller): Promise<Record<
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
|
||||
await fs.writeFile(groqModelsFilePath, JSON.stringify(models))
|
||||
Logger.log("Groq models fetched and saved", models)
|
||||
} else {
|
||||
Logger.error("Invalid response from Groq API")
|
||||
}
|
||||
await fs.writeFile(groqModelsFilePath, JSON.stringify(models))
|
||||
Logger.log("Groq models fetched and saved", models)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Error fetching Groq models:", error)
|
||||
@@ -159,6 +189,9 @@ export async function refreshGroqModels(controller: Controller): Promise<Record<
|
||||
}
|
||||
}
|
||||
|
||||
// Store in StateManager's in-memory cache
|
||||
StateManager.get().setModelsCache("groq", typedModels)
|
||||
|
||||
return typedModels
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,10 @@ export async function refreshLiteLlmModels(): Promise<Record<string, ModelInfo>>
|
||||
|
||||
// Use litellm_params.model as the key since that's the actual model ID users select
|
||||
// model_name may not include the region prefix (e.g., "us." for Bedrock models)
|
||||
const modelId = rawModel.litellm_params?.model || rawModel.model_name
|
||||
models[modelId] = modelInfo
|
||||
if (rawModel.litellm_params?.model) {
|
||||
models[rawModel.litellm_params?.model] = modelInfo
|
||||
}
|
||||
models[rawModel.model_name] = modelInfo
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,7 +7,9 @@ import path from "path"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import {
|
||||
ANTHROPIC_MAX_THINKING_BUDGET,
|
||||
CLAUDE_OPUS_1M_TIERS,
|
||||
CLAUDE_SONNET_1M_TIERS,
|
||||
openRouterClaudeOpus461mModelId,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@/shared/api"
|
||||
@@ -72,12 +74,40 @@ interface OpenRouterRawModelInfo {
|
||||
supported_parameters?: OpenRouterSupportedParams[] | null
|
||||
}
|
||||
|
||||
// Track pending refresh promise to prevent duplicate concurrent fetches
|
||||
let pendingRefresh: Promise<Record<string, ModelInfo>> | null = null
|
||||
|
||||
/**
|
||||
* Core function: Refreshes the OpenRouter models and returns application types
|
||||
* @param controller The controller instance
|
||||
* @returns Record of model ID to ModelInfo (application types)
|
||||
*/
|
||||
export async function refreshOpenRouterModels(controller: Controller): Promise<Record<string, ModelInfo>> {
|
||||
// Check in-memory cache first
|
||||
const cache = StateManager.get().getModelsCache("openRouter")
|
||||
if (cache) {
|
||||
return cache
|
||||
}
|
||||
|
||||
// If a fetch is already in progress, return the same promise
|
||||
if (pendingRefresh) {
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
// Start new fetch and track the promise
|
||||
pendingRefresh = (async () => {
|
||||
try {
|
||||
return await fetchAndCacheModels(controller)
|
||||
} finally {
|
||||
// Clear pending promise when done (success or error)
|
||||
pendingRefresh = null
|
||||
}
|
||||
})()
|
||||
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
async function fetchAndCacheModels(controller: Controller): Promise<Record<string, ModelInfo>> {
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
@@ -135,6 +165,12 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
|
||||
modelInfo.cacheWritesPrice = 3.75
|
||||
modelInfo.cacheReadsPrice = 0.3
|
||||
break
|
||||
case "anthropic/claude-opus-4.6":
|
||||
modelInfo.contextWindow = 200_000 // restrict to 200k, 1m variant created below
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 6.25
|
||||
modelInfo.cacheReadsPrice = 0.5
|
||||
break
|
||||
case "anthropic/claude-opus-4.5":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 6.25
|
||||
@@ -227,7 +263,7 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
|
||||
// add custom :1m model variant
|
||||
// add custom :1m model variant for sonnet
|
||||
if (rawModel.id === "anthropic/claude-sonnet-4" || rawModel.id === "anthropic/claude-sonnet-4.5") {
|
||||
const claudeSonnet1mModelInfo = cloneDeep(modelInfo)
|
||||
claudeSonnet1mModelInfo.contextWindow = 1_000_000 // limiting providers to those that support 1m context window
|
||||
@@ -237,12 +273,21 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
|
||||
// sonnet 4.5
|
||||
models[openRouterClaudeSonnet451mModelId] = claudeSonnet1mModelInfo
|
||||
}
|
||||
|
||||
// add custom :1m model variant for opus 4.6
|
||||
if (rawModel.id === "anthropic/claude-opus-4.6") {
|
||||
const claudeOpus1mModelInfo = cloneDeep(modelInfo)
|
||||
claudeOpus1mModelInfo.contextWindow = 1_000_000
|
||||
claudeOpus1mModelInfo.tiers = CLAUDE_OPUS_1M_TIERS
|
||||
models[openRouterClaudeOpus461mModelId] = claudeOpus1mModelInfo
|
||||
}
|
||||
}
|
||||
// Save models and cache them in memory
|
||||
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
|
||||
Logger.log("OpenRouter models fetched and saved")
|
||||
} else {
|
||||
Logger.error("Invalid response from OpenRouter API")
|
||||
throw new Error("Invalid response data when fetching OpenRouter models")
|
||||
}
|
||||
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
|
||||
Logger.log("OpenRouter models fetched and saved")
|
||||
} catch (error) {
|
||||
Logger.error("Error fetching OpenRouter models:", error)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fileExistsAtPath } from "@utils/fs"
|
||||
import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
@@ -75,12 +76,40 @@ function deriveTemperature(modelId: string): number | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Track pending refresh promise to prevent duplicate concurrent fetches
|
||||
let pendingRefresh: Promise<Record<string, ModelInfo>> | null = null
|
||||
|
||||
/**
|
||||
* Core function: Refreshes Vercel AI Gateway models and returns application types
|
||||
* @param _controller The controller instance (unused)
|
||||
* @returns Record of model ID to ModelInfo (application types)
|
||||
*/
|
||||
export async function refreshVercelAiGatewayModels(_controller: Controller): Promise<Record<string, ModelInfo>> {
|
||||
// Check in-memory cache first
|
||||
const cache = StateManager.get().getModelsCache("vercel")
|
||||
if (cache) {
|
||||
return cache
|
||||
}
|
||||
|
||||
// If a fetch is already in progress, return the same promise
|
||||
if (pendingRefresh) {
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
// Start new fetch and track the promise
|
||||
pendingRefresh = (async () => {
|
||||
try {
|
||||
return await fetchAndCacheModels()
|
||||
} finally {
|
||||
// Clear pending promise when done (success or error)
|
||||
pendingRefresh = null
|
||||
}
|
||||
})()
|
||||
|
||||
return pendingRefresh
|
||||
}
|
||||
|
||||
async function fetchAndCacheModels(): Promise<Record<string, ModelInfo>> {
|
||||
const vercelAiGatewayModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.vercelAiGatewayModels)
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
@@ -122,7 +151,7 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
|
||||
await fs.writeFile(vercelAiGatewayModelsFilePath, JSON.stringify(models))
|
||||
Logger.log("Vercel AI Gateway models fetched and saved")
|
||||
} else {
|
||||
Logger.error("Invalid response from Vercel AI Gateway API")
|
||||
throw new Error("Invalid response from Vercel AI Gateway API")
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Error fetching Vercel AI Gateway models:", error)
|
||||
@@ -134,6 +163,9 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
|
||||
}
|
||||
}
|
||||
|
||||
// Store in StateManager's in-memory cache
|
||||
StateManager.get().setModelsCache("vercel", models)
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
|
||||
@@ -62,18 +62,22 @@ export class StateManager {
|
||||
private context: ExtensionContext
|
||||
private isInitialized = false
|
||||
|
||||
// Cache TTL: 1 hour - long enough to prevent duplicate fetches, short enough to see new models
|
||||
private readonly MODEL_CACHE_TTL_MS = 60 * 60 * 1000
|
||||
|
||||
// In-memory model info cache (not persisted to disk)
|
||||
// These are for dynamic providers that fetch models from APIs
|
||||
private modelInfoCache: {
|
||||
openRouterModels: Record<string, ModelInfo> | null
|
||||
groqModels: Record<string, ModelInfo> | null
|
||||
basetenModels: Record<string, ModelInfo> | null
|
||||
huggingFaceModels: Record<string, ModelInfo> | null
|
||||
requestyModels: Record<string, ModelInfo> | null
|
||||
huaweiCloudMaasModels: Record<string, ModelInfo> | null
|
||||
hicapModels: Record<string, ModelInfo> | null
|
||||
aihubmixModels: Record<string, ModelInfo> | null
|
||||
liteLlmModels: Record<string, ModelInfo> | null
|
||||
openRouterModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
groqModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
basetenModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
huggingFaceModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
requestyModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
huaweiCloudMaasModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
hicapModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
aihubmixModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
liteLlmModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
vercelModels: { data: Record<string, ModelInfo>; timestamp: number } | null
|
||||
} = {
|
||||
openRouterModels: null,
|
||||
groqModels: null,
|
||||
@@ -84,6 +88,7 @@ export class StateManager {
|
||||
hicapModels: null,
|
||||
aihubmixModels: null,
|
||||
liteLlmModels: null,
|
||||
vercelModels: null,
|
||||
}
|
||||
|
||||
// Debounced persistence state
|
||||
@@ -417,11 +422,41 @@ export class StateManager {
|
||||
| "huaweiCloudMaas"
|
||||
| "hicap"
|
||||
| "aihubmix"
|
||||
| "liteLlm",
|
||||
| "liteLlm"
|
||||
| "vercel",
|
||||
models: Record<string, ModelInfo>,
|
||||
): void {
|
||||
const cacheKey = `${provider}Models` as keyof typeof this.modelInfoCache
|
||||
this.modelInfoCache[cacheKey] = models
|
||||
this.modelInfoCache[cacheKey] = { data: models, timestamp: Date.now() }
|
||||
}
|
||||
|
||||
getModelsCache(
|
||||
provider:
|
||||
| "openRouter"
|
||||
| "groq"
|
||||
| "baseten"
|
||||
| "huggingFace"
|
||||
| "requesty"
|
||||
| "huaweiCloudMaas"
|
||||
| "hicap"
|
||||
| "aihubmix"
|
||||
| "liteLlm"
|
||||
| "vercel",
|
||||
): Record<string, ModelInfo> | null {
|
||||
const cacheKey = `${provider}Models` as keyof typeof this.modelInfoCache
|
||||
const cached = this.modelInfoCache[cacheKey]
|
||||
|
||||
if (!cached) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if cache has expired
|
||||
if (Date.now() - cached.timestamp > this.MODEL_CACHE_TTL_MS) {
|
||||
this.modelInfoCache[cacheKey] = null
|
||||
return null
|
||||
}
|
||||
|
||||
return cached.data
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,7 +476,19 @@ export class StateManager {
|
||||
modelId: string,
|
||||
): ModelInfo | undefined {
|
||||
const cacheKey = `${provider}Models` as keyof typeof this.modelInfoCache
|
||||
return this.modelInfoCache[cacheKey]?.[modelId]
|
||||
const cached = this.modelInfoCache[cacheKey]
|
||||
|
||||
if (!cached) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Check if cache has expired
|
||||
if (Date.now() - cached.timestamp > this.MODEL_CACHE_TTL_MS) {
|
||||
this.modelInfoCache[cacheKey] = null
|
||||
return undefined
|
||||
}
|
||||
|
||||
return cached.data[modelId]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { synchronizeRemoteRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { RemoteConfig } from "@shared/remote-config/schema"
|
||||
import type { RemoteConfig, S3AccessKeySettings } from "@shared/remote-config/schema"
|
||||
import { ConfiguredAPIKeys, GlobalStateAndSettings, RemoteConfigFields } from "@shared/storage/state-keys"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { getDistinctId } from "@/services/logging/distinctId"
|
||||
@@ -12,10 +12,29 @@ import { ApiProvider } from "@/shared/api"
|
||||
import { isOpenTelemetryConfigValid, remoteConfigToOtelConfig } from "@/shared/services/config/otel-config"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { syncWorker } from "@/shared/services/worker/sync"
|
||||
import { BlobStoreSettings } from "@/shared/storage"
|
||||
import { ensureSettingsDirectoryExists } from "../disk"
|
||||
import { StateManager } from "../StateManager"
|
||||
import { syncRemoteMcpServersToSettings } from "./syncRemoteMcpServers"
|
||||
|
||||
function accessSettingsToBlobStorage(type: BlobStoreSettings["adapterType"], settings: S3AccessKeySettings): BlobStoreSettings {
|
||||
return {
|
||||
adapterType: type,
|
||||
accessKeyId: settings.accessKeyId,
|
||||
secretAccessKey: settings.secretAccessKey,
|
||||
region: settings.region,
|
||||
bucket: settings.bucket,
|
||||
endpoint: settings.endpoint,
|
||||
accountId: settings.accountId,
|
||||
intervalMs: settings.intervalMs,
|
||||
maxRetries: settings.maxRetries,
|
||||
batchSize: settings.batchSize,
|
||||
maxQueueSize: settings.maxQueueSize,
|
||||
maxFailedAgeMs: settings.maxFailedAgeMs,
|
||||
backfillEnabled: settings.backfillEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms RemoteConfig schema to RemoteConfigFields shape
|
||||
* @param remoteConfig The remote configuration object
|
||||
@@ -182,6 +201,17 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
|
||||
}
|
||||
}
|
||||
|
||||
const anthropicSettings = remoteConfig.providerSettings?.Anthropic
|
||||
if (anthropicSettings) {
|
||||
transformed.planModeApiProvider = "anthropic"
|
||||
transformed.actModeApiProvider = "anthropic"
|
||||
providers.push("anthropic")
|
||||
|
||||
if (anthropicSettings.baseUrl) {
|
||||
transformed.anthropicBaseUrl = anthropicSettings.baseUrl
|
||||
}
|
||||
}
|
||||
|
||||
// This line needs to stay here, it is order dependent on the above code checking the configured providers
|
||||
if (providers.length > 0) {
|
||||
transformed.remoteConfiguredProviders = providers
|
||||
@@ -198,21 +228,9 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
|
||||
if (remoteConfig.enterpriseTelemetry?.promptUploading) {
|
||||
const promptUplaoding = remoteConfig.enterpriseTelemetry.promptUploading
|
||||
if (promptUplaoding.type === "s3_access_keys" && promptUplaoding.s3AccessSettings) {
|
||||
transformed.blobStoreConfig = {
|
||||
adapterType: "s3",
|
||||
accessKeyId: promptUplaoding.s3AccessSettings.accessKeyId,
|
||||
secretAccessKey: promptUplaoding.s3AccessSettings.secretAccessKey,
|
||||
region: promptUplaoding.s3AccessSettings.region,
|
||||
bucket: promptUplaoding.s3AccessSettings.bucket,
|
||||
endpoint: promptUplaoding.s3AccessSettings.endpoint,
|
||||
accountId: promptUplaoding.s3AccessSettings.accountId,
|
||||
intervalMs: promptUplaoding.s3AccessSettings.intervalMs,
|
||||
maxRetries: promptUplaoding.s3AccessSettings.maxRetries,
|
||||
batchSize: promptUplaoding.s3AccessSettings.batchSize,
|
||||
maxQueueSize: promptUplaoding.s3AccessSettings.maxQueueSize,
|
||||
maxFailedAgeMs: promptUplaoding.s3AccessSettings.maxFailedAgeMs,
|
||||
backfillEnabled: promptUplaoding.s3AccessSettings.backfillEnabled,
|
||||
}
|
||||
transformed.blobStoreConfig = accessSettingsToBlobStorage("s3", promptUplaoding.s3AccessSettings)
|
||||
} else if (promptUplaoding.type === "r2_access_keys" && promptUplaoding.r2AccessSettings) {
|
||||
transformed.blobStoreConfig = accessSettingsToBlobStorage("r2", promptUplaoding.r2AccessSettings)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as vscode from "vscode"
|
||||
import { HistoryItem } from "@/shared/HistoryItem"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ensureRulesDirectoryExists, readTaskHistoryFromState, writeTaskHistoryToState } from "./disk"
|
||||
import { StateManager } from "./StateManager"
|
||||
|
||||
export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionContext) {
|
||||
// Keys to migrate from workspace storage back to global storage
|
||||
@@ -663,3 +664,14 @@ export async function cleanupMcpMarketplaceCatalogFromGlobalState(context: vscod
|
||||
// Continue execution - cleanup failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupOldApiKey() {
|
||||
try {
|
||||
// Old API Keys were introduced in March 2025 and later replaced with tokens
|
||||
// Now that we have new API keys that are prefixed with `sk_`,
|
||||
// we need to clean up the old ones to free the secret storage
|
||||
StateManager.get().setSecret("clineApiKey", undefined)
|
||||
} catch (error) {
|
||||
Logger.error("Failed to cleanup old clineApiKey", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { ClineAsk, ClineAskUseMcpServer } from "@shared/ExtensionMessage"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { truncateContent } from "@/shared/content-limits"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApproval } from "../../utils"
|
||||
@@ -158,7 +159,10 @@ export class AccessMcpResourceHandler implements IFullyManagedTool {
|
||||
// Display result to user
|
||||
await config.callbacks.say("mcp_server_response", resourceResultPretty)
|
||||
|
||||
// Truncate response if it exceeds 400KB to prevent context overflow
|
||||
const truncatedResult = truncateContent(resourceResultPretty)
|
||||
|
||||
// Return formatted result
|
||||
return formatResponse.toolResult(resourceResultPretty)
|
||||
return formatResponse.toolResult(truncatedResult)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,22 +25,13 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
|
||||
|
||||
/**
|
||||
* Handle partial block streaming for attempt_completion
|
||||
* Matches the original conditional logic structure for command vs no-command cases
|
||||
*/
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
const result = block.params.result
|
||||
const command = block.params.command
|
||||
|
||||
if (!command) {
|
||||
// no command, still outputting partial result
|
||||
await uiHelpers.say(
|
||||
"completion_result",
|
||||
uiHelpers.removeClosingTag(block, "result", result),
|
||||
undefined,
|
||||
undefined,
|
||||
block.partial,
|
||||
)
|
||||
const result = uiHelpers.removeClosingTag(block, "result", block.params.result)
|
||||
if (result) {
|
||||
await uiHelpers.say("completion_result", result, undefined, undefined, block.partial)
|
||||
}
|
||||
// We will handle command in the final execution step
|
||||
}
|
||||
|
||||
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { ClineAsk, ClineAskUseMcpServer } from "@shared/ExtensionMessage"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { truncateContent } from "@/shared/content-limits"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApproval } from "../../utils"
|
||||
@@ -204,6 +205,9 @@ export class UseMcpToolHandler implements IFullyManagedTool {
|
||||
toolResultText += `\n\n[${toolResultImages.length} images were provided in the response, and while they are displayed to the user, you do not have the ability to view them.]`
|
||||
}
|
||||
|
||||
// Truncate response if it exceeds 400KB to prevent context overflow
|
||||
toolResultText = truncateContent(toolResultText)
|
||||
|
||||
// Return formatted result (only pass images if model supports them)
|
||||
return formatResponse.toolResult(toolResultText, supportsImages ? toolResultImages : undefined)
|
||||
} catch (error) {
|
||||
|
||||
+3
-1
@@ -31,6 +31,7 @@ import { HookDiscoveryCache } from "./core/hooks/HookDiscoveryCache"
|
||||
import { StateManager } from "./core/storage/StateManager"
|
||||
import {
|
||||
cleanupMcpMarketplaceCatalogFromGlobalState,
|
||||
cleanupOldApiKey,
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
migrateTaskHistoryToFile,
|
||||
migrateWelcomeViewCompleted,
|
||||
@@ -493,7 +494,7 @@ ${ctx.cellJson || "{}"}
|
||||
// Register the generateGitCommitMessage command handler
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.GenerateCommit, async (scm) => {
|
||||
generateCommitMsg(webview.controller.stateManager, scm)
|
||||
generateCommitMsg(webview.controller, scm)
|
||||
}),
|
||||
vscode.commands.registerCommand(commands.AbortCommit, () => {
|
||||
abortCommitGeneration()
|
||||
@@ -655,6 +656,7 @@ if (IS_DEV) {
|
||||
// VSCode-specific storage migrations
|
||||
async function performStorageMigrations(context: ExtensionContext): Promise<void> {
|
||||
try {
|
||||
cleanupOldApiKey()
|
||||
// Migrate is not done if the new storage does not have the lastShownAnnouncementId flag
|
||||
const hasMigrated = StateManager.get().getGlobalStateKey("lastShownAnnouncementId")
|
||||
if (hasMigrated !== undefined) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user