mirror of
https://github.com/cline/cline.git
synced 2026-09-07 04:44:58 +08:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2212deb3d | |||
| 36bdf643a6 | |||
| 8215d3fdb9 | |||
| 59260cefab | |||
| a767f844b9 | |||
| bb7c3556d6 | |||
| 8b80c337ae | |||
| d0cac53d5b | |||
| 8aedc90214 | |||
| 0c118086df | |||
| e9b8991e5e | |||
| 261a7dd505 | |||
| 9abf2f567a | |||
| 42de7c81b4 | |||
| 06b710f62b | |||
| 06cc49f115 | |||
| ea9b8fe0b1 | |||
| 05213f2a71 | |||
| d5bad1357d | |||
| a4b1549dac | |||
| e015ce94c0 | |||
| be076cf407 | |||
| 580db36476 | |||
| 87c28fc422 | |||
| 096ba34948 | |||
| 07dedee284 | |||
| dde81ec0af | |||
| 8f8b98bb58 | |||
| 36022438cb | |||
| c7afb61e28 | |||
| dfd113a6e5 | |||
| 3698d2356c | |||
| 90807717a7 | |||
| bef69c3be2 | |||
| 7b60a97771 | |||
| fca81a1eba | |||
| ff20c4addc | |||
| 6b7ce5d7c7 | |||
| 8eeeabb966 | |||
| 9b1dc5bd92 | |||
| 1cfff0a45f | |||
| 833d8c7395 | |||
| 49e6bc2c1f | |||
| 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 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add new provider AIhubmix
|
||||
@@ -1,54 +1,15 @@
|
||||
#!/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)
|
||||
input=$(cat)
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
# 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')
|
||||
sleep 3
|
||||
|
||||
# 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": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "PostToolUse hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -1,42 +1,15 @@
|
||||
#!/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)
|
||||
input=$(cat)
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
# Extract tool information
|
||||
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName // "unknown"')
|
||||
parameters=$(echo "$input" | jq -r '.preToolUse.parameters // {}')
|
||||
sleep 3
|
||||
|
||||
# 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": "Make sure that all variables in python code in this repo always start with the string 'EVIEDEBUG_'. This is important.",
|
||||
"errorMessage": "PreToolUse hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
|
||||
+122
-41
@@ -3,7 +3,7 @@
|
||||
## 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)
|
||||
- **Global hooks directory**: `~/Documents/Cline/Hooks/` (applies to all workspaces)
|
||||
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to specific workspace)
|
||||
|
||||
Hooks run automatically when enabled.
|
||||
@@ -17,18 +17,55 @@ 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` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskStart` (all platforms)
|
||||
|
||||
### 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` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskResume` (all platforms)
|
||||
|
||||
### TaskCancel Hook
|
||||
- **When**: Runs when a task is cancelled 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` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskCancel` (all platforms)
|
||||
- **Note**: This hook is NOT cancellable and will complete even if the task is being aborted
|
||||
|
||||
### TaskComplete Hook
|
||||
- **When**: Runs when a task is marked as complete
|
||||
- **Purpose**: Log completion status, perform final cleanup, generate reports
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/TaskComplete` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskComplete` (all platforms)
|
||||
|
||||
### 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` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/UserPromptSubmit` (all platforms)
|
||||
|
||||
### 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)
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/PreToolUse` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/PreToolUse` (all platforms)
|
||||
|
||||
### 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)
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/PostToolUse` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/PostToolUse` (all platforms)
|
||||
|
||||
### PreCompact Hook
|
||||
- **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` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/PreCompact` (all platforms)
|
||||
|
||||
## Cross-Platform Hook Format
|
||||
|
||||
Cline uses a git-style approach for hooks that works consistently across all platforms:
|
||||
@@ -55,16 +92,16 @@ 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
|
||||
chmod +x ~/Documents/Cline/Hooks/PreToolUse
|
||||
```
|
||||
|
||||
**On Windows:**
|
||||
```batch
|
||||
REM Create hook file (note: no file extension)
|
||||
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
|
||||
notepad %USERPROFILE%\Documents\Cline\Hooks\PreToolUse
|
||||
```
|
||||
|
||||
## Context Injection Timing
|
||||
@@ -107,11 +144,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 +194,11 @@ All hooks receive:
|
||||
"result": "string",
|
||||
"success": boolean,
|
||||
"executionTimeMs": number
|
||||
},
|
||||
"preCompact": { // Only for PreCompact
|
||||
"contextSize": number,
|
||||
"messagesToCompact": number,
|
||||
"compactionStrategy": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -131,12 +208,16 @@ 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
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `cancel` field works as follows:
|
||||
- `false` (or omitted): Allow execution to continue
|
||||
- `true`: Block execution and show error message to user
|
||||
|
||||
## Context Modification Format
|
||||
|
||||
Use structured prefixes to help the AI understand context type:
|
||||
@@ -152,7 +233,7 @@ Example:
|
||||
```bash
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"cancel": false,
|
||||
"contextModification": "WORKSPACE_RULES: This is a TypeScript project. All new files must use .ts or .tsx extensions."
|
||||
}
|
||||
EOF
|
||||
@@ -160,9 +241,9 @@ EOF
|
||||
|
||||
## 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,7 +258,7 @@ 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"
|
||||
}
|
||||
@@ -185,7 +266,7 @@ EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
### 2. Context Building - Learn from Operations
|
||||
@@ -200,12 +281,12 @@ path=$(echo "$input" | jq -r '.postToolUse.parameters.path // ""')
|
||||
if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"cancel": false,
|
||||
"contextModification": "FILE_OPERATIONS: Created '$path'. Maintain consistency with this file's patterns in future operations."
|
||||
}
|
||||
EOF
|
||||
else
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -220,12 +301,12 @@ tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
|
||||
if [[ "$execution_time" -gt 5000 ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"cancel": false,
|
||||
"contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
|
||||
}
|
||||
EOF
|
||||
else
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -239,7 +320,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 +328,44 @@ 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) or `%USERPROFILE%\Documents\Cline\Hooks\` (Windows)
|
||||
- **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/`
|
||||
- Windows: `%USERPROFILE%\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
|
||||
nano ~/Documents/Cline/Hooks/PreToolUse
|
||||
chmod +x ~/Documents/Cline/Hooks/PreToolUse
|
||||
|
||||
# Windows
|
||||
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
|
||||
notepad %USERPROFILE%\Documents\Cline\Hooks\PreToolUse
|
||||
```
|
||||
|
||||
3. Enable hooks in Cline settings
|
||||
@@ -294,18 +375,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 +399,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 +412,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
|
||||
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
sleep 3
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel", false,
|
||||
"contextModification": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "TaskCancel hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
sleep 3
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "TaskResume hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
sleep 1
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "TaskStart hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
sleep 3
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "UserPromptSubmit hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
@@ -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>
|
||||
|
||||
@@ -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: console,otlp
|
||||
OTEL_METRICS_EXPORTER: console,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
|
||||
|
||||
@@ -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: console,otlp
|
||||
OTEL_METRICS_EXPORTER: console,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"
|
||||
|
||||
+30
-1
@@ -1,12 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## [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 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+13
-13
@@ -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": [
|
||||
|
||||
+18
-18
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+66
-60
@@ -1,62 +1,68 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.0-nightly.18",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
||||
+25
-5
@@ -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,240 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
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,15 @@ 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: "Oracle Code Assist", Provider: cline.ApiProvider_OCA},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +72,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 +85,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 +100,8 @@ 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_OCA:
|
||||
return "e.g., oca/llama4"
|
||||
default:
|
||||
return "Enter model ID"
|
||||
}
|
||||
@@ -127,8 +132,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 +154,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 +171,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,8 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
cline.ApiProvider_GEMINI,
|
||||
cline.ApiProvider_OLLAMA,
|
||||
cline.ApiProvider_CEREBRAS,
|
||||
cline.ApiProvider_OCA,
|
||||
cline.ApiProvider_HICAP,
|
||||
}
|
||||
|
||||
// Check each provider to see if it's ready to use
|
||||
@@ -120,16 +123,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 +155,7 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
Mode: "Ready",
|
||||
Provider: provider,
|
||||
ModelID: modelID,
|
||||
HasAPIKey: hasAPIKey,
|
||||
HasAPIKey: checkAPIKeyExists(r.apiConfig, provider),
|
||||
BaseURL: baseURL,
|
||||
})
|
||||
seenProviders[provider] = true
|
||||
@@ -207,9 +217,9 @@ func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
|
||||
switch providerStr {
|
||||
case "anthropic":
|
||||
return cline.ApiProvider_ANTHROPIC, true
|
||||
case "openai":
|
||||
case "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
|
||||
return cline.ApiProvider_OPENAI, true
|
||||
case "openai-native":
|
||||
case "openai", "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 +235,10 @@ 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
|
||||
default:
|
||||
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
|
||||
}
|
||||
@@ -237,7 +251,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 +268,10 @@ 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"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -312,9 +330,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 +347,10 @@ 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"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -378,7 +400,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 +452,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 +472,7 @@ 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"},
|
||||
}
|
||||
|
||||
for _, providerCheck := range providersToCheck {
|
||||
@@ -459,6 +488,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,25 @@ 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
|
||||
|
||||
default:
|
||||
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
|
||||
}
|
||||
@@ -150,9 +171,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 +206,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 +223,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 +274,10 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,11 +296,17 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +321,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 +347,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 +414,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 +467,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 +520,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 +561,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 +613,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
|
||||
@@ -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
|
||||
|
||||
@@ -339,9 +339,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -47,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)
|
||||
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
+6
-5
@@ -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)
|
||||
@@ -159,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
|
||||
@@ -210,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
|
||||
@@ -298,6 +297,7 @@ 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
|
||||
@@ -318,6 +318,7 @@ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
|
||||
InputType: InputTypeApproval,
|
||||
Approved: approved,
|
||||
NeedsFeedback: false,
|
||||
NoAskAgain: noAskAgain,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
-18
@@ -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 {
|
||||
@@ -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,6 +662,9 @@ 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 {
|
||||
@@ -644,4 +672,4 @@ func CreateAndFollowTask(ctx context.Context, prompt string, opts TaskOptions) e
|
||||
} else {
|
||||
return taskManager.FollowConversation(ctx, taskManager.GetCurrentInstance(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,46 @@ 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
|
||||
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 +290,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 +392,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
|
||||
|
||||
+44
-15
@@ -282,7 +282,6 @@ func (m *Manager) CheckSendEnabled(ctx context.Context) error {
|
||||
errorTypes := []string{
|
||||
string(types.AskTypeAPIReqFailed), // "api_req_failed"
|
||||
string(types.AskTypeMistakeLimitReached), // "mistake_limit_reached"
|
||||
string(types.AskTypeAutoApprovalMaxReached), // "auto_approval_max_req_reached"
|
||||
}
|
||||
|
||||
isError := false
|
||||
@@ -1004,26 +1003,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 +1236,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,24 +416,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 +452,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 +660,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)
|
||||
}
|
||||
@@ -754,6 +744,8 @@ func setSecretField(secrets *cline.Secrets, key, value string) error {
|
||||
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"
|
||||
@@ -247,8 +246,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)
|
||||
}
|
||||
+27
-1
@@ -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,7 @@ const (
|
||||
OPENAI_NATIVE = "openai-native"
|
||||
XAI = "xai"
|
||||
CEREBRAS = "cerebras"
|
||||
OCA = "oca"
|
||||
)
|
||||
|
||||
// AllProviders returns a slice of enabled provider IDs for the CLI build.
|
||||
@@ -159,6 +160,7 @@ var AllProviders = []string{
|
||||
"openai-native",
|
||||
"xai",
|
||||
"cerebras",
|
||||
"oca",
|
||||
}
|
||||
|
||||
// ConfigField represents a configuration field requirement
|
||||
@@ -441,7 +443,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 +478,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 +600,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,
|
||||
@@ -1389,6 +1420,18 @@ 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`,
|
||||
}
|
||||
|
||||
return definitions, nil
|
||||
}
|
||||
@@ -1415,6 +1458,7 @@ func GetProviderDisplayName(providerID string) string {
|
||||
"openai-native": "OpenAI",
|
||||
"xai": "X AI (Grok)",
|
||||
"cerebras": "Cerebras",
|
||||
"oca": "Oca",
|
||||
}
|
||||
|
||||
if name, exists := displayNames[providerID]; exists {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
@@ -3,19 +3,21 @@ title: "Installation"
|
||||
description: "Install Cline CLI and authenticate with your account"
|
||||
---
|
||||
|
||||
<Tabs>
|
||||
<Tab title="NPM">
|
||||
```bash
|
||||
npm install -g cline
|
||||
```
|
||||
</Tab>
|
||||
## Prerequisites
|
||||
|
||||
<Tab title="CURL">
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/cline/cline/main/scripts/install.sh | bash
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
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:
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@ 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.
|
||||
@@ -13,9 +19,23 @@ The CLI tracks instances across your system and outputs in formats designed for
|
||||
Ready to get started? Check out the [installation guide](/cline-cli/installation) to install Cline CLI and run your first task.
|
||||
</Tip>
|
||||
|
||||
## What you can build with this
|
||||
## Supported Model Providers
|
||||
|
||||
The CLI's design opens up creative possibilities:
|
||||
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
|
||||
|
||||
@@ -126,6 +126,10 @@ For in-depth commands and flags, check out the [CLI reference](/cline-cli/cli-re
|
||||
## 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>
|
||||
@@ -134,11 +138,7 @@ For in-depth commands and flags, check out the [CLI reference](/cline-cli/cli-re
|
||||
Understand how YOLO mode works and when to use full automation versus manual approval.
|
||||
</Card>
|
||||
|
||||
<Card title="Task management" icon="list-checks" href="/getting-started/task-management">
|
||||
<Card title="Task management" icon="clipboard-check" href="/getting-started/task-management">
|
||||
Learn how Cline tracks and manages tasks, including saving and restoring state from checkpoints.
|
||||
</Card>
|
||||
|
||||
<Card title="CLI reference" icon="terminal" href="/cline-cli/cli-reference">
|
||||
Complete command documentation including configuration, instance management, and task commands.
|
||||
</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.
|
||||
+241
-177
@@ -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,178 +54,210 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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/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",
|
||||
{
|
||||
"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",
|
||||
{
|
||||
"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",
|
||||
"provider-config/baseten"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "Enterprise",
|
||||
"pages": [
|
||||
"enterprise-solutions/overview",
|
||||
"enterprise-solutions/security-concerns"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Reference",
|
||||
"pages": [
|
||||
"troubleshooting/terminal-quick-fixes",
|
||||
"troubleshooting/terminal-integration-guide",
|
||||
"more-info/telemetry"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "CLI",
|
||||
"pages": [
|
||||
"cline-cli/overview",
|
||||
"cline-cli/installation",
|
||||
"cline-cli/three-core-flows",
|
||||
"cline-cli/cli-reference"
|
||||
]
|
||||
"tab": "Learn",
|
||||
"icon": "graduation-cap",
|
||||
"href": "https://cline.bot/learn"
|
||||
},
|
||||
{
|
||||
"group": "Improving Your Prompting Skills",
|
||||
"pages": [
|
||||
"prompting/prompt-engineering-guide",
|
||||
"prompting/cline-memory-bank"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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/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": "Blog",
|
||||
"icon": "newspaper",
|
||||
"href": "https://cline.bot/blog"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -237,23 +270,54 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"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."
|
||||
@@ -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,95 @@
|
||||
---
|
||||
title: "Cline Enterprise"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Enterprise security, governance, and observability for the coding agent 3 million developers trust"
|
||||
---
|
||||
|
||||
Cline Enterprise brings centralized governance to the same open-source architecture that millions of developers already use. Your code stays in your environment, you use your own inference at your negotiated rates, and you get the security and observability capabilities that platform teams need for org-wide deployment.
|
||||
|
||||
<Card title="Learn More About Enterprise" icon="building" href="https://cline.bot/enterprise">
|
||||
Visit our website for detailed information about enterprise features, pricing, and deployment options.
|
||||
</Card>
|
||||
|
||||
## What You Get
|
||||
|
||||
It delivers five core capabilities that platform teams need for production deployment. Each addresses a specific requirement for scaling AI coding across your organization.
|
||||
|
||||
### Security by Design
|
||||
|
||||
Your code never leaves your environment. Cline processes everything locally - no uploads, no indexing, no training on your data.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Client-side execution" icon="computer">
|
||||
All processing happens within your environment
|
||||
</Card>
|
||||
|
||||
<Card title="No data exfiltration" icon="shield-check">
|
||||
Code and context never transmitted externally
|
||||
</Card>
|
||||
|
||||
<Card title="No codebase indexing" icon="database">
|
||||
Repositories are never indexed or cached
|
||||
</Card>
|
||||
|
||||
<Card title="No model training" icon="ban">
|
||||
Your code and prompts aren't used for training
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### Bring Your Own Inference
|
||||
|
||||
Use your existing cloud contracts and negotiated rates. Most AI tools force you to buy inference through them with markup. Cline connects directly to your providers.
|
||||
|
||||
Connect to any inference provider:
|
||||
- AWS Bedrock
|
||||
- Google Vertex AI
|
||||
- Azure OpenAI
|
||||
- Anthropic direct
|
||||
- OpenAI direct
|
||||
- Cerebras
|
||||
- Any OpenAI-compatible endpoint
|
||||
|
||||
Switch models instantly as new ones release. Use Claude Sonnet 4.5 as your daily driver, GPT-5 for complex refactoring, open-source models for simple tasks. Your existing cloud credits and startup program contracts now cover AI coding. We handle the agent loop. You handle the inference. No markup, no vendor lock-in.
|
||||
|
||||
### Governance at Scale
|
||||
|
||||
Platform teams need central control when thousands of developers use AI. Individual API keys scattered across laptops create security risks and cost overruns.
|
||||
|
||||
Enterprise governance provides:
|
||||
- **SSO authentication**: Corporate credentials instead of personal API keys
|
||||
- **Role-based access control**: Fine-grained permissions per team and project
|
||||
- **Model and tool controls**: Govern which models and tools each team accesses
|
||||
- **Remote configuration**: Manage settings for all developers from one dashboard
|
||||
- **Full audit logging**: Every AI interaction tracked with detailed logs
|
||||
|
||||
Configure once, deploy everywhere. Developers work how they prefer while you maintain control.
|
||||
|
||||
### Complete Observability
|
||||
|
||||
Export logs to your existing observability stack. Track usage, costs, and performance across all teams.
|
||||
|
||||
- **OpenTelemetry export**: Direct integration with Datadog, Grafana, Splunk
|
||||
- **Real-time analytics**: Track adoption, performance, and patterns
|
||||
- **Cost breakdown**: See exactly what each team spends on which models
|
||||
- **JSON output**: Build custom dashboards in your existing tools
|
||||
|
||||
The same observability standards you require for production systems.
|
||||
|
||||
## Deployment
|
||||
|
||||
Cline Enterprise connects securely to your infrastructure. Deploy in cloud environments, on-premises, or air-gapped networks. Configure to work with your existing security policies and compliance requirements.
|
||||
|
||||
Rolling out to your organization:
|
||||
1. Configure Cline Core to connect to your infrastructure
|
||||
2. Set SSO, RBAC, and governance policies
|
||||
3. Deploy to developers via your existing software distribution
|
||||
4. Monitor usage through your observability tools
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Review [security architecture](/enterprise-solutions/security-concerns)
|
||||
- Configure [cloud provider setup](/provider-config/aws-bedrock/api-key) (AWS Bedrock, Vertex AI, Azure)
|
||||
- Set up [MCP servers](/mcp/mcp-overview) for custom tooling
|
||||
- Add [custom instructions](/features/cline-rules) for your codebase
|
||||
|
||||
Schedule a walkthrough to see how Cline Enterprise fits your infrastructure. We'll work with your security and compliance requirements to deploy in your environment.
|
||||
@@ -4,9 +4,7 @@ title: "Security Concerns"
|
||||
|
||||
## Enterprise Security with Cline
|
||||
|
||||
#### Cline addresses enterprise security concerns through its unique client-side architecture that prioritizes data privacy, secure cloud integration, and transparent operations. Below is a comprehensive overview of how Cline maintains robust security measures for enterprise environments.
|
||||
|
||||
---
|
||||
Cline addresses enterprise security concerns through its unique client-side architecture that prioritizes data privacy, secure cloud integration, and transparent operations. Below is a comprehensive overview of how Cline maintains robust security measures for enterprise environments.
|
||||
|
||||
### Client-Side Architecture
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: "Remote Browser Support"
|
||||
description: "Remote browser support allows Cline to utilize a remote Chrome instance, leveraging authentication tokens and session cookies relevant to certain web development test cases."
|
||||
icon: globe-pointer
|
||||
---
|
||||
|
||||
The Remote Browser feature in Cline allows the AI assistant to interact with web content directly through a controlled browser instance. This enables several powerful capabilities:
|
||||
|
||||
@@ -42,17 +42,17 @@ To open Cline in the right sidebar:
|
||||
4. Set the value to `vertical`
|
||||
5. Restart Cursor for the changes to take effect
|
||||
</Step>
|
||||
<Step title="Open Agent Panel">
|
||||
Click the Cursor cube icon button that opens Cursor's agent (right side view panel)
|
||||
<Step title="Open the AI Pane">
|
||||
Click the Cursor cube icon button (AI Pane) that opens Cursor's agent (right side view panel)
|
||||
</Step>
|
||||
<Step title="Drag to Three Dots">
|
||||
Drag the Cline icon directly onto the three dots button - it doesn't work if you just drag it to the top, it has to be the three dots
|
||||
<Step title="Drag Cline to the AI Pane Sidebar">
|
||||
Drag the Cline icon directly into the AI Pane sidebar.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/cursor-side-bar.gif"
|
||||
src="https://storage.googleapis.com/cline_public_images/Cursor-sidebar.gif"
|
||||
alt="Cursor Right Sidebar Setup"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "Task Management"
|
||||
description: "Learn how to organize, search, and manage your task history in Cline."
|
||||
---
|
||||
|
||||
Cline provides tools to manage your task history, helping you organize, search, and maintain your workspace efficiently. As you accumulate tasks over time, these features become essential for productivity.
|
||||
|
||||
## Accessing Task History
|
||||
|
||||
Learn the different ways to open and navigate to your task history in Cline. Whether you prefer clicking buttons, using keyboard shortcuts, or the command palette, there are multiple convenient methods to access your past work.
|
||||
|
||||
You can access your task history by:
|
||||
|
||||
1. **Clicking the "History" button** in the Cline sidebar
|
||||
2. **Using Command Palette**: Search for "Cline: Show Task History"
|
||||
3. **Keyboard shortcut** (if configured in your VSCode settings)
|
||||
|
||||
## Task History Interface
|
||||
|
||||
Explore the main interface where all your tasks are displayed and managed. This section covers the layout, search capabilities, sorting options, and filtering tools that help you efficiently navigate through your accumulated tasks. The task history view provides a comprehensive interface for managing all your past and current tasks.
|
||||
|
||||
### Search and Filter
|
||||
|
||||
The history view includes search and filtering capabilities:
|
||||
|
||||
#### Search Bar
|
||||
- **Fuzzy search** across all task content
|
||||
- Searches through prompts, responses, and code
|
||||
- Instantly filters results as you type
|
||||
- Highlights matching text in results
|
||||
|
||||
#### Sort Options
|
||||
Sort your tasks by:
|
||||
- **Newest** (default) - Most recent tasks first
|
||||
- **Oldest** - Earliest tasks first
|
||||
- **Most Expensive** - Highest API cost tasks
|
||||
- **Most Tokens** - Highest token usage
|
||||
- **Most Relevant** - Best matches when searching
|
||||
|
||||
#### Favorites Filter
|
||||
- Toggle to show only starred tasks
|
||||
- Quickly access your most important work
|
||||
- Combine with search for precise filtering
|
||||
|
||||
## Task Actions
|
||||
|
||||
Discover the various actions you can perform on individual tasks in your history. From reopening and resuming tasks to exporting and managing them, this section explains all the available operations for task manipulation.
|
||||
|
||||
Each task in the history provides several actions:
|
||||
|
||||
### Primary Actions
|
||||
|
||||
- **Open**: Click on a task to reopen it in the Cline chat
|
||||
- **Resume**: Continue an interrupted task from where it left off
|
||||
- **Export**: Save the conversation to markdown for documentation
|
||||
|
||||
### Management Actions
|
||||
|
||||
- **Favorite** ⭐: Click the star icon to mark important tasks
|
||||
- **Delete** 🗑️: Remove individual tasks (favorites are protected)
|
||||
- **Duplicate**: Create a new task based on an existing one
|
||||
|
||||
## ⭐ Task Favorites
|
||||
|
||||
Master the favorites system to mark and protect your most valuable tasks. This feature allows you to star important work, preventing accidental deletion while providing quick access to reference implementations and successful patterns.
|
||||
|
||||
The favorites system helps you preserve and quickly access important tasks.
|
||||
|
||||
### Using Favorites
|
||||
|
||||
**Marking Favorites**
|
||||
- Click the star icon next to any task
|
||||
- Star fills in when favorited
|
||||
- Click again to unfavorite
|
||||
|
||||
**Protection Features**
|
||||
- Favorited tasks are protected from accidental deletion
|
||||
- Bulk delete operations skip favorites by default
|
||||
- Can override protection with explicit confirmation
|
||||
|
||||
**Use Cases for Favorites**
|
||||
- Reference implementations you want to keep
|
||||
- Successful problem-solving patterns
|
||||
- Tasks with reusable code snippets
|
||||
- Important project milestones
|
||||
- Learning examples for team members
|
||||
|
||||
## Task Metrics
|
||||
|
||||
Gain insights into your Cline usage through task metrics. This section explains how to track token usage, API costs, and other metrics to help you optimize your workflow and manage resources effectively.
|
||||
|
||||
Understanding your task metrics helps optimize usage:
|
||||
|
||||
### Available Metrics
|
||||
|
||||
- **Token Usage**: Total input/output tokens consumed
|
||||
- **API Cost**: Estimated cost based on model pricing
|
||||
- **Checkpoint Count**: Number of file snapshots created
|
||||
|
||||
### Using Metrics
|
||||
|
||||
- **Budget Tracking**: Monitor API costs across tasks
|
||||
- **Efficiency Analysis**: Identify expensive operations
|
||||
- **Model Comparison**: Compare costs between models
|
||||
- **Optimization**: Find tasks that could be more efficient
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: "Understanding Tasks"
|
||||
description: "Learn what tasks are in Cline, how they work, and how to create effective prompts for better results."
|
||||
---
|
||||
|
||||
## What are Tasks?
|
||||
|
||||
Most users interact with Cline through **tasks** - the fundamental unit of work that drives every coding session. Whether you're building a new feature, fixing a bug, refactoring code, or exploring a codebase, every interaction with Cline happens within the context of a task. A task represents a complete conversation and work session between you and the AI agent, created through **prompts** - the instructions you provide to tell Cline what you want to accomplish. Tasks serve as self-contained work sessions that capture your entire conversation with Cline, including all the code changes, command executions, and decisions made along the way.
|
||||
|
||||
This approach ensures that your work is organized, traceable, and resumable. Each task maintains its own isolated context, allowing you to work on multiple projects simultaneously without confusion. The beauty of Cline's task system lies in its flexibility and persistence, providing a collaborative coding session where you provide the direction through prompts, and Cline executes your vision with precision.
|
||||
|
||||
### Key Characteristics
|
||||
|
||||
Each task in Cline:
|
||||
|
||||
- **Has a unique identifier**: Every task gets its own ID and dedicated storage directory
|
||||
- **Contains the full conversation**: All messages, tool uses, and results are preserved
|
||||
- **Tracks resources used**: Token usage, API costs, and execution time are monitored
|
||||
- **Can be interrupted and resumed**: Tasks maintain their state across VSCode sessions
|
||||
- **Creates checkpoints**: File changes are tracked through Git-based snapshots
|
||||
- **Enables documentation**: Tasks can be exported as markdown for team documentation
|
||||
- **Provides cost management**: Resource tracking helps monitor API usage and costs
|
||||
|
||||
These features make Cline not just a coding tool, but a comprehensive development agent that understands the full lifecycle of your work.
|
||||
|
||||
## Creating Tasks with Prompts
|
||||
|
||||
Tasks begin with prompts - your instructions to Cline. The quality of your results depends heavily on how you describe what you want.
|
||||
|
||||
### Prompt Components
|
||||
|
||||
A well-structured prompt typically includes:
|
||||
|
||||
- **Goal**: What you want to accomplish
|
||||
- **Context**: Background information and constraints
|
||||
- **Requirements**: Specific features or functionality needed
|
||||
- **Preferences**: Technology choices, coding style, etc.
|
||||
- **Examples**: References to guide the implementation
|
||||
|
||||
<Note>
|
||||
**Want to master the art of prompting?**
|
||||
|
||||
Deep dive into **Module 1: "Prompting"** in [Cline Learn](https://clinelearn.com) to become an expert at creating effective prompts. The module covers:
|
||||
- Structured prompting techniques
|
||||
- Context optimization strategies
|
||||
- Common prompting patterns
|
||||
- Advanced prompt engineering
|
||||
- Real-world examples and exercises
|
||||
|
||||
Good prompting skills lead to faster task completion, more accurate results, fewer iterations needed, and better code quality.
|
||||
</Note>
|
||||
|
||||
## Task Execution Modes
|
||||
|
||||
Cline operates in two distinct modes that help structure your workflow:
|
||||
|
||||
- **Plan Mode**: For information gathering, discussing approaches, and creating strategies without making changes
|
||||
- **Act Mode**: For actual implementation where Cline executes file modifications, runs commands, and uses tools
|
||||
|
||||
→ **[Learn more about Plan and Act modes](/features/plan-and-act)** to understand when and how to use each mode effectively.
|
||||
|
||||
## Task Resources
|
||||
|
||||
Each task consumes resources that are tracked:
|
||||
|
||||
- **Tokens**: The amount of text processed (input and output)
|
||||
- **API Costs**: Monetary cost based on the model and token usage
|
||||
- **Time**: Duration from start to completion
|
||||
- **Checkpoints**: Number of file state snapshots created
|
||||
|
||||
## Common Task Patterns
|
||||
|
||||
### Code Generation
|
||||
```
|
||||
Create a TypeScript function that validates email addresses using regex.
|
||||
Include unit tests using Jest and handle edge cases like international domains.
|
||||
```
|
||||
|
||||
### Bug Fixing
|
||||
```
|
||||
@terminal The app crashes when clicking the submit button.
|
||||
Fix the error and ensure proper error handling is in place.
|
||||
```
|
||||
|
||||
### Refactoring
|
||||
```
|
||||
Refactor the authentication logic in @auth.ts to use async/await
|
||||
instead of callbacks. Maintain all existing functionality.
|
||||
```
|
||||
|
||||
### Feature Implementation
|
||||
```
|
||||
Add a dark mode toggle to the settings page. Use the existing theme
|
||||
context and persist the preference to localStorage.
|
||||
```
|
||||
|
||||
## Task Resumption
|
||||
|
||||
One of Cline's powerful features is the ability to resume interrupted tasks:
|
||||
|
||||
### When Tasks Get Interrupted
|
||||
|
||||
- You stop a long-running task
|
||||
- An error occurs that needs intervention
|
||||
- You need to switch to another task
|
||||
|
||||
### Resuming a Task
|
||||
|
||||
1. Open the task from history
|
||||
2. Cline loads the complete conversation
|
||||
3. File states are checked against checkpoints
|
||||
4. The task continues with awareness of the interruption
|
||||
5. You can provide additional context if needed
|
||||
|
||||
## Understanding Task Context
|
||||
|
||||
Tasks maintain context throughout their lifecycle:
|
||||
|
||||
- **Conversation History**: All previous messages and responses
|
||||
- **File Changes**: Tracked modifications and their order
|
||||
- **Tool Results**: Output from commands and operations
|
||||
- **Checkpoint States**: Snapshots of file states at key points
|
||||
|
||||
This context allows Cline to:
|
||||
- Understand what has been done
|
||||
- Maintain consistency in approach
|
||||
- Resume work intelligently
|
||||
- Learn from previous attempts
|
||||
|
||||
→ **[Learn more about Context Management](/getting-started/understanding-context-management)** to understand how Cline manages and optimizes context across tasks.
|
||||
|
||||
Understanding how tasks work is fundamental to using Cline effectively. With well-crafted prompts and an understanding of the task lifecycle, you can leverage Cline's full potential to accelerate your development workflow.
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
title: "For New Coders"
|
||||
description: "Welcome to Cline, your AI-powered coding companion! This guide will help you quickly set up your development environment and begin your coding journey with ease."
|
||||
---
|
||||
|
||||
> **Tip:** If you're completely new to coding, take your time with each step. There's no rush — Cline is here to guide you!
|
||||
|
||||
### Getting Started
|
||||
|
||||
Before you jump into coding, make sure you have these essentials ready:
|
||||
|
||||
#### 1. **VS Code**
|
||||
|
||||
A popular, free, and powerful code editor.
|
||||
|
||||
- [<u>Download VS Code</u>](https://code.visualstudio.com/)
|
||||
|
||||
**Recommended YouTube Tutorial:** [<u>How to Install VS Code</u>](https://www.youtube.com/watch?v=MlIzFUI1QGA)
|
||||
|
||||
> **Pro Tip:** Install VS Code in your Applications folder (macOS) or Program Files (Windows) for easy access from your dock or start menu.
|
||||
|
||||
#### 2. **Organize Your Projects**
|
||||
|
||||
Create a dedicated folder named `Cline` in your Documents folder for all your coding projects:
|
||||
|
||||
- **macOS:** `/Users/[your-username]/Documents/Cline`
|
||||
- **Windows:** `C:\Users\[your-username]\Documents\Cline`
|
||||
|
||||
Inside your `Cline` folder, structure projects clearly:
|
||||
|
||||
- `Documents/Cline/workout-app` _(e.g., for a fitness tracking app)_
|
||||
- `Documents/Cline/portfolio-website` _(e.g., to showcase your work)_
|
||||
|
||||
> **Tip:** Keeping your projects organized from the start will save you time and confusion later!
|
||||
|
||||
#### 3. **Install the Cline VS Code Extension**
|
||||
|
||||
Enhance your coding workflow by installing the Cline extension directly within VS Code:
|
||||
|
||||
- Get Started with Cline Extension Tutorial
|
||||
|
||||
**Recommended YouTube Tutorial:** [<u>How To Install Extensions in VS Code</u>](https://www.youtube.com/watch?v=E7trgwZa-mk)
|
||||
|
||||
> **Pro Tip:** After installing, reload VS Code to ensure the extension is activated properly.
|
||||
|
||||
#### 4. **Essential Development Tools**
|
||||
|
||||
Basic software required for coding efficiently:
|
||||
|
||||
- Homebrew (macOS)
|
||||
- Node.js
|
||||
- Git
|
||||
|
||||
[<u>Follow our detailed guide on Installing Essential Development Tools with step-by-step help from Cline.</u>](https://docs.cline.bot/getting-started/installing-dev-essentials#installing-dev-essentials)
|
||||
|
||||
**Recommended YouTube Tutorials for Manual Installation:**
|
||||
|
||||
- **For macOS:**
|
||||
- [<u>Install Homebrew on Mac</u>](https://www.youtube.com/watch?v=hwGNgVbqasc)
|
||||
- [<u>Install Git on macOS 2024</u>](https://www.youtube.com/watch?v=B4qsvQ5IqWk)
|
||||
- [<u>Install Node.js on Mac (M1 | M2 | M3)</u>](https://www.youtube.com/watch?v=I8H4wolRFBk)
|
||||
- **For Windows:**
|
||||
- [<u>Install Git on Windows 10/11 (2024)</u>](https://www.youtube.com/watch?v=yjxv1HuRQy0)
|
||||
- [<u>Install Node.js in Windows 10/11</u>](https://www.youtube.com/watch?v=uCgAuOYpJd0)
|
||||
|
||||
> **Note:** If you run into permission issues during installation, try running your terminal or command prompt as an administrator.
|
||||
|
||||
You're all set! Dive in and start coding smarter and faster with **Cline**.
|
||||
@@ -1,54 +1,89 @@
|
||||
---
|
||||
title: "Installing Cline"
|
||||
description: "Get Cline set up in your editor and start building projects with AI assistance."
|
||||
description: "Get Cline up and running in your favorite IDE with these simple installation steps"
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
<Note>
|
||||
**Ready to get started?** Installation takes less than 2 minutes! Choose your editor below and follow the simple steps.
|
||||
</Note>
|
||||
|
||||
Before installing Cline, make sure you have the following:
|
||||
## Before You Begin
|
||||
|
||||
### Create a Cline Account
|
||||
|
||||
Create a Cline account for the best experience. Creating a Cline account is completely free and you can [sign up here](https://app.cline.bot/signup). A Cline account provides:
|
||||
- Access to multiple AI models including stealth models
|
||||
- Seamless setup without needing to manage API keys
|
||||
- At times, we partner with model providers to offer inferencing at no cost through your Cline account
|
||||
|
||||
### Compatible Editor
|
||||
|
||||
Cline works with the following IDEs:
|
||||
- **VS Code** - Microsoft's popular code editor
|
||||
- **Cursor** - AI-powered code editor based on VS Code
|
||||
- **JetBrains IDEs** - IntelliJ IDEA, PyCharm, WebStorm, DataSpell, PhpStorm, and other JetBrains products
|
||||
- **VSCodium** - Open-source version of VS Code
|
||||
- **Windsurf** - VS Code-compatible editor
|
||||
|
||||
Make sure you have one of these editors installed before proceeding with the Cline installation.
|
||||
|
||||
## Choose Your Editor
|
||||
|
||||
Cline works across multiple IDEs. Select your preferred editor below for installation instructions:
|
||||
<CardGroup cols={1}>
|
||||
<Card title="Create Your Account" icon="user-plus" href="https://app.cline.bot/signup">
|
||||
Sign up for a **free Cline account** to get:
|
||||
- Access to multiple AI models including stealth models
|
||||
- Seamless setup without managing API keys
|
||||
- Occasional free inferencing through partner providers
|
||||
</Card>
|
||||
|
||||
<Card title="Have a Compatible Editor" icon="code">
|
||||
Cline works with:
|
||||
- **VS Code** / **Cursor**
|
||||
- **JetBrains IDEs** (IntelliJ, PyCharm, WebStorm, etc.)
|
||||
- **VSCodium** / **Windsurf**
|
||||
|
||||
Install one before proceeding.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Installation Instructions
|
||||
<Tabs>
|
||||
<Tab title="VS Code/Cursor" icon="code">
|
||||
### Installation Steps
|
||||
|
||||
1. **Open VS Code** and navigate to the Extensions view (`Ctrl/Cmd + Shift + X`)
|
||||
2. **Search for "Cline"** in the Extensions marketplace
|
||||
3. **Click Install** on the Cline extension
|
||||
|
||||
<Frame caption="VS Code marketplace with Cline extension ready to install">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(20).png"
|
||||
alt="VS Code marketplace showing Cline extension"
|
||||
/>
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/vscode.png"
|
||||
alt="VS Code logo"
|
||||
style={{ width: "100px", height: "auto", display: "block" }}
|
||||
/>
|
||||
</Frame>
|
||||
<Steps>
|
||||
<Step title="Open Extensions">
|
||||
Launch VS Code and open the Extensions view:
|
||||
- Press `Ctrl/Cmd + Shift + X`, or
|
||||
- Click the Extensions icon in the Activity Bar
|
||||
</Step>
|
||||
|
||||
4. **Access Cline** after installation:
|
||||
- Click the Cline icon in the Activity Bar, or
|
||||
- Use Command Palette (`Ctrl/Cmd + Shift + P`) → "Cline: Open In New Tab"
|
||||
<Step title="Search for Cline">
|
||||
Type **"Cline"** in the Extensions marketplace search bar
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/extension-installation.png" alt="VS Code marketplace showing Cline extension"
|
||||
/>
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
> **Note:** If VS Code shows "Running extensions might..." dialog, click "Allow". If you don't see the Cline icon, restart VS Code.
|
||||
<Step title="Install">
|
||||
Click the **Install** button on the Cline extension
|
||||
|
||||
<Frame caption="VS Code marketplace with Cline extension ready to install">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(20).png"
|
||||
alt="VS Code marketplace showing Cline extension"
|
||||
/>
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Access Cline">
|
||||
After installation completes:
|
||||
- Click the **Cline icon** in the Activity Bar, or
|
||||
- Open Command Palette (`Ctrl/Cmd + Shift + P`) → type **"Cline: Open In New Tab"**
|
||||
|
||||
<Frame caption="Cline opened in VSCode">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/after-installation.png"
|
||||
alt="Cline opened in VSCode"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
If VS Code shows "Running extensions might..." dialog, click **Allow**. If you don't see the Cline icon, restart VS Code.
|
||||
</Warning>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Check>
|
||||
**Installation Complete!** You should now see the Cline interface in your editor. Time to sign in!
|
||||
</Check>
|
||||
|
||||
<Accordion title="Troubleshooting">
|
||||
|
||||
@@ -96,57 +131,113 @@ Cline works across multiple IDEs. Select your preferred editor below for install
|
||||
style={{ width: "200px", height: "auto", margin: "0 auto 20px auto", display: "block" }}
|
||||
/>
|
||||
|
||||
Cline for JetBrains works almost identically to Cline in VSCode. All the core features work properly: diff editing, using tools, logging in with different providers, MCP servers, Cline rules and workflows, and more.
|
||||
<Note>
|
||||
Cline for JetBrains works almost identically to VS Code, with all core features: diff editing, tools, multiple API providers, MCP servers, Cline rules/workflows, and more.
|
||||
</Note>
|
||||
|
||||
### Choose Your Installation Method
|
||||
|
||||
### Installation Steps
|
||||
<Tabs>
|
||||
<Tab title="From IDE (Recommended)">
|
||||
<Steps>
|
||||
<Step title="Open Settings">
|
||||
In your JetBrains IDE, go to **Settings**:
|
||||
- Windows/Linux: `Ctrl+Alt+S`
|
||||
- macOS: `Cmd+,`
|
||||
</Step>
|
||||
|
||||
**Method 1: From IDE (Recommended)**
|
||||
1. Open your JetBrains IDE
|
||||
2. Go to **Settings** (`Ctrl+Alt+S` on Windows/Linux, `Cmd+,` on macOS)
|
||||
3. Navigate to **Plugins** → **Marketplace**
|
||||
4. Search for "Cline" and click **Install**
|
||||
5. Restart your IDE
|
||||
<Step title="Navigate to Plugins">
|
||||
Go to **Plugins** → **Marketplace** tab
|
||||
</Step>
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-jetbrains-marketplace-search.png"
|
||||
alt="JetBrains marketplace showing Cline plugin search results"
|
||||
/>
|
||||
</Frame>
|
||||
<Step title="Install Cline">
|
||||
Search for **"Cline"** and click **Install**
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/jetbrains-installation.png"
|
||||
alt="JetBrains marketplace showing Cline plugin search results"
|
||||
/>
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
**Method 2: Browser Install**
|
||||
|
||||
Visit the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/28247-cline) and click **Install to IDE**.
|
||||
<Step title="Restart IDE">
|
||||
Restart your IDE to complete the installation
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/jetbrains-post-installation.png"
|
||||
alt="JetBrains marketplace showing Cline plugin search results"
|
||||
/>
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
|
||||
<Accordion title="Method 3: Manual Installation">
|
||||
<Tab title="Browser Install">
|
||||
<Steps>
|
||||
<Step title="Visit Marketplace">
|
||||
Go to the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/28247-cline)
|
||||
</Step>
|
||||
|
||||
1. Download the plugin from the [marketplace page](https://plugins.jetbrains.com/plugin/28247-cline)
|
||||
2. Go to **Settings** → **Plugins**
|
||||
3. Click the gear icon → **Install Plugin from Disk**
|
||||
4. Select the downloaded `.zip` file
|
||||
5. Restart your IDE
|
||||
<Step title="Click Install to IDE">
|
||||
Click the **Install to IDE** button
|
||||
</Step>
|
||||
|
||||
</Accordion>
|
||||
<Step title="Confirm in IDE">
|
||||
Your IDE will open and prompt you to confirm the installation
|
||||
</Step>
|
||||
|
||||
### Using the Plugin
|
||||
<Step title="Restart IDE">
|
||||
Restart to complete the installation
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
|
||||
After installation, you’ll find Cline in your IDE. Look for the Cline tool window (usually on the right side) or go to View → Tool Windows → Cline.
|
||||
<Tab title="Manual Install">
|
||||
<Steps>
|
||||
<Step title="Download Plugin">
|
||||
Download from the [marketplace page](https://plugins.jetbrains.com/plugin/28247-cline)
|
||||
</Step>
|
||||
|
||||
### Key Features
|
||||
<Step title="Open Settings">
|
||||
Go to **Settings** → **Plugins**
|
||||
</Step>
|
||||
|
||||
Cline for JetBrains includes all core features:
|
||||
- Diff editing and file modifications
|
||||
- Multiple API providers (Anthropic, OpenAI, local models)
|
||||
- MCP servers and custom tools
|
||||
- Cline rules and workflows
|
||||
- @ mentions for files, folders, and problems
|
||||
- Drag & drop support
|
||||
<Step title="Install from Disk">
|
||||
Click the gear icon → **Install Plugin from Disk**
|
||||
</Step>
|
||||
|
||||
> **Note:** Terminal output appears in collapsible sections rather than streaming directly to chat.
|
||||
<Step title="Select File & Restart">
|
||||
Select the downloaded `.zip` file and restart your IDE
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Key Differences from VSCode
|
||||
The terminal integration works differently in JetBrains. Unlike VSCode where terminal output streams directly to the chat, JetBrains shows command output in a collapsible section. Commands still execute successfully - you just need to expand the Command Output section to see results.
|
||||
<Check>
|
||||
**Installation Complete!** Find Cline in **View** → **Tool Windows** → **Cline** (usually on the right side).
|
||||
</Check>
|
||||
|
||||
### What Works in JetBrains
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="✅ All Core Features" defaultOpen>
|
||||
- Diff editing and file modifications
|
||||
- Multiple API providers (Anthropic, OpenAI, local models)
|
||||
- MCP servers and custom tools
|
||||
- Cline rules and workflows
|
||||
- @ mentions for files, folders, and problems
|
||||
- Drag & drop support
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="📋 Terminal Integration Difference">
|
||||
**JetBrains shows terminal output differently than VS Code:**
|
||||
- VS Code: Output streams directly to chat
|
||||
- JetBrains: Output appears in collapsible "Command Output" sections
|
||||
|
||||
Commands execute successfully in both—just expand the section to see results in JetBrains.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Accordion title="Troubleshooting">
|
||||
|
||||
@@ -188,17 +279,32 @@ Cline works across multiple IDEs. Select your preferred editor below for install
|
||||
</Tab>
|
||||
|
||||
<Tab title="VSCodium/Windsurf" icon="terminal">
|
||||
### Installation Steps
|
||||
<Note>
|
||||
These editors use the **Open VSX Registry** instead of the VS Code Marketplace, but the installation process is nearly identical.
|
||||
</Note>
|
||||
|
||||
For VS Code-compatible editors using Open VSX Registry:
|
||||
<Steps>
|
||||
<Step title="Open Extensions">
|
||||
Launch your editor (VSCodium, Windsurf, etc.) and open Extensions view:
|
||||
- Press `Ctrl/Cmd + Shift + X`
|
||||
</Step>
|
||||
|
||||
1. **Open your editor** (VSCodium, Windsurf, etc.)
|
||||
2. **Navigate to Extensions view** (`Ctrl/Cmd + Shift + X`)
|
||||
3. **Search for "Cline"** in the marketplace
|
||||
4. **Select "Cline" by saoudrizwan** and click **Install**
|
||||
5. **Reload** if prompted
|
||||
<Step title="Search for Cline">
|
||||
Type **"Cline"** in the marketplace search bar
|
||||
</Step>
|
||||
|
||||
> **Note:** These editors use the Open VSX Registry instead of the VS Code Marketplace.
|
||||
<Step title="Install by Author">
|
||||
Select **"Cline" by saoudrizwan** and click **Install**
|
||||
</Step>
|
||||
|
||||
<Step title="Reload if Needed">
|
||||
Reload your editor if prompted to complete installation
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Check>
|
||||
**Installation Complete!** Look for the Cline icon in your Activity Bar or use the Command Palette.
|
||||
</Check>
|
||||
|
||||
<Accordion title="Troubleshooting">
|
||||
|
||||
@@ -240,33 +346,64 @@ Cline works across multiple IDEs. Select your preferred editor below for install
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Sign In to Your Cline Account
|
||||
## Next Steps: Sign In & Start Building
|
||||
|
||||
Now that you have Cline installed, sign in to access your account:
|
||||
<Steps>
|
||||
<Step title="Open Cline">
|
||||
Find and open Cline in your editor:
|
||||
- **VS Code/Cursor/VSCodium/Windsurf:** Click the Cline icon in the Activity Bar
|
||||
- **JetBrains:** Go to **View** → **Tool Windows** → **Cline**
|
||||
</Step>
|
||||
|
||||
1. **Open Cline** in your editor (click the Cline icon in the Activity Bar or Tool Windows)
|
||||
2. **Click "Sign In"** - you'll see this button in the Cline interface
|
||||
3. **Complete authentication** - you'll be redirected to [app.cline.bot](https://app.cline.bot) to sign in
|
||||
4. **Return to your editor** - once signed in, you'll be automatically redirected back
|
||||
<Step title="Sign In">
|
||||
Click the **Sign Up** button in the Cline interface
|
||||
|
||||
<Info>
|
||||
You'll be redirected to [app.cline.bot](https://app.cline.bot) to authenticate. After signing in, you'll automatically return to your editor.
|
||||
</Info>
|
||||
<Frame>
|
||||
<img src="/assets/installation/login.png" alt="Cline sign up screen"
|
||||
/>
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="You're All Set!">
|
||||
<Check>
|
||||
**Congratulations!** You're all set to start using Cline!
|
||||
|
||||
Cline is now ready to help you build projects.
|
||||
</Check>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Your First Interaction with Cline
|
||||
## Tips for Success
|
||||
|
||||
You're ready to start building! Copy and paste this prompt into the Cline chat window:
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Ask Questions" icon="circle-question">
|
||||
Don't know something? Ask in Plan Mode! Cline can explain concepts, debug errors, and guide you through tasks.
|
||||
</Card>
|
||||
|
||||
<Card title="Use Screenshots" icon="image">
|
||||
Some models understand screenshots of what you're working on or errors you encounter.
|
||||
</Card>
|
||||
|
||||
<Card title="Share Error Messages" icon="triangle-exclamation">
|
||||
Use @problems to share error messages for quick solutions and debugging help.
|
||||
</Card>
|
||||
|
||||
<Card title="Speak Naturally" icon="comments">
|
||||
Use your own words—no need for technical jargon. Cline will translate your ideas into code.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
```
|
||||
Hey Cline! Could you help me create a new project folder called "hello-world" in my Cline directory and make a simple webpage that says "Hello World" in big blue text?
|
||||
```
|
||||
## Need Help?
|
||||
|
||||
> **Pro Tip:** Cline will help you create the project folder and set up your first webpage!
|
||||
|
||||
### Tips for Working with Cline
|
||||
|
||||
- **Ask Questions:** If you're unsure about something, ask Cline!
|
||||
- **Use Screenshots:** Cline can understand images — show him what you're working on.
|
||||
- **Copy and Paste Errors:** Share error messages in the chat for solutions.
|
||||
- **Speak Plainly:** Use your own words — Cline will translate them into code.
|
||||
|
||||
### Still Struggling?
|
||||
|
||||
Join our [Discord community](https://discord.gg/cline) and engage with our team and other Cline users directly.
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Join Our Discord" icon="discord" href="https://discord.gg/cline">
|
||||
Connect with our team and community for support, tips, and discussions.
|
||||
</Card>
|
||||
|
||||
<Card title="Read the Docs" icon="book-open" href="/getting-started/for-new-coders">
|
||||
Explore guides for new coders, model selection, and advanced features.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
title: "Installing Dev Essentials"
|
||||
description: >-
|
||||
When you start coding, you'll need some essential development tools installed
|
||||
on your computer. Cline can help you install everything you need in a safe,
|
||||
guided way.
|
||||
---
|
||||
|
||||
### The Essential Tools
|
||||
|
||||
Here are the core tools you'll need for development:
|
||||
|
||||
- **Node.js & npm:** Required for JavaScript and web development
|
||||
- **Git:** For tracking changes in your code and collaborating with others
|
||||
- **Package Managers:** Tools that make it easy to install other development tools
|
||||
- Homebrew for macOS
|
||||
- Chocolatey for Windows
|
||||
- apt/yum for Linux
|
||||
|
||||
> **Tip:** These tools are the foundation of your developer toolkit. Installing them properly will set you up for success!
|
||||
|
||||
### Let Cline Install Everything
|
||||
|
||||
Copy one of these prompts based on your operating system and paste it into **Cline**:
|
||||
|
||||
#### For macOS
|
||||
|
||||
```
|
||||
Hello Cline! I need help setting up my Mac for software development. Could you please help me install the essential development tools like Homebrew, Node.js, Git, and any other core utilities that are commonly needed for coding? I'd like you to guide me through the process step-by-step.
|
||||
```
|
||||
|
||||
#### For Windows
|
||||
|
||||
```
|
||||
Hello Cline! I need help setting up my Windows PC for software development. Could you please help me install the essential development tools like Node.js, Git, and any other core utilities that are commonly needed for coding? I'd like you to guide me through the process step-by-step.
|
||||
```
|
||||
|
||||
#### For Linux
|
||||
|
||||
```
|
||||
Hello Cline! I need help setting up my Linux system for software development. Could you please help me install the essential development tools like Node.js, Git, and any other core utilities that are commonly needed for coding? I'd like you to guide me through the process step-by-step.
|
||||
```
|
||||
|
||||
> **Pro Tip:** Cline will show you each command before running it. You stay in control the entire time!
|
||||
|
||||
### What Will Happen
|
||||
|
||||
Cline will guide you through the following steps:
|
||||
|
||||
1. Installing the appropriate package manager for your system
|
||||
2. Using the package manager to install Node.js and Git
|
||||
3. Showing you the exact command before it runs (you approve each step!)
|
||||
4. Verifying each installation is successful
|
||||
|
||||
> **Note:** You might need to enter your computer's password for some installations. This is normal!
|
||||
|
||||
### Why These Tools Are Important
|
||||
|
||||
- **Node.js & npm:**
|
||||
- Build websites with frameworks like React or Next.js
|
||||
- Run JavaScript code
|
||||
- Install JavaScript packages
|
||||
- **Git:**
|
||||
- Save different versions of your code
|
||||
- Collaborate with other developers
|
||||
- Back up your work
|
||||
- **Package Managers:**
|
||||
- Quickly install and update development tools
|
||||
- Keep your environment organized and up to date
|
||||
|
||||
### Notes
|
||||
|
||||
> **Tip:** The installation process is interactive — Cline will guide you step by step!
|
||||
|
||||
- All commands are shown to you for approval before they run.
|
||||
- If you run into any issues, Cline will help troubleshoot them.
|
||||
- You may need to enter your computer's password for certain steps.
|
||||
|
||||
### Additional Tips for New Coders
|
||||
|
||||
#### Understanding the Terminal
|
||||
|
||||
The Terminal is an application where you can type commands to interact with your computer.
|
||||
|
||||
- **macOS:** Open it by searching for "Terminal" in Spotlight.
|
||||
- **Example:**
|
||||
|
||||
```
|
||||
$ open -a Terminal
|
||||
```
|
||||
|
||||
#### Understanding VS Code Features
|
||||
|
||||
- **Terminal in VS Code:** Run commands directly from within VS Code!
|
||||
- Go to **View > Terminal** or press \`Ctrl + \`\`.
|
||||
- Example:
|
||||
|
||||
```
|
||||
$ node -v
|
||||
v16.14.0
|
||||
```
|
||||
|
||||
- **Document View:** Where you edit your code files.
|
||||
- Open files from the Explorer panel on the left.
|
||||
- **Problems Section:** View errors or warnings in your code.
|
||||
- Access it by clicking the lightbulb icon or **View > Problems**.
|
||||
|
||||
#### Common Features
|
||||
|
||||
- **Command Line Interface (CLI):** A powerful tool for running commands.
|
||||
- **Permissions:** You might need to grant permissions to certain commands — this keeps your system secure.
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
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.
|
||||
|
||||
## Current Top Models
|
||||
|
||||
| Model | Context Window | Input Price* | Output Price* | Best For |
|
||||
|-------|---------------|--------------|---------------|----------|
|
||||
| **Claude Sonnet 4.5** | 1M tokens | $3-6 | $15-22.50 | Reliable tool usage, complex codebases |
|
||||
| **Qwen3 Coder** | 256K tokens | $0.20 | $0.80 | Coding tasks, open source flexibility |
|
||||
| **Gemini 2.5 Pro** | 1M+ tokens | TBD | TBD | Large codebases, document analysis |
|
||||
| **GPT-5** | 400K tokens | $1.25 | $10 | Latest OpenAI tech, three modes |
|
||||
|
||||
*Per million tokens
|
||||
|
||||
## Budget Options
|
||||
|
||||
| Model | Context Window | Input Price* | Output Price* | Notes |
|
||||
|-------|---------------|--------------|---------------|-------|
|
||||
| **DeepSeek V3** | 128K tokens | $0.14 | $0.28 | Great value for daily coding |
|
||||
| **DeepSeek R1** | 128K tokens | $0.55 | $2.19 | Budget reasoning champion |
|
||||
| **Qwen3 32B** | 128K tokens | Varies | Varies | Open source, multiple providers |
|
||||
| **Z AI GLM 4.5** | 128K tokens | TBD | TBD | MIT licensed, hybrid reasoning |
|
||||
|
||||
*Per million tokens
|
||||
|
||||
|
||||
## Context Window Guide
|
||||
|
||||
| Size | Word Count | Use Case |
|
||||
|------|------------|----------|
|
||||
| 32K tokens | ~24,000 words | Single files, small projects |
|
||||
| 128K tokens | ~96,000 words | Most coding projects |
|
||||
| 200K tokens | ~150,000 words | Large codebases |
|
||||
| 400K+ tokens | ~300,000+ words | Entire applications |
|
||||
|
||||
**Performance note**: Most models start dropping in quality around 400-500K tokens, even if they claim higher limits.
|
||||
|
||||
## 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.
|
||||
|
||||
## Context Management
|
||||
|
||||
Cline automatically handles context limits with [auto-compact](/features/auto-compact). When you approach your model's limit, Cline summarizes the conversation to keep working. You don't need to micromanage this.
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
Start with **Claude Sonnet 4.5** if you want reliability. Experiment with **open source options** once you're comfortable to find the best fit for your workflow and budget.
|
||||
|
||||
The landscape moves fast - these recommendations reflect what's working now, but keep an eye on new releases.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: "Selecting Your Model"
|
||||
description: "Get started with your first AI model in Cline"
|
||||
---
|
||||
|
||||
Cline needs an AI model to understand your requests and write code. Think of it like choosing which expert to work with - different models have different strengths and costs.
|
||||
|
||||
## Quick Start: Choose Your Provider
|
||||
|
||||
The easiest way to get started is with **Cline** as your provider:
|
||||
|
||||
1. **Open Cline Settings**: Click the gear icon (⚙️) in the top-right corner of Cline's chat
|
||||
2. **Select "Cline"** from the API Provider dropdown
|
||||
3. **Choose a model** from the dropdown - we recommend starting with **Claude Sonnet 4.5** or **DeepSeek V3**
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/step2-provider.png" alt="Select Cline Provider" />
|
||||
</Frame>
|
||||
|
||||
**That's it!** No API keys to manage, and you'll get access to multiple models.
|
||||
|
||||
<Tip>
|
||||
**Free models available**: Cline occasionally offers free inferencing through partner providers. When available, you'll see these options in your model dropdown.
|
||||
</Tip>
|
||||
|
||||
## Alternative: Use Another Provider
|
||||
|
||||
If you prefer to use your own API keys, you can select from providers like:
|
||||
|
||||
- **OpenRouter** - Great value, multiple models
|
||||
- **Anthropic** - Direct access to Claude models
|
||||
- **OpenAI** - Access to GPT models
|
||||
- **Google Gemini** - Google's AI models
|
||||
- **Ollama** - Run models locally on your computer
|
||||
|
||||
After selecting a provider, you'll need to:
|
||||
1. Get an API key from their website
|
||||
2. Paste it into the API Key field in Cline settings
|
||||
3. Choose your model
|
||||
|
||||
<Note>
|
||||
Most providers require payment information before generating API keys.
|
||||
</Note>
|
||||
|
||||
## Which Model Should I Choose?
|
||||
|
||||
If you're just getting started, we recommend:
|
||||
|
||||
| Your Priority | Choose This Model | Why |
|
||||
|---------------|-------------------|-----|
|
||||
| **Reliability** | Claude Sonnet 4.5 | Most reliable for coding tasks |
|
||||
| **Value** | DeepSeek V3 | Great performance at low cost |
|
||||
| **Speed** | Qwen3 Coder | Fast responses |
|
||||
| **Privacy** | Any Ollama model | Runs on your computer |
|
||||
|
||||
You can switch models anytime without losing your conversation.
|
||||
|
||||
## Next Steps
|
||||
|
||||
With your model configured, you're all set! In the next section, we'll walk you through completing your first task with Cline and show you how to interact with the AI to write, debug, and refactor code.
|
||||
|
||||
<Card title="Deep Dive: Model Selection Guide" icon="graduation-cap" href="/core-features/model-selection-guide">
|
||||
Want to understand model pricing, context windows, and advanced selection strategies? Check out our comprehensive Model Selection Guide.
|
||||
</Card>
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
title: "Task Management in Cline"
|
||||
description: "Learn how to effectively manage your task history, use favorites, and organize your work in Cline."
|
||||
---
|
||||
|
||||
# Task Management
|
||||
|
||||
As you use Cline, you'll accumulate many tasks over time. The task management system helps you organize, filter, search, and clean up your task history to keep your workspace efficient.
|
||||
|
||||
## Accessing Task History
|
||||
|
||||
You can access your task history by:
|
||||
|
||||
1. Clicking on the "History" button in the Cline sidebar
|
||||
2. Using the command palette to search for "Cline: Show Task History"
|
||||
|
||||
## Task History Features
|
||||
|
||||
The task history view provides several powerful features:
|
||||
|
||||
### Searching and Filtering
|
||||
|
||||
- **Search Bar**: Use the fuzzy search at the top to quickly find tasks by content
|
||||
- **Sort Options**: Sort tasks by:
|
||||
- Newest (default)
|
||||
- Oldest
|
||||
- Most Expensive (highest API cost)
|
||||
- Most Tokens (highest token usage)
|
||||
- Most Relevant (when searching)
|
||||
- **Favorites Filter**: Toggle to show only favorited tasks
|
||||
|
||||
### Task Actions
|
||||
|
||||
Each task in the history view has several actions available:
|
||||
|
||||
- **Open**: Click on a task to reopen it in the Cline chat
|
||||
- **Favorite**: Click the star icon to mark a task as a favorite
|
||||
- **Delete**: Remove individual tasks (favorites are protected from deletion)
|
||||
- **Export**: Export a task's conversation to markdown
|
||||
|
||||
## ⭐ Task Favorites
|
||||
|
||||
The favorites feature allows you to mark important tasks that you want to preserve and find quickly.
|
||||
|
||||
### How Favorites Work
|
||||
|
||||
- **Marking Favorites**: Click the star icon next to any task to toggle its favorite status
|
||||
- **Protection**: Favorited tasks are protected from individual and bulk deletion operations (can be overridden)
|
||||
- **Filtering**: Use the favorites filter to quickly access your important tasks
|
||||
|
||||
## Batch Operations
|
||||
|
||||
The task history view supports several batch operations:
|
||||
|
||||
- **Select Multiple**: Use the checkboxes to select multiple tasks
|
||||
- **Select All/None**: Quickly select or deselect all tasks
|
||||
- **Delete Selected**: Remove all selected tasks
|
||||
- **Delete All**: Remove all tasks from history (favorites are preserved unless you choose to include them)
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Favorite Important Tasks**: Mark reference tasks or frequently accessed conversations as favorites
|
||||
2. **Regular Cleanup**: Periodically remove old or unused tasks to improve performance
|
||||
3. **Use Search**: Leverage the fuzzy search to quickly find specific conversations
|
||||
4. **Export Valuable Tasks**: Export important tasks to markdown for external reference
|
||||
|
||||
Task management helps you maintain an organized workflow when using Cline, allowing you to quickly find past conversations, preserve important work, and keep your history clean and efficient.
|
||||
@@ -1,196 +0,0 @@
|
||||
---
|
||||
title: "Context Management"
|
||||
description: "Context is key to getting the most out of Cline"
|
||||
---
|
||||
|
||||
> **Quick Reference**
|
||||
>
|
||||
> - Context = The information Cline knows about your project
|
||||
> - Context Window = How much information Cline can hold at once
|
||||
> - Use context files to maintain project knowledge
|
||||
> - Reset when the context window gets full
|
||||
|
||||
## Understanding Context & Context Windows
|
||||
|
||||
<Frame caption="In a world of infinite context, the context window is what Cline currently has available">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2).png"
|
||||
alt="In a world of infinite context, the context window is what Cline currently has available"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
Think of working with Cline like collaborating with a thorough, proactive teammate:
|
||||
|
||||
### How Context is Built
|
||||
|
||||
Cline actively builds context in two ways:
|
||||
|
||||
1. **Automatic Context Gathering (i.e. Cline-driven)**
|
||||
- Proactively reads related files
|
||||
- Explores project structure
|
||||
- Analyzes patterns and relationships
|
||||
- Maps dependencies and imports
|
||||
- Asks clarifying questions
|
||||
2. **User-Guided Context**
|
||||
- Share specific files
|
||||
- Provide documentation
|
||||
- Answer Cline's questions
|
||||
- Guide focus areas
|
||||
- Share design thoughts and requirements
|
||||
|
||||
**Key Point**: Cline isn't passive - it actively seeks to understand your project. You can either let it explore or guide its focus, especially in [Plan Mode](/features/plan-and-act).
|
||||
|
||||
### Context & Context Windows
|
||||
|
||||
Think of context like a whiteboard you and Cline share:
|
||||
|
||||
- **Context** is all the information available:
|
||||
- What Cline has discovered
|
||||
- What you've shared
|
||||
- Your conversation history
|
||||
- Project requirements
|
||||
- Previous decisions
|
||||
- **Context Window** is the size of the whiteboard itself:
|
||||
- Measured in tokens (1 token ≈ 3/4 of an English word)
|
||||
- Each model has a fixed size:
|
||||
- Claude Sonnet 4.5: 1,000,000 tokens
|
||||
- Qwen3 Coder: 256,000 tokens
|
||||
- Gemini 2.5 Pro: 1,000,000+ tokens
|
||||
- GPT-5: 400,000 tokens
|
||||
- When the whiteboard is full, Cline automatically summarizes the conversation to free up space
|
||||
|
||||
**Important**: Having a large context window doesn't mean you should fill it completely. Models start degrading around 400-500K tokens even if they claim higher limits. Just like a cluttered whiteboard, too much information can make it harder to focus on what's important.
|
||||
|
||||
## Understanding the Context Window Progress Bar
|
||||
|
||||
Cline provides a visual way to monitor your context window usage through a progress bar:
|
||||
|
||||
<Frame caption="Visual representation of the context window usage">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1)%20(1).png"
|
||||
alt="Context window progress bar"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### Reading the Bar
|
||||
|
||||
- ↑ shows input tokens (what you've sent to the LLM)
|
||||
- ↓ shows output tokens (what the LLM has generated)
|
||||
- The progress bar visualizes how much of your context window you've used
|
||||
- The total shows your model's maximum capacity (e.g., 1M for Claude Sonnet 4.5)
|
||||
|
||||
### When to Watch the Bar
|
||||
|
||||
- During long coding sessions
|
||||
- When working with multiple files
|
||||
- Before starting complex tasks
|
||||
- When Cline seems to lose context
|
||||
|
||||
**Tip**: With [Auto Compact](/features/auto-compact), Cline can now handle long conversations automatically. When combined with [Focus Chain](/features/focus-chain), you can work on complex projects that span multiple context windows without losing progress.
|
||||
|
||||
## Automatic Context Management
|
||||
|
||||
Cline includes intelligent features to manage context automatically:
|
||||
|
||||
### Default Settings You Should Keep On
|
||||
|
||||
**Focus Chain** - Enabled by default in v3.25. Cline generates a todo list at task start and keeps it in context so the thread doesn't drift. You can edit the markdown to add or reorder steps and Cline will adapt. [Learn more about Focus Chain](/features/focus-chain).
|
||||
|
||||
**Auto Compact** - Always on. As the context window reaches its limit, Cline creates a comprehensive summary, replaces the bloated history, and continues where it left off. Decisions, code changes, and state are preserved. [Learn more about Auto Compact](/features/auto-compact).
|
||||
|
||||
## Advanced Context Tools
|
||||
|
||||
When you need more control over context management:
|
||||
|
||||
### Deep Planning (`/deep-planning`)
|
||||
For substantial features, refactors, or integrations. Cline investigates your codebase, asks targeted questions, then writes `implementation_plan.md`. It creates a fresh task with distilled, high-value context. [Learn more about Deep Planning](/features/slash-commands/deep-planning).
|
||||
|
||||
### New Task (`/newtask`)
|
||||
At natural transition points, packages only what matters into a fresh task. Clean slate for implementation after research, or crisp handoff between teammates. [Learn more about New Task](/features/slash-commands/new-task).
|
||||
|
||||
### Smol (`/smol`)
|
||||
Compress the conversation in place to keep momentum. Ideal during debugging or exploratory work when you don't want to break flow. [Learn more about Smol](/features/slash-commands/smol).
|
||||
|
||||
### Memory Bank + .clinerules
|
||||
For non-trivial projects. The Memory Bank captures project knowledge as Markdown in your repo. `.clinerules` are version-controlled instructions that align Cline's behavior with your team. [Learn more about Memory Bank](/prompting/cline-memory-bank) and [Cline Rules](/features/cline-rules).
|
||||
|
||||
## Working with Context Files
|
||||
|
||||
Context files help maintain understanding across sessions. They serve as documentation specifically designed to help AI assistants understand your project.
|
||||
|
||||
#### Approaches to Context Files
|
||||
|
||||
1. **Evergreen Project Context (Memory Bank)**
|
||||
- Living documentation that evolves with your project
|
||||
- Updated as architecture and patterns emerge
|
||||
- Example: The Memory Bank pattern maintains files like `techContext.md` and `systemPatterns.md`
|
||||
- Useful for long-running projects and teams
|
||||
2. **Task-Specific Context**
|
||||
|
||||
- Created for specific implementation tasks
|
||||
- Document requirements, constraints, and decisions
|
||||
- Example:
|
||||
|
||||
```markdown
|
||||
# auth-system-implementation.md
|
||||
|
||||
## Requirements
|
||||
|
||||
- OAuth2 implementation
|
||||
- Support for Google and GitHub
|
||||
- Rate limiting on auth endpoints
|
||||
|
||||
## Technical Decisions
|
||||
|
||||
- Using Passport.js for provider integration
|
||||
- JWT for session management
|
||||
- Redis for rate limiting
|
||||
```
|
||||
|
||||
3. **Knowledge Transfer Docs**
|
||||
- Switch to plan mode and ask Cline to document everything you've accomplished so far, along with the remaining steps, in a markdown file.
|
||||
- Copy the contents of the markdown file.
|
||||
- Start a new task using that content as context.
|
||||
|
||||
#### Using Context Files Effectively
|
||||
|
||||
1. **Structure and Format**
|
||||
- Use clear, consistent organization
|
||||
- Include relevant examples
|
||||
- Link related concepts
|
||||
- Keep information focused
|
||||
2. **Maintenance**
|
||||
- Update after significant changes
|
||||
- Version control your context files
|
||||
- Remove outdated information
|
||||
- Document key decisions
|
||||
|
||||
## Practical Tips
|
||||
|
||||
1. **Starting New Projects**
|
||||
- Let Cline explore the codebase
|
||||
- Answer its questions about structure and patterns
|
||||
- Consider setting up basic context files
|
||||
- Document key design decisions
|
||||
2. **Ongoing Development**
|
||||
- Update context files with significant changes
|
||||
- Share relevant documentation
|
||||
- Use Plan mode for complex discussions
|
||||
- Start fresh sessions when needed
|
||||
3. **Team Projects**
|
||||
- Share common context files (consider using [.clinerules](/features/cline-rules) files in project roots)
|
||||
- Document architectural decisions
|
||||
- Maintain consistent patterns
|
||||
- Keep documentation current
|
||||
|
||||
## Bonus Context Tips
|
||||
|
||||
- You can @ links and have the webpage's context added to Cline (docs, blogs, etc.)
|
||||
- Utilize MCP servers to pull in context from your external knowledge bases
|
||||
- Screenshots can be used as context for models that support image inputs
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
Cline already does a lot of context work for you - [Focus Chain](/features/focus-chain), [Auto Compact](/features/auto-compact), and the planning flow are designed to keep the thread intact across long horizons. The goal is to help Cline maintain consistent understanding of your project across sessions.
|
||||
|
||||
Remember: The goal is to keep only what matters in view, at every step.
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
title: "What is Cline?"
|
||||
description: "An introduction to Cline, your AI-powered development assistant for modern IDEs."
|
||||
---
|
||||
|
||||
Cline is an open source AI coding agent that brings frontier AI models directly to your IDE. Unlike autocomplete tools, Cline is a true coding agent that can understand entire codebases, plan complex changes, and execute multi-step tasks.
|
||||
|
||||
## Open Source AI Coding, Uncompromised
|
||||
|
||||
Cline gives you direct, transparent access to frontier AI with no limits, no surprises, and no model ecosystem lock-in. See every decision. Choose any model. Control your costs.
|
||||
|
||||
### Complete Transparency
|
||||
|
||||
Watch in real-time as Cline reads files, considers approaches, and proposes changes. Every decision is visible, every edit reviewable before it's made. This isn't just "explainable AI" - it's complete transparency.
|
||||
|
||||
### Your Models, Your Control
|
||||
|
||||
Use Claude for complex reasoning, Gemini for massive contexts, or Qwen3 Coder for efficiency. Switch instantly as new models launch. Your API keys, your choice. No gatekeeping innovation.
|
||||
|
||||
### Built for Real Engineering
|
||||
|
||||
Cline can:
|
||||
- **Read and write files** across your entire codebase
|
||||
- **Execute terminal commands** and debug errors
|
||||
- **Plan complex features** before writing code
|
||||
- **Connect to external systems** through MCP servers
|
||||
- **Understand large codebases** with intelligent context management
|
||||
|
||||
## Plan & Act Mode
|
||||
|
||||
Cline explores your codebase and works with you to create comprehensive plans before writing a single line of code, ensuring it understands the full context of your project.
|
||||
|
||||
**Plan Mode** for complex tasks - Cline explores, asks questions, and creates detailed implementation plans.
|
||||
|
||||
**Act Mode** for execution - Cline implements the plan with full transparency and control.
|
||||
|
||||
## Zero Trust by Design
|
||||
|
||||
Your code never touches our servers. Cline runs entirely client-side with your API keys, making it the only option for enterprises with strict security requirements.
|
||||
|
||||
**Open source** means your security team can review every line. See exactly how Cline works, what it sends to AI providers, and how decisions are made.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Focus Chain
|
||||
Automatic todo list management with real-time progress tracking throughout your tasks. Keeps Cline on track across long projects.
|
||||
|
||||
### Auto Compact
|
||||
When conversations get long, Cline automatically summarizes to preserve context while freeing up space to continue working.
|
||||
|
||||
### Deep Planning
|
||||
For complex features, Cline investigates your codebase, asks clarifying questions, and creates comprehensive implementation plans.
|
||||
|
||||
### MCP Integration
|
||||
Connect to databases, APIs, and documentation through the Model Context Protocol. Cline becomes your bridge to any external system.
|
||||
|
||||
### .clinerules
|
||||
Define project-specific instructions that Cline follows including coding standards, architecture patterns, or team conventions.
|
||||
|
||||
## Why Developers Choose Cline
|
||||
|
||||
**100% Open Source** - Every line of code on GitHub. 48k+ stars from developers who've read it, improved it, and trust it with their work.
|
||||
|
||||
**No Inference Games** - We don't profit from AI usage. While others limit context or route to cheaper models, we give you unrestricted access to any model's full capabilities.
|
||||
|
||||
**Future-Proof by Design** - New model released? Use it immediately. Cline works with any AI provider, any model.
|
||||
|
||||
**True Visibility** - See every file read, every decision considered, every token used.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Ready to experience AI coding without limits? [Install Cline](/getting-started/installing-cline) for your preferred IDE and start with our [Model Selection Guide](/getting-started/model-selection-guide) to choose the right AI model for your needs.
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: "Build Your First Project"
|
||||
description: "Build your first project with Cline in under a minute."
|
||||
---
|
||||
|
||||
Ready to see Cline in action? This hands-on tutorial will walk you through building a website in under a minute. You'll experience how Cline understands your requirements, creates files, and iterates on your feedback—all through natural conversation.
|
||||
|
||||
By the end of this guide, you'll have built a working website and learned the fundamentals of working with Cline.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Cline installed** in your editor ([Install Guide](/getting-started/installing-cline))
|
||||
- **AI model selected** ([Model Setup](/getting-started/selecting-your-model))
|
||||
- **Any folder open** in your editor (or create a new empty folder)
|
||||
|
||||
## Step 1: Open Cline
|
||||
|
||||
Click the Cline icon in your editor's sidebar (left side). The chat panel will open.
|
||||
|
||||
<Info>
|
||||
**Quick Tip:** You can also use `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) and search for "Cline: Open In New Tab"
|
||||
</Info>
|
||||
|
||||
## Step 2: Give Cline a Task
|
||||
|
||||
Copy and paste this prompt into Cline's chat:
|
||||
|
||||
```
|
||||
Create a simple website in a single HTML file. It should have:
|
||||
- A welcome message saying "Hello from Cline!"
|
||||
- A colorful gradient background
|
||||
- A button that cycles through different color themes when clicked
|
||||
- Modern, clean design
|
||||
- All CSS and JavaScript should be included in the same HTML file
|
||||
```
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/chat-prompt.png" alt="Cline Chat Prompt"/>
|
||||
</Frame>
|
||||
|
||||
Press Enter and watch Cline work!
|
||||
|
||||
## Step 3: What Happens Next
|
||||
|
||||
Cline will:
|
||||
|
||||
1. **Create a single file:**
|
||||
- `index.html` - A complete webpage with embedded CSS and JavaScript
|
||||
|
||||
2. **Ask for approval** (unless you've enabled auto-approve)
|
||||
- Click "Approve" to let Cline create the file
|
||||
- You can review what it plans to do first
|
||||
|
||||
3. **Complete the task** within seconds
|
||||
|
||||
## Step 4: View Your Website
|
||||
|
||||
Once Cline finishes:
|
||||
|
||||
1. **Find `index.html`** in your editor's file explorer
|
||||
2. **Right-click it** and select:
|
||||
- "Reveal in Finder/Explorer" then double-click to open in your browser
|
||||
3. **Click the button** to see the color themes change!
|
||||
|
||||
## Try Making Changes
|
||||
|
||||
In the same chat, try asking:
|
||||
|
||||
```
|
||||
Add a counter that shows how many times the button has been clicked
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
Make the welcome message fade in when the page loads
|
||||
```
|
||||
|
||||
Cline understands the context from your previous conversation and will update the file accordingly.
|
||||
|
||||
<Tip>
|
||||
**You now know how to:**
|
||||
- Give Cline a task with a clear prompt
|
||||
- Review and approve Cline's actions
|
||||
- Build a complete project in seconds
|
||||
- Iterate and improve on existing work
|
||||
</Tip>
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you've experienced Cline's capabilities, explore more:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="@ Mentions" href="/features/at-mentions/overview" icon="at">
|
||||
Reference specific files, folders, and URLs in your prompts
|
||||
</Card>
|
||||
|
||||
<Card title="Plan & Act Modes" href="/features/plan-and-act" icon="diagram-project">
|
||||
Master planning vs. execution for complex tasks
|
||||
</Card>
|
||||
|
||||
<Card title="Cline Rules" href="/features/cline-rules" icon="list-check">
|
||||
Set project-specific guidelines for consistent results
|
||||
</Card>
|
||||
|
||||
<Card title="Prompting Guide" href="/prompting/prompt-engineering-guide" icon="wand-magic-sparkles">
|
||||
Learn to write prompts that get the best results
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **Stuck?** Try starting fresh with `/new` in the chat
|
||||
- **Found a bug?** Use `/reportbug` to help us improve
|
||||
- **Have questions?** Join our [Discord community](https://discord.gg/cline)
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: "Overview"
|
||||
description: "An introduction to Cline, your AI-powered coding agent for modern development."
|
||||
---
|
||||
|
||||
## Open Source AI Coding, Uncompromised
|
||||
|
||||
Cline gives you direct, transparent access to frontier AI with no limits, no surprises, and no model ecosystem lock-in. See every decision. Choose any model. Control your costs.
|
||||
|
||||
<CardGroup cols={1}>
|
||||
<Card title="Complete Transparency" icon="eye">
|
||||
Watch in real-time as Cline reads files, considers approaches, and proposes changes. Every decision is visible, every edit reviewable before it's made.
|
||||
</Card>
|
||||
|
||||
<Card title="Choose Your Inference" icon="sliders">
|
||||
Use Claude for complex reasoning, Gemini for massive contexts, or Qwen3 Coder for efficiency. Switch instantly as new models launch. Bring your API keys, your choice.
|
||||
</Card>
|
||||
|
||||
<Card title="Zero Trust by Design" icon="lock">
|
||||
Your code never touches our servers. Cline runs entirely client-side with your API keys. Open source means your security team can review every line.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Built for Real Engineering
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Read & Write Files" icon="file-code">
|
||||
Work across your entire codebase with intelligent file operations
|
||||
</Card>
|
||||
|
||||
<Card title="Execute Commands" icon="terminal">
|
||||
Run terminal commands and debug errors in real-time
|
||||
</Card>
|
||||
|
||||
<Card title="Plan Complex Features" icon="brain">
|
||||
Explore and plan before writing a single line of code
|
||||
</Card>
|
||||
|
||||
<Card title="Connect External Systems" icon="plug">
|
||||
Integrate with databases, APIs, and documentation through MCP servers
|
||||
</Card>
|
||||
|
||||
<Card title="Understand Large Codebases" icon="magnifying-glass">
|
||||
Intelligent context management for massive projects
|
||||
</Card>
|
||||
|
||||
<Card title="Multi-Step Tasks" icon="list-check">
|
||||
Execute complex workflows from start to finish
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Plan & Act Mode
|
||||
|
||||
Cline explores your codebase and works with you to create comprehensive plans before writing code, ensuring it understands the full context of your project.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Plan Mode" icon="lightbulb">
|
||||
For complex tasks, Cline explores your codebase, asks clarifying questions, and creates detailed implementation plans before making changes.
|
||||
|
||||
- Information gathering and context building
|
||||
- Asking clarifying questions
|
||||
- Creating detailed execution plans
|
||||
- Discussing approaches with you
|
||||
</Tab>
|
||||
|
||||
<Tab title="Act Mode" icon="play">
|
||||
Once you approve the plan, Cline implements the solution with full transparency and control.
|
||||
|
||||
- Executing planned actions
|
||||
- Using tools to modify files and run commands
|
||||
- Implementing the solution
|
||||
- Providing results and completion feedback
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Key Features
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Intelligent Planning & Execution" icon="brain">
|
||||
<div style={{ display: 'grid', gap: '1rem', marginTop: '0.5rem' }}>
|
||||
<div>
|
||||
<strong><a href="/features/plan-and-act">Plan & Act Mode</a></strong> • Plan complex features before writing code, then execute with full transparency
|
||||
</div>
|
||||
<div>
|
||||
<strong><a href="/features/focus-chain">Focus Chain</a></strong> • Automatic todo list management with real-time progress tracking
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Workflow Optimization" icon="bolt">
|
||||
<div style={{ display: 'grid', gap: '1rem', marginTop: '0.5rem' }}>
|
||||
<div>
|
||||
<strong><a href="/features/auto-approve">Auto Approve</a></strong> • Streamline your workflow by automatically approving trusted operations
|
||||
</div>
|
||||
<div>
|
||||
<strong><a href="/features/auto-compact">Auto Compact</a></strong> • Automatic conversation summarization to preserve context while freeing space
|
||||
</div>
|
||||
<div>
|
||||
<strong><a href="/features/dictation">Dictation</a></strong> • Speak naturally to Cline for rapid planning and complex requirements
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Extensions & Integrations" icon="plug">
|
||||
<div style={{ display: 'grid', gap: '1rem', marginTop: '0.5rem' }}>
|
||||
<div>
|
||||
<strong><a href="/mcp/mcp-overview">MCP Integration</a></strong> • Connect to databases, APIs, and documentation through the Model Context Protocol
|
||||
</div>
|
||||
<div>
|
||||
<strong><a href="/exploring-clines-tools/remote-browser-support">Remote Browser</a></strong> • Test and interact with web applications through browser automation
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Customization & Control" icon="gear">
|
||||
<div style={{ display: 'grid', gap: '1rem', marginTop: '0.5rem' }}>
|
||||
<div>
|
||||
<strong><a href="/features/cline-rules">.clinerules</a></strong> • Define project-specific instructions including coding standards and patterns
|
||||
</div>
|
||||
<div>
|
||||
<strong><a href="/features/checkpoints">Checkpoints</a></strong> • Save and restore project states with Git-based checkpoints
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Why Developers Choose Cline
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="100% Open Source" icon="github">
|
||||
Every line of code on GitHub. **50k+ stars** from developers who've used it, improved it, and trust it with their work.
|
||||
</Card>
|
||||
|
||||
<Card title="No Inference Games" icon="ban">
|
||||
We don't profit from AI usage. While others limit context or route to cheaper models, we give you unrestricted access to any model's full capabilities.
|
||||
</Card>
|
||||
|
||||
<Card title="Future-Proof by Design" icon="rocket">
|
||||
New model released? Use it immediately. Cline works with multiple AI providers.
|
||||
</Card>
|
||||
|
||||
<Card title="True Visibility" icon="eye">
|
||||
See every file read, every decision considered, every token used. No black box, no surprises.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: "Welcome to Cline"
|
||||
description: "Your guide to AI-powered development with complete transparency and control"
|
||||
---
|
||||
|
||||
Cline is an open source AI coding agent that brings frontier AI models directly to your IDE. Unlike autocomplete tools, Cline is a true coding agent that can understand entire codebases, plan complex changes, and execute multi-step tasks.
|
||||
|
||||
<Frame>
|
||||
<video
|
||||
src="https://storage.googleapis.com/cline_public_images/cline-in-action.mp4"
|
||||
alt="Cline in action"
|
||||
autoPlay
|
||||
muted
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
## Navigate the Docs
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="New to Cline" icon="rocket" href="/getting-started/installing-cline">
|
||||
Start your journey with Cline - installation, setup, and first steps
|
||||
</Card>
|
||||
|
||||
<Card title="Configuration" icon="sliders" href="/core-features/model-selection-guide">
|
||||
Set up your AI models and providers
|
||||
</Card>
|
||||
|
||||
<Card title="Best Practices" icon="wrench" href="/prompting/understanding-context-management">
|
||||
Master Cline's powerful features and optimize your workflow
|
||||
</Card>
|
||||
|
||||
<Card title="Enterprise" icon="building" href="/enterprise-solutions/security-concerns">
|
||||
Deploy Cline in your organization with confidence
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
||||
## Community & Support
|
||||
|
||||
Join thousands of developers using Cline to build better software faster.
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Discord" icon="discord" href="https://discord.gg/cline">
|
||||
Chat with the community and get help
|
||||
</Card>
|
||||
|
||||
<Card title="GitHub" icon="github" href="https://github.com/cline/cline">
|
||||
Contribute to the open source project
|
||||
</Card>
|
||||
|
||||
<Card title="Report a Bug" icon="bug" href="https://github.com/cline/cline/issues">
|
||||
Help us improve by reporting problems
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
title: "Context Window Guide"
|
||||
description: "Understanding and managing AI model context windows"
|
||||
---
|
||||
|
||||
## What is a Context Window?
|
||||
|
||||
A context window is the maximum amount of text an AI model can process at once. Think of it as the model's "working memory" - it determines how much of your conversation and code the model can consider when generating responses.
|
||||
|
||||
<Note>
|
||||
**Key Point**: Larger context windows allow the model to understand more of your codebase at once, but may increase costs and response times.
|
||||
</Note>
|
||||
|
||||
## Context Window Sizes
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| Size | Tokens | Approximate Words | Use Case |
|
||||
|------|--------|------------------|----------|
|
||||
| **Small** | 8K-32K | 6,000-24,000 | Single files, quick fixes |
|
||||
| **Medium** | 128K | ~96,000 | Most coding projects |
|
||||
| **Large** | 200K | ~150,000 | Complex codebases |
|
||||
| **Extra Large** | 400K+ | ~300,000+ | Entire applications |
|
||||
| **Massive** | 1M+ | ~750,000+ | Multi-project analysis |
|
||||
|
||||
### Model Context Windows
|
||||
|
||||
| Model | Context Window | Effective Window* | Notes |
|
||||
|-------|---------------|------------------|-------|
|
||||
| **Claude Sonnet 4.5** | 1M tokens | ~500K tokens | Best quality at high context |
|
||||
| **GPT-5** | 400K tokens | ~300K tokens | Three modes affect performance |
|
||||
| **Gemini 2.5 Pro** | 1M+ tokens | ~600K tokens | Excellent for documents |
|
||||
| **DeepSeek V3** | 128K tokens | ~100K tokens | Optimal for most tasks |
|
||||
| **Qwen3 Coder** | 256K tokens | ~200K tokens | Good balance |
|
||||
|
||||
*Effective window is where model maintains high quality
|
||||
|
||||
## Managing Context Efficiently
|
||||
|
||||
### What Counts Toward Context
|
||||
|
||||
1. **Your current conversation** - All messages in the chat
|
||||
2. **File contents** - Any files you've shared or Cline has read
|
||||
3. **Tool outputs** - Results from executed commands
|
||||
4. **System prompts** - Cline's instructions (minimal impact)
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
#### 1. Start Fresh for New Features
|
||||
```
|
||||
/new - Creates a new task with clean context
|
||||
```
|
||||
Benefits:
|
||||
- Maximum context available
|
||||
- No irrelevant history
|
||||
- Better model focus
|
||||
|
||||
#### 2. Use @ Mentions Strategically
|
||||
Instead of including entire files:
|
||||
- `@filename.ts` - Include only when needed
|
||||
- Use search instead of reading large files
|
||||
- Reference specific functions rather than whole files
|
||||
|
||||
#### 3. Enable Auto-compact
|
||||
Cline can automatically summarize long conversations:
|
||||
- Settings → Features → Auto-compact
|
||||
- Preserves important context
|
||||
- Reduces token usage
|
||||
|
||||
## Context Window Warnings
|
||||
|
||||
### Signs You're Hitting Limits
|
||||
|
||||
| Warning Sign | What It Means | Solution |
|
||||
|-------------|---------------|----------|
|
||||
| **"Context window exceeded"** | Hard limit reached | Start new task or enable auto-compact |
|
||||
| **Slower responses** | Model struggling with context | Reduce included files |
|
||||
| **Repetitive suggestions** | Context fragmentation | Summarize and start fresh |
|
||||
| **Missing recent changes** | Context overflow | Use checkpoints to track changes |
|
||||
|
||||
### Best Practices by Project Size
|
||||
|
||||
#### Small Projects (< 50 files)
|
||||
- Any model works well
|
||||
- Include relevant files freely
|
||||
- No special optimization needed
|
||||
|
||||
#### Medium Projects (50-500 files)
|
||||
- Use 128K+ context models
|
||||
- Include only working set of files
|
||||
- Clear context between features
|
||||
|
||||
#### Large Projects (500+ files)
|
||||
- Use 200K+ context models
|
||||
- Focus on specific modules
|
||||
- Use search instead of reading many files
|
||||
- Break work into smaller tasks
|
||||
|
||||
## Advanced Context Management
|
||||
|
||||
### Plan/Act Mode Optimization
|
||||
|
||||
Leverage Plan/Act mode for better context usage:
|
||||
- **Plan Mode**: Use smaller context for discussion
|
||||
- **Act Mode**: Include necessary files for implementation
|
||||
|
||||
Configuration:
|
||||
```
|
||||
Plan Mode: DeepSeek V3 (128K) - Lower cost planning
|
||||
Act Mode: Claude Sonnet (1M) - Maximum context for coding
|
||||
```
|
||||
|
||||
### Context Pruning Strategies
|
||||
|
||||
1. **Temporal Pruning**: Remove old conversation parts
|
||||
2. **Semantic Pruning**: Keep only relevant code sections
|
||||
3. **Hierarchical Pruning**: Maintain high-level structure, prune details
|
||||
|
||||
### Token Counting Tips
|
||||
|
||||
#### Rough Estimates
|
||||
- **1 token ≈ 0.75 words**
|
||||
- **1 token ≈ 4 characters**
|
||||
- **100 lines of code ≈ 500-1000 tokens**
|
||||
|
||||
#### File Size Guidelines
|
||||
| File Type | Tokens per KB |
|
||||
|-----------|---------------|
|
||||
| **Code** | ~250-400 |
|
||||
| **JSON** | ~300-500 |
|
||||
| **Markdown** | ~200-300 |
|
||||
| **Plain text** | ~200-250 |
|
||||
|
||||
## Context Window FAQ
|
||||
|
||||
### Q: Why do responses get worse with very long conversations?
|
||||
**A:** Models can lose focus with too much context. The "effective window" is typically 50-70% of the advertised limit.
|
||||
|
||||
### Q: Should I use the largest context window available?
|
||||
**A:** Not always. Larger contexts increase cost and can reduce response quality. Match the context to your task size.
|
||||
|
||||
### Q: How can I tell how much context I'm using?
|
||||
**A:** Cline shows token usage in the interface. Watch for the context meter approaching limits.
|
||||
|
||||
### Q: What happens when I exceed the context limit?
|
||||
**A:** Cline will either:
|
||||
- Automatically compact the conversation (if enabled)
|
||||
- Show an error and suggest starting a new task
|
||||
- Truncate older messages (with warning)
|
||||
|
||||
## Recommendations by Use Case
|
||||
|
||||
| Use Case | Recommended Context | Model Suggestion |
|
||||
|----------|-------------------|------------------|
|
||||
| **Quick fixes** | 32K-128K | DeepSeek V3 |
|
||||
| **Feature development** | 128K-200K | Qwen3 Coder |
|
||||
| **Large refactoring** | 400K+ | Claude Sonnet 4.5 |
|
||||
| **Code review** | 200K-400K | GPT-5 |
|
||||
| **Documentation** | 128K | Any budget model |
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: "Model Comparison & Pricing"
|
||||
description: "Compare AI models by performance, features, and pricing"
|
||||
---
|
||||
|
||||
## Model Comparison Table
|
||||
|
||||
### Premium Models
|
||||
|
||||
| Model | Provider | Context Window | Input Price* | Output Price* | Best For |
|
||||
|-------|----------|---------------|--------------|---------------|----------|
|
||||
| **Claude Sonnet 4.5** | Anthropic | 1M tokens | $3-6 | $15-22.50 | Reliable tool usage, complex codebases |
|
||||
| **GPT-5** | OpenAI | 400K tokens | $1.25 | $10 | Latest OpenAI tech, three modes |
|
||||
| **Gemini 2.5 Pro** | Google | 1M+ tokens | TBD | TBD | Large codebases, document analysis |
|
||||
| **Qwen3 Coder** | Multiple | 256K tokens | $0.20 | $0.80 | Coding tasks, open source flexibility |
|
||||
|
||||
*Per million tokens
|
||||
|
||||
### Budget Models
|
||||
|
||||
| Model | Provider | Context Window | Input Price* | Output Price* | Notes |
|
||||
|-------|----------|---------------|--------------|---------------|-------|
|
||||
| **DeepSeek V3** | DeepSeek | 128K tokens | $0.14 | $0.28 | Great value for daily coding |
|
||||
| **DeepSeek R1** | DeepSeek | 128K tokens | $0.55 | $2.19 | Budget reasoning champion |
|
||||
| **Qwen3 32B** | Multiple | 128K tokens | Varies | Varies | Open source, multiple providers |
|
||||
| **Z AI GLM 4.5** | Z AI | 128K tokens | TBD | TBD | MIT licensed, hybrid reasoning |
|
||||
|
||||
*Per million tokens
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
### Speed vs Quality Trade-offs
|
||||
|
||||
| Priority | Recommended Model | Why |
|
||||
|----------|------------------|-----|
|
||||
| **Speed** | Qwen3 Coder on Cerebras | Fastest inference available |
|
||||
| **Quality** | Claude Sonnet 4.5 | Most reliable for complex tasks |
|
||||
| **Balance** | DeepSeek V3 | Good quality at low cost |
|
||||
|
||||
### Tool Reliability
|
||||
|
||||
Models ranked by tool usage reliability:
|
||||
1. **Claude Sonnet 4.5** - Most reliable tool execution
|
||||
2. **GPT-5** - Excellent but occasional formatting issues
|
||||
3. **Gemini 2.5 Pro** - Good for standard tools
|
||||
4. **DeepSeek V3** - Reliable for basic tools
|
||||
5. **Qwen3 variants** - May need retry for complex tools
|
||||
|
||||
## Cost Calculator
|
||||
|
||||
### Typical Task Costs
|
||||
|
||||
| Task Type | Token Usage (avg) | Claude Sonnet | DeepSeek V3 | Difference |
|
||||
|-----------|------------------|---------------|-------------|------------|
|
||||
| **Simple Bug Fix** | 5K tokens | $0.05 | $0.001 | 50x cheaper |
|
||||
| **Feature Implementation** | 50K tokens | $0.50 | $0.01 | 50x cheaper |
|
||||
| **Large Refactoring** | 200K tokens | $2.00 | $0.04 | 50x cheaper |
|
||||
|
||||
### Monthly Budget Estimates
|
||||
|
||||
| Budget | Claude Usage | DeepSeek Usage | Mixed Strategy |
|
||||
|--------|-------------|----------------|----------------|
|
||||
| **$10/month** | ~20 features | ~1000 features | Plan: DeepSeek, Act: Claude |
|
||||
| **$50/month** | ~100 features | ~5000 features | Critical: Claude, Rest: DeepSeek |
|
||||
| **$100/month** | ~200 features | ~10000 features | Complex: Claude, Simple: DeepSeek |
|
||||
|
||||
## Provider Comparison
|
||||
|
||||
### Provider Features
|
||||
|
||||
| Provider | Models Available | Billing | API Stability | Support |
|
||||
|----------|-----------------|---------|---------------|---------|
|
||||
| **Cline** | Multiple | Credit-based | High | In-app |
|
||||
| **Anthropic** | Claude only | Usage-based | High | Email |
|
||||
| **OpenRouter** | 100+ models | Usage-based | High | Discord |
|
||||
| **OpenAI** | GPT only | Usage-based | High | Forum |
|
||||
| **Local (Ollama)** | Open source | Free | N/A | Community |
|
||||
|
||||
### Provider Selection Guide
|
||||
|
||||
Choose your provider based on:
|
||||
- **Simplicity**: Cline (no API key management)
|
||||
- **Variety**: OpenRouter (access to all models)
|
||||
- **Direct Access**: Individual providers (Anthropic, OpenAI)
|
||||
- **Privacy**: Ollama or LM Studio (local models)
|
||||
|
||||
## Community Usage Stats
|
||||
|
||||
Real-time usage data from the Cline community:
|
||||
- View current trends at [OpenRouter's Cline stats](https://openrouter.ai/apps?url=https%3A%2F%2Fcline.bot%2F)
|
||||
- Most popular: Claude Sonnet 4.5 (40%)
|
||||
- Rising star: DeepSeek V3 (25%)
|
||||
- Budget favorite: Qwen3 variants (20%)
|
||||
@@ -8,10 +8,9 @@ title: "Cline Memory Bank"
|
||||
|
||||
To get started with Cline Memory Bank:
|
||||
|
||||
1. **Install or Open Cline**
|
||||
2. **Copy the Custom Instructions** - Use the code block below
|
||||
3. **Paste into Cline** - Add as custom instructions or in a .clinerules file
|
||||
4. **Initialize** - Ask Cline to "initialize memory bank"
|
||||
1. **Copy the Custom Instructions** - Use the code block below
|
||||
2. **Paste into Cline** - Add as custom instructions or in a .clinerules file
|
||||
3. **Initialize** - Ask Cline to "initialize memory bank"
|
||||
|
||||
[See detailed setup instructions](#getting-started-with-memory-bank)
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: "Context Management"
|
||||
description: "Master context management to unlock Cline's full potential"
|
||||
---
|
||||
|
||||
<Info>
|
||||
**Quick Reference**
|
||||
|
||||
- **Context** = All information Cline knows about your project
|
||||
- **Context Window** = Maximum information Cline can process at once (varies by model)
|
||||
- **Token** = Unit of text measurement (~3/4 of an English word)
|
||||
- **Auto-management** = Cline automatically handles context through Focus Chain & Auto Compact
|
||||
</Info>
|
||||
|
||||
## What is Context Management?
|
||||
|
||||
Context management is how Cline maintains understanding of your project throughout a conversation. Think of it as the shared memory between you and Cline - containing code, decisions, requirements, and progress.
|
||||
|
||||
<Frame caption="Context is like a shared workspace where Cline builds understanding of your project">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2).png"
|
||||
alt="Context visualization showing the relationship between total context and context window"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### The Three Layers of Context
|
||||
|
||||
1. **Immediate Context** - Current conversation and active files
|
||||
2. **Project Context** - Your codebase, structure, and patterns
|
||||
3. **Persistent Context** - Memory Bank, .clinerules, and documentation
|
||||
|
||||
## Understanding Context Windows
|
||||
|
||||
Every AI model has a **context window** - the maximum amount of information it can process in a single conversation. This is measured in tokens:
|
||||
|
||||
### Token Limits by Model
|
||||
|
||||
| Model | Context Window | Effective Limit* | Best For |
|
||||
|-------|---------------|-----------------|----------|
|
||||
| **Claude 3.5 Sonnet** | 200,000 tokens | 150,000 tokens | Complex tasks, large codebases |
|
||||
| **Claude 3.5 Haiku** | 200,000 tokens | 150,000 tokens | Faster responses, simpler tasks |
|
||||
| **GPT-4o** | 128,000 tokens | 100,000 tokens | General purpose development |
|
||||
| **Gemini 2.0 Flash** | 1,000,000+ tokens | 400,000 tokens | Very large contexts |
|
||||
| **DeepSeek v3** | 64,000 tokens | 50,000 tokens | Cost-effective coding |
|
||||
| **Qwen 2.5 Coder** | 128,000 tokens | 100,000 tokens | Specialized coding tasks |
|
||||
|
||||
*Effective limit is ~75-80% of maximum for optimal performance
|
||||
|
||||
<Tip>
|
||||
**Token Math Made Simple**
|
||||
- 1 token ≈ 3/4 of an English word
|
||||
- 100 tokens ≈ 75 words ≈ 3-5 lines of code
|
||||
- 10,000 tokens ≈ 7,500 words ≈ ~15 pages of text
|
||||
- A typical source file: 500-2,000 tokens
|
||||
</Tip>
|
||||
|
||||
## How Cline Builds Context
|
||||
|
||||
Building effective context is what makes Cline truly useful. When you start a task, Cline doesn't just passively wait for information - he actively gathers context about your project, asks clarifying questions when needed, and adapts to what's happening in real-time. This combination of automatic discovery, user guidance, and dynamic adaptation ensures Cline always has the right information to solve your problems effectively.
|
||||
|
||||
### 1. Automatic Context Gathering
|
||||
|
||||
When you start a task, Cline proactively:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Task Start] --> B[Scan Project Structure]
|
||||
B --> C[Identify Relevant Files]
|
||||
C --> D[Read Key Components]
|
||||
D --> E[Map Dependencies]
|
||||
E --> F[Build Mental Model]
|
||||
```
|
||||
|
||||
**What Cline automatically discovers:**
|
||||
- Project structure and file organization
|
||||
- Import relationships and dependencies
|
||||
- Code patterns and conventions
|
||||
- Configuration files and settings
|
||||
- Recent changes and git history (when using @git)
|
||||
|
||||
### 2. User-Guided Context
|
||||
|
||||
While automatic discovery handles much of the work, you control what Cline focuses on. The more specific and relevant context you provide, the better Cline can understand your needs and deliver accurate solutions.
|
||||
|
||||
You enhance context by:
|
||||
- **@ Mentioning** files, folders, or URLs
|
||||
- **Providing requirements** in natural language
|
||||
- **Sharing screenshots** for UI context
|
||||
- **Adding documentation** through .clinerules or Memory Bank
|
||||
- **Answering questions** when Cline needs clarification
|
||||
|
||||
### 3. Dynamic Context Adaptation
|
||||
|
||||
Cline adapts context dynamically throughout your conversation. It considers the complexity of your request, available context window space, current task progress, error messages and feedback, plus previous decisions made during the conversation to determine what information matters most at each step.
|
||||
|
||||
## The Context Window Progress Bar
|
||||
|
||||
Monitor your context usage in real-time:
|
||||
|
||||
<Frame caption="The context window bar shows input/output token usage">
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/context-bar.png"
|
||||
alt="Context window progress bar showing token usage"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### Understanding the Indicators
|
||||
|
||||
- ⬆️ **Input Tokens**: Information sent to the model (your messages + context)
|
||||
- ⬇️ **Output Tokens**: Model's responses and generated code
|
||||
- ➡️ **Cache Tokens**: Previously processed tokens that's reused (reduces costs and improves speed)
|
||||
- **Progress Bar**: Visual representation of usage
|
||||
- **Percentage**: Current usage of total capacity
|
||||
|
||||
## Automatic Context Management Features
|
||||
|
||||
Cline includes intelligent systems that handle context for you:
|
||||
|
||||
### Focus Chain (Default: ON)
|
||||
|
||||
Focus Chain maintains task continuity through automatic todo lists. When you start a task, Cline generates actionable steps and updates them as work progresses. This keeps critical context visible even after Auto Compact runs, letting you track progress without scrolling through the entire conversation.
|
||||
|
||||
[Learn more →](/features/focus-chain)
|
||||
|
||||
### Auto Compact (Always ON)
|
||||
|
||||
When context usage hits around 80%, Auto Compact automatically creates a comprehensive summary of the conversation. This preserves all decisions and code changes while freeing up space for continued work. You'll see a message when this happens. The task continues seamlessly - you don't need to do anything.
|
||||
|
||||
[Learn more →](/features/auto-compact)
|
||||
|
||||
### Context Truncation System
|
||||
|
||||
If your conversation approaches the model's context window limit before Auto Compact runs, Cline's Context Manager automatically truncates older parts of the conversation to prevent errors.
|
||||
|
||||
The system prioritizes what matters most:
|
||||
- Your original task description stays
|
||||
- Recent tool executions and their results remain intact
|
||||
- Current code state and active errors are preserved
|
||||
- The logical flow of user-assistant messages is maintained
|
||||
|
||||
What gets removed first:
|
||||
- Redundant conversation history from earlier in the task
|
||||
- Completed tool outputs that are no longer relevant
|
||||
- Intermediate debugging steps
|
||||
- Verbose explanations that served their purpose
|
||||
|
||||
This happens automatically. You'll keep working without interruption, and Cline maintains enough context to continue solving your problem effectively.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Be specific** - Clear objectives help Cline understand your needs
|
||||
- **Use @ mentions strategically** - Reference specific files rather than entire folders
|
||||
- **Monitor the progress bar** - Yellow/red means consider using `/smol` or `/newtask`
|
||||
- **Trust auto-management** - Focus Chain and Auto Compact handle complexity automatically
|
||||
- **Use Memory Bank** - Document persistent patterns and conventions
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Focus Chain" icon="link" href="/features/focus-chain">
|
||||
Learn how Focus Chain maintains task continuity
|
||||
</Card>
|
||||
<Card title="Auto Compact" icon="compress" href="/features/auto-compact">
|
||||
Understand automatic conversation compression
|
||||
</Card>
|
||||
<Card title="Memory Bank" icon="brain" href="/prompting/cline-memory-bank">
|
||||
Set up persistent project knowledge
|
||||
</Card>
|
||||
<Card title="Cline Rules" icon="gavel" href="/features/cline-rules">
|
||||
Define project-specific conventions
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -21,20 +21,16 @@ For the most updated pricing, please visit: https://www.baseten.co/products/mode
|
||||
Note: Kimi K2 0711, Llama 4 Maverick, and Llama 4 Scout Model APIs have been deprecated at 5pm PT on October 8th.
|
||||
https://www.baseten.co/resources/changelog/model-api-deprecation-notice-kimi-k2-0711-scout-maverick/
|
||||
|
||||
**Reasoning Models:**
|
||||
- `zai-org/GLM-4.6` (Z AI) - Frontier open model with advanced agentic, reasoning and coding capabilities by Z AI (200k context) \$0.60/\$2.20 per 1M tokens
|
||||
- `moonshotai/Kimi-K2-Instruct-0905` (Moonshot AI) - September update with enhanced capabilities (262K context) - \$0.60/\$2.50 per 1M tokens
|
||||
- `openai/gpt-oss-120b` (OpenAI) - 120B MoE with strong reasoning capabilities (128K context) - \$0.10/\$0.50 per 1M tokens
|
||||
- `Qwen/Qwen3-Coder-480B-A35B-Instruct`- Advanced coding and reasoning (262K context) - \$0.38/\$1.53 per 1M tokens
|
||||
- `Qwen/Qwen3-235B-A22B-Instruct-2507` - Math and reasoning expert (262K context) - \$0.22/\$0.80 per 1M tokens
|
||||
- `deepseek-ai/DeepSeek-R1` - DeepSeek's first-generation reasoning model (163K context) - \$2.55/\$5.95 per 1M tokens
|
||||
- `deepseek-ai/DeepSeek-R1-0528` - Latest revision of DeepSeek's reasoning model (163K context) - \$2.55/\$5.95 per 1M tokens
|
||||
- `deepseek-ai/DeepSeek-V3.1` - Hybrid reasoning with advanced tool calling (163K context) - \$0.50/\$1.50 per 1M tokens
|
||||
- `deepseek-ai/DeepSeek-V3-0324` - Fast general-purpose with enhanced reasoning (163K context) - \$0.77/\$0.77 per 1M tokens
|
||||
|
||||
**Flagship Models:**
|
||||
- `openai/gpt-oss-120b` (OpenAI) - 120B MoE with strong reasoning capabilities (128K context) - \$0.10/\$0.50 per 1M tokens
|
||||
- `moonshotai/Kimi-K2-Instruct-0905` (Moonshot AI) - September update with enhanced capabilities (262K context) - \$0.60/\$2.50 per 1M tokens
|
||||
|
||||
**Coding Specialists:**
|
||||
- `Qwen/Qwen3-Coder-480B-A35B-Instruct`- Advanced coding and reasoning (262K context) - \$0.38/\$1.53 per 1M tokens
|
||||
- `Qwen/Qwen3-235B-A22B-Instruct-2507` - Math and reasoning expert (262K context) - \$0.22/\$0.80 per 1M tokens
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
|
||||
@@ -34,18 +34,10 @@ First, you'll need to install and authenticate Claude Code on your system:
|
||||
|
||||
<br />
|
||||
|
||||
<Accordion title="Windows Setup">
|
||||
Anthropic introduced full support for Claude Code on Windows. Follow the [instructions on how to set up Claude Code
|
||||
normally](#setup) and make sure you have the latest Claude Code and Cline versions.
|
||||
</Accordion>
|
||||
|
||||
### Finding your Claude Code path
|
||||
|
||||
If you're not sure where Claude Code is installed:
|
||||
|
||||
- **macOS / Linux**: Run `which claude` in your terminal
|
||||
- **Windows (Command Prompt)**: Run `where claude`
|
||||
- **Windows (PowerShell)**: Run `Get-Command claude`
|
||||
- **macOS / Linux / WSL / Git Bash**: `which claude`
|
||||
- **Windows Command Prompt**: `where claude`
|
||||
|
||||
## Supported Models
|
||||
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: "Local Models Overview"
|
||||
---
|
||||
|
||||
## Running Models Locally with Cline
|
||||
|
||||
Run Cline completely offline with genuinely capable models on your own hardware. No API costs, no data leaving your machine, no internet dependency.
|
||||
|
||||
Local models have reached a turning point where they're now practical for real development work. This guide covers everything you need to know about running Cline with local models.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Check your hardware** - 32GB+ RAM minimum
|
||||
2. **Choose your runtime** - [LM Studio](/running-models-locally/lm-studio) or [Ollama](/running-models-locally/ollama)
|
||||
3. **Download Qwen3 Coder 30B** - The recommended model
|
||||
4. **Configure settings** - Enable compact prompts, set max context
|
||||
5. **Start coding** - Completely offline
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
Your RAM determines which models you can run effectively:
|
||||
|
||||
| RAM | Recommended Model | Quantization | Performance Level |
|
||||
| --- | --- | --- | --- |
|
||||
| 32GB | Qwen3 Coder 30B | 4-bit | Entry-level local coding |
|
||||
| 64GB | Qwen3 Coder 30B | 8-bit | Full Cline features |
|
||||
| 128GB+ | GLM-4.5-Air | 4-bit | Cloud-competitive performance |
|
||||
|
||||
## Recommended Models
|
||||
|
||||
### Primary Recommendation: Qwen3 Coder 30B
|
||||
|
||||
After extensive testing, **Qwen3 Coder 30B** is the most reliable model under 70B parameters for Cline:
|
||||
|
||||
- **256K native context window** - Handle entire repositories
|
||||
- **Strong tool-use capabilities** - Reliable command execution
|
||||
- **Repository-scale understanding** - Maintains context across files
|
||||
- **Proven reliability** - Consistent outputs with Cline's tool format
|
||||
|
||||
Download sizes:
|
||||
- 4-bit: ~17GB (recommended for 32GB RAM)
|
||||
- 8-bit: ~32GB (recommended for 64GB RAM)
|
||||
- 16-bit: ~60GB (requires 128GB+ RAM)
|
||||
|
||||
### Why Not Smaller Models?
|
||||
|
||||
Most models under 30B parameters (7B-20B) fail with Cline because they:
|
||||
- Produce broken tool-use outputs
|
||||
- Refuse to execute commands
|
||||
- Can't maintain conversation context
|
||||
- Struggle with complex coding tasks
|
||||
|
||||
## Runtime Options
|
||||
|
||||
### LM Studio
|
||||
- **Pros**: User-friendly GUI, easy model management, built-in server
|
||||
- **Cons**: Memory overhead from UI, limited to single model at a time
|
||||
- **Best for**: Desktop users who want simplicity
|
||||
- [Setup Guide →](/running-models-locally/lm-studio)
|
||||
|
||||
### Ollama
|
||||
- **Pros**: Command-line based, lower memory overhead, scriptable
|
||||
- **Cons**: Requires terminal comfort, manual model management
|
||||
- **Best for**: Power users and server deployments
|
||||
- [Setup Guide →](/running-models-locally/ollama)
|
||||
|
||||
## Critical Configuration
|
||||
|
||||
### Required Settings
|
||||
|
||||
**In Cline:**
|
||||
- ✅ Enable "Use Compact Prompt" - Reduces prompt size by 90%
|
||||
- ✅ Set appropriate model in settings
|
||||
- ✅ Configure Base URL to match your server
|
||||
|
||||
**In LM Studio:**
|
||||
- Context Length: `262144` (maximum)
|
||||
- KV Cache Quantization: `OFF` (critical for proper function)
|
||||
- Flash Attention: `ON` (if available on your hardware)
|
||||
|
||||
**In Ollama:**
|
||||
- Set context window: `num_ctx 262144`
|
||||
- Enable flash attention if supported
|
||||
|
||||
### Understanding Quantization
|
||||
|
||||
Quantization reduces model precision to fit on consumer hardware:
|
||||
|
||||
| Type | Size Reduction | Quality | Use Case |
|
||||
| --- | --- | --- | --- |
|
||||
| 4-bit | ~75% | Good | Most coding tasks, limited RAM |
|
||||
| 8-bit | ~50% | Better | Professional work, more nuance |
|
||||
| 16-bit | None | Best | Maximum quality, requires high RAM |
|
||||
|
||||
### Model Formats
|
||||
|
||||
**GGUF (Universal)**
|
||||
- Works on all platforms (Windows, Linux, Mac)
|
||||
- Extensive quantization options
|
||||
- Broader tool compatibility
|
||||
- Recommended for most users
|
||||
|
||||
**MLX (Mac only)**
|
||||
- Optimized for Apple Silicon (M1/M2/M3)
|
||||
- Leverages Metal and AMX acceleration
|
||||
- Faster inference on Mac
|
||||
- Requires macOS 13+
|
||||
|
||||
## Performance Expectations
|
||||
|
||||
### What's Normal
|
||||
|
||||
- **Initial load time**: 10-30 seconds for model warmup
|
||||
- **Token generation**: 5-20 tokens/second on consumer hardware
|
||||
- **Context processing**: Slower with large codebases
|
||||
- **Memory usage**: Close to your quantization size
|
||||
|
||||
### Performance Tips
|
||||
|
||||
1. **Use compact prompts** - Essential for local inference
|
||||
2. **Limit context when possible** - Start with smaller windows
|
||||
3. **Choose right quantization** - Balance quality vs speed
|
||||
4. **Close other applications** - Free up RAM for the model
|
||||
5. **Use SSD storage** - Faster model loading
|
||||
|
||||
## Use Case Comparison
|
||||
|
||||
### When to Use Local Models
|
||||
|
||||
✅ **Perfect for:**
|
||||
- Offline development environments
|
||||
- Privacy-sensitive projects
|
||||
- Learning without API costs
|
||||
- Unlimited experimentation
|
||||
- Air-gapped environments
|
||||
- Cost-conscious development
|
||||
|
||||
### When to Use Cloud Models
|
||||
|
||||
☁️ **Better for:**
|
||||
- Very large codebases (>256K tokens)
|
||||
- Multi-hour refactoring sessions
|
||||
- Teams needing consistent performance
|
||||
- Latest model capabilities
|
||||
- Time-critical projects
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues & Solutions
|
||||
|
||||
**"Shell integration unavailable"**
|
||||
- Switch to bash in Cline Settings → Terminal → Default Terminal Profile
|
||||
- Resolves 90% of terminal integration problems
|
||||
|
||||
**"No connection could be made"**
|
||||
- Verify server is running (LM Studio or Ollama)
|
||||
- Check Base URL matches server address
|
||||
- Ensure no firewall blocking connection
|
||||
- Default ports: LM Studio (1234), Ollama (11434)
|
||||
|
||||
**Slow or incomplete responses**
|
||||
- Normal for local models (5-20 tokens/sec typical)
|
||||
- Try smaller quantization (4-bit instead of 8-bit)
|
||||
- Enable compact prompts if not already
|
||||
- Reduce context window size
|
||||
|
||||
**Model confusion or errors**
|
||||
- Verify KV Cache Quantization is OFF (LM Studio)
|
||||
- Ensure compact prompts enabled
|
||||
- Check context length set to maximum
|
||||
- Confirm sufficient RAM for quantization
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
**For faster inference:**
|
||||
1. Use 4-bit quantization
|
||||
2. Enable Flash Attention
|
||||
3. Reduce context window if not needed
|
||||
4. Close unnecessary applications
|
||||
5. Use NVMe SSD for model storage
|
||||
|
||||
**For better quality:**
|
||||
1. Use 8-bit or higher quantization
|
||||
2. Maximize context window
|
||||
3. Ensure adequate cooling
|
||||
4. Allocate maximum RAM to model
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Multi-GPU Setup
|
||||
If you have multiple GPUs, you can split model layers:
|
||||
- LM Studio: Automatic GPU detection
|
||||
- Ollama: Set `num_gpu` parameter
|
||||
|
||||
### Custom Models
|
||||
While Qwen3 Coder 30B is recommended, you can experiment with:
|
||||
- DeepSeek Coder V2
|
||||
- Codestral 22B
|
||||
- StarCoder2 15B
|
||||
|
||||
Note: These may require additional configuration and testing.
|
||||
|
||||
## Community & Support
|
||||
|
||||
- **Discord**: [Join our community](https://discord.gg/cline) for real-time help
|
||||
- **Reddit**: [r/cline](https://www.reddit.com/r/CLine/) for discussions
|
||||
- **GitHub**: [Report issues](https://github.com/cline/cline/issues)
|
||||
|
||||
## Next Steps
|
||||
|
||||
Ready to get started? Choose your path:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="LM Studio Setup" icon="desktop" href="/running-models-locally/lm-studio">
|
||||
User-friendly GUI approach with detailed configuration guide
|
||||
</Card>
|
||||
<Card title="Ollama Setup" icon="terminal" href="/running-models-locally/ollama">
|
||||
Command-line setup for power users and automation
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Summary
|
||||
|
||||
Local models with Cline are now genuinely practical. While they won't match top-tier cloud APIs in speed, they offer complete privacy, zero costs, and offline capability. With proper configuration and the right hardware, Qwen3 Coder 30B can handle most coding tasks effectively.
|
||||
|
||||
The key is proper setup: adequate RAM, correct configuration, and realistic expectations. Follow this guide, and you'll have a capable coding assistant running entirely on your hardware.
|
||||
@@ -1,154 +0,0 @@
|
||||
---
|
||||
title: "Read Me First"
|
||||
---
|
||||
|
||||
## Running Local Models with Cline
|
||||
|
||||
Local models have reached a turning point. For the first time, you can run Cline completely offline with genuinely capable models. No API costs, no data leaving your machine, no internet dependency.
|
||||
|
||||
The key is choosing the right model for your hardware and configuring it properly.
|
||||
|
||||
## What You Need to Know
|
||||
|
||||
### Hardware Requirements
|
||||
|
||||
Your RAM determines which models you can run:
|
||||
|
||||
| RAM Tier | Recommended Model | Quantization | What You Get |
|
||||
| --- | --- | --- | --- |
|
||||
| 32GB | Qwen3 Coder 30B | 4-bit | Entry-level local coding |
|
||||
| 64GB | Qwen3 Coder 30B | 8-bit | Full Cline features |
|
||||
| 128GB+ | GLM-4.5-Air | 4-bit | Cloud-competitive performance |
|
||||
|
||||
### The Model That Works: Qwen3 Coder 30B
|
||||
|
||||
After extensive testing, **Qwen3 Coder 30B** is the only model under 70B parameters that reliably works with Cline. It brings:
|
||||
|
||||
- 256K native context window
|
||||
- Strong tool-use capabilities
|
||||
- Repository-scale understanding
|
||||
- Reliable command execution
|
||||
|
||||
Most smaller models (7B-20B) fail with Cline. They produce broken outputs, refuse to execute commands, or can't handle tool use properly.
|
||||
|
||||
### Critical Configuration
|
||||
|
||||
Getting local models to work requires specific settings:
|
||||
|
||||
**For LM Studio:**
|
||||
1. Context Length: 262,144 (maximum)
|
||||
2. KV Cache Quantization: OFF (critical)
|
||||
3. Flash Attention: ON (if available)
|
||||
|
||||
**For All Local Models:**
|
||||
- Enable "Use Compact Prompt" in Cline settings
|
||||
- This reduces prompt size by 90% while maintaining core functionality
|
||||
- Essential for local inference performance
|
||||
|
||||
### Quantization Explained
|
||||
|
||||
Quantization reduces model precision to fit on consumer hardware. Think of it as compression:
|
||||
|
||||
- **4-bit**: ~75% size reduction. Completely usable for coding tasks.
|
||||
- **8-bit**: ~50% size reduction. Better quality, more nuanced responses.
|
||||
- **16-bit**: Full precision. Matches cloud APIs but requires 4x the memory.
|
||||
|
||||
For Qwen3 Coder 30B:
|
||||
- 4-bit: ~17GB download
|
||||
- 8-bit: ~32GB download
|
||||
- 16-bit: ~60GB download
|
||||
|
||||
### Model Format
|
||||
|
||||
Choose based on your platform:
|
||||
|
||||
**MLX (Mac only)**
|
||||
- Optimized for Apple Silicon
|
||||
- Leverages Metal and AMX acceleration
|
||||
- Faster inference on M1/M2/M3 chips
|
||||
|
||||
**GGUF (Universal)**
|
||||
- Works on Windows, Linux, and Mac
|
||||
- Extensive quantization options
|
||||
- Broader tool compatibility
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
Local models perform differently than cloud APIs:
|
||||
|
||||
**Expect:**
|
||||
- Warmup time when first loading (normal, happens once)
|
||||
- Slower inference than cloud models
|
||||
- Context ingestion slows with very large repositories
|
||||
|
||||
**Don't Expect:**
|
||||
- Instant responses like cloud APIs
|
||||
- Unlimited context processing speed
|
||||
- Zero configuration
|
||||
|
||||
## When Local Models Excel
|
||||
|
||||
Use local models for:
|
||||
|
||||
- Offline development where internet is unreliable
|
||||
- Privacy-sensitive projects where code can't leave your environment
|
||||
- Cost-conscious development where API usage would be prohibitive
|
||||
- Learning and experimentation with unlimited usage
|
||||
|
||||
## When to Use Cloud Models
|
||||
|
||||
Cloud models still have advantages for:
|
||||
|
||||
- Very large repositories exceeding local context limits
|
||||
- Multi-hour refactoring sessions needing maximum context
|
||||
- Teams requiring consistent performance across different hardware
|
||||
- Tasks requiring the absolute latest model capabilities
|
||||
|
||||
## Common Issues
|
||||
|
||||
**"Shell integration unavailable" or command execution fails**
|
||||
|
||||
Switch to a simpler shell in Cline settings. Go to Cline Settings → Terminal → Default Terminal Profile and select "bash". This resolves 90% of terminal integration problems.
|
||||
|
||||
**"No connection could be made"**
|
||||
|
||||
Your local server (Ollama or LM Studio) isn't running, or is running on a different port. Check that:
|
||||
- The server is actually running
|
||||
- The Base URL in Cline settings matches your server's address
|
||||
- No firewall is blocking the connection
|
||||
|
||||
**Slow or incomplete responses**
|
||||
|
||||
This is normal for local models. They're significantly slower than cloud APIs. If it's too slow:
|
||||
- Try a smaller quantization (4-bit instead of 8-bit)
|
||||
- Reduce context window size
|
||||
- Enable compact prompts if you haven't already
|
||||
|
||||
**Model seems confused or makes errors**
|
||||
|
||||
Ensure you have:
|
||||
- Compact prompts enabled
|
||||
- KV Cache Quantization disabled (LM Studio)
|
||||
- Context length set to maximum
|
||||
- Sufficient RAM for your chosen quantization
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Choose your runtime**: [LM Studio](/running-models-locally/lm-studio) or [Ollama](/running-models-locally/ollama)
|
||||
2. **Download Qwen3 Coder 30B** in the appropriate quantization for your RAM
|
||||
3. **Configure critical settings** as outlined above
|
||||
4. **Enable compact prompts** in Cline settings
|
||||
5. **Start coding** offline
|
||||
|
||||
## The Reality of Local Models
|
||||
|
||||
Local models are now genuinely useful for coding tasks, but they're not magic. You're trading some convenience and speed for privacy and cost savings. The setup requires attention to detail, and performance won't match top-tier cloud APIs.
|
||||
|
||||
But for the first time, you can run a capable coding agent entirely on your laptop. That's a significant milestone.
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Join our [Discord](https://discord.gg/cline) community
|
||||
- Visit [r/cline](https://www.reddit.com/r/CLine/) on Reddit
|
||||
- Check the [LM Studio guide](/running-models-locally/lm-studio) for detailed setup
|
||||
- See the [Ollama guide](/running-models-locally/ollama) for alternative setup
|
||||
+42
-1
@@ -1,8 +1,49 @@
|
||||
/* Custom styles for Cline documentation */
|
||||
|
||||
/* Import Geist Sans font from Google Fonts */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Geist+Sans:wght@300;400;500;600;700&display=swap");
|
||||
|
||||
/* Import Geist Mono font from Google Fonts */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Geist+Mono:wght@300;400;500;600;700&display=swap");
|
||||
|
||||
/* Apply Geist Sans to body text, but not headings or code */
|
||||
body,
|
||||
p,
|
||||
li,
|
||||
td,
|
||||
th,
|
||||
span:not(code *),
|
||||
div:not(code *):not(pre *) {
|
||||
font-family:
|
||||
"Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Ensure code blocks use Geist Mono */
|
||||
code,
|
||||
pre,
|
||||
.code,
|
||||
pre code,
|
||||
code *,
|
||||
pre * {
|
||||
font-family: "Geist Mono", "Monaco", "Courier New", monospace !important;
|
||||
}
|
||||
|
||||
/* Make h1 titles lighter in font weight */
|
||||
h1 {
|
||||
font-weight: 500 !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Keep headings and images at full opacity */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
img {
|
||||
opacity: 1 !important;
|
||||
font-family:
|
||||
"Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Also apply to any h1 elements within content areas */
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
repositories
|
||||
|
||||
results/evals.db
|
||||
temp-files
|
||||
results
|
||||
|
||||
diff-edits/cases/
|
||||
diff-edits/results/
|
||||
@@ -21,4 +21,4 @@ diff_editing/test_outputs/
|
||||
# Python bytecode cache
|
||||
*__pycache__/
|
||||
|
||||
diff-edits/cases.zip
|
||||
diff-edits/cases.zip
|
||||
|
||||
+32
-70
@@ -15,48 +15,32 @@ The Cline Evaluation System allows you to:
|
||||
|
||||
The evaluation system consists of two main components:
|
||||
|
||||
1. **Test Server**: Enhanced HTTP server in `src/services/test/TestServer.ts` that provides detailed task results
|
||||
2. **CLI Tool**: Command-line interface in `evals/cli/` for orchestrating evaluations
|
||||
3. **Diff Edit Benchmark**: Separate command using the CLI tool that runs a comprehensive diff editing benchmark suite on real world cases, along with a streamlit dashboard displaying the results. For more details, see the [Diff Edit Benchmark README](./diff-edits/README.md). Make sure you add a `evals/diff-edits/cases` folder with all the conversation jsons.
|
||||
1. **CLI Tool**: Command-line interface in `evals/cli/` for orchestrating evaluations
|
||||
2. **Diff Edit Benchmark**: Separate command using the CLI tool that runs a comprehensive diff editing benchmark suite on real world cases, along with a streamlit dashboard displaying the results. For more details, see the Diff Edit Benchmark [README](./diff-edits/README.md). Make sure you add a `evals/diff-edits/cases` folder with all the conversation jsons.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
cline-repo/
|
||||
├── src/
|
||||
│ ├── services/
|
||||
│ │ ├── test/
|
||||
│ │ │ ├── TestServer.ts # Enhanced HTTP server for task execution
|
||||
│ │ │ ├── GitHelper.ts # Git utilities for file tracking
|
||||
│ │ │ └── ...
|
||||
│ │ └── ...
|
||||
│ └── ...
|
||||
├── evals/ # Main directory for evaluation system
|
||||
│ ├── cli/ # CLI tool for orchestrating evaluations
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── index.ts # CLI entry point
|
||||
│ │ │ ├── commands/ # CLI commands (setup, run, report)
|
||||
│ │ │ ├── adapters/ # Benchmark adapters
|
||||
│ │ │ ├── db/ # Database management
|
||||
│ │ │ └── utils/ # Utility functions
|
||||
│ │ ├── package.json
|
||||
│ │ └── tsconfig.json
|
||||
│ ├── diff-edits/ # Diff editing evaluation suite
|
||||
│ │ ├── cases/ # Test case JSON files
|
||||
│ │ ├── results/ # Evaluation results
|
||||
│ │ ├── diff-apply/ # Diff application logic
|
||||
│ │ ├── parsing/ # Assistant message parsing
|
||||
│ │ └── prompts/ # System prompts
|
||||
│ ├── repositories/ # Cloned benchmark repositories
|
||||
│ │ ├── exercism/ # Modified Exercism (from pashpashpash/evals)
|
||||
│ │ ├── swe-bench/ # SWE-Bench repository
|
||||
│ │ ├── swelancer/ # SWELancer repository
|
||||
│ │ └── multi-swe/ # Multi-SWE-Bench repository
|
||||
│ ├── results/ # Evaluation results storage
|
||||
│ │ ├── runs/ # Individual run results
|
||||
│ │ └── reports/ # Generated reports
|
||||
│ └── README.md # This file
|
||||
└── ...
|
||||
evals/ # Main directory for evaluation system
|
||||
├── cli/ # CLI tool for orchestrating evaluations
|
||||
│ └── src/
|
||||
│ ├── index.ts # CLI entry point
|
||||
│ ├── commands/ # CLI commands (setup, run, report)
|
||||
│ ├── adapters/ # Benchmark adapters
|
||||
│ ├── db/ # Database management
|
||||
│ └── utils/ # Utility functions
|
||||
├── diff-edits/ # Diff editing evaluation suite
|
||||
│ ├── cases/ # Test case JSON files
|
||||
│ ├── results/ # Evaluation results
|
||||
│ ├── diff-apply/ # Diff application logic
|
||||
│ ├── parsing/ # Assistant message parsing
|
||||
│ └── prompts/ # System prompts
|
||||
├── repositories/ # Cloned benchmark repositories
|
||||
│ └── exercism/ # Exercism (Aider Polyglot)
|
||||
├── results/ # Evaluation results storage
|
||||
│ ├── runs/ # Individual run results
|
||||
│ └── reports/ # Generated reports
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
@@ -67,25 +51,14 @@ cline-repo/
|
||||
- VSCode with Cline extension installed
|
||||
- Git
|
||||
|
||||
### Activation Mechanism
|
||||
|
||||
The evaluation system uses an `evals.env` file approach to activate test mode in the Cline extension. When an evaluation is run:
|
||||
|
||||
1. The CLI creates an `evals.env` file in the workspace directory
|
||||
2. The Cline extension activates due to the `workspaceContains:evals.env` activation event
|
||||
3. The extension detects this file and automatically enters test mode
|
||||
4. After evaluation completes, the file is automatically removed
|
||||
|
||||
This approach eliminates the need for environment variables during the build process and allows for targeted activation only when needed for evaluations. The extension remains dormant during normal use, only activating when an evals.env file is present. For more details, see [Evals Env Activation](./docs/evals-env-activation.md).
|
||||
|
||||
### Installation
|
||||
|
||||
1. Build the CLI tool:
|
||||
|
||||
```bash
|
||||
cd evals/cli
|
||||
cd evals
|
||||
npm install
|
||||
npm run build
|
||||
npm run build:cli
|
||||
```
|
||||
|
||||
### Usage
|
||||
@@ -106,13 +79,14 @@ node dist/index.js setup --benchmarks exercism
|
||||
#### Running Evaluations
|
||||
|
||||
```bash
|
||||
node dist/index.js run --model claude-3-opus-20240229 --benchmark exercism
|
||||
node dist/index.js run --benchmark exercism --count 10
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--model`: The model to evaluate (default: claude-3-opus-20240229)
|
||||
- `--benchmark`: Specific benchmark to run (default: all)
|
||||
- `--count`: Number of tasks to run (default: all)
|
||||
- `--benchmark`: Specific benchmark to run (default: exercism)
|
||||
- `--count`: Number of tasks to run (default: all available tasks)
|
||||
|
||||
**Note:** Model selection is currently configured through the Cline CLI itself, not through evaluation flags.
|
||||
|
||||
#### Generating Reports
|
||||
|
||||
@@ -124,24 +98,11 @@ Options:
|
||||
- `--format`: Report format (json, markdown) (default: markdown)
|
||||
- `--output`: Output path for the report
|
||||
|
||||
#### Managing Test Mode Activation
|
||||
|
||||
The CLI provides a command to manually manage the evals.env file for test mode activation:
|
||||
|
||||
```bash
|
||||
node dist/index.js evals-env create # Create evals.env file in current directory
|
||||
node dist/index.js evals-env remove # Remove evals.env file from current directory
|
||||
node dist/index.js evals-env check # Check if evals.env file exists in current directory
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--directory`: Specify a directory other than the current one
|
||||
|
||||
## Benchmarks
|
||||
|
||||
### Exercism
|
||||
|
||||
Modified Exercism exercises from the [pashpashpash/evals](https://github.com/pashpashpash/evals) repository. These are small, focused programming exercises in various languages.
|
||||
Modified Exercism exercises from the [polyglot-benchmark](https://github.com/Aider-AI/polyglot-benchmark) repository. These are small, focused programming exercises in various languages.
|
||||
|
||||
### SWE-Bench (Coming Soon)
|
||||
|
||||
@@ -350,7 +311,8 @@ The evaluation system collects the following metrics:
|
||||
- **Duration**: Time taken to complete tasks
|
||||
- **Tool Usage**: Number of tool calls and failures
|
||||
- **Success Rate**: Percentage of tasks completed successfully
|
||||
- **Functional Correctness**: Percentage of tests passed
|
||||
- **Test Success Rate**: Percentage of tests passed
|
||||
- **Functional Correctness**: Ratio of tests passed to total tests
|
||||
|
||||
## Reports
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import chalk from "chalk"
|
||||
import execa from "execa"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
@@ -20,8 +21,12 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
|
||||
if (!fs.existsSync(exercismDir)) {
|
||||
console.log(`Cloning Exercism repository to ${exercismDir}...`)
|
||||
await execa("git", ["clone", "https://github.com/pashpashpash/evals.git", exercismDir])
|
||||
await execa("git", ["clone", "https://github.com/Aider-AI/polyglot-benchmark.git", exercismDir])
|
||||
console.log("Exercism repository cloned successfully")
|
||||
|
||||
// Unskip all JavaScript and Java tests after cloning
|
||||
this.unskipAllJavaScriptTests(exercismDir)
|
||||
this.unskipAllJavaTests(exercismDir)
|
||||
} else {
|
||||
console.log(`Exercism repository already exists at ${exercismDir}`)
|
||||
|
||||
@@ -29,6 +34,10 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
console.log("Pulling latest changes...")
|
||||
await execa("git", ["pull"], { cwd: exercismDir })
|
||||
console.log("Repository updated successfully")
|
||||
|
||||
// Unskip tests again after pulling
|
||||
this.unskipAllJavaScriptTests(exercismDir)
|
||||
this.unskipAllJavaTests(exercismDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +60,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
.filter((dir) => !dir.startsWith(".") && !["node_modules", ".git"].includes(dir))
|
||||
|
||||
for (const language of languages) {
|
||||
const languageDir = path.join(exercisesDir, language)
|
||||
const languageDir = path.join(exercisesDir, language, "exercises", "practice")
|
||||
|
||||
// Read exercise directories
|
||||
const exercises = fs.readdirSync(languageDir).filter((dir) => fs.statSync(path.join(languageDir, dir)).isDirectory())
|
||||
@@ -61,7 +70,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
|
||||
// Read instructions
|
||||
let description = ""
|
||||
const instructionsPath = path.join(exerciseDir, "docs", "instructions.md")
|
||||
const instructionsPath = path.join(exerciseDir, ".docs", "instructions.md")
|
||||
if (fs.existsSync(instructionsPath)) {
|
||||
description = fs.readFileSync(instructionsPath, "utf-8")
|
||||
}
|
||||
@@ -69,20 +78,23 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
// Determine test commands based on language
|
||||
let testCommands: string[] = []
|
||||
switch (language) {
|
||||
case "cpp":
|
||||
testCommands = ["cmake -DEXERCISM_RUN_ALL_TESTS=1 .", "make"]
|
||||
break
|
||||
case "javascript":
|
||||
testCommands = ["npm install", "npm test"]
|
||||
testCommands = ["npm install", "npm test -- --testNamePattern=."]
|
||||
break
|
||||
case "python":
|
||||
testCommands = ["python -m pytest -o markers=task *_test.py"]
|
||||
testCommands = ["python3 -m pytest -o markers=task *_test.py"]
|
||||
break
|
||||
case "go":
|
||||
testCommands = ["go test"]
|
||||
testCommands = ["GOWORK=off go test -v"]
|
||||
break
|
||||
case "java":
|
||||
testCommands = ["./gradlew test"]
|
||||
break
|
||||
case "rust":
|
||||
testCommands = ["cargo test"]
|
||||
testCommands = ["cargo test -- --include-ignored"]
|
||||
break
|
||||
default:
|
||||
testCommands = []
|
||||
@@ -118,53 +130,117 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Check if Git repository is already initialized
|
||||
const gitDirExists = fs.existsSync(path.join(task.workspacePath, ".git"))
|
||||
// Create temp directory outside workspace for hiding files
|
||||
const tempDir = path.join(EVALS_DIR, "temp-files", task.id)
|
||||
fs.mkdirSync(tempDir, { recursive: true })
|
||||
|
||||
try {
|
||||
// Initialize Git repository if needed
|
||||
if (!gitDirExists) {
|
||||
await execa("git", ["init"], { cwd: task.workspacePath })
|
||||
}
|
||||
// Read config.json to get solution and test files
|
||||
const configPath = path.join(task.workspacePath, ".meta", "config.json")
|
||||
let config: any = { files: { solution: [], test: [] } }
|
||||
|
||||
// Create a dummy file to ensure there's something to commit
|
||||
const dummyFilePath = path.join(task.workspacePath, ".eval-timestamp")
|
||||
fs.writeFileSync(dummyFilePath, new Date().toISOString())
|
||||
|
||||
// Add all files and commit
|
||||
await execa("git", ["add", "."], { cwd: task.workspacePath })
|
||||
|
||||
try {
|
||||
await execa("git", ["commit", "-m", "Initial commit"], { cwd: task.workspacePath })
|
||||
} catch (error: any) {
|
||||
// If commit fails because there are no changes, that's okay
|
||||
if (!error.stderr?.includes("nothing to commit")) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(`Warning: Git operations failed: ${error.message}`)
|
||||
console.warn("Continuing without Git initialization")
|
||||
if (fs.existsSync(configPath)) {
|
||||
config = JSON.parse(fs.readFileSync(configPath, "utf-8"))
|
||||
}
|
||||
|
||||
return task
|
||||
// Build enhanced description with instructions
|
||||
let description = ""
|
||||
const instructionsPath = path.join(task.workspacePath, ".docs", "instructions.md")
|
||||
const appendPath = path.join(task.workspacePath, ".docs", "instructions.append.md")
|
||||
|
||||
if (fs.existsSync(instructionsPath)) {
|
||||
description = fs.readFileSync(instructionsPath, "utf-8")
|
||||
}
|
||||
|
||||
if (fs.existsSync(appendPath)) {
|
||||
description += "\n\n" + fs.readFileSync(appendPath, "utf-8")
|
||||
}
|
||||
|
||||
// Add solution files constraint to description
|
||||
const solutionFiles = config.files.solution || []
|
||||
const fileList = solutionFiles.join(", ")
|
||||
description += `\n\nUse the above instructions to modify the supplied files: ${fileList}. Don't change the names of existing functions or classes, as they may be referenced from other code like unit tests, etc. Only use standard libraries, don't suggest installing any packages.`
|
||||
description +=
|
||||
" You should ignore all test or test related files in this directory. The final test file has been removed and will be used to evaluate your work after your implementation is complete. Think deeply about the problem prior to working on the implementation. Consider all edge cases and test your solution prior to finalizing."
|
||||
|
||||
// Move test files to temp directory
|
||||
if (config.files.test) {
|
||||
config.files.test.forEach((testFile: string) => {
|
||||
const src = path.join(task.workspacePath, testFile)
|
||||
if (fs.existsSync(src)) {
|
||||
const dest = path.join(tempDir, testFile)
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Move all dot directories (except .git) to temp directory
|
||||
const items = fs.readdirSync(task.workspacePath)
|
||||
items.forEach((item) => {
|
||||
if (item.startsWith(".") && item !== ".git") {
|
||||
const src = path.join(task.workspacePath, item)
|
||||
const stat = fs.statSync(src)
|
||||
if (stat.isDirectory()) {
|
||||
const dest = path.join(tempDir, item)
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...task,
|
||||
description,
|
||||
metadata: {
|
||||
...task.metadata,
|
||||
solutionFiles,
|
||||
tempDir,
|
||||
config,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution
|
||||
* Cleanup after task execution (restores hidden files from temp directory)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
async cleanupTask(task: Task): Promise<void> {
|
||||
const tempDir = path.join(EVALS_DIR, "temp-files", task.id)
|
||||
|
||||
if (fs.existsSync(tempDir)) {
|
||||
const items = fs.readdirSync(tempDir)
|
||||
items.forEach((item) => {
|
||||
const src = path.join(tempDir, item)
|
||||
const dest = path.join(task.workspacePath, item)
|
||||
// Only move if destination doesn't exist (keeps newer test artifacts like .pytest_cache)
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up temp directory
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution by running tests
|
||||
* @param task The task that was executed
|
||||
*/
|
||||
async verifyResult(task: Task): Promise<VerificationResult> {
|
||||
// Run verification commands
|
||||
let success = true
|
||||
let output = ""
|
||||
|
||||
for (const command of task.verificationCommands) {
|
||||
try {
|
||||
const [cmd, ...args] = command.split(" ")
|
||||
const { stdout } = await execa(cmd, args, { cwd: task.workspacePath })
|
||||
const { stdout, stderr } = await execa(command, {
|
||||
cwd: task.workspacePath,
|
||||
shell: true,
|
||||
})
|
||||
output += stdout + "\n"
|
||||
if (stderr) {
|
||||
output += stderr + "\n"
|
||||
}
|
||||
} catch (error: any) {
|
||||
success = false
|
||||
if (error.stdout) {
|
||||
@@ -176,13 +252,92 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse test results
|
||||
const testsPassed = (output.match(/PASS/g) || []).length
|
||||
const testsFailed = (output.match(/FAIL/g) || []).length
|
||||
// Log the raw output
|
||||
// console.log("\n=== TEST OUTPUT START ===")
|
||||
// console.log(output)
|
||||
// console.log("=== TEST OUTPUT END ===\n")
|
||||
|
||||
// Parse test results based on language
|
||||
const language = task.metadata.language
|
||||
let testsPassed = 0
|
||||
let testsFailed = 0
|
||||
|
||||
switch (language) {
|
||||
case "python":
|
||||
const pyPassMatch = output.match(/(\d+) passed/)
|
||||
const pyFailMatch = output.match(/(\d+) failed/)
|
||||
testsPassed = pyPassMatch ? parseInt(pyPassMatch[1]) : 0
|
||||
testsFailed = pyFailMatch ? parseInt(pyFailMatch[1]) : 0
|
||||
break
|
||||
|
||||
case "javascript":
|
||||
const jestMatch = output.match(/Tests:\s+(?:\d+ skipped,\s+)?(\d+) passed(?:,\s+(\d+) failed)?/)
|
||||
if (jestMatch) {
|
||||
testsPassed = parseInt(jestMatch[1])
|
||||
testsFailed = jestMatch[2] ? parseInt(jestMatch[2]) : 0
|
||||
} else {
|
||||
// Fallback to counting test suites
|
||||
testsPassed = (output.match(/PASS/g) || []).length
|
||||
testsFailed = (output.match(/FAIL/g) || []).length
|
||||
}
|
||||
break
|
||||
|
||||
case "go":
|
||||
// This incorrectly counts the parent, but minor and doesn't affect final boolean metric
|
||||
testsPassed = (output.match(/--- PASS:/g) || []).length
|
||||
testsFailed = (output.match(/--- FAIL:/g) || []).length
|
||||
break
|
||||
|
||||
case "rust":
|
||||
// Rust runs multiple test suites (unit, integration, doc tests)
|
||||
// Sum results across all test result lines
|
||||
const resultLines = output.match(/test result:.*?(\d+) passed; (\d+) failed/g)
|
||||
if (resultLines) {
|
||||
testsPassed = 0
|
||||
testsFailed = 0
|
||||
for (const line of resultLines) {
|
||||
const match = line.match(/(\d+) passed; (\d+) failed/)
|
||||
if (match) {
|
||||
testsPassed += parseInt(match[1])
|
||||
testsFailed += parseInt(match[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case "java":
|
||||
testsPassed = (output.match(/PASSED/g) || []).length
|
||||
testsFailed = (output.match(/FAILED/g) || []).length
|
||||
break
|
||||
|
||||
case "cpp":
|
||||
const cppAllPassedMatch = output.match(/All tests passed \(.*?(\d+) test cases?\)/)
|
||||
const cppTestCasesMatch = output.match(/test cases?: (\d+) \| (\d+) passed/)
|
||||
const cppFailedMatch = output.match(/(\d+) failed/)
|
||||
|
||||
if (cppAllPassedMatch) {
|
||||
// All tests passed - extract total test cases
|
||||
testsPassed = parseInt(cppAllPassedMatch[1])
|
||||
testsFailed = 0
|
||||
} else if (cppTestCasesMatch) {
|
||||
// Mixed results - extract passed count and calculate failed
|
||||
const totalTests = parseInt(cppTestCasesMatch[1])
|
||||
testsPassed = parseInt(cppTestCasesMatch[2])
|
||||
testsFailed = cppFailedMatch ? parseInt(cppFailedMatch[1]) : totalTests - testsPassed
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
// Fallback to generic PASS/FAIL counting
|
||||
testsPassed = (output.match(/PASS/g) || []).length
|
||||
testsFailed = (output.match(/FAIL/g) || []).length
|
||||
}
|
||||
|
||||
const testsTotal = testsPassed + testsFailed
|
||||
|
||||
return {
|
||||
success,
|
||||
rawOutput: output,
|
||||
metrics: {
|
||||
testsPassed,
|
||||
testsFailed,
|
||||
@@ -191,4 +346,280 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide test files by moving them to temp directory
|
||||
* @param task The task to hide test files for
|
||||
*/
|
||||
private hideTestFiles(task: Task): void {
|
||||
const tempDir = task.metadata.tempDir
|
||||
const config = task.metadata.config
|
||||
|
||||
if (config?.files?.test) {
|
||||
config.files.test.forEach((testFile: string) => {
|
||||
const src = path.join(task.workspacePath, testFile)
|
||||
if (fs.existsSync(src)) {
|
||||
const dest = path.join(tempDir, testFile)
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Hide dot directories again (except .git)
|
||||
const items = fs.readdirSync(task.workspacePath)
|
||||
items.forEach((item) => {
|
||||
if (item.startsWith(".") && item !== ".git") {
|
||||
const src = path.join(task.workspacePath, item)
|
||||
if (fs.existsSync(src)) {
|
||||
const stat = fs.statSync(src)
|
||||
if (stat.isDirectory()) {
|
||||
const dest = path.join(tempDir, item)
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore test files by moving them from temp directory
|
||||
* @param task The task to restore test files for
|
||||
*/
|
||||
private restoreTestFiles(task: Task): void {
|
||||
const tempDir = task.metadata.tempDir
|
||||
const config = task.metadata.config
|
||||
|
||||
if (config?.files?.test) {
|
||||
config.files.test.forEach((testFile: string) => {
|
||||
const src = path.join(tempDir, testFile)
|
||||
if (fs.existsSync(src)) {
|
||||
const dest = path.join(task.workspacePath, testFile)
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Restore dot directories (except .git)
|
||||
if (fs.existsSync(tempDir)) {
|
||||
const items = fs.readdirSync(tempDir)
|
||||
items.forEach((item) => {
|
||||
if (item.startsWith(".") && item !== ".git") {
|
||||
const src = path.join(tempDir, item)
|
||||
const dest = path.join(task.workspacePath, item)
|
||||
if (fs.existsSync(src) && !fs.existsSync(dest)) {
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds retry message with test errors and fix instructions
|
||||
* @param testOutput The raw test output showing errors
|
||||
* @param solutionFiles List of solution files to fix
|
||||
* @returns Formatted retry message
|
||||
*/
|
||||
private buildRetryMessage(testOutput: string, solutionFiles: string[]): string {
|
||||
const fileList = solutionFiles.join(", ")
|
||||
return `${testOutput}\n\nSee the testing errors above. The tests are correct, don't try and change them. Fix the code in ${fileList} to resolve the errors.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Unskip all JavaScript tests in the repository by replacing xtest with test
|
||||
* @param repoPath Path to the exercism repository
|
||||
*/
|
||||
private unskipAllJavaScriptTests(repoPath: string): void {
|
||||
const jsDir = path.join(repoPath, "javascript", "exercises", "practice")
|
||||
|
||||
if (!fs.existsSync(jsDir)) {
|
||||
console.log("JavaScript exercises directory not found, skipping test unskipping")
|
||||
return
|
||||
}
|
||||
|
||||
// Walk through all exercise directories
|
||||
const exercises = fs.readdirSync(jsDir).filter((dir) => {
|
||||
const fullPath = path.join(jsDir, dir)
|
||||
return fs.statSync(fullPath).isDirectory()
|
||||
})
|
||||
|
||||
let filesModified = 0
|
||||
for (const exercise of exercises) {
|
||||
const exerciseDir = path.join(jsDir, exercise)
|
||||
|
||||
// Find all .spec.js files
|
||||
const files = fs.readdirSync(exerciseDir).filter((file) => file.endsWith(".spec.js"))
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(exerciseDir, file)
|
||||
let content = fs.readFileSync(filePath, "utf-8")
|
||||
const originalContent = content
|
||||
|
||||
// Replace xtest with test to unskip tests
|
||||
content = content.replace(/xtest\(/g, "test(")
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(filePath, content)
|
||||
filesModified++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Unskipped tests in ${filesModified} JavaScript test files`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unskip all Java tests in the repository by removing @Disabled annotations
|
||||
* @param repoPath Path to the exercism repository
|
||||
*/
|
||||
private unskipAllJavaTests(repoPath: string): void {
|
||||
const javaDir = path.join(repoPath, "java", "exercises", "practice")
|
||||
|
||||
if (!fs.existsSync(javaDir)) {
|
||||
console.log("Java exercises directory not found, skipping test unskipping")
|
||||
return
|
||||
}
|
||||
|
||||
// Walk through all exercise directories
|
||||
const exercises = fs.readdirSync(javaDir).filter((dir) => {
|
||||
const fullPath = path.join(javaDir, dir)
|
||||
return fs.statSync(fullPath).isDirectory()
|
||||
})
|
||||
|
||||
let filesModified = 0
|
||||
for (const exercise of exercises) {
|
||||
const testDir = path.join(javaDir, exercise, "src", "test", "java")
|
||||
|
||||
if (!fs.existsSync(testDir)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find all .java test files
|
||||
const files = fs.readdirSync(testDir).filter((file) => file.endsWith(".java"))
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(testDir, file)
|
||||
let content = fs.readFileSync(filePath, "utf-8")
|
||||
const originalContent = content
|
||||
|
||||
// Remove @Disabled("Remove to run test") annotations
|
||||
content = content.replace(/@Disabled\("Remove to run test"\)\s*\n/g, "")
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(filePath, content)
|
||||
filesModified++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Unskipped tests in ${filesModified} Java test files`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a Cline task with automatic retry on test failure
|
||||
* Creates a new Cline instance, runs the task, verifies with tests,
|
||||
* and retries once if tests fail
|
||||
* @param task The task to execute
|
||||
* @returns The final verification result, or null
|
||||
*/
|
||||
async runTask(task: Task): Promise<VerificationResult | null> {
|
||||
const startTime = Date.now()
|
||||
let instanceAddress: string | null = null
|
||||
let attempts = 0
|
||||
let finalVerification: VerificationResult | null = null
|
||||
|
||||
try {
|
||||
// Step 1: Start a new Cline instance in the working directory
|
||||
const instanceResult = await execa("cline", ["instance", "new"], {
|
||||
cwd: task.workspacePath,
|
||||
stdin: "ignore",
|
||||
})
|
||||
|
||||
// Step 2: Parse the instance address from output
|
||||
const addressMatch = instanceResult.stdout.match(/Address:\s*([\d.]+:\d+)/)
|
||||
if (!addressMatch) {
|
||||
throw new Error("Failed to parse instance address from output")
|
||||
}
|
||||
instanceAddress = addressMatch[1]
|
||||
|
||||
// Step 3: Create the initial task on this specific instance
|
||||
await execa("cline", ["task", "new", "--yolo", "--address", instanceAddress, task.description], {
|
||||
cwd: task.workspacePath,
|
||||
stdin: "ignore",
|
||||
})
|
||||
|
||||
// Step 4: Wait for initial implementation to complete
|
||||
console.log(chalk.blue(`Waiting for first attempt to complete...`))
|
||||
await execa("cline", ["task", "view", "--follow-complete", "--address", instanceAddress], {
|
||||
cwd: task.workspacePath,
|
||||
stdin: "ignore",
|
||||
})
|
||||
|
||||
// Step 5: Run first test attempt
|
||||
console.log(chalk.blue(`Running tests (attempt 1)...`))
|
||||
this.restoreTestFiles(task)
|
||||
attempts = 1
|
||||
const firstVerification = await this.verifyResult(task)
|
||||
finalVerification = firstVerification
|
||||
|
||||
// Step 6: Retry if tests failed
|
||||
if (!firstVerification.success) {
|
||||
console.log(chalk.blue(`Tests failed on first attempt. Retrying...`))
|
||||
|
||||
// Hide test files again for retry
|
||||
this.hideTestFiles(task)
|
||||
|
||||
attempts = 2
|
||||
const solutionFiles = task.metadata.solutionFiles || []
|
||||
const retryMessage = this.buildRetryMessage(firstVerification.rawOutput || "", solutionFiles)
|
||||
|
||||
// Send retry task message
|
||||
await execa("cline", ["task", "send", "--yolo", "--address", instanceAddress], {
|
||||
cwd: task.workspacePath,
|
||||
input: retryMessage,
|
||||
})
|
||||
|
||||
// Follow retry until complete
|
||||
await execa("cline", ["task", "view", "--follow-complete", "--address", instanceAddress], {
|
||||
cwd: task.workspacePath,
|
||||
stdin: "ignore",
|
||||
})
|
||||
|
||||
// Run second test attempt (final)
|
||||
console.log(chalk.blue(`Running tests (attempt 2)...`))
|
||||
this.restoreTestFiles(task)
|
||||
const secondVerification = await this.verifyResult(task)
|
||||
finalVerification = secondVerification
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Task completed in ${(duration / 1000).toFixed(1)}s after ${attempts} attempt${attempts > 1 ? "s" : ""}`,
|
||||
),
|
||||
)
|
||||
|
||||
return finalVerification
|
||||
} catch (error: any) {
|
||||
const duration = Date.now() - startTime
|
||||
console.error(chalk.red(`Task failed after ${(duration / 1000).toFixed(1)}s: ${error.message}`))
|
||||
|
||||
return finalVerification
|
||||
} finally {
|
||||
// Step 7: Always clean up the instance, even if task failed
|
||||
if (instanceAddress) {
|
||||
try {
|
||||
await execa("cline", ["instance", "kill", instanceAddress], {
|
||||
stdin: "ignore",
|
||||
})
|
||||
} catch (cleanupError: any) {
|
||||
console.error(chalk.yellow(`Warning: Failed to kill instance ${instanceAddress}: ${cleanupError.message}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import { BenchmarkAdapter } from "./types"
|
||||
import { ExercismAdapter } from "./exercism"
|
||||
import { SWEBenchAdapter } from "./swe-bench"
|
||||
import { SWELancerAdapter } from "./swelancer"
|
||||
import { MultiSWEAdapter } from "./multi-swe"
|
||||
import { BenchmarkAdapter } from "./types"
|
||||
|
||||
// Registry of all available adapters
|
||||
const adapters: Record<string, BenchmarkAdapter> = {
|
||||
// Exercism is the primary adapter with real implementation
|
||||
exercism: new ExercismAdapter(),
|
||||
|
||||
// Dummy adapters for testing
|
||||
"swe-bench": new SWEBenchAdapter(),
|
||||
swelancer: new SWELancerAdapter(),
|
||||
"multi-swe": new MultiSWEAdapter(),
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Dummy adapter for the Multi-SWE-Bench benchmark
|
||||
*/
|
||||
export class MultiSWEAdapter implements BenchmarkAdapter {
|
||||
name = "multi-swe"
|
||||
|
||||
/**
|
||||
* Set up the Multi-SWE-Bench benchmark repository (dummy implementation)
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
console.log("Multi-SWE-Bench dummy setup completed")
|
||||
|
||||
// Create repositories directory if it doesn't exist
|
||||
const repoDir = path.join(EVALS_DIR, "repositories", "multi-swe")
|
||||
if (!fs.existsSync(repoDir)) {
|
||||
fs.mkdirSync(repoDir, { recursive: true })
|
||||
console.log(`Created dummy Multi-SWE-Bench directory at ${repoDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the Multi-SWE-Bench benchmark (dummy implementation)
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
return [
|
||||
{
|
||||
id: "multi-swe-task-1",
|
||||
name: "Multi-Language API Integration",
|
||||
description:
|
||||
"Implement a system that integrates a Python backend with a TypeScript frontend and a Rust processing service.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
languages: ["python", "typescript", "rust"],
|
||||
complexity: "high",
|
||||
type: "multi-swe",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "multi-swe-task-2",
|
||||
name: "Cross-Platform Mobile App",
|
||||
description: "Create a cross-platform mobile app using React Native with native modules in Swift and Kotlin.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
languages: ["javascript", "swift", "kotlin"],
|
||||
complexity: "medium",
|
||||
type: "multi-swe",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "multi-swe-task-3",
|
||||
name: "Microservice Architecture",
|
||||
description: "Design and implement a microservice architecture with services written in Go, Node.js, and Java.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
languages: ["go", "javascript", "java"],
|
||||
complexity: "high",
|
||||
type: "multi-swe",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution (dummy implementation)
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Create a dummy workspace for the task
|
||||
const taskDir = path.join(task.workspacePath, taskId)
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
|
||||
// Create a dummy file for the task
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "README.md"),
|
||||
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
|
||||
)
|
||||
|
||||
// Create additional dummy files based on task type
|
||||
if (task.id === "multi-swe-task-1") {
|
||||
// Python backend
|
||||
fs.mkdirSync(path.join(taskDir, "backend"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "backend", "app.py"),
|
||||
`# TODO: Implement Python backend\nfrom flask import Flask\n\napp = Flask(__name__)\n\n@app.route('/')\ndef hello():\n return "Hello, World!"\n`,
|
||||
)
|
||||
|
||||
// TypeScript frontend
|
||||
fs.mkdirSync(path.join(taskDir, "frontend"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "frontend", "app.ts"),
|
||||
`// TODO: Implement TypeScript frontend\nconsole.log('Frontend starting...');\n`,
|
||||
)
|
||||
|
||||
// Rust processing service
|
||||
fs.mkdirSync(path.join(taskDir, "processor"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "processor", "main.rs"),
|
||||
`// TODO: Implement Rust processing service\nfn main() {\n println!("Processor starting...");\n}\n`,
|
||||
)
|
||||
} else if (task.id === "multi-swe-task-2") {
|
||||
// React Native app
|
||||
fs.mkdirSync(path.join(taskDir, "app"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "app", "App.js"),
|
||||
`// TODO: Implement React Native app\nimport React from 'react';\nimport { View, Text } from 'react-native';\n\nexport default function App() {\n return (\n <View>\n <Text>Hello, World!</Text>\n </View>\n );\n}\n`,
|
||||
)
|
||||
|
||||
// Swift native module
|
||||
fs.mkdirSync(path.join(taskDir, "ios"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "ios", "NativeModule.swift"),
|
||||
`// TODO: Implement Swift native module\nimport Foundation\n\n@objc(NativeModule)\nclass NativeModule: NSObject {\n @objc\n func hello() -> String {\n return "Hello from Swift"\n }\n}\n`,
|
||||
)
|
||||
|
||||
// Kotlin native module
|
||||
fs.mkdirSync(path.join(taskDir, "android"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "android", "NativeModule.kt"),
|
||||
`// TODO: Implement Kotlin native module\npackage com.example.app\n\nclass NativeModule {\n fun hello(): String {\n return "Hello from Kotlin"\n }\n}\n`,
|
||||
)
|
||||
} else if (task.id === "multi-swe-task-3") {
|
||||
// Go service
|
||||
fs.mkdirSync(path.join(taskDir, "service-go"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "service-go", "main.go"),
|
||||
`// TODO: Implement Go service\npackage main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Go service starting...")\n}\n`,
|
||||
)
|
||||
|
||||
// Node.js service
|
||||
fs.mkdirSync(path.join(taskDir, "service-node"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "service-node", "server.js"),
|
||||
`// TODO: Implement Node.js service\nconsole.log('Node.js service starting...');\n`,
|
||||
)
|
||||
|
||||
// Java service
|
||||
fs.mkdirSync(path.join(taskDir, "service-java"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "service-java", "Main.java"),
|
||||
`// TODO: Implement Java service\npublic class Main {\n public static void main(String[] args) {\n System.out.println("Java service starting...");\n }\n}\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the task's workspace path to the task-specific directory
|
||||
return {
|
||||
...task,
|
||||
workspacePath: taskDir,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution (dummy implementation)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Always return success for dummy implementation
|
||||
return {
|
||||
success: true,
|
||||
metrics: {
|
||||
testsPassed: 1,
|
||||
testsFailed: 0,
|
||||
testsTotal: 1,
|
||||
functionalCorrectness: 1.0,
|
||||
crossLanguageIntegration: 0.9, // Dummy metric specific to Multi-SWE
|
||||
architectureQuality: 0.85, // Dummy metric specific to Multi-SWE
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Dummy adapter for the SWE-Bench benchmark
|
||||
*/
|
||||
export class SWEBenchAdapter implements BenchmarkAdapter {
|
||||
name = "swe-bench"
|
||||
|
||||
/**
|
||||
* Set up the SWE-Bench benchmark repository (dummy implementation)
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
console.log("SWE-Bench dummy setup completed")
|
||||
|
||||
// Create repositories directory if it doesn't exist
|
||||
const repoDir = path.join(EVALS_DIR, "repositories", "swe-bench")
|
||||
if (!fs.existsSync(repoDir)) {
|
||||
fs.mkdirSync(repoDir, { recursive: true })
|
||||
console.log(`Created dummy SWE-Bench directory at ${repoDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the SWE-Bench benchmark (dummy implementation)
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
return [
|
||||
{
|
||||
id: "swe-bench-task-1",
|
||||
name: "Fix React Component Bug",
|
||||
description: "Fix a bug in a React component where the state is not properly updated.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
repository: "facebook/react",
|
||||
issue: "#12345",
|
||||
type: "swe-bench",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swe-bench-task-2",
|
||||
name: "Optimize Database Query",
|
||||
description: "Optimize a slow database query in a Django application.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
repository: "django/django",
|
||||
issue: "#6789",
|
||||
type: "swe-bench",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swe-bench-task-3",
|
||||
name: "Fix Memory Leak",
|
||||
description: "Fix a memory leak in a Node.js application.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
repository: "nodejs/node",
|
||||
issue: "#9876",
|
||||
type: "swe-bench",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution (dummy implementation)
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Create a dummy workspace for the task
|
||||
const taskDir = path.join(task.workspacePath, taskId)
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
|
||||
// Create a dummy file for the task
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "README.md"),
|
||||
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Update the task's workspace path to the task-specific directory
|
||||
return {
|
||||
...task,
|
||||
workspacePath: taskDir,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution (dummy implementation)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Always return success for dummy implementation
|
||||
return {
|
||||
success: true,
|
||||
metrics: {
|
||||
testsPassed: 1,
|
||||
testsFailed: 0,
|
||||
testsTotal: 1,
|
||||
functionalCorrectness: 1.0,
|
||||
performanceImprovement: 0.25, // Dummy metric specific to SWE-Bench
|
||||
codeQuality: 0.9, // Dummy metric specific to SWE-Bench
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Dummy adapter for the SWELancer benchmark
|
||||
*/
|
||||
export class SWELancerAdapter implements BenchmarkAdapter {
|
||||
name = "swelancer"
|
||||
|
||||
/**
|
||||
* Set up the SWELancer benchmark repository (dummy implementation)
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
console.log("SWELancer dummy setup completed")
|
||||
|
||||
// Create repositories directory if it doesn't exist
|
||||
const repoDir = path.join(EVALS_DIR, "repositories", "swelancer")
|
||||
if (!fs.existsSync(repoDir)) {
|
||||
fs.mkdirSync(repoDir, { recursive: true })
|
||||
console.log(`Created dummy SWELancer directory at ${repoDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the SWELancer benchmark (dummy implementation)
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
return [
|
||||
{
|
||||
id: "swelancer-task-1",
|
||||
name: "Create Landing Page",
|
||||
description: "Create a responsive landing page for a new product using HTML, CSS, and JavaScript.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
client: "TechStartup Inc.",
|
||||
difficulty: "medium",
|
||||
type: "swelancer",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swelancer-task-2",
|
||||
name: "Build REST API",
|
||||
description: "Create a RESTful API for a blog application using Node.js and Express.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
client: "BlogCo",
|
||||
difficulty: "hard",
|
||||
type: "swelancer",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swelancer-task-3",
|
||||
name: "Fix CSS Layout Issues",
|
||||
description: "Fix layout issues in a responsive website across different screen sizes.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
client: "DesignAgency",
|
||||
difficulty: "easy",
|
||||
type: "swelancer",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution (dummy implementation)
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Create a dummy workspace for the task
|
||||
const taskDir = path.join(task.workspacePath, taskId)
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
|
||||
// Create a dummy file for the task
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "README.md"),
|
||||
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
|
||||
)
|
||||
|
||||
// Create additional dummy files based on task type
|
||||
if (task.id === "swelancer-task-1") {
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "index.html"),
|
||||
`<!DOCTYPE html>\n<html>\n<head>\n <title>Landing Page</title>\n</head>\n<body>\n <!-- TODO: Implement landing page -->\n</body>\n</html>`,
|
||||
)
|
||||
} else if (task.id === "swelancer-task-2") {
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "server.js"),
|
||||
`// TODO: Implement REST API\nconsole.log('Server starting...');`,
|
||||
)
|
||||
} else if (task.id === "swelancer-task-3") {
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "styles.css"),
|
||||
`/* TODO: Fix layout issues */\nbody {\n margin: 0;\n padding: 0;\n}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the task's workspace path to the task-specific directory
|
||||
return {
|
||||
...task,
|
||||
workspacePath: taskDir,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution (dummy implementation)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Always return success for dummy implementation
|
||||
return {
|
||||
success: true,
|
||||
metrics: {
|
||||
testsPassed: 1,
|
||||
testsFailed: 0,
|
||||
testsTotal: 1,
|
||||
functionalCorrectness: 1.0,
|
||||
clientSatisfaction: 0.95, // Dummy metric specific to SWELancer
|
||||
timeEfficiency: 0.85, // Dummy metric specific to SWELancer
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export interface Task {
|
||||
export interface VerificationResult {
|
||||
success: boolean
|
||||
metrics: Record<string, any>
|
||||
rawOutput?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,5 +28,7 @@ export interface BenchmarkAdapter {
|
||||
setup(): Promise<void>
|
||||
listTasks(): Promise<Task[]>
|
||||
prepareTask(taskId: string): Promise<Task>
|
||||
verifyResult(task: Task, result: any): Promise<VerificationResult>
|
||||
cleanupTask(task: Task): Promise<void>
|
||||
verifyResult(task: Task): Promise<VerificationResult>
|
||||
runTask(task: Task): Promise<VerificationResult | null>
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import * as path from "path"
|
||||
import chalk from "chalk"
|
||||
import { createEvalsEnvFile, removeEvalsEnvFile, checkEvalsEnvFile } from "../utils/evals-env"
|
||||
|
||||
interface EvalsEnvOptions {
|
||||
action: "create" | "remove" | "check"
|
||||
directory?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the evals-env command
|
||||
* @param options Command options
|
||||
*/
|
||||
export async function evalsEnvHandler(options: EvalsEnvOptions): Promise<void> {
|
||||
// Determine the directory to use - default to repository root instead of current directory
|
||||
const currentDir = process.cwd()
|
||||
const repoRoot = path.resolve(currentDir, "..", "..") // Navigate up from evals/cli to root
|
||||
const directory = options.directory || repoRoot
|
||||
|
||||
console.log(chalk.blue(`Working with directory: ${directory}`))
|
||||
|
||||
// Perform the requested action
|
||||
switch (options.action) {
|
||||
case "create":
|
||||
console.log(chalk.blue("Creating evals.env file..."))
|
||||
createEvalsEnvFile(directory)
|
||||
console.log(chalk.green("The Cline extension should now detect this file and enter test mode."))
|
||||
console.log(chalk.yellow("Note: You may need to reload VSCode for the changes to take effect."))
|
||||
break
|
||||
|
||||
case "remove":
|
||||
console.log(chalk.blue("Removing evals.env file..."))
|
||||
removeEvalsEnvFile(directory)
|
||||
console.log(chalk.green("The Cline extension should now exit test mode."))
|
||||
console.log(chalk.yellow("Note: You may need to reload VSCode for the changes to take effect."))
|
||||
break
|
||||
|
||||
case "check":
|
||||
console.log(chalk.blue("Checking for evals.env file..."))
|
||||
const exists = checkEvalsEnvFile(directory)
|
||||
if (exists) {
|
||||
console.log(chalk.green("The Cline extension should be in test mode."))
|
||||
} else {
|
||||
console.log(chalk.yellow("The Cline extension should not be in test mode."))
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
console.error(chalk.red(`Unknown action: ${options.action}`))
|
||||
console.log(chalk.yellow("Valid actions are: create, remove, check"))
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import chalk from "chalk"
|
||||
import * as fs from "fs"
|
||||
import ora from "ora"
|
||||
import * as path from "path"
|
||||
import { ResultsDatabase } from "../db"
|
||||
import { generateMarkdownReport } from "../utils/markdown"
|
||||
|
||||
@@ -34,7 +34,6 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
// Generate summary report
|
||||
const summary = {
|
||||
runs: runs.length,
|
||||
models: [...new Set(runs.map((run) => run.model))],
|
||||
benchmarks: [...new Set(runs.map((run) => run.benchmark))],
|
||||
tasks: 0,
|
||||
successRate: 0,
|
||||
@@ -45,6 +44,10 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
totalToolFailures: 0,
|
||||
toolSuccessRate: 0,
|
||||
toolUsage: {} as Record<string, { calls: number; failures: number }>,
|
||||
totalTests: 0,
|
||||
totalTestsPassed: 0,
|
||||
totalTestsFailed: 0,
|
||||
testSuccessRate: 0,
|
||||
}
|
||||
|
||||
let totalTasks = 0
|
||||
@@ -54,6 +57,9 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
let totalDuration = 0
|
||||
let totalToolCalls = 0
|
||||
let totalToolFailures = 0
|
||||
let totalTests = 0
|
||||
let totalTestsPassed = 0
|
||||
let totalTestsFailed = 0
|
||||
|
||||
for (const run of runs) {
|
||||
const tasks = db.getRunTasks(run.id)
|
||||
@@ -73,6 +79,14 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
totalCost += metrics.find((m) => m.name === "cost")?.value || 0
|
||||
totalDuration += metrics.find((m) => m.name === "duration")?.value || 0
|
||||
|
||||
// Collect test metrics
|
||||
const testsPassed = metrics.find((m) => m.name === "testsPassed")?.value || 0
|
||||
const testsFailed = metrics.find((m) => m.name === "testsFailed")?.value || 0
|
||||
const testsTotal = metrics.find((m) => m.name === "testsTotal")?.value || 0
|
||||
totalTestsPassed += testsPassed
|
||||
totalTestsFailed += testsFailed
|
||||
totalTests += testsTotal
|
||||
|
||||
// Collect tool call metrics
|
||||
totalToolCalls += task.total_tool_calls || 0
|
||||
totalToolFailures += task.total_tool_failures || 0
|
||||
@@ -99,6 +113,12 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
summary.totalToolFailures = totalToolFailures
|
||||
summary.toolSuccessRate = totalToolCalls > 0 ? 1 - totalToolFailures / totalToolCalls : 1.0
|
||||
|
||||
// Calculate test metrics
|
||||
summary.totalTests = totalTests
|
||||
summary.totalTestsPassed = totalTestsPassed
|
||||
summary.totalTestsFailed = totalTestsFailed
|
||||
summary.testSuccessRate = totalTests > 0 ? totalTestsPassed / totalTests : 0
|
||||
|
||||
summary.tasks = totalTasks
|
||||
summary.successRate = totalTasks > 0 ? successfulTasks / totalTasks : 0
|
||||
summary.averageTokens = totalTasks > 0 ? totalTokens / totalTasks : 0
|
||||
@@ -112,12 +132,15 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
const benchmarkRuns = runs.filter((run) => run.benchmark === benchmark)
|
||||
const benchmarkSummary = {
|
||||
runs: benchmarkRuns.length,
|
||||
models: [...new Set(benchmarkRuns.map((run) => run.model))],
|
||||
tasks: 0,
|
||||
successRate: 0,
|
||||
averageTokens: 0,
|
||||
averageCost: 0,
|
||||
averageDuration: 0,
|
||||
totalTests: 0,
|
||||
totalTestsPassed: 0,
|
||||
totalTestsFailed: 0,
|
||||
testSuccessRate: 0,
|
||||
}
|
||||
|
||||
let benchmarkTasks = 0
|
||||
@@ -125,6 +148,9 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
let benchmarkTotalTokens = 0
|
||||
let benchmarkTotalCost = 0
|
||||
let benchmarkTotalDuration = 0
|
||||
let benchmarkTotalTests = 0
|
||||
let benchmarkTotalTestsPassed = 0
|
||||
let benchmarkTotalTestsFailed = 0
|
||||
|
||||
for (const run of benchmarkRuns) {
|
||||
const tasks = db.getRunTasks(run.id)
|
||||
@@ -143,6 +169,14 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
|
||||
benchmarkTotalCost += metrics.find((m) => m.name === "cost")?.value || 0
|
||||
benchmarkTotalDuration += metrics.find((m) => m.name === "duration")?.value || 0
|
||||
|
||||
// Collect test metrics
|
||||
const testsPassed = metrics.find((m) => m.name === "testsPassed")?.value || 0
|
||||
const testsFailed = metrics.find((m) => m.name === "testsFailed")?.value || 0
|
||||
const testsTotal = metrics.find((m) => m.name === "testsTotal")?.value || 0
|
||||
benchmarkTotalTestsPassed += testsPassed
|
||||
benchmarkTotalTestsFailed += testsFailed
|
||||
benchmarkTotalTests += testsTotal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,60 +185,14 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
benchmarkSummary.averageTokens = benchmarkTasks > 0 ? benchmarkTotalTokens / benchmarkTasks : 0
|
||||
benchmarkSummary.averageCost = benchmarkTasks > 0 ? benchmarkTotalCost / benchmarkTasks : 0
|
||||
benchmarkSummary.averageDuration = benchmarkTasks > 0 ? benchmarkTotalDuration / benchmarkTasks : 0
|
||||
benchmarkSummary.totalTests = benchmarkTotalTests
|
||||
benchmarkSummary.totalTestsPassed = benchmarkTotalTestsPassed
|
||||
benchmarkSummary.totalTestsFailed = benchmarkTotalTestsFailed
|
||||
benchmarkSummary.testSuccessRate = benchmarkTotalTests > 0 ? benchmarkTotalTestsPassed / benchmarkTotalTests : 0
|
||||
|
||||
benchmarkReports[benchmark] = benchmarkSummary
|
||||
}
|
||||
|
||||
// Generate model-specific reports
|
||||
const modelReports: Record<string, any> = {}
|
||||
|
||||
for (const model of summary.models) {
|
||||
const modelRuns = runs.filter((run) => run.model === model)
|
||||
const modelSummary = {
|
||||
runs: modelRuns.length,
|
||||
benchmarks: [...new Set(modelRuns.map((run) => run.benchmark))],
|
||||
tasks: 0,
|
||||
successRate: 0,
|
||||
averageTokens: 0,
|
||||
averageCost: 0,
|
||||
averageDuration: 0,
|
||||
}
|
||||
|
||||
let modelTasks = 0
|
||||
let modelSuccessfulTasks = 0
|
||||
let modelTotalTokens = 0
|
||||
let modelTotalCost = 0
|
||||
let modelTotalDuration = 0
|
||||
|
||||
for (const run of modelRuns) {
|
||||
const tasks = db.getRunTasks(run.id)
|
||||
modelTasks += tasks.length
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.success) {
|
||||
modelSuccessfulTasks++
|
||||
}
|
||||
|
||||
const metrics = db.getTaskMetrics(task.id)
|
||||
|
||||
const tokensIn = metrics.find((m) => m.name === "tokensIn")?.value || 0
|
||||
const tokensOut = metrics.find((m) => m.name === "tokensOut")?.value || 0
|
||||
modelTotalTokens += tokensIn + tokensOut
|
||||
|
||||
modelTotalCost += metrics.find((m) => m.name === "cost")?.value || 0
|
||||
modelTotalDuration += metrics.find((m) => m.name === "duration")?.value || 0
|
||||
}
|
||||
}
|
||||
|
||||
modelSummary.tasks = modelTasks
|
||||
modelSummary.successRate = modelTasks > 0 ? modelSuccessfulTasks / modelTasks : 0
|
||||
modelSummary.averageTokens = modelTasks > 0 ? modelTotalTokens / modelTasks : 0
|
||||
modelSummary.averageCost = modelTasks > 0 ? modelTotalCost / modelTasks : 0
|
||||
modelSummary.averageDuration = modelTasks > 0 ? modelTotalDuration / modelTasks : 0
|
||||
|
||||
modelReports[model] = modelSummary
|
||||
}
|
||||
|
||||
// Save reports
|
||||
const reportDir = path.join(path.resolve(__dirname, "../../../"), "results", "reports")
|
||||
fs.mkdirSync(reportDir, { recursive: true })
|
||||
@@ -217,14 +205,12 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
|
||||
fs.writeFileSync(path.join(reportDir, `benchmarks-${timestamp}.json`), JSON.stringify(benchmarkReports, null, 2))
|
||||
|
||||
fs.writeFileSync(path.join(reportDir, `models-${timestamp}.json`), JSON.stringify(modelReports, null, 2))
|
||||
|
||||
spinner.succeed(`JSON reports generated in ${reportDir}`)
|
||||
} else {
|
||||
// Generate markdown report
|
||||
const outputPath = options.output || path.join(reportDir, `report-${timestamp}.md`)
|
||||
|
||||
generateMarkdownReport(summary, benchmarkReports, modelReports, outputPath)
|
||||
generateMarkdownReport(summary, benchmarkReports, outputPath)
|
||||
|
||||
spinner.succeed(`Markdown report generated at ${outputPath}`)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import * as path from "path"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import chalk from "chalk"
|
||||
import ora from "ora"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { getAdapter } from "../adapters"
|
||||
import { ResultsDatabase } from "../db"
|
||||
import { spawnVSCode, cleanupVSCode } from "../utils/vscode"
|
||||
import { sendTaskToServer } from "../utils/task"
|
||||
import { storeTaskResult } from "../utils/results"
|
||||
|
||||
interface RunOptions {
|
||||
benchmark?: string
|
||||
model: string
|
||||
count?: number
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,12 +16,10 @@ interface RunOptions {
|
||||
*/
|
||||
export async function runHandler(options: RunOptions): Promise<void> {
|
||||
// Determine which benchmarks to run
|
||||
const benchmarks = options.benchmark ? [options.benchmark] : ["exercism"] // Default to exercism for now
|
||||
const model = options.model
|
||||
const benchmarks = options.benchmark ? [options.benchmark] : ["exercism"] // Default to exercism
|
||||
const count = options.count || Infinity
|
||||
|
||||
console.log(chalk.blue(`Running evaluations for model: ${model}`))
|
||||
console.log(chalk.blue(`Benchmarks: ${benchmarks.join(", ")}`))
|
||||
console.log(chalk.blue(`Running evaluations for the following benchmarks: ${benchmarks.join(", ")}`))
|
||||
|
||||
// Create a run for each benchmark
|
||||
for (const benchmark of benchmarks) {
|
||||
@@ -36,7 +29,7 @@ export async function runHandler(options: RunOptions): Promise<void> {
|
||||
console.log(chalk.green(`\nStarting run for benchmark: ${benchmark}`))
|
||||
|
||||
// Create run in database
|
||||
db.createRun(runId, model, benchmark)
|
||||
db.createRun(runId, benchmark)
|
||||
|
||||
// Get adapter for this benchmark
|
||||
try {
|
||||
@@ -63,58 +56,47 @@ export async function runHandler(options: RunOptions): Promise<void> {
|
||||
const preparedTask = await adapter.prepareTask(task.id)
|
||||
prepareSpinner.succeed("Task prepared")
|
||||
|
||||
// Spawn VSCode
|
||||
console.log("Spawning VSCode...")
|
||||
await spawnVSCode(preparedTask.workspacePath)
|
||||
let cleanedUp = false
|
||||
|
||||
// Send task to server
|
||||
const sendSpinner = ora("Sending task to server...").start()
|
||||
try {
|
||||
const result = await sendTaskToServer(preparedTask.description, options.apiKey)
|
||||
sendSpinner.succeed("Task completed")
|
||||
// Run task using adapter's execution strategy
|
||||
const finalVerification = await adapter.runTask(preparedTask)
|
||||
|
||||
// Verify result
|
||||
const verifySpinner = ora("Verifying result...").start()
|
||||
const verification = await adapter.verifyResult(preparedTask, result)
|
||||
// Cleanup task
|
||||
const cleanupSpinner = ora("Cleaning up task...").start()
|
||||
await adapter.cleanupTask(preparedTask)
|
||||
cleanedUp = true
|
||||
cleanupSpinner.succeed("Cleanup complete")
|
||||
|
||||
// Use final verification from runTask
|
||||
const verification = finalVerification || (await adapter.verifyResult(preparedTask))
|
||||
|
||||
if (verification.success) {
|
||||
verifySpinner.succeed(
|
||||
`Verification successful: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
|
||||
console.log(
|
||||
chalk.green(`Tests passed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`),
|
||||
)
|
||||
} else {
|
||||
verifySpinner.fail(
|
||||
`Verification failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
|
||||
console.log(
|
||||
chalk.red(`Tests failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`),
|
||||
)
|
||||
}
|
||||
|
||||
// Store result
|
||||
const storeSpinner = ora("Storing result...").start()
|
||||
await storeTaskResult(runId, preparedTask, result, verification)
|
||||
await storeTaskResult(runId, preparedTask, {}, verification)
|
||||
storeSpinner.succeed("Result stored")
|
||||
|
||||
console.log(chalk.green(`Task completed. Success: ${verification.success}`))
|
||||
|
||||
// Clean up VS Code and temporary files
|
||||
const cleanupSpinner = ora("Cleaning up...").start()
|
||||
try {
|
||||
await cleanupVSCode(preparedTask.workspacePath)
|
||||
cleanupSpinner.succeed("Cleanup completed")
|
||||
} catch (cleanupError: any) {
|
||||
cleanupSpinner.fail(`Cleanup failed: ${cleanupError.message}`)
|
||||
console.error(chalk.yellow(cleanupError.stack))
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendSpinner.fail(`Task failed: ${error.message}`)
|
||||
console.error(chalk.red(error.stack))
|
||||
|
||||
// Clean up VS Code and temporary files even if the task failed
|
||||
const cleanupSpinner = ora("Cleaning up...").start()
|
||||
try {
|
||||
await cleanupVSCode(preparedTask.workspacePath)
|
||||
cleanupSpinner.succeed("Cleanup completed")
|
||||
} catch (cleanupError: any) {
|
||||
cleanupSpinner.fail(`Cleanup failed: ${cleanupError.message}`)
|
||||
console.error(chalk.yellow(cleanupError.stack))
|
||||
console.error(chalk.red(`Task failed: ${error.message}`))
|
||||
} finally {
|
||||
// Ensure cleanup always happens
|
||||
if (!cleanedUp) {
|
||||
try {
|
||||
const finalCleanupSpinner = ora("Performing cleanup...").start()
|
||||
await adapter.cleanupTask(preparedTask)
|
||||
finalCleanupSpinner.succeed("Cleanup complete")
|
||||
} catch (cleanupError: any) {
|
||||
console.error(chalk.red(`Cleanup failed: ${cleanupError.message}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +107,6 @@ export async function runHandler(options: RunOptions): Promise<void> {
|
||||
console.log(chalk.green(`\nRun complete for benchmark: ${benchmark}`))
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Error running benchmark ${benchmark}: ${error.message}`))
|
||||
console.error(error.stack)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import Database from "better-sqlite3"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { SCHEMA } from "./schema"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
@@ -34,16 +34,15 @@ export class ResultsDatabase {
|
||||
/**
|
||||
* Create a new evaluation run
|
||||
* @param id Run ID
|
||||
* @param model Model name
|
||||
* @param benchmark Benchmark name
|
||||
*/
|
||||
createRun(id: string, model: string, benchmark: string): void {
|
||||
createRun(id: string, benchmark: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO runs (id, timestamp, model, benchmark)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO runs (id, timestamp, benchmark)
|
||||
VALUES (?, ?, ?)
|
||||
`)
|
||||
|
||||
stmt.run(id, Date.now(), model, benchmark)
|
||||
stmt.run(id, Date.now(), benchmark)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ export const SCHEMA = `
|
||||
CREATE TABLE IF NOT EXISTS runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
timestamp INTEGER NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
benchmark TEXT NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
+10
-28
@@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
import { Command } from "commander"
|
||||
import chalk from "chalk"
|
||||
import { setupHandler } from "./commands/setup"
|
||||
import { runHandler } from "./commands/run"
|
||||
import { Command } from "commander"
|
||||
import { reportHandler } from "./commands/report"
|
||||
import { evalsEnvHandler } from "./commands/evals-env"
|
||||
import { runHandler } from "./commands/run"
|
||||
import { runDiffEvalHandler } from "./commands/runDiffEval"
|
||||
import { setupHandler } from "./commands/setup"
|
||||
|
||||
// Create the CLI program
|
||||
const program = new Command()
|
||||
@@ -17,11 +16,7 @@ program.name("cline-eval").description("CLI tool for orchestrating Cline evaluat
|
||||
program
|
||||
.command("setup")
|
||||
.description("Clone and set up benchmark repositories")
|
||||
.option(
|
||||
"-b, --benchmarks <benchmarks>",
|
||||
"Comma-separated list of benchmarks to set up",
|
||||
"exercism,swe-bench,swelancer,multi-swe",
|
||||
)
|
||||
.option("-b, --benchmarks <benchmarks>", "Comma-separated list of benchmarks to set up", "exercism")
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await setupHandler(options)
|
||||
@@ -36,9 +31,7 @@ program
|
||||
.command("run")
|
||||
.description("Run evaluations")
|
||||
.option("-b, --benchmark <benchmark>", "Specific benchmark to run")
|
||||
.option("-m, --model <model>", "Model to evaluate", "claude-3-opus-20240229")
|
||||
.option("-c, --count <count>", "Number of tasks to run", parseInt)
|
||||
.option("-k, --api-key <apiKey>", "Cline API key to use for evaluations")
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await runHandler(options)
|
||||
@@ -63,21 +56,6 @@ program
|
||||
}
|
||||
})
|
||||
|
||||
// Evals-env command
|
||||
program
|
||||
.command("evals-env")
|
||||
.description("Manage evals.env files for test mode activation")
|
||||
.argument("<action>", "Action to perform: create, remove, or check")
|
||||
.option("-d, --directory <directory>", "Directory to create/remove/check evals.env file in (defaults to current directory)")
|
||||
.action(async (action, options) => {
|
||||
try {
|
||||
await evalsEnvHandler({ action, ...options })
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error managing evals.env file: ${error instanceof Error ? error.message : String(error)}`))
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Run-diff-eval command
|
||||
program
|
||||
.command("run-diff-eval")
|
||||
@@ -86,11 +64,15 @@ program
|
||||
.option("--output-path <path>", "Path to the directory to save the test output JSON files")
|
||||
.option("--model-ids <model_ids>", "Comma-separated list of model IDs to test")
|
||||
.option("--system-prompt-name <name>", "The name of the system prompt to use", "basicSystemPrompt")
|
||||
.option("-n, --valid-attempts-per-case <number>", "Number of valid attempts per test case per model (will retry until this many valid attempts are collected)", "1")
|
||||
.option(
|
||||
"-n, --valid-attempts-per-case <number>",
|
||||
"Number of valid attempts per test case per model (will retry until this many valid attempts are collected)",
|
||||
"1",
|
||||
)
|
||||
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
|
||||
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
|
||||
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
|
||||
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
|
||||
.option("--diff-edit-function <name>", "The diff editing function to use", "diff-06-26-25")
|
||||
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
|
||||
.option("--provider <provider>", "API provider to use (openrouter, openai)", "openrouter")
|
||||
.option("--parallel", "Run tests in parallel", false)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import chalk from "chalk"
|
||||
|
||||
/**
|
||||
* Creates an evals.env file in the specified directory
|
||||
* @param directory The directory where the evals.env file should be created
|
||||
* @returns True if the file was created, false if it already exists
|
||||
*/
|
||||
export function createEvalsEnvFile(directory: string): boolean {
|
||||
const evalsEnvPath = path.join(directory, "evals.env")
|
||||
|
||||
// Check if the file already exists
|
||||
if (fs.existsSync(evalsEnvPath)) {
|
||||
console.log(chalk.yellow(`evals.env file already exists at ${evalsEnvPath}`))
|
||||
return false
|
||||
}
|
||||
|
||||
// Create the file
|
||||
try {
|
||||
const content = `# This file activates Cline test mode
|
||||
# Created at: ${new Date().toISOString()}
|
||||
#
|
||||
# This file is automatically detected by the Cline extension
|
||||
# and enables test mode for automated evaluations.
|
||||
#
|
||||
# Delete this file to deactivate test mode.
|
||||
`
|
||||
fs.writeFileSync(evalsEnvPath, content)
|
||||
console.log(chalk.green(`Created evals.env file at ${evalsEnvPath}`))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error creating evals.env file: ${error}`))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an evals.env file from the specified directory
|
||||
* @param directory The directory where the evals.env file should be removed
|
||||
* @returns True if the file was removed, false if it doesn't exist
|
||||
*/
|
||||
export function removeEvalsEnvFile(directory: string): boolean {
|
||||
const evalsEnvPath = path.join(directory, "evals.env")
|
||||
|
||||
// Check if the file exists
|
||||
if (!fs.existsSync(evalsEnvPath)) {
|
||||
console.log(chalk.yellow(`No evals.env file found at ${evalsEnvPath}`))
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove the file
|
||||
try {
|
||||
fs.unlinkSync(evalsEnvPath)
|
||||
console.log(chalk.green(`Removed evals.env file from ${evalsEnvPath}`))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error removing evals.env file: ${error}`))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an evals.env file exists in the specified directory
|
||||
* @param directory The directory to check for an evals.env file
|
||||
* @returns True if the file exists, false otherwise
|
||||
*/
|
||||
export function checkEvalsEnvFile(directory: string): boolean {
|
||||
const evalsEnvPath = path.join(directory, "evals.env")
|
||||
const exists = fs.existsSync(evalsEnvPath)
|
||||
|
||||
if (exists) {
|
||||
console.log(chalk.green(`evals.env file found at ${evalsEnvPath}`))
|
||||
} else {
|
||||
console.log(chalk.yellow(`No evals.env file found at ${evalsEnvPath}`))
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import execa from "execa"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
|
||||
/**
|
||||
* List of VSCode extensions to install for evaluation environments
|
||||
* These extensions provide language support and other useful features
|
||||
*/
|
||||
export const REQUIRED_EXTENSIONS = [
|
||||
"golang.go", // Go language support
|
||||
"dbaeumer.vscode-eslint", // ESLint support
|
||||
"redhat.java", // Java support
|
||||
"ms-python.python", // Python support
|
||||
"rust-lang.rust-analyzer", // Rust support
|
||||
"ms-vscode.cpptools", // C/C++ support
|
||||
]
|
||||
|
||||
/**
|
||||
* Install required VSCode extensions in the specified extensions directory
|
||||
* @param extensionsDir The directory where extensions should be installed
|
||||
* @returns Promise that resolves when all extensions are installed
|
||||
*/
|
||||
export async function installRequiredExtensions(extensionsDir: string): Promise<void> {
|
||||
console.log("Installing required VSCode extensions...")
|
||||
|
||||
// Create the extensions directory if it doesn't exist
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
fs.mkdirSync(extensionsDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Install each extension
|
||||
for (const extension of REQUIRED_EXTENSIONS) {
|
||||
try {
|
||||
console.log(`Installing extension: ${extension}...`)
|
||||
await execa("code", ["--extensions-dir", extensionsDir, "--install-extension", extension, "--force"])
|
||||
console.log(`✅ Extension ${extension} installed successfully`)
|
||||
} catch (error: any) {
|
||||
console.warn(`⚠️ Failed to install extension ${extension}: ${error.message}`)
|
||||
// Continue with other extensions even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ All required extensions installed")
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a VSCode extension is installed in the specified directory
|
||||
* @param extensionsDir The directory to check for installed extensions
|
||||
* @param extensionId The ID of the extension to check
|
||||
* @returns True if the extension is installed, false otherwise
|
||||
*/
|
||||
export function isExtensionInstalled(extensionsDir: string, extensionId: string): boolean {
|
||||
// Extensions are installed in directories named publisher.name-version
|
||||
// We need to check if any directory starts with the extensionId
|
||||
const extensionPrefix = extensionId.toLowerCase() + "-"
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(extensionsDir)
|
||||
return files.some((file) => {
|
||||
const lowerCaseFile = file.toLowerCase()
|
||||
return lowerCaseFile === extensionId.toLowerCase() || lowerCaseFile.startsWith(extensionPrefix)
|
||||
})
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the VSCode settings file in the specified user data directory
|
||||
* @param userDataDir The VSCode user data directory
|
||||
* @returns The path to the settings.json file
|
||||
*/
|
||||
export function getSettingsPath(userDataDir: string): string {
|
||||
const settingsDir = path.join(userDataDir, "User")
|
||||
fs.mkdirSync(settingsDir, { recursive: true })
|
||||
return path.join(settingsDir, "settings.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure extension settings in the VSCode user data directory
|
||||
* @param userDataDir The VSCode user data directory
|
||||
*/
|
||||
export function configureExtensionSettings(userDataDir: string): void {
|
||||
const settingsPath = getSettingsPath(userDataDir)
|
||||
|
||||
// Read existing settings if they exist
|
||||
let settings = {}
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"))
|
||||
} catch (error) {
|
||||
console.warn(`Error reading settings file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update extension-specific settings
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
// Go extension settings
|
||||
"go.toolsManagement.autoUpdate": false,
|
||||
"go.survey.prompt": false,
|
||||
|
||||
// ESLint settings
|
||||
"eslint.enable": true,
|
||||
"eslint.run": "onSave",
|
||||
|
||||
// Java settings
|
||||
"java.configuration.checkProjectSettingsExclusions": false,
|
||||
"java.configure.checkForOutdatedExtensions": false,
|
||||
"java.help.firstView": false,
|
||||
|
||||
// Python settings
|
||||
"python.experiments.enabled": false,
|
||||
"python.showStartPage": false,
|
||||
|
||||
// Rust settings
|
||||
"rust-analyzer.checkOnSave.command": "check",
|
||||
|
||||
// C/C++ settings
|
||||
"C_Cpp.intelliSenseEngine": "default",
|
||||
|
||||
// General extension settings
|
||||
"extensions.autoUpdate": false,
|
||||
"extensions.ignoreRecommendations": true,
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(updatedSettings, null, 2))
|
||||
console.log("✅ Extension settings configured")
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
|
||||
/**
|
||||
* Generate a markdown report from evaluation results
|
||||
* @param summary Overall summary
|
||||
* @param benchmarkReports Benchmark-specific reports
|
||||
* @param modelReports Model-specific reports
|
||||
* @param outputPath Output file path
|
||||
*/
|
||||
export function generateMarkdownReport(
|
||||
summary: any,
|
||||
benchmarkReports: Record<string, any>,
|
||||
modelReports: Record<string, any>,
|
||||
outputPath: string,
|
||||
): void {
|
||||
export function generateMarkdownReport(summary: any, benchmarkReports: Record<string, any>, outputPath: string): void {
|
||||
let markdown = `# Cline Evaluation Report\n\n`
|
||||
|
||||
// Generate summary section
|
||||
markdown += `## Summary\n\n`
|
||||
markdown += `- **Total Runs:** ${summary.runs}\n`
|
||||
markdown += `- **Models:** ${summary.models.join(", ")}\n`
|
||||
markdown += `- **Benchmarks:** ${summary.benchmarks.join(", ")}\n`
|
||||
markdown += `- **Total Tasks:** ${summary.tasks}\n`
|
||||
markdown += `- **Success Rate:** ${(summary.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Task Success Rate:** ${(summary.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Total Tests:** ${summary.totalTests}\n`
|
||||
markdown += `- **Tests Passed:** ${summary.totalTestsPassed}\n`
|
||||
markdown += `- **Tests Failed:** ${summary.totalTestsFailed}\n`
|
||||
markdown += `- **Test Success Rate:** ${(summary.testSuccessRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Average Tokens:** ${Math.round(summary.averageTokens)}\n`
|
||||
markdown += `- **Average Cost:** $${summary.averageCost.toFixed(4)}\n`
|
||||
markdown += `- **Average Duration:** ${(summary.averageDuration / 1000).toFixed(2)}s\n`
|
||||
@@ -48,23 +44,12 @@ export function generateMarkdownReport(
|
||||
for (const [benchmark, report] of Object.entries(benchmarkReports)) {
|
||||
markdown += `### ${benchmark}\n\n`
|
||||
markdown += `- **Runs:** ${report.runs}\n`
|
||||
markdown += `- **Models:** ${report.models.join(", ")}\n`
|
||||
markdown += `- **Tasks:** ${report.tasks}\n`
|
||||
markdown += `- **Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Average Tokens:** ${Math.round(report.averageTokens)}\n`
|
||||
markdown += `- **Average Cost:** $${report.averageCost.toFixed(4)}\n`
|
||||
markdown += `- **Average Duration:** ${(report.averageDuration / 1000).toFixed(2)}s\n\n`
|
||||
}
|
||||
|
||||
// Generate model results section
|
||||
markdown += `## Model Results\n\n`
|
||||
|
||||
for (const [model, report] of Object.entries(modelReports)) {
|
||||
markdown += `### ${model}\n\n`
|
||||
markdown += `- **Runs:** ${report.runs}\n`
|
||||
markdown += `- **Benchmarks:** ${report.benchmarks.join(", ")}\n`
|
||||
markdown += `- **Tasks:** ${report.tasks}\n`
|
||||
markdown += `- **Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Task Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Total Tests:** ${report.totalTests}\n`
|
||||
markdown += `- **Tests Passed:** ${report.totalTestsPassed}\n`
|
||||
markdown += `- **Tests Failed:** ${report.totalTestsFailed}\n`
|
||||
markdown += `- **Test Success Rate:** ${(report.testSuccessRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Average Tokens:** ${Math.round(report.averageTokens)}\n`
|
||||
markdown += `- **Average Cost:** $${report.averageCost.toFixed(4)}\n`
|
||||
markdown += `- **Average Duration:** ${(report.averageDuration / 1000).toFixed(2)}s\n\n`
|
||||
@@ -87,20 +72,6 @@ export function generateMarkdownReport(
|
||||
|
||||
markdown += "```\n\n"
|
||||
|
||||
// Success rate by model chart
|
||||
markdown += `### Success Rate by Model\n\n`
|
||||
markdown += "```mermaid\n"
|
||||
markdown += "graph TD\n"
|
||||
markdown += " title[Success Rate by Model]\n"
|
||||
markdown += " style title fill:none,stroke:none\n\n"
|
||||
|
||||
for (const [model, report] of Object.entries(modelReports)) {
|
||||
const successRate = (report.successRate * 100).toFixed(2)
|
||||
markdown += ` ${model.replace(/[-\.]/g, "_")}[${model}: ${successRate}%]\n`
|
||||
}
|
||||
|
||||
markdown += "```\n\n"
|
||||
|
||||
// Add timestamp
|
||||
markdown += `\n\n---\n\nReport generated on ${new Date().toISOString()}\n`
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user