Compare commits
375 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c569c3a7a9 | |||
| 7a523fbaf6 | |||
| c22ea39dc1 | |||
| c9f23076c2 | |||
| a5f6c1d732 | |||
| 3c37a160ac | |||
| 5c3294051f | |||
| 4c2f28f2af | |||
| 6e016298cb | |||
| 0cd7bebfba | |||
| 363aac61fb | |||
| eeb1cc7da8 | |||
| f760f13de5 | |||
| dd52a4a39c | |||
| 639edb5db6 | |||
| 3eac9b04de | |||
| 09692d7d3a | |||
| c81fa0a9d6 | |||
| 326c9c9f99 | |||
| a4518b90c2 | |||
| 79f4d938e6 | |||
| 37152329cd | |||
| 1332d1d70d | |||
| 6a0d92d683 | |||
| e761a8c252 | |||
| c26d0a076d | |||
| c037619b90 | |||
| a575a76e8f | |||
| b1d15d4fe7 | |||
| a0708e57ee | |||
| ab5796fa72 | |||
| b15c364a62 | |||
| 29dcc4e1e1 | |||
| 126d066893 | |||
| fa3e095a79 | |||
| 4033c83b51 | |||
| 0b7ea86e9b | |||
| 1d9a0b5986 | |||
| c47ffe2861 | |||
| e85d918816 | |||
| 49be10ead8 | |||
| aa9573fb0a | |||
| b913e47332 | |||
| 5be7a1b3cf | |||
| c2e91aa9c9 | |||
| 68b93fcbea | |||
| 0b0e8c36cb | |||
| b0bd0e3974 | |||
| 42b7a1e450 | |||
| 14ccf33d25 | |||
| d9a340523c | |||
| 87b3e79b90 | |||
| 60f2e85fc7 | |||
| 1826d98019 | |||
| af69b30a36 | |||
| e62fbf6b0c | |||
| 64254fc97a | |||
| c312c4aef6 | |||
| fab49e810b | |||
| 2a20523e16 | |||
| 06585821d1 | |||
| 9e802b11da | |||
| afb77c5a8d | |||
| 0a4811222f | |||
| b4ce378e4b | |||
| 2f60a898af | |||
| 8ffd82eda3 | |||
| 81276fdf85 | |||
| 164e11aae1 | |||
| 9792f174b1 | |||
| a590200c64 | |||
| 297a45d73a | |||
| e84de0ab3c | |||
| 515cb81439 | |||
| 550428eabd | |||
| 22c22a1cfc | |||
| 8202479cec | |||
| bcbaa4518d | |||
| 9d799643ba | |||
| 6271c5da37 | |||
| 89aeb3db3d | |||
| 0caeea1b37 | |||
| 852a7c9198 | |||
| c4ef472aeb | |||
| e22c457d19 | |||
| c15287ace0 | |||
| d30f54a89c | |||
| 56b913d951 | |||
| 55a30e0ffa | |||
| a017f3dfd3 | |||
| 41ebe7c9d1 | |||
| 4d11f0d2fa | |||
| 4baa2474eb | |||
| c94e2cf913 | |||
| dbedc6cfaa | |||
| 2ac568e649 | |||
| b13d0e75ea | |||
| 4d395deefd | |||
| 834a5b1df2 | |||
| 3089233298 | |||
| f2b7347a5c | |||
| 04bfef75cf | |||
| 716e8f236b | |||
| f2ddab71f1 | |||
| 0b56a45a65 | |||
| d6ebd2438a | |||
| 4c07c7e5c5 | |||
| ab66a5fd93 | |||
| bd1d6159fc | |||
| 7f2d28716f | |||
| 653727db2a | |||
| d3c2f1878d | |||
| ba92be9401 | |||
| 66eb5a62ba | |||
| 499ee22b3b | |||
| 7abeae5019 | |||
| accf47cb52 | |||
| 556d3e6f79 | |||
| 9e35048db2 | |||
| 9b0f2b82ef | |||
| c2c23054b9 | |||
| 3baaa5c8b4 | |||
| abafcc7290 | |||
| d82aa0add9 | |||
| abe4721a0b | |||
| 60d55b69a8 | |||
| d18e0271d3 | |||
| af71f9da90 | |||
| 2a1c8826aa | |||
| 9a54f2d246 | |||
| d928d58a40 | |||
| 31859b5fda | |||
| 0d5d89e8c7 | |||
| af34451eec | |||
| 027a4f6386 | |||
| 49642882c5 | |||
| 21ed6bc432 | |||
| da2689f885 | |||
| 5049326f02 | |||
| b02ce46a57 | |||
| de974737c8 | |||
| d072156e9a | |||
| 4939309a09 | |||
| 1a07ca7906 | |||
| c1eefbad3f | |||
| 1bfdce9b84 | |||
| b002cdacdb | |||
| cf4005b25e | |||
| 1494d145d5 | |||
| 1ab4b3cc24 | |||
| 535b653228 | |||
| 1335fa5452 | |||
| b2a4395f71 | |||
| ae34a3a8c5 | |||
| 0fb4a6c7e9 | |||
| 855db7d8d8 | |||
| bb375b78ca | |||
| 31af254f0a | |||
| 2745cdd54b | |||
| c94c6fd8f1 | |||
| a87664318e | |||
| ba70718c83 | |||
| cf8dd1c150 | |||
| e714c74ec4 | |||
| a6f8b33895 | |||
| 02abbcf045 | |||
| 8ca2706cca | |||
| e747d211e6 | |||
| 19aa81a1cc | |||
| f5f3654966 | |||
| 344b99988b | |||
| 7d3c39bcdb | |||
| 33c1692f8a | |||
| 3be3ffae4c | |||
| 87c6708fbe | |||
| b296706bc0 | |||
| 7fdc82ae23 | |||
| 246ed87350 | |||
| a28465151f | |||
| f1967bc3a1 | |||
| ccc83d6728 | |||
| c151d719c8 | |||
| cc25833963 | |||
| 2ef3b7cf0f | |||
| 19d7bd5198 | |||
| 121066f938 | |||
| f9b88318ea | |||
| 1d64f64f43 | |||
| 766e2e6a25 | |||
| 8bd7260350 | |||
| a1f4e8b9d4 | |||
| aa3e0860cd | |||
| 67bc9932e5 | |||
| ba98b44504 | |||
| fdeb639f75 | |||
| 76410b42b0 | |||
| e4e347a8a4 | |||
| 59e7c2b7b9 | |||
| 5371377b21 | |||
| 5a444dc30a | |||
| 34c48264c0 | |||
| daf14ef181 | |||
| f1bf9b3f90 | |||
| 6e20047c3e | |||
| 1d4fe88bec | |||
| 74dbbb9d6b | |||
| 3073cf40be | |||
| 50056768d6 | |||
| e7cd32b830 | |||
| d983b451e1 | |||
| bb32aac8e7 | |||
| e6d5f55fb2 | |||
| 93f60ebe5b | |||
| a3e000d5dd | |||
| 0076be531d | |||
| 9de4fc2a2f | |||
| 8a4b87a133 | |||
| 571480e3b6 | |||
| 5c2c264a1d | |||
| 04549cf1e3 | |||
| f2a93b6201 | |||
| 2f09b994dd | |||
| 8aac67bf75 | |||
| 10422dd3fc | |||
| 6191c681ce | |||
| 9ab7b41c24 | |||
| bddfb93b38 | |||
| a219eb8000 | |||
| 515b4e5d6a | |||
| aaa3846b82 | |||
| 8229dcfe88 | |||
| b4ea1a17a5 | |||
| e263c74325 | |||
| 08ca115c66 | |||
| 68ffe20b7d | |||
| 4517408537 | |||
| 42c947afd4 | |||
| d4736afce2 | |||
| b00053ff16 | |||
| 2ab8fcb54f | |||
| e697c12e94 | |||
| 64dcc3fc89 | |||
| 8215d3fdb9 | |||
| 59260cefab | |||
| a767f844b9 | |||
| bb7c3556d6 | |||
| 8b80c337ae | |||
| d0cac53d5b | |||
| 8aedc90214 | |||
| 0c118086df | |||
| 261a7dd505 | |||
| 9abf2f567a | |||
| 42de7c81b4 | |||
| 06b710f62b | |||
| ea9b8fe0b1 | |||
| 05213f2a71 | |||
| d5bad1357d | |||
| a4b1549dac | |||
| e015ce94c0 | |||
| be076cf407 | |||
| 580db36476 | |||
| 8f8b98bb58 | |||
| 36022438cb | |||
| c7afb61e28 | |||
| dfd113a6e5 | |||
| 3698d2356c | |||
| ff20c4addc | |||
| 8eeeabb966 | |||
| 9b1dc5bd92 | |||
| 1cfff0a45f | |||
| e7d00dec2d | |||
| 2c5748ccfd | |||
| 5196adce33 | |||
| d95d86f329 | |||
| f6eb3aa386 | |||
| 5c4b9e54c2 | |||
| 29a1d08685 | |||
| 9664ddd106 | |||
| 3c1327b115 | |||
| bb993e4a9a | |||
| 473b3d0204 | |||
| 7e68614631 | |||
| a19e9907d5 | |||
| 19f74cac01 | |||
| b877abc708 | |||
| c90e64e763 | |||
| e02e1eca7f | |||
| f8925fb8fd | |||
| ac81aeaf4e | |||
| f12b5a1573 | |||
| a66b57ef51 | |||
| 944ed41f4a | |||
| 970e941e57 | |||
| 45abe977e4 | |||
| 5755b30bce | |||
| 5fde21dffd | |||
| cf9f2a8630 | |||
| 5a3416ff09 | |||
| 826b2b1276 | |||
| 8da38b2a2e | |||
| cb121170cb | |||
| e9d2d344c6 | |||
| 69fb954a6c | |||
| e9e616e317 | |||
| 545ac29e07 | |||
| d38489aebc | |||
| 8d47026640 | |||
| 268cd5c527 | |||
| c7c4e43322 | |||
| aae9d432fd | |||
| 604dbd7bb0 | |||
| 062a32f93d | |||
| 535b29f465 | |||
| a8027dc570 | |||
| 978a8a0aa6 | |||
| 0cd462a414 | |||
| 65dbd85a92 | |||
| ee1bb2f788 | |||
| 6f69ffb16f | |||
| f91769bda7 | |||
| 7692adacf5 | |||
| a98faf5af4 | |||
| e3f4ce618f | |||
| dcf519d2f7 | |||
| 3ef4aea0f7 | |||
| a820026e0b | |||
| 29d1b0507c | |||
| 929d13a4dd | |||
| c729e8c7c6 | |||
| 737452b2b1 | |||
| ba6a72cf15 | |||
| 65a0c35163 | |||
| 0c8e02c6e4 | |||
| b636018ef5 | |||
| cfc6b0d7f5 | |||
| f116a6323d | |||
| 46d3b2a3ed | |||
| 9679917532 | |||
| 89bf81f7f6 | |||
| afe01df8b4 | |||
| eb1325686e | |||
| 7c7962ce0f | |||
| 0707df2205 | |||
| ca87c21b77 | |||
| d6f736e8d5 | |||
| 4336471d84 | |||
| 3191e23c1d | |||
| c5f12b8dc6 | |||
| b21ff1e44a | |||
| 2860ffe147 | |||
| 2b25ef63b5 | |||
| e70d60d5c4 | |||
| bddbea04ef | |||
| 176ccedb2a | |||
| 63276aba70 | |||
| ae0f2557fe | |||
| 0d9909c80e | |||
| c0b4081a53 | |||
| 0c0ba93a41 | |||
| 513c518d19 | |||
| 03d6561383 | |||
| 8f8c4561a6 | |||
| 45d751b913 | |||
| 915259ca96 | |||
| 46ef8b10b0 | |||
| e4e07fc0d3 | |||
| 738e959030 | |||
| 16e1c02b98 | |||
| 9e3c3982ec | |||
| 2ed5ce9b15 | |||
| 7dceaea056 | |||
| b351a8b92b | |||
| 78c3664bf3 | |||
| ec543a230f | |||
| 43fabaab8e |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added getCwdHash proto
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add OpenTelemetry integration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
removed multi-root feature flag
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add version information injection to CLI build script. The CLI binaries now include version, commit hash, build date, and builder information extracted from package.json and git, improving debugging and version tracking capabilities.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Baseten Provider Model APIs Deprecation
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
allowing user to uncheck requesty base url
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added folder locking, task locking, and checkpoints locking to cline-core
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added updateApiConfigurationPartial with FieldMask to allow for partial ApiProvider updates
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add auto-retry with exponential backof for failed API requests
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added new settings for future subagent PR
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added subscribeToCheckpoints proto
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improved system prompt guidance for proactively limiting command output when large results are anticipated
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
OpenTelemetry settings schema
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add UserAgent to Bedrock Client
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
auto-cleanup stale default instance config
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added GPT-5 as reasoning model in openai.ts to pass correct parameters to SDK.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add interactive provider configuration wizard with add/list capabilities, support for 8 API providers (Anthropic, OpenAI, OpenAI Native, OpenRouter, X AI, AWS Bedrock, Google Gemini, Ollama), and UpdateSettings gRPC implementation for persisting configurations to Cline Core state.
|
||||
@@ -1,54 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse Hook Example
|
||||
#
|
||||
# This hook runs AFTER a tool is executed. It can:
|
||||
# 1. Observe tool results and outcomes
|
||||
# 2. Add context for FUTURE tool uses via contextModification
|
||||
# 3. Log or track tool usage patterns
|
||||
#
|
||||
# IMPORTANT: Context injection affects FUTURE AI decisions, not the current tool execution.
|
||||
# The tool has already completed when this hook runs.
|
||||
|
||||
# Read the hook input (JSON via stdin)
|
||||
echo "PostToolUse running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
# Extract tool information
|
||||
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName // "unknown"')
|
||||
parameters=$(echo "$input" | jq -r '.postToolUse.parameters // {}')
|
||||
result=$(echo "$input" | jq -r '.postToolUse.result // ""')
|
||||
success=$(echo "$input" | jq -r '.postToolUse.success // false')
|
||||
execution_time=$(echo "$input" | jq -r '.postToolUse.executionTimeMs // 0')
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
# Example 1: Learning from file operations
|
||||
# Track successful file creations to build context about project structure
|
||||
# if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
|
||||
# path=$(echo "$parameters" | jq -r '.path // ""')
|
||||
# cat <<EOF
|
||||
# {
|
||||
# "shouldContinue": true,
|
||||
# "contextModification": "FILE_OPERATIONS: Successfully created '$path'. Future operations should maintain consistency with this file's patterns and structure."
|
||||
# }
|
||||
# EOF
|
||||
# exit 0
|
||||
# fi
|
||||
|
||||
# Example 2: Performance monitoring
|
||||
# Warn about slow operations
|
||||
# if [[ "$execution_time" -gt 5000 ]]; then
|
||||
# cat <<EOF
|
||||
# {
|
||||
# "shouldContinue": true,
|
||||
# "contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms to complete. Consider optimizing future similar operations or breaking them into smaller steps."
|
||||
# }
|
||||
# EOF
|
||||
# exit 0
|
||||
# fi
|
||||
|
||||
# Example 3: Context injection for future tool uses
|
||||
# The context will be available in the NEXT API request
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "TOOL_RESULT: The tool '$tool_name' completed with success=$success. Consider validating the results before proceeding to the next step."
|
||||
"cancel": false,
|
||||
"contextModification": "PostToolUse response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "PostToolUse hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse Hook Example
|
||||
#
|
||||
# This hook runs BEFORE a tool is executed. It can:
|
||||
# 1. Block execution by returning {"shouldContinue": false}
|
||||
# 2. Add context for FUTURE tool uses via contextModification
|
||||
# 3. Validate tool parameters
|
||||
#
|
||||
# IMPORTANT: Context injection affects FUTURE AI decisions, not the current tool execution.
|
||||
# The tool parameters are already determined when this hook runs.
|
||||
|
||||
# Read the hook input (JSON via stdin)
|
||||
echo "PreToolUse running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
# Extract tool information
|
||||
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName // "unknown"')
|
||||
parameters=$(echo "$input" | jq -r '.preToolUse.parameters // {}')
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
# Example 1: Validation - Block invalid operations
|
||||
# Uncomment to prevent creating .js files in a TypeScript project
|
||||
# if [[ "$tool_name" == "write_to_file" ]]; then
|
||||
# path=$(echo "$parameters" | jq -r '.path // ""')
|
||||
# if [[ "$path" == *.js ]]; then
|
||||
# cat <<EOF
|
||||
# {
|
||||
# "shouldContinue": false,
|
||||
# "errorMessage": "VALIDATION FAILED: Cannot create .js files in TypeScript project. Please use .ts extension instead.",
|
||||
# "contextModification": "WORKSPACE_RULES: This is a strict TypeScript project. All new files must use .ts or .tsx extensions."
|
||||
# }
|
||||
# EOF
|
||||
# exit 0
|
||||
# fi
|
||||
# fi
|
||||
|
||||
# Example 2: Context injection for future tool uses
|
||||
# The context will be available in the NEXT API request after this tool completes
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "WORKSPACE_RULES: [For future tool uses] This is a TypeScript React project. When creating files, use .ts/.tsx extensions and include detailed comments explaining the purpose and usage of each function."
|
||||
"cancel": false,
|
||||
"contextModification": "PreToolUse response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "PreToolUse hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
## Overview
|
||||
|
||||
Cline hooks allow you to execute custom scripts at specific points in the agentic workflow. Hooks can be placed in either:
|
||||
- **Global hooks directory**: `~/Documents/Cline/Rules/Hooks/` (applies to all workspaces)
|
||||
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to specific workspace)
|
||||
- **Global hooks directory**: `~/Documents/Cline/Hooks/` (applies to all workspaces)
|
||||
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to the workspace the repo is part of)
|
||||
|
||||
Hooks run automatically when enabled.
|
||||
|
||||
@@ -17,17 +17,54 @@ Hooks run automatically when enabled.
|
||||
|
||||
## Available Hooks
|
||||
|
||||
### TaskStart Hook
|
||||
- **When**: Runs when a NEW task is started (not when resuming)
|
||||
- **Purpose**: Initialize task context, validate task requirements, set up environment
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/TaskStart`
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskStart`
|
||||
|
||||
### TaskResume Hook
|
||||
- **When**: Runs when an EXISTING task is resumed (after user clicks resume button)
|
||||
- **Purpose**: Validate resumed task state, restore context, check for changes since last run
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/TaskResume`
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskResume`
|
||||
|
||||
### TaskCancel Hook
|
||||
- **When**: Runs when a task is cancelled or a hook is aborted by the user (only if there's actual active work or work was started)
|
||||
- **Purpose**: Clean up resources, log cancellation, save state
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/TaskCancel`
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskCancel`
|
||||
- **Note**: This hook is NOT cancellable
|
||||
|
||||
### TaskComplete Hook (coming soon!)
|
||||
- **When**: Runs when a task is marked as complete
|
||||
- **Purpose**: Log completion status, perform final cleanup, generate reports
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/TaskComplete`
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskComplete`
|
||||
|
||||
### UserPromptSubmit Hook
|
||||
- **When**: Runs when the user submits a prompt/message (initial task, resume, or feedback)
|
||||
- **Purpose**: Validate user input, preprocess prompts, add context to user messages
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/UserPromptSubmit`
|
||||
- **Workspace Location**: `.clinerules/hooks/UserPromptSubmit`
|
||||
|
||||
### PreToolUse Hook
|
||||
- **When**: Runs BEFORE a tool is executed
|
||||
- **Purpose**: Validate parameters, block execution, or add context
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/PreToolUse` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/PreToolUse` (all platforms)
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/PreToolUse`
|
||||
- **Workspace Location**: `.clinerules/hooks/PreToolUse`
|
||||
|
||||
### PostToolUse Hook
|
||||
- **When**: Runs AFTER a tool completes
|
||||
- **Purpose**: Observe results, track patterns, or add context
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/PostToolUse` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/PostToolUse` (all platforms)
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/PostToolUse`
|
||||
- **Workspace Location**: `.clinerules/hooks/PostToolUse`
|
||||
|
||||
### PreCompact Hook (coming soon!)
|
||||
- **When**: Runs BEFORE the conversation context is compacted/truncated
|
||||
- **Purpose**: Observe compaction events, log context management, track token usage
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/PreCompact`
|
||||
- **Workspace Location**: `.clinerules/hooks/PreCompact`
|
||||
|
||||
## Cross-Platform Hook Format
|
||||
|
||||
@@ -37,13 +74,12 @@ Cline uses a git-style approach for hooks that works consistently across all pla
|
||||
- **No file extensions**: Hooks are named exactly `PreToolUse` or `PostToolUse` (no `.bat`, `.cmd`, `.sh` etc.)
|
||||
- **Shebang required**: First line must be a shebang (e.g., `#!/usr/bin/env bash` or `#!/usr/bin/env node`)
|
||||
- **Executable on Unix**: On Unix/Linux/macOS, hooks must be executable: `chmod +x PreToolUse`
|
||||
- **Windows**: No special permissions needed - hooks are executed through the shell
|
||||
- **Windows**: Not currently supported.
|
||||
|
||||
### How It Works
|
||||
|
||||
Like git hooks, Cline executes hook files through a shell that interprets the shebang line:
|
||||
- On Unix/Linux/macOS: Native shell execution with shebang support
|
||||
- On Windows: Shell execution handles shebang interpretation
|
||||
|
||||
This means:
|
||||
- ✅ Same hook script works on all platforms
|
||||
@@ -55,16 +91,10 @@ This means:
|
||||
**On Unix/Linux/macOS:**
|
||||
```bash
|
||||
# Create hook file
|
||||
nano ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
nano ~/Documents/Cline/Hooks/PreToolUse
|
||||
|
||||
# Make executable
|
||||
chmod +x ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
```
|
||||
|
||||
**On Windows:**
|
||||
```batch
|
||||
REM Create hook file (note: no file extension)
|
||||
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
|
||||
chmod +x ~/Documents/Cline/Hooks/PreToolUse
|
||||
```
|
||||
|
||||
## Context Injection Timing
|
||||
@@ -107,11 +137,46 @@ All hooks receive:
|
||||
```json
|
||||
{
|
||||
"clineVersion": "string",
|
||||
"hookName": "PreToolUse" | "PostToolUse",
|
||||
"hookName": "TaskStart" | "TaskResume" | "TaskCancel" | "TaskComplete" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PreCompact",
|
||||
"timestamp": "string",
|
||||
"taskId": "string",
|
||||
"workspaceRoots": ["string"],
|
||||
"userId": "string",
|
||||
"taskStart": { // Only for TaskStart
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string",
|
||||
"initialTask": "string"
|
||||
}
|
||||
},
|
||||
"taskResume": { // Only for TaskResume
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string"
|
||||
},
|
||||
"previousState": {
|
||||
"lastMessageTs": "string",
|
||||
"messageCount": "string",
|
||||
"conversationHistoryDeleted": "string"
|
||||
}
|
||||
},
|
||||
"taskCancel": { // Only for TaskCancel
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string",
|
||||
"completionStatus": "string"
|
||||
}
|
||||
},
|
||||
"taskComplete": { // Only for TaskComplete
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string"
|
||||
}
|
||||
},
|
||||
"userPromptSubmit": { // Only for UserPromptSubmit
|
||||
"prompt": "string",
|
||||
"attachments": ["string"]
|
||||
},
|
||||
"preToolUse": { // Only for PreToolUse
|
||||
"toolName": "string",
|
||||
"parameters": {}
|
||||
@@ -122,6 +187,11 @@ All hooks receive:
|
||||
"result": "string",
|
||||
"success": boolean,
|
||||
"executionTimeMs": number
|
||||
},
|
||||
"preCompact": { // Only for PreCompact
|
||||
"contextSize": number,
|
||||
"messagesToCompact": number,
|
||||
"compactionStrategy": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -131,38 +201,21 @@ All hooks receive:
|
||||
All hooks must return:
|
||||
```json
|
||||
{
|
||||
"shouldContinue": boolean, // Required: Allow or block execution
|
||||
"contextModification": "string", // Optional: Context for future tool uses
|
||||
"cancel": boolean, // Required: false to continue, true to block execution
|
||||
"contextModification": "string", // Optional: Context for future AI decisions
|
||||
"errorMessage": "string" // Optional: Error details if blocking
|
||||
}
|
||||
```
|
||||
|
||||
## Context Modification Format
|
||||
|
||||
Use structured prefixes to help the AI understand context type:
|
||||
|
||||
- `WORKSPACE_RULES:` - Project conventions and requirements
|
||||
- `FILE_OPERATIONS:` - File creation/modification patterns
|
||||
- `TOOL_RESULT:` - Outcomes of tool executions
|
||||
- `PERFORMANCE:` - Performance concerns
|
||||
- `VALIDATION:` - Validation results
|
||||
- Custom prefixes as needed
|
||||
|
||||
Example:
|
||||
```bash
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "WORKSPACE_RULES: This is a TypeScript project. All new files must use .ts or .tsx extensions."
|
||||
}
|
||||
EOF
|
||||
```
|
||||
**Note**: The `cancel` field works as follows:
|
||||
- `false` (or omitted): Allow execution to continue
|
||||
- `true`: Block execution and show error message to user
|
||||
|
||||
## Hook Execution Limits
|
||||
|
||||
- **Timeout**: Hooks must complete within 30 seconds
|
||||
- **Context Size**: Context modifications are limited to 50KB
|
||||
- **Error Handling**: Unexpected file system errors are propagated; expected errors (file not found, permission denied) are handled silently
|
||||
- **Timeout**: Hooks must complete within 30 seconds (configurable via `HOOK_EXECUTION_TIMEOUT_MS`)
|
||||
- **Context Size**: Context modifications are limited to 50KB (configurable via `MAX_CONTEXT_MODIFICATION_SIZE`)
|
||||
- **Error Handling**: Expected errors (file not found, permission denied, not a directory) are handled silently; unexpected file system errors are propagated
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
@@ -177,15 +230,15 @@ path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
|
||||
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": false,
|
||||
"cancel": true,
|
||||
"errorMessage": "Cannot create .js files in TypeScript project",
|
||||
"contextModification": "WORKSPACE_RULES: Use .ts/.tsx extensions only"
|
||||
"contextModification": "Use .ts/.tsx extensions only"
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
### 2. Context Building - Learn from Operations
|
||||
@@ -200,12 +253,12 @@ path=$(echo "$input" | jq -r '.postToolUse.parameters.path // ""')
|
||||
if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "FILE_OPERATIONS: Created '$path'. Maintain consistency with this file's patterns in future operations."
|
||||
"cancel": false,
|
||||
"contextModification": "Created '$path'. Maintain consistency with this file's patterns in future operations."
|
||||
}
|
||||
EOF
|
||||
else
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -220,12 +273,12 @@ tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
|
||||
if [[ "$execution_time" -gt 5000 ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
|
||||
"cancel": false,
|
||||
"contextModification": "Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
|
||||
}
|
||||
EOF
|
||||
else
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -239,7 +292,7 @@ input=$(cat)
|
||||
echo "$input" >> ~/.cline/hook-logs/tool-usage.jsonl
|
||||
|
||||
# Allow execution
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
## Global vs Workspace Hooks
|
||||
@@ -247,44 +300,40 @@ echo '{"shouldContinue": true}'
|
||||
Cline supports two levels of hooks:
|
||||
|
||||
### Global Hooks
|
||||
- **Location**: `~/Documents/Cline/Rules/Hooks/` (macOS/Linux) or `%USERPROFILE%\Documents\Cline\Rules\Hooks\` (Windows)
|
||||
- **Location**: `~/Documents/Cline/Hooks/` (macOS/Linux)
|
||||
- **Scope**: Apply to ALL workspaces and projects
|
||||
- **Use Case**: Organization-wide policies, personal preferences, universal validations
|
||||
- **Priority**: Execute FIRST, before workspace hooks
|
||||
- **Priority**: Order not guaranteed when combined with workspace hooks
|
||||
|
||||
### Workspace Hooks
|
||||
- **Location**: `.clinerules/hooks/` in each workspace root
|
||||
- **Scope**: Apply only to the specific workspace
|
||||
- **Use Case**: Project-specific rules, team conventions, repository requirements
|
||||
- **Priority**: Execute AFTER global hooks
|
||||
- **Priority**: Order not guaranteed when combined with global hooks
|
||||
|
||||
### Hook Execution
|
||||
|
||||
When multiple hooks exist (global and/or workspace):
|
||||
- All hooks for a given step (PreToolUse or PostToolUse) are executed
|
||||
- **Execution order is not guaranteed** - hooks may run concurrently
|
||||
- If ALL hooks allow execution (`shouldContinue: true`), the tool proceeds
|
||||
- If ANY hook blocks (`shouldContinue: false`), execution is blocked
|
||||
- All hooks for a given step are executed **concurrently** using `Promise.all`
|
||||
- **Execution order is not guaranteed** - hooks run in parallel
|
||||
- If ALL hooks allow execution (`cancel: false`), the tool proceeds
|
||||
- If ANY hook blocks (`cancel: true`), execution is blocked
|
||||
|
||||
**Result Combination:**
|
||||
- `shouldContinue`: Must be `true` from ALL hooks for execution to proceed
|
||||
- `contextModification`: All context strings are concatenated
|
||||
- `errorMessage`: All error messages are concatenated
|
||||
- `cancel`: If ANY hook returns `true`, execution is blocked
|
||||
- `contextModification`: All context strings are concatenated with double newlines (`\n\n`)
|
||||
- `errorMessage`: All error messages are concatenated with single newlines (`\n`)
|
||||
|
||||
### Setting Up Global Hooks
|
||||
|
||||
1. The global hooks directory is automatically created at:
|
||||
- macOS/Linux: `~/Documents/Cline/Rules/Hooks/`
|
||||
- Windows: `%USERPROFILE%\Documents\Cline\Rules\Hooks\`
|
||||
- macOS/Linux: `~/Documents/Cline/Hooks/`
|
||||
|
||||
2. Add your hook script:
|
||||
```bash
|
||||
# Unix/Linux/macOS
|
||||
nano ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
chmod +x ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
|
||||
# Windows
|
||||
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
|
||||
nano ~/Documents/Cline/Hooks/PreToolUse
|
||||
chmod +x ~/Documents/Cline/Hooks/PreToolUse
|
||||
```
|
||||
|
||||
3. Enable hooks in Cline settings
|
||||
@@ -294,18 +343,18 @@ When multiple hooks exist (global and/or workspace):
|
||||
**Global Hook** (applies to all projects):
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
# ~/Documents/Cline/Hooks/PreToolUse
|
||||
# Universal rule: Never delete package.json
|
||||
input=$(cat)
|
||||
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
|
||||
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
|
||||
|
||||
if [[ "$tool_name" == "write_to_file" && "$path" == *"package.json"* ]]; then
|
||||
echo '{"shouldContinue": false, "errorMessage": "Global policy: Cannot modify package.json"}'
|
||||
echo '{"cancel": true, "errorMessage": "Global policy: Cannot modify package.json"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
**Workspace Hook** (applies to specific project):
|
||||
@@ -318,11 +367,11 @@ tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
|
||||
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
|
||||
|
||||
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
|
||||
echo '{"shouldContinue": false, "errorMessage": "Project rule: Use .ts files only"}'
|
||||
echo '{"cancel": true, "errorMessage": "Project rule: Use .ts files only"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
**All hooks must allow execution for the tool to proceed.** Hooks may execute concurrently.
|
||||
@@ -331,7 +380,7 @@ echo '{"shouldContinue": true}'
|
||||
|
||||
If you have multiple workspace roots, you can place hooks in each root's `.clinerules/hooks/` directory. All hooks (global and workspace) may execute concurrently. Their results will be combined:
|
||||
|
||||
- **shouldContinue**: If ANY hook returns false, execution is blocked
|
||||
- **cancel**: If ANY hook returns `true`, execution is blocked
|
||||
- **contextModification**: All context modifications are concatenated
|
||||
- **errorMessage**: All error messages are concatenated
|
||||
|
||||
@@ -352,7 +401,6 @@ If you have multiple workspace roots, you can place hooks in each root's `.cline
|
||||
|
||||
### Context Not Affecting Behavior
|
||||
- Remember: context affects FUTURE decisions, not the current tool
|
||||
- Use PreToolUse for validation (blocking) if you need immediate effect
|
||||
- Ensure context modifications are clear and actionable
|
||||
- Check that context isn't being truncated (50KB limit)
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "TaskCancel running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "TaskCancel response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "TaskCancel hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "TaskResume running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "TaskResume response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "TaskResume hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "TaskStart running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "TaskStart response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "TaskStart hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "UserPromptSubmit running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "UserPromptSubmit response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "UserPromptSubmit hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -0,0 +1,90 @@
|
||||
# Networking & Proxy Support
|
||||
|
||||
To ensure Cline works correctly in all environments (VSCode, JetBrains, CLI) and with various network configurations (especially corporate proxies), strictly follow these guidelines for all network activity.
|
||||
|
||||
In extension code, do NOT use the global `fetch` or a default `axios` instance. (Note, `shared/net.ts` is exempt from these rules because it sets up the fetch wrappers.) In Webview code, you SHOULD use global `fetch`.
|
||||
|
||||
Global `fetch` and default `axios` do not automatically pick up proxy configurations in all environments (specifically JetBrains and CLI). You MUST use the provided utilities in `@/shared/net` which handle proxy agent configuration. In the webview, the browser/embedder handles proxies.
|
||||
|
||||
## Guidelines
|
||||
|
||||
### 1. Using `fetch`
|
||||
|
||||
Instead of `fetch(...)`, import the proxy-aware wrapper:
|
||||
|
||||
```typescript
|
||||
import { fetch } from '@/shared/net'
|
||||
|
||||
// Usage is identical to global fetch
|
||||
const response = await fetch('https://api.example.com/data')
|
||||
```
|
||||
|
||||
### 2. Using `axios`
|
||||
|
||||
When using `axios`, you must apply the settings from `getAxiosSettings()`:
|
||||
|
||||
```typescript
|
||||
import axios from 'axios'
|
||||
import { getAxiosSettings } from '@/shared/net'
|
||||
|
||||
const response = await axios.get('https://api.example.com/data', {
|
||||
headers: { 'Authorization': '...' },
|
||||
...getAxiosSettings() // <--- CRITICAL: Injects the proxy agent if needed
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Third-Party Clients (OpenAI, Ollama, etc.)
|
||||
|
||||
Most API client libraries allow you to customize the `fetch` implementation. You **MUST** pass the proxy-aware `fetch` to these clients.
|
||||
|
||||
**Example (OpenAI):**
|
||||
```typescript
|
||||
import OpenAI from "openai"
|
||||
import { fetch } from "@/shared/net"
|
||||
|
||||
this.client = new OpenAI({
|
||||
apiKey: '...',
|
||||
fetch, // <--- CRITICAL: Pass our fetch wrapper
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Tests
|
||||
|
||||
Use `mockFetchForTesting` to mock the underlying fetch implementation.
|
||||
|
||||
**Example (callback):**
|
||||
|
||||
```
|
||||
import { mockFetchForTesting } from "@/shared/net"
|
||||
|
||||
...
|
||||
let mockFetch = ...
|
||||
mockFetchForTesting(mockFetch, () => {
|
||||
// This calls mockFetch
|
||||
fetch('https://foo.example').then(...)
|
||||
})
|
||||
// Original fetch is restored immediately when the call returns.
|
||||
```
|
||||
|
||||
**Example (Promise):**
|
||||
|
||||
```
|
||||
import { mockFetchForTesting } from "@/shared/net"
|
||||
|
||||
...
|
||||
let mockFetch = ...
|
||||
await mockFetchForTesting(mockFetch, async () => {
|
||||
await ...
|
||||
// This calls mockFetch
|
||||
await fetch('https://foo.example')
|
||||
...
|
||||
})
|
||||
// Original fetch is restored when the Promise from the callback settles
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
If you are adding a new network call or integration:
|
||||
1. Check `@/shared/net.ts` is imported.
|
||||
2. Ensure `fetch` or `getAxiosSettings` is being used.
|
||||
3. Verify that third-party clients are configured to use the custom fetch.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Address PR Comments
|
||||
|
||||
Review and address all comments on the current branch's PR.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Get the current branch name and find the associated PR:
|
||||
```bash
|
||||
gh pr view --json number,title,body
|
||||
```
|
||||
|
||||
2. Understand the PR context:
|
||||
- Get the full diff: `git diff origin/main...HEAD`
|
||||
- Read the changed files to understand what the PR is doing
|
||||
- Read related files if needed to understand the broader context
|
||||
- Understand the intent and spirit of the changes, not just the code
|
||||
|
||||
3. Fetch all PR comments:
|
||||
- Inline comments: `gh api repos/{owner}/{repo}/pulls/{pr_number}/comments`
|
||||
- General comments: `gh pr view {pr_number} --json comments,reviews`
|
||||
|
||||
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (changeset-bot, CI status, etc.).
|
||||
|
||||
5. **Wait for my approval** before proceeding.
|
||||
|
||||
6. After approval:
|
||||
- Apply code changes and commit
|
||||
- Reply to comments that were addressed or intentionally skipped
|
||||
- Push commits
|
||||
@@ -122,9 +122,9 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
title="Previous Updates:"
|
||||
classNames={{
|
||||
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
|
||||
title: "font-bold text-[var(--vscode-foreground)]",
|
||||
title: "font-bold text-(--vscode-foreground)",
|
||||
indicator:
|
||||
"text-[var(--vscode-foreground)] mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
|
||||
"text-(--vscode-foreground) mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
|
||||
}}>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Find Best Reviewers for Current Branch
|
||||
|
||||
Analyze my current branch to find the best people to review my PR based on **domain expertise** and git history.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Get the current branch name and verify it's not `main`
|
||||
2. Get the diff between the current branch and `origin/main`:
|
||||
- Use `git diff origin/main...HEAD --name-only` to get changed files
|
||||
- Use `git diff origin/main...HEAD` to understand the nature/spirit of the changes
|
||||
3. **Identify the domain/feature area** being changed:
|
||||
- Read the diff carefully to understand WHAT is being changed conceptually (e.g., "slash commands", "authentication", "API client", "UI components")
|
||||
- This semantic understanding is crucial for finding the right reviewers
|
||||
4. Find domain experts by searching for related files and their contributors:
|
||||
- Identify all files related to the feature/domain (not just the ones changed)
|
||||
- Example: if changing slash commands, find ALL slash-command related files across the codebase
|
||||
- Use `git log --format="%an <%ae>" -- <related-files-pattern>` to find who has expertise in that domain
|
||||
5. For additional context, also gather:
|
||||
- `git blame -L <start>,<end> origin/main -- <file-path>` for exact lines changed
|
||||
- Recent commit activity on related files
|
||||
6. Score and rank contributors by:
|
||||
- **Highest weight: Domain expertise** - who has the most commits to files in this feature area (even files not touched by this PR)
|
||||
- **Medium weight: Direct file expertise** - commits to the specific files being changed
|
||||
- **Lower weight: Line-level ownership** - authored the exact lines being modified
|
||||
7. Exclude myself (check against my git config user.email)
|
||||
8. Present the top 5 reviewers as an ordered list
|
||||
|
||||
## Output Format
|
||||
|
||||
Output an ordered list:
|
||||
|
||||
1. **Name** - Domain expert: 15 commits to slash-command related files, authored core parsing logic
|
||||
2. **Name** - 8 commits to affected files, recently added the feature being modified
|
||||
3. ...
|
||||
|
||||
## Commands Reference
|
||||
```bash
|
||||
git config user.email
|
||||
git diff origin/main...HEAD --name-only
|
||||
git diff origin/main...HEAD
|
||||
# Find related files for a domain (adjust pattern based on what you learn from the diff)
|
||||
find . -type f \( -name "*slash-command*" -o -name "*SlashCommand*" \) | head -20
|
||||
# Get contributors for related files
|
||||
find . -type f \( -name "*slash-command*" -o -name "*SlashCommand*" \) -print0 | xargs -0 git log --format="%an <%ae>" -- | sort | uniq -c | sort -rn
|
||||
git log --format="%an <%ae>" -- <file> | sort | uniq -c | sort -rn
|
||||
git blame -L 10,20 origin/main -- <file>
|
||||
```
|
||||
|
||||
Do NOT ask questions - analyze the changes, identify the domain, and output the reviewer list.
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install changeset
|
||||
run: npm ci
|
||||
|
||||
# Check if there are any new changesets to process
|
||||
- name: Check for changesets
|
||||
|
||||
@@ -74,10 +74,9 @@ jobs:
|
||||
CLINE_ENVIRONMENT: production
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
|
||||
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
OTEL_METRIC_EXPORT_INTERVAL: ${{ secrets.OTEL_METRIC_EXPORT_INTERVAL }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
|
||||
@@ -60,11 +60,11 @@ jobs:
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
run: npm install --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
run: cd webview-ui && npm install --include=optional
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
@@ -99,12 +99,11 @@ jobs:
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
|
||||
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
OTEL_METRIC_EXPORT_INTERVAL: ${{ secrets.OTEL_METRIC_EXPORT_INTERVAL }}
|
||||
run: |
|
||||
# Required to generate the .vsix
|
||||
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
@@ -165,6 +165,27 @@
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": "Open Storybook",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"storybook"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/webview-ui",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"serverReadyAction": {
|
||||
"pattern": "Local:.*http://localhost:([0-9]+)",
|
||||
"uriFormat": "http://localhost:%s",
|
||||
"action": "openExternally"
|
||||
},
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -263,6 +263,26 @@
|
||||
"watch"
|
||||
],
|
||||
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "storybook",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": false,
|
||||
"label": "npm: storybook",
|
||||
"dependsOn": [
|
||||
"npm: protos",
|
||||
"npm: build:webview"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -20,6 +20,9 @@ eslint-rules/**
|
||||
.husky/**
|
||||
.env
|
||||
|
||||
# cli
|
||||
cli/**
|
||||
|
||||
# Custom
|
||||
**/demo.gif
|
||||
.nvmrc
|
||||
|
||||
@@ -1,5 +1,158 @@
|
||||
# Changelog
|
||||
|
||||
## [3.40.0]
|
||||
|
||||
- Fix highlighted text flashing when task header is collapsed
|
||||
- Add X-Cerebras-3rd-Party-Integration header to Cerebras API requests
|
||||
- Add microwave family system prompt configuration
|
||||
- Remove tooltips from auto approve menu
|
||||
- Fix Standalone, ensure cwd is the install dir to find resources reliably
|
||||
- Fix a bug where terminal commands with double quotes are broken when "Terminal Execution Mode" is set to "Background Exec"
|
||||
- Add support for slash commands anywhere in a message, not just at the beginning. This matches the behavior of @ mentions for a more flexible input experience.
|
||||
- Add bottom padding to the last message to fix last response text getting cut off by auto approve settings bar.
|
||||
- Add default thinking level for Gemini 3 Pro models in Gemini provider
|
||||
|
||||
## [3.39.2]
|
||||
|
||||
- Fix for microwave model and thinking settings
|
||||
|
||||
## [3.39.1]
|
||||
|
||||
- Fix Openrouter and Cline Provider model info
|
||||
|
||||
## [3.39.0]
|
||||
|
||||
- Add Explain Changes feature
|
||||
- Add microwave Stealth model
|
||||
- Add Tabbed Model Picker with Recommended and Free tabs
|
||||
- Add support to View remote rules and workflows in the editor
|
||||
- Enable NTC (Native Tool Calling) by default
|
||||
- Bug fixes and improvements for LiteLLM provider
|
||||
|
||||
## [3.38.3]
|
||||
|
||||
- Task export feature now opens the task directory, allowing easy access to the full task files
|
||||
- Add Grok 4.1 and Grok Code to XAI provider
|
||||
- Enabled native tool calling for Baseten and Kimi K2 models
|
||||
- Add thinking level to Gemini 3.0 Pro preview
|
||||
- Expanded Hooks functionality
|
||||
- Removed Task Timeline from Task Header
|
||||
- Bug fix for slash commands
|
||||
- Bug fixes for Vertex provider
|
||||
- Bug fixes for thinking/reasoning issues across multiple providers when using native tool calling
|
||||
- Bug fixes for terminal usage on Windows devices
|
||||
|
||||
## [3.38.2]
|
||||
|
||||
- Add Claude Opus 4.5
|
||||
|
||||
## [3.38.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed handling of 'signature' field in sanitizeAnthropicContentBlock to properly preserve it when thinking is enabled, as required by Anthropic's API.
|
||||
|
||||
## [3.38.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Gemini 3 Pro Preview model
|
||||
- AquaVoice Avalon model for voice-to-text dictation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Automatic context truncation when AWS Bedrock token usage rate limits are exceeded
|
||||
- Removed new_task tool from system prompts, updated slash command prompts, and added helper function for native tool calling validation
|
||||
|
||||
## [3.37.1]
|
||||
|
||||
- Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
|
||||
- Add AGENTS.md support
|
||||
- feat(models): Add free minimax/mimax-m2 model to the model picker
|
||||
|
||||
## [3.37.0]
|
||||
|
||||
### Added
|
||||
|
||||
- GPT-5.1 with model-specific prompting: tailored system prompts, tool usage, focus chain, and deep-planning optimizations
|
||||
- Nous Research provider with Hermes 4 model family and custom system prompts
|
||||
- Switched to Aqua Voice's Avalon model in speech to text transcription
|
||||
- Added Linux support for speech to text
|
||||
- Model-family breakouts for deep-planning prompting, laying groundwork for enhanced slash commands
|
||||
- Expanded HTTP proxy support throughout the codebase
|
||||
- Improved focus chain prompting for frontier models (Anthropic, OpenAI, Gemini, xAI)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Duplicate tool results prevention through existence checking
|
||||
- XML entity escaping in model content processor
|
||||
- Commit message generation in command palette
|
||||
- OpenAI Compatible provider temperature parameter type conversion
|
||||
|
||||
## Documentation
|
||||
|
||||
- Added missing proto generation step in CONTRIBUTING.md
|
||||
- New `npm run dev` script for streamlined terminal workflow (fixes #7335)
|
||||
|
||||
## [3.36.1]
|
||||
|
||||
- fix: remove native tool calling support from Gemini and XAI provider due to invalid tool names issues
|
||||
- fix: disable native tool callings for grok code models
|
||||
- Add MCP tool usage to GLM
|
||||
- Removes reasoning_details content field from Anthropic providers
|
||||
|
||||
## [3.36.0]
|
||||
|
||||
- Add: Hooks allow you to inject custom logic into Cline's workflow
|
||||
- Add: new provider AIhubmix
|
||||
- Add: Use http_proxy, https_proxy and no_proxy in JetBrains
|
||||
- Fix: Oca Token Refresh logic
|
||||
- Fix: issues where assistant message with empty content is added to conversation history
|
||||
- Fix: bug where the checkbox shows in the model selector dropdown
|
||||
- Fix: Switch from defaultUserAgentProvider to customUserAgent for Bedrock
|
||||
- Fix: support for `<think>` tags for better compatibility with open-source models
|
||||
- Fix: refinements to the GLM-4.6 system prompt
|
||||
|
||||
## [3.35.1]
|
||||
|
||||
- Add: Hicap API integration as provider
|
||||
- Fix: enable Add Header button in OpenAICompatibleProvider UI
|
||||
- Fix: Remove orphaned tool_results after truncation and empty content field issues in native tool call
|
||||
- Fix: render model description in markdown
|
||||
|
||||
## [3.35.0]
|
||||
|
||||
- Add native tool calling support with configurable setting.
|
||||
- Auto-approve is now always-on with a redesigned expanding menu. Settings simplified and notifications moved to General Settings.
|
||||
- added zai-glm-4.6 as a Cerebras model
|
||||
- Created GPT5 family specific system prompt template
|
||||
- Fix: show reasoning budget slider to models with valid thinking config
|
||||
- Requesty base URL, and API key fixes
|
||||
- Delete all Auth Tokens when logging out
|
||||
- Support for <think> tags for models that prefer that over <thinking>
|
||||
|
||||
## [3.34.1]
|
||||
|
||||
- Added support for MiniMax provider with MiniMax-M2 model
|
||||
- Remove Cline/code-supernova-1-million model
|
||||
- Changes to allow users to manually enter model names (eg. presets) when using OpenRouter
|
||||
|
||||
## [3.34.0]
|
||||
|
||||
- Cline Teams is now free through 2025 for unlimited users. Includes Jetbrains, RBAC, centralized billing and more.
|
||||
- Use the “exacto” versions of GLM-4.6, Kimi-K2, and Qwen3-Coder in the Cline provider for the best balance of cost, speed, accuracy and tool-calling.
|
||||
|
||||
## [3.33.1]
|
||||
|
||||
- Fix CLI installation copy text
|
||||
|
||||
## [3.33.0]
|
||||
|
||||
- Added Cline CLI (Preview)
|
||||
- Added Subagent support (Experimental)
|
||||
- Added Multi-Root Workspaces support (Enable in feature settings)
|
||||
- Add auto-retry with exponential backof for failed API requests
|
||||
|
||||
## [3.32.8]
|
||||
|
||||
- Add Claude Haiku 4.5 support
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file is the secret sauce for working effectively in this codebase. It captures tribal knowledge—the nuanced, non-obvious patterns that make the difference between a quick fix and hours of back-and-forth & human intervention.
|
||||
|
||||
**When to add to this file:**
|
||||
- User had to intervene, correct, or hand-hold
|
||||
- Multiple back-and-forth attempts were needed to get something working
|
||||
- You discovered something that required reading many files to understand
|
||||
- A change touched files you wouldn't have guessed
|
||||
- Something worked differently than you expected
|
||||
- User explicitly asks to "add this to CLAUDE.md"
|
||||
|
||||
**Proactively suggest additions** when any of the above happen—don't wait to be asked.
|
||||
|
||||
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
|
||||
|
||||
## gRPC/Protobuf Communication
|
||||
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
|
||||
|
||||
**Proto files live in `proto/`** (e.g., `proto/cline/task.proto`, `proto/cline/ui.proto`)
|
||||
- Each feature domain has its own `.proto` file
|
||||
- For simple data, use shared types in `proto/cline/common.proto` (`StringRequest`, `Empty`, `Int64Request`)
|
||||
- For complex data, define custom messages in the feature's `.proto` file
|
||||
- Naming: Services `PascalCaseService`, RPCs `camelCase`, Messages `PascalCase`
|
||||
- For streaming responses, use `stream` keyword (see `subscribeToAuthCallback` in `account.proto`)
|
||||
|
||||
**Run `npm run protos`** after any proto changes—generates types in:
|
||||
- `src/shared/proto/` - Shared type definitions
|
||||
- `src/generated/grpc-js/` - Service implementations
|
||||
- `src/generated/nice-grpc/` - Promise-based clients
|
||||
- `src/generated/hosts/` - Generated handlers
|
||||
|
||||
**Adding new enum values** (like a new `ClineSay` type) requires updating conversion mappings in `src/shared/proto-conversions/cline-message.ts`
|
||||
|
||||
**Adding new RPC methods** requires:
|
||||
- Handler in `src/core/controller/<domain>/`
|
||||
- Call from webview via generated client: `UiServiceClient.scrollToSettings(StringRequest.create({ value: "browser" }))`
|
||||
|
||||
**Example—the `explain-changes` feature touched:**
|
||||
- `proto/cline/task.proto` - Added `ExplainChangesRequest` message and `explainChanges` RPC
|
||||
- `proto/cline/ui.proto` - Added `GENERATE_EXPLANATION = 29` to `ClineSay` enum
|
||||
- `src/shared/ExtensionMessage.ts` - Added `ClineSayGenerateExplanation` type
|
||||
- `src/shared/proto-conversions/cline-message.ts` - Added mapping for new say type
|
||||
- `src/core/controller/task/explainChanges.ts` - Handler implementation
|
||||
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
|
||||
|
||||
## Adding Tools to System Prompt
|
||||
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
|
||||
|
||||
1. **Add to `ClineDefaultTool` enum** in `src/shared/tools.ts`
|
||||
2. **Tool definition** in `src/core/prompts/system-prompt/tools/` (create file like `generate_explanation.ts`)
|
||||
- Define variants for each `ModelFamily` (generic, next-gen, xs, etc.)
|
||||
- Export variants array (e.g., `export const my_tool_variants = [GENERIC, NATIVE_NEXT_GEN, XS]`)
|
||||
- **Fallback behavior**: If a variant isn't defined for a model family, `ClineToolSet.getToolByNameWithFallback()` automatically falls back to GENERIC. So you only need to export `[GENERIC]` unless the tool needs model-specific behavior.
|
||||
3. **Register in `src/core/prompts/system-prompt/tools/init.ts`** - Import and spread into `allToolVariants`
|
||||
4. **Add to variant configs** - Each model family has its own config in `src/core/prompts/system-prompt/variants/*/config.ts`. Add your tool's enum to the `.tools()` list:
|
||||
- `generic/config.ts`, `next-gen/config.ts`, `gpt-5/config.ts`, `native-gpt-5/config.ts`, `native-gpt-5-1/config.ts`, `native-next-gen/config.ts`, `gemini-3/config.ts`, `glm/config.ts`, `hermes/config.ts`, `xs/config.ts`
|
||||
- **Important**: If you add to a variant's config, make sure the tool spec exports a variant for that ModelFamily (or relies on GENERIC fallback)
|
||||
5. **Create handler** in `src/core/task/tools/handlers/`
|
||||
6. **Wire up in `ToolExecutor.ts`** if needed for execution flow
|
||||
7. **Add to tool parsing** in `src/core/assistant-message/index.ts` if needed
|
||||
8. **If tool has UI feedback**: add `ClineSay` enum in proto, update `src/shared/ExtensionMessage.ts`, update `src/shared/proto-conversions/cline-message.ts`, update `webview-ui/src/components/chat/ChatRow.tsx`
|
||||
|
||||
## Modifying System Prompt
|
||||
**Read these first:** `src/core/prompts/system-prompt/README.md`, `tools/README.md`, `__tests__/README.md`
|
||||
|
||||
System prompt is modular: **components** (reusable sections) + **variants** (model-specific configs) + **templates** (with `{{PLACEHOLDER}}` resolution).
|
||||
|
||||
**Key directories:**
|
||||
- `components/` - Shared sections: `rules.ts`, `capabilities.ts`, `editing_files.ts`, etc.
|
||||
- `variants/` - Model-specific: `generic/`, `next-gen/`, `xs/`, `gpt-5/`, `gemini-3/`, `hermes/`, `glm/`, etc.
|
||||
- `templates/` - Template engine and placeholder definitions
|
||||
|
||||
**Variant tiers (ask user which to modify):**
|
||||
- **Next-gen** (Claude 4, GPT-5, Gemini 2.5): `next-gen/`, `native-next-gen/`, `native-gpt-5/`, `native-gpt-5-1/`, `gemini-3/`, `gpt-5/`
|
||||
- **Standard** (default fallback): `generic/`
|
||||
- **Local/small models**: `xs/`, `hermes/`, `glm/`
|
||||
|
||||
**How overrides work:** Variants can override components via `componentOverrides` in their `config.ts`, or provide a custom template in `template.ts` (e.g., `next-gen/template.ts` exports `rules_template`). If no override, the shared component from `components/` is used.
|
||||
|
||||
**Example: Adding a rule to RULES section**
|
||||
1. Check if variant overrides rules: look for `rules_template` in `variants/*/template.ts` or `componentOverrides.RULES` in `config.ts`
|
||||
2. If shared: modify `components/rules.ts`
|
||||
3. If overridden: modify that variant's template
|
||||
4. XS variant is special—has heavily condensed inline content in `template.ts`
|
||||
|
||||
**After any changes, regenerate snapshots:**
|
||||
```bash
|
||||
UPDATE_SNAPSHOTS=true npm run test:unit
|
||||
```
|
||||
Snapshots live in `__tests__/__snapshots__/`. Tests validate across model families and context variations (browser, MCP, focus chain).
|
||||
|
||||
## Modifying Default Slash Commands
|
||||
Three places need updates:
|
||||
- `src/core/slash-commands/index.ts` - Command definitions
|
||||
- `src/core/prompts/commands.ts` - System prompt integration
|
||||
- `webview-ui/src/utils/slash-commands.ts` - Webview autocomplete
|
||||
|
||||
## ChatRow Cancelled/Interrupted States
|
||||
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
|
||||
|
||||
**The pattern:**
|
||||
1. A message has a `status` field (e.g., `"generating"`, `"complete"`, `"error"`) stored in `message.text` as JSON
|
||||
2. When cancelled mid-operation, the status stays `"generating"` forever—no one updates it
|
||||
3. To detect cancellation, check TWO conditions:
|
||||
- `!isLast` — if this message is no longer the last message, something else happened after it (interrupted)
|
||||
- `lastModifiedMessage?.ask === "resume_task" || "resume_completed_task"` — task was just cancelled and is waiting to resume
|
||||
|
||||
**Example from `generate_explanation`:**
|
||||
```tsx
|
||||
const wasCancelled =
|
||||
explanationInfo.status === "generating" &&
|
||||
(!isLast ||
|
||||
lastModifiedMessage?.ask === "resume_task" ||
|
||||
lastModifiedMessage?.ask === "resume_completed_task")
|
||||
const isGenerating = explanationInfo.status === "generating" && !wasCancelled
|
||||
```
|
||||
|
||||
**Why both checks?**
|
||||
- `!isLast` catches: cancelled → resumed → did other stuff → this old message is stale
|
||||
- `lastModifiedMessage?.ask === "resume_task"` catches: just cancelled, hasn't resumed yet, this message is still technically "last"
|
||||
|
||||
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
|
||||
|
||||
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
|
||||
@@ -46,7 +46,11 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
```bash
|
||||
npm run install:all
|
||||
```
|
||||
4. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
|
||||
4. Generate Protocol Buffer files (required before first build):
|
||||
```bash
|
||||
npm run protos
|
||||
```
|
||||
5. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
|
||||
|
||||
|
||||
|
||||
@@ -85,8 +89,10 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
|
||||
2. **Local Development**
|
||||
- Run `npm run install:all` to install dependencies
|
||||
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
|
||||
- Run `npm run test` to run tests locally
|
||||
- Run → Start Debugging or `>Debug: Select and Start Debugging` and wait for a new VS Code instance to open
|
||||
- **Terminal Workflow**: Use `npm run dev` (generates protos + runs watch mode) or `npm run watch` (if protos already generated)
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
|
||||
3. **Linux-specific Setup**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
|
||||
</sub></div>
|
||||
|
||||
# Cline – \#1 on OpenRouter
|
||||
# Cline
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
@@ -43,7 +43,7 @@ Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.c
|
||||
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
|
||||
|
||||
> [!TIP]
|
||||
> Use the `CMD/CTRL + Shift + P` shortcut to open the command palette and type "Cline: Open In New Tab" to open the extension as a tab in your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
|
||||
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
|
||||
|
||||
---
|
||||
|
||||
@@ -141,6 +141,11 @@ For example, when working with a local web server, you can use 'Restore Workspac
|
||||
|
||||
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
|
||||
|
||||
## Enterprise
|
||||
|
||||
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>
|
||||
<json>
|
||||
<![CDATA[
|
||||
{
|
||||
"fontFamily": "cline-bot",
|
||||
"majorVersion": 1,
|
||||
"minorVersion": 0,
|
||||
"fontURL": "https://cline.bot",
|
||||
"designerURL": "https://cline.bot",
|
||||
"licenseURL": "https://cline.bot",
|
||||
"version": "Version 1.0",
|
||||
"fontId": "cline-bot",
|
||||
"psName": "cline-bot",
|
||||
"subFamily": "Regular",
|
||||
"fullName": "cline-bot",
|
||||
"description": "Font generated by IcoMoon."
|
||||
}
|
||||
]]>
|
||||
</json>
|
||||
</metadata>
|
||||
<defs>
|
||||
<font id="cline-bot" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="cline" data-tags="cline" horiz-adv-x="977" d="M964.553 383.11l-60.285 121.406v69.495c0 115.545-92.939 209.321-207.647 209.321h-102.986c7.536 15.071 11.722 32.654 11.722 51.074 0 64.471-51.912 116.383-115.545 116.383s-115.545-51.912-115.545-116.383 4.186-35.166 11.722-51.074h-102.986c-114.708 0-207.647-93.776-207.647-209.321v-69.495l-61.959-121.406c-5.861-11.722-5.861-26.793 0-38.515l61.959-119.732v-69.495c0-115.545 92.939-209.321 207.647-209.321h415.294c114.708 0 207.647 93.776 207.647 209.321v69.495l60.285 119.732c5.861 11.722 5.861 25.956 0 38.515v0zM426.178 284.311c0-52.749-42.702-95.451-94.613-95.451s-94.613 42.702-94.613 95.451v169.132c0 52.749 42.702 95.451 94.613 95.451s94.613-42.702 94.613-95.451v-169.132zM731.787 284.311c0-52.749-42.702-95.451-94.613-95.451s-94.613 42.702-94.613 95.451v169.132c0 52.749 42.702 95.451 94.613 95.451s94.613-42.702 94.613-95.451v-169.132z" />
|
||||
</font></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -70,7 +70,7 @@
|
||||
"noControlCharactersInRegex": "off",
|
||||
"noShadowRestrictedNames": "off",
|
||||
"noArrayIndexKey": "info",
|
||||
"noAssignInExpressions": "warn"
|
||||
"noAssignInExpressions": "info"
|
||||
},
|
||||
"complexity": {
|
||||
"noUselessConstructor": "off",
|
||||
@@ -82,7 +82,7 @@
|
||||
"noStaticOnlyClass": "off"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "warn"
|
||||
"noDangerouslySetInnerHtml": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -114,17 +114,17 @@
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/dist/**",
|
||||
"!**/dist-*/**",
|
||||
"!**/out/**",
|
||||
"!**/evals/**",
|
||||
"!**/playwright/**",
|
||||
"!**/test-results/**",
|
||||
"!**/node_modules/**",
|
||||
"!**/webview-ui/build/**",
|
||||
"!**/generated/**",
|
||||
"!**/proto/**",
|
||||
"!**/tests/specs/**"
|
||||
"!**/dist",
|
||||
"!**/dist-*",
|
||||
"!**/out",
|
||||
"!**/evals",
|
||||
"!**/playwright",
|
||||
"!**/test-results",
|
||||
"!**/node_modules",
|
||||
"!**/webview-ui/build",
|
||||
"!**/generated",
|
||||
"!**/proto",
|
||||
"!**/tests/specs"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
|
||||
@@ -101,10 +101,7 @@ see the manual page: man cline`,
|
||||
if !isUserReadyToUse(ctx, instanceAddress) {
|
||||
// Create renderer for welcome messages
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
|
||||
markdown := "## hey there! looks like you're new here. let's get you set up"
|
||||
rendered := renderer.RenderMarkdown(markdown)
|
||||
fmt.Printf("\n%s\n\n", rendered)
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
|
||||
|
||||
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
|
||||
// Check if user cancelled - exit cleanly
|
||||
@@ -119,9 +116,7 @@ see the manual page: man cline`,
|
||||
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
|
||||
}
|
||||
|
||||
markdown = "## ✓ setup complete, you can now use the cline cli"
|
||||
rendered = renderer.RenderMarkdown(markdown)
|
||||
fmt.Printf("\n%s\n\n", rendered)
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
|
||||
}
|
||||
} else {
|
||||
// User specified --address flag, use that
|
||||
@@ -163,6 +158,7 @@ see the manual page: man cline`,
|
||||
Settings: settings,
|
||||
Yolo: yolo,
|
||||
Address: instanceAddress,
|
||||
Verbose: verbose,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -186,6 +182,7 @@ see the manual page: man cline`,
|
||||
rootCmd.AddCommand(cli.NewVersionCommand())
|
||||
rootCmd.AddCommand(cli.NewAuthCommand())
|
||||
rootCmd.AddCommand(cli.NewLogsCommand())
|
||||
// rootCmd.AddCommand(cli.NewDoctorCommand()) // Disabled for now
|
||||
|
||||
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
|
||||
os.Exit(1)
|
||||
@@ -238,7 +235,7 @@ func promptForInitialTask(ctx context.Context, instanceAddress, modeFlag string)
|
||||
// showSessionBanner displays session info before initial prompt
|
||||
func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string) {
|
||||
bannerInfo := display.BannerInfo{
|
||||
Version: global.Version,
|
||||
Version: global.CliVersion,
|
||||
Mode: modeFlag, // Use the mode from command flag, not state
|
||||
}
|
||||
|
||||
@@ -330,17 +327,20 @@ func getContentFromStdinAndArgs(args []string) (string, error) {
|
||||
|
||||
// Check if data is being piped to stdin
|
||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||
stdinBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
|
||||
stdinContent := strings.TrimSpace(string(stdinBytes))
|
||||
if stdinContent != "" {
|
||||
if content.Len() > 0 {
|
||||
content.WriteString(" ")
|
||||
// Only try to read if there's actually data available
|
||||
if stat.Size() > 0 {
|
||||
stdinBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
|
||||
stdinContent := strings.TrimSpace(string(stdinBytes))
|
||||
if stdinContent != "" {
|
||||
if content.Len() > 0 {
|
||||
content.WriteString(" ")
|
||||
}
|
||||
content.WriteString(stdinContent)
|
||||
}
|
||||
content.WriteString(stdinContent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ require (
|
||||
github.com/charmbracelet/bubbletea v1.3.6
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/charmbracelet/huh v0.7.1-0.20251005153135-a01a1e304532
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||
github.com/cline/grpc-go v0.0.0
|
||||
github.com/glebarez/go-sqlite v1.22.0
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
golang.org/x/term v0.32.0
|
||||
google.golang.org/grpc v1.75.0
|
||||
@@ -24,7 +26,6 @@ require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||
@@ -45,7 +46,6 @@ require (
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
|
||||
@@ -1,62 +1,68 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.0-nightly.14",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
"cline": "./bin/cline",
|
||||
"cline-host": "./bin/cline-host"
|
||||
},
|
||||
"man": "./man/cline.1",
|
||||
"scripts": {
|
||||
"postinstall": "node postinstall.js"
|
||||
},
|
||||
"bundleDependencies": [
|
||||
"@grpc/grpc-js",
|
||||
"@grpc/reflection",
|
||||
"better-sqlite3",
|
||||
"grpc-health-check",
|
||||
"open",
|
||||
"vscode-uri"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"claude",
|
||||
"dev",
|
||||
"mcp",
|
||||
"openrouter",
|
||||
"coding",
|
||||
"agent",
|
||||
"autonomous",
|
||||
"chatgpt",
|
||||
"sonnet",
|
||||
"ai",
|
||||
"llama",
|
||||
"cli"
|
||||
],
|
||||
"author": {
|
||||
"name": "Cline Bot Inc."
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline"
|
||||
},
|
||||
"homepage": "https://cline.bot",
|
||||
"bugs": {
|
||||
"url": "https://github.com/cline/cline/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"open": "^10.1.2",
|
||||
"vscode-uri": "^3.1.0"
|
||||
},
|
||||
"os": ["darwin", "linux"],
|
||||
"cpu": ["x64", "arm64"]
|
||||
"name": "cline",
|
||||
"version": "1.0.3",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
"cline": "./bin/cline",
|
||||
"cline-host": "./bin/cline-host"
|
||||
},
|
||||
"man": "./man/cline.1",
|
||||
"scripts": {
|
||||
"postinstall": "node postinstall.js"
|
||||
},
|
||||
"bundleDependencies": [
|
||||
"@grpc/grpc-js",
|
||||
"@grpc/reflection",
|
||||
"better-sqlite3",
|
||||
"grpc-health-check",
|
||||
"open",
|
||||
"vscode-uri"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"claude",
|
||||
"dev",
|
||||
"mcp",
|
||||
"openrouter",
|
||||
"coding",
|
||||
"agent",
|
||||
"autonomous",
|
||||
"chatgpt",
|
||||
"sonnet",
|
||||
"ai",
|
||||
"llama",
|
||||
"cli"
|
||||
],
|
||||
"author": {
|
||||
"name": "Cline Bot Inc."
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline"
|
||||
},
|
||||
"homepage": "https://cline.bot",
|
||||
"bugs": {
|
||||
"url": "https://github.com/cline/cline/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"open": "^10.1.2",
|
||||
"vscode-uri": "^3.1.0"
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,18 +6,38 @@ import (
|
||||
)
|
||||
|
||||
func NewAuthCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Authenticate a provider and configure model used",
|
||||
Long: `Authenticate a provider and configure model used
|
||||
Short: "Authenticate a provider and configure what model is used",
|
||||
Long: `Authenticate a provider and configure what model is used
|
||||
|
||||
This command opens an interactive menu where you can:
|
||||
Interactive Mode:
|
||||
Run without flags to open an interactive menu where you can:
|
||||
- Sign in to your Cline account
|
||||
- Configure other LLM providers (Anthropic, OpenAI, etc.)
|
||||
- Select and switch between AI models
|
||||
- Manage provider settings`,
|
||||
- Manage provider settings
|
||||
|
||||
Quick Setup Mode:
|
||||
Use flags to quickly configure a BYO provider non-interactively:
|
||||
|
||||
Examples:
|
||||
cline auth --provider openai-native --apikey sk-xxx --modelid gpt-5
|
||||
cline auth -p anthropic -k sk-ant-xxx -m claude-sonnet-4-5-20250929
|
||||
cline auth -p openai-compatible -k xxx -m gpt-4 -b https://api.example.com/v1
|
||||
|
||||
Supported providers: openai-native, openai, anthropic, gemini, openrouter, xai, cerebras, ollama
|
||||
Note: Bedrock provider requires interactive setup due to complex auth fields`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return auth.RunAuthFlow(cmd.Context(), args)
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags for quick setup mode
|
||||
cmd.Flags().StringVarP(&auth.QuickProvider, "provider", "p", "", "Provider ID for quick setup (e.g., openai-native, anthropic)")
|
||||
cmd.Flags().StringVarP(&auth.QuickAPIKey, "apikey", "k", "", "API key for the provider")
|
||||
cmd.Flags().StringVarP(&auth.QuickModelID, "modelid", "m", "", "Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)")
|
||||
cmd.Flags().StringVarP(&auth.QuickBaseURL, "baseurl", "b", "", "Base URL (optional, only for openai provider)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/cline/cli/pkg/cli/display"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
@@ -38,7 +39,7 @@ const (
|
||||
// ┃ Change Cline model (only if authenticated) - hidden if not authenticated
|
||||
// ┃ Authenticate with Cline account / Sign out of Cline - changes based on auth status
|
||||
// ┃ Select active provider (Cline or BYO) - always shown. Used to switch between Cline and BYO providers
|
||||
// ┃ Configure API provider - always shown. Launches provider setup wizard
|
||||
// ┃ Configure BYO API providers - always shown. Launches provider setup wizard
|
||||
// ┃ Exit authorization wizard - always shown. Exits the auth menu
|
||||
|
||||
// RunAuthFlow is the entry point for the entire auth flow with instance management
|
||||
@@ -68,18 +69,25 @@ func RunAuthFlow(ctx context.Context, args []string) error {
|
||||
// Main entry point for handling the `cline auth` command
|
||||
// HandleAuthCommand routes the auth command based on the number of arguments
|
||||
func HandleAuthCommand(ctx context.Context, args []string) error {
|
||||
|
||||
// Check if flags are provided for quick setup
|
||||
if QuickProvider != "" || QuickAPIKey != "" || QuickModelID != "" || QuickBaseURL != "" {
|
||||
if QuickProvider == "" || QuickAPIKey == "" || QuickModelID == "" {
|
||||
return fmt.Errorf("quick setup requires --provider, --apikey, and --modelid flags. Use 'cline auth --help' for more information")
|
||||
}
|
||||
return QuickSetupFromFlags(ctx, QuickProvider, QuickAPIKey, QuickModelID, QuickBaseURL)
|
||||
}
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// No args: Show menu (ShowAuthMenuNoArgs)
|
||||
// No args: Show uth wizard
|
||||
return HandleAuthMenuNoArgs(ctx)
|
||||
case 1:
|
||||
// One arg: Provider ID only, prompt for API key
|
||||
return QuickAPISetup(args[0], "")
|
||||
case 2:
|
||||
// Two args: Provider ID and API key
|
||||
return QuickAPISetup(args[0], args[1])
|
||||
case 1, 2, 3, 4:
|
||||
fmt.Println("Invalid positional arguments. Correct usage:")
|
||||
fmt.Println(" cline auth --provider <provider> --apikey <key> --modelid <model> --baseurl <optional>")
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("quick BYO API setup is currently stubbed - not yet implemented")
|
||||
return fmt.Errorf("too many arguments. Use flags for quick setup: --provider, --apikey, --modelid --baseurl(optional)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,32 +173,34 @@ func ShowAuthMenuWithStatus(isClineAuthenticated bool, hasOrganizations bool, cu
|
||||
options = append(options,
|
||||
huh.NewOption("Sign out of Cline", AuthActionClineLogin),
|
||||
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
|
||||
huh.NewOption("Configure API provider", AuthActionBYOSetup),
|
||||
huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
|
||||
huh.NewOption("Exit authorization wizard", AuthActionExit),
|
||||
)
|
||||
} else {
|
||||
options = []huh.Option[AuthAction]{
|
||||
huh.NewOption("Authenticate with Cline account", AuthActionClineLogin),
|
||||
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
|
||||
huh.NewOption("Configure API provider", AuthActionBYOSetup),
|
||||
huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
|
||||
huh.NewOption("Exit authorization wizard", AuthActionExit),
|
||||
}
|
||||
}
|
||||
|
||||
// Determine menu title based on status
|
||||
var title string
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
|
||||
// Always show Cline authentication status
|
||||
if isClineAuthenticated {
|
||||
title = "Cline Account: \033[32m✓\033[0m Authenticated\n"
|
||||
title = fmt.Sprintf("Cline Account: %s Authenticated\n", renderer.Green("✓"))
|
||||
} else {
|
||||
title = "Cline Account: \033[31m✗\033[0m Not authenticated\n"
|
||||
title = fmt.Sprintf("Cline Account: %s Not authenticated\n", renderer.Red("✗"))
|
||||
}
|
||||
|
||||
// Show active provider and model if configured (regardless of Cline auth status)
|
||||
// ANSI color codes: Normal intensity = \033[22m, White = \033[37m, Reset = \033[0m
|
||||
if currentProvider != "" && currentModel != "" {
|
||||
title += fmt.Sprintf("Active Provider: \033[22m\033[37m%s\033[0m\nActive Model: \033[22m\033[37m%s\033[0m\n", currentProvider, currentModel)
|
||||
title += fmt.Sprintf("Active Provider: %s\nActive Model: %s\n",
|
||||
renderer.White(currentProvider),
|
||||
renderer.White(currentModel))
|
||||
}
|
||||
|
||||
// Always end with a huh?
|
||||
@@ -258,11 +268,6 @@ func HandleSelectProvider(ctx context.Context) error {
|
||||
return HandleAuthMenuNoArgs(ctx)
|
||||
}
|
||||
|
||||
if len(providerOptions) == 1 {
|
||||
fmt.Println("Only one provider is configured. Configure another provider to switch between them.")
|
||||
return HandleAuthMenuNoArgs(ctx)
|
||||
}
|
||||
|
||||
providerOptions = append(providerOptions, huh.NewOption("(Cancel)", "cancel"))
|
||||
|
||||
// Show selection menu
|
||||
|
||||
@@ -1,13 +1,247 @@
|
||||
package auth
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
// QuickAPISetup performs quick provider setup with provider ID and optional API key
|
||||
func QuickAPISetup(providerID, apiKey string) error {
|
||||
fmt.Println("Quick BYO API setup is currently stubbed - not yet implemented.")
|
||||
fmt.Printf("Requested provider: %s\n", providerID)
|
||||
if apiKey != "" {
|
||||
fmt.Println("Provided API key:", "<jk redacted>")
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
// Package-level variables for command-line flags
|
||||
var (
|
||||
QuickProvider string // Provider ID (e.g., "openai", "anthropic")
|
||||
QuickAPIKey string // API key for the provider
|
||||
QuickModelID string // Model ID to configure
|
||||
QuickBaseURL string // Base URL (optional, for openai compatible only)
|
||||
)
|
||||
|
||||
// QuickSetupFromFlags performs quick setup using command-line flags
|
||||
// Returns error if validation fails or configuration cannot be applied
|
||||
func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL string) error {
|
||||
// Validate all input parameters
|
||||
providerEnum, err := validateQuickSetupInputs(provider, apiKey, modelID, baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create task manager for state operations
|
||||
manager, err := createTaskManager(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create task manager: %w", err)
|
||||
}
|
||||
|
||||
// Validate and fetch model information if needed
|
||||
finalModelID, modelInfo, err := validateAndFetchModel(ctx, manager, providerEnum, modelID, apiKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("model validation failed: %w", err)
|
||||
}
|
||||
|
||||
// For Ollama, baseURL is stored in the API key field
|
||||
finalAPIKey := apiKey
|
||||
finalBaseURL := baseURL
|
||||
if providerEnum == cline.ApiProvider_OLLAMA {
|
||||
if baseURL != "" {
|
||||
finalAPIKey = baseURL
|
||||
finalBaseURL = ""
|
||||
} else if apiKey != "" {
|
||||
// User provided API key for Ollama - treat it as baseURL
|
||||
finalAPIKey = apiKey
|
||||
finalBaseURL = ""
|
||||
} else {
|
||||
// Use default Ollama baseURL
|
||||
finalAPIKey = "http://localhost:11434"
|
||||
finalBaseURL = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the provider using existing AddProviderPartial function
|
||||
if err := AddProviderPartial(ctx, manager, providerEnum, finalModelID, finalAPIKey, finalBaseURL, modelInfo); err != nil {
|
||||
return fmt.Errorf("failed to configure provider: %w", err)
|
||||
}
|
||||
|
||||
// Set the provider as active for both Plan and Act modes
|
||||
if err := UpdateProviderPartial(ctx, manager, providerEnum, ProviderUpdatesPartial{}, true); err != nil {
|
||||
return fmt.Errorf("failed to set provider as active: %w", err)
|
||||
}
|
||||
|
||||
// Mark welcome view as completed
|
||||
if err := markWelcomeViewCompleted(ctx, manager); err != nil {
|
||||
// Non-fatal error, just log it
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("[DEBUG] Warning: failed to mark welcome view as completed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush pending state changes to disk immediately
|
||||
// This ensures all configuration changes are persisted before the instance terminates
|
||||
if _, err := manager.GetClient().State.FlushPendingState(ctx, &cline.EmptyRequest{}); err != nil {
|
||||
return fmt.Errorf("failed to flush pending state: %w", err)
|
||||
}
|
||||
|
||||
// Success message
|
||||
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
|
||||
fmt.Printf(" Model: %s\n", finalModelID)
|
||||
if providerEnum == cline.ApiProvider_OLLAMA {
|
||||
fmt.Printf(" Base URL: %s\n", finalAPIKey)
|
||||
} else {
|
||||
fmt.Println(" API Key: Configured")
|
||||
}
|
||||
if finalBaseURL != "" {
|
||||
fmt.Printf(" Custom Base URL: %s\n", finalBaseURL)
|
||||
}
|
||||
fmt.Println("\nYou can now use Cline with this provider.")
|
||||
fmt.Println("Run 'cline start' to begin a new task.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateQuickSetupInputs validates all input parameters for quick setup
|
||||
// Returns the validated provider enum or an error if validation fails
|
||||
func validateQuickSetupInputs(provider, apiKey, modelID, baseURL string) (cline.ApiProvider, error) {
|
||||
// Validate required parameters
|
||||
if provider == "" {
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("provider is required. Use --provider or -p flag")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(apiKey) == "" && provider != "ollama" {
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("API key is required for %s provider. Use --apikey or -k flag", provider)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(modelID) == "" {
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("model ID is required. Use --modelid or -m flag")
|
||||
}
|
||||
|
||||
// Validate and map provider string to enum
|
||||
providerEnum, err := validateQuickSetupProvider(provider)
|
||||
if err != nil {
|
||||
return cline.ApiProvider_ANTHROPIC, err
|
||||
}
|
||||
|
||||
// Validate that baseURL is only provided for OpenAI-compatible providers
|
||||
if err := validateBaseURL(baseURL, providerEnum); err != nil {
|
||||
return cline.ApiProvider_ANTHROPIC, err
|
||||
}
|
||||
|
||||
return providerEnum, nil
|
||||
}
|
||||
|
||||
// validateBaseURL checks if the user's input includes a baseURL for a provider other than OpenAI (compatible)
|
||||
// Returns error if baseURL is provided for unsupported providers
|
||||
func validateBaseURL(baseURL string, providerEnum cline.ApiProvider) error {
|
||||
if providerEnum != cline.ApiProvider_OPENAI {
|
||||
if baseURL != "" {
|
||||
return fmt.Errorf("base URL is only supported for OpenAI and OpenAI-compatible providers")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// validateQuickSetupProvider validates the provider ID and returns the enum value
|
||||
// Returns error if provider is invalid or not supported for quick setup
|
||||
func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
|
||||
// Normalize provider ID (trim whitespace, lowercase)
|
||||
normalizedID := strings.TrimSpace(strings.ToLower(providerID))
|
||||
|
||||
// Explicitly block Bedrock
|
||||
if normalizedID == "bedrock" {
|
||||
return cline.ApiProvider_BEDROCK, fmt.Errorf("bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup: cline auth")
|
||||
}
|
||||
|
||||
// Map provider string to enum using existing function
|
||||
provider, ok := mapProviderStringToEnum(normalizedID)
|
||||
if !ok {
|
||||
// Provider not found - provide helpful error message
|
||||
supportedProviders := []string{
|
||||
"openai-native", "openai", "anthropic", "gemini",
|
||||
"openrouter", "xai", "cerebras", "ollama",
|
||||
}
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf(
|
||||
"invalid provider '%s'. Supported providers: %s",
|
||||
providerID,
|
||||
strings.Join(supportedProviders, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
// Validate against supported quick setup providers
|
||||
supportedProviders := map[cline.ApiProvider]bool{
|
||||
cline.ApiProvider_OPENAI_NATIVE: true,
|
||||
cline.ApiProvider_OPENAI: true,
|
||||
cline.ApiProvider_ANTHROPIC: true,
|
||||
cline.ApiProvider_GEMINI: true,
|
||||
cline.ApiProvider_OPENROUTER: true,
|
||||
cline.ApiProvider_XAI: true,
|
||||
cline.ApiProvider_CEREBRAS: true,
|
||||
cline.ApiProvider_OLLAMA: true,
|
||||
cline.ApiProvider_NOUSRESEARCH: true,
|
||||
}
|
||||
|
||||
if !supportedProviders[provider] {
|
||||
return provider, fmt.Errorf(
|
||||
"provider '%s' is not supported for quick setup. Please use interactive setup: cline auth",
|
||||
providerID,
|
||||
)
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// validateAndFetchModel validates the model ID or fetches from provider if needed
|
||||
// Returns the final model ID and optional model info
|
||||
// For providers with static models, validates against the list
|
||||
// For providers with dynamic models, fetches the list if possible
|
||||
func validateAndFetchModel(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID, apiKey string) (string, interface{}, error) {
|
||||
// Normalize model ID
|
||||
modelID = strings.TrimSpace(modelID)
|
||||
if modelID == "" {
|
||||
return "", nil, fmt.Errorf("model ID cannot be empty")
|
||||
}
|
||||
|
||||
// For most providers, we trust the user's input since we can't easily validate without making API calls
|
||||
// The actual validation will happen when the model is used
|
||||
switch provider {
|
||||
case cline.ApiProvider_OPENROUTER:
|
||||
// OpenRouter supports model info fetching, but it requires an API call
|
||||
// For quick setup, we'll trust the user's input and return nil for model info
|
||||
// The actual model info will be fetched when needed
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("[DEBUG] OpenRouter model ID: %s (will be validated on first use)\n", modelID)
|
||||
}
|
||||
return modelID, nil, nil
|
||||
|
||||
case cline.ApiProvider_OLLAMA:
|
||||
// Ollama models can be validated by fetching the list, but this requires the server to be running
|
||||
// For quick setup, we'll trust the user's input
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("[DEBUG] Ollama model ID: %s (will be validated when server is accessible)\n", modelID)
|
||||
}
|
||||
return modelID, nil, nil
|
||||
|
||||
default:
|
||||
// For other providers (Anthropic, OpenAI, Gemini, XAI, Cerebras), trust user input
|
||||
// Model validation will occur when the model is actually used
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("[DEBUG] %s model ID: %s (will be validated on first use)\n", GetProviderDisplayName(provider), modelID)
|
||||
}
|
||||
return modelID, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// markWelcomeViewCompleted marks the welcome view as completed in the state
|
||||
// This prevents the welcome view from showing up after quick setup
|
||||
func markWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
|
||||
// Use the State service to update the welcome view flag
|
||||
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark welcome view as completed: %w", err)
|
||||
}
|
||||
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("[DEBUG] Marked welcome view as completed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package auth
|
||||
@@ -14,13 +14,33 @@ import (
|
||||
|
||||
// FetchOpenRouterModels fetches available OpenRouter models from Cline Core
|
||||
func FetchOpenRouterModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
|
||||
resp, err := manager.GetClient().Models.RefreshOpenRouterModels(ctx, &cline.EmptyRequest{})
|
||||
resp, err := manager.GetClient().Models.RefreshOpenRouterModelsRpc(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch OpenRouter models: %w", err)
|
||||
}
|
||||
return resp.Models, nil
|
||||
}
|
||||
|
||||
// FetchOcaModels fetches available Oca models from Cline Core
|
||||
func FetchOcaModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OcaModelInfo, error) {
|
||||
resp, err := manager.GetClient().Models.RefreshOcaModels(ctx, &cline.StringRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch Oca models: %w", err)
|
||||
}
|
||||
return resp.Models, nil
|
||||
}
|
||||
|
||||
// ConvertOpenRouterModelsToInterface converts OpenRouter model map to generic interface map.
|
||||
// This allows OpenRouter and Cline models to be used with the generic fetching utilities.
|
||||
func ConvertOpenRouterModelsToInterface(models map[string]*cline.OpenRouterModelInfo) map[string]interface{} {
|
||||
result := make(map[string]interface{}, len(models))
|
||||
for k, v := range models {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// FetchOpenAiModels fetches available OpenAI models from Cline Core
|
||||
// Takes the API key and returns a list of model IDs
|
||||
func FetchOpenAiModels(ctx context.Context, manager *task.Manager, baseURL, apiKey string) ([]string, error) {
|
||||
@@ -100,9 +120,9 @@ func ConvertModelsMapToSlice(models map[string]interface{}) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// ConvertOpenRouterModelsToInterface converts OpenRouter model map to generic interface map.
|
||||
// This allows OpenRouter and Cline models to be used with the generic fetching utilities.
|
||||
func ConvertOpenRouterModelsToInterface(models map[string]*cline.OpenRouterModelInfo) map[string]interface{} {
|
||||
// ConvertOcaModelsToInterface converts Oca model map to generic interface map.
|
||||
// This allows Oca and Cline models to be used with the generic fetching utilities.
|
||||
func ConvertOcaModelsToInterface(models map[string]*cline.OcaModelInfo) map[string]interface{} {
|
||||
result := make(map[string]interface{}, len(models))
|
||||
for k, v := range models {
|
||||
result[k] = v
|
||||
|
||||
@@ -18,14 +18,16 @@ type BYOProviderOption struct {
|
||||
func GetBYOProviderList() []BYOProviderOption {
|
||||
return []BYOProviderOption{
|
||||
{Name: "Anthropic", Provider: cline.ApiProvider_ANTHROPIC},
|
||||
{Name: "OpenAI", Provider: cline.ApiProvider_OPENAI},
|
||||
{Name: "OpenAI Native", Provider: cline.ApiProvider_OPENAI_NATIVE},
|
||||
{Name: "OpenAI Compatible", Provider: cline.ApiProvider_OPENAI},
|
||||
{Name: "OpenAI (Official)", Provider: cline.ApiProvider_OPENAI_NATIVE},
|
||||
{Name: "OpenRouter", Provider: cline.ApiProvider_OPENROUTER},
|
||||
{Name: "X AI (Grok)", Provider: cline.ApiProvider_XAI},
|
||||
{Name: "AWS Bedrock", Provider: cline.ApiProvider_BEDROCK},
|
||||
{Name: "Google Gemini", Provider: cline.ApiProvider_GEMINI},
|
||||
{Name: "Ollama", Provider: cline.ApiProvider_OLLAMA},
|
||||
{Name: "Cerebras", Provider: cline.ApiProvider_CEREBRAS},
|
||||
{Name: "NousResearch", Provider: cline.ApiProvider_NOUSRESEARCH},
|
||||
{Name: "Oracle Code Assist", Provider: cline.ApiProvider_OCA},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +73,8 @@ func SupportsBYOModelFetching(provider cline.ApiProvider) bool {
|
||||
return true
|
||||
case cline.ApiProvider_OLLAMA:
|
||||
return true
|
||||
case cline.ApiProvider_OCA:
|
||||
return true
|
||||
}
|
||||
|
||||
return SupportsStaticModelList(provider)
|
||||
@@ -82,9 +86,9 @@ func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
|
||||
case cline.ApiProvider_ANTHROPIC:
|
||||
return "e.g., claude-sonnet-4-5-20250929"
|
||||
case cline.ApiProvider_OPENAI:
|
||||
return "e.g., gpt-5-2025-08-07"
|
||||
case cline.ApiProvider_OPENAI_NATIVE:
|
||||
return "e.g., openai/gpt-oss-120b"
|
||||
case cline.ApiProvider_OPENAI_NATIVE:
|
||||
return "e.g., gpt-5-2025-08-07"
|
||||
case cline.ApiProvider_OPENROUTER:
|
||||
return "e.g., google/gemini-2.0-flash-exp:free"
|
||||
case cline.ApiProvider_XAI:
|
||||
@@ -97,6 +101,10 @@ func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
|
||||
return "e.g., qwen3-coder:30b"
|
||||
case cline.ApiProvider_CEREBRAS:
|
||||
return "e.g., gpt-oss-120b"
|
||||
case cline.ApiProvider_NOUSRESEARCH:
|
||||
return "e.g., Hermes-4-405B"
|
||||
case cline.ApiProvider_OCA:
|
||||
return "e.g., oca/llama4"
|
||||
default:
|
||||
return "Enter model ID"
|
||||
}
|
||||
@@ -127,8 +135,8 @@ func GetBYOAPIKeyFieldConfig(provider cline.ApiProvider) APIKeyFieldConfig {
|
||||
}
|
||||
|
||||
// PromptForAPIKey prompts the user to enter an API key (or base URL for Ollama).
|
||||
// For OpenAI Native provider, also prompts for an optional base URL.
|
||||
func PromptForAPIKey(provider cline.ApiProvider) (string, error) {
|
||||
// For OpenAI (Compatible) provider, also prompts for an optional base URL.
|
||||
func PromptForAPIKey(provider cline.ApiProvider) (string, string, error) {
|
||||
var apiKey string
|
||||
config := GetBYOAPIKeyFieldConfig(provider)
|
||||
|
||||
@@ -149,11 +157,11 @@ func PromptForAPIKey(provider cline.ApiProvider) (string, error) {
|
||||
form := huh.NewForm(huh.NewGroup(apiKeyField))
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return "", fmt.Errorf("failed to get API key: %w", err)
|
||||
return "", "", fmt.Errorf("failed to get API key: %w", err)
|
||||
}
|
||||
|
||||
// For OpenAI Native provider, also prompt for base URL
|
||||
if provider == cline.ApiProvider_OPENAI_NATIVE {
|
||||
// For OpenAI (Compatible) provider, prompt for base URL
|
||||
if provider == cline.ApiProvider_OPENAI {
|
||||
var baseURL string
|
||||
baseURLForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
@@ -166,12 +174,11 @@ func PromptForAPIKey(provider cline.ApiProvider) (string, error) {
|
||||
)
|
||||
|
||||
if err := baseURLForm.Run(); err != nil {
|
||||
return "", fmt.Errorf("failed to get base URL: %w", err)
|
||||
return "", "", fmt.Errorf("failed to get base URL: %w", err)
|
||||
}
|
||||
|
||||
// TODO - connect baseURL
|
||||
_ = baseURL
|
||||
return apiKey, baseURL, nil
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
return apiKey, "", nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
@@ -110,6 +111,9 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
cline.ApiProvider_GEMINI,
|
||||
cline.ApiProvider_OLLAMA,
|
||||
cline.ApiProvider_CEREBRAS,
|
||||
cline.ApiProvider_NOUSRESEARCH,
|
||||
cline.ApiProvider_OCA,
|
||||
cline.ApiProvider_HICAP,
|
||||
}
|
||||
|
||||
// Check each provider to see if it's ready to use
|
||||
@@ -120,16 +124,23 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this provider has an API key
|
||||
hasAPIKey := checkAPIKeyExists(r.apiConfig, provider)
|
||||
if !hasAPIKey {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this provider has a model configured
|
||||
modelID := getProviderSpecificModelID(r.apiConfig, "plan", provider)
|
||||
if modelID == "" {
|
||||
continue
|
||||
|
||||
// Determine if credentials exist
|
||||
hasCreds := checkAPIKeyExists(r.apiConfig, provider)
|
||||
|
||||
// Determine readiness: OCA uses auth state presence; others need creds and model
|
||||
if provider == cline.ApiProvider_OCA {
|
||||
state, _ := GetLatestOCAState(context.Background(), 2 *time.Second)
|
||||
if state == nil || state.User == nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Provider is not ready unless it has credentials AND a model configured
|
||||
if !hasCreds || modelID == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Get base URL for Ollama
|
||||
@@ -145,7 +156,7 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
Mode: "Ready",
|
||||
Provider: provider,
|
||||
ModelID: modelID,
|
||||
HasAPIKey: hasAPIKey,
|
||||
HasAPIKey: checkAPIKeyExists(r.apiConfig, provider),
|
||||
BaseURL: baseURL,
|
||||
})
|
||||
seenProviders[provider] = true
|
||||
@@ -203,13 +214,15 @@ func extractProviderFromState(stateData map[string]interface{}, mode string) *Pr
|
||||
// mapProviderStringToEnum converts provider string from state to ApiProvider enum
|
||||
// Returns (provider, ok) where ok is false if the provider is unknown
|
||||
func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
|
||||
normalizedStr := strings.ToLower(providerStr)
|
||||
|
||||
// Map string values to enum values
|
||||
switch providerStr {
|
||||
switch normalizedStr {
|
||||
case "anthropic":
|
||||
return cline.ApiProvider_ANTHROPIC, true
|
||||
case "openai":
|
||||
case "openai", "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
|
||||
return cline.ApiProvider_OPENAI, true
|
||||
case "openai-native":
|
||||
case "openai-native": // This is the native, official Open AI provider
|
||||
return cline.ApiProvider_OPENAI_NATIVE, true
|
||||
case "openrouter":
|
||||
return cline.ApiProvider_OPENROUTER, true
|
||||
@@ -225,6 +238,12 @@ func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
|
||||
return cline.ApiProvider_CEREBRAS, true
|
||||
case "cline":
|
||||
return cline.ApiProvider_CLINE, true
|
||||
case "oca":
|
||||
return cline.ApiProvider_OCA, true
|
||||
case "hicap":
|
||||
return cline.ApiProvider_HICAP, true
|
||||
case "nousResearch":
|
||||
return cline.ApiProvider_NOUSRESEARCH, true
|
||||
default:
|
||||
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
|
||||
}
|
||||
@@ -237,7 +256,7 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
|
||||
case cline.ApiProvider_ANTHROPIC:
|
||||
return "anthropic"
|
||||
case cline.ApiProvider_OPENAI:
|
||||
return "openai"
|
||||
return "openai-compatible"
|
||||
case cline.ApiProvider_OPENAI_NATIVE:
|
||||
return "openai-native"
|
||||
case cline.ApiProvider_OPENROUTER:
|
||||
@@ -254,6 +273,12 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
|
||||
return "cerebras"
|
||||
case cline.ApiProvider_CLINE:
|
||||
return "cline"
|
||||
case cline.ApiProvider_OCA:
|
||||
return "oca"
|
||||
case cline.ApiProvider_HICAP:
|
||||
return "hicap"
|
||||
case cline.ApiProvider_NOUSRESEARCH:
|
||||
return "nousResearch"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -312,9 +337,9 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
|
||||
case cline.ApiProvider_ANTHROPIC:
|
||||
return "Anthropic"
|
||||
case cline.ApiProvider_OPENAI:
|
||||
return "OpenAI"
|
||||
return "OpenAI Compatible"
|
||||
case cline.ApiProvider_OPENAI_NATIVE:
|
||||
return "OpenAI Native"
|
||||
return "OpenAI (Official)"
|
||||
case cline.ApiProvider_OPENROUTER:
|
||||
return "OpenRouter"
|
||||
case cline.ApiProvider_XAI:
|
||||
@@ -329,6 +354,12 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
|
||||
return "Cerebras"
|
||||
case cline.ApiProvider_CLINE:
|
||||
return "Cline (Official)"
|
||||
case cline.ApiProvider_OCA:
|
||||
return "Oracle Code Assist"
|
||||
case cline.ApiProvider_HICAP:
|
||||
return "Hicap"
|
||||
case cline.ApiProvider_NOUSRESEARCH:
|
||||
return "NousResearch"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -378,7 +409,7 @@ func FormatProviderList(result *ProviderListResult) string {
|
||||
} else {
|
||||
output.WriteString(" Base URL: (default)\n")
|
||||
}
|
||||
} else if display.Provider == cline.ApiProvider_CLINE {
|
||||
} else if display.Provider == cline.ApiProvider_CLINE || display.Provider == cline.ApiProvider_OCA {
|
||||
output.WriteString(" Status: Authenticated\n")
|
||||
} else {
|
||||
output.WriteString(" API Key: Configured\n")
|
||||
@@ -430,6 +461,12 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
verboseLog("[DEBUG] Cline provider is authenticated")
|
||||
}
|
||||
|
||||
// Check OCA provider via global auth subscription (state presence)
|
||||
if state, _ := GetLatestOCAState(context.Background(), 2*time.Second); state != nil && state.User != nil {
|
||||
configuredProviders = append(configuredProviders, cline.ApiProvider_OCA)
|
||||
verboseLog("[DEBUG] OCA provider has active auth state")
|
||||
}
|
||||
|
||||
// Check each BYO provider for API key presence
|
||||
providersToCheck := []struct {
|
||||
provider cline.ApiProvider
|
||||
@@ -444,6 +481,8 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
{cline.ApiProvider_GEMINI, "geminiApiKey"},
|
||||
{cline.ApiProvider_OLLAMA, "ollamaBaseUrl"}, // Ollama uses baseUrl instead of API key
|
||||
{cline.ApiProvider_CEREBRAS, "cerebrasApiKey"},
|
||||
{cline.ApiProvider_HICAP, "hicapApiKey"},
|
||||
{cline.ApiProvider_NOUSRESEARCH, "nousResearchApiKey"},
|
||||
}
|
||||
|
||||
for _, providerCheck := range providersToCheck {
|
||||
@@ -459,6 +498,7 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
verboseLog("[DEBUG] Total configured providers: %d", len(configuredProviders))
|
||||
for _, p := range configuredProviders {
|
||||
verboseLog("[DEBUG] - %s", GetProviderDisplayName(p))
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// updateApiConfigurationPartial is a helper that calls the gRPC method with optional verbose logging.
|
||||
// This replaces the Manager.UpdateApiConfigurationPartial method to keep auth-specific code in the auth package.
|
||||
// This replaces the Manager.updateApiConfigurationPartial method to keep auth-specific code in the auth package.
|
||||
func updateApiConfigurationPartial(ctx context.Context, manager *task.Manager, request *cline.UpdateApiConfigurationPartialRequest) error {
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("[DEBUG] Updating API configuration (partial)")
|
||||
@@ -46,6 +46,7 @@ func updateApiConfigurationPartial(ctx context.Context, manager *task.Manager, r
|
||||
// ProviderFields defines all the field names associated with a specific provider
|
||||
type ProviderFields struct {
|
||||
APIKeyField string // API key field name (e.g., "apiKey", "openAiApiKey")
|
||||
BaseURLField string // Base URL field name (optional, empty if not applicable)
|
||||
PlanModeModelIDField string // Plan mode model ID field (e.g., "planModeApiModelId")
|
||||
ActModeModelIDField string // Act mode model ID field (e.g., "actModeApiModelId")
|
||||
PlanModeModelInfoField string // Plan mode model info field (optional, empty if not applicable)
|
||||
@@ -68,6 +69,7 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
|
||||
case cline.ApiProvider_OPENAI:
|
||||
return ProviderFields{
|
||||
APIKeyField: "openAiApiKey",
|
||||
BaseURLField: "openAiBaseUrl",
|
||||
PlanModeModelIDField: "planModeApiModelId",
|
||||
ActModeModelIDField: "actModeApiModelId",
|
||||
PlanModeProviderSpecificModelIDField: "planModeOpenAiModelId",
|
||||
@@ -142,6 +144,34 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
|
||||
ActModeProviderSpecificModelIDField: "actModeOpenRouterModelId",
|
||||
}, nil
|
||||
|
||||
case cline.ApiProvider_OCA:
|
||||
return ProviderFields{
|
||||
APIKeyField: "ocaApiKey",
|
||||
PlanModeModelIDField: "planModeApiModelId",
|
||||
ActModeModelIDField: "actModeApiModelId",
|
||||
PlanModeModelInfoField: "planModeOcaModelInfo",
|
||||
ActModeModelInfoField: "actModeOcaModelInfo",
|
||||
PlanModeProviderSpecificModelIDField: "planModeOcaModelId",
|
||||
ActModeProviderSpecificModelIDField: "actModeOcaModelId",
|
||||
}, nil
|
||||
case cline.ApiProvider_HICAP:
|
||||
return ProviderFields{
|
||||
APIKeyField: "hicapApiKey",
|
||||
PlanModeModelInfoField: "planModeHicapModelInfo",
|
||||
ActModeModelInfoField: "actModeHicapModelInfo",
|
||||
PlanModeProviderSpecificModelIDField: "planModeHicapModelId",
|
||||
ActModeProviderSpecificModelIDField: "actModeHicapModelId",
|
||||
}, nil
|
||||
|
||||
case cline.ApiProvider_NOUSRESEARCH:
|
||||
return ProviderFields{
|
||||
APIKeyField: "nousResearchApiKey",
|
||||
PlanModeModelIDField: "planModeApiModelId",
|
||||
ActModeModelIDField: "actModeApiModelId",
|
||||
PlanModeProviderSpecificModelIDField: "planModeNousResearchModelId",
|
||||
ActModeProviderSpecificModelIDField: "actModeNousResearchModelId",
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
|
||||
}
|
||||
@@ -150,9 +180,12 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
|
||||
// ProviderUpdatesPartial defines optional fields for partial provider updates
|
||||
// Uses pointers to distinguish between "not provided" and "set to empty"
|
||||
type ProviderUpdatesPartial struct {
|
||||
ModelID *string // New model ID (optional)
|
||||
APIKey *string // New API key (optional)
|
||||
ModelInfo interface{} // New model info (optional, provider-specific)
|
||||
ModelID *string // New model ID (optional)
|
||||
APIKey *string // New API key (optional)
|
||||
ModelInfo interface{} // New model info (optional, provider-specific)
|
||||
BaseURL *string // New base URL (optional, e.g., for OCA, Ollama)
|
||||
RefreshToken *string // New refresh token (optional, e.g., for OCA)
|
||||
Mode *string // New mode (optional, e.g., "internal" or "external" for OCA)
|
||||
}
|
||||
|
||||
// GetModelIDFieldName returns the appropriate model ID field name for a provider and mode.
|
||||
@@ -182,7 +215,7 @@ func GetModelIDFieldName(provider cline.ApiProvider, mode string) (string, error
|
||||
// buildProviderFieldMask builds a list of camelCase field paths for the field mask.
|
||||
// When includeProviderEnums is true, the provider enum fields are included (for setting active provider).
|
||||
// When false, only the data fields are included (for configuring without activating).
|
||||
func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeModelID bool, includeModelInfo bool, includeProviderEnums bool) []string {
|
||||
func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeModelID bool, includeModelInfo bool, includeBaseURL bool, includeProviderEnums bool) []string {
|
||||
var fieldPaths []string
|
||||
|
||||
// Include provider enums if requested (used when setting active provider)
|
||||
@@ -199,6 +232,11 @@ func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeMo
|
||||
}
|
||||
}
|
||||
|
||||
// Add base URL field if requested and applicable
|
||||
if includeBaseURL && fields.BaseURLField != "" {
|
||||
fieldPaths = append(fieldPaths, fields.BaseURLField)
|
||||
}
|
||||
|
||||
// Add model ID fields if requested
|
||||
if includeModelID {
|
||||
// Only include provider-specific fields if they exist, otherwise use generic fields
|
||||
@@ -245,6 +283,12 @@ func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, v
|
||||
apiConfig.CerebrasApiKey = value
|
||||
case "clineApiKey":
|
||||
apiConfig.ClineApiKey = value
|
||||
case "ocaApiKey":
|
||||
apiConfig.OcaApiKey = value
|
||||
case "hicapApiKey":
|
||||
apiConfig.HicapApiKey = value
|
||||
case "nousResearchApiKey":
|
||||
apiConfig.NousResearchApiKey = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,11 +307,20 @@ func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldNa
|
||||
case "planModeAwsBedrockCustomModelBaseId":
|
||||
apiConfig.PlanModeAwsBedrockCustomModelBaseId = value
|
||||
apiConfig.ActModeAwsBedrockCustomModelBaseId = value
|
||||
case "planModeOcaModelId":
|
||||
apiConfig.PlanModeOcaModelId = value
|
||||
apiConfig.ActModeOcaModelId = value
|
||||
case "planModeHicapModelId":
|
||||
apiConfig.PlanModeHicapModelId = value
|
||||
apiConfig.ActModeHicapModelId = value
|
||||
case "planModeNousResearchModelId":
|
||||
apiConfig.PlanModeNousResearchModelId = value
|
||||
apiConfig.ActModeNousResearchModelId = value
|
||||
}
|
||||
}
|
||||
|
||||
// AddProviderPartial configures a new provider with all necessary fields using partial updates.
|
||||
func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID string, apiKey string, modelInfo interface{}) error {
|
||||
func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID string, apiKey string, baseURL string, modelInfo interface{}) error {
|
||||
// Get field mapping for this provider
|
||||
fields, err := GetProviderFields(provider)
|
||||
if err != nil {
|
||||
@@ -282,6 +335,13 @@ func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cli
|
||||
setAPIKeyField(apiConfig, fields.APIKeyField, proto.String(apiKey))
|
||||
}
|
||||
|
||||
// Set base URL field if provided and applicable
|
||||
includeBaseURL := false
|
||||
if baseURL != "" && fields.BaseURLField != "" {
|
||||
setBaseURLField(apiConfig, fields.BaseURLField, proto.String(baseURL))
|
||||
includeBaseURL = true
|
||||
}
|
||||
|
||||
// Set model ID fields
|
||||
apiConfig.PlanModeApiModelId = proto.String(modelID)
|
||||
apiConfig.ActModeApiModelId = proto.String(modelID)
|
||||
@@ -301,7 +361,7 @@ func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cli
|
||||
|
||||
// Build field mask including all fields we're setting (without provider enums)
|
||||
includeModelInfo := fields.PlanModeModelInfoField != "" && modelInfo != nil
|
||||
fieldPaths := buildProviderFieldMask(fields, true, true, includeModelInfo, false)
|
||||
fieldPaths := buildProviderFieldMask(fields, true, true, includeModelInfo, includeBaseURL, false)
|
||||
|
||||
// Create field mask
|
||||
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
|
||||
@@ -368,7 +428,7 @@ func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider
|
||||
}
|
||||
|
||||
// Build field mask for only the fields being updated
|
||||
fieldPaths := buildProviderFieldMask(fields, includeAPIKey, includeModelID, includeModelInfo, setAsActive)
|
||||
fieldPaths := buildProviderFieldMask(fields, includeAPIKey, includeModelID, includeModelInfo, false, setAsActive)
|
||||
|
||||
// Create field mask
|
||||
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
|
||||
@@ -421,6 +481,46 @@ func RemoveProviderPartial(ctx context.Context, manager *task.Manager, provider
|
||||
return nil
|
||||
}
|
||||
|
||||
// setBaseURLField sets the appropriate base URL field in the config based on the field name
|
||||
func setBaseURLField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
|
||||
switch fieldName {
|
||||
case "ocaBaseUrl":
|
||||
apiConfig.OcaBaseUrl = value
|
||||
case "ollamaBaseUrl":
|
||||
apiConfig.OllamaBaseUrl = value
|
||||
case "openAiBaseUrl":
|
||||
apiConfig.OpenAiBaseUrl = value
|
||||
case "geminiBaseUrl":
|
||||
apiConfig.GeminiBaseUrl = value
|
||||
case "liteLlmBaseUrl":
|
||||
apiConfig.LiteLlmBaseUrl = value
|
||||
case "anthropicBaseUrl":
|
||||
apiConfig.AnthropicBaseUrl = value
|
||||
case "requestyBaseUrl":
|
||||
apiConfig.RequestyBaseUrl = value
|
||||
case "lmStudioBaseUrl":
|
||||
apiConfig.LmStudioBaseUrl = value
|
||||
case "oca":
|
||||
apiConfig.OcaBaseUrl = value
|
||||
}
|
||||
}
|
||||
|
||||
// setRefreshTokenField sets the appropriate refresh token field in the config
|
||||
func setRefreshTokenField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
|
||||
switch fieldName {
|
||||
case "ocaRefreshToken":
|
||||
apiConfig.OcaRefreshToken = value
|
||||
}
|
||||
}
|
||||
|
||||
// setModeField sets the appropriate mode field in the config
|
||||
func setModeField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
|
||||
switch fieldName {
|
||||
case "ocaMode":
|
||||
apiConfig.OcaMode = value
|
||||
}
|
||||
}
|
||||
|
||||
// BedrockOptionalFields holds optional configuration fields for AWS Bedrock
|
||||
type BedrockOptionalFields struct {
|
||||
SessionToken *string // Optional: AWS session token for temporary credentials
|
||||
@@ -434,6 +534,12 @@ type BedrockOptionalFields struct {
|
||||
Endpoint *string // Optional: Custom endpoint URL
|
||||
}
|
||||
|
||||
// OcaOptionalFields holds optional configuration fields for Oracle Code Assist
|
||||
type OcaOptionalFields struct {
|
||||
BaseURL *string // Optional: Base URL
|
||||
Mode *string // Optional: Mode ("internal" or "external")
|
||||
}
|
||||
|
||||
// setBedrockOptionalFields sets optional Bedrock-specific fields in the API configuration
|
||||
func setBedrockOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *BedrockOptionalFields) {
|
||||
if fields == nil {
|
||||
@@ -469,6 +575,20 @@ func setBedrockOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *B
|
||||
}
|
||||
}
|
||||
|
||||
// setOcaOptionalFields sets optional Oca-specific fields in the API configuration
|
||||
func setOcaOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *OcaOptionalFields) {
|
||||
if fields == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if fields.Mode != nil {
|
||||
apiConfig.OcaMode = fields.Mode
|
||||
}
|
||||
if fields.BaseURL != nil {
|
||||
apiConfig.OcaBaseUrl = fields.BaseURL
|
||||
}
|
||||
}
|
||||
|
||||
// buildBedrockOptionalFieldMask builds field mask paths for Bedrock optional fields that have values
|
||||
func buildBedrockOptionalFieldMask(fields *BedrockOptionalFields) []string {
|
||||
if fields == nil {
|
||||
@@ -507,3 +627,21 @@ func buildBedrockOptionalFieldMask(fields *BedrockOptionalFields) []string {
|
||||
|
||||
return fieldPaths
|
||||
}
|
||||
|
||||
// buildOcaOptionalFieldMask builds field mask paths for Bedrock optional fields that have values
|
||||
func buildOcaOptionalFieldMask(fields *OcaOptionalFields) []string {
|
||||
if fields == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var fieldPaths []string
|
||||
|
||||
if fields.Mode != nil {
|
||||
fieldPaths = append(fieldPaths, "ocaMode")
|
||||
}
|
||||
if fields.BaseURL != nil {
|
||||
fieldPaths = append(fieldPaths, "ocaBaseUrl")
|
||||
}
|
||||
|
||||
return fieldPaths
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
@@ -40,7 +41,7 @@ func (pw *ProviderWizard) showMainMenu() (string, error) {
|
||||
huh.NewSelect[string]().
|
||||
Title("What would you like to do?").
|
||||
Options(
|
||||
huh.NewOption("Configure a new provider", "add"),
|
||||
huh.NewOption("Add or change an API provider", "add"),
|
||||
huh.NewOption("Change model for API provider", "change-model"),
|
||||
huh.NewOption("Remove a provider", "remove"),
|
||||
huh.NewOption("List configured providers", "list"),
|
||||
@@ -107,8 +108,13 @@ func (pw *ProviderWizard) handleAddProvider() error {
|
||||
return pw.handleAddBedrockProvider()
|
||||
}
|
||||
|
||||
// Step 2b: Special handling for OCA provider
|
||||
if provider == cline.ApiProvider_OCA {
|
||||
return pw.handleAddOcaProvider()
|
||||
}
|
||||
|
||||
// Step 3: Get API key first (for non-Bedrock providers)
|
||||
apiKey, err := PromptForAPIKey(provider)
|
||||
apiKey, baseURL, err := PromptForAPIKey(provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API key: %w", err)
|
||||
}
|
||||
@@ -120,7 +126,7 @@ func (pw *ProviderWizard) handleAddProvider() error {
|
||||
}
|
||||
|
||||
// Step 5: Apply configuration using AddProviderPartial
|
||||
if err := AddProviderPartial(pw.ctx, pw.manager, provider, modelID, apiKey, modelInfo); err != nil {
|
||||
if err := AddProviderPartial(pw.ctx, pw.manager, provider, modelID, apiKey, baseURL, modelInfo); err != nil {
|
||||
return fmt.Errorf("failed to save configuration: %w", err)
|
||||
}
|
||||
|
||||
@@ -162,6 +168,51 @@ func (pw *ProviderWizard) handleAddBedrockProvider() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleAddOcaProvider handles adding Oracle Code Assist provider with optional settings and auth
|
||||
func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
// Step 1: Get OCA configuration (base URL and mode)
|
||||
config, err := PromptForOcaConfig(pw.ctx, pw.manager)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "user aborted") || strings.Contains(err.Error(), "cancelled") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
// Apply OCA configuration (base URL and mode)
|
||||
if err := ApplyOcaConfig(pw.ctx, pw.manager, config); err != nil {
|
||||
return fmt.Errorf("failed to save OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Ensure OCA authentication
|
||||
if err := ensureOcaAuthenticated(pw.ctx); err != nil {
|
||||
return fmt.Errorf("failed to authenticate with OCA: %w", err)
|
||||
}
|
||||
|
||||
// Step 3: Select model
|
||||
modelID, _, err := pw.selectModel(cline.ApiProvider_OCA, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("model selection failed: %w", err)
|
||||
}
|
||||
|
||||
// Step 4: Apply the OCA model configuration and set as active
|
||||
updates := ProviderUpdatesPartial{
|
||||
ModelID: &modelID,
|
||||
ModelInfo: nil,
|
||||
}
|
||||
|
||||
if err := UpdateProviderPartial(pw.ctx, pw.manager, cline.ApiProvider_OCA, updates, true); err != nil {
|
||||
return fmt.Errorf("failed to save OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
if err := setWelcomeViewCompleted(pw.ctx, pw.manager); err != nil {
|
||||
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ OCA provider configured successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleListProviders retrieves and displays configured providers
|
||||
func (pw *ProviderWizard) handleListProviders() error {
|
||||
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
|
||||
@@ -259,6 +310,15 @@ func (pw *ProviderWizard) fetchModelsForProvider(provider cline.ApiProvider, api
|
||||
}
|
||||
// Ollama returns just model IDs without additional info, so modelInfo map is nil
|
||||
return modelIDs, nil, nil
|
||||
|
||||
case cline.ApiProvider_OCA:
|
||||
// OCA supports dynamic model fetching
|
||||
models, err := FetchOcaModels(pw.ctx, pw.manager)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
interfaceMap := ConvertOcaModelsToInterface(models)
|
||||
return ConvertModelsMapToSlice(interfaceMap), interfaceMap, nil
|
||||
}
|
||||
|
||||
// Fall back to static models for providers that don't support dynamic fetching
|
||||
@@ -457,7 +517,7 @@ func (pw *ProviderWizard) applyModelChange(provider cline.ApiProvider, modelID s
|
||||
ModelInfo: modelInfo,
|
||||
}
|
||||
|
||||
return UpdateProviderPartial(pw.ctx, pw.manager, provider, updates, false)
|
||||
return UpdateProviderPartial(pw.ctx, pw.manager, provider, updates, true)
|
||||
}
|
||||
|
||||
// SwitchToBYOProvider switches to a BYO provider that's already configured.
|
||||
@@ -525,8 +585,17 @@ func getProviderModelIDFromState(stateData map[string]interface{}, provider clin
|
||||
return ""
|
||||
}
|
||||
|
||||
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
|
||||
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
|
||||
func getProviderAPIKeyFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
|
||||
// OCA uses account authentication, not API keys. Consider it "present" if authenticated.
|
||||
if provider == cline.ApiProvider_OCA {
|
||||
if state, _ := GetLatestOCAState(context.TODO(), 2 * time.Second); state != nil && state.User != nil {
|
||||
// Return a sentinel non-empty string so upstream checks pass.
|
||||
return "OCA_AUTH_VERIFIED"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fields, err := GetProviderFields(provider)
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -656,7 +725,16 @@ func (pw *ProviderWizard) handleRemoveProvider() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 7: Clear the API key for the selected provider
|
||||
// Step 7: If removing OCA, sign out first
|
||||
if selectedProvider.Provider == cline.ApiProvider_OCA {
|
||||
if err := signOutOca(pw.ctx); err != nil {
|
||||
fmt.Printf("Warning: Failed to sign out of OCA: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Signed out of OCA.")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: Clear the API key for the selected provider
|
||||
if err := pw.clearProviderAPIKey(selectedProvider.Provider); err != nil {
|
||||
return fmt.Errorf("failed to remove provider: %w", err)
|
||||
}
|
||||
@@ -670,6 +748,16 @@ func (pw *ProviderWizard) clearProviderAPIKey(provider cline.ApiProvider) error
|
||||
return RemoveProviderPartial(pw.ctx, pw.manager, provider)
|
||||
}
|
||||
|
||||
|
||||
func signOutOca(ctx context.Context) error {
|
||||
client, err := global.GetDefaultClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.Ocaaccount.OcaAccountLogoutClicked(ctx, &cline.EmptyRequest{})
|
||||
return err
|
||||
}
|
||||
|
||||
func setWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
|
||||
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
)
|
||||
|
||||
// OcaConfig holds Oracle Code Assist (OCA) configuration fields
|
||||
type OcaConfig struct {
|
||||
BaseURL string
|
||||
Mode string
|
||||
}
|
||||
|
||||
// PromptForOcaConfig displays a form for OCA configuration (base URL and mode)
|
||||
func PromptForOcaConfig(ctx context.Context, manager *task.Manager) (*OcaConfig, error) {
|
||||
config := &OcaConfig{}
|
||||
var mode string
|
||||
|
||||
// Collect optional settings
|
||||
configForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Base URL").
|
||||
Value(&config.BaseURL).
|
||||
Description("Leave empty to use default Base URL"),
|
||||
|
||||
huh.NewSelect[string]().
|
||||
Title("Choose OCA mode (used for authentication)").
|
||||
Description("Select 'Internal' to use Cline's internal OCA, or 'External' for your own OCA instance").
|
||||
Options(
|
||||
huh.NewOption("Internal", "internal"),
|
||||
huh.NewOption("External", "external"),
|
||||
).
|
||||
Value(&mode),
|
||||
),
|
||||
)
|
||||
|
||||
if err := configForm.Run(); err != nil {
|
||||
return nil, fmt.Errorf("failed to get OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
// Trim whitespace from string fields
|
||||
config.BaseURL = strings.TrimSpace(config.BaseURL)
|
||||
config.Mode = strings.TrimSpace(mode)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// ApplyOcaConfig applies OCA configuration using partial updates
|
||||
func ApplyOcaConfig(ctx context.Context, manager *task.Manager, config *OcaConfig) error {
|
||||
// Build the API configuration with all OCA fields
|
||||
apiConfig := &cline.ModelsApiConfiguration{}
|
||||
|
||||
// Set profile authentication fields (always required)
|
||||
optionalFields := &OcaOptionalFields{}
|
||||
|
||||
// Set profile name (can be empty for default profile)
|
||||
if config.BaseURL != "" {
|
||||
optionalFields.BaseURL = proto.String(config.BaseURL)
|
||||
}
|
||||
|
||||
// Set optional fields if provided
|
||||
if config.Mode != "" {
|
||||
optionalFields.Mode = proto.String(config.Mode)
|
||||
}
|
||||
|
||||
// Apply all fields to the config
|
||||
setOcaOptionalFields(apiConfig, optionalFields)
|
||||
|
||||
// Add profile authentication field paths
|
||||
optionalPaths := buildOcaOptionalFieldMask(optionalFields)
|
||||
|
||||
// Create field mask
|
||||
fieldMask := &fieldmaskpb.FieldMask{Paths: optionalPaths}
|
||||
|
||||
// Apply the partial update
|
||||
request := &cline.UpdateApiConfigurationPartialRequest{
|
||||
ApiConfiguration: apiConfig,
|
||||
UpdateMask: fieldMask,
|
||||
}
|
||||
|
||||
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
|
||||
return fmt.Errorf("failed to apply OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// OCA Auth Listener Singleton
|
||||
// ===========================
|
||||
|
||||
type ocaAuthStream interface {
|
||||
Recv() (*cline.OcaAuthState, error)
|
||||
}
|
||||
|
||||
// OcaAuthStatusListener manages subscription to OCA auth status updates
|
||||
type OcaAuthStatusListener struct {
|
||||
stream ocaAuthStream
|
||||
updatesCh chan *cline.OcaAuthState
|
||||
errCh chan error
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
lastState *cline.OcaAuthState
|
||||
firstEventCh chan struct{}
|
||||
firstEventOnce sync.Once
|
||||
}
|
||||
|
||||
// NewOcaAuthStatusListener creates a new OCA auth status listener
|
||||
func NewOcaAuthStatusListener(parentCtx context.Context) (*OcaAuthStatusListener, error) {
|
||||
client, err := global.GetDefaultClient(parentCtx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get client: %w", err)
|
||||
}
|
||||
|
||||
// Keep the listener alive independently of short-lived caller contexts
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Subscribe to OCA auth status updates
|
||||
stream, err := client.Ocaaccount.OcaSubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to subscribe to OCA auth updates: %w", err)
|
||||
}
|
||||
|
||||
return &OcaAuthStatusListener{
|
||||
stream: stream,
|
||||
updatesCh: make(chan *cline.OcaAuthState, 10),
|
||||
errCh: make(chan error, 1),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
firstEventCh: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start begins listening to the auth status update stream
|
||||
func (l *OcaAuthStatusListener) Start() error {
|
||||
go l.readStream()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *OcaAuthStatusListener) readStream() {
|
||||
defer close(l.updatesCh)
|
||||
defer close(l.errCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
return
|
||||
default:
|
||||
state, err := l.stream.Recv()
|
||||
if err != nil {
|
||||
// Propagate error and exit
|
||||
if err == io.EOF {
|
||||
// Treat as error to notify waiters
|
||||
err = fmt.Errorf("OCA auth status stream closed")
|
||||
}
|
||||
select {
|
||||
case l.errCh <- err:
|
||||
case <-l.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
l.lastState = state
|
||||
l.mu.Unlock()
|
||||
|
||||
// Notify first event waiters
|
||||
l.firstEventOnce.Do(func() { close(l.firstEventCh) })
|
||||
|
||||
select {
|
||||
case l.updatesCh <- state:
|
||||
case <-l.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForFirstEvent blocks until the first event is received or timeout occurs
|
||||
func (l *OcaAuthStatusListener) WaitForFirstEvent(timeout time.Duration) error {
|
||||
// Fast-path if already have a state
|
||||
l.mu.RLock()
|
||||
ready := l.lastState != nil
|
||||
l.mu.RUnlock()
|
||||
if ready {
|
||||
return nil
|
||||
}
|
||||
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-l.firstEventCh:
|
||||
return nil
|
||||
case <-timer.C:
|
||||
return fmt.Errorf("timeout waiting for initial OCA auth event")
|
||||
case <-l.ctx.Done():
|
||||
return fmt.Errorf("OCA auth listener cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
// IsAuthenticated returns true if the last known OCA auth state is authenticated
|
||||
func (l *OcaAuthStatusListener) IsAuthenticated() bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return isOCAStateAuthenticated(l.lastState)
|
||||
}
|
||||
|
||||
// WaitForAuthentication waits until OCA authentication succeeds or timeout occurs
|
||||
func (l *OcaAuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
// If already authenticated, return immediately
|
||||
if l.IsAuthenticated() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
return fmt.Errorf("OCA authentication timeout after %v - please try again", timeout)
|
||||
case <-l.ctx.Done():
|
||||
return fmt.Errorf("OCA authentication cancelled")
|
||||
case err := <-l.errCh:
|
||||
return fmt.Errorf("OCA authentication stream error: %w", err)
|
||||
case state := <-l.updatesCh:
|
||||
if isOCAStateAuthenticated(state) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop closes the stream and cleans up resources
|
||||
func (l *OcaAuthStatusListener) Stop() {
|
||||
l.cancel()
|
||||
}
|
||||
|
||||
func isOCAStateAuthenticated(state *cline.OcaAuthState) bool {
|
||||
return state != nil && state.User != nil
|
||||
}
|
||||
|
||||
// Singleton holder
|
||||
var (
|
||||
ocaListener *OcaAuthStatusListener
|
||||
ocaListenerOnce sync.Once
|
||||
ocaListenerErr error
|
||||
)
|
||||
|
||||
// GetOcaAuthListener returns the OCA auth listener singleton
|
||||
func GetOcaAuthListener(ctx context.Context) (*OcaAuthStatusListener, error) {
|
||||
// Allow optional ctx: if nil, use context.TODO(). If already initialized, return singleton.
|
||||
if ctx == nil {
|
||||
ctx = context.TODO()
|
||||
}
|
||||
|
||||
ocaListenerOnce.Do(func() {
|
||||
l, err := NewOcaAuthStatusListener(ctx)
|
||||
if err != nil {
|
||||
ocaListenerErr = err
|
||||
return
|
||||
}
|
||||
if err := l.Start(); err != nil {
|
||||
ocaListenerErr = err
|
||||
return
|
||||
}
|
||||
ocaListener = l
|
||||
})
|
||||
return ocaListener, ocaListenerErr
|
||||
}
|
||||
|
||||
// IsOCAAuthenticated returns true if the global OCA auth status is authenticated.
|
||||
// It attempts a brief wait for the first event to avoid stale reads.
|
||||
func IsOCAAuthenticated(ctx context.Context) bool {
|
||||
l, err := GetOcaAuthListener(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = l.WaitForFirstEvent(1 * time.Second) // best-effort
|
||||
return l.IsAuthenticated()
|
||||
}
|
||||
|
||||
// LatestState returns the last received OCA auth state (may be nil)
|
||||
func (l *OcaAuthStatusListener) LatestState() *cline.OcaAuthState {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return l.lastState
|
||||
}
|
||||
|
||||
// GetLatestOCAState returns the latest known OCA auth state, optionally waiting for the first event
|
||||
func GetLatestOCAState(ctx context.Context, timeout time.Duration) (*cline.OcaAuthState, error) {
|
||||
l, err := GetOcaAuthListener(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if timeout > 0 {
|
||||
if err := l.WaitForFirstEvent(timeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return l.LatestState(), nil
|
||||
}
|
||||
|
||||
// ensureOcaAuthenticated initiates OCA login (if needed) and waits for success using the singleton listener
|
||||
func ensureOcaAuthenticated(ctx context.Context) error {
|
||||
// Ensure listener exists
|
||||
listener, err := GetOcaAuthListener(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize OCA auth listener: %w", err)
|
||||
}
|
||||
|
||||
// Briefly wait for first event to know current state
|
||||
_ = listener.WaitForFirstEvent(1 * time.Second)
|
||||
|
||||
// If already authenticated, nothing to do
|
||||
if listener.IsAuthenticated() {
|
||||
fmt.Println("✓ OCA authentication already active.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create gRPC client for initiating login
|
||||
client, err := global.GetDefaultClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to obtain client: %w", err)
|
||||
}
|
||||
|
||||
// Start login and wait for authentication
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Initiate login (opens the browser with a callback URL from Cline Core)
|
||||
response, err := client.Ocaaccount.OcaAccountLoginClicked(waitCtx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initiate OCA login: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\nOpening browser for OCA authentication...")
|
||||
if response != nil && response.Value != "" {
|
||||
fmt.Printf("If the browser doesn't open automatically, visit this URL:\n%s\n\n", response.Value)
|
||||
}
|
||||
fmt.Println("Waiting for you to complete OCA authentication in your browser...")
|
||||
fmt.Println("(This may take a few moments. Timeout: 5 minutes)")
|
||||
|
||||
// Block until authenticated or timeout
|
||||
if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("✓ OCA authentication successful!")
|
||||
return nil
|
||||
}
|
||||
@@ -123,7 +123,10 @@ func setCommand() *cobra.Command {
|
||||
Use: "set <key=value> [key=value...]",
|
||||
Aliases: []string{"s"},
|
||||
Short: "Set configuration variables",
|
||||
Long: `Set one or more global configuration variables using key=value format.`,
|
||||
Long: `Set one or more global configuration variables using key=value format.
|
||||
|
||||
This command merges the provided settings with existing values, preserving
|
||||
unspecified fields. Only the fields you explicitly set will be updated.`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
@@ -139,7 +142,7 @@ func setCommand() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update settings
|
||||
// Update settings (server-side merge handles preserving existing values)
|
||||
return configManager.UpdateSettings(ctx, settings, secrets)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ func renderAutoApprovalSettings(value interface{}, censor bool) error {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Print other fields normally (enabled, maxRequests, enableNotifications, favorites)
|
||||
// Print other fields normally (enabled, enableNotifications, favorites)
|
||||
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/output"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
@@ -14,6 +15,16 @@ type Renderer struct {
|
||||
typewriter *TypewriterPrinter
|
||||
mdRenderer *MarkdownRenderer
|
||||
outputFormat string
|
||||
|
||||
// Lipgloss styles that respect outputFormat
|
||||
dimStyle lipgloss.Style
|
||||
greenStyle lipgloss.Style
|
||||
redStyle lipgloss.Style
|
||||
yellowStyle lipgloss.Style
|
||||
blueStyle lipgloss.Style
|
||||
whiteStyle lipgloss.Style
|
||||
boldStyle lipgloss.Style
|
||||
successStyle lipgloss.Style
|
||||
}
|
||||
|
||||
func NewRenderer(outputFormat string) *Renderer {
|
||||
@@ -22,11 +33,23 @@ func NewRenderer(outputFormat string) *Renderer {
|
||||
mdRenderer = nil
|
||||
}
|
||||
|
||||
return &Renderer{
|
||||
r := &Renderer{
|
||||
typewriter: NewTypewriterPrinter(DefaultTypewriterConfig()),
|
||||
mdRenderer: mdRenderer,
|
||||
outputFormat: outputFormat,
|
||||
}
|
||||
|
||||
// Initialize lipgloss styles (will respect the global color profile)
|
||||
r.dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
r.greenStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
|
||||
r.redStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
|
||||
r.yellowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3"))
|
||||
r.blueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
|
||||
r.whiteStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("7"))
|
||||
r.boldStyle = lipgloss.NewStyle().Bold(true)
|
||||
r.successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Renderer) RenderMessage(prefix, text string, newline bool) error {
|
||||
@@ -206,21 +229,76 @@ func (r *Renderer) GetMdRenderer() *MarkdownRenderer {
|
||||
|
||||
// RenderMarkdown renders markdown text to terminal format with ANSI codes
|
||||
// Falls back to plaintext if markdown rendering is unavailable or fails
|
||||
// Respects output format - skips rendering in plain mode
|
||||
// Respects output format - skips rendering in plain mode or non-TTY contexts
|
||||
func (r *Renderer) RenderMarkdown(markdown string) string {
|
||||
// Skip markdown rendering in plain mode
|
||||
if r.outputFormat == "plain" {
|
||||
// Skip markdown rendering if:
|
||||
// 1. Output format is explicitly "plain"
|
||||
// 2. Not in a TTY (piped output, file redirect, CI, etc.)
|
||||
if r.outputFormat == "plain" || !isTTY() {
|
||||
return markdown
|
||||
}
|
||||
|
||||
|
||||
if r.mdRenderer == nil {
|
||||
return markdown
|
||||
}
|
||||
|
||||
|
||||
rendered, err := r.mdRenderer.Render(markdown)
|
||||
if err != nil {
|
||||
return markdown
|
||||
}
|
||||
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
// Lipgloss-based color rendering methods
|
||||
// These automatically respect the output format via lipgloss color profile
|
||||
|
||||
// Dim renders text in dim gray (bright black)
|
||||
func (r *Renderer) Dim(text string) string {
|
||||
return r.dimStyle.Render(text)
|
||||
}
|
||||
|
||||
// Green renders text in green
|
||||
func (r *Renderer) Green(text string) string {
|
||||
return r.greenStyle.Render(text)
|
||||
}
|
||||
|
||||
// Red renders text in red
|
||||
func (r *Renderer) Red(text string) string {
|
||||
return r.redStyle.Render(text)
|
||||
}
|
||||
|
||||
// Yellow renders text in yellow
|
||||
func (r *Renderer) Yellow(text string) string {
|
||||
return r.yellowStyle.Render(text)
|
||||
}
|
||||
|
||||
// Blue renders text in 256-color blue (index 39)
|
||||
func (r *Renderer) Blue(text string) string {
|
||||
return r.blueStyle.Render(text)
|
||||
}
|
||||
|
||||
// White renders text in white
|
||||
func (r *Renderer) White(text string) string {
|
||||
return r.whiteStyle.Render(text)
|
||||
}
|
||||
|
||||
// Bold renders text in bold
|
||||
func (r *Renderer) Bold(text string) string {
|
||||
return r.boldStyle.Render(text)
|
||||
}
|
||||
|
||||
// Success renders text in green with bold
|
||||
func (r *Renderer) Success(text string) string {
|
||||
return r.successStyle.Render(text)
|
||||
}
|
||||
|
||||
// SuccessWithCheckmark renders text in green with bold and a checkmark prefix
|
||||
func (r *Renderer) SuccessWithCheckmark(text string) string {
|
||||
return r.Success("✓ " + text)
|
||||
}
|
||||
|
||||
// ErrorWithX renders text in red with an X prefix
|
||||
func (r *Renderer) ErrorWithX(text string) string {
|
||||
return r.Red("✗ " + text)
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ func NewStreamingSegment(sayType, prefix string, mdRenderer *MarkdownRenderer, s
|
||||
toolParser: NewToolResultParser(mdRenderer),
|
||||
}
|
||||
|
||||
// Render rich header immediately when creating segment (if in rich mode)
|
||||
if shouldMarkdown && outputFormat != "plain" {
|
||||
// Render rich header immediately when creating segment (if in rich mode and TTY)
|
||||
if shouldMarkdown && outputFormat != "plain" && isTTY() {
|
||||
header := ss.generateRichHeader()
|
||||
rendered, _ := mdRenderer.Render(header)
|
||||
output.Println("")
|
||||
@@ -113,8 +113,8 @@ func (ss *StreamingSegment) renderFinal(currentBuffer string) {
|
||||
} else if ss.sayType == string(types.SayTypeCommand) {
|
||||
// Command output
|
||||
bodyContent = "```shell\n" + currentBuffer + "\n```"
|
||||
// Render markdown
|
||||
if ss.shouldMarkdown && ss.outputFormat != "plain" {
|
||||
// Render markdown only in rich mode and TTY
|
||||
if ss.shouldMarkdown && ss.outputFormat != "plain" && isTTY() {
|
||||
rendered, err := ss.mdRenderer.Render(bodyContent)
|
||||
if err == nil {
|
||||
bodyContent = rendered
|
||||
@@ -122,7 +122,7 @@ func (ss *StreamingSegment) renderFinal(currentBuffer string) {
|
||||
}
|
||||
} else {
|
||||
// For other types (reasoning, text, etc.), render markdown as-is
|
||||
if ss.shouldMarkdown && ss.outputFormat != "plain" {
|
||||
if ss.shouldMarkdown && ss.outputFormat != "plain" && isTTY() {
|
||||
rendered, err := ss.mdRenderer.Render(currentBuffer)
|
||||
if err == nil {
|
||||
bodyContent = rendered
|
||||
|
||||
@@ -106,6 +106,14 @@ func (tr *ToolRenderer) generateToolHeader(tool *types.ToolMessage, verbTense st
|
||||
}
|
||||
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
|
||||
|
||||
case string(types.ToolTypeFileDeleted):
|
||||
if verbTense == "wants to" {
|
||||
action = "wants to delete"
|
||||
} else {
|
||||
action = "is deleting"
|
||||
}
|
||||
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
|
||||
|
||||
case string(types.ToolTypeListFilesTopLevel):
|
||||
if verbTense == "wants to" {
|
||||
action = "wants to list files in"
|
||||
@@ -199,7 +207,7 @@ func (tr *ToolRenderer) GenerateToolContentPreview(tool *types.ToolMessage) stri
|
||||
previewMd := fmt.Sprintf("```\n%s\n```", preview)
|
||||
return tr.renderMarkdown(previewMd)
|
||||
|
||||
case string(types.ToolTypeReadFile), string(types.ToolTypeWebFetch):
|
||||
case string(types.ToolTypeReadFile), string(types.ToolTypeWebFetch), string(types.ToolTypeFileDeleted):
|
||||
// No preview for read/fetch operations
|
||||
return ""
|
||||
|
||||
@@ -226,7 +234,8 @@ func (tr *ToolRenderer) GenerateToolContentBody(tool *types.ToolMessage) string
|
||||
toolParser := NewToolResultParser(tr.mdRenderer)
|
||||
|
||||
switch tool.Tool {
|
||||
case string(types.ToolTypeReadFile):
|
||||
case string(types.ToolTypeReadFile),
|
||||
string(types.ToolTypeFileDeleted):
|
||||
// readFile: show header only, no body
|
||||
return ""
|
||||
|
||||
@@ -339,9 +348,10 @@ func (tr *ToolRenderer) RenderUserResponse(approved bool, feedback string) strin
|
||||
return fmt.Sprintf("%s %s\n", symbol, status)
|
||||
}
|
||||
|
||||
// renderMarkdown renders markdown if not in plain mode
|
||||
// renderMarkdown renders markdown if not in plain mode and in a TTY
|
||||
func (tr *ToolRenderer) renderMarkdown(markdown string) string {
|
||||
if tr.outputFormat == "plain" {
|
||||
// Skip markdown rendering if plain mode or not in TTY
|
||||
if tr.outputFormat == "plain" || !isTTY() {
|
||||
return markdown
|
||||
}
|
||||
|
||||
|
||||
@@ -221,83 +221,7 @@ func (p *ToolResultParser) ParseCodeDefinitions(content string) string {
|
||||
|
||||
// ParseWebFetch formats webFetch tool results with content preview
|
||||
func (p *ToolResultParser) ParseWebFetch(content, url string) string {
|
||||
if content == "" {
|
||||
return fmt.Sprintf("*Fetched content from %s (empty response)*", url)
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
var result strings.Builder
|
||||
|
||||
// Try to extract title
|
||||
var title string
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "##") {
|
||||
title = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if title != "" {
|
||||
result.WriteString(fmt.Sprintf("**Title:** %s\n\n", title))
|
||||
}
|
||||
|
||||
// Show preview of content
|
||||
result.WriteString("**Preview:**\n")
|
||||
|
||||
charCount := 0
|
||||
maxChars := 500
|
||||
previewLines := []string{}
|
||||
|
||||
for _, line := range lines {
|
||||
// Skip markdown headers
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if charCount+len(trimmed) > maxChars {
|
||||
break
|
||||
}
|
||||
|
||||
previewLines = append(previewLines, trimmed)
|
||||
charCount += len(trimmed)
|
||||
}
|
||||
|
||||
result.WriteString(strings.Join(previewLines, " "))
|
||||
result.WriteString("...\n\n")
|
||||
|
||||
// Extract sections
|
||||
sections := []string{}
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "##") {
|
||||
section := strings.TrimSpace(strings.TrimPrefix(trimmed, "##"))
|
||||
sections = append(sections, section)
|
||||
if len(sections) >= 5 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(sections) > 0 {
|
||||
result.WriteString("**Sections Found:**\n")
|
||||
for _, section := range sections {
|
||||
result.WriteString(fmt.Sprintf("- %s\n", section))
|
||||
}
|
||||
result.WriteString("\n")
|
||||
}
|
||||
|
||||
// Word count estimate
|
||||
wordCount := len(strings.Fields(content))
|
||||
result.WriteString(fmt.Sprintf("*[Full content: ~%s]*", p.formatWordCount(wordCount)))
|
||||
|
||||
return result.String()
|
||||
return ""
|
||||
}
|
||||
|
||||
// detectLanguage returns syntax highlighting language based on file extension
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/display"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/terminal"
|
||||
"github.com/cline/cli/pkg/cli/updater"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewDoctorCommand creates the doctor command
|
||||
func NewDoctorCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "doctor",
|
||||
Aliases: []string{"d"},
|
||||
Short: "Check system health and diagnose problems",
|
||||
Long: `Check the health of your Cline CLI installation and diagnose problems.
|
||||
|
||||
Currently this command performs the following checks and fixes:
|
||||
|
||||
Terminal Configuration:
|
||||
- Detects your terminal emulator (VS Code, Cursor, Ghostty, Kitty, WezTerm, Alacritty)
|
||||
- Configures shift+enter to insert newlines in multiline input
|
||||
- Creates backups before modifying configuration files
|
||||
- Supported terminals: VS Code, Cursor, Ghostty, Kitty, WezTerm, Alacritty
|
||||
- iTerm2 works by default, Terminal.app requires manual setup
|
||||
|
||||
CLI Updates:
|
||||
- Checks npm registry for the latest version
|
||||
- Automatically installs updates via npm if available
|
||||
- Respects NO_AUTO_UPDATE environment variable
|
||||
- Skipped in CI environments
|
||||
|
||||
Note: Future versions will include additional health checks for Node.js version,
|
||||
npm availability, Cline Core connectivity, database integrity, and more.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDoctorChecks()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runDoctorChecks performs all doctor diagnostics and configuration
|
||||
func runDoctorChecks() error {
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
|
||||
fmt.Printf("\n%s\n\n", renderer.Bold("Cline Doctor - System Health Check"))
|
||||
|
||||
// Configure terminal keybindings (terminal.go prints its own status)
|
||||
fmt.Printf("%s\n\n", renderer.Dim("━━━ Terminal Configuration ━━━"))
|
||||
terminal.SetupKeyboardSync()
|
||||
|
||||
// Check for updates (updater.go prints its own status)
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("━━━ CLI Updates ━━━"))
|
||||
updater.CheckAndUpdateSync(global.Config.Verbose, true)
|
||||
|
||||
// Summary
|
||||
fmt.Printf("\n%s\n", renderer.Dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))
|
||||
fmt.Printf("\n%s\n\n", renderer.SuccessWithCheckmark("Health check complete"))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/common"
|
||||
"github.com/cline/grpc-go/client"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
type Port uint16
|
||||
@@ -23,11 +25,14 @@ var (
|
||||
Config *GlobalConfig
|
||||
Clients *ClineClients
|
||||
|
||||
// Version info - set at build time via ldflags in cli/version.go
|
||||
// Version info - set at build time via ldflags
|
||||
// Version is the Cline Core version (from root package.json)
|
||||
Version = "dev"
|
||||
Commit = "unknown"
|
||||
Date = "unknown"
|
||||
BuiltBy = "unknown"
|
||||
// CliVersion is the CLI package version (from cli/package.json)
|
||||
CliVersion = "dev"
|
||||
Commit = "unknown"
|
||||
Date = "unknown"
|
||||
BuiltBy = "unknown"
|
||||
)
|
||||
|
||||
func InitializeGlobalConfig(cfg *GlobalConfig) error {
|
||||
@@ -44,6 +49,12 @@ func InitializeGlobalConfig(cfg *GlobalConfig) error {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
// Configure lipgloss color profile based on output format
|
||||
if cfg.OutputFormat == "plain" {
|
||||
lipgloss.SetColorProfile(termenv.Ascii) // NO COLOR mode
|
||||
}
|
||||
// Otherwise lipgloss auto-detects terminal capabilities (default behavior)
|
||||
|
||||
Config = cfg
|
||||
Clients = NewClineClients(cfg.ConfigPath)
|
||||
|
||||
@@ -99,4 +110,4 @@ func EnsureDefaultInstance(ctx context.Context) error {
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,6 @@ func (h *AskHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
return h.handleResumeCompletedTask(msg, dc)
|
||||
case string(types.AskTypeMistakeLimitReached):
|
||||
return h.handleMistakeLimitReached(msg, dc)
|
||||
case string(types.AskTypeAutoApprovalMaxReached):
|
||||
return h.handleAutoApprovalMaxReached(msg, dc)
|
||||
case string(types.AskTypeBrowserActionLaunch):
|
||||
return h.handleBrowserActionLaunch(msg, dc)
|
||||
case string(types.AskTypeUseMcpServer):
|
||||
@@ -71,22 +69,25 @@ func (h *AskHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
|
||||
// handleFollowup handles followup questions
|
||||
func (h *AskHandler) handleFollowup(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
// Use ToolRenderer for unified rendering
|
||||
header := dc.ToolRenderer.GenerateAskFollowupHeader()
|
||||
body := dc.ToolRenderer.GenerateAskFollowupBody(msg.Text)
|
||||
|
||||
if body == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render header
|
||||
rendered := dc.Renderer.RenderMarkdown(header)
|
||||
output.Print("\n")
|
||||
output.Print(rendered)
|
||||
output.Print("\n")
|
||||
|
||||
// Render body
|
||||
output.Print(body)
|
||||
if dc.IsStreamingMode {
|
||||
// In streaming mode, header was already shown by partial stream
|
||||
// Just render the body content
|
||||
output.Print(body)
|
||||
} else {
|
||||
// Non-streaming mode: render header + body together
|
||||
header := dc.ToolRenderer.GenerateAskFollowupHeader()
|
||||
rendered := dc.Renderer.RenderMarkdown(header)
|
||||
output.Print("\n")
|
||||
output.Print(rendered)
|
||||
output.Print("\n")
|
||||
output.Print(body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -125,8 +126,8 @@ func (h *AskHandler) handlePlanModeRespond(msg *types.ClineMessage, dc *DisplayC
|
||||
// showApprovalHint displays a hint in non-interactive mode about how to approve/deny
|
||||
func (h *AskHandler) showApprovalHint(dc *DisplayContext) {
|
||||
if !dc.IsInteractive {
|
||||
output.Printf("\n\033[90mCline is requesting approval to use this tool\033[0m\n")
|
||||
output.Printf("\033[90mUse \033[0mcline task send --approve\033[90m or \033[0m--deny\033[90m to respond\033[0m\n")
|
||||
output.Printf("\n%s\n", dc.Renderer.Dim("Cline is requesting approval to use this tool"))
|
||||
output.Printf("%s\n", dc.Renderer.Dim("Use cline task send --approve or --deny to respond"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,9 +178,19 @@ func (h *AskHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) err
|
||||
return dc.Renderer.RenderMessage("TOOL", msg.Text, true)
|
||||
}
|
||||
|
||||
// Use unified ToolRenderer
|
||||
rendered := dc.ToolRenderer.RenderToolApprovalRequest(&tool)
|
||||
output.Print(rendered)
|
||||
if dc.IsStreamingMode {
|
||||
// In streaming mode, header was already shown by partial stream
|
||||
// Just render the content preview
|
||||
contentPreview := dc.ToolRenderer.GenerateToolContentPreview(&tool)
|
||||
if contentPreview != "" {
|
||||
output.Print("\n")
|
||||
output.Print(contentPreview)
|
||||
}
|
||||
} else {
|
||||
// Non-streaming mode: render full approval (header + preview)
|
||||
rendered := dc.ToolRenderer.RenderToolApprovalRequest(&tool)
|
||||
output.Print(rendered)
|
||||
}
|
||||
|
||||
h.showApprovalHint(dc)
|
||||
return nil
|
||||
@@ -242,25 +253,6 @@ func (h *AskHandler) handleMistakeLimitReached(msg *types.ClineMessage, dc *Disp
|
||||
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("Mistake Limit Reached: %s. Approval required.", msg.Text), true)
|
||||
}
|
||||
|
||||
// handleAutoApprovalMaxReached handles auto-approval max reached
|
||||
func (h *AskHandler) handleAutoApprovalMaxReached(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
if dc.SystemRenderer != nil {
|
||||
details := make(map[string]string)
|
||||
if msg.Text != "" {
|
||||
details["reason"] = msg.Text
|
||||
}
|
||||
dc.SystemRenderer.RenderError(
|
||||
"warning",
|
||||
"Auto-Approval Limit Reached",
|
||||
"The maximum number of auto-approved requests has been reached. Manual approval is now required.",
|
||||
details,
|
||||
)
|
||||
fmt.Printf("\n**Approval required to continue.**\n")
|
||||
return nil
|
||||
}
|
||||
return dc.Renderer.RenderMessage("WARNING", fmt.Sprintf("Auto-approval limit reached: %s. Approval required.", msg.Text), true)
|
||||
}
|
||||
|
||||
// handleBrowserActionLaunch handles browser action launch requests
|
||||
func (h *AskHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
url := strings.TrimSpace(msg.Text)
|
||||
|
||||
@@ -389,20 +389,21 @@ func newInstanceListCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
// Render the markdown table with terminal width for nice table layout
|
||||
renderer, err := display.NewMarkdownRendererForTerminal()
|
||||
mdRenderer, err := display.NewMarkdownRendererForTerminal()
|
||||
if err != nil {
|
||||
// Fallback to plain table if markdown renderer fails
|
||||
fmt.Println(markdown.String())
|
||||
} else {
|
||||
rendered, err := renderer.Render(markdown.String())
|
||||
rendered, err := mdRenderer.Render(markdown.String())
|
||||
if err != nil {
|
||||
fmt.Println(markdown.String())
|
||||
} else {
|
||||
// Post-process to colorize status values
|
||||
rendered = strings.ReplaceAll(rendered, "SERVING", "\033[32mSERVING\033[0m") // Green
|
||||
rendered = strings.ReplaceAll(rendered, "✓", "\033[32m✓\033[0m") // Green
|
||||
rendered = strings.ReplaceAll(rendered, "NOT_SERVING", "\033[31mNOT_SERVING\033[0m") // Red
|
||||
rendered = strings.ReplaceAll(rendered, "UNKNOWN", "\033[33mUNKNOWN\033[0m") // Yellow
|
||||
colorRenderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
rendered = strings.ReplaceAll(rendered, "SERVING", colorRenderer.Green("SERVING"))
|
||||
rendered = strings.ReplaceAll(rendered, "✓", colorRenderer.Green("✓"))
|
||||
rendered = strings.ReplaceAll(rendered, "NOT_SERVING", colorRenderer.Red("NOT_SERVING"))
|
||||
rendered = strings.ReplaceAll(rendered, "UNKNOWN", colorRenderer.Yellow("UNKNOWN"))
|
||||
|
||||
fmt.Print(strings.TrimLeft(rendered, "\n"))
|
||||
}
|
||||
|
||||
@@ -208,9 +208,9 @@ func listLogFiles(logsDir string) ([]logFileInfo, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by created time (newest first)
|
||||
// Sort by created time (oldest first)
|
||||
sort.Slice(logs, func(i, j int) bool {
|
||||
return logs[i].created.After(logs[j].created)
|
||||
return logs[i].created.Before(logs[j].created)
|
||||
})
|
||||
|
||||
return logs, nil
|
||||
@@ -340,6 +340,7 @@ func renderLogsTable(logs []logFileInfo, markForDeletion bool) error {
|
||||
}
|
||||
|
||||
// Use markdown table for rich output
|
||||
colorRenderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString("| **FILENAME** | **SIZE** | **CREATED** | **AGE** |\n")
|
||||
markdown.WriteString("|--------------|----------|-------------|---------|")
|
||||
@@ -352,9 +353,9 @@ func renderLogsTable(logs []logFileInfo, markForDeletion bool) error {
|
||||
row.age,
|
||||
)
|
||||
|
||||
// If marking for deletion, wrap in red ANSI codes
|
||||
// If marking for deletion, wrap in red
|
||||
if markForDeletion {
|
||||
line = "\033[31m" + line + "\033[0m"
|
||||
line = colorRenderer.Red(line)
|
||||
}
|
||||
|
||||
markdown.WriteString(line)
|
||||
@@ -378,4 +379,4 @@ func renderLogsTable(logs []logFileInfo, markForDeletion bool) error {
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,11 @@ const (
|
||||
|
||||
// InputSubmitMsg is sent when the user submits input
|
||||
type InputSubmitMsg struct {
|
||||
Value string
|
||||
InputType InputType
|
||||
Approved bool // For approval type
|
||||
NeedsFeedback bool // For approval type
|
||||
Value string
|
||||
InputType InputType
|
||||
Approved bool // For approval type
|
||||
NeedsFeedback bool // For approval type
|
||||
NoAskAgain bool // For approval type - indicates "don't ask again" was selected
|
||||
}
|
||||
|
||||
// InputCancelMsg is sent when the user cancels input (Ctrl+C)
|
||||
@@ -61,6 +62,7 @@ type InputModel struct {
|
||||
// For approval type
|
||||
approvalOptions []string
|
||||
selectedOption int
|
||||
pendingApproval bool // Stores approval decision when transitioning to feedback input
|
||||
|
||||
// Styles (huh-inspired theme)
|
||||
styles fieldStyles
|
||||
@@ -158,8 +160,7 @@ func NewInputModel(inputType InputType, title, placeholder, currentMode string)
|
||||
if inputType == InputTypeApproval {
|
||||
m.approvalOptions = []string{
|
||||
"Yes",
|
||||
"Yes, with feedback",
|
||||
"No",
|
||||
"Yes, and don't ask again for this task",
|
||||
"No, with feedback",
|
||||
}
|
||||
m.selectedOption = 0
|
||||
@@ -209,8 +210,7 @@ func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if msg.InputType == InputTypeApproval {
|
||||
m.approvalOptions = []string{
|
||||
"Yes",
|
||||
"Yes, with feedback",
|
||||
"No",
|
||||
"Yes, and don't ask again for this task",
|
||||
"No, with feedback",
|
||||
}
|
||||
m.selectedOption = 0
|
||||
@@ -297,8 +297,11 @@ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
|
||||
selected := m.approvalOptions[m.selectedOption]
|
||||
approved := strings.HasPrefix(selected, "Yes")
|
||||
needsFeedback := strings.Contains(selected, "feedback")
|
||||
noAskAgain := strings.Contains(selected, "don't ask again")
|
||||
|
||||
if needsFeedback {
|
||||
// Store the approval decision before switching to feedback input
|
||||
m.pendingApproval = approved
|
||||
// Switch to feedback input
|
||||
return m, func() tea.Msg {
|
||||
return ChangeInputTypeMsg{
|
||||
@@ -315,6 +318,7 @@ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
|
||||
InputType: InputTypeApproval,
|
||||
Approved: approved,
|
||||
NeedsFeedback: false,
|
||||
NoAskAgain: noAskAgain,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +328,7 @@ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
|
||||
return InputSubmitMsg{
|
||||
Value: value,
|
||||
InputType: InputTypeFeedback,
|
||||
Approved: m.pendingApproval, // Pass the stored approval decision
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,6 +444,7 @@ func (m *InputModel) Clone() *InputModel {
|
||||
lastHeight: m.lastHeight,
|
||||
approvalOptions: m.approvalOptions,
|
||||
selectedOption: m.selectedOption,
|
||||
pendingApproval: m.pendingApproval, // Preserve approval decision
|
||||
styles: m.styles,
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"github.com/cline/cli/pkg/cli/config"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
"github.com/cline/cli/pkg/cli/updater"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -24,6 +26,7 @@ type TaskOptions struct {
|
||||
Settings []string
|
||||
Yolo bool
|
||||
Address string
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func NewTaskCommand() *cobra.Command {
|
||||
@@ -391,7 +394,7 @@ func newTaskViewCommand() *cobra.Command {
|
||||
return taskManager.FollowConversation(ctx, taskManager.GetCurrentInstance(), false)
|
||||
} else if followComplete {
|
||||
// Follow until completion
|
||||
return taskManager.FollowConversationUntilCompletion(ctx)
|
||||
return taskManager.FollowConversationUntilCompletion(ctx, task.DefaultFollowOptions())
|
||||
} else {
|
||||
// Default: show snapshot
|
||||
return taskManager.ShowConversation(ctx)
|
||||
@@ -474,15 +477,34 @@ func newTaskOpenCommand() *cobra.Command {
|
||||
return fmt.Errorf("failed to parse settings: %w", err)
|
||||
}
|
||||
|
||||
// Create config manager to apply settings
|
||||
configManager, err := config.NewManager(ctx, taskManager.GetCurrentInstance())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config manager: %w", err)
|
||||
// Apply task-specific settings using UpdateTaskSettings RPC
|
||||
if parsedSettings != nil {
|
||||
_, err = taskManager.GetClient().State.UpdateTaskSettings(ctx, &cline.UpdateTaskSettingsRequest{
|
||||
Settings: parsedSettings,
|
||||
TaskId: &taskID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply task settings: %w", err)
|
||||
}
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("Task-specific settings applied successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the settings to the instance
|
||||
if err := configManager.UpdateSettings(ctx, parsedSettings, secrets); err != nil {
|
||||
return fmt.Errorf("failed to apply settings: %w", err)
|
||||
// Handle secrets separately if provided (they must go to global config)
|
||||
if secrets != nil {
|
||||
// Secrets are always global, not task-specific
|
||||
configManager, err := config.NewManager(ctx, taskManager.GetCurrentInstance())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config manager: %w", err)
|
||||
}
|
||||
|
||||
if err := configManager.UpdateSettings(ctx, nil, secrets); err != nil {
|
||||
return fmt.Errorf("failed to apply secrets: %w", err)
|
||||
}
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("Global secrets applied successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,17 +592,20 @@ func getContentFromStdinAndArgs(args []string) (string, error) {
|
||||
|
||||
// Check if data is being piped to stdin
|
||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||
stdinBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
|
||||
stdinContent := strings.TrimSpace(string(stdinBytes))
|
||||
if stdinContent != "" {
|
||||
if content.Len() > 0 {
|
||||
content.WriteString(" ")
|
||||
// Only try to read if there's actually data available
|
||||
if stat.Size() > 0 {
|
||||
stdinBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
|
||||
stdinContent := strings.TrimSpace(string(stdinBytes))
|
||||
if stdinContent != "" {
|
||||
if content.Len() > 0 {
|
||||
content.WriteString(" ")
|
||||
}
|
||||
content.WriteString(stdinContent)
|
||||
}
|
||||
content.WriteString(stdinContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,11 +662,17 @@ func CreateAndFollowTask(ctx context.Context, prompt string, opts TaskOptions) e
|
||||
fmt.Printf("Task created successfully with ID: %s\n\n", taskID)
|
||||
}
|
||||
|
||||
// Check for updates in background after task is created
|
||||
updater.CheckAndUpdate(opts.Verbose)
|
||||
|
||||
// If yolo mode is enabled, follow until completion (non-interactive)
|
||||
// Otherwise, follow in interactive mode
|
||||
if opts.Yolo {
|
||||
return taskManager.FollowConversationUntilCompletion(ctx)
|
||||
// Skip active task check since we just created the task
|
||||
return taskManager.FollowConversationUntilCompletion(ctx, task.FollowOptions{
|
||||
SkipActiveTaskCheck: true,
|
||||
})
|
||||
} else {
|
||||
return taskManager.FollowConversation(ctx, taskManager.GetCurrentInstance(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package task
|
||||
|
||||
// FollowOptions contains options for following a conversation
|
||||
type FollowOptions struct {
|
||||
// SkipActiveTaskCheck skips the check for an active task
|
||||
// This is useful when following a task that was just created to avoid race conditions
|
||||
SkipActiveTaskCheck bool
|
||||
}
|
||||
|
||||
// DefaultFollowOptions returns the default options for following a conversation
|
||||
func DefaultFollowOptions() FollowOptions {
|
||||
return FollowOptions{
|
||||
SkipActiveTaskCheck: false,
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/output"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
@@ -16,20 +18,21 @@ import (
|
||||
|
||||
// InputHandler manages interactive user input during follow mode
|
||||
type InputHandler struct {
|
||||
manager *Manager
|
||||
coordinator *StreamCoordinator
|
||||
cancelFunc context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
pollTicker *time.Ticker
|
||||
program *tea.Program
|
||||
programRunning bool
|
||||
programDoneChan chan struct{} // Signals when program actually exits
|
||||
resultChan chan output.InputSubmitMsg
|
||||
cancelChan chan struct{}
|
||||
feedbackApproval bool // Track if we're in feedback after approval
|
||||
feedbackApproved bool // Track the approval decision
|
||||
ctx context.Context // Context for restart callback
|
||||
manager *Manager
|
||||
coordinator *StreamCoordinator
|
||||
cancelFunc context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
pollTicker *time.Ticker
|
||||
program *tea.Program
|
||||
programRunning bool
|
||||
programDoneChan chan struct{} // Signals when program actually exits
|
||||
resultChan chan output.InputSubmitMsg
|
||||
cancelChan chan struct{}
|
||||
feedbackApproval bool // Track if we're in feedback after approval
|
||||
feedbackApproved bool // Track the approval decision
|
||||
approvalMessage *types.ClineMessage // Store the approval message for determining action
|
||||
ctx context.Context // Context for restart callback
|
||||
}
|
||||
|
||||
// NewInputHandler creates a new input handler
|
||||
@@ -162,6 +165,10 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
|
||||
// Check for mode switch commands first
|
||||
newMode, remainingMessage, isModeSwitch := ih.parseModeSwitch(message)
|
||||
if isModeSwitch {
|
||||
// Create styles for mode switch messages (respect global color profile)
|
||||
actStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
|
||||
planStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Bold(true)
|
||||
|
||||
if remainingMessage != "" {
|
||||
// Switching with a message - behavior differs by mode
|
||||
if newMode == "act" {
|
||||
@@ -170,16 +177,14 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
|
||||
output.Printf("\nError switching to act mode with message: %v\n", err)
|
||||
continue
|
||||
}
|
||||
// 256-color index 39 for act mode (matches lipgloss color "39" in input form)
|
||||
output.Printf("\n\033[38;5;39m\033[1mSwitched to act mode\033[0m\n")
|
||||
output.Printf("\n%s\n", actStyle.Render("Switched to act mode"))
|
||||
} else {
|
||||
// Plan mode: must switch first, then send message separately
|
||||
if err := ih.manager.SetMode(ctx, newMode, nil, nil, nil); err != nil {
|
||||
output.Printf("\nError switching to plan mode: %v\n", err)
|
||||
continue
|
||||
}
|
||||
// Yellow color for plan mode (ANSI color 3)
|
||||
output.Printf("\n\033[33m\033[1mSwitched to plan mode\033[0m\n")
|
||||
output.Printf("\n%s\n", planStyle.Render("Switched to plan mode"))
|
||||
|
||||
// Now send the message separately
|
||||
time.Sleep(500 * time.Millisecond) // Give mode switch time to process
|
||||
@@ -196,9 +201,9 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
|
||||
}
|
||||
// Color based on mode
|
||||
if newMode == "act" {
|
||||
output.Printf("\n\033[38;5;39m\033[1mSwitched to act mode\033[0m\n")
|
||||
output.Printf("\n%s\n", actStyle.Render("Switched to act mode"))
|
||||
} else {
|
||||
output.Printf("\n\033[33m\033[1mSwitched to plan mode\033[0m\n")
|
||||
output.Printf("\n%s\n", planStyle.Render("Switched to plan mode"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +234,48 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
|
||||
}
|
||||
}
|
||||
|
||||
// determineAutoApprovalAction determines which auto-approval action to enable based on the ask type
|
||||
func determineAutoApprovalAction(msg *types.ClineMessage) (string, error) {
|
||||
switch types.AskType(msg.Ask) {
|
||||
case types.AskTypeTool:
|
||||
// Parse tool message to determine if it's a read or edit operation
|
||||
var toolMsg types.ToolMessage
|
||||
if err := json.Unmarshal([]byte(msg.Text), &toolMsg); err != nil {
|
||||
return "", fmt.Errorf("failed to parse tool message: %w", err)
|
||||
}
|
||||
|
||||
// Determine action based on tool type
|
||||
switch types.ToolType(toolMsg.Tool) {
|
||||
case types.ToolTypeReadFile,
|
||||
types.ToolTypeListFilesTopLevel,
|
||||
types.ToolTypeListFilesRecursive,
|
||||
types.ToolTypeListCodeDefinitionNames,
|
||||
types.ToolTypeSearchFiles,
|
||||
types.ToolTypeWebFetch:
|
||||
return "read_files", nil
|
||||
case types.ToolTypeEditedExistingFile,
|
||||
types.ToolTypeNewFileCreated:
|
||||
return "edit_files", nil
|
||||
case types.ToolTypeFileDeleted:
|
||||
return "apply_patch", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported tool type: %s", toolMsg.Tool)
|
||||
}
|
||||
|
||||
case types.AskTypeCommand:
|
||||
return "execute_all_commands", nil
|
||||
|
||||
case types.AskTypeBrowserActionLaunch:
|
||||
return "use_browser", nil
|
||||
|
||||
case types.AskTypeUseMcpServer:
|
||||
return "use_mcp", nil
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported ask type: %s", msg.Ask)
|
||||
}
|
||||
}
|
||||
|
||||
// promptForInput displays an interactive prompt and waits for user input
|
||||
func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error) {
|
||||
currentMode := ih.manager.GetCurrentMode()
|
||||
@@ -245,6 +292,9 @@ func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error
|
||||
|
||||
// promptForApproval displays an approval prompt for tool/command requests
|
||||
func (ih *InputHandler) promptForApproval(ctx context.Context, msg *types.ClineMessage) (bool, string, error) {
|
||||
// Store the approval message for later use in determining auto-approval action
|
||||
ih.approvalMessage = msg
|
||||
|
||||
model := output.NewInputModel(
|
||||
output.InputTypeApproval,
|
||||
"Let Cline use this tool?",
|
||||
@@ -344,6 +394,23 @@ func (ih *InputHandler) runInputProgram(ctx context.Context, model output.InputM
|
||||
// Need to collect feedback - will be handled by model state change
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// Check if NoAskAgain was selected
|
||||
if result.NoAskAgain && result.Approved && ih.approvalMessage != nil {
|
||||
// Determine which auto-approval action to enable
|
||||
action, err := determineAutoApprovalAction(ih.approvalMessage)
|
||||
if err != nil {
|
||||
output.Printf("\nWarning: Could not determine auto-approval action: %v\n", err)
|
||||
} else {
|
||||
// Enable the auto-approval action
|
||||
if err := ih.manager.UpdateTaskAutoApprovalAction(ctx, action); err != nil {
|
||||
output.Printf("\nWarning: Could not update auto-approval: %v\n", err)
|
||||
} else {
|
||||
output.Printf("\nAuto-approval enabled for %s\n", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store approval state for when feedback comes back
|
||||
ih.feedbackApproval = false
|
||||
ih.feedbackApproved = result.Approved
|
||||
@@ -352,6 +419,7 @@ func (ih *InputHandler) runInputProgram(ctx context.Context, model output.InputM
|
||||
case output.InputTypeFeedback:
|
||||
// This came from approval flow
|
||||
ih.feedbackApproval = true
|
||||
ih.feedbackApproved = result.Approved // Use the approval decision from the feedback
|
||||
return result.Value, true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -280,9 +280,8 @@ func (m *Manager) CheckSendEnabled(ctx context.Context) error {
|
||||
|
||||
// Error types which we allow sending on
|
||||
errorTypes := []string{
|
||||
string(types.AskTypeAPIReqFailed), // "api_req_failed"
|
||||
string(types.AskTypeMistakeLimitReached), // "mistake_limit_reached"
|
||||
string(types.AskTypeAutoApprovalMaxReached), // "auto_approval_max_req_reached"
|
||||
string(types.AskTypeAPIReqFailed), // "api_req_failed"
|
||||
string(types.AskTypeMistakeLimitReached), // "mistake_limit_reached"
|
||||
}
|
||||
|
||||
isError := false
|
||||
@@ -754,7 +753,21 @@ func (m *Manager) FollowConversation(ctx context.Context, instanceAddress string
|
||||
}
|
||||
|
||||
// FollowConversationUntilCompletion streams conversation updates until task completion
|
||||
func (m *Manager) FollowConversationUntilCompletion(ctx context.Context) error {
|
||||
func (m *Manager) FollowConversationUntilCompletion(ctx context.Context, opts FollowOptions) error {
|
||||
// Check if there's an active task before entering follow mode
|
||||
// Skip this check if we just created a task (to avoid race condition where task isn't active yet)
|
||||
if !opts.SkipActiveTaskCheck {
|
||||
err := m.CheckSendEnabled(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoActiveTask) {
|
||||
fmt.Println("No task is currently running.")
|
||||
return nil
|
||||
}
|
||||
// For other errors (like task busy), we can still enter follow mode
|
||||
// as the user may want to observe the task
|
||||
}
|
||||
}
|
||||
|
||||
// Enable streaming mode
|
||||
m.mu.Lock()
|
||||
m.isStreamingMode = true
|
||||
@@ -1004,26 +1017,18 @@ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *Stre
|
||||
|
||||
case msg.Ask == string(types.AskTypePlanModeRespond):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
// In streaming mode, partial stream handles this message
|
||||
// State stream should skip to avoid duplication
|
||||
if m.isStreamingMode {
|
||||
// Skip - partial stream already handled this
|
||||
} else {
|
||||
// Non-streaming mode: render normally when message is complete
|
||||
if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
m.displayMessage(msg, false, false, i)
|
||||
// Non-streaming mode: render normally when message is complete
|
||||
if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
m.displayMessage(msg, false, false, i)
|
||||
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Type == types.MessageTypeAsk:
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
// Only render if not already handled by partial stream
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
fmt.Println()
|
||||
if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
m.displayMessage(msg, false, false, i)
|
||||
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
}
|
||||
@@ -1245,10 +1250,48 @@ func (m *Manager) updateMode(stateJson string) {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// UpdateTaskAutoApprovalAction enables a specific auto-approval action for the current task
|
||||
func (m *Manager) UpdateTaskAutoApprovalAction(ctx context.Context, actionKey string) error {
|
||||
boolPtr := func(b bool) *bool { return &b }
|
||||
|
||||
settings := &cline.Settings{
|
||||
AutoApprovalSettings: &cline.AutoApprovalSettings{
|
||||
Actions: &cline.AutoApprovalActions{},
|
||||
},
|
||||
}
|
||||
|
||||
// Set the specific action to true based on actionKey
|
||||
truePtr := boolPtr(true)
|
||||
|
||||
switch actionKey {
|
||||
case "read_files":
|
||||
settings.AutoApprovalSettings.Actions.ReadFiles = truePtr
|
||||
case "edit_files":
|
||||
settings.AutoApprovalSettings.Actions.EditFiles = truePtr
|
||||
case "execute_all_commands":
|
||||
settings.AutoApprovalSettings.Actions.ExecuteAllCommands = truePtr
|
||||
case "use_browser":
|
||||
settings.AutoApprovalSettings.Actions.UseBrowser = truePtr
|
||||
case "use_mcp":
|
||||
settings.AutoApprovalSettings.Actions.UseMcp = truePtr
|
||||
default:
|
||||
return fmt.Errorf("unknown auto-approval action: %s", actionKey)
|
||||
}
|
||||
|
||||
_, err := m.client.State.UpdateTaskSettings(ctx, &cline.UpdateTaskSettingsRequest{
|
||||
Settings: settings,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update task settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup cleans up resources
|
||||
func (m *Manager) Cleanup() {
|
||||
// Clean up streaming display resources if needed
|
||||
if m.streamingDisplay != nil {
|
||||
m.streamingDisplay.Cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,8 +180,6 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
|
||||
settings.PlanModeHuaweiCloudMaasModelId = strPtr(value)
|
||||
case "plan_mode_oca_model_id":
|
||||
settings.PlanModeOcaModelId = strPtr(value)
|
||||
case "plan_mode_vercel_ai_gateway_model_id":
|
||||
settings.PlanModeVercelAiGatewayModelId = strPtr(value)
|
||||
case "act_mode_api_model_id":
|
||||
settings.ActModeApiModelId = strPtr(value)
|
||||
case "act_mode_reasoning_effort":
|
||||
@@ -218,8 +216,6 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
|
||||
settings.ActModeHuaweiCloudMaasModelId = strPtr(value)
|
||||
case "act_mode_oca_model_id":
|
||||
settings.ActModeOcaModelId = strPtr(value)
|
||||
case "act_mode_vercel_ai_gateway_model_id":
|
||||
settings.ActModeVercelAiGatewayModelId = strPtr(value)
|
||||
|
||||
// Boolean fields
|
||||
case "aws_use_cross_region_inference":
|
||||
@@ -416,24 +412,12 @@ func setNestedField(settings *cline.Settings, parentField string, childFields ma
|
||||
func setAutoApprovalSettings(settings *cline.AutoApprovalSettings, fields map[string]string) error {
|
||||
for key, value := range fields {
|
||||
switch key {
|
||||
case "enabled":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.Enabled = val
|
||||
case "max_requests":
|
||||
val, err := parseInt32(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.MaxRequests = val
|
||||
case "enable_notifications":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.EnableNotifications = val
|
||||
settings.EnableNotifications = boolPtr(val)
|
||||
case "actions":
|
||||
return fmt.Errorf("auto_approval_settings.actions requires nested dot notation (e.g., auto-approval-settings.actions.read-files=true)")
|
||||
default:
|
||||
@@ -464,21 +448,21 @@ func setAutoApprovalAction(actions *cline.AutoApprovalActions, key, value string
|
||||
|
||||
switch key {
|
||||
case "read_files":
|
||||
actions.ReadFiles = val
|
||||
actions.ReadFiles = boolPtr(val)
|
||||
case "read_files_externally":
|
||||
actions.ReadFilesExternally = val
|
||||
actions.ReadFilesExternally = boolPtr(val)
|
||||
case "edit_files":
|
||||
actions.EditFiles = val
|
||||
actions.EditFiles = boolPtr(val)
|
||||
case "edit_files_externally":
|
||||
actions.EditFilesExternally = val
|
||||
actions.EditFilesExternally = boolPtr(val)
|
||||
case "execute_safe_commands":
|
||||
actions.ExecuteSafeCommands = val
|
||||
actions.ExecuteSafeCommands = boolPtr(val)
|
||||
case "execute_all_commands":
|
||||
actions.ExecuteAllCommands = val
|
||||
actions.ExecuteAllCommands = boolPtr(val)
|
||||
case "use_browser":
|
||||
actions.UseBrowser = val
|
||||
actions.UseBrowser = boolPtr(val)
|
||||
case "use_mcp":
|
||||
actions.UseMcp = val
|
||||
actions.UseMcp = boolPtr(val)
|
||||
default:
|
||||
return fmt.Errorf("unsupported auto_approval_actions field '%s'", key)
|
||||
}
|
||||
@@ -672,6 +656,8 @@ func parseApiProvider(value string) (cline.ApiProvider, error) {
|
||||
return cline.ApiProvider_DIFY, nil
|
||||
case "oca":
|
||||
return cline.ApiProvider_OCA, nil
|
||||
case "minimax":
|
||||
return cline.ApiProvider_MINIMAX, nil
|
||||
default:
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("invalid api_provider '%s'", value)
|
||||
}
|
||||
@@ -746,14 +732,14 @@ func setSecretField(secrets *cline.Secrets, key, value string) error {
|
||||
secrets.HuaweiCloudMaasApiKey = strPtr(value)
|
||||
case "baseten_api_key":
|
||||
secrets.BasetenApiKey = strPtr(value)
|
||||
case "vercel_ai_gateway_api_key":
|
||||
secrets.VercelAiGatewayApiKey = strPtr(value)
|
||||
case "dify_api_key":
|
||||
secrets.DifyApiKey = strPtr(value)
|
||||
case "oca_api_key":
|
||||
secrets.OcaApiKey = strPtr(value)
|
||||
case "oca_refresh_token":
|
||||
secrets.OcaRefreshToken = strPtr(value)
|
||||
case "hicap_api_key":
|
||||
secrets.HicapApiKey = strPtr(value)
|
||||
default:
|
||||
return fmt.Errorf("unsupported secret field '%s'", key)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,695 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/display"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
)
|
||||
|
||||
// KeyboardProtocol manages enhanced keyboard protocol support for detecting
|
||||
// modified keys like shift+enter across all major terminals.
|
||||
type KeyboardProtocol struct {
|
||||
enabled bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var globalProtocol = &KeyboardProtocol{}
|
||||
|
||||
// EnableEnhancedKeyboard enables enhanced keyboard protocols to support
|
||||
// shift+enter and other modified keys across all major terminals:
|
||||
// - VS Code integrated terminal
|
||||
// - iTerm2
|
||||
// - Terminal.app
|
||||
// - Ghostty
|
||||
// - Kitty
|
||||
// - WezTerm
|
||||
// - Alacritty
|
||||
// - foot
|
||||
// - xterm
|
||||
//
|
||||
// This function is safe to call multiple times and handles cleanup automatically.
|
||||
// It enables both modifyOtherKeys (xterm protocol) and Kitty keyboard protocol
|
||||
// for maximum compatibility.
|
||||
func EnableEnhancedKeyboard() {
|
||||
globalProtocol.mu.Lock()
|
||||
defer globalProtocol.mu.Unlock()
|
||||
|
||||
if globalProtocol.enabled {
|
||||
return // Already enabled
|
||||
}
|
||||
|
||||
// Check if we're in a TTY (not piped/redirected)
|
||||
if !isatty(os.Stdin.Fd()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Enable modifyOtherKeys mode 2
|
||||
// This tells xterm-compatible terminals (VS Code, iTerm2, Terminal.app, etc.)
|
||||
// to send escape sequences for modified keys including shift+enter
|
||||
// Format: CSI > 4 ; 2 m
|
||||
// - Mode 2 enables for ALL keys including well-known ones
|
||||
fmt.Print("\x1b[>4;2m")
|
||||
|
||||
// Also enable Kitty keyboard protocol for terminals that support it
|
||||
// This is a more modern protocol supported by Kitty, Ghostty, WezTerm, foot, etc.
|
||||
// Format: CSI = <flags> u where flags=1 means "disambiguate escape codes"
|
||||
// This makes shift+enter distinguishable from plain enter
|
||||
fmt.Print("\x1b[=1u")
|
||||
|
||||
globalProtocol.enabled = true
|
||||
}
|
||||
|
||||
// DisableEnhancedKeyboard restores the terminal to its default keyboard mode.
|
||||
// This should be called on program exit to be a good citizen.
|
||||
func DisableEnhancedKeyboard() {
|
||||
globalProtocol.mu.Lock()
|
||||
defer globalProtocol.mu.Unlock()
|
||||
|
||||
if !globalProtocol.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
// Disable modifyOtherKeys (restore to mode 0)
|
||||
fmt.Print("\x1b[>4;0m")
|
||||
|
||||
// Disable Kitty keyboard protocol
|
||||
fmt.Print("\x1b[<u")
|
||||
|
||||
globalProtocol.enabled = false
|
||||
}
|
||||
|
||||
// isatty checks if a file descriptor is a terminal
|
||||
func isatty(fd uintptr) bool {
|
||||
// Use the standard library's terminal package
|
||||
// This works across all platforms (Unix, Windows, etc.)
|
||||
fileInfo, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (fileInfo.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
// SetupKeyboard detects the current terminal and configures keybindings if needed.
|
||||
// Runs in background and doesn't block. Prints status when configs are modified.
|
||||
func SetupKeyboard() {
|
||||
go func() {
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
setupKeyboardInternal(renderer)
|
||||
}()
|
||||
}
|
||||
|
||||
// SetupKeyboardSync is the synchronous version used by doctor command.
|
||||
// Blocks until complete and prints status for all terminals.
|
||||
func SetupKeyboardSync() {
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
setupKeyboardInternal(renderer)
|
||||
}
|
||||
|
||||
func setupKeyboardInternal(renderer *display.Renderer) {
|
||||
terminalName := DetectTerminal()
|
||||
|
||||
switch terminalName {
|
||||
case "vscode":
|
||||
// VS Code and Cursor use the same TERM_PROGRAM value
|
||||
modified, path := SetupVSCodeKeybindings()
|
||||
if modified {
|
||||
fmt.Printf("%s VS Code %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
} else if path != "" {
|
||||
fmt.Printf("%s\n", renderer.Dim("✓ VS Code shift+enter already configured"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
}
|
||||
|
||||
modified, path = SetupCursorKeybindings()
|
||||
if modified {
|
||||
fmt.Printf("%s Cursor %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
} else if path != "" {
|
||||
fmt.Printf("%s\n", renderer.Dim("✓ Cursor shift+enter already configured"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
}
|
||||
|
||||
case "ghostty":
|
||||
modified, path := SetupGhosttyKeybindings()
|
||||
if modified {
|
||||
fmt.Printf("%s Ghostty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
fmt.Printf("%s\n", renderer.Dim(" Fully restart Ghostty (quit all windows) for changes to take effect"))
|
||||
} else if path != "" {
|
||||
fmt.Printf("%s\n", renderer.Dim("✓ Ghostty shift+enter already configured"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
}
|
||||
|
||||
case "wezterm":
|
||||
modified, path := SetupWezTermKeybindings()
|
||||
if modified {
|
||||
fmt.Printf("%s WezTerm %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
} else if path != "" {
|
||||
fmt.Printf("%s\n", renderer.Dim("✓ WezTerm shift+enter already configured"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
}
|
||||
|
||||
case "alacritty":
|
||||
modified, path := SetupAlacrittyKeybindings()
|
||||
if modified {
|
||||
fmt.Printf("%s Alacritty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
} else if path != "" {
|
||||
fmt.Printf("%s\n", renderer.Dim("✓ Alacritty shift+enter already configured"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
}
|
||||
|
||||
case "kitty":
|
||||
modified, path := SetupKittyKeybindings()
|
||||
if modified {
|
||||
fmt.Printf("%s Kitty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
} else if path != "" {
|
||||
fmt.Printf("%s\n", renderer.Dim("✓ Kitty shift+enter already configured"))
|
||||
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
||||
}
|
||||
|
||||
case "iterm2":
|
||||
fmt.Printf("%s\n", renderer.Dim("✓ iTerm2 shift+enter works by default (maps to alt+enter)"))
|
||||
|
||||
case "terminal.app":
|
||||
fmt.Printf("%s\n", renderer.Dim("⚠ Terminal.app requires manual configuration"))
|
||||
fmt.Printf("%s\n", renderer.Dim(" See: Terminal → Preferences → Profiles → Keyboard"))
|
||||
|
||||
case "unknown":
|
||||
fmt.Printf("%s\n", renderer.Dim("ℹ Terminal not detected - use alt+enter or ctrl+j for newlines"))
|
||||
}
|
||||
}
|
||||
|
||||
// getVSCodeConfigPath returns the platform-specific path to VS Code's User directory
|
||||
func getVSCodeConfigPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return filepath.Join(home, "Library", "Application Support", "Code", "User"), nil
|
||||
case "windows":
|
||||
appData := os.Getenv("APPDATA")
|
||||
if appData == "" {
|
||||
appData = filepath.Join(home, "AppData", "Roaming")
|
||||
}
|
||||
return filepath.Join(appData, "Code", "User"), nil
|
||||
default: // linux, freebsd, etc.
|
||||
return filepath.Join(home, ".config", "Code", "User"), nil
|
||||
}
|
||||
}
|
||||
|
||||
// getCursorConfigPath returns the platform-specific path to Cursor's User directory
|
||||
func getCursorConfigPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return filepath.Join(home, "Library", "Application Support", "Cursor", "User"), nil
|
||||
case "windows":
|
||||
appData := os.Getenv("APPDATA")
|
||||
if appData == "" {
|
||||
appData = filepath.Join(home, "AppData", "Roaming")
|
||||
}
|
||||
return filepath.Join(appData, "Cursor", "User"), nil
|
||||
default: // linux, freebsd, etc.
|
||||
return filepath.Join(home, ".config", "Cursor", "User"), nil
|
||||
}
|
||||
}
|
||||
|
||||
// DetectTerminal identifies which terminal emulator is currently running
|
||||
func DetectTerminal() string {
|
||||
// Check TERM_PROGRAM (works for most terminals)
|
||||
termProgram := os.Getenv("TERM_PROGRAM")
|
||||
switch termProgram {
|
||||
case "vscode":
|
||||
return "vscode" // Also covers Cursor (uses same value)
|
||||
case "WezTerm":
|
||||
return "wezterm"
|
||||
case "ghostty":
|
||||
return "ghostty"
|
||||
case "iTerm.app":
|
||||
return "iterm2"
|
||||
case "Apple_Terminal":
|
||||
return "terminal.app"
|
||||
}
|
||||
|
||||
// Kitty doesn't set TERM_PROGRAM, check KITTY_WINDOW_ID
|
||||
if os.Getenv("KITTY_WINDOW_ID") != "" {
|
||||
return "kitty"
|
||||
}
|
||||
|
||||
// Alacritty doesn't set TERM_PROGRAM, check ALACRITTY_SOCKET
|
||||
if os.Getenv("ALACRITTY_SOCKET") != "" {
|
||||
return "alacritty"
|
||||
}
|
||||
|
||||
// Ghostty fallback (cross-platform - more reliable than TERM_PROGRAM)
|
||||
if os.Getenv("GHOSTTY_RESOURCES_DIR") != "" {
|
||||
return "ghostty"
|
||||
}
|
||||
|
||||
// Alacritty fallback
|
||||
if os.Getenv("ALACRITTY_LOG") != "" {
|
||||
return "alacritty"
|
||||
}
|
||||
|
||||
// Check TERM variable as last resort
|
||||
term := os.Getenv("TERM")
|
||||
if strings.Contains(term, "kitty") {
|
||||
return "kitty"
|
||||
}
|
||||
if term == "alacritty" {
|
||||
return "alacritty"
|
||||
}
|
||||
if term == "xterm-ghostty" {
|
||||
return "ghostty"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// VSCodeKeybinding represents a VS Code keyboard shortcut
|
||||
type VSCodeKeybinding struct {
|
||||
Key string `json:"key"`
|
||||
Command string `json:"command"`
|
||||
Args map[string]interface{} `json:"args,omitempty"`
|
||||
When string `json:"when,omitempty"`
|
||||
}
|
||||
|
||||
// SetupVSCodeKeybindings adds shift+enter support to VS Code's integrated terminal
|
||||
// by modifying the user's keybindings.json file.
|
||||
// Returns (wasModified, configPath) to allow caller to log the change.
|
||||
func SetupVSCodeKeybindings() (bool, string) {
|
||||
// Get platform-specific VS Code config path
|
||||
configDir, err := getVSCodeConfigPath()
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
keybindingsPath := filepath.Join(configDir, "keybindings.json")
|
||||
|
||||
// Check if VS Code is installed (keybindings file or parent dir exists)
|
||||
if _, err := os.Stat(filepath.Dir(keybindingsPath)); os.IsNotExist(err) {
|
||||
// VS Code not installed, skip silently
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Read existing keybindings
|
||||
var keybindings []VSCodeKeybinding
|
||||
|
||||
data, err := os.ReadFile(keybindingsPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return false, ""
|
||||
}
|
||||
// File doesn't exist, start with empty array
|
||||
keybindings = []VSCodeKeybinding{}
|
||||
} else {
|
||||
// Parse existing keybindings
|
||||
if err := json.Unmarshal(data, &keybindings); err != nil {
|
||||
// If parse fails, don't modify the file
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
// Check if shift+enter binding already exists
|
||||
for _, kb := range keybindings {
|
||||
if kb.Key == "shift+enter" && kb.Command == "workbench.action.terminal.sendSequence" {
|
||||
// Already configured
|
||||
return false, keybindingsPath
|
||||
}
|
||||
}
|
||||
|
||||
// Add shift+enter keybinding
|
||||
newBinding := VSCodeKeybinding{
|
||||
Key: "shift+enter",
|
||||
Command: "workbench.action.terminal.sendSequence",
|
||||
Args: map[string]interface{}{
|
||||
"text": "\u001b\n", // ESC + newline (alt+enter sequence)
|
||||
},
|
||||
When: "terminalFocus",
|
||||
}
|
||||
|
||||
keybindings = append(keybindings, newBinding)
|
||||
|
||||
// Create backup
|
||||
if data != nil {
|
||||
backupPath := keybindingsPath + ".backup"
|
||||
_ = os.WriteFile(backupPath, data, 0644)
|
||||
}
|
||||
|
||||
// Write updated keybindings
|
||||
updatedData, err := json.MarshalIndent(keybindings, "", " ")
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(keybindingsPath), 0755); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if err := os.WriteFile(keybindingsPath, updatedData, 0644); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, keybindingsPath
|
||||
}
|
||||
|
||||
// SetupCursorKeybindings adds shift+enter support to Cursor's integrated terminal
|
||||
// by modifying the user's keybindings.json file.
|
||||
// Cursor is a fork of VS Code, so it uses the same keybinding format.
|
||||
// Returns (wasModified, configPath) to allow caller to log the change.
|
||||
func SetupCursorKeybindings() (bool, string) {
|
||||
// Get platform-specific Cursor config path
|
||||
configDir, err := getCursorConfigPath()
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
keybindingsPath := filepath.Join(configDir, "keybindings.json")
|
||||
|
||||
// Check if Cursor is installed (keybindings file or parent dir exists)
|
||||
if _, err := os.Stat(filepath.Dir(keybindingsPath)); os.IsNotExist(err) {
|
||||
// Cursor not installed, skip silently
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Read existing keybindings
|
||||
var keybindings []VSCodeKeybinding
|
||||
|
||||
data, err := os.ReadFile(keybindingsPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return false, ""
|
||||
}
|
||||
// File doesn't exist, start with empty array
|
||||
keybindings = []VSCodeKeybinding{}
|
||||
} else {
|
||||
// Parse existing keybindings
|
||||
if err := json.Unmarshal(data, &keybindings); err != nil {
|
||||
// If parse fails, don't modify the file
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
// Check if shift+enter binding already exists
|
||||
for _, kb := range keybindings {
|
||||
if kb.Key == "shift+enter" && kb.Command == "workbench.action.terminal.sendSequence" {
|
||||
// Already configured
|
||||
return false, keybindingsPath
|
||||
}
|
||||
}
|
||||
|
||||
// Add shift+enter keybinding
|
||||
newBinding := VSCodeKeybinding{
|
||||
Key: "shift+enter",
|
||||
Command: "workbench.action.terminal.sendSequence",
|
||||
Args: map[string]interface{}{
|
||||
"text": "\u001b\n", // ESC + newline (alt+enter sequence)
|
||||
},
|
||||
When: "terminalFocus",
|
||||
}
|
||||
|
||||
keybindings = append(keybindings, newBinding)
|
||||
|
||||
// Create backup
|
||||
if data != nil {
|
||||
backupPath := keybindingsPath + ".backup"
|
||||
_ = os.WriteFile(backupPath, data, 0644)
|
||||
}
|
||||
|
||||
// Write updated keybindings
|
||||
updatedData, err := json.MarshalIndent(keybindings, "", " ")
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(keybindingsPath), 0755); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if err := os.WriteFile(keybindingsPath, updatedData, 0644); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, keybindingsPath
|
||||
}
|
||||
|
||||
// SetupGhosttyKeybindings adds shift+enter support to Ghostty terminal
|
||||
// by appending to the user's config file.
|
||||
// Returns (wasModified, configPath) to allow caller to log the change.
|
||||
func SetupGhosttyKeybindings() (bool, string) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Ghostty config location: ~/.config/ghostty/config
|
||||
configPath := filepath.Join(home, ".config", "ghostty", "config")
|
||||
|
||||
// Check if config directory exists
|
||||
configDir := filepath.Dir(configPath)
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
// Ghostty not installed, skip silently
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Read existing config if it exists
|
||||
var existingContent []byte
|
||||
if data, err := os.ReadFile(configPath); err == nil {
|
||||
existingContent = data
|
||||
// Check if shift+enter already configured
|
||||
if strings.Contains(string(data), "keybind = shift+enter") {
|
||||
return false, configPath
|
||||
}
|
||||
}
|
||||
|
||||
// Keybinding to add - send newline character (0x0a)
|
||||
// Ghostty requires \x0a hex escape syntax, verified working
|
||||
keybinding := "keybind = shift+enter=text:\\x0a\n"
|
||||
|
||||
// Append to config
|
||||
newContent := append(existingContent, []byte(keybinding)...)
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Create backup if file exists
|
||||
if existingContent != nil {
|
||||
backupPath := configPath + ".backup"
|
||||
_ = os.WriteFile(backupPath, existingContent, 0644)
|
||||
}
|
||||
|
||||
// Write updated config
|
||||
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, configPath
|
||||
}
|
||||
|
||||
// SetupWezTermKeybindings adds shift+enter support to WezTerm
|
||||
// by appending to the user's .wezterm.lua file.
|
||||
// Returns (wasModified, configPath)
|
||||
func SetupWezTermKeybindings() (bool, string) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".wezterm.lua")
|
||||
|
||||
// Check if WezTerm config exists
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
// WezTerm not configured, skip silently
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Read existing config
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Check if shift+enter already configured
|
||||
if strings.Contains(string(data), "key = 'Enter'") && strings.Contains(string(data), "mods = 'SHIFT'") {
|
||||
return false, configPath
|
||||
}
|
||||
|
||||
// Create backup
|
||||
backupPath := configPath + ".backup"
|
||||
_ = os.WriteFile(backupPath, data, 0644)
|
||||
|
||||
// Keybinding to add (insert before final return statement)
|
||||
keybinding := `
|
||||
-- Shift+Enter for newlines (added by Cline CLI)
|
||||
config.keys = config.keys or {}
|
||||
table.insert(config.keys, {
|
||||
key = 'Enter',
|
||||
mods = 'SHIFT',
|
||||
action = wezterm.action.SendString '\x1b\n',
|
||||
})
|
||||
`
|
||||
|
||||
content := string(data)
|
||||
// Try to insert before the final return statement
|
||||
if strings.Contains(content, "return config") {
|
||||
content = strings.Replace(content, "return config", keybinding+"\nreturn config", 1)
|
||||
} else {
|
||||
// No return statement, append at end
|
||||
content += keybinding
|
||||
}
|
||||
|
||||
// Write updated config
|
||||
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, configPath
|
||||
}
|
||||
|
||||
// SetupAlacrittyKeybindings adds shift+enter support to Alacritty
|
||||
// by appending to the user's alacritty.yml file.
|
||||
// Returns (wasModified, configPath)
|
||||
func SetupAlacrittyKeybindings() (bool, string) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Try both possible locations
|
||||
configPaths := []string{
|
||||
filepath.Join(home, ".config", "alacritty", "alacritty.yml"),
|
||||
filepath.Join(home, ".config", "alacritty", "alacritty.toml"),
|
||||
filepath.Join(home, ".alacritty.yml"),
|
||||
}
|
||||
|
||||
var configPath string
|
||||
for _, path := range configPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
configPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
// Alacritty not configured, skip silently
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Read existing config
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Check if shift+enter already configured
|
||||
if strings.Contains(string(data), "key: Return") && strings.Contains(string(data), "mods: Shift") {
|
||||
return false, configPath
|
||||
}
|
||||
|
||||
// Create backup
|
||||
backupPath := configPath + ".backup"
|
||||
_ = os.WriteFile(backupPath, data, 0644)
|
||||
|
||||
// Keybinding to add
|
||||
var keybinding string
|
||||
if strings.HasSuffix(configPath, ".yml") || strings.HasSuffix(configPath, ".yaml") {
|
||||
keybinding = `
|
||||
# Shift+Enter for newlines (added by Cline CLI)
|
||||
key_bindings:
|
||||
- { key: Return, mods: Shift, chars: "\x1b\n" }
|
||||
`
|
||||
} else {
|
||||
// TOML format
|
||||
keybinding = `
|
||||
# Shift+Enter for newlines (added by Cline CLI)
|
||||
[[keyboard.bindings]]
|
||||
key = "Return"
|
||||
mods = "Shift"
|
||||
chars = "\x1b\n"
|
||||
`
|
||||
}
|
||||
|
||||
// Append to config
|
||||
newContent := append(data, []byte(keybinding)...)
|
||||
|
||||
// Write updated config
|
||||
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, configPath
|
||||
}
|
||||
|
||||
// SetupKittyKeybindings adds shift+enter support to Kitty terminal
|
||||
// by appending to the user's kitty.conf file.
|
||||
// Returns (wasModified, configPath)
|
||||
func SetupKittyKeybindings() (bool, string) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".config", "kitty", "kitty.conf")
|
||||
|
||||
// Check if config directory exists
|
||||
configDir := filepath.Dir(configPath)
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
// Kitty not installed, skip silently
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Read existing config if it exists
|
||||
var existingContent []byte
|
||||
if data, err := os.ReadFile(configPath); err == nil {
|
||||
existingContent = data
|
||||
// Check if shift+enter already configured
|
||||
if strings.Contains(string(data), "map shift+enter") {
|
||||
return false, configPath
|
||||
}
|
||||
}
|
||||
|
||||
// Keybinding to add
|
||||
keybinding := "# Shift+Enter for newlines (added by Cline CLI)\nmap shift+enter send_text all \\x1b\\n\n"
|
||||
|
||||
// Append to config
|
||||
newContent := append(existingContent, []byte(keybinding)...)
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Create backup if file exists
|
||||
if existingContent != nil {
|
||||
backupPath := configPath + ".backup"
|
||||
_ = os.WriteFile(backupPath, existingContent, 0644)
|
||||
}
|
||||
|
||||
// Write updated config
|
||||
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, configPath
|
||||
}
|
||||
@@ -37,17 +37,16 @@ const (
|
||||
type AskType string
|
||||
|
||||
const (
|
||||
AskTypeFollowup AskType = "followup"
|
||||
AskTypePlanModeRespond AskType = "plan_mode_respond"
|
||||
AskTypeCommand AskType = "command"
|
||||
AskTypeCommandOutput AskType = "command_output"
|
||||
AskTypeCompletionResult AskType = "completion_result"
|
||||
AskTypeTool AskType = "tool"
|
||||
AskTypeAPIReqFailed AskType = "api_req_failed"
|
||||
AskTypeResumeTask AskType = "resume_task"
|
||||
AskTypeResumeCompletedTask AskType = "resume_completed_task"
|
||||
AskTypeMistakeLimitReached AskType = "mistake_limit_reached"
|
||||
AskTypeAutoApprovalMaxReached AskType = "auto_approval_max_req_reached"
|
||||
AskTypeFollowup AskType = "followup"
|
||||
AskTypePlanModeRespond AskType = "plan_mode_respond"
|
||||
AskTypeCommand AskType = "command"
|
||||
AskTypeCommandOutput AskType = "command_output"
|
||||
AskTypeCompletionResult AskType = "completion_result"
|
||||
AskTypeTool AskType = "tool"
|
||||
AskTypeAPIReqFailed AskType = "api_req_failed"
|
||||
AskTypeResumeTask AskType = "resume_task"
|
||||
AskTypeResumeCompletedTask AskType = "resume_completed_task"
|
||||
AskTypeMistakeLimitReached AskType = "mistake_limit_reached"
|
||||
AskTypeBrowserActionLaunch AskType = "browser_action_launch"
|
||||
AskTypeUseMcpServer AskType = "use_mcp_server"
|
||||
AskTypeNewTask AskType = "new_task"
|
||||
@@ -108,6 +107,7 @@ const (
|
||||
ToolTypeEditedExistingFile ToolType = "editedExistingFile"
|
||||
ToolTypeNewFileCreated ToolType = "newFileCreated"
|
||||
ToolTypeReadFile ToolType = "readFile"
|
||||
ToolTypeFileDeleted ToolType = "fileDeleted"
|
||||
ToolTypeListFilesTopLevel ToolType = "listFilesTopLevel"
|
||||
ToolTypeListFilesRecursive ToolType = "listFilesRecursive"
|
||||
ToolTypeListCodeDefinitionNames ToolType = "listCodeDefinitionNames"
|
||||
@@ -247,8 +247,6 @@ func convertProtoAskType(askType cline.ClineAsk) string {
|
||||
return string(AskTypeResumeCompletedTask)
|
||||
case cline.ClineAsk_MISTAKE_LIMIT_REACHED:
|
||||
return string(AskTypeMistakeLimitReached)
|
||||
case cline.ClineAsk_AUTO_APPROVAL_MAX_REQ_REACHED:
|
||||
return string(AskTypeAutoApprovalMaxReached)
|
||||
case cline.ClineAsk_BROWSER_ACTION_LAUNCH:
|
||||
return string(AskTypeBrowserActionLaunch)
|
||||
case cline.ClineAsk_USE_MCP_SERVER:
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/output"
|
||||
)
|
||||
|
||||
type cacheData struct {
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
LatestVersion string `json:"latest_version"`
|
||||
}
|
||||
|
||||
type npmRegistryResponse struct {
|
||||
DistTags struct {
|
||||
Latest string `json:"latest"`
|
||||
Nightly string `json:"nightly"`
|
||||
} `json:"dist-tags"`
|
||||
}
|
||||
|
||||
const (
|
||||
checkInterval = 24 * time.Hour
|
||||
requestTimeout = 3 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true)
|
||||
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true)
|
||||
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
)
|
||||
|
||||
var verbose bool
|
||||
|
||||
// CheckAndUpdate performs a background update check and attempts to auto-update if needed.
|
||||
// This is non-blocking and safe to call on CLI startup.
|
||||
func CheckAndUpdate(isVerbose bool) {
|
||||
verbose = isVerbose
|
||||
|
||||
// Skip in CI environments
|
||||
if os.Getenv("CI") != "" {
|
||||
if verbose {
|
||||
output.Printf("[updater] Skipping update check (CI environment)\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if user disabled auto-updates
|
||||
if os.Getenv("NO_AUTO_UPDATE") != "" {
|
||||
if verbose {
|
||||
output.Printf("[updater] Skipping update check (NO_AUTO_UPDATE set)\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if verbose {
|
||||
output.Printf("[updater] Starting background update check...\n")
|
||||
}
|
||||
|
||||
// Run in background so we don't block CLI startup
|
||||
go func() {
|
||||
if err := checkAndUpdateInternal(false); err != nil {
|
||||
if verbose {
|
||||
output.Printf("[updater] Update check failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// CheckAndUpdateSync performs a synchronous update check (blocks until complete).
|
||||
// If bypassCache is true, ignores the 24-hour cache and always checks npm registry.
|
||||
// This is used by the doctor command.
|
||||
func CheckAndUpdateSync(isVerbose bool, bypassCache bool) {
|
||||
verbose = isVerbose
|
||||
|
||||
// Skip in CI environments
|
||||
if os.Getenv("CI") != "" {
|
||||
if verbose {
|
||||
output.Printf("[updater] Skipping update check (CI environment)\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if user disabled auto-updates
|
||||
if os.Getenv("NO_AUTO_UPDATE") != "" {
|
||||
if verbose {
|
||||
output.Printf("[updater] Skipping update check (NO_AUTO_UPDATE set)\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if verbose {
|
||||
output.Printf("[updater] Starting update check...\n")
|
||||
}
|
||||
|
||||
// Run synchronously
|
||||
if err := checkAndUpdateInternal(bypassCache); err != nil {
|
||||
if verbose {
|
||||
output.Printf("[updater] Update check failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkAndUpdateInternal(bypassCache bool) error {
|
||||
if verbose {
|
||||
output.Printf("[updater] Loading update cache...\n")
|
||||
}
|
||||
|
||||
// Load cache
|
||||
cache, err := loadCache()
|
||||
if !bypassCache && err == nil && time.Since(cache.LastCheck) < checkInterval {
|
||||
// Checked recently, skip (unless cache is bypassed)
|
||||
if verbose {
|
||||
output.Printf("[updater] Cache is fresh (last checked %v ago), skipping\n", time.Since(cache.LastCheck))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil && verbose {
|
||||
output.Printf("[updater] Cache load failed or doesn't exist: %v\n", err)
|
||||
}
|
||||
|
||||
// Determine channel
|
||||
distTag := "latest"
|
||||
if strings.Contains(global.CliVersion, "nightly") {
|
||||
distTag = "nightly"
|
||||
}
|
||||
|
||||
if verbose {
|
||||
output.Printf("[updater] Current version: %s (channel: %s)\n", global.CliVersion, distTag)
|
||||
output.Printf("[updater] Fetching latest version from npm registry...\n")
|
||||
}
|
||||
|
||||
// Fetch latest version from npm
|
||||
latestVersion, err := fetchLatestVersion()
|
||||
if err != nil {
|
||||
if verbose {
|
||||
output.Printf("[updater] Failed to fetch latest version: %v\n", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
output.Printf("[updater] Latest version on npm: %s\n", latestVersion)
|
||||
}
|
||||
|
||||
// Update cache
|
||||
cache = cacheData{
|
||||
LastCheck: time.Now(),
|
||||
LatestVersion: latestVersion,
|
||||
}
|
||||
saveCache(cache)
|
||||
|
||||
if verbose {
|
||||
output.Printf("[updater] Updated cache\n")
|
||||
}
|
||||
|
||||
// Compare versions
|
||||
currentVersion := strings.TrimPrefix(global.CliVersion, "v")
|
||||
latestVersion = strings.TrimPrefix(latestVersion, "v")
|
||||
|
||||
if verbose {
|
||||
output.Printf("[updater] Comparing versions: current=%s latest=%s\n", currentVersion, latestVersion)
|
||||
}
|
||||
|
||||
if !isNewer(latestVersion, currentVersion) {
|
||||
// Already up to date
|
||||
if verbose {
|
||||
output.Printf("[updater] Already on latest version, no update needed\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if verbose {
|
||||
output.Printf("[updater] Update available! Attempting to install...\n")
|
||||
}
|
||||
|
||||
// Determine channel for update command
|
||||
channel := "latest"
|
||||
if strings.Contains(global.CliVersion, "nightly") {
|
||||
channel = "nightly"
|
||||
}
|
||||
|
||||
// Attempt update
|
||||
if verbose {
|
||||
output.Printf("[updater] Running: npm install -g cline%s\n",
|
||||
map[bool]string{true: "@"+channel, false: ""}[channel == "nightly"])
|
||||
}
|
||||
|
||||
if err := attemptUpdate(channel); err != nil {
|
||||
if verbose {
|
||||
output.Printf("[updater] Update failed: %v\n", err)
|
||||
}
|
||||
showFailureMessage(channel)
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
output.Printf("[updater] Update completed successfully!\n")
|
||||
}
|
||||
|
||||
showSuccessMessage(latestVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchLatestVersion() (string, error) {
|
||||
// Determine dist-tag from current version
|
||||
distTag := "latest"
|
||||
if strings.Contains(global.CliVersion, "nightly") {
|
||||
distTag = "nightly"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://registry.npmjs.org/cline", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("npm registry returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var data npmRegistryResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if distTag == "nightly" {
|
||||
return data.DistTags.Nightly, nil
|
||||
}
|
||||
return data.DistTags.Latest, nil
|
||||
}
|
||||
|
||||
func attemptUpdate(channel string) error {
|
||||
packageName := "cline"
|
||||
if channel == "nightly" {
|
||||
packageName = "cline@nightly"
|
||||
}
|
||||
|
||||
cmd := exec.Command("npm", "install", "-g", packageName)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func isNewer(latest, current string) bool {
|
||||
// Parse version strings (e.g., "1.0.0-nightly.19")
|
||||
latestBase, latestSuffix := parseVersion(latest)
|
||||
currentBase, currentSuffix := parseVersion(current)
|
||||
|
||||
// Compare base versions (1.0.0)
|
||||
comparison := compareVersionParts(latestBase, currentBase)
|
||||
if comparison != 0 {
|
||||
return comparison > 0
|
||||
}
|
||||
|
||||
// Base versions are equal, compare suffixes (nightly.19)
|
||||
return compareSuffix(latestSuffix, currentSuffix) > 0
|
||||
}
|
||||
|
||||
func parseVersion(version string) (string, string) {
|
||||
parts := strings.SplitN(version, "-", 2)
|
||||
if len(parts) == 2 {
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
return parts[0], ""
|
||||
}
|
||||
|
||||
func compareVersionParts(v1, v2 string) int {
|
||||
parts1 := strings.Split(v1, ".")
|
||||
parts2 := strings.Split(v2, ".")
|
||||
|
||||
for i := 0; i < len(parts1) && i < len(parts2); i++ {
|
||||
// Convert to int for proper numeric comparison
|
||||
n1 := parseInt(parts1[i])
|
||||
n2 := parseInt(parts2[i])
|
||||
|
||||
if n1 > n2 {
|
||||
return 1
|
||||
}
|
||||
if n1 < n2 {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// If all parts are equal, longer version is newer
|
||||
if len(parts1) > len(parts2) {
|
||||
return 1
|
||||
}
|
||||
if len(parts1) < len(parts2) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func compareSuffix(s1, s2 string) int {
|
||||
// If one has no suffix, stable > prerelease
|
||||
if s1 == "" && s2 == "" {
|
||||
return 0
|
||||
}
|
||||
if s1 == "" {
|
||||
return 1 // Stable is newer than prerelease
|
||||
}
|
||||
if s2 == "" {
|
||||
return -1 // Prerelease is older than stable
|
||||
}
|
||||
|
||||
// Both have suffixes (e.g., "nightly.19" vs "nightly.18")
|
||||
// Extract the numeric part after the last dot
|
||||
n1 := extractBuildNumber(s1)
|
||||
n2 := extractBuildNumber(s2)
|
||||
|
||||
if n1 > n2 {
|
||||
return 1
|
||||
}
|
||||
if n1 < n2 {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func extractBuildNumber(suffix string) int {
|
||||
// Extract number from "nightly.19" -> 19
|
||||
parts := strings.Split(suffix, ".")
|
||||
if len(parts) > 1 {
|
||||
return parseInt(parts[len(parts)-1])
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseInt(s string) int {
|
||||
var result int
|
||||
fmt.Sscanf(s, "%d", &result)
|
||||
return result
|
||||
}
|
||||
|
||||
func showSuccessMessage(version string) {
|
||||
output.Printf("\n%s Updated to %s %s Changes will take effect next session\n\n",
|
||||
successStyle.Render("✓"),
|
||||
successStyle.Render("v"+version),
|
||||
dimStyle.Render("→"),
|
||||
)
|
||||
}
|
||||
|
||||
func showFailureMessage(channel string) {
|
||||
packageName := "cline"
|
||||
if channel == "nightly" {
|
||||
packageName = "cline@nightly"
|
||||
}
|
||||
|
||||
output.Printf("\n%s Auto-update failed %s Try: %s\n\n",
|
||||
errorStyle.Render("✗"),
|
||||
dimStyle.Render("·"),
|
||||
"npm install -g "+packageName,
|
||||
)
|
||||
}
|
||||
|
||||
func getCacheFilePath() string {
|
||||
configDir := filepath.Join(os.Getenv("HOME"), ".cline", "data")
|
||||
return filepath.Join(configDir, "cli-update-cache")
|
||||
}
|
||||
|
||||
func loadCache() (cacheData, error) {
|
||||
var cache cacheData
|
||||
cacheFile := getCacheFilePath()
|
||||
|
||||
data, err := os.ReadFile(cacheFile)
|
||||
if err != nil {
|
||||
return cache, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &cache)
|
||||
return cache, err
|
||||
}
|
||||
|
||||
func saveCache(cache cacheData) error {
|
||||
cacheFile := getCacheFilePath()
|
||||
|
||||
// Ensure config directory exists
|
||||
configDir := filepath.Dir(cacheFile)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(cacheFile, data, 0644)
|
||||
}
|
||||
@@ -1,54 +1,13 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type PackageInfo struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// getCliVersion reads the CLI version from package.json
|
||||
func getCliVersion() string {
|
||||
// Try to find package.json relative to the executable
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// Look for package.json in the same directory as the executable
|
||||
packagePath := filepath.Join(filepath.Dir(execPath), "package.json")
|
||||
|
||||
// If not found, try parent directory (for development builds)
|
||||
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
|
||||
packagePath = filepath.Join(filepath.Dir(execPath), "..", "package.json")
|
||||
}
|
||||
|
||||
// If still not found, try cli directory from project root
|
||||
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
|
||||
packagePath = filepath.Join(filepath.Dir(execPath), "..", "..", "cli", "package.json")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(packagePath)
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
var pkgInfo PackageInfo
|
||||
if err := json.Unmarshal(data, &pkgInfo); err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
return pkgInfo.Version
|
||||
}
|
||||
|
||||
// NewVersionCommand creates the version command
|
||||
func NewVersionCommand() *cobra.Command {
|
||||
var short bool
|
||||
@@ -59,16 +18,14 @@ func NewVersionCommand() *cobra.Command {
|
||||
Short: "Show version information",
|
||||
Long: `Display version information for the Cline CLI.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Get CLI version from package.json
|
||||
cliVersion := getCliVersion()
|
||||
|
||||
// Versions are injected at build time via ldflags
|
||||
if short {
|
||||
fmt.Println(cliVersion)
|
||||
fmt.Println(global.CliVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Cline CLI\n")
|
||||
fmt.Printf("Cline CLI Version: %s\n", cliVersion)
|
||||
fmt.Printf("Cline CLI Version: %s\n", global.CliVersion)
|
||||
fmt.Printf("Cline Core Version: %s\n", global.Version)
|
||||
fmt.Printf("Commit: %s\n", global.Commit)
|
||||
fmt.Printf("Built: %s\n", global.Date)
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
@@ -124,6 +126,16 @@ func NormalizeAddressForGRPC(address string) (string, error) {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// GetNodeVersion returns the current Node.js version, or "unknown" if unable to detect
|
||||
func GetNodeVersion() string {
|
||||
cmd := exec.Command("node", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
// RetryOperation performs an operation with retry logic
|
||||
func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation func() error) error {
|
||||
var lastErr error
|
||||
@@ -155,5 +167,19 @@ func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation f
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("operation failed after %d attempts: %w", maxRetries, lastErr)
|
||||
return fmt.Errorf(`operation failed to after %d attempts: %w
|
||||
|
||||
This is usually caused by an incompatible Node.js version
|
||||
|
||||
REQUIREMENTS:
|
||||
• Node.js version 20+ is required
|
||||
• Current Node.js version: %s
|
||||
|
||||
DEBUGGING STEPS:
|
||||
1. View recent logs: cline log list
|
||||
2. Logs are available in: ~/.cline/logs/
|
||||
3. The most recent cline-core log file is usually valuable
|
||||
|
||||
For additional help, visit: https://github.com/cline/cline/issues
|
||||
`, maxRetries, lastErr, GetNodeVersion())
|
||||
}
|
||||
|
||||
@@ -144,6 +144,8 @@ const (
|
||||
OPENAI_NATIVE = "openai-native"
|
||||
XAI = "xai"
|
||||
CEREBRAS = "cerebras"
|
||||
OCA = "oca"
|
||||
NOUSRESEARCH = "nousResearch"
|
||||
)
|
||||
|
||||
// AllProviders returns a slice of enabled provider IDs for the CLI build.
|
||||
@@ -159,6 +161,8 @@ var AllProviders = []string{
|
||||
"openai-native",
|
||||
"xai",
|
||||
"cerebras",
|
||||
"oca",
|
||||
"nousResearch",
|
||||
}
|
||||
|
||||
// ConfigField represents a configuration field requirement
|
||||
@@ -316,6 +320,15 @@ var rawConfigFields = ` [
|
||||
"fieldType": "password",
|
||||
"placeholder": "Enter your API key"
|
||||
},
|
||||
{
|
||||
"name": "nousResearchApiKey",
|
||||
"type": "string",
|
||||
"comment": "",
|
||||
"category": "nousResearch",
|
||||
"required": true,
|
||||
"fieldType": "password",
|
||||
"placeholder": "Enter your API key"
|
||||
},
|
||||
{
|
||||
"name": "ulid",
|
||||
"type": "string",
|
||||
@@ -433,6 +446,15 @@ var rawConfigFields = ` [
|
||||
"fieldType": "url",
|
||||
"placeholder": "https://api.example.com"
|
||||
},
|
||||
{
|
||||
"name": "minimaxApiLine",
|
||||
"type": "string",
|
||||
"comment": "",
|
||||
"category": "general",
|
||||
"required": false,
|
||||
"fieldType": "string",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"name": "ocaMode",
|
||||
"type": "string",
|
||||
@@ -441,7 +463,16 @@ var rawConfigFields = ` [
|
||||
"required": false,
|
||||
"fieldType": "string",
|
||||
"placeholder": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "hicapApiKey",
|
||||
"type": "string",
|
||||
"comment": "",
|
||||
"category": "general",
|
||||
"required": true,
|
||||
"fieldType": "password",
|
||||
"placeholder": "Enter your API key"
|
||||
},
|
||||
]`
|
||||
|
||||
// Raw model definitions data (parsed from TypeScript)
|
||||
@@ -467,6 +498,16 @@ var rawModelDefinitions = ` {
|
||||
"supportsImages": true,
|
||||
"supportsPromptCache": true
|
||||
},
|
||||
"claude-haiku-4-5-20251001": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 200000,
|
||||
"inputPrice": 1,
|
||||
"outputPrice": 5,
|
||||
"cacheWritesPrice": 1,
|
||||
"cacheReadsPrice": 0,
|
||||
"supportsImages": true,
|
||||
"supportsPromptCache": true
|
||||
},
|
||||
"claude-sonnet-4-20250514": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 200000,
|
||||
@@ -579,6 +620,16 @@ var rawModelDefinitions = ` {
|
||||
"supportsImages": true,
|
||||
"supportsPromptCache": true
|
||||
},
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 200000,
|
||||
"inputPrice": 1,
|
||||
"outputPrice": 5,
|
||||
"cacheWritesPrice": 1,
|
||||
"cacheReadsPrice": 0,
|
||||
"supportsImages": true,
|
||||
"supportsPromptCache": true
|
||||
},
|
||||
"anthropic.claude-sonnet-4-20250514-v1:0": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 200000,
|
||||
@@ -744,6 +795,24 @@ var rawModelDefinitions = ` {
|
||||
"supportsImages": false,
|
||||
"supportsPromptCache": false,
|
||||
"description": "A compact 20B open-weight Mixture-of-Experts language model designed for strong reasoning and tool use, ideal for edge devices and local inference."
|
||||
},
|
||||
"qwen.qwen3-coder-30b-a3b-v1:0": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 262144,
|
||||
"inputPrice": 0,
|
||||
"outputPrice": 0,
|
||||
"supportsImages": false,
|
||||
"supportsPromptCache": false,
|
||||
"description": "Qwen3 Coder 30B MoE model with 3.3B activated parameters, optimized for code generation and analysis with 256K context window."
|
||||
},
|
||||
"qwen.qwen3-coder-480b-a35b-v1:0": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 262144,
|
||||
"inputPrice": 0,
|
||||
"outputPrice": 1,
|
||||
"supportsImages": false,
|
||||
"supportsPromptCache": false,
|
||||
"description": "Qwen3 Coder 480B flagship MoE model with 35B activated parameters, designed for complex coding tasks with advanced reasoning capabilities and 256K context window."
|
||||
}
|
||||
},
|
||||
"gemini": {
|
||||
@@ -1232,6 +1301,26 @@ var rawModelDefinitions = ` {
|
||||
"supportsPromptCache": false,
|
||||
"description": "SOTA performance with ~1500 tokens/s"
|
||||
}
|
||||
},
|
||||
"nousResearch": {
|
||||
"Hermes-4-405B": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 128000,
|
||||
"inputPrice": 0,
|
||||
"outputPrice": 0,
|
||||
"supportsImages": false,
|
||||
"supportsPromptCache": false,
|
||||
"description": "This is the largest model in the Hermes 4 family, and it is the fullest expression of our design, focused on advanced reasoning and creative depth rather than optimizing inference speed or cost."
|
||||
},
|
||||
"Hermes-4-70B": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 128000,
|
||||
"inputPrice": 0,
|
||||
"outputPrice": 0,
|
||||
"supportsImages": false,
|
||||
"supportsPromptCache": false,
|
||||
"description": "This incarnation of Hermes 4 balances scale and size. It handles complex reasoning tasks, while staying fast and cost effective. A versatile choice for many use cases."
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
@@ -1389,6 +1478,30 @@ func GetProviderDefinitions() (map[string]ProviderDefinition, error) {
|
||||
HasDynamicModels: false,
|
||||
SetupInstructions: `Get your API key from https://cloud.cerebras.ai/`,
|
||||
}
|
||||
|
||||
// Oca
|
||||
definitions["oca"] = ProviderDefinition{
|
||||
ID: "oca",
|
||||
Name: "Oca",
|
||||
RequiredFields: getFieldsByProvider("oca", configFields, true),
|
||||
OptionalFields: getFieldsByProvider("oca", configFields, false),
|
||||
Models: modelDefinitions["oca"],
|
||||
DefaultModelID: "",
|
||||
HasDynamicModels: false,
|
||||
SetupInstructions: `Configure Oca API credentials`,
|
||||
}
|
||||
|
||||
// NousResearch
|
||||
definitions["nousResearch"] = ProviderDefinition{
|
||||
ID: "nousResearch",
|
||||
Name: "NousResearch",
|
||||
RequiredFields: getFieldsByProvider("nousResearch", configFields, true),
|
||||
OptionalFields: getFieldsByProvider("nousResearch", configFields, false),
|
||||
Models: modelDefinitions["nousResearch"],
|
||||
DefaultModelID: "Hermes-4-405B",
|
||||
HasDynamicModels: false,
|
||||
SetupInstructions: `Configure NousResearch API credentials`,
|
||||
}
|
||||
|
||||
return definitions, nil
|
||||
}
|
||||
@@ -1415,6 +1528,8 @@ func GetProviderDisplayName(providerID string) string {
|
||||
"openai-native": "OpenAI",
|
||||
"xai": "X AI (Grok)",
|
||||
"cerebras": "Cerebras",
|
||||
"oca": "Oca",
|
||||
"nousResearch": "NousResearch",
|
||||
}
|
||||
|
||||
if name, exists := displayNames[providerID]; exists {
|
||||
|
||||
@@ -77,9 +77,9 @@ func (s *EnvService) GetHostVersion(ctx context.Context, req *cline.EmptyRequest
|
||||
|
||||
return &host.GetHostVersionResponse{
|
||||
Platform: proto.String("Cline CLI"),
|
||||
Version: proto.String(""),
|
||||
Version: proto.String(global.CliVersion),
|
||||
ClineType: proto.String("CLI"),
|
||||
ClineVersion: proto.String(global.Version),
|
||||
ClineVersion: proto.String(global.CliVersion),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 187 KiB |
@@ -0,0 +1,416 @@
|
||||
---
|
||||
title: "CLI Reference"
|
||||
description: "Complete command reference for Cline CLI including configuration, instance management, and task commands"
|
||||
---
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
### 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
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: "Installation"
|
||||
description: "Install Cline CLI and authenticate with your account"
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Cline CLI requires Node.js version 20 or higher. We recommend using Node.js 22 for the best experience.
|
||||
|
||||
To check your Node.js version:
|
||||
|
||||
```bash
|
||||
node --version
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g cline
|
||||
```
|
||||
|
||||
After installation, authenticate with your Cline account:
|
||||
|
||||
```bash
|
||||
cline auth
|
||||
```
|
||||
|
||||
This starts an authentication wizard to sign you in and configure your preferred AI model provider.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Get started with Cline in seconds:
|
||||
|
||||
```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.
|
||||
|
||||
For even faster execution without interaction:
|
||||
|
||||
```bash
|
||||
cline "Add unit tests to utils.js"
|
||||
```
|
||||
|
||||
This runs Cline with a single command, perfect for quick tasks or automation.
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: "Overview"
|
||||
description: "Install the CLI, run your first task, and learn to automate code reviews and integrate AI agents into your development workflow"
|
||||
---
|
||||
|
||||
<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.
|
||||
|
||||
The CLI tracks instances across your system and outputs in formats designed for both humans and scripts—JSON, plain text, or rich terminal output.
|
||||
|
||||
<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
|
||||
|
||||
Cline CLI supports multiple AI model providers, giving you flexibility in choosing the best model for your needs:
|
||||
|
||||
- **Anthropic**
|
||||
- **OpenAI**
|
||||
- **OpenAI Compatible**
|
||||
- **OpenRouter**
|
||||
- **X AI (Grok)**
|
||||
- **AWS Bedrock**
|
||||
- **Google Gemini**
|
||||
- **Ollama**
|
||||
- **Cerebras**
|
||||
|
||||
During installation, you'll authenticate and configure your preferred provider using the `cline auth` command.
|
||||
|
||||
## 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
|
||||
|
||||
## 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>
|
||||
|
||||
<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>
|
||||
</Columns>
|
||||
@@ -0,0 +1,324 @@
|
||||
---
|
||||
title: "GitHub Actions Integration"
|
||||
description: "Automatically respond to GitHub issues by mentioning @cline in comments using Cline CLI in GitHub Actions."
|
||||
---
|
||||
|
||||
# GitHub Integration Sample
|
||||
|
||||
Automate GitHub issue analysis with AI. Mention `@cline` in any issue comment to trigger an autonomous investigation that reads files, analyzes code, and provides actionable insights - all running automatically in GitHub Actions.
|
||||
|
||||
|
||||
<Note>
|
||||
**New to Cline CLI?** This sample assumes you understand Cline CLI basics and have completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation). If you're new to Cline CLI, we recommend starting with the [GitHub RCA sample](./github-issue-rca) first, as it's simpler and will help you understand the fundamentals before setting up GitHub Actions.
|
||||
</Note>
|
||||
|
||||
## The Workflow
|
||||
|
||||
Trigger Cline by mentioning `@cline` in any issue comment:
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss0a-comment.png" alt="Issue comment with @cline mention" width="600" />
|
||||
</Frame>
|
||||
|
||||
Cline's automated analysis appears as a new comment, with insights drawn from your actual codebase:
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss0b-final.png" alt="Automated analysis response from Cline" width="600" />
|
||||
</Frame>
|
||||
|
||||
The entire investigation runs autonomously in GitHub Actions - from file exploration to posting results.
|
||||
|
||||
Let's configure your repository.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, you'll need:
|
||||
|
||||
- **Cline CLI knowledge** - Completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation) and understand basic usage
|
||||
- **GitHub repository** - With admin access to configure Actions and secrets
|
||||
- **GitHub Actions familiarity** - Basic understanding of workflows and CI/CD
|
||||
- **API provider account** - OpenRouter, Anthropic, or similar with API key
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Copy the Workflow File
|
||||
|
||||
|
||||
|
||||
Copy the workflow file from this sample to your repository. The workflow file must be placed in the `.github/workflows/` directory in your repository root for GitHub Actions to detect and run it. In this case, we'll name it `cline-responder.yml`.
|
||||
|
||||
```bash
|
||||
# In your repository root
|
||||
mkdir -p .github/workflows
|
||||
curl -o .github/workflows/cline-responder.yml https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-integration/cline-responder.yml
|
||||
```
|
||||
|
||||
Alternatively, you can copy the full workflow file directly into `.github/workflows/cline-responder.yml`:
|
||||
|
||||
<Accordion title="Click to view the complete cline-responder.yml workflow">
|
||||
```yaml
|
||||
name: Cline Issue Assistant
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
respond:
|
||||
runs-on: ubuntu-latest
|
||||
environment: cline-actions
|
||||
steps:
|
||||
- name: Check for @cline mention
|
||||
id: detect
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const body = context.payload.comment?.body || "";
|
||||
const isPR = !!context.payload.issue?.pull_request;
|
||||
const hit = body.toLowerCase().includes("@cline");
|
||||
core.setOutput("hit", (!isPR && hit) ? "true" : "false");
|
||||
core.setOutput("issue_number", String(context.payload.issue?.number || ""));
|
||||
core.setOutput("issue_url", context.payload.issue?.html_url || "");
|
||||
core.setOutput("comment_body", body);
|
||||
|
||||
- name: Checkout repository
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Node v20 is needed for Cline CLI on GitHub Actions Linux
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup Cline CLI
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
run: |
|
||||
# Install the Cline CLI
|
||||
sudo npm install -g cline
|
||||
|
||||
- name: Create Cline Instance
|
||||
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
|
||||
|
||||
- name: Download analyze script
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
run: |
|
||||
export GITORG="YOUR-GITHUB-ORG"
|
||||
export GITREPO="YOUR-GITHUB-REPO"
|
||||
|
||||
curl -L https://raw.githubusercontent.com/${GITORG}/${GITREPO}/refs/heads/main/git-scripts/analyze-issue.sh -o analyze-issue.sh
|
||||
chmod +x analyze-issue.sh
|
||||
|
||||
- name: Run analysis
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
id: analyze
|
||||
env:
|
||||
ISSUE_URL: ${{ steps.detect.outputs.issue_url }}
|
||||
COMMENT: ${{ steps.detect.outputs.comment_body }}
|
||||
CLINE_ADDRESS: ${{ env.CLINE_ADDRESS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
RESULT=$(./analyze-issue.sh "${ISSUE_URL}" "Analyze this issue. The user asked: ${COMMENT}" "$CLINE_ADDRESS")
|
||||
|
||||
{
|
||||
echo 'result<<EOF'
|
||||
printf "%s\n" "$RESULT"
|
||||
echo 'EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Post response
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ steps.detect.outputs.issue_number }}
|
||||
RESULT: ${{ steps.analyze.outputs.result }}
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: Number(process.env.ISSUE_NUMBER),
|
||||
body: process.env.RESULT || "(no output)"
|
||||
});
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Warning>
|
||||
**You MUST edit the workflow file before committing!**
|
||||
|
||||
Open `.github/workflows/cline-responder.yml` and update the "Download analyze script" step within the workflow to specify your GitHub organization and repository where the analysis script is stored:
|
||||
|
||||
```yaml
|
||||
export GITORG="YOUR-GITHUB-ORG" # Change this!
|
||||
export GITREPO="YOUR-GITHUB-REPO" # Change this!
|
||||
```
|
||||
|
||||
**Example:** If your repository is `github.com/acme/myproject`, set:
|
||||
```yaml
|
||||
export GITORG="acme"
|
||||
export GITREPO="myproject"
|
||||
```
|
||||
|
||||
This tells the workflow where to download the analysis script from your repository after you commit it in step 3.
|
||||
</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.
|
||||
|
||||
### 2. Configure API Keys
|
||||
|
||||
Add your AI provider API keys as repository secrets:
|
||||
|
||||
1. Go to your GitHub repository
|
||||
2. Navigate to **Settings** → **Environment** and Add a new environment.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss01-environment.png" alt="Navigate to Actions secrets" width="600" />
|
||||
</Frame>
|
||||
|
||||
Make sure to name it "cline-actions" so that it matches the `environment`
|
||||
value at the top of the `cline-responder.yml` file.
|
||||
|
||||
3. Click **New repository secret**
|
||||
4. Add a secret for the `OPENROUTER_API_KEY` with a value of an API key from
|
||||
[openrouter.com](https://openrouter.com).
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss02-api-key.png" alt="Add API key secret" width="600" />
|
||||
</Frame>
|
||||
|
||||
5. Verify your secret is configured:
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss03-ready.png" alt="API key configured" width="600" />
|
||||
</Frame>
|
||||
|
||||
Now you're ready to supply Cline with the credentials it needs in a GitHub Action.
|
||||
|
||||
### 3. Add Analysis Script
|
||||
|
||||
Add the analysis script from the `github-issue-rca` sample to your repository. **First, you'll need to create a `git-scripts` directory in your repository root where the script will be located.** Choose one of these options:
|
||||
|
||||
**Option A: Download directly (Recommended)**
|
||||
|
||||
```bash
|
||||
# In your repository root, create the directory and download the script
|
||||
mkdir -p git-scripts
|
||||
curl -o git-scripts/analyze-issue.sh https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-issue-rca/analyze-issue.sh
|
||||
chmod +x git-scripts/analyze-issue.sh
|
||||
```
|
||||
|
||||
**Option B: Manual copy-paste**
|
||||
|
||||
Create the directory and file manually, then paste the script content:
|
||||
|
||||
```bash
|
||||
# In your repository root
|
||||
mkdir -p git-scripts
|
||||
# Create and edit the file with your preferred editor
|
||||
nano git-scripts/analyze-issue.sh # or use vim, code, etc.
|
||||
```
|
||||
|
||||
<Accordion title="Click to view the complete analyze-issue.sh script">
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Analyze a GitHub issue using Cline CLI
|
||||
|
||||
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"
|
||||
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 | \
|
||||
sed -n '/^{/,$p' | \
|
||||
jq -r 'select(.say == "completion_result") | .text' | \
|
||||
sed 's/\\n/\n/g'
|
||||
```
|
||||
|
||||
After pasting the script content, make it executable:
|
||||
```bash
|
||||
chmod +x git-scripts/analyze-issue.sh
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
This analysis script calls Cline to execute a prompt on a GitHub issue,
|
||||
summarizing the output to populate the reply to the issue.
|
||||
|
||||
### 4. Commit and Push
|
||||
|
||||
```bash
|
||||
git add .github/workflows/cline-responder.yml
|
||||
git add git-scripts/analyze-issue.sh
|
||||
git commit -m "Add Cline issue assistant workflow"
|
||||
git push
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once set up, simply mention `@cline` in any issue comment:
|
||||
|
||||
```
|
||||
@cline what's causing this error?
|
||||
|
||||
@cline analyze the root cause
|
||||
|
||||
@cline what are the security implications?
|
||||
```
|
||||
|
||||
GitHub Actions will:
|
||||
1. Detect the `@cline` mention
|
||||
2. Start a Cline CLI instance
|
||||
3. Download the analysis script
|
||||
4. Analyze the issue using act mode with yolo (fully autonomous)
|
||||
5. Post Cline's analysis as a new comment
|
||||
|
||||
**Note**: The workflow only triggers on issue comments, not pull request
|
||||
comments.
|
||||
|
||||
## How It Works
|
||||
|
||||
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 ...`
|
||||
6. **Downloads** the reusable `analyze-issue.sh` script from the
|
||||
`github-issue-rca` sample
|
||||
7. **Runs** analysis with the instance address
|
||||
8. **Posts** the analysis result as a comment
|
||||
|
||||
## Related Samples
|
||||
|
||||
- **[github-issue-rca](./github-issue-rca)**: The reusable script that powers this integration
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
title: "GitHub Issue RCA Sample"
|
||||
description: "Automated GitHub issue analysis using Cline CLI to identify root causes."
|
||||
---
|
||||
|
||||
# GitHub Root Cause Analysis
|
||||
|
||||
Automated GitHub issue analysis using Cline CLI. This script uses Cline's autonomous AI capabilities to fetch, analyze, and identify root causes of GitHub issues, outputting clean, parseable results that can be easily integrated into your development workflows.
|
||||
|
||||
<Note>
|
||||
**New to Cline CLI?** This sample assumes you have already completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation) and authenticated with `cline auth`. If you haven't set up Cline CLI yet, please start there first.
|
||||
</Note>
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/cli-rca.gif" alt="CLI Root Cause Analysis Demo" width="600" />
|
||||
</Frame>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This sample assumes you have already:
|
||||
|
||||
- **Cline CLI** installed and authenticated ([Installation Guide](https://docs.cline.bot/cline-cli/installation))
|
||||
- **At least one AI model provider** configured (e.g., OpenRouter, Anthropic, OpenAI)
|
||||
- **Basic familiarity** with Cline CLI commands
|
||||
|
||||
Additionally, you'll need:
|
||||
|
||||
- **GitHub CLI** (`gh`) installed and authenticated
|
||||
- **jq** installed for JSON parsing
|
||||
- **bash** shell (or compatible shell)
|
||||
|
||||
### Installation Instructions
|
||||
|
||||
#### macOS
|
||||
|
||||
<Note>
|
||||
These instructions require [Homebrew](https://brew.sh/) to be installed. If you don't have Homebrew, install it first by running:
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
</Note>
|
||||
|
||||
```bash
|
||||
# Install GitHub CLI
|
||||
brew install gh
|
||||
|
||||
# Install jq
|
||||
brew install jq
|
||||
|
||||
# Authenticate with GitHub
|
||||
gh auth login
|
||||
```
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
# Install GitHub CLI (Debian/Ubuntu)
|
||||
sudo apt install gh
|
||||
|
||||
# Or for other Linux distributions, see: https://cli.github.com/manual/installation
|
||||
|
||||
# Install jq (Debian/Ubuntu)
|
||||
sudo apt install jq
|
||||
|
||||
# Authenticate with GitHub
|
||||
gh auth login
|
||||
```
|
||||
|
||||
## Getting the Script
|
||||
|
||||
**Option 1: Download directly with curl**
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-issue-rca/analyze-issue.sh
|
||||
```
|
||||
|
||||
**Option 2: Copy the full script**
|
||||
|
||||
<Accordion title="Click to view the complete analyze-issue.sh script">
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Analyze a GitHub issue using Cline CLI
|
||||
|
||||
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"
|
||||
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 | \
|
||||
sed -n '/^{/,$p' | \
|
||||
jq -r 'select(.say == "completion_result") | .text' | \
|
||||
sed 's/\\n/\n/g'
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Note>
|
||||
**After downloading or creating the script**, make it executable by running:
|
||||
```bash
|
||||
chmod +x analyze-issue.sh
|
||||
```
|
||||
</Note>
|
||||
|
||||
## Quick Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Run this command in your terminal from the directory where you saved the script to analyze an issue with the default root cause prompt:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/owner/repo/issues/123
|
||||
```
|
||||
|
||||
This will:
|
||||
- Fetch issue #123 from the repository
|
||||
- Analyze the issue to identify root causes
|
||||
- Provide detailed analysis with recommendations
|
||||
|
||||
### Custom Analysis Prompt
|
||||
|
||||
Ask specific questions about the issue:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/owner/repo/issues/456 "What is the security impact?"
|
||||
```
|
||||
|
||||
### Using Specific Cline Instance
|
||||
|
||||
Target a particular Cline instance by address:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/owner/repo/issues/123 \
|
||||
"What is the root cause of this issue?" \
|
||||
127.0.0.1:46529
|
||||
```
|
||||
|
||||
<Warning>
|
||||
This is useful when:
|
||||
- Running multiple Cline instances
|
||||
- Using a remote Cline server
|
||||
- Testing with specific configurations
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
The script will automatically handle everything: fetching the issue, analyzing it with Cline, and displaying the results. The analysis typically takes 30-60 seconds depending on the issue complexity.
|
||||
</Note>
|
||||
|
||||
## How It Works
|
||||
|
||||
Let's analyze each component of the script to understand how it works.
|
||||
|
||||
### Argument Validation
|
||||
|
||||
The script validates input and provides usage instructions:
|
||||
|
||||
```bash
|
||||
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?'"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'Analyze security impact' 127.0.0.1:46529"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Validates required GitHub issue URL
|
||||
- Shows clear usage examples
|
||||
- Supports optional custom prompt
|
||||
- Supports optional Cline instance address
|
||||
|
||||
### Argument Parsing
|
||||
|
||||
The script extracts and sets up the arguments:
|
||||
|
||||
```bash
|
||||
# Gather the args
|
||||
ISSUE_URL="$1"
|
||||
PROMPT="${2:-What is the root cause of this issue?}"
|
||||
if [ -n "$3" ]; then
|
||||
ADDRESS="--address $3"
|
||||
fi
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
- `ISSUE_URL="$1"` - First argument is always the issue URL
|
||||
- `PROMPT="${2:-...}"` - Second argument is optional, defaults to root cause analysis
|
||||
- `ADDRESS` - Third argument is optional, only set if provided
|
||||
|
||||
### The Core Analysis Pipeline
|
||||
|
||||
This is where the magic happens:
|
||||
|
||||
```bash
|
||||
# Ask Cline for his analysis, showing only the summary
|
||||
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
|
||||
sed -n '/^{/,$p' | \
|
||||
jq -r 'select(.say == "completion_result") | .text' | \
|
||||
sed 's/\\n/\n/g'
|
||||
```
|
||||
|
||||
<Accordion title="Pipeline Breakdown: Understanding Each Component">
|
||||
|
||||
**1. `cline -y "$PROMPT: $ISSUE_URL"`**
|
||||
- `-y` enables yolo mode (no user interaction)
|
||||
- Constructs prompt with issue URL
|
||||
|
||||
**2. `--mode act`**
|
||||
- Enables act mode for active investigation
|
||||
- Allows Cline to use tools (read files, run commands, etc.)
|
||||
|
||||
**3. `$ADDRESS`**
|
||||
- Optional address flag for specific instance
|
||||
- Expands to `--address <ip:port>` if set
|
||||
|
||||
**4. `-F json`**
|
||||
- Outputs in JSON format for parsing
|
||||
|
||||
**5. `sed -n '/^{/,$p'`**
|
||||
- Extracts JSON from output
|
||||
- Skips any non-JSON prefix lines
|
||||
|
||||
**6. `jq -r 'select(.say == "completion_result") | .text'`**
|
||||
- Filters for completion result messages
|
||||
- Extracts the text field
|
||||
- `-r` outputs raw strings (no JSON quotes)
|
||||
|
||||
**7. `sed 's/\\n/\n/g'`**
|
||||
- Converts escaped newlines to actual newlines
|
||||
- Makes output readable
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Sample Output
|
||||
|
||||
Here's an example analyzing a real Flutter issue:
|
||||
|
||||
```bash
|
||||
$ ./analyze-issue.sh https://github.com/csells/flutter_counter/issues/2
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```markdown
|
||||
**Root Cause Analysis of Issue #2: "setState isn't cutting it"**
|
||||
|
||||
After examining the GitHub issue and analyzing the Flutter counter codebase,
|
||||
I've identified the root cause of why setState() is insufficient for this
|
||||
project's needs:
|
||||
|
||||
## Current Implementation Problems
|
||||
|
||||
The current Flutter counter app uses setState() for state management, which
|
||||
has several limitations:
|
||||
|
||||
1. **Local State Only**: setState() only works within a single widget, making
|
||||
it difficult to share state across the app
|
||||
2. **Rebuild Overhead**: Every setState() call rebuilds the entire widget tree,
|
||||
causing performance issues with complex UIs
|
||||
3. **No State Persistence**: State is lost when the widget is disposed
|
||||
4. **Testing Challenges**: setState-based logic is tightly coupled to the UI,
|
||||
making unit testing difficult
|
||||
|
||||
## Why This Matters
|
||||
|
||||
As the app grows beyond a simple counter, these limitations become critical:
|
||||
- Multiple screens need to access the count
|
||||
- State needs to persist across navigation
|
||||
- Business logic should be testable independently
|
||||
- UI should only rebuild when necessary
|
||||
|
||||
## Recommended Solutions
|
||||
|
||||
The issue mentions "Provider or Bloc" - both are excellent alternatives:
|
||||
|
||||
1. **Provider**: Simple, lightweight state management using InheritedWidget
|
||||
- Easy migration path from setState
|
||||
- Good for small to medium apps
|
||||
- Official Flutter recommendation
|
||||
|
||||
2. **Bloc**: More structured approach with clear separation between events,
|
||||
states, and business logic
|
||||
- Better for complex apps
|
||||
- Excellent testability
|
||||
- Clear architectural patterns
|
||||
|
||||
3. **Riverpod**: Modern alternative to Provider with better performance and
|
||||
developer experience
|
||||
- Compile-time safety
|
||||
- Better testing support
|
||||
- More flexible than Provider
|
||||
|
||||
4. **GetX**: Full-featured solution with state management, routing, and
|
||||
dependency injection
|
||||
- Minimal boilerplate
|
||||
- Fast and lightweight
|
||||
- All-in-one solution
|
||||
|
||||
## Next Steps
|
||||
|
||||
The current codebase needs refactoring to implement proper state management
|
||||
architecture to handle more complex state scenarios effectively. Provider
|
||||
would be the easiest migration path while Bloc provides better long-term
|
||||
scalability.
|
||||
```
|
||||
|
||||
## When to Use This Pattern
|
||||
|
||||
This script pattern is ideal for various development scenarios where automated GitHub issue analysis can accelerate your workflow.
|
||||
|
||||
### Bug Investigation
|
||||
|
||||
Quickly analyze bug reports and identify root causes without manual code exploration:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/123 \
|
||||
"What is the root cause of this bug?"
|
||||
```
|
||||
|
||||
### Feature Request Analysis
|
||||
|
||||
Understand context and implications of feature requests:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/456 \
|
||||
"What are the implementation challenges?"
|
||||
```
|
||||
|
||||
### Security Audits
|
||||
|
||||
Assess security implications of reported issues:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/789 \
|
||||
"What are the security implications?"
|
||||
```
|
||||
|
||||
### Documentation Generation
|
||||
|
||||
Generate detailed technical documentation from issues:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/654 \
|
||||
"Provide detailed technical documentation for this issue"
|
||||
```
|
||||
|
||||
### Code Review Assistance
|
||||
|
||||
Get second opinions on proposed changes:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/987 \
|
||||
"Review the proposed solution approach"
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This sample demonstrates how to build an autonomous GitHub issue analysis tool using Cline CLI:
|
||||
|
||||
1. **Building autonomous CLI tools** using Cline's capabilities
|
||||
2. **Parsing structured JSON output** from Cline CLI
|
||||
3. **Creating flexible automation scripts** with custom prompting
|
||||
4. **Integrating with GitHub** for issue analysis
|
||||
5. **Handling command-line arguments** effectively
|
||||
|
||||
This pattern can be adapted for many other automation scenarios, from pull request reviews to documentation generation to code quality analysis.
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [CLI Installation Guide](https://docs.cline.bot/cline-cli/installation)
|
||||
- [CLI Reference Documentation](https://docs.cline.bot/cline-cli/cli-reference)
|
||||
- [Three Core Flows](https://docs.cline.bot/cline-cli/three-core-flows)
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: "Samples Overview"
|
||||
description: Example implementations demonstrating Cline CLI capabilities
|
||||
---
|
||||
|
||||
This section provides sample implementations that demonstrate various Cline CLI features and capabilities. Each sample includes complete code, detailed explanations, and real-world usage examples.
|
||||
|
||||
## Available Samples
|
||||
|
||||
<CardGroup cols={1}>
|
||||
<Card
|
||||
title="GitHub Root Cause Analysis"
|
||||
icon="magnifying-glass-chart"
|
||||
href="/cline-cli/samples/github-issue-rca"
|
||||
>
|
||||
A command-line script that uses Cline's autonomous AI capabilities to fetch, analyze, and identify root causes of GitHub issues. Features JSON output parsing and non-interactive execution.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="GitHub Integration (Actions)"
|
||||
icon="github"
|
||||
href="/cline-cli/samples/github-integration"
|
||||
>
|
||||
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>
|
||||
</CardGroup>
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [CLI Installation Guide](/cline-cli/installation)
|
||||
- [CLI Reference Documentation](/cline-cli/cli-reference)
|
||||
- [Three Core Flows](/cline-cli/three-core-flows)
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: "Three Core Flows"
|
||||
description: "Learn the three ways to use Cline CLI: interactive mode, headless automation, and multi-instance parallelization"
|
||||
---
|
||||
|
||||
Two concepts to understand:
|
||||
|
||||
**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.
|
||||
|
||||
**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: Plan first, then act
|
||||
|
||||
Start here to see how Cline works. Interactive mode opens a chat session where you can review plans before execution.
|
||||
|
||||
```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.
|
||||
|
||||
Review or edit the plan in chat. When you're ready, switch to execution:
|
||||
|
||||
```bash
|
||||
/act
|
||||
```
|
||||
|
||||
Cline executes the approved steps—reading files, writing code, running commands. You maintain control throughout the process.
|
||||
|
||||
## 2. Headless single-shot: Complete a task without chat
|
||||
|
||||
Use this for automation where you want a one-liner that just does the work.
|
||||
|
||||
```bash
|
||||
cline instance new --default
|
||||
cline task new -y "Generate unit tests for all Go files"
|
||||
```
|
||||
|
||||
With the `-y` (YOLO) flag, Cline plans and executes autonomously without interactive chat. Perfect for CI, cron jobs, or scripts.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Create a complete feature
|
||||
cline task new -y "Create a REST API for user authentication"
|
||||
|
||||
# Generate documentation
|
||||
cline task new -y "Add JSDoc comments to all functions in src/"
|
||||
|
||||
# Refactor code
|
||||
cline task new -y "Convert all var declarations to const/let"
|
||||
```
|
||||
|
||||
Monitor your task with:
|
||||
|
||||
```bash
|
||||
# View task status
|
||||
cline task view
|
||||
|
||||
# Follow task progress in real-time
|
||||
cline task view --follow
|
||||
```
|
||||
|
||||
Press Ctrl+C to exit the view.
|
||||
|
||||
<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:
|
||||
|
||||
```bash
|
||||
cline instance new
|
||||
```
|
||||
|
||||
This returns an instance address you'll use to target tasks. Attach a task to this instance:
|
||||
|
||||
```bash
|
||||
# Frontend work on first instance
|
||||
cline task new -y "Build React components"
|
||||
```
|
||||
|
||||
Create a second instance and set it as default in one command:
|
||||
|
||||
```bash
|
||||
cline instance new --default
|
||||
```
|
||||
|
||||
Now you can create tasks without specifying the address—they automatically use the default instance:
|
||||
|
||||
```bash
|
||||
# Backend work on the new default instance
|
||||
cline task new -y "Implement API endpoints"
|
||||
```
|
||||
|
||||
List all running instances:
|
||||
|
||||
```bash
|
||||
cline instances list
|
||||
```
|
||||
|
||||
Stop all instances when done:
|
||||
|
||||
```bash
|
||||
cline instances kill -a
|
||||
```
|
||||
|
||||
<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>
|
||||
|
||||
## Configuring context window for local providers
|
||||
|
||||
For Ollama and LM Studio, you can configure the model context window via CLI:
|
||||
|
||||
```bash
|
||||
# For Ollama
|
||||
cline config s ollama-api-options-ctx-num=32768
|
||||
|
||||
# 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
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</Columns>
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: "Model Selection Guide"
|
||||
description: "Last updated: August 20, 2025."
|
||||
---
|
||||
|
||||
New models drop constantly, so this guide focuses on what's working well with Cline right now. We'll keep it updated as the landscape shifts.
|
||||
|
||||
<Callout type="tip">
|
||||
**New to model selection?** Start with [Module 2 of Cline's Learning Path](https://cline.bot/learn) for a comprehensive guide to choosing and configuring models.
|
||||
</Callout>
|
||||
|
||||
## What is an AI Model?
|
||||
|
||||
Think of an AI model as the "brain" that powers Cline. When you ask Cline to write code, fix bugs, or refactor your project, it's the model that actually understands your request and generates the response.
|
||||
|
||||
**Key points:**
|
||||
- **Models are trained AI systems** that understand natural language and code
|
||||
- **Different models have different strengths** some excel at complex reasoning, others prioritize speed or cost
|
||||
- **You choose which model Cline uses** like picking between different experts for different tasks
|
||||
- **Models are accessed via API providers** - companies like Anthropic, OpenAI, and OpenRouter host these models
|
||||
|
||||
**Why it matters:** The model you choose directly impacts Cline's capabilities, response quality, speed, and cost. A premium model might handle complex refactoring beautifully but cost more, while a budget model works great for routine tasks at a fraction of the price.
|
||||
|
||||
## How to Select a Model in Cline
|
||||
|
||||
Follow these 5 simple steps to get Cline up and running with your preferred AI model:
|
||||
|
||||
### Step 1: Open Cline Settings
|
||||
|
||||
First, you need to access Cline's configuration panel.
|
||||
|
||||
**Two ways to open settings:**
|
||||
- **Quick method**: Click the **gear icon (⚙️)** in the top-right corner of Cline's chat interface
|
||||
- **Command palette**: Press **Cmd/Ctrl + Shift + P** → type "Cline: Open Settings"
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/step1-config.png" alt="Cline Settings Panel" />
|
||||
</Frame>
|
||||
|
||||
The settings panel will open, showing configuration options with "API Provider" at the top.
|
||||
|
||||
<Note>
|
||||
The settings panel remembers your last configuration, so you'll only need to set this up once.
|
||||
</Note>
|
||||
|
||||
### Step 2: Select an API Provider
|
||||
|
||||
Choose your preferred AI provider from the dropdown menu.
|
||||
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/step2-provider.png" alt="Cline Settings Panel" />
|
||||
</Frame>
|
||||
|
||||
**Popular providers at a glance:**
|
||||
|
||||
| Provider | Best For | Notes |
|
||||
|----------|----------|-------|
|
||||
| **Cline** | Easiest setup | No API keys needed, access to multiple models including stealth models |
|
||||
| **OpenRouter** | Value seekers | Multiple models, competitive pricing |
|
||||
| **Anthropic** | Reliability | Claude models, most dependable tool usage |
|
||||
| **OpenAI** | Latest tech | GPT models |
|
||||
| **Google Gemini** | Large context | Google's AI models |
|
||||
| **AWS Bedrock** | Enterprise | Advanced features |
|
||||
| **Ollama** | Privacy | Run models locally |
|
||||
|
||||
See the [full provider list](/provider-config) for more options including Cerebras, Vertex AI, Azure, and more.
|
||||
|
||||
<Info>
|
||||
**Recommended for beginners:** Start with **Cline** as your provider - no API key management needed, instant access to multiple models, and occasional free inferencing through partner providers.
|
||||
</Info>
|
||||
|
||||
### Step 3: Add Your API Key (or Sign In)
|
||||
|
||||
The next step depends on which provider you selected.
|
||||
|
||||
#### If you selected **Cline** as your provider:
|
||||
|
||||
- **No API key needed!** Simply sign in with your Cline account
|
||||
- Click the **Sign In** button when prompted
|
||||
- You'll be redirected to [app.cline.bot](https://app.cline.bot) to authenticate
|
||||
- After signing in, return to your IDE
|
||||
|
||||
#### If you selected any other provider:
|
||||
|
||||
You'll need to get an API key from your chosen provider:
|
||||
|
||||
1. **Visit your provider's website to get an API key:**
|
||||
- **Anthropic**: [console.anthropic.com](https://console.anthropic.com/)
|
||||
- **OpenRouter**: [openrouter.ai/keys](https://openrouter.ai/keys)
|
||||
- **OpenAI**: [platform.openai.com/api-keys](https://platform.openai.com/api-keys)
|
||||
- **Google**: [aistudio.google.com/apikey](https://aistudio.google.com/apikey)
|
||||
- **Others**: See [Provider Setup Guide](/provider-config)
|
||||
|
||||
2. **Generate a new API key** on the provider's website
|
||||
|
||||
3. **Copy the API key** to your clipboard
|
||||
|
||||
4. **Paste your key** in the **"API Key"** field in Cline settings
|
||||
|
||||
5. **Save automatically** - Your key is stored securely in your editor's secrets storage
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/step3-API.png" alt="Cline API Selection" />
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Payment required for most providers**: Most providers need payment information before generating keys. You only pay for what you use (typically $0.01-$0.10 per coding task).
|
||||
</Warning>
|
||||
|
||||
### Step 4: Choose Your Model
|
||||
|
||||
Once your API key is added (or you've signed in), the **"Model"** dropdown becomes available.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/step4-model.png" alt="Cline Model Selection" />
|
||||
</Frame>
|
||||
|
||||
**Quick model selection guide:**
|
||||
|
||||
| Your Priority | Choose This Model | Why |
|
||||
|---------------|-------------------|-----|
|
||||
| **Maximum reliability** | Claude Sonnet 4.5 | Most reliable tool usage, excellent at complex tasks |
|
||||
| **Best value** | DeepSeek V3 or Qwen3 Coder | Great performance at budget prices |
|
||||
| **Fastest speed** | Qwen3 Coder on Cerebras | Lightning-fast responses |
|
||||
| **Run locally** | Any Ollama model | Complete privacy, no internet needed |
|
||||
| **Latest features** | GPT-5 | OpenAI's newest capabilities |
|
||||
|
||||
Not sure which to pick? Start with **Claude Sonnet 4.5** for reliability or **DeepSeek V3** for value.
|
||||
|
||||
<Tip>
|
||||
You can switch models at any time without losing your conversation. Try different models to find what works best for your specific tasks.
|
||||
</Tip>
|
||||
|
||||
See the [model comparison tables](#current-top-models) below for detailed specifications and pricing.
|
||||
|
||||
### Step 5: Start Using Cline
|
||||
|
||||
**Congratulations! You're all set up.** Here's how to start coding with Cline:
|
||||
|
||||
1. **Type your request** in the Cline chat box
|
||||
- Example: "Create a React component for a login form"
|
||||
- Example: "Debug this TypeScript error"
|
||||
- Example: "Refactor this function to be more efficient"
|
||||
|
||||
2. **Press Enter** or click the send icon to submit
|
||||
|
||||
## Choosing the Right Model
|
||||
|
||||
Selecting the right model involves balancing several factors. Use this framework to find your ideal match:
|
||||
|
||||
<Note>
|
||||
**Pro tips**: Configure separate models for Plan Mode and Act Mode. Make the most out the each model's strengths. For example, use a budget model for planning discussions and a premium model for implementation.
|
||||
</Note>
|
||||
|
||||
### Key Selection Factors
|
||||
|
||||
| Factor | What to Consider | Recommendation |
|
||||
|--------|------------------|----------------|
|
||||
| **Task Complexity** | Simple fixes vs complex refactoring | Budget models for routine tasks; Premium models for complex work |
|
||||
| **Budget** | Monthly spending capacity | \$10-\$30: Budget, \$30-\$100: Mid-tier, \$100+: Premium |
|
||||
| **Context Window** | Project size and file count | Small: 32K-128K, Medium: 128K-200K, Large: 400K+ |
|
||||
| **Speed** | Response time requirements | Interactive: Fast models, Background: Reasoning models OK |
|
||||
| **Tool Reliability** | Complex operations | Claude excels at tool usage; Test others with your workflow |
|
||||
| **Provider** | Access and pricing needs | OpenRouter: Many options, Direct: Faster/reliable, Local: Privacy |
|
||||
|
||||
|
||||
|
||||
## Model Comparison Resources
|
||||
|
||||
For detailed model comparisons, pricing, and performance metrics, see:
|
||||
- [**Model Comparison & Pricing**](/model-config/model-comparison) - Complete pricing tables and performance benchmarks
|
||||
- [**Context Window Guide**](/model-config/context-windows) - Understanding and optimizing context usage
|
||||
|
||||
## Open Source vs Closed Source
|
||||
|
||||
### Open Source Advantages
|
||||
- **Multiple providers** compete to host them
|
||||
- **Cheaper pricing** due to competition
|
||||
- **Provider choice** - switch if one goes down
|
||||
- **Faster innovation** cycles
|
||||
|
||||
### Open Source Models Available
|
||||
- **Qwen3 Coder** (Apache 2.0)
|
||||
- **Z AI GLM 4.5** (MIT)
|
||||
- **Kimi K2** (Open source)
|
||||
- **DeepSeek series** (Various licenses)
|
||||
|
||||
## Quick Decision Matrix
|
||||
|
||||
| If you want... | Use this |
|
||||
|----------------|----------|
|
||||
| Something that just works | Claude Sonnet 4.5 |
|
||||
| To save money | DeepSeek V3 or Qwen3 variants |
|
||||
| Huge context windows | Gemini 2.5 Pro or Claude Sonnet 4.5 |
|
||||
| Open source | Qwen3 Coder, Z AI GLM 4.5, or Kimi K2 |
|
||||
| Latest tech | GPT-5 |
|
||||
| Speed | Qwen3 Coder on Cerebras (fastest available) |
|
||||
|
||||
## What Others Are Using
|
||||
|
||||
Check [OpenRouter's Cline usage stats](https://openrouter.ai/apps?url=https%3A%2F%2Fcline.bot%2F) to see real usage patterns from the community.
|
||||
@@ -2,15 +2,15 @@
|
||||
"$schema": "https://mintlify.com/docs.json",
|
||||
"theme": "linden",
|
||||
"name": "Cline",
|
||||
"description": "AI-powered coding assistant for VSCode",
|
||||
"description": "AI-powered coding agent for complex work",
|
||||
"colors": {
|
||||
"primary": "#9D4EDD",
|
||||
"light": "#F0E6FF",
|
||||
"dark": "#000000"
|
||||
},
|
||||
"logo": {
|
||||
"light": "/assets/robot_panel_light.png",
|
||||
"dark": "/assets/robot_panel_dark.png"
|
||||
"light": "/assets/Cline_Logo-complete_black.png",
|
||||
"dark": "/assets/Cline_Logo-complete_white.png"
|
||||
},
|
||||
"favicon": {
|
||||
"light": "/assets/robot_panel_light.png",
|
||||
@@ -18,10 +18,9 @@
|
||||
},
|
||||
"background": {
|
||||
"color": {
|
||||
"light": "#F0E6FF",
|
||||
"dark": "#000000"
|
||||
},
|
||||
"decoration": "gradient"
|
||||
"light": "#fafaf9",
|
||||
"dark": "#0f0f0f"
|
||||
}
|
||||
},
|
||||
"styling": {
|
||||
"eyebrows": "breadcrumbs",
|
||||
@@ -33,16 +32,18 @@
|
||||
"strict": false
|
||||
},
|
||||
"fonts": {
|
||||
"family": "Roboto"
|
||||
"family": "Geist Sans"
|
||||
},
|
||||
"navbar": {
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"icon": "github",
|
||||
"href": "https://github.com/cline/cline"
|
||||
},
|
||||
{
|
||||
"label": "Discord",
|
||||
"icon": "discord",
|
||||
"href": "https://discord.gg/cline"
|
||||
}
|
||||
],
|
||||
@@ -53,169 +54,251 @@
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"groups": [
|
||||
"tabs": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"getting-started/what-is-cline",
|
||||
"getting-started/installing-cline",
|
||||
"getting-started/model-selection-guide",
|
||||
"getting-started/task-management",
|
||||
"getting-started/understanding-context-management",
|
||||
"tab": "Docs",
|
||||
"icon": "square-terminal",
|
||||
"groups": [
|
||||
{
|
||||
"group": "For New Coders",
|
||||
"group": "Introduction",
|
||||
"pages": [
|
||||
"getting-started/for-new-coders",
|
||||
"getting-started/installing-dev-essentials"
|
||||
"introduction/welcome",
|
||||
"introduction/overview"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"getting-started/installing-cline",
|
||||
"getting-started/selecting-your-model",
|
||||
"getting-started/your-first-project"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Best Practices",
|
||||
"pages": [
|
||||
"prompting/understanding-context-management",
|
||||
"prompting/prompt-engineering-guide",
|
||||
"prompting/cline-memory-bank"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "CLI",
|
||||
"pages": [
|
||||
"cline-cli/overview",
|
||||
"cline-cli/installation",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Features",
|
||||
"pages": [
|
||||
{
|
||||
"group": "@ Mentions",
|
||||
"pages": [
|
||||
"features/at-mentions/overview",
|
||||
"features/at-mentions/file-mentions",
|
||||
"features/at-mentions/terminal-mentions",
|
||||
"features/at-mentions/problem-mentions",
|
||||
"features/at-mentions/git-mentions",
|
||||
"features/at-mentions/url-mentions"
|
||||
]
|
||||
},
|
||||
"features/auto-approve",
|
||||
"features/auto-compact",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
{
|
||||
"group": "Commands & Shortcuts",
|
||||
"pages": [
|
||||
"features/commands-and-shortcuts/overview",
|
||||
"features/commands-and-shortcuts/code-commands",
|
||||
"features/commands-and-shortcuts/terminal-integration",
|
||||
"features/commands-and-shortcuts/git-integration",
|
||||
"features/commands-and-shortcuts/keyboard-shortcuts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Customization",
|
||||
"pages": [
|
||||
"features/customization/opening-cline-in-sidebar",
|
||||
"features/customization/disable-terminal-pagers"
|
||||
]
|
||||
},
|
||||
"features/dictation",
|
||||
"features/drag-and-drop",
|
||||
"features/editing-messages",
|
||||
"features/explain-changes",
|
||||
"features/focus-chain",
|
||||
{
|
||||
"group": "Hooks",
|
||||
"pages": [
|
||||
"features/hooks/index",
|
||||
"features/hooks/hook-reference",
|
||||
"features/hooks/samples"
|
||||
]
|
||||
},
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
{
|
||||
"group": "Slash Commands",
|
||||
"pages": [
|
||||
"features/slash-commands/new-task",
|
||||
"features/slash-commands/new-rule",
|
||||
"features/slash-commands/explain-changes",
|
||||
"features/slash-commands/smol",
|
||||
"features/slash-commands/report-bug",
|
||||
"features/slash-commands/deep-planning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Workflows",
|
||||
"pages": [
|
||||
"features/slash-commands/workflows/index",
|
||||
"features/slash-commands/workflows/quickstart",
|
||||
"features/slash-commands/workflows/best-practices"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Task Management",
|
||||
"pages": [
|
||||
"features/tasks/understanding-tasks",
|
||||
"features/tasks/task-management"
|
||||
]
|
||||
},
|
||||
"features/yolo-mode"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Model & Provider Configuration",
|
||||
"pages": [
|
||||
{
|
||||
"group": "Model Selection",
|
||||
"pages": [
|
||||
"core-features/model-selection-guide",
|
||||
"model-config/model-comparison",
|
||||
"model-config/context-windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Cloud Providers",
|
||||
"pages": [
|
||||
"provider-config/anthropic",
|
||||
"provider-config/claude-code",
|
||||
"provider-config/openai",
|
||||
"provider-config/openrouter",
|
||||
"provider-config/cerebras",
|
||||
"provider-config/deepseek",
|
||||
"provider-config/groq",
|
||||
"provider-config/xai-grok",
|
||||
"provider-config/mistral-ai",
|
||||
"provider-config/doubao",
|
||||
"provider-config/fireworks",
|
||||
"provider-config/zai",
|
||||
"provider-config/gcp-vertex-ai",
|
||||
"provider-config/baseten",
|
||||
{
|
||||
"group": "AWS Bedrock",
|
||||
"pages": [
|
||||
"provider-config/aws-bedrock/api-key",
|
||||
"provider-config/aws-bedrock/iam-credentials",
|
||||
"provider-config/aws-bedrock/cli-profile"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Running Models Locally",
|
||||
"pages": [
|
||||
"running-models-locally/overview",
|
||||
"running-models-locally/ollama",
|
||||
"running-models-locally/lm-studio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Advanced Configuration",
|
||||
"pages": [
|
||||
"provider-config/openai-compatible",
|
||||
"provider-config/litellm-and-cline-using-codestral",
|
||||
"provider-config/vscode-language-model-api",
|
||||
"provider-config/sap-aicore",
|
||||
"provider-config/vercel-ai-gateway",
|
||||
"provider-config/requesty"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "MCP Integration",
|
||||
"pages": [
|
||||
"mcp/mcp-overview",
|
||||
"mcp/adding-mcp-servers-from-github",
|
||||
"mcp/configuring-mcp-servers",
|
||||
"mcp/connecting-to-a-remote-server",
|
||||
"mcp/mcp-marketplace",
|
||||
"mcp/mcp-server-development-protocol",
|
||||
"mcp/mcp-transport-mechanisms"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Cline Tools Reference",
|
||||
"pages": [
|
||||
"exploring-clines-tools/cline-tools-guide",
|
||||
"exploring-clines-tools/new-task-tool",
|
||||
"exploring-clines-tools/remote-browser-support"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"troubleshooting/networking-and-proxies",
|
||||
"troubleshooting/terminal-quick-fixes",
|
||||
"troubleshooting/terminal-integration-guide",
|
||||
"troubleshooting/task-history-recovery",
|
||||
"more-info/telemetry"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Improving Your Prompting Skills",
|
||||
"pages": [
|
||||
"prompting/prompt-engineering-guide",
|
||||
"prompting/cline-memory-bank"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Features",
|
||||
"pages": [
|
||||
"tab": "Enterprise",
|
||||
"icon": "building",
|
||||
"groups": [
|
||||
{
|
||||
"group": "@ Mentions",
|
||||
"group": "Enterprise Solutions",
|
||||
"pages": [
|
||||
"features/at-mentions/overview",
|
||||
"features/at-mentions/file-mentions",
|
||||
"features/at-mentions/terminal-mentions",
|
||||
"features/at-mentions/problem-mentions",
|
||||
"features/at-mentions/git-mentions",
|
||||
"features/at-mentions/url-mentions"
|
||||
"enterprise-solutions/overview",
|
||||
"enterprise-solutions/onboarding",
|
||||
"enterprise-solutions/members/roles-and-permissions",
|
||||
{
|
||||
"group": "Provider Remote Configuration",
|
||||
"pages": [
|
||||
{
|
||||
"group": "AWS Bedrock",
|
||||
"pages": [
|
||||
"enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration",
|
||||
"enterprise-solutions/provider-remote-config/aws-bedrock/member-configuration"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"features/auto-approve",
|
||||
"features/auto-compact",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
{
|
||||
"group": "Commands & Shortcuts",
|
||||
"pages": [
|
||||
"features/commands-and-shortcuts/overview",
|
||||
"features/commands-and-shortcuts/code-commands",
|
||||
"features/commands-and-shortcuts/terminal-integration",
|
||||
"features/commands-and-shortcuts/git-integration",
|
||||
"features/commands-and-shortcuts/keyboard-shortcuts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Customization",
|
||||
"pages": [
|
||||
"features/customization/opening-cline-in-sidebar",
|
||||
"features/customization/disable-terminal-pagers"
|
||||
]
|
||||
},
|
||||
"features/dictation",
|
||||
"features/drag-and-drop",
|
||||
"features/editing-messages",
|
||||
"features/focus-chain",
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
{
|
||||
"group": "Slash Commands",
|
||||
"pages": [
|
||||
"features/slash-commands/new-task",
|
||||
"features/slash-commands/new-rule",
|
||||
"features/slash-commands/smol",
|
||||
"features/slash-commands/report-bug",
|
||||
"features/slash-commands/deep-planning"
|
||||
]
|
||||
},
|
||||
"features/slash-commands/workflows",
|
||||
"features/yolo-mode"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Exploring Cline's Tools",
|
||||
"pages": [
|
||||
"exploring-clines-tools/cline-tools-guide",
|
||||
"exploring-clines-tools/new-task-tool",
|
||||
"exploring-clines-tools/remote-browser-support"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Enterprise Solutions",
|
||||
"pages": [
|
||||
"enterprise-solutions/cloud-provider-integration",
|
||||
"enterprise-solutions/custom-instructions",
|
||||
"enterprise-solutions/mcp-servers",
|
||||
"enterprise-solutions/security-concerns"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "MCP Servers",
|
||||
"pages": [
|
||||
"mcp/mcp-overview",
|
||||
"mcp/adding-mcp-servers-from-github",
|
||||
"mcp/configuring-mcp-servers",
|
||||
"mcp/connecting-to-a-remote-server",
|
||||
"mcp/mcp-marketplace",
|
||||
"mcp/mcp-server-development-protocol",
|
||||
"mcp/mcp-transport-mechanisms"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Provider Configuration",
|
||||
"pages": [
|
||||
"provider-config/anthropic",
|
||||
"provider-config/claude-code",
|
||||
{
|
||||
"group": "AWS Bedrock",
|
||||
"pages": [
|
||||
"provider-config/aws-bedrock/api-key",
|
||||
"provider-config/aws-bedrock/iam-credentials",
|
||||
"provider-config/aws-bedrock/cli-profile"
|
||||
]
|
||||
},
|
||||
"provider-config/gcp-vertex-ai",
|
||||
"provider-config/litellm-and-cline-using-codestral",
|
||||
"provider-config/vscode-language-model-api",
|
||||
"provider-config/xai-grok",
|
||||
"provider-config/mistral-ai",
|
||||
"provider-config/deepseek",
|
||||
"provider-config/groq",
|
||||
"provider-config/cerebras",
|
||||
"provider-config/doubao",
|
||||
"provider-config/fireworks",
|
||||
"provider-config/zai",
|
||||
"provider-config/ollama",
|
||||
"provider-config/openai",
|
||||
"provider-config/openai-compatible",
|
||||
"provider-config/openrouter",
|
||||
"provider-config/sap-aicore",
|
||||
"provider-config/vercel-ai-gateway",
|
||||
"provider-config/requesty",
|
||||
"provider-config/baseten"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Running Models Locally",
|
||||
"pages": [
|
||||
"running-models-locally/read-me-first",
|
||||
"running-models-locally/lm-studio",
|
||||
"running-models-locally/ollama"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Troubleshooting",
|
||||
"pages": [
|
||||
"troubleshooting/terminal-quick-fixes",
|
||||
"troubleshooting/terminal-integration-guide"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "More Info",
|
||||
"pages": [
|
||||
"more-info/telemetry"
|
||||
]
|
||||
"tab": "Learn",
|
||||
"icon": "graduation-cap",
|
||||
"href": "https://cline.bot/learn"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -228,23 +311,78 @@
|
||||
},
|
||||
"anchors": [
|
||||
{
|
||||
"name": "What is Cline",
|
||||
"name": "Overview",
|
||||
"icon": "house",
|
||||
"url": "getting-started/what-is-cline"
|
||||
"url": "introduction/overview"
|
||||
}
|
||||
],
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/getting-started/installing-cline-jetbrains",
|
||||
"destination": "/getting-started/installing-cline"
|
||||
},
|
||||
{
|
||||
"source": "/getting-started/what-is-cline",
|
||||
"destination": "/introduction/overview"
|
||||
},
|
||||
{
|
||||
"source": "/getting-started/overview",
|
||||
"destination": "/introduction/overview"
|
||||
},
|
||||
{
|
||||
"source": "/introduction",
|
||||
"destination": "/introduction/welcome"
|
||||
},
|
||||
{
|
||||
"source": "/getting-started/model-selection-guide",
|
||||
"destination": "/core-features/model-selection-guide"
|
||||
},
|
||||
{
|
||||
"source": "/provider-config/ollama",
|
||||
"destination": "/running-models-locally/ollama"
|
||||
},
|
||||
{
|
||||
"source": "/running-models-locally/read-me-first",
|
||||
"destination": "/running-models-locally/overview"
|
||||
},
|
||||
{
|
||||
"source": "/getting-started/understanding-context-management",
|
||||
"destination": "/prompting/understanding-context-management"
|
||||
},
|
||||
{
|
||||
"source": "/best-practices/understanding-context-management",
|
||||
"destination": "/prompting/understanding-context-management"
|
||||
},
|
||||
{
|
||||
"source": "/getting-started/your-first-task",
|
||||
"destination": "/getting-started/your-first-project"
|
||||
},
|
||||
{
|
||||
"source": "/cline-cli/samples",
|
||||
"destination": "/cline-cli/samples/overview"
|
||||
},
|
||||
{
|
||||
"source": "/features/hooks/real-world-examples",
|
||||
"destination": "/features/hooks/samples"
|
||||
},
|
||||
{
|
||||
"source": "/enterprise-solutions/configure-AWS-Bedrock-Admin",
|
||||
"destination": "/enterprise-solutions/provider-remote-config/aws-bedrock/admin-configuration"
|
||||
},
|
||||
{
|
||||
"source": "/enterprise-solutions/configure-AWS-Bedrock-Member",
|
||||
"destination": "/enterprise-solutions/provider-remote-config/aws-bedrock/member-configuration"
|
||||
},
|
||||
{
|
||||
"source": "/enterprise-solutions/configure-workOS-authkit",
|
||||
"destination": "/enterprise-solutions/onboarding"
|
||||
},
|
||||
{
|
||||
"source": "/enterprise-solutions/Onboarding your Organization",
|
||||
"destination": "/enterprise-solutions/onboarding"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
"prompt": "Search Cline documentation..."
|
||||
},
|
||||
"contextual": {
|
||||
"options": [
|
||||
"copy"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
title: "Cloud Provider Integration"
|
||||
---
|
||||
|
||||
Cline supports major cloud providers like AWS Bedrock and Google's Cloud Vertex; whichever your team currently uses is appropriate, and there's no need to change providers to utilize Cline's features.
|
||||
|
||||
For the purpose of this document, we assume your organization will use cloud-based frontier models. Cloud inference providers offer cutting-edge capabilities and the flexibility to select models which best suit your needs.
|
||||
|
||||
Certain scenarios may warrant using local models, including handling highly sensitive data, applications requiring consistent low-latency responses, or compliance with strict data sovereignty requirements. If your team needs to utilize local models, see [Running Local Models ](/running-models-locally/read-me-first.mdx)with Cline.
|
||||
|
||||
---
|
||||
|
||||
## AWS Bedrock Setup Guides
|
||||
|
||||
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
|
||||
|
||||
#### [AWS Bedrock setup for API Keys](/provider-config/aws-bedrock-with-apikey-authentication)
|
||||
|
||||
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/provider-config/aws-bedrock-with-credentials-authentication)
|
||||
|
||||
#### [AWS Bedrock setup for SSO token (AWS Profile)](/provider-config/aws-bedrock-with-profile-authentication)
|
||||
|
||||
#### VPC Endpoint Setup
|
||||
|
||||
To protect your team's data, Cline supports VPC (Virtual Private Cloud) endpoints, which create private connections between your data and AWS Bedrock. AWS VPCs enhance security by eliminating the need for public IP addresses, network gateways, or complex firewall rules—essentially creating a private highway for data that bypasses the public internet entirely. By keeping traffic within AWS's private network, teams also benefit from lower latency and more predictable performance when accessing services like AWS Bedrock or custom APIs. For those working with confidential information or operating in highly regulated industries like healthcare or finance, VPCs offers the perfect balance between the accessibility of cloud services and the security of private infrastructure.
|
||||
|
||||
---
|
||||
|
||||
1. Consult the [AWS guide](https://docs.aws.amazon.com/bedrock/latest/userguide/vpc-interface-endpoints.html) to creating VPC endpoints. This document specifies pre-requisites and describes the syntax used for creating VPC endpoints.
|
||||
2. Follow the directions for [creating a VPC endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html#create-interface-endpoint-aws) in the AWS console. The image below pertains to steps 4 and 5 of the AWS guide linked above.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/vpc-console.png" alt="VPC Console" />
|
||||
</Frame>
|
||||
|
||||
3. Note the IP address of your VPC endpoint, open Cline's settings menu, and select `AWS Bedrock`from the API Provider dropdown.
|
||||
4. Click the `Use Custom VPC endpoint`checkbox and enter the IP address of your VPC endpoint
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/vpc-settings-menu.png" alt="VPC Settings Menu" />
|
||||
</Frame>
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
title: "Custom Instructions"
|
||||
---
|
||||
|
||||
## Building Custom Instructions for Teams
|
||||
|
||||
**Creating standardized project instructions ensures that all team members work within consistent guidelines. Start by documenting your project's technical foundation, then identify which information needs to be included in the instructions. The exact scope will vary depending on your team's needs, but generally it's best to provide as much information as possible. By creating comprehensive instructions that all team members follow, you establish a shared understanding of how code should be written, tested, and deployed across your project, resulting in more maintainable and consistent software.**
|
||||
|
||||
---
|
||||
|
||||
Here are a few topics and examples to consider for your team's custom instructions:
|
||||
|
||||
1. **Testing framework and specific commands**
|
||||
- "All components must include Jest tests with at least 85% coverage. Run tests using `npm run test:coverage` before submitting any pull request."
|
||||
2. **Explicit library preferences**
|
||||
- "Use React Query for data fetching and state management. Avoid Redux unless specifically required for complex global state. For styling, use Tailwind CSS with our custom theme configuration found in `src/styles/theme.js.`"
|
||||
3. **Where to find documentation**
|
||||
- "All API documentation is available in our internal Notion workspace under 'Engineering > API Reference'. For component usage examples, refer to our Storybook instance at `https://storybook.internal.company.com`"
|
||||
4. **Which MCP servers to use, and for which purposes**
|
||||
- "For database operations, use the Postgres MCP server with credentials stored in 1Password under 'Development > Database'. For deployments, use the AWS MCP server which requires the deployment role from IAM. Refer to `docs/mcp-setup.md` for configuration instructions."
|
||||
5. **Coding conventions specific to your project**
|
||||
- "Name all React components using PascalCase and all helper functions using camelCase. Place components in the `src/components` directory organized by feature, not by type. Always use TypeScript interfaces for prop definitions."
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "Managing Members"
|
||||
sidebarTitle: "Managing Members"
|
||||
description: "A guide to adding, removing, and editing members in your enterprise organization."
|
||||
---
|
||||
|
||||
This guide covers the practical steps for adding, editing, and removing members from your enterprise dashboard. For a conceptual overview of roles and permissions, see the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions).
|
||||
|
||||
<Frame caption="The Members Dashboard provides a central place to manage your team.">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/members-dash.png" alt="Members Dashboard" />
|
||||
</Frame>
|
||||
|
||||
## Adding Members
|
||||
|
||||
To invite someone to your organization, you must have an open seat available on your organization.
|
||||
|
||||
1. Navigate to the **Members** tab in your dashboard.
|
||||
2. Click the **Add Members** button.
|
||||
3. Enter one or more email addresses, separated by commas.
|
||||
4. Select a role for the new member(s). It's best practice to start with the "Member" role unless you know they need admin privileges.
|
||||
5. Click **Send Invitation**.
|
||||
|
||||
Invited users will receive an email with a link to join. You can cancel a pending invitation at any time by clicking the trash icon next to the user's email in the 'Pending Invites' section.
|
||||
|
||||
<Tip>
|
||||
**Managing Users at Scale**
|
||||
|
||||
When inviting a large number of users, you can paste a comma-separated list of emails directly into the invitation field. While role changes and removals are performed individually, this bulk invitation feature helps streamline the onboarding process for entire teams.
|
||||
</Tip>
|
||||
|
||||
<Frame caption="Adding members to your organization">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/adding-members.png" alt="Confirm Member Removal" />
|
||||
</Frame>
|
||||
|
||||
## Editing Member Roles
|
||||
|
||||
As your team's needs change, you can adjust member roles directly from the dashboard.
|
||||
|
||||
- Find the member in your list.
|
||||
- Under the "Role" column, click the dropdown menu.
|
||||
- Select their new role. The change takes effect immediately.
|
||||
|
||||
Refer to the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions) for a detailed breakdown of what each role can do.
|
||||
|
||||
## Removing Members
|
||||
|
||||
Removing a member immediately revokes their access to all organization-specific resources, including shared API keys and configurations.
|
||||
|
||||
1. Go to the **Members Dashboard**.
|
||||
2. Find the member in the list and click the red trash icon (<Icon icon="trash" iconType="solid" />).
|
||||
3. Confirm the removal when prompted.
|
||||
|
||||
<Frame caption="You will be asked to confirm before a member is permanently removed.">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/remove-user.png" alt="Confirm Member Removal" />
|
||||
</Frame>
|
||||
|
||||
## Troubleshooting Invitations
|
||||
|
||||
If an invited user is having trouble joining, check these common issues:
|
||||
|
||||
- **Invitation Not Received**: Ask the user to check their spam or junk mail folder. If it's not there, cancel the pending invitation and try sending it again, verifying the email address is correct.
|
||||
|
||||
- **"Invalid Domain" Error**: The user's email address must belong to a domain that has been verified for your organization. Work with your IT administrator to ensure the necessary domains are configured.
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
title: "MCP Servers"
|
||||
---
|
||||
|
||||
**Model Context Protocol (MCP) servers expand Cline's capabilities by providing standardized access to external data sources and executable functions. By implementing MCP servers, LLM tools can dynamically retrieve and incorporate relevant information from both local and remote data sources. This capability ensures that the models operate with the most current and contextually appropriate data, improving the accuracy and relevance of their outputs.**
|
||||
|
||||
---
|
||||
|
||||
### Secure Architecture Fundamentals
|
||||
|
||||
MCP servers follow a client-server architecture where hosts (LLM applications like Cline) initiate connections through a transport layer to MCP servers. This architecture inherently provides security benefits as it maintains clear separation between components. Enterprise deployments should focus on the proper implementation of this architecture to ensure secure operations, particularly regarding the message exchange patterns and connection lifecycle management. For MCP architecture details, see [MCP Architecture](https://modelcontextprotocol.io/docs/concepts/architecture), and for latest specifications, see [MCP Specifications](https://spec.modelcontextprotocol.io/specification/2024-11-05/).
|
||||
|
||||
### Transport Layer Security
|
||||
|
||||
For enterprise environments, selecting the appropriate transport mechanism is crucial. While stdio transport works efficiently for local processes, HTTP with Server-Sent Events (SSE) transport requires additional security measures. TLS should be used for all remote connections whenever possible. This is especially important when MCP servers are deployed across different network segments within corporate infrastructure.
|
||||
|
||||
### Message Validation and Access Control
|
||||
|
||||
The MCP architecture defines standard error codes and message types (Requests, Results, Errors, and Notifications), providing a structured framework for secure communication. Security teams should consider message validation, sanitizing inputs, checking message size limits, and verifying JSON-RPC format. Additionally, implementing resource protection through access controls, path validation, and request rate limiting helps prevent potential abuse of MCP server capabilities.
|
||||
|
||||
### Monitoring and Compliance
|
||||
|
||||
For enterprise compliance requirements, implementing comprehensive logging of protocol events, message flows, and errors is essential. The MCP architecture supports diagnostic capabilities including health checks, connection state monitoring, and resource usage tracking. Organizations should extend these capabilities to meet their specific compliance needs, particularly for audit trails of all MCP server interactions and resource access patterns.
|
||||
|
||||
By leveraging the client-server design of the MCP architecture and implementing appropriate security controls at each layer, enterprises can safely integrate MCP servers into their environments while maintaining their security posture and meeting regulatory requirements.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "Managing Members"
|
||||
sidebarTitle: "Managing Members"
|
||||
description: "A guide to adding, removing, and editing members in your enterprise organization."
|
||||
---
|
||||
|
||||
This guide covers the practical steps for adding, editing, and removing members from your enterprise dashboard. For a conceptual overview of roles and permissions, see the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions).
|
||||
|
||||
<Frame caption="The Members Dashboard provides a central place to manage your team.">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/members-dash.png" alt="Members Dashboard" />
|
||||
</Frame>
|
||||
|
||||
## Adding Members
|
||||
|
||||
To invite someone to your organization, you must have an open seat available on your organization.
|
||||
|
||||
1. Navigate to the **Members** tab in your dashboard.
|
||||
2. Click the **Add Members** button.
|
||||
3. Enter one or more email addresses, separated by commas.
|
||||
4. Select a role for the new member(s). It's best practice to start with the "Member" role unless you know they need admin privileges.
|
||||
5. Click **Send Invitation**.
|
||||
|
||||
Invited users will receive an email with a link to join. You can cancel a pending invitation at any time by clicking the trash icon next to the user's email in the 'Pending Invites' section.
|
||||
|
||||
<Tip>
|
||||
**Managing Users at Scale**
|
||||
|
||||
When inviting a large number of users, you can paste a comma-separated list of emails directly into the invitation field. While role changes and removals are performed individually, this bulk invitation feature helps streamline the onboarding process for entire teams.
|
||||
</Tip>
|
||||
|
||||
<Frame caption="Adding members to your organization">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/adding-members.png" alt="Confirm Member Removal" />
|
||||
</Frame>
|
||||
|
||||
## Editing Member Roles
|
||||
|
||||
As your team's needs change, you can adjust member roles directly from the dashboard.
|
||||
|
||||
- Find the member in your list.
|
||||
- Under the "Role" column, click the dropdown menu.
|
||||
- Select their new role. The change takes effect immediately.
|
||||
|
||||
Refer to the [Roles and Permissions](/enterprise-solutions/members/roles-and-permissions) for a detailed breakdown of what each role can do.
|
||||
|
||||
## Removing Members
|
||||
|
||||
Removing a member immediately revokes their access to all organization-specific resources, including shared API keys and configurations.
|
||||
|
||||
1. Go to the **Members Dashboard**.
|
||||
2. Find the member in the list and click the red trash icon (<Icon icon="trash" iconType="solid" />).
|
||||
3. Confirm the removal when prompted.
|
||||
|
||||
<Frame caption="You will be asked to confirm before a member is permanently removed.">
|
||||
<img src="https://storage.googleapis.com/cline_public_images/remove-user.png" alt="Confirm Member Removal" />
|
||||
</Frame>
|
||||
|
||||
## Troubleshooting Invitations
|
||||
|
||||
If an invited user is having trouble joining, check these common issues:
|
||||
|
||||
- **Invitation Not Received**: Ask the user to check their spam or junk mail folder. If it's not there, cancel the pending invitation and try sending it again, verifying the email address is correct.
|
||||
|
||||
- **"Invalid Domain" Error**: The user's email address must belong to a domain that has been verified for your organization. Work with your IT administrator to ensure the necessary domains are configured.
|
||||