Compare commits

..

1 Commits

Author SHA1 Message Date
Igor Tceglevskii 125ef8127c VS Code plugin system 2025-10-19 23:08:03 -07:00
640 changed files with 13187 additions and 42336 deletions
+45 -10
View File
@@ -1,19 +1,54 @@
#!/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.
echo "PostToolUse running inside local cline/.clinerules/hooks/ directory"
# Read the hook input (JSON via stdin)
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
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')
# 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
{
"cancel": false,
"contextModification": "PostToolUse response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "PostToolUse hook custom errorMessage"
"shouldContinue": true,
"contextModification": "TOOL_RESULT: The tool '$tool_name' completed with success=$success. Consider validating the results before proceeding to the next step."
}
EOF
+33 -10
View File
@@ -1,19 +1,42 @@
#!/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.
echo "PreToolUse running inside local cline/.clinerules/hooks/ directory"
# Read the hook input (JSON via stdin)
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
# Extract tool information
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName // "unknown"')
parameters=$(echo "$input" | jq -r '.preToolUse.parameters // {}')
# 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
{
"cancel": false,
"contextModification": "PreToolUse response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "PreToolUse hook custom errorMessage"
"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."
}
EOF
+76 -124
View File
@@ -3,8 +3,8 @@
## Overview
Cline hooks allow you to execute custom scripts at specific points in the agentic workflow. Hooks can be placed in either:
- **Global hooks directory**: `~/Documents/Cline/Hooks/` (applies to all workspaces)
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to the workspace the repo is part of)
- **Global hooks directory**: `~/Documents/Cline/Rules/Hooks/` (applies to all workspaces)
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to specific workspace)
Hooks run automatically when enabled.
@@ -17,54 +17,17 @@ Hooks run automatically when enabled.
## Available Hooks
### TaskStart Hook
- **When**: Runs when a NEW task is started (not when resuming)
- **Purpose**: Initialize task context, validate task requirements, set up environment
- **Global Location**: `~/Documents/Cline/Hooks/TaskStart`
- **Workspace Location**: `.clinerules/hooks/TaskStart`
### TaskResume Hook
- **When**: Runs when an EXISTING task is resumed (after user clicks resume button)
- **Purpose**: Validate resumed task state, restore context, check for changes since last run
- **Global Location**: `~/Documents/Cline/Hooks/TaskResume`
- **Workspace Location**: `.clinerules/hooks/TaskResume`
### TaskCancel Hook
- **When**: Runs when a task is cancelled or a hook is aborted by the user (only if there's actual active work or work was started)
- **Purpose**: Clean up resources, log cancellation, save state
- **Global Location**: `~/Documents/Cline/Hooks/TaskCancel`
- **Workspace Location**: `.clinerules/hooks/TaskCancel`
- **Note**: This hook is NOT cancellable
### TaskComplete Hook (coming soon!)
- **When**: Runs when a task is marked as complete
- **Purpose**: Log completion status, perform final cleanup, generate reports
- **Global Location**: `~/Documents/Cline/Hooks/TaskComplete`
- **Workspace Location**: `.clinerules/hooks/TaskComplete`
### UserPromptSubmit Hook
- **When**: Runs when the user submits a prompt/message (initial task, resume, or feedback)
- **Purpose**: Validate user input, preprocess prompts, add context to user messages
- **Global Location**: `~/Documents/Cline/Hooks/UserPromptSubmit`
- **Workspace Location**: `.clinerules/hooks/UserPromptSubmit`
### PreToolUse Hook
- **When**: Runs BEFORE a tool is executed
- **Purpose**: Validate parameters, block execution, or add context
- **Global Location**: `~/Documents/Cline/Hooks/PreToolUse`
- **Workspace Location**: `.clinerules/hooks/PreToolUse`
- **Global Location**: `~/Documents/Cline/Rules/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/Hooks/PostToolUse`
- **Workspace Location**: `.clinerules/hooks/PostToolUse`
### PreCompact Hook (coming soon!)
- **When**: Runs BEFORE the conversation context is compacted/truncated
- **Purpose**: Observe compaction events, log context management, track token usage
- **Global Location**: `~/Documents/Cline/Hooks/PreCompact`
- **Workspace Location**: `.clinerules/hooks/PreCompact`
- **Global Location**: `~/Documents/Cline/Rules/Hooks/PostToolUse` (all platforms)
- **Workspace Location**: `.clinerules/hooks/PostToolUse` (all platforms)
## Cross-Platform Hook Format
@@ -74,12 +37,13 @@ Cline uses a git-style approach for hooks that works consistently across all pla
- **No file extensions**: Hooks are named exactly `PreToolUse` or `PostToolUse` (no `.bat`, `.cmd`, `.sh` etc.)
- **Shebang required**: First line must be a shebang (e.g., `#!/usr/bin/env bash` or `#!/usr/bin/env node`)
- **Executable on Unix**: On Unix/Linux/macOS, hooks must be executable: `chmod +x PreToolUse`
- **Windows**: Not currently supported.
- **Windows**: No special permissions needed - hooks are executed through the shell
### How It Works
Like git hooks, Cline executes hook files through a shell that interprets the shebang line:
- On Unix/Linux/macOS: Native shell execution with shebang support
- On Windows: Shell execution handles shebang interpretation
This means:
- ✅ Same hook script works on all platforms
@@ -91,10 +55,16 @@ This means:
**On Unix/Linux/macOS:**
```bash
# Create hook file
nano ~/Documents/Cline/Hooks/PreToolUse
nano ~/Documents/Cline/Rules/Hooks/PreToolUse
# Make executable
chmod +x ~/Documents/Cline/Hooks/PreToolUse
chmod +x ~/Documents/Cline/Rules/Hooks/PreToolUse
```
**On Windows:**
```batch
REM Create hook file (note: no file extension)
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
```
## Context Injection Timing
@@ -137,46 +107,11 @@ All hooks receive:
```json
{
"clineVersion": "string",
"hookName": "TaskStart" | "TaskResume" | "TaskCancel" | "TaskComplete" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PreCompact",
"hookName": "PreToolUse" | "PostToolUse",
"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": {}
@@ -187,11 +122,6 @@ All hooks receive:
"result": "string",
"success": boolean,
"executionTimeMs": number
},
"preCompact": { // Only for PreCompact
"contextSize": number,
"messagesToCompact": number,
"compactionStrategy": "string"
}
}
```
@@ -201,21 +131,38 @@ All hooks receive:
All hooks must return:
```json
{
"cancel": boolean, // Required: false to continue, true to block execution
"contextModification": "string", // Optional: Context for future AI decisions
"shouldContinue": boolean, // Required: Allow or block execution
"contextModification": "string", // Optional: Context for future tool uses
"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:
- `WORKSPACE_RULES:` - Project conventions and requirements
- `FILE_OPERATIONS:` - File creation/modification patterns
- `TOOL_RESULT:` - Outcomes of tool executions
- `PERFORMANCE:` - Performance concerns
- `VALIDATION:` - Validation results
- Custom prefixes as needed
Example:
```bash
cat <<EOF
{
"shouldContinue": true,
"contextModification": "WORKSPACE_RULES: This is a TypeScript project. All new files must use .ts or .tsx extensions."
}
EOF
```
## Hook Execution Limits
- **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
- **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
## Common Use Cases
@@ -230,15 +177,15 @@ path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
cat <<EOF
{
"cancel": true,
"shouldContinue": false,
"errorMessage": "Cannot create .js files in TypeScript project",
"contextModification": "Use .ts/.tsx extensions only"
"contextModification": "WORKSPACE_RULES: Use .ts/.tsx extensions only"
}
EOF
exit 0
fi
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
```
### 2. Context Building - Learn from Operations
@@ -253,12 +200,12 @@ path=$(echo "$input" | jq -r '.postToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
cat <<EOF
{
"cancel": false,
"contextModification": "Created '$path'. Maintain consistency with this file's patterns in future operations."
"shouldContinue": true,
"contextModification": "FILE_OPERATIONS: Created '$path'. Maintain consistency with this file's patterns in future operations."
}
EOF
else
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
fi
```
@@ -273,12 +220,12 @@ tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
if [[ "$execution_time" -gt 5000 ]]; then
cat <<EOF
{
"cancel": false,
"contextModification": "Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
"shouldContinue": true,
"contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
}
EOF
else
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
fi
```
@@ -292,7 +239,7 @@ input=$(cat)
echo "$input" >> ~/.cline/hook-logs/tool-usage.jsonl
# Allow execution
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
```
## Global vs Workspace Hooks
@@ -300,40 +247,44 @@ echo '{"cancel": false}'
Cline supports two levels of hooks:
### Global Hooks
- **Location**: `~/Documents/Cline/Hooks/` (macOS/Linux)
- **Location**: `~/Documents/Cline/Rules/Hooks/` (macOS/Linux) or `%USERPROFILE%\Documents\Cline\Rules\Hooks\` (Windows)
- **Scope**: Apply to ALL workspaces and projects
- **Use Case**: Organization-wide policies, personal preferences, universal validations
- **Priority**: Order not guaranteed when combined with workspace hooks
- **Priority**: Execute FIRST, before 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**: Order not guaranteed when combined with global hooks
- **Priority**: Execute AFTER global hooks
### Hook Execution
When multiple hooks exist (global and/or workspace):
- 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
- 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
**Result Combination:**
- `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`)
- `shouldContinue`: Must be `true` from ALL hooks for execution to proceed
- `contextModification`: All context strings are concatenated
- `errorMessage`: All error messages are concatenated
### Setting Up Global Hooks
1. The global hooks directory is automatically created at:
- macOS/Linux: `~/Documents/Cline/Hooks/`
- macOS/Linux: `~/Documents/Cline/Rules/Hooks/`
- Windows: `%USERPROFILE%\Documents\Cline\Rules\Hooks\`
2. Add your hook script:
```bash
# Unix/Linux/macOS
nano ~/Documents/Cline/Hooks/PreToolUse
chmod +x ~/Documents/Cline/Hooks/PreToolUse
nano ~/Documents/Cline/Rules/Hooks/PreToolUse
chmod +x ~/Documents/Cline/Rules/Hooks/PreToolUse
# Windows
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
```
3. Enable hooks in Cline settings
@@ -343,18 +294,18 @@ When multiple hooks exist (global and/or workspace):
**Global Hook** (applies to all projects):
```bash
#!/usr/bin/env bash
# ~/Documents/Cline/Hooks/PreToolUse
# ~/Documents/Cline/Rules/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 '{"cancel": true, "errorMessage": "Global policy: Cannot modify package.json"}'
echo '{"shouldContinue": false, "errorMessage": "Global policy: Cannot modify package.json"}'
exit 0
fi
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
```
**Workspace Hook** (applies to specific project):
@@ -367,11 +318,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 '{"cancel": true, "errorMessage": "Project rule: Use .ts files only"}'
echo '{"shouldContinue": false, "errorMessage": "Project rule: Use .ts files only"}'
exit 0
fi
echo '{"cancel": false}'
echo '{"shouldContinue": true}'
```
**All hooks must allow execution for the tool to proceed.** Hooks may execute concurrently.
@@ -380,7 +331,7 @@ echo '{"cancel": false}'
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:
- **cancel**: If ANY hook returns `true`, execution is blocked
- **shouldContinue**: If ANY hook returns false, execution is blocked
- **contextModification**: All context modifications are concatenated
- **errorMessage**: All error messages are concatenated
@@ -401,6 +352,7 @@ If you have multiple workspace roots, you can place hooks in each root's `.cline
### Context Not Affecting Behavior
- Remember: context affects FUTURE decisions, not the current tool
- Use PreToolUse for validation (blocking) if you need immediate effect
- Ensure context modifications are clear and actionable
- Check that context isn't being truncated (50KB limit)
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "TaskCancel running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskCancel response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskCancel hook custom errorMessage"
}
EOF
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "TaskResume running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskResume response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskResume hook custom errorMessage"
}
EOF
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "TaskStart running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskStart response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskStart hook custom errorMessage"
}
EOF
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "UserPromptSubmit running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "UserPromptSubmit response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "UserPromptSubmit hook custom errorMessage"
}
EOF
+2 -2
View File
@@ -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-(--vscode-foreground)",
title: "font-bold text-[var(--vscode-foreground)]",
indicator:
"text-(--vscode-foreground) mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
"text-[var(--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>
+3 -2
View File
@@ -74,9 +74,10 @@ jobs:
CLINE_ENVIRONMENT: production
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: console,otlp
OTEL_METRICS_EXPORTER: console,otlp
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
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
+3 -2
View File
@@ -99,11 +99,12 @@ 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: console,otlp
OTEL_METRICS_EXPORTER: console,otlp
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
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"
-3
View File
@@ -20,9 +20,6 @@ eslint-rules/**
.husky/**
.env
# cli
cli/**
# Custom
**/demo.gif
.nvmrc
+1 -79
View File
@@ -1,90 +1,12 @@
# Changelog
## 3.37.1
- cf8dd1c: Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
- 02abbcf: Add AGENTS.md support
- 855db7d: feat(models): Add free minimax/mimax-m2 model to the model picker
## [3.37.0]
## Added
- GPT-5.1 with model-specific prompting: tailored system prompts, tool usage, focus chain, and deep-planning optimizations
- Nous Research provider with Hermes 4 model family and custom system prompts
- Switched to Aqua Voice's Avalon model in speech to text transcription
- Added Linux support for speech to text
- Model-family breakouts for deep-planning prompting, laying groundwork for enhanced slash commands
- Expanded HTTP proxy support throughout the codebase
- Improved focus chain prompting for frontier models (Anthropic, OpenAI, Gemini, xAI)
## Fixed
- Duplicate tool results prevention through existence checking
- XML entity escaping in model content processor
- Commit message generation in command palette
- OpenAI Compatible provider temperature parameter type conversion
## Documentation
- Added missing proto generation step in CONTRIBUTING.md
- New `npm run dev` script for streamlined terminal workflow (fixes #7335)
## [3.36.1]
- fix: remove native tool calling support from Gemini and XAI provider due to invalid tool names issues
- fix: disable native tool callings for grok code models
- Add MCP tool usage to GLM
- Removes reasoning_details content field from Anthropic providers
## [3.36.0]
- Add: Hooks allow you to inject custom logic into Cline's workflow
- Add: new provider AIhubmix
- Add: Use http_proxy, https_proxy and no_proxy in JetBrains
- Fix: Oca Token Refresh logic
- Fix: issues where assistant message with empty content is added to conversation history
- Fix: bug where the checkbox shows in the model selector dropdown
- Fix: Switch from defaultUserAgentProvider to customUserAgent for Bedrock
- Fix: support for `<think>` tags for better compatibility with open-source models
- Fix: refinements to the GLM-4.6 system prompt
## [3.35.1]
- Add: Hicap API integration as provider
- Fix: enable Add Header button in OpenAICompatibleProvider UI
- Fix: Remove orphaned tool_results after truncation and empty content field issues in native tool call
- Fix: render model description in markdown
## [3.35.0]
- Add native tool calling support with configurable setting.
- Auto-approve is now always-on with a redesigned expanding menu. Settings simplified and notifications moved to General Settings.
- added zai-glm-4.6 as a Cerebras model
- Created GPT5 family specific system prompt template
- Fix: show reasoning budget slider to models with valid thinking config
- Requesty base URL, and API key fixes
- Delete all Auth Tokens when logging out
- Support for <think> tags for models that prefer that over <thinking>
## [3.34.1]
- Added support for MiniMax provider with MiniMax-M2 model
- Remove Cline/code-supernova-1-million model
- Changes to allow users to manually enter model names (eg. presets) when using OpenRouter
## [3.34.0]
- Cline Teams is now free through 2025 for unlimited users. Includes Jetbrains, RBAC, centralized billing and more.
- Use the “exacto” versions of GLM-4.6, Kimi-K2, and Qwen3-Coder in the Cline provider for the best balance of cost, speed, accuracy and tool-calling.
## [3.33.1]
- Fix CLI installation copy text
## [3.33.0]
- Added Cline CLI (Preview)
- Added 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
+1 -7
View File
@@ -46,11 +46,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
```bash
npm run install:all
```
4. Generate Protocol Buffer files (required before first build):
```bash
npm run protos
```
5. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
4. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
@@ -89,10 +85,8 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
2. **Local Development**
- Run `npm run install:all` to install dependencies
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
- Run `npm run test` to run tests locally
- Run → Start Debugging or `>Debug: Select and Start Debugging` and wait for a new VS Code instance to open
- **Terminal Workflow**: Use `npm run dev` (generates protos + runs watch mode) or `npm run watch` (if protos already generated)
- Before submitting PR, run `npm run format:fix` to format your code
3. **Linux-specific Setup**
+2 -2
View File
@@ -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
# Cline \#1 on OpenRouter
<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]
> 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.
> 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.
---
+13 -13
View File
@@ -70,7 +70,7 @@
"noControlCharactersInRegex": "off",
"noShadowRestrictedNames": "off",
"noArrayIndexKey": "info",
"noAssignInExpressions": "info"
"noAssignInExpressions": "warn"
},
"complexity": {
"noUselessConstructor": "off",
@@ -82,7 +82,7 @@
"noStaticOnlyClass": "off"
},
"security": {
"noDangerouslySetInnerHtml": "info"
"noDangerouslySetInnerHtml": "warn"
}
}
},
@@ -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": [
+16 -15
View File
@@ -101,7 +101,10 @@ see the manual page: man cline`,
if !isUserReadyToUse(ctx, instanceAddress) {
// Create renderer for welcome messages
renderer := display.NewRenderer(global.Config.OutputFormat)
fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
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)
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
// Check if user cancelled - exit cleanly
@@ -116,7 +119,9 @@ see the manual page: man cline`,
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
}
fmt.Printf("\n%s\n\n", renderer.Dim("Setup complete, you can now use the Cline CLI"))
markdown = "## setup complete, you can now use the cline cli"
rendered = renderer.RenderMarkdown(markdown)
fmt.Printf("\n%s\n\n", rendered)
}
} else {
// User specified --address flag, use that
@@ -182,7 +187,6 @@ 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)
@@ -327,20 +331,17 @@ func getContentFromStdinAndArgs(args []string) (string, error) {
// Check if data is being piped to stdin
if (stat.Mode() & os.ModeCharDevice) == 0 {
// Only try to read if there's actually data available
if stat.Size() > 0 {
stdinBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("failed to read from stdin: %w", err)
}
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)
stdinContent := strings.TrimSpace(string(stdinBytes))
if stdinContent != "" {
if content.Len() > 0 {
content.WriteString(" ")
}
content.WriteString(stdinContent)
}
}
+2 -2
View File
@@ -8,10 +8,8 @@ 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
@@ -26,6 +24,7 @@ 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
@@ -46,6 +45,7 @@ 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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "1.0.3",
"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": {
@@ -20,7 +20,7 @@
"vscode-uri"
],
"engines": {
"node": ">=20.0.0"
"node": ">=18.0.0"
},
"keywords": [
"cline",
+5 -25
View File
@@ -6,38 +6,18 @@ import (
)
func NewAuthCommand() *cobra.Command {
cmd := &cobra.Command{
return &cobra.Command{
Use: "auth",
Short: "Authenticate a provider and configure what model is used",
Long: `Authenticate a provider and configure what model is used
Short: "Authenticate a provider and configure model used",
Long: `Authenticate a provider and configure model used
Interactive Mode:
Run without flags to open an interactive menu where you can:
This command opens an interactive menu where you can:
- Sign in to your Cline account
- Configure other LLM providers (Anthropic, OpenAI, etc.)
- Select and switch between AI models
- Manage provider settings
Quick Setup Mode:
Use flags to quickly configure a BYO provider non-interactively:
Examples:
cline auth --provider openai-native --apikey sk-xxx --modelid gpt-5
cline auth -p anthropic -k sk-ant-xxx -m claude-sonnet-4-5-20250929
cline auth -p openai-compatible -k xxx -m gpt-4 -b https://api.example.com/v1
Supported providers: openai-native, openai, anthropic, gemini, openrouter, xai, cerebras, ollama
Note: Bedrock provider requires interactive setup due to complex auth fields`,
- Manage provider settings`,
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
}
+20 -25
View File
@@ -5,7 +5,6 @@ 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"
@@ -39,7 +38,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 BYO API providers - always shown. Launches provider setup wizard
// ┃ Configure API provider - 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
@@ -69,25 +68,18 @@ 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 uth wizard
// No args: Show menu (ShowAuthMenuNoArgs)
return HandleAuthMenuNoArgs(ctx)
case 1, 2, 3, 4:
fmt.Println("Invalid positional arguments. Correct usage:")
fmt.Println(" cline auth --provider <provider> --apikey <key> --modelid <model> --baseurl <optional>")
return nil
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])
default:
return fmt.Errorf("too many arguments. Use flags for quick setup: --provider, --apikey, --modelid --baseurl(optional)")
return fmt.Errorf("quick BYO API setup is currently stubbed - not yet implemented")
}
}
@@ -173,34 +165,32 @@ 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 BYO API providers", AuthActionBYOSetup),
huh.NewOption("Configure API provider", AuthActionBYOSetup),
huh.NewOption("Exit authorization wizard", AuthActionExit),
)
} else {
options = []huh.Option[AuthAction]{
huh.NewOption("Authenticate with Cline account", AuthActionClineLogin),
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
huh.NewOption("Configure API provider", AuthActionBYOSetup),
huh.NewOption("Exit authorization wizard", AuthActionExit),
}
}
// Determine menu title based on status
var title string
renderer := display.NewRenderer(global.Config.OutputFormat)
// Always show Cline authentication status
if isClineAuthenticated {
title = fmt.Sprintf("Cline Account: %s Authenticated\n", renderer.Green("✓"))
title = "Cline Account: \033[32m✓\033[0m Authenticated\n"
} else {
title = fmt.Sprintf("Cline Account: %s Not authenticated\n", renderer.Red("✗"))
title = "Cline Account: \033[31m✗\033[0m Not authenticated\n"
}
// 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: %s\nActive Model: %s\n",
renderer.White(currentProvider),
renderer.White(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)
}
// Always end with a huh?
@@ -268,6 +258,11 @@ 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
+7 -241
View File
@@ -1,247 +1,13 @@
package auth
import (
"context"
"fmt"
"strings"
import "fmt"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
)
// Package-level variables for command-line flags
var (
QuickProvider string // Provider ID (e.g., "openai", "anthropic")
QuickAPIKey string // API key for the provider
QuickModelID string // Model ID to configure
QuickBaseURL string // Base URL (optional, for openai compatible only)
)
// QuickSetupFromFlags performs quick setup using command-line flags
// Returns error if validation fails or configuration cannot be applied
func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL string) error {
// Validate all input parameters
providerEnum, err := validateQuickSetupInputs(provider, apiKey, modelID, baseURL)
if err != nil {
return err
}
// Create task manager for state operations
manager, err := createTaskManager(ctx)
if err != nil {
return fmt.Errorf("failed to create task manager: %w", err)
}
// Validate and fetch model information if needed
finalModelID, modelInfo, err := validateAndFetchModel(ctx, manager, providerEnum, modelID, apiKey)
if err != nil {
return fmt.Errorf("model validation failed: %w", err)
}
// For Ollama, baseURL is stored in the API key field
finalAPIKey := apiKey
finalBaseURL := baseURL
if providerEnum == cline.ApiProvider_OLLAMA {
if baseURL != "" {
finalAPIKey = baseURL
finalBaseURL = ""
} else if apiKey != "" {
// User provided API key for Ollama - treat it as baseURL
finalAPIKey = apiKey
finalBaseURL = ""
} else {
// Use default Ollama baseURL
finalAPIKey = "http://localhost:11434"
finalBaseURL = ""
}
}
// Configure the provider using existing AddProviderPartial function
if err := AddProviderPartial(ctx, manager, providerEnum, finalModelID, finalAPIKey, finalBaseURL, modelInfo); err != nil {
return fmt.Errorf("failed to configure provider: %w", err)
}
// Set the provider as active for both Plan and Act modes
if err := UpdateProviderPartial(ctx, manager, providerEnum, ProviderUpdatesPartial{}, true); err != nil {
return fmt.Errorf("failed to set provider as active: %w", err)
}
// Mark welcome view as completed
if err := markWelcomeViewCompleted(ctx, manager); err != nil {
// Non-fatal error, just log it
if global.Config.Verbose {
fmt.Printf("[DEBUG] Warning: failed to mark welcome view as completed: %v\n", err)
}
}
// Flush pending state changes to disk immediately
// This ensures all configuration changes are persisted before the instance terminates
if _, err := manager.GetClient().State.FlushPendingState(ctx, &cline.EmptyRequest{}); err != nil {
return fmt.Errorf("failed to flush pending state: %w", err)
}
// Success message
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
fmt.Printf(" Model: %s\n", finalModelID)
if providerEnum == cline.ApiProvider_OLLAMA {
fmt.Printf(" Base URL: %s\n", finalAPIKey)
} else {
fmt.Println(" API Key: Configured")
}
if finalBaseURL != "" {
fmt.Printf(" Custom Base URL: %s\n", finalBaseURL)
}
fmt.Println("\nYou can now use Cline with this provider.")
fmt.Println("Run 'cline start' to begin a new task.")
return nil
}
// validateQuickSetupInputs validates all input parameters for quick setup
// Returns the validated provider enum or an error if validation fails
func validateQuickSetupInputs(provider, apiKey, modelID, baseURL string) (cline.ApiProvider, error) {
// Validate required parameters
if provider == "" {
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("provider is required. Use --provider or -p flag")
}
if strings.TrimSpace(apiKey) == "" && provider != "ollama" {
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("API key is required for %s provider. Use --apikey or -k flag", provider)
}
if strings.TrimSpace(modelID) == "" {
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("model ID is required. Use --modelid or -m flag")
}
// Validate and map provider string to enum
providerEnum, err := validateQuickSetupProvider(provider)
if err != nil {
return cline.ApiProvider_ANTHROPIC, err
}
// Validate that baseURL is only provided for OpenAI-compatible providers
if err := validateBaseURL(baseURL, providerEnum); err != nil {
return cline.ApiProvider_ANTHROPIC, err
}
return providerEnum, nil
}
// validateBaseURL checks if the user's input includes a baseURL for a provider other than OpenAI (compatible)
// Returns error if baseURL is provided for unsupported providers
func validateBaseURL(baseURL string, providerEnum cline.ApiProvider) error {
if providerEnum != cline.ApiProvider_OPENAI {
if baseURL != "" {
return fmt.Errorf("base URL is only supported for OpenAI and OpenAI-compatible providers")
}
// 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>")
}
return nil
}
// validateQuickSetupProvider validates the provider ID and returns the enum value
// Returns error if provider is invalid or not supported for quick setup
func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
// Normalize provider ID (trim whitespace, lowercase)
normalizedID := strings.TrimSpace(strings.ToLower(providerID))
// Explicitly block Bedrock
if normalizedID == "bedrock" {
return cline.ApiProvider_BEDROCK, fmt.Errorf("bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup: cline auth")
}
// Map provider string to enum using existing function
provider, ok := mapProviderStringToEnum(normalizedID)
if !ok {
// Provider not found - provide helpful error message
supportedProviders := []string{
"openai-native", "openai", "anthropic", "gemini",
"openrouter", "xai", "cerebras", "ollama",
}
return cline.ApiProvider_ANTHROPIC, fmt.Errorf(
"invalid provider '%s'. Supported providers: %s",
providerID,
strings.Join(supportedProviders, ", "),
)
}
// Validate against supported quick setup providers
supportedProviders := map[cline.ApiProvider]bool{
cline.ApiProvider_OPENAI_NATIVE: true,
cline.ApiProvider_OPENAI: true,
cline.ApiProvider_ANTHROPIC: true,
cline.ApiProvider_GEMINI: true,
cline.ApiProvider_OPENROUTER: true,
cline.ApiProvider_XAI: true,
cline.ApiProvider_CEREBRAS: true,
cline.ApiProvider_OLLAMA: true,
cline.ApiProvider_NOUSRESEARCH: true,
}
if !supportedProviders[provider] {
return provider, fmt.Errorf(
"provider '%s' is not supported for quick setup. Please use interactive setup: cline auth",
providerID,
)
}
return provider, nil
}
// validateAndFetchModel validates the model ID or fetches from provider if needed
// Returns the final model ID and optional model info
// For providers with static models, validates against the list
// For providers with dynamic models, fetches the list if possible
func validateAndFetchModel(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID, apiKey string) (string, interface{}, error) {
// Normalize model ID
modelID = strings.TrimSpace(modelID)
if modelID == "" {
return "", nil, fmt.Errorf("model ID cannot be empty")
}
// For most providers, we trust the user's input since we can't easily validate without making API calls
// The actual validation will happen when the model is used
switch provider {
case cline.ApiProvider_OPENROUTER:
// OpenRouter supports model info fetching, but it requires an API call
// For quick setup, we'll trust the user's input and return nil for model info
// The actual model info will be fetched when needed
if global.Config.Verbose {
fmt.Printf("[DEBUG] OpenRouter model ID: %s (will be validated on first use)\n", modelID)
}
return modelID, nil, nil
case cline.ApiProvider_OLLAMA:
// Ollama models can be validated by fetching the list, but this requires the server to be running
// For quick setup, we'll trust the user's input
if global.Config.Verbose {
fmt.Printf("[DEBUG] Ollama model ID: %s (will be validated when server is accessible)\n", modelID)
}
return modelID, nil, nil
default:
// For other providers (Anthropic, OpenAI, Gemini, XAI, Cerebras), trust user input
// Model validation will occur when the model is actually used
if global.Config.Verbose {
fmt.Printf("[DEBUG] %s model ID: %s (will be validated on first use)\n", GetProviderDisplayName(provider), modelID)
}
return modelID, nil, nil
}
}
// markWelcomeViewCompleted marks the welcome view as completed in the state
// This prevents the welcome view from showing up after quick setup
func markWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
// Use the State service to update the welcome view flag
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
if err != nil {
return fmt.Errorf("failed to mark welcome view as completed: %w", err)
}
if global.Config.Verbose {
fmt.Println("[DEBUG] Marked welcome view as completed")
}
return nil
}
+1
View File
@@ -0,0 +1 @@
package auth
+4 -24
View File
@@ -14,33 +14,13 @@ 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.RefreshOpenRouterModelsRpc(ctx, &cline.EmptyRequest{})
resp, err := manager.GetClient().Models.RefreshOpenRouterModels(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) {
@@ -120,9 +100,9 @@ func ConvertModelsMapToSlice(models map[string]interface{}) []string {
return result
}
// ConvertOcaModelsToInterface converts Oca model map to generic interface map.
// This allows Oca and Cline models to be used with the generic fetching utilities.
func ConvertOcaModelsToInterface(models map[string]*cline.OcaModelInfo) map[string]interface{} {
// 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
+13 -20
View File
@@ -18,16 +18,14 @@ type BYOProviderOption struct {
func GetBYOProviderList() []BYOProviderOption {
return []BYOProviderOption{
{Name: "Anthropic", Provider: cline.ApiProvider_ANTHROPIC},
{Name: "OpenAI Compatible", Provider: cline.ApiProvider_OPENAI},
{Name: "OpenAI (Official)", Provider: cline.ApiProvider_OPENAI_NATIVE},
{Name: "OpenAI", Provider: cline.ApiProvider_OPENAI},
{Name: "OpenAI Native", Provider: cline.ApiProvider_OPENAI_NATIVE},
{Name: "OpenRouter", Provider: cline.ApiProvider_OPENROUTER},
{Name: "X AI (Grok)", Provider: cline.ApiProvider_XAI},
{Name: "AWS Bedrock", Provider: cline.ApiProvider_BEDROCK},
{Name: "Google Gemini", Provider: cline.ApiProvider_GEMINI},
{Name: "Ollama", Provider: cline.ApiProvider_OLLAMA},
{Name: "Cerebras", Provider: cline.ApiProvider_CEREBRAS},
{Name: "NousResearch", Provider: cline.ApiProvider_NOUSRESEARCH},
{Name: "Oracle Code Assist", Provider: cline.ApiProvider_OCA},
}
}
@@ -73,8 +71,6 @@ func SupportsBYOModelFetching(provider cline.ApiProvider) bool {
return true
case cline.ApiProvider_OLLAMA:
return true
case cline.ApiProvider_OCA:
return true
}
return SupportsStaticModelList(provider)
@@ -86,9 +82,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., openai/gpt-oss-120b"
case cline.ApiProvider_OPENAI_NATIVE:
return "e.g., gpt-5-2025-08-07"
case cline.ApiProvider_OPENAI_NATIVE:
return "e.g., openai/gpt-oss-120b"
case cline.ApiProvider_OPENROUTER:
return "e.g., google/gemini-2.0-flash-exp:free"
case cline.ApiProvider_XAI:
@@ -101,10 +97,6 @@ func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
return "e.g., qwen3-coder:30b"
case cline.ApiProvider_CEREBRAS:
return "e.g., gpt-oss-120b"
case cline.ApiProvider_NOUSRESEARCH:
return "e.g., Hermes-4-405B"
case cline.ApiProvider_OCA:
return "e.g., oca/llama4"
default:
return "Enter model ID"
}
@@ -135,8 +127,8 @@ func GetBYOAPIKeyFieldConfig(provider cline.ApiProvider) APIKeyFieldConfig {
}
// PromptForAPIKey prompts the user to enter an API key (or base URL for Ollama).
// For OpenAI (Compatible) provider, also prompts for an optional base URL.
func PromptForAPIKey(provider cline.ApiProvider) (string, string, error) {
// For OpenAI Native provider, also prompts for an optional base URL.
func PromptForAPIKey(provider cline.ApiProvider) (string, error) {
var apiKey string
config := GetBYOAPIKeyFieldConfig(provider)
@@ -157,11 +149,11 @@ func PromptForAPIKey(provider cline.ApiProvider) (string, 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 (Compatible) provider, prompt for base URL
if provider == cline.ApiProvider_OPENAI {
// For OpenAI Native provider, also prompt for base URL
if provider == cline.ApiProvider_OPENAI_NATIVE {
var baseURL string
baseURLForm := huh.NewForm(
huh.NewGroup(
@@ -174,11 +166,12 @@ func PromptForAPIKey(provider cline.ApiProvider) (string, 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)
}
return apiKey, baseURL, nil
// TODO - connect baseURL
_ = baseURL
}
return apiKey, "", nil
return apiKey, nil
}
+16 -56
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
@@ -111,9 +110,6 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
cline.ApiProvider_GEMINI,
cline.ApiProvider_OLLAMA,
cline.ApiProvider_CEREBRAS,
cline.ApiProvider_NOUSRESEARCH,
cline.ApiProvider_OCA,
cline.ApiProvider_HICAP,
}
// Check each provider to see if it's ready to use
@@ -124,23 +120,16 @@ 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)
// 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
}
if modelID == "" {
continue
}
// Get base URL for Ollama
@@ -156,7 +145,7 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
Mode: "Ready",
Provider: provider,
ModelID: modelID,
HasAPIKey: checkAPIKeyExists(r.apiConfig, provider),
HasAPIKey: hasAPIKey,
BaseURL: baseURL,
})
seenProviders[provider] = true
@@ -214,15 +203,13 @@ func extractProviderFromState(stateData map[string]interface{}, mode string) *Pr
// mapProviderStringToEnum converts provider string from state to ApiProvider enum
// Returns (provider, ok) where ok is false if the provider is unknown
func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
normalizedStr := strings.ToLower(providerStr)
// Map string values to enum values
switch normalizedStr {
switch providerStr {
case "anthropic":
return cline.ApiProvider_ANTHROPIC, true
case "openai", "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
case "openai":
return cline.ApiProvider_OPENAI, true
case "openai-native": // This is the native, official Open AI provider
case "openai-native":
return cline.ApiProvider_OPENAI_NATIVE, true
case "openrouter":
return cline.ApiProvider_OPENROUTER, true
@@ -238,12 +225,6 @@ func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
return cline.ApiProvider_CEREBRAS, true
case "cline":
return cline.ApiProvider_CLINE, true
case "oca":
return cline.ApiProvider_OCA, true
case "hicap":
return cline.ApiProvider_HICAP, true
case "nousResearch":
return cline.ApiProvider_NOUSRESEARCH, true
default:
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
}
@@ -256,7 +237,7 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
case cline.ApiProvider_ANTHROPIC:
return "anthropic"
case cline.ApiProvider_OPENAI:
return "openai-compatible"
return "openai"
case cline.ApiProvider_OPENAI_NATIVE:
return "openai-native"
case cline.ApiProvider_OPENROUTER:
@@ -273,12 +254,6 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
return "cerebras"
case cline.ApiProvider_CLINE:
return "cline"
case cline.ApiProvider_OCA:
return "oca"
case cline.ApiProvider_HICAP:
return "hicap"
case cline.ApiProvider_NOUSRESEARCH:
return "nousResearch"
default:
return ""
}
@@ -337,9 +312,9 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
case cline.ApiProvider_ANTHROPIC:
return "Anthropic"
case cline.ApiProvider_OPENAI:
return "OpenAI Compatible"
return "OpenAI"
case cline.ApiProvider_OPENAI_NATIVE:
return "OpenAI (Official)"
return "OpenAI Native"
case cline.ApiProvider_OPENROUTER:
return "OpenRouter"
case cline.ApiProvider_XAI:
@@ -354,12 +329,6 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
return "Cerebras"
case cline.ApiProvider_CLINE:
return "Cline (Official)"
case cline.ApiProvider_OCA:
return "Oracle Code Assist"
case cline.ApiProvider_HICAP:
return "Hicap"
case cline.ApiProvider_NOUSRESEARCH:
return "NousResearch"
default:
return "Unknown"
}
@@ -409,7 +378,7 @@ func FormatProviderList(result *ProviderListResult) string {
} else {
output.WriteString(" Base URL: (default)\n")
}
} else if display.Provider == cline.ApiProvider_CLINE || display.Provider == cline.ApiProvider_OCA {
} else if display.Provider == cline.ApiProvider_CLINE {
output.WriteString(" Status: Authenticated\n")
} else {
output.WriteString(" API Key: Configured\n")
@@ -461,12 +430,6 @@ 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
@@ -481,8 +444,6 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
{cline.ApiProvider_GEMINI, "geminiApiKey"},
{cline.ApiProvider_OLLAMA, "ollamaBaseUrl"}, // Ollama uses baseUrl instead of API key
{cline.ApiProvider_CEREBRAS, "cerebrasApiKey"},
{cline.ApiProvider_HICAP, "hicapApiKey"},
{cline.ApiProvider_NOUSRESEARCH, "nousResearchApiKey"},
}
for _, providerCheck := range providersToCheck {
@@ -498,7 +459,6 @@ 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))
+8 -146
View File
@@ -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,7 +46,6 @@ 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)
@@ -69,7 +68,6 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
case cline.ApiProvider_OPENAI:
return ProviderFields{
APIKeyField: "openAiApiKey",
BaseURLField: "openAiBaseUrl",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeOpenAiModelId",
@@ -144,34 +142,6 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
ActModeProviderSpecificModelIDField: "actModeOpenRouterModelId",
}, nil
case cline.ApiProvider_OCA:
return ProviderFields{
APIKeyField: "ocaApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeModelInfoField: "planModeOcaModelInfo",
ActModeModelInfoField: "actModeOcaModelInfo",
PlanModeProviderSpecificModelIDField: "planModeOcaModelId",
ActModeProviderSpecificModelIDField: "actModeOcaModelId",
}, nil
case cline.ApiProvider_HICAP:
return ProviderFields{
APIKeyField: "hicapApiKey",
PlanModeModelInfoField: "planModeHicapModelInfo",
ActModeModelInfoField: "actModeHicapModelInfo",
PlanModeProviderSpecificModelIDField: "planModeHicapModelId",
ActModeProviderSpecificModelIDField: "actModeHicapModelId",
}, nil
case cline.ApiProvider_NOUSRESEARCH:
return ProviderFields{
APIKeyField: "nousResearchApiKey",
PlanModeModelIDField: "planModeApiModelId",
ActModeModelIDField: "actModeApiModelId",
PlanModeProviderSpecificModelIDField: "planModeNousResearchModelId",
ActModeProviderSpecificModelIDField: "actModeNousResearchModelId",
}, nil
default:
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
}
@@ -180,12 +150,9 @@ 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)
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)
ModelID *string // New model ID (optional)
APIKey *string // New API key (optional)
ModelInfo interface{} // New model info (optional, provider-specific)
}
// GetModelIDFieldName returns the appropriate model ID field name for a provider and mode.
@@ -215,7 +182,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, includeBaseURL bool, includeProviderEnums bool) []string {
func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeModelID bool, includeModelInfo bool, includeProviderEnums bool) []string {
var fieldPaths []string
// Include provider enums if requested (used when setting active provider)
@@ -232,11 +199,6 @@ 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
@@ -283,12 +245,6 @@ func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, v
apiConfig.CerebrasApiKey = value
case "clineApiKey":
apiConfig.ClineApiKey = value
case "ocaApiKey":
apiConfig.OcaApiKey = value
case "hicapApiKey":
apiConfig.HicapApiKey = value
case "nousResearchApiKey":
apiConfig.NousResearchApiKey = value
}
}
@@ -307,20 +263,11 @@ func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldNa
case "planModeAwsBedrockCustomModelBaseId":
apiConfig.PlanModeAwsBedrockCustomModelBaseId = value
apiConfig.ActModeAwsBedrockCustomModelBaseId = value
case "planModeOcaModelId":
apiConfig.PlanModeOcaModelId = value
apiConfig.ActModeOcaModelId = value
case "planModeHicapModelId":
apiConfig.PlanModeHicapModelId = value
apiConfig.ActModeHicapModelId = value
case "planModeNousResearchModelId":
apiConfig.PlanModeNousResearchModelId = value
apiConfig.ActModeNousResearchModelId = value
}
}
// AddProviderPartial configures a new provider with all necessary fields using partial updates.
func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID string, apiKey string, baseURL string, modelInfo interface{}) error {
func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID string, apiKey string, modelInfo interface{}) error {
// Get field mapping for this provider
fields, err := GetProviderFields(provider)
if err != nil {
@@ -335,13 +282,6 @@ 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)
@@ -361,7 +301,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, includeBaseURL, false)
fieldPaths := buildProviderFieldMask(fields, true, true, includeModelInfo, false)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
@@ -428,7 +368,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, false, setAsActive)
fieldPaths := buildProviderFieldMask(fields, includeAPIKey, includeModelID, includeModelInfo, setAsActive)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
@@ -481,46 +421,6 @@ 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
@@ -534,12 +434,6 @@ 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 {
@@ -575,20 +469,6 @@ 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 {
@@ -627,21 +507,3 @@ 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 -93
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
@@ -41,7 +40,7 @@ func (pw *ProviderWizard) showMainMenu() (string, error) {
huh.NewSelect[string]().
Title("What would you like to do?").
Options(
huh.NewOption("Add or change an API provider", "add"),
huh.NewOption("Configure a new provider", "add"),
huh.NewOption("Change model for API provider", "change-model"),
huh.NewOption("Remove a provider", "remove"),
huh.NewOption("List configured providers", "list"),
@@ -108,13 +107,8 @@ 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, baseURL, err := PromptForAPIKey(provider)
apiKey, err := PromptForAPIKey(provider)
if err != nil {
return fmt.Errorf("failed to get API key: %w", err)
}
@@ -126,7 +120,7 @@ func (pw *ProviderWizard) handleAddProvider() error {
}
// Step 5: Apply configuration using AddProviderPartial
if err := AddProviderPartial(pw.ctx, pw.manager, provider, modelID, apiKey, baseURL, modelInfo); err != nil {
if err := AddProviderPartial(pw.ctx, pw.manager, provider, modelID, apiKey, modelInfo); err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}
@@ -168,51 +162,6 @@ 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)
@@ -310,15 +259,6 @@ 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
@@ -585,17 +525,8 @@ 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 ""
@@ -725,16 +656,7 @@ func (pw *ProviderWizard) handleRemoveProvider() error {
return nil
}
// Step 7: If removing OCA, sign out first
if selectedProvider.Provider == cline.ApiProvider_OCA {
if err := signOutOca(pw.ctx); err != nil {
fmt.Printf("Warning: Failed to sign out of OCA: %v\n", err)
} else {
fmt.Println("Signed out of OCA.")
}
}
// Step 8: Clear the API key for the selected provider
// Step 7: 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)
}
@@ -748,16 +670,6 @@ 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
-366
View File
@@ -1,366 +0,0 @@
package auth
import (
"context"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/charmbracelet/huh"
"github.com/cline/cli/pkg/cli/global"
"github.com/cline/cli/pkg/cli/task"
"github.com/cline/grpc-go/cline"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// OcaConfig holds Oracle Code Assist (OCA) configuration fields
type OcaConfig struct {
BaseURL string
Mode string
}
// PromptForOcaConfig displays a form for OCA configuration (base URL and mode)
func PromptForOcaConfig(ctx context.Context, manager *task.Manager) (*OcaConfig, error) {
config := &OcaConfig{}
var mode string
// Collect optional settings
configForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Base URL").
Value(&config.BaseURL).
Description("Leave empty to use default Base URL"),
huh.NewSelect[string]().
Title("Choose OCA mode (used for authentication)").
Description("Select 'Internal' to use Cline's internal OCA, or 'External' for your own OCA instance").
Options(
huh.NewOption("Internal", "internal"),
huh.NewOption("External", "external"),
).
Value(&mode),
),
)
if err := configForm.Run(); err != nil {
return nil, fmt.Errorf("failed to get OCA configuration: %w", err)
}
// Trim whitespace from string fields
config.BaseURL = strings.TrimSpace(config.BaseURL)
config.Mode = strings.TrimSpace(mode)
return config, nil
}
// ApplyOcaConfig applies OCA configuration using partial updates
func ApplyOcaConfig(ctx context.Context, manager *task.Manager, config *OcaConfig) error {
// Build the API configuration with all OCA fields
apiConfig := &cline.ModelsApiConfiguration{}
// Set profile authentication fields (always required)
optionalFields := &OcaOptionalFields{}
// Set profile name (can be empty for default profile)
if config.BaseURL != "" {
optionalFields.BaseURL = proto.String(config.BaseURL)
}
// Set optional fields if provided
if config.Mode != "" {
optionalFields.Mode = proto.String(config.Mode)
}
// Apply all fields to the config
setOcaOptionalFields(apiConfig, optionalFields)
// Add profile authentication field paths
optionalPaths := buildOcaOptionalFieldMask(optionalFields)
// Create field mask
fieldMask := &fieldmaskpb.FieldMask{Paths: optionalPaths}
// Apply the partial update
request := &cline.UpdateApiConfigurationPartialRequest{
ApiConfiguration: apiConfig,
UpdateMask: fieldMask,
}
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
return fmt.Errorf("failed to apply OCA configuration: %w", err)
}
return nil
}
// ===========================
// OCA Auth Listener Singleton
// ===========================
type ocaAuthStream interface {
Recv() (*cline.OcaAuthState, error)
}
// OcaAuthStatusListener manages subscription to OCA auth status updates
type OcaAuthStatusListener struct {
stream ocaAuthStream
updatesCh chan *cline.OcaAuthState
errCh chan error
ctx context.Context
cancel context.CancelFunc
mu sync.RWMutex
lastState *cline.OcaAuthState
firstEventCh chan struct{}
firstEventOnce sync.Once
}
// NewOcaAuthStatusListener creates a new OCA auth status listener
func NewOcaAuthStatusListener(parentCtx context.Context) (*OcaAuthStatusListener, error) {
client, err := global.GetDefaultClient(parentCtx)
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
// Keep the listener alive independently of short-lived caller contexts
ctx, cancel := context.WithCancel(context.Background())
// Subscribe to OCA auth status updates
stream, err := client.Ocaaccount.OcaSubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
if err != nil {
cancel()
return nil, fmt.Errorf("failed to subscribe to OCA auth updates: %w", err)
}
return &OcaAuthStatusListener{
stream: stream,
updatesCh: make(chan *cline.OcaAuthState, 10),
errCh: make(chan error, 1),
ctx: ctx,
cancel: cancel,
firstEventCh: make(chan struct{}),
}, nil
}
// Start begins listening to the auth status update stream
func (l *OcaAuthStatusListener) Start() error {
go l.readStream()
return nil
}
func (l *OcaAuthStatusListener) readStream() {
defer close(l.updatesCh)
defer close(l.errCh)
for {
select {
case <-l.ctx.Done():
return
default:
state, err := l.stream.Recv()
if err != nil {
// Propagate error and exit
if err == io.EOF {
// Treat as error to notify waiters
err = fmt.Errorf("OCA auth status stream closed")
}
select {
case l.errCh <- err:
case <-l.ctx.Done():
}
return
}
l.mu.Lock()
l.lastState = state
l.mu.Unlock()
// Notify first event waiters
l.firstEventOnce.Do(func() { close(l.firstEventCh) })
select {
case l.updatesCh <- state:
case <-l.ctx.Done():
return
}
}
}
}
// WaitForFirstEvent blocks until the first event is received or timeout occurs
func (l *OcaAuthStatusListener) WaitForFirstEvent(timeout time.Duration) error {
// Fast-path if already have a state
l.mu.RLock()
ready := l.lastState != nil
l.mu.RUnlock()
if ready {
return nil
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-l.firstEventCh:
return nil
case <-timer.C:
return fmt.Errorf("timeout waiting for initial OCA auth event")
case <-l.ctx.Done():
return fmt.Errorf("OCA auth listener cancelled")
}
}
// IsAuthenticated returns true if the last known OCA auth state is authenticated
func (l *OcaAuthStatusListener) IsAuthenticated() bool {
l.mu.RLock()
defer l.mu.RUnlock()
return isOCAStateAuthenticated(l.lastState)
}
// WaitForAuthentication waits until OCA authentication succeeds or timeout occurs
func (l *OcaAuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
// If already authenticated, return immediately
if l.IsAuthenticated() {
return nil
}
for {
select {
case <-timer.C:
return fmt.Errorf("OCA authentication timeout after %v - please try again", timeout)
case <-l.ctx.Done():
return fmt.Errorf("OCA authentication cancelled")
case err := <-l.errCh:
return fmt.Errorf("OCA authentication stream error: %w", err)
case state := <-l.updatesCh:
if isOCAStateAuthenticated(state) {
return nil
}
}
}
}
// Stop closes the stream and cleans up resources
func (l *OcaAuthStatusListener) Stop() {
l.cancel()
}
func isOCAStateAuthenticated(state *cline.OcaAuthState) bool {
return state != nil && state.User != nil
}
// Singleton holder
var (
ocaListener *OcaAuthStatusListener
ocaListenerOnce sync.Once
ocaListenerErr error
)
// GetOcaAuthListener returns the OCA auth listener singleton
func GetOcaAuthListener(ctx context.Context) (*OcaAuthStatusListener, error) {
// Allow optional ctx: if nil, use context.TODO(). If already initialized, return singleton.
if ctx == nil {
ctx = context.TODO()
}
ocaListenerOnce.Do(func() {
l, err := NewOcaAuthStatusListener(ctx)
if err != nil {
ocaListenerErr = err
return
}
if err := l.Start(); err != nil {
ocaListenerErr = err
return
}
ocaListener = l
})
return ocaListener, ocaListenerErr
}
// IsOCAAuthenticated returns true if the global OCA auth status is authenticated.
// It attempts a brief wait for the first event to avoid stale reads.
func IsOCAAuthenticated(ctx context.Context) bool {
l, err := GetOcaAuthListener(ctx)
if err != nil {
return false
}
_ = l.WaitForFirstEvent(1 * time.Second) // best-effort
return l.IsAuthenticated()
}
// LatestState returns the last received OCA auth state (may be nil)
func (l *OcaAuthStatusListener) LatestState() *cline.OcaAuthState {
l.mu.RLock()
defer l.mu.RUnlock()
return l.lastState
}
// GetLatestOCAState returns the latest known OCA auth state, optionally waiting for the first event
func GetLatestOCAState(ctx context.Context, timeout time.Duration) (*cline.OcaAuthState, error) {
l, err := GetOcaAuthListener(ctx)
if err != nil {
return nil, err
}
if timeout > 0 {
if err := l.WaitForFirstEvent(timeout); err != nil {
return nil, err
}
}
return l.LatestState(), nil
}
// ensureOcaAuthenticated initiates OCA login (if needed) and waits for success using the singleton listener
func ensureOcaAuthenticated(ctx context.Context) error {
// Ensure listener exists
listener, err := GetOcaAuthListener(ctx)
if err != nil {
return fmt.Errorf("failed to initialize OCA auth listener: %w", err)
}
// Briefly wait for first event to know current state
_ = listener.WaitForFirstEvent(1 * time.Second)
// If already authenticated, nothing to do
if listener.IsAuthenticated() {
fmt.Println("✓ OCA authentication already active.")
return nil
}
// Create gRPC client for initiating login
client, err := global.GetDefaultClient(ctx)
if err != nil {
return fmt.Errorf("failed to obtain client: %w", err)
}
// Start login and wait for authentication
waitCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
// Initiate login (opens the browser with a callback URL from Cline Core)
response, err := client.Ocaaccount.OcaAccountLoginClicked(waitCtx, &cline.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to initiate OCA login: %w", err)
}
fmt.Println("\nOpening browser for OCA authentication...")
if response != nil && response.Value != "" {
fmt.Printf("If the browser doesn't open automatically, visit this URL:\n%s\n\n", response.Value)
}
fmt.Println("Waiting for you to complete OCA authentication in your browser...")
fmt.Println("(This may take a few moments. Timeout: 5 minutes)")
// Block until authenticated or timeout
if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
return err
}
fmt.Println("✓ OCA authentication successful!")
return nil
}
+2 -5
View File
@@ -123,10 +123,7 @@ 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.
This command merges the provided settings with existing values, preserving
unspecified fields. Only the fields you explicitly set will be updated.`,
Long: `Set one or more global configuration variables using key=value format.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
@@ -142,7 +139,7 @@ unspecified fields. Only the fields you explicitly set will be updated.`,
return err
}
// Update settings (server-side merge handles preserving existing values)
// Update settings
return configManager.UpdateSettings(ctx, settings, secrets)
},
}
+1 -1
View File
@@ -189,7 +189,7 @@ func renderAutoApprovalSettings(value interface{}, censor bool) error {
}
}
} else {
// Print other fields normally (enabled, enableNotifications, favorites)
// Print other fields normally (enabled, maxRequests, enableNotifications, favorites)
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
}
}
+7 -85
View File
@@ -4,7 +4,6 @@ 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"
@@ -15,16 +14,6 @@ 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 {
@@ -33,23 +22,11 @@ func NewRenderer(outputFormat string) *Renderer {
mdRenderer = nil
}
r := &Renderer{
return &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 {
@@ -229,76 +206,21 @@ 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 or non-TTY contexts
// Respects output format - skips rendering in plain mode
func (r *Renderer) RenderMarkdown(markdown string) string {
// 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() {
// Skip markdown rendering in plain mode
if r.outputFormat == "plain" {
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)
}
+5 -5
View File
@@ -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 and TTY)
if shouldMarkdown && outputFormat != "plain" && isTTY() {
// Render rich header immediately when creating segment (if in rich mode)
if shouldMarkdown && outputFormat != "plain" {
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 only in rich mode and TTY
if ss.shouldMarkdown && ss.outputFormat != "plain" && isTTY() {
// Render markdown
if ss.shouldMarkdown && ss.outputFormat != "plain" {
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" && isTTY() {
if ss.shouldMarkdown && ss.outputFormat != "plain" {
rendered, err := ss.mdRenderer.Render(currentBuffer)
if err == nil {
bodyContent = rendered
+4 -14
View File
@@ -106,14 +106,6 @@ func (tr *ToolRenderer) generateToolHeader(tool *types.ToolMessage, verbTense st
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeFileDeleted):
if verbTense == "wants to" {
action = "wants to delete"
} else {
action = "is deleting"
}
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
case string(types.ToolTypeListFilesTopLevel):
if verbTense == "wants to" {
action = "wants to list files in"
@@ -207,7 +199,7 @@ func (tr *ToolRenderer) GenerateToolContentPreview(tool *types.ToolMessage) stri
previewMd := fmt.Sprintf("```\n%s\n```", preview)
return tr.renderMarkdown(previewMd)
case string(types.ToolTypeReadFile), string(types.ToolTypeWebFetch), string(types.ToolTypeFileDeleted):
case string(types.ToolTypeReadFile), string(types.ToolTypeWebFetch):
// No preview for read/fetch operations
return ""
@@ -234,8 +226,7 @@ func (tr *ToolRenderer) GenerateToolContentBody(tool *types.ToolMessage) string
toolParser := NewToolResultParser(tr.mdRenderer)
switch tool.Tool {
case string(types.ToolTypeReadFile),
string(types.ToolTypeFileDeleted):
case string(types.ToolTypeReadFile):
// readFile: show header only, no body
return ""
@@ -348,10 +339,9 @@ 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 and in a TTY
// renderMarkdown renders markdown if not in plain mode
func (tr *ToolRenderer) renderMarkdown(markdown string) string {
// Skip markdown rendering if plain mode or not in TTY
if tr.outputFormat == "plain" || !isTTY() {
if tr.outputFormat == "plain" {
return markdown
}
-65
View File
@@ -1,65 +0,0 @@
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
}
-8
View File
@@ -6,10 +6,8 @@ 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
@@ -49,12 +47,6 @@ 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)
+23 -2
View File
@@ -52,6 +52,8 @@ 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):
@@ -126,8 +128,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%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"))
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")
}
}
@@ -253,6 +255,25 @@ 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)
+6 -7
View File
@@ -389,21 +389,20 @@ func newInstanceListCommand() *cobra.Command {
}
// Render the markdown table with terminal width for nice table layout
mdRenderer, err := display.NewMarkdownRendererForTerminal()
renderer, err := display.NewMarkdownRendererForTerminal()
if err != nil {
// Fallback to plain table if markdown renderer fails
fmt.Println(markdown.String())
} else {
rendered, err := mdRenderer.Render(markdown.String())
rendered, err := renderer.Render(markdown.String())
if err != nil {
fmt.Println(markdown.String())
} else {
// Post-process to colorize status values
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"))
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
fmt.Print(strings.TrimLeft(rendered, "\n"))
}
+5 -6
View File
@@ -208,9 +208,9 @@ func listLogFiles(logsDir string) ([]logFileInfo, error) {
})
}
// Sort by created time (oldest first)
// Sort by created time (newest first)
sort.Slice(logs, func(i, j int) bool {
return logs[i].created.Before(logs[j].created)
return logs[i].created.After(logs[j].created)
})
return logs, nil
@@ -340,7 +340,6 @@ 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("|--------------|----------|-------------|---------|")
@@ -353,9 +352,9 @@ func renderLogsTable(logs []logFileInfo, markForDeletion bool) error {
row.age,
)
// If marking for deletion, wrap in red
// If marking for deletion, wrap in red ANSI codes
if markForDeletion {
line = colorRenderer.Red(line)
line = "\033[31m" + line + "\033[0m"
}
markdown.WriteString(line)
@@ -379,4 +378,4 @@ func renderLogsTable(logs []logFileInfo, markForDeletion bool) error {
fmt.Println()
return nil
}
}
+17 -40
View File
@@ -14,7 +14,6 @@ import (
"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"
)
@@ -477,34 +476,15 @@ func newTaskOpenCommand() *cobra.Command {
return fmt.Errorf("failed to parse settings: %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")
}
// 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)
}
// 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")
}
// Apply the settings to the instance
if err := configManager.UpdateSettings(ctx, parsedSettings, secrets); err != nil {
return fmt.Errorf("failed to apply settings: %w", err)
}
}
@@ -592,20 +572,17 @@ func getContentFromStdinAndArgs(args []string) (string, error) {
// Check if data is being piped to stdin
if (stat.Mode() & os.ModeCharDevice) == 0 {
// Only try to read if there's actually data available
if stat.Size() > 0 {
stdinBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("failed to read from stdin: %w", err)
}
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)
stdinContent := strings.TrimSpace(string(stdinBytes))
if stdinContent != "" {
if content.Len() > 0 {
content.WriteString(" ")
}
content.WriteString(stdinContent)
}
}
@@ -672,4 +649,4 @@ func CreateAndFollowTask(ctx context.Context, prompt string, opts TaskOptions) e
} else {
return taskManager.FollowConversation(ctx, taskManager.GetCurrentInstance(), true)
}
}
}
+6 -11
View File
@@ -10,7 +10,6 @@ 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"
@@ -165,10 +164,6 @@ 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" {
@@ -177,14 +172,16 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
output.Printf("\nError switching to act mode with message: %v\n", err)
continue
}
output.Printf("\n%s\n", actStyle.Render("Switched to act mode"))
// 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")
} 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
}
output.Printf("\n%s\n", planStyle.Render("Switched to plan mode"))
// Yellow color for plan mode (ANSI color 3)
output.Printf("\n\033[33m\033[1mSwitched to plan mode\033[0m\n")
// Now send the message separately
time.Sleep(500 * time.Millisecond) // Give mode switch time to process
@@ -201,9 +198,9 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
}
// Color based on mode
if newMode == "act" {
output.Printf("\n%s\n", actStyle.Render("Switched to act mode"))
output.Printf("\n\033[38;5;39m\033[1mSwitched to act mode\033[0m\n")
} else {
output.Printf("\n%s\n", planStyle.Render("Switched to plan mode"))
output.Printf("\n\033[33m\033[1mSwitched to plan mode\033[0m\n")
}
}
@@ -256,8 +253,6 @@ func determineAutoApprovalAction(msg *types.ClineMessage) (string, error) {
case types.ToolTypeEditedExistingFile,
types.ToolTypeNewFileCreated:
return "edit_files", nil
case types.ToolTypeFileDeleted:
return "apply_patch", nil
default:
return "", fmt.Errorf("unsupported tool type: %s", toolMsg.Tool)
}
+5 -4
View File
@@ -282,6 +282,7 @@ 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
@@ -1238,16 +1239,16 @@ func (m *Manager) updateMode(stateJson string) {
// 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{},
Enabled: true,
MaxRequests: 20, // Important: avoid maxRequests=0 bug
Actions: &cline.AutoApprovalActions{},
},
}
// Set the specific action to true based on actionKey
truePtr := boolPtr(true)
truePtr := func() *bool { b := true; return &b }()
switch actionKey {
case "read_files":
+19 -5
View File
@@ -180,6 +180,8 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
settings.PlanModeHuaweiCloudMaasModelId = strPtr(value)
case "plan_mode_oca_model_id":
settings.PlanModeOcaModelId = strPtr(value)
case "plan_mode_vercel_ai_gateway_model_id":
settings.PlanModeVercelAiGatewayModelId = strPtr(value)
case "act_mode_api_model_id":
settings.ActModeApiModelId = strPtr(value)
case "act_mode_reasoning_effort":
@@ -216,6 +218,8 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
settings.ActModeHuaweiCloudMaasModelId = strPtr(value)
case "act_mode_oca_model_id":
settings.ActModeOcaModelId = strPtr(value)
case "act_mode_vercel_ai_gateway_model_id":
settings.ActModeVercelAiGatewayModelId = strPtr(value)
// Boolean fields
case "aws_use_cross_region_inference":
@@ -412,12 +416,24 @@ 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 = boolPtr(val)
settings.EnableNotifications = val
case "actions":
return fmt.Errorf("auto_approval_settings.actions requires nested dot notation (e.g., auto-approval-settings.actions.read-files=true)")
default:
@@ -656,8 +672,6 @@ 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)
}
@@ -732,14 +746,14 @@ func setSecretField(secrets *cline.Secrets, key, value string) error {
secrets.HuaweiCloudMaasApiKey = strPtr(value)
case "baseten_api_key":
secrets.BasetenApiKey = strPtr(value)
case "vercel_ai_gateway_api_key":
secrets.VercelAiGatewayApiKey = strPtr(value)
case "dify_api_key":
secrets.DifyApiKey = strPtr(value)
case "oca_api_key":
secrets.OcaApiKey = strPtr(value)
case "oca_refresh_token":
secrets.OcaRefreshToken = strPtr(value)
case "hicap_api_key":
secrets.HicapApiKey = strPtr(value)
default:
return fmt.Errorf("unsupported secret field '%s'", key)
}
-695
View File
@@ -1,695 +0,0 @@
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
}
+13 -11
View File
@@ -37,16 +37,17 @@ 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"
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"
AskTypeBrowserActionLaunch AskType = "browser_action_launch"
AskTypeUseMcpServer AskType = "use_mcp_server"
AskTypeNewTask AskType = "new_task"
@@ -107,7 +108,6 @@ const (
ToolTypeEditedExistingFile ToolType = "editedExistingFile"
ToolTypeNewFileCreated ToolType = "newFileCreated"
ToolTypeReadFile ToolType = "readFile"
ToolTypeFileDeleted ToolType = "fileDeleted"
ToolTypeListFilesTopLevel ToolType = "listFilesTopLevel"
ToolTypeListFilesRecursive ToolType = "listFilesRecursive"
ToolTypeListCodeDefinitionNames ToolType = "listCodeDefinitionNames"
@@ -247,6 +247,8 @@ 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:
+6 -40
View File
@@ -68,7 +68,7 @@ func CheckAndUpdate(isVerbose bool) {
// Run in background so we don't block CLI startup
go func() {
if err := checkAndUpdateInternal(false); err != nil {
if err := checkAndUpdateSync(); err != nil {
if verbose {
output.Printf("[updater] Update check failed: %v\n", err)
}
@@ -76,49 +76,15 @@ func CheckAndUpdate(isVerbose bool) {
}()
}
// 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 {
func checkAndUpdateSync() 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 err == nil && time.Since(cache.LastCheck) < checkInterval {
// Checked recently, skip
if verbose {
output.Printf("[updater] Cache is fresh (last checked %v ago), skipping\n", time.Since(cache.LastCheck))
}
@@ -375,7 +341,7 @@ func showFailureMessage(channel string) {
func getCacheFilePath() string {
configDir := filepath.Join(os.Getenv("HOME"), ".cline", "data")
return filepath.Join(configDir, "cli-update-cache")
return filepath.Join(configDir, ".update-cache")
}
func loadCache() (cacheData, error) {
@@ -406,4 +372,4 @@ func saveCache(cache cacheData) error {
}
return os.WriteFile(cacheFile, data, 0644)
}
}
+1 -27
View File
@@ -4,9 +4,7 @@ import (
"context"
"fmt"
"net"
"os/exec"
"strconv"
"strings"
"time"
"google.golang.org/grpc"
@@ -126,16 +124,6 @@ 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
@@ -167,19 +155,5 @@ func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation f
}
}
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())
return fmt.Errorf("operation failed after %d attempts: %w", maxRetries, lastErr)
}
+1 -116
View File
@@ -144,8 +144,6 @@ const (
OPENAI_NATIVE = "openai-native"
XAI = "xai"
CEREBRAS = "cerebras"
OCA = "oca"
NOUSRESEARCH = "nousResearch"
)
// AllProviders returns a slice of enabled provider IDs for the CLI build.
@@ -161,8 +159,6 @@ var AllProviders = []string{
"openai-native",
"xai",
"cerebras",
"oca",
"nousResearch",
}
// ConfigField represents a configuration field requirement
@@ -320,15 +316,6 @@ var rawConfigFields = ` [
"fieldType": "password",
"placeholder": "Enter your API key"
},
{
"name": "nousResearchApiKey",
"type": "string",
"comment": "",
"category": "nousResearch",
"required": true,
"fieldType": "password",
"placeholder": "Enter your API key"
},
{
"name": "ulid",
"type": "string",
@@ -446,15 +433,6 @@ var rawConfigFields = ` [
"fieldType": "url",
"placeholder": "https://api.example.com"
},
{
"name": "minimaxApiLine",
"type": "string",
"comment": "",
"category": "general",
"required": false,
"fieldType": "string",
"placeholder": ""
},
{
"name": "ocaMode",
"type": "string",
@@ -463,16 +441,7 @@ 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)
@@ -498,16 +467,6 @@ 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,
@@ -620,16 +579,6 @@ 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,
@@ -795,24 +744,6 @@ var rawModelDefinitions = ` {
"supportsImages": false,
"supportsPromptCache": false,
"description": "A compact 20B open-weight Mixture-of-Experts language model designed for strong reasoning and tool use, ideal for edge devices and local inference."
},
"qwen.qwen3-coder-30b-a3b-v1:0": {
"maxTokens": 8192,
"contextWindow": 262144,
"inputPrice": 0,
"outputPrice": 0,
"supportsImages": false,
"supportsPromptCache": false,
"description": "Qwen3 Coder 30B MoE model with 3.3B activated parameters, optimized for code generation and analysis with 256K context window."
},
"qwen.qwen3-coder-480b-a35b-v1:0": {
"maxTokens": 8192,
"contextWindow": 262144,
"inputPrice": 0,
"outputPrice": 1,
"supportsImages": false,
"supportsPromptCache": false,
"description": "Qwen3 Coder 480B flagship MoE model with 35B activated parameters, designed for complex coding tasks with advanced reasoning capabilities and 256K context window."
}
},
"gemini": {
@@ -1301,26 +1232,6 @@ var rawModelDefinitions = ` {
"supportsPromptCache": false,
"description": "SOTA performance with ~1500 tokens/s"
}
},
"nousResearch": {
"Hermes-4-405B": {
"maxTokens": 8192,
"contextWindow": 128000,
"inputPrice": 0,
"outputPrice": 0,
"supportsImages": false,
"supportsPromptCache": false,
"description": "This is the largest model in the Hermes 4 family, and it is the fullest expression of our design, focused on advanced reasoning and creative depth rather than optimizing inference speed or cost."
},
"Hermes-4-70B": {
"maxTokens": 8192,
"contextWindow": 128000,
"inputPrice": 0,
"outputPrice": 0,
"supportsImages": false,
"supportsPromptCache": false,
"description": "This incarnation of Hermes 4 balances scale and size. It handles complex reasoning tasks, while staying fast and cost effective. A versatile choice for many use cases."
}
}
}`
@@ -1478,30 +1389,6 @@ func GetProviderDefinitions() (map[string]ProviderDefinition, error) {
HasDynamicModels: false,
SetupInstructions: `Get your API key from https://cloud.cerebras.ai/`,
}
// Oca
definitions["oca"] = ProviderDefinition{
ID: "oca",
Name: "Oca",
RequiredFields: getFieldsByProvider("oca", configFields, true),
OptionalFields: getFieldsByProvider("oca", configFields, false),
Models: modelDefinitions["oca"],
DefaultModelID: "",
HasDynamicModels: false,
SetupInstructions: `Configure Oca API credentials`,
}
// NousResearch
definitions["nousResearch"] = ProviderDefinition{
ID: "nousResearch",
Name: "NousResearch",
RequiredFields: getFieldsByProvider("nousResearch", configFields, true),
OptionalFields: getFieldsByProvider("nousResearch", configFields, false),
Models: modelDefinitions["nousResearch"],
DefaultModelID: "Hermes-4-405B",
HasDynamicModels: false,
SetupInstructions: `Configure NousResearch API credentials`,
}
return definitions, nil
}
@@ -1528,8 +1415,6 @@ func GetProviderDisplayName(providerID string) string {
"openai-native": "OpenAI",
"xai": "X AI (Grok)",
"cerebras": "Cerebras",
"oca": "Oca",
"nousResearch": "NousResearch",
}
if name, exists := displayNames[providerID]; exists {
-48
View File
@@ -1,48 +0,0 @@
# Git
.git
.gitignore
.gitattributes
# Node modules
node_modules
npm-debug.log
# Build artifacts
dist
dist-standalone
build
*.log
# Generated code
src/generated
# CLI build artifacts
cli/bin
cli/dist
# Webview build artifacts
webview-ui/dist
webview-ui/build
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Documentation
*.md
!README.md
# Tests
tests
*.test.js
*.spec.js
# CI/CD
.github
.gitlab-ci.yml
-49
View File
@@ -1,49 +0,0 @@
FROM node:22-slim
# TARGETARCH enables multi-architecture support without emulation warnings:
# - Docker automatically sets TARGETARCH to the build platform's architecture
# - On arm64 machines (Apple Silicon): TARGETARCH=arm64, uses linux-arm64 binaries
# - On amd64 machines (Intel/AMD): TARGETARCH=amd64, uses linux-x64 binaries
# The corresponding platform-specific binaries and native modules (better-sqlite3)
# are pre-built by scripts/package-standalone.mjs during the build process.
ARG TARGETARCH
# Install only runtime dependencies
RUN apt-get update && apt-get install -y \
git curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/cline
# Copy the entire pre-built distribution
COPY dist-standalone/ ./
# Create symlink for Linux native modules
# Map Docker's TARGETARCH (arm64/amd64) to Node's platform naming (x64 for amd64)
RUN if [ "$TARGETARCH" = "amd64" ]; then \
ln -sf /opt/cline/binaries/linux-x64/node_modules/better-sqlite3 /opt/cline/node_modules/better-sqlite3; \
else \
ln -sf /opt/cline/binaries/linux-$TARGETARCH/node_modules/better-sqlite3 /opt/cline/node_modules/better-sqlite3; \
fi
# Set up CLI binaries
# The Linux binaries are already in /opt/cline/bin/ from dist-standalone
# Just need to create symlinks to the platform-specific ones
RUN cd /opt/cline/bin && \
ln -sf cline-linux-$TARGETARCH cline && \
ln -sf cline-host-linux-$TARGETARCH cline-host && \
chmod +x cline-linux-$TARGETARCH cline-host-linux-$TARGETARCH cline cline-host
# Add binaries to PATH
ENV PATH="/opt/cline/bin:${PATH}"
ENV NODE_ENV=production
ENV CLINE_HOME=/root/.cline
RUN mkdir -p $CLINE_HOME
WORKDIR /workspace
EXPOSE 8000
ENTRYPOINT ["/opt/cline/bin/cline"]
CMD ["--help"]
@@ -1,324 +0,0 @@
---
title: "GitHub Actions Integration"
description: "Automatically respond to GitHub issues by mentioning @cline in comments using Cline CLI in GitHub Actions."
---
# GitHub Integration Sample
Automate GitHub issue analysis with AI. Mention `@cline` in any issue comment to trigger an autonomous investigation that reads files, analyzes code, and provides actionable insights - all running automatically in GitHub Actions.
<Note>
**New to Cline CLI?** This sample assumes you understand Cline CLI basics and have completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation). If you're new to Cline CLI, we recommend starting with the [GitHub RCA sample](../github-issue-rca) first, as it's simpler and will help you understand the fundamentals before setting up GitHub Actions.
</Note>
## The Workflow
Trigger Cline by mentioning `@cline` in any issue comment:
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss0a-comment.png" alt="Issue comment with @cline mention" width="600" />
</Frame>
Cline's automated analysis appears as a new comment, with insights drawn from your actual codebase:
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss0b-final.png" alt="Automated analysis response from Cline" width="600" />
</Frame>
The entire investigation runs autonomously in GitHub Actions - from file exploration to posting results.
Let's configure your repository.
## Prerequisites
Before you begin, you'll need:
- **Cline CLI knowledge** - Completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation) and understand basic usage
- **GitHub repository** - With admin access to configure Actions and secrets
- **GitHub Actions familiarity** - Basic understanding of workflows and CI/CD
- **API provider account** - OpenRouter, Anthropic, or similar with API key
## Setup
### 1. Copy the Workflow File
Copy the workflow file from this sample to your repository. The workflow file must be placed in the `.github/workflows/` directory in your repository root for GitHub Actions to detect and run it. In this case, we'll name it `cline-responder.yml`.
```bash
# In your repository root
mkdir -p .github/workflows
curl -o .github/workflows/cline-responder.yml https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-integration/cline-responder.yml
```
Alternatively, you can copy the full workflow file directly into `.github/workflows/cline-responder.yml`:
<Accordion title="Click to view the complete cline-responder.yml workflow">
```yaml
name: Cline Issue Assistant
on:
issue_comment:
types: [created, edited]
permissions:
issues: write
jobs:
respond:
runs-on: ubuntu-latest
environment: cline-actions
steps:
- name: Check for @cline mention
id: detect
uses: actions/github-script@v7
with:
script: |
const body = context.payload.comment?.body || "";
const isPR = !!context.payload.issue?.pull_request;
const hit = body.toLowerCase().includes("@cline");
core.setOutput("hit", (!isPR && hit) ? "true" : "false");
core.setOutput("issue_number", String(context.payload.issue?.number || ""));
core.setOutput("issue_url", context.payload.issue?.html_url || "");
core.setOutput("comment_body", body);
- name: Checkout repository
if: steps.detect.outputs.hit == 'true'
uses: actions/checkout@v4
# Node v20 is needed for Cline CLI on GitHub Actions Linux
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup Cline CLI
if: steps.detect.outputs.hit == 'true'
run: |
# Install the Cline CLI
sudo npm install -g cline
- name: Create Cline Instance
if: steps.detect.outputs.hit == 'true'
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
CLINE_DIR: ${{ runner.temp }}/cline
run: |
# Create instance and capture output
INSTANCE_OUTPUT=$(cline instance new 2>&1)
# Parse address from output (format: " Address: 127.0.0.1:36733")
CLINE_ADDRESS=$(echo "$INSTANCE_OUTPUT" | grep "Address:" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]+')
echo "CLINE_ADDRESS=$CLINE_ADDRESS" >> $GITHUB_ENV
# Configure API key
cline config set open-router-api-key=$OPENROUTER_API_KEY --address $CLINE_ADDRESS -v
- name: Download analyze script
if: steps.detect.outputs.hit == 'true'
run: |
export GITORG="YOUR-GITHUB-ORG"
export GITREPO="YOUR-GITHUB-REPO"
curl -L https://raw.githubusercontent.com/${GITORG}/${GITREPO}/refs/heads/main/git-scripts/analyze-issue.sh -o analyze-issue.sh
chmod +x analyze-issue.sh
- name: Run analysis
if: steps.detect.outputs.hit == 'true'
id: analyze
env:
ISSUE_URL: ${{ steps.detect.outputs.issue_url }}
COMMENT: ${{ steps.detect.outputs.comment_body }}
CLINE_ADDRESS: ${{ env.CLINE_ADDRESS }}
run: |
set -euo pipefail
RESULT=$(./analyze-issue.sh "${ISSUE_URL}" "Analyze this issue. The user asked: ${COMMENT}" "$CLINE_ADDRESS")
{
echo 'result<<EOF'
printf "%s\n" "$RESULT"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Post response
if: steps.detect.outputs.hit == 'true'
uses: actions/github-script@v7
env:
ISSUE_NUMBER: ${{ steps.detect.outputs.issue_number }}
RESULT: ${{ steps.analyze.outputs.result }}
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(process.env.ISSUE_NUMBER),
body: process.env.RESULT || "(no output)"
});
```
</Accordion>
<Warning>
**You MUST edit the workflow file before committing!**
Open `.github/workflows/cline-responder.yml` and update the "Download analyze script" step within the workflow to specify your GitHub organization and repository where the analysis script is stored:
```yaml
export GITORG="YOUR-GITHUB-ORG" # Change this!
export GITREPO="YOUR-GITHUB-REPO" # Change this!
```
**Example:** If your repository is `github.com/acme/myproject`, set:
```yaml
export GITORG="acme"
export GITREPO="myproject"
```
This tells the workflow where to download the analysis script from your repository after you commit it in step 3.
</Warning>
The workflow will look for new or updated issues, check for `@cline` mentions, and then
start up an instance of the Cline CLI to dig into the issue, providing feedback
as a reply to the issue.
### 2. Configure API Keys
Add your AI provider API keys as repository secrets:
1. Go to your GitHub repository
2. Navigate to **Settings** → **Environment** and Add a new environment.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss01-environment.png" alt="Navigate to Actions secrets" width="600" />
</Frame>
Make sure to name it "cline-actions" so that it matches the `environment`
value at the top of the `cline-responder.yml` file.
3. Click **New repository secret**
4. Add a secret for the `OPENROUTER_API_KEY` with a value of an API key from
[openrouter.com](https://openrouter.com).
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss02-api-key.png" alt="Add API key secret" width="600" />
</Frame>
5. Verify your secret is configured:
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/ss03-ready.png" alt="API key configured" width="600" />
</Frame>
Now you're ready to supply Cline with the credentials it needs in a GitHub Action.
### 3. Add Analysis Script
Add the analysis script from the `github-issue-rca` sample to your repository. **First, you'll need to create a `git-scripts` directory in your repository root where the script will be located.** Choose one of these options:
**Option A: Download directly (Recommended)**
```bash
# In your repository root, create the directory and download the script
mkdir -p git-scripts
curl -o git-scripts/analyze-issue.sh https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-issue-rca/analyze-issue.sh
chmod +x git-scripts/analyze-issue.sh
```
**Option B: Manual copy-paste**
Create the directory and file manually, then paste the script content:
```bash
# In your repository root
mkdir -p git-scripts
# Create and edit the file with your preferred editor
nano git-scripts/analyze-issue.sh # or use vim, code, etc.
```
<Accordion title="Click to view the complete analyze-issue.sh script">
```bash
#!/bin/bash
# Analyze a GitHub issue using Cline CLI
if [ -z "$1" ]; then
echo "Usage: $0 <github-issue-url> [prompt] [address]"
echo "Example: $0 https://github.com/owner/repo/issues/123"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?'"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?' 127.0.0.1:46529"
exit 1
fi
# Gather the args
ISSUE_URL="$1"
PROMPT="${2:-What is the root cause of this issue?}"
if [ -n "$3" ]; then
ADDRESS="--address $3"
fi
# Ask Cline for its analysis, showing only the summary
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
sed -n '/^{/,$p' | \
jq -r 'select(.say == "completion_result") | .text' | \
sed 's/\\n/\n/g'
```
After pasting the script content, make it executable:
```bash
chmod +x git-scripts/analyze-issue.sh
```
</Accordion>
This analysis script calls Cline to execute a prompt on a GitHub issue,
summarizing the output to populate the reply to the issue.
### 4. Commit and Push
```bash
git add .github/workflows/cline-responder.yml
git add git-scripts/analyze-issue.sh
git commit -m "Add Cline issue assistant workflow"
git push
```
## Usage
Once set up, simply mention `@cline` in any issue comment:
```
@cline what's causing this error?
@cline analyze the root cause
@cline what are the security implications?
```
GitHub Actions will:
1. Detect the `@cline` mention
2. Start a Cline CLI instance
3. Download the analysis script
4. Analyze the issue using act mode with yolo (fully autonomous)
5. Post Cline's analysis as a new comment
**Note**: The workflow only triggers on issue comments, not pull request
comments.
## How It Works
The workflow (`cline-responder.yml`):
1. **Triggers** on issue comments (created or edited)
2. **Detects** `@cline` mentions (case-insensitive)
3. **Installs** Cline CLI globally using npm
4. **Creates** a Cline instance using `cline instance new`
5. **Configures** authentication using `cline config set open-router-api-key=...
--address ...`
6. **Downloads** the reusable `analyze-issue.sh` script from the
`github-issue-rca` sample
7. **Runs** analysis with the instance address
8. **Posts** the analysis result as a comment
## Related Samples
- **[github-issue-rca](./github-issue-rca)**: The reusable script that powers this integration
-383
View File
@@ -1,383 +0,0 @@
---
title: "GitHub Issue RCA Sample"
description: "Automated GitHub issue analysis using Cline CLI to identify root causes."
---
# GitHub Root Cause Analysis
Automated GitHub issue analysis using Cline CLI. This script uses Cline's autonomous AI capabilities to fetch, analyze, and identify root causes of GitHub issues, outputting clean, parseable results that can be easily integrated into your development workflows.
<Note>
**New to Cline CLI?** This sample assumes you have already completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation) and authenticated with `cline auth`. If you haven't set up Cline CLI yet, please start there first.
</Note>
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/cli-rca.gif" alt="CLI Root Cause Analysis Demo" width="600" />
</Frame>
## Prerequisites
This sample assumes you have already:
- **Cline CLI** installed and authenticated ([Installation Guide](https://docs.cline.bot/cline-cli/installation))
- **At least one AI model provider** configured (e.g., OpenRouter, Anthropic, OpenAI)
- **Basic familiarity** with Cline CLI commands
Additionally, you'll need:
- **GitHub CLI** (`gh`) installed and authenticated
- **jq** installed for JSON parsing
- **bash** shell (or compatible shell)
### Installation Instructions
#### macOS
<Note>
These instructions require [Homebrew](https://brew.sh/) to be installed. If you don't have Homebrew, install it first by running:
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
</Note>
```bash
# Install GitHub CLI
brew install gh
# Install jq
brew install jq
# Authenticate with GitHub
gh auth login
```
#### Linux
```bash
# Install GitHub CLI (Debian/Ubuntu)
sudo apt install gh
# Or for other Linux distributions, see: https://cli.github.com/manual/installation
# Install jq (Debian/Ubuntu)
sudo apt install jq
# Authenticate with GitHub
gh auth login
```
## Getting the Script
**Option 1: Download directly with curl**
```bash
curl -O https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-issue-rca/analyze-issue.sh
```
**Option 2: Copy the full script**
<Accordion title="Click to view the complete analyze-issue.sh script">
```bash
#!/bin/bash
# Analyze a GitHub issue using Cline CLI
if [ -z "$1" ]; then
echo "Usage: $0 <github-issue-url> [prompt] [address]"
echo "Example: $0 https://github.com/owner/repo/issues/123"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?'"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?' 127.0.0.1:46529"
exit 1
fi
# Gather the args
ISSUE_URL="$1"
PROMPT="${2:-What is the root cause of this issue?}"
if [ -n "$3" ]; then
ADDRESS="--address $3"
fi
# Ask Cline for its analysis, showing only the summary
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
sed -n '/^{/,$p' | \
jq -r 'select(.say == "completion_result") | .text' | \
sed 's/\\n/\n/g'
```
</Accordion>
<Note>
**After downloading or creating the script**, make it executable by running:
```bash
chmod +x analyze-issue.sh
```
</Note>
## Quick Usage Examples
### Basic Usage
Run this command in your terminal from the directory where you saved the script to analyze an issue with the default root cause prompt:
```bash
./analyze-issue.sh https://github.com/owner/repo/issues/123
```
This will:
- Fetch issue #123 from the repository
- Analyze the issue to identify root causes
- Provide detailed analysis with recommendations
### Custom Analysis Prompt
Ask specific questions about the issue:
```bash
./analyze-issue.sh https://github.com/owner/repo/issues/456 "What is the security impact?"
```
### Using Specific Cline Instance
Target a particular Cline instance by address:
```bash
./analyze-issue.sh https://github.com/owner/repo/issues/123 \
"What is the root cause of this issue?" \
127.0.0.1:46529
```
<Warning>
This is useful when:
- Running multiple Cline instances
- Using a remote Cline server
- Testing with specific configurations
</Warning>
<Note>
The script will automatically handle everything: fetching the issue, analyzing it with Cline, and displaying the results. The analysis typically takes 30-60 seconds depending on the issue complexity.
</Note>
## How It Works
Let's analyze each component of the script to understand how it works.
### Argument Validation
The script validates input and provides usage instructions:
```bash
if [ -z "$1" ]; then
echo "Usage: $0 <github-issue-url> [prompt] [address]"
echo "Example: $0 https://github.com/owner/repo/issues/123"
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause?'"
echo "Example: $0 https://github.com/owner/repo/issues/123 'Analyze security impact' 127.0.0.1:46529"
exit 1
fi
```
**Key Points:**
- Validates required GitHub issue URL
- Shows clear usage examples
- Supports optional custom prompt
- Supports optional Cline instance address
### Argument Parsing
The script extracts and sets up the arguments:
```bash
# Gather the args
ISSUE_URL="$1"
PROMPT="${2:-What is the root cause of this issue?}"
if [ -n "$3" ]; then
ADDRESS="--address $3"
fi
```
**Explanation:**
- `ISSUE_URL="$1"` - First argument is always the issue URL
- `PROMPT="${2:-...}"` - Second argument is optional, defaults to root cause analysis
- `ADDRESS` - Third argument is optional, only set if provided
### The Core Analysis Pipeline
This is where the magic happens:
```bash
# Ask Cline for his analysis, showing only the summary
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
sed -n '/^{/,$p' | \
jq -r 'select(.say == "completion_result") | .text' | \
sed 's/\\n/\n/g'
```
<Accordion title="Pipeline Breakdown: Understanding Each Component">
**1. `cline -y "$PROMPT: $ISSUE_URL"`**
- `-y` enables yolo mode (no user interaction)
- Constructs prompt with issue URL
**2. `--mode act`**
- Enables act mode for active investigation
- Allows Cline to use tools (read files, run commands, etc.)
**3. `$ADDRESS`**
- Optional address flag for specific instance
- Expands to `--address <ip:port>` if set
**4. `-F json`**
- Outputs in JSON format for parsing
**5. `sed -n '/^{/,$p'`**
- Extracts JSON from output
- Skips any non-JSON prefix lines
**6. `jq -r 'select(.say == "completion_result") | .text'`**
- Filters for completion result messages
- Extracts the text field
- `-r` outputs raw strings (no JSON quotes)
**7. `sed 's/\\n/\n/g'`**
- Converts escaped newlines to actual newlines
- Makes output readable
</Accordion>
## Sample Output
Here's an example analyzing a real Flutter issue:
```bash
$ ./analyze-issue.sh https://github.com/csells/flutter_counter/issues/2
```
**Output:**
```markdown
**Root Cause Analysis of Issue #2: "setState isn't cutting it"**
After examining the GitHub issue and analyzing the Flutter counter codebase,
I've identified the root cause of why setState() is insufficient for this
project's needs:
## Current Implementation Problems
The current Flutter counter app uses setState() for state management, which
has several limitations:
1. **Local State Only**: setState() only works within a single widget, making
it difficult to share state across the app
2. **Rebuild Overhead**: Every setState() call rebuilds the entire widget tree,
causing performance issues with complex UIs
3. **No State Persistence**: State is lost when the widget is disposed
4. **Testing Challenges**: setState-based logic is tightly coupled to the UI,
making unit testing difficult
## Why This Matters
As the app grows beyond a simple counter, these limitations become critical:
- Multiple screens need to access the count
- State needs to persist across navigation
- Business logic should be testable independently
- UI should only rebuild when necessary
## Recommended Solutions
The issue mentions "Provider or Bloc" - both are excellent alternatives:
1. **Provider**: Simple, lightweight state management using InheritedWidget
- Easy migration path from setState
- Good for small to medium apps
- Official Flutter recommendation
2. **Bloc**: More structured approach with clear separation between events,
states, and business logic
- Better for complex apps
- Excellent testability
- Clear architectural patterns
3. **Riverpod**: Modern alternative to Provider with better performance and
developer experience
- Compile-time safety
- Better testing support
- More flexible than Provider
4. **GetX**: Full-featured solution with state management, routing, and
dependency injection
- Minimal boilerplate
- Fast and lightweight
- All-in-one solution
## Next Steps
The current codebase needs refactoring to implement proper state management
architecture to handle more complex state scenarios effectively. Provider
would be the easiest migration path while Bloc provides better long-term
scalability.
```
## When to Use This Pattern
This script pattern is ideal for various development scenarios where automated GitHub issue analysis can accelerate your workflow.
### Bug Investigation
Quickly analyze bug reports and identify root causes without manual code exploration:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/123 \
"What is the root cause of this bug?"
```
### Feature Request Analysis
Understand context and implications of feature requests:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/456 \
"What are the implementation challenges?"
```
### Security Audits
Assess security implications of reported issues:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/789 \
"What are the security implications?"
```
### Documentation Generation
Generate detailed technical documentation from issues:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/654 \
"Provide detailed technical documentation for this issue"
```
### Code Review Assistance
Get second opinions on proposed changes:
```bash
./analyze-issue.sh https://github.com/project/repo/issues/987 \
"Review the proposed solution approach"
```
## Conclusion
This sample demonstrates how to build an autonomous GitHub issue analysis tool using Cline CLI:
1. **Building autonomous CLI tools** using Cline's capabilities
2. **Parsing structured JSON output** from Cline CLI
3. **Creating flexible automation scripts** with custom prompting
4. **Integrating with GitHub** for issue analysis
5. **Handling command-line arguments** effectively
This pattern can be adapted for many other automation scenarios, from pull request reviews to documentation generation to code quality analysis.
## Related Resources
- [CLI Installation Guide](https://docs.cline.bot/cline-cli/installation)
- [CLI Reference Documentation](https://docs.cline.bot/cline-cli/cli-reference)
- [Three Core Flows](https://docs.cline.bot/cline-cli/three-core-flows)
-32
View File
@@ -1,32 +0,0 @@
---
title: "Samples Overview"
description: Example implementations demonstrating Cline CLI capabilities
---
This section provides sample implementations that demonstrate various Cline CLI features and capabilities. Each sample includes complete code, detailed explanations, and real-world usage examples.
## Available Samples
<CardGroup cols={1}>
<Card
title="GitHub Root Cause Analysis"
icon="magnifying-glass-chart"
href="/cline-cli/samples/github-issue-rca"
>
A command-line script that uses Cline's autonomous AI capabilities to fetch, analyze, and identify root causes of GitHub issues. Features JSON output parsing and non-interactive execution.
</Card>
<Card
title="GitHub Integration (Actions)"
icon="github"
href="/cline-cli/samples/github-integration"
>
Automatically respond to GitHub issues by mentioning @cline in comments. Uses Cline CLI in GitHub Actions to create an AI-powered issue assistant that analyzes and responds autonomously.
</Card>
</CardGroup>
## Additional Resources
- [CLI Installation Guide](/cline-cli/installation)
- [CLI Reference Documentation](/cline-cli/cli-reference)
- [Three Core Flows](/cline-cli/three-core-flows)
@@ -0,0 +1,398 @@
# Plugin System Architecture
## Overview
The Cline Plugin System is a parallel extension architecture that allows third-party VS Code extensions to register tools and capabilities with Cline through a JavaScript API. This system operates independently from the MCP (Model Context Protocol) infrastructure while following similar patterns for capability discovery and execution.
### Key Design Principles
1. **Parallel Architecture**: Plugins run alongside MCP servers without interference or data conversion overhead
2. **VS Code Native**: Direct integration with VS Code extension API for seamless discovery and activation
3. **Isolated Context**: Plugins receive limited execution context with safe service boundaries
4. **Dynamic Discovery**: Capabilities are discovered at runtime and included in LLM system prompts
5. **Graceful Errors**: Plugin failures are isolated and reported to the LLM without breaking the task flow
## Architecture Components
### Component Hierarchy
```
Extension.ts (Activation)
Controller
PluginHub (Service Layer)
Task → ToolExecutor
PluginToolHandler (Tool Coordinator)
Plugin Extension (External)
```
### Core Components
#### 1. PluginHub (`src/services/plugins/PluginHub.ts`)
**Responsibilities:**
- Discover compatible VS Code extensions during Cline activation
- Manage plugin registry and lifecycle
- Execute plugin capabilities with error handling
- Generate plugin sections for system prompts
**Key Methods:**
```typescript
class PluginHub {
// Discovery and registration
async discoverPlugins(): Promise<void>
async registerPlugin(plugin: ClinePlugin, extensionId: string): Promise<void>
async unregisterPlugin(pluginId: string): Promise<void>
// Execution
async executePluginCapability(
pluginId: string,
capabilityName: string,
parameters: Record<string, any>,
taskConfig: TaskConfig
): Promise<any>
// System prompt integration
getPluginPrompts(): string
getPluginCapabilities(): PluginCapability[]
}
```
**State Management:**
- Maintains `Map<string, RegisteredPlugin>` for active plugins
- Tracks capability mappings per plugin
- Stores last error state for debugging
#### 2. PluginContext (`src/services/plugins/PluginContext.ts`)
**Responsibilities:**
- Provide isolated execution context to plugins
- Expose safe services with appropriate boundaries
- Implement logging, storage, and HTTP capabilities
**Security Boundaries:**
- No direct access to VSCode API
- No access to internal Cline state (TaskState, MessageState)
- No file system access (prevents arbitrary file operations)
- Rate-limited HTTP client
- Scoped storage (plugin-specific only)
**Interface:**
```typescript
interface PluginContext {
// Read-only task information
taskId: string
taskMode: 'plan' | 'act'
workingDirectory: string
// Safe services
logger: PluginLogger
storage: PluginStorage
http: PluginHttpClient
// Communication
notify(message: string): void
requestInput(prompt: string): Promise<string>
}
```
#### 3. PluginToolHandler (`src/core/task/tools/handlers/PluginToolHandler.ts`)
**Responsibilities:**
- Integrate plugins into the tool coordinator pattern
- Handle tool execution requests from the LLM
- Format results and errors for LLM consumption
**Implementation Pattern:**
```typescript
export class PluginToolHandler implements IFullyManagedTool {
readonly name = ClineDefaultTool.PLUGIN_EXECUTE
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
const { plugin_id, capability_name, parameters } = block.params
try {
const result = await config.services.pluginHub.executePluginCapability(
plugin_id,
capability_name,
parameters,
config
)
return formatResponse.pluginSuccess(plugin_id, capability_name, result)
} catch (error) {
return formatResponse.pluginError(plugin_id, capability_name, error)
}
}
}
```
## Integration Points
### 1. Extension Activation (`src/extension.ts`)
Plugins are discovered during Cline's activation phase:
```typescript
export async function activate(context: vscode.ExtensionContext) {
// ... existing initialization
// Initialize plugin hub
const pluginHub = new PluginHub(context)
await pluginHub.discoverPlugins()
// Make available to controller
controller.pluginHub = pluginHub
// ... rest of activation
}
```
### 2. API Export (`src/exports/index.ts`)
Plugins register through the exported API:
```typescript
export function createClineAPI(controller: Controller): ClineAPI {
return {
// Existing API methods
startNewTask: async (task, images) => { ... },
sendMessage: async (message, images) => { ... },
// New plugin API
plugins: {
registerPlugin: async (plugin: ClinePlugin) => {
const extensionId = getCallingExtensionId()
await controller.pluginHub.registerPlugin(plugin, extensionId)
},
unregisterPlugin: async (pluginId: string) => {
await controller.pluginHub.unregisterPlugin(pluginId)
}
}
}
}
```
### 3. Tool Coordinator (`src/core/task/ToolExecutor.ts`)
Plugin handler is registered like other tools:
```typescript
private registerToolHandlers(): void {
// ... existing tool registrations
// Register plugin handler
this.coordinator.register(new PluginToolHandler())
}
```
### 4. System Prompt (`src/core/prompts/system-prompt/components/plugins.ts`)
Plugin capabilities are included in the system prompt:
```typescript
export function getPluginSection(context: SystemPromptContext): string {
const pluginHub = context.pluginHub
if (!pluginHub || pluginHub.getPluginCount() === 0) {
return ''
}
return `
# Plugin Extensions
The following plugin extensions are available:
${pluginHub.getPluginPrompts()}
Use the plugin_execute tool to call plugin capabilities.
`
}
```
## Data Flow
### Plugin Registration Flow
```
1. Plugin Extension activates
2. Extension calls Cline's exported API
3. API extracts calling extension ID
4. PluginHub.registerPlugin() called
5. Plugin.getCapabilities() retrieved
6. Capabilities stored in registry
7. Confirmation returned to plugin
```
### Plugin Execution Flow
```
1. LLM generates plugin_execute tool use
2. ToolExecutor routes to PluginToolHandler
3. PluginToolHandler validates parameters
4. PluginContext created from TaskConfig
5. PluginHub.executePluginCapability() called
6. Plugin.executeCapability() invoked
7. Result formatted and returned to LLM
```
## Error Handling Strategy
### Isolation Principles
1. **Try-Catch Boundaries**: All plugin calls wrapped in try-catch
2. **Timeout Protection**: Plugin execution has maximum time limit
3. **Error Propagation**: Errors formatted for LLM understanding
4. **State Preservation**: Plugin errors don't corrupt task state
### Error Types
```typescript
enum PluginErrorType {
REGISTRATION_FAILED = 'registration_failed',
CAPABILITY_NOT_FOUND = 'capability_not_found',
EXECUTION_TIMEOUT = 'execution_timeout',
EXECUTION_ERROR = 'execution_error',
PARAMETER_VALIDATION = 'parameter_validation'
}
```
### Error Reporting to LLM
```
Error executing plugin 'weather-plugin' capability 'getCurrentWeather':
Invalid parameter 'location' - must be a non-empty string.
Available parameters:
- location (string, required): City name or coordinates
- units (string, optional): Temperature units (celsius/fahrenheit)
```
## Comparison with MCP
| Aspect | MCP | Plugin System |
|--------|-----|---------------|
| **Protocol** | JSON-RPC 2.0 | Direct JS API |
| **Transport** | stdio/SSE/HTTP | In-process |
| **Discovery** | Settings file | VS Code extension API |
| **Configuration** | Per-server settings | Package.json metadata |
| **Permissions** | Per-tool approval | Extension-level trust |
| **State** | External process | In-process isolation |
| **Performance** | Protocol overhead | Direct function calls |
| **Use Case** | External tools/APIs | VS Code integration |
## Testing Strategy
### Unit Tests
- **PluginHub**: Registration, execution, error handling
- **PluginContext**: Service boundaries, isolation
- **PluginToolHandler**: Coordinator integration
### Integration Tests
- **End-to-end**: Plugin registration → execution → result
- **Error scenarios**: Timeouts, invalid parameters, plugin crashes
- **System prompt**: Capability inclusion and formatting
### Mock Plugin Pattern
```typescript
class MockWeatherPlugin implements ClinePlugin {
readonly id = 'mock-weather'
readonly name = 'Mock Weather'
readonly version = '1.0.0'
async getCapabilities() {
return [{
name: 'getWeather',
description: 'Get weather data',
parameters: [...]
}]
}
async executeCapability(name, params, context) {
return { temperature: 72, condition: 'sunny' }
}
}
```
## Performance Considerations
1. **Lazy Loading**: Plugins discovered at activation, not on every task
2. **Capability Caching**: Capabilities cached after first retrieval
3. **Async Execution**: All plugin calls are asynchronous
4. **Resource Limits**: HTTP client rate-limited, storage size-limited
## Future Extensibility
Potential enhancements:
1. **Plugin Marketplace**: Discover and install plugins from marketplace
2. **Capability Versioning**: Support multiple versions of same capability
3. **Plugin Dependencies**: Plugins that depend on other plugins
4. **Streaming Results**: Support for streaming responses from plugins
5. **UI Integration**: Plugin-provided UI panels and commands
6. **Resource Access**: Plugin-defined resources (like MCP resources)
## Migration Guide
For developers extending the plugin system:
### Adding New Safe Services to PluginContext
1. Define interface in `PluginContext`
2. Implement service in `PluginContext.ts`
3. Add security boundaries and rate limits
4. Update documentation
5. Add tests for new service
### Adding Plugin-Related Tools
Follow the standard tool handler pattern:
1. Create handler in `src/core/task/tools/handlers/`
2. Implement `IToolHandler` or `IFullyManagedTool`
3. Register in `ToolExecutor.registerToolHandlers()`
4. Add tool to system prompt
5. Update `ClineDefaultTool` enum
## Debugging
### Enable Plugin Logging
```typescript
// In plugin extension
context.logger.setLevel('debug')
context.logger.debug('Executing capability', { name, params })
```
### Inspect Plugin Registry
```typescript
// In Cline developer console
const pluginHub = controller.pluginHub
console.log('Registered plugins:', pluginHub.getPlugins())
console.log('Plugin capabilities:', pluginHub.getPluginCapabilities())
```
### Common Issues
1. **Plugin not discovered**: Check `extensionDependencies` in package.json
2. **Registration fails**: Ensure plugin implements ClinePlugin interface
3. **Execution timeout**: Check plugin execution time, add logging
4. **Context errors**: Verify plugin only uses provided context APIs
## Security Considerations
1. **Extension Trust**: Plugins run with extension permissions - users must trust installed extensions
2. **No Arbitrary Code**: Plugins cannot execute arbitrary code through Cline
3. **Scoped Storage**: Plugin storage isolated from other plugins and Cline
4. **Rate Limiting**: HTTP requests rate-limited to prevent abuse
5. **Error Isolation**: Plugin errors don't expose internal Cline state
## Conclusion
The Plugin System provides a clean, performant way for VS Code extensions to extend Cline's capabilities while maintaining security boundaries and error isolation. By following the patterns established by the internal tool system and MCP integration, plugins integrate seamlessly into Cline's workflow while remaining independent and maintainable.
-13
View File
@@ -88,14 +88,6 @@
"cline-cli/overview",
"cline-cli/installation",
"cline-cli/three-core-flows",
{
"group": "CLI Samples",
"pages": [
"cline-cli/samples/overview",
"cline-cli/samples/github-issue-rca",
"cline-cli/samples/github-integration"
]
},
"cline-cli/cli-reference"
]
},
@@ -138,7 +130,6 @@
"features/drag-and-drop",
"features/editing-messages",
"features/focus-chain",
"features/hooks",
"features/multiroot-workspace",
"features/plan-and-act",
{
@@ -324,10 +315,6 @@
{
"source": "/getting-started/your-first-task",
"destination": "/getting-started/your-first-project"
},
{
"source": "/cline-cli/samples",
"destination": "/cline-cli/samples/overview"
}
],
"search": {
-14
View File
@@ -81,20 +81,6 @@ your-project/
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
### AGENTS.md Standard Support
Cline also supports the [AGENTS.md](https://agents.md/) standard as a fallback
(in addition to Cline Rules) by automatically detecting `AGENTS.md` files in
your workspace root. This allows you to use the same rules file across different AI
coding tools.
```
your-project/
├── AGENTS.md
├── src/
└── ...
```
### Tips for Writing Effective Cline Rules
- Be Clear and Concise: Use simple language and avoid ambiguity.
@@ -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 the AI Pane">
Click the Cursor cube icon button (AI Pane) that opens Cursor's agent (right side view panel)
<Step title="Open Agent Panel">
Click the Cursor cube icon button that opens Cursor's agent (right side view panel)
</Step>
<Step title="Drag Cline to the AI Pane Sidebar">
Drag the Cline icon directly into the AI Pane sidebar.
<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>
</Steps>
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/Cursor-sidebar.gif"
src="https://storage.googleapis.com/cline_public_images/cursor-side-bar.gif"
alt="Cursor Right Sidebar Setup"
/>
</Frame>
+2 -5
View File
@@ -3,7 +3,7 @@ title: "Dictation"
description: "Communicate with Cline using your voice for faster, more natural AI collaboration"
---
Dictation transforms how you work with AI. Instead of typing out complex thoughts, you speak naturally and share your complete intent. This isn't just about speed - though voice is faster - it's about enabling fluid collaboration that typing can't match.
Dictation transforms how you work with AI. Instead of typing out complex thoughts, you speak naturally and share your complete intent. This isn't just about speed - though voice is faster - it's about unlocking the kind of fluid collaboration that typing can't match.
## Why Voice Changes Everything
@@ -35,14 +35,11 @@ Dictation works with any AI model you've configured. The transcription happens t
## System Requirements
<Note>
Dictation is currently not available on Windows. Support for Windows is planned for a future release.
</Note>
Dictation uses FFmpeg to capture your voice across all platforms:
- **macOS**: FFmpeg (via Homebrew: `brew install ffmpeg`)
- **Linux**: FFmpeg (via apt: `sudo apt-get install ffmpeg`)
- **Windows**: FFmpeg (via winget: `winget install Gyan.FFmpeg`)
If you don't have FFmpeg installed, Cline will automatically detect this and prompt you to install it with a single click.
-419
View File
@@ -1,419 +0,0 @@
---
title: "Hooks"
sidebarTitle: "Hooks"
description: "Inject custom logic into Cline's workflow to validate operations, monitor tool usage, and shape AI decisions"
---
Hooks let you inject custom logic into Cline's workflow at key moments. Think of them as automated checkpoints where you can validate operations before they execute, monitor tool usage as it happens, and shape how Cline makes decisions.
Hooks run automatically when specific events happen during development. They receive detailed information about each operation, can block problematic actions before they cause issues, and can inject context that guides future AI decisions.
The real power comes from combining these capabilities. You can:
- Stop operations before they cause problems (like creating `.js` files in a TypeScript project)
- Learn from what's happening and build up project knowledge over time
- Monitor performance and catch issues as they emerge
- Track everything for analytics or compliance
- Trigger external tools or services at the right moments
<Warning>
Hooks are currently supported on macOS and Linux only. Windows support is not available.
</Warning>
## Getting Started
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/hooks.gif" alt="Hooks in action" />
</Frame>
Enabling hooks in Cline is straightforward. Here's what you need to do:
<Steps>
<Step title="Enable Hooks in Settings">
Open Cline settings and check the **"Enable Hooks"** checkbox.
You can find this setting by:
1. Opening Cline
2. Click the "Settings" button on the top right corner
3. Click the "Feature" section in the left side navigation menu.
4. Scroll down until you see the "Enable Hooks" checkbox and check it.
</Step>
<Step title="Choose Your Hook Location">
Decide where to place your hooks:
**For personal or organization-wide hooks:**
- Create hooks in `~/Documents/Cline/Rules/Hooks/`
- These apply to all workspaces automatically
**For project-specific hooks:**
- Create hooks in `.clinerules/hooks/` in your project root
- These only apply to the specific workspace
- Commit them to version control so your team can use them too
</Step>
<Step title="Create Your First Hook">
Hook files must have exact names with no file extensions. For example, to create a TaskStart hook:
```bash
# Create the hook file
vim .clinerules/hooks/TaskStart
```
Add your script (must start with shebang)
``` bash
#!/usr/bin/env bash
# Store piped input into a variable
input=$(cat)
# Dump the entire JSON payload
echo "$input" | jq .
# Get the type of a field
echo "$input" | jq -r '.timestamp | type'
```
This example script demonstrates the key mechanics of hook input/output: reading the JSON payload from stdin with `input=$(cat)`, and using `jq` to inspect the data structure and field types that your hook receives. This helps you understand what data is available before building more complex hook logic.
**Make it executable**
```bash
chmod +x .clinerules/hooks/TaskStart
```
</Step>
<Step title="Test Your Hook">
Start a task in Cline and verify your hook executes.
</Step>
</Steps>
<Tip>
Start with a simple hook that just logs information before building complex validation logic. This helps you understand the data structure and timing.
</Tip>
## What You Can Build
Once you understand the basics, hooks open up creative possibilities:
<CardGroup cols={2}>
<Card title="Intelligent Code Review" icon="code-branch">
Run linters or custom validators before files get saved. Block commits that don't pass checks. Track code quality metrics over time.
</Card>
<Card title="Security Enforcement" icon="shield-halved">
Prevent operations that violate security policies. Detect when sensitive data might be exposed. Audit all file access for compliance.
</Card>
<Card title="Development Analytics" icon="chart-line">
Measure how long different operations take. Identify patterns in how the AI works. Generate productivity reports from hook data.
</Card>
<Card title="Integration Hub" icon="plug">
Connect to issue trackers when certain keywords appear. Update project management tools. Sync with external APIs at the right moments.
</Card>
</CardGroup>
The key is combining hooks with external tools. A hook can be the glue between Cline's workflow and the rest of your development ecosystem.
## Hook Types
Cline provides multiple hook types that let you tap into different stages of the AI workflow. They're organized into categories based on their trigger points and use cases.
<Note>
The hook names below are the exact file names you need to create. For example, to use the TaskStart hook, create a file named `TaskStart` (no file extension) in your hooks directory.
</Note>
Each hook receives base fields in addition to its specific data: `clineVersion`, `hookName`, `timestamp`, `taskId`, `workspaceRoots`, `userId`.
### Tool Execution
These hooks intercept and validate tool operations before and after they execute. Use them to enforce policies, track changes, and learn from operations.
#### PreToolUse
Runs before any tool executes. Use it to block invalid operations, validate parameters, and enforce project policies before changes happen.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PreToolUse",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"preToolUse": {
"toolName": "string",
"parameters": {}
}
}
```
#### PostToolUse
Runs after a tool completes. Use it to learn from results, track performance metrics, and build project knowledge based on operations performed.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PostToolUse",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"postToolUse": {
"toolName": "string",
"parameters": {},
"result": "string",
"success": boolean,
"executionTimeMs": number
}
}
```
### User Interaction
These hooks monitor and enhance user communication with Cline. Use them to validate input, inject context, and track interaction patterns.
#### UserPromptSubmit
Runs when a user sends a message to Cline. Use it to validate input, inject context based on the prompt, and track interaction patterns.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "UserPromptSubmit",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"userPromptSubmit": {
"prompt": "string",
"attachments": ["string"]
}
}
```
### Task Lifecycle
These hooks monitor and respond to task state changes from start to finish. Use them to track progress, restore state, and trigger workflows.
#### TaskStart
Runs when a new task begins. Use it to detect project type, initialize tracking, and inject initial context that shapes how Cline approaches the work.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskStart",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskStart": {
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"initialTask": "string"
}
}
}
```
#### TaskResume
Runs when a task resumes after interruption. Use it to restore state, refresh context, and log resumption for analytics or external system notifications.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskResume",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskResume": {
"taskMetadata": {
"taskId": "string",
"ulid": "string"
},
"previousState": {
"lastMessageTs": "string",
"messageCount": "string",
"conversationHistoryDeleted": "string"
}
}
}
```
#### TaskCancel
Runs when a task is cancelled. Use it to cleanup resources, log cancellation details, and notify external systems about interrupted work.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskCancel",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskCancel": {
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"completionStatus": "string"
}
}
}
```
{/*
#### TaskComplete
Runs when a task finishes successfully. Use it for final cleanup, tracking metrics, generating reports, and triggering post-task workflows.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "TaskComplete",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskComplete": {
"taskMetadata": {
"taskId": "string",
"ulid": "string"
}
}
}
```
*/}
### System Events
These hooks monitor internal Cline operations and system-level events. Use them to track context usage, log system behavior, and analyze performance patterns.
{/*
#### PreCompact
Runs before conversation context is truncated to fit token limits. Use it to monitor compaction frequency, log events, and track context usage patterns.
**Input Fields:**
```json
{
"clineVersion": "string",
"hookName": "PreCompact",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"preCompact": {
"contextSize": number,
"messagesToCompact": number,
"compactionStrategy": "string"
}
}
```
*/}
### JSON Communication
Hooks receive JSON via stdin and return JSON via stdout.
**Output structure:**
```json
{
"cancel": false,
"contextModification": "WORKSPACE_RULES: Use TypeScript",
"errorMessage": "Error details if blocking"
}
```
Your hook script can output logging or diagnostic information to stdout during execution, as long as the JSON response is the last thing written. Cline will parse only the final JSON object from stdout.
For example:
```bash
#!/usr/bin/env bash
echo "Processing hook..." # This is fine
echo "Tool: $tool_name" # This is also fine
# The JSON must be last:
echo '{"cancel": false}'
```
The `cancel` field controls whether execution continues. Set it to `true` to block an action, `false` to allow it.
The `contextModification` field injects text into the conversation. This affects future AI decisions, not the current one. Use prefixes like `WORKSPACE_RULES:` or `PERFORMANCE:` to help categorize the context.
### Understanding Context Timing
Context injection affects future decisions, not current ones. When a hook runs:
1. The AI has already decided what to do
2. The hook can block or allow it
3. Any context gets added to the conversation
4. The next AI request sees that context
This means PreToolUse hooks are for blocking bad actions, while PostToolUse hooks are for learning from completed ones.
## Troubleshooting
### Hook Not Running
- Ensure the "Enable Hooks" setting is checked
- Verify the hook file is executable (`chmod +x hookname`)
- Check the hook file has no syntax errors
- Look for errors in VSCode's Output panel (Cline channel)
### Hook Timing Out
- Reduce complexity of the hook script
- Avoid expensive operations (network calls, heavy computations)
- Consider moving complex logic to a background process
### Context Not Affecting Behavior
Remember that context modifications affect future AI decisions, not the current operation. The AI's current behavior is based on the previous "API Request..." block, and your `contextModification` gets injected into the next "API Request..." block. This means if you need immediate effect, you should use PreToolUse hooks for validation and return `cancel: true` in your hook's JSON response to block Cline from continuing.
When adding context, ensure your modifications are clear and actionable so the AI can understand and apply them effectively. Also check that your context isn't being truncated due to the 50KB limit, as this could prevent important information from reaching the AI.
### Handling Strings with Quotes in JSON Payloads
When your hook needs to include strings containing unescaped quote characters (`"`) in JSON output, use jq's `--arg` flag for proper escaping:
```bash
#!/usr/bin/env bash
# When $output contains unescaped quote characters (")...
output='{"foo":"bar"}'
# Use the --arg flag for automatic string escaping
jq -n --arg ctx "$output" '{cancel: false, contextModification: $ctx}'
# This will result in:
# {
# "cancel": false,
# "contextModification": "{\"foo\":\"bar\"}"
# }
```
The `--arg` flag automatically escapes special characters, preventing JSON parsing errors when your context modification includes complex strings or nested JSON structures.
<Warning>
Hooks run with the same permissions as VS Code. They can access all workspace files and environment variables. Review hooks from untrusted sources before enabling them.
</Warning>
## Related Features
Hooks complement other Cline features:
- [Cline Rules](/features/cline-rules) define high-level guidance that hooks can enforce
- [Checkpoints](/features/checkpoints) let you roll back changes if a hook didn't catch an issue
- [Auto-Approve](/features/auto-approve) works well with hooks as safety nets for automated operations
+1 -1
View File
@@ -35,7 +35,7 @@ Create a simple website in a single HTML file. It should have:
```
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/chat-prompt.png" alt="Cline Chat Prompt"/>
<img src="/assets/installation/chat-prompt.png" alt="Cline Chat Prompt"/>
</Frame>
Press Enter and watch Cline work!
+6 -58
View File
@@ -29,33 +29,11 @@ The "Remote Servers" tab allows you to connect to any MCP server that's accessib
2. Fill in the required information:
- **Server Name**: Provide a unique, descriptive name for the server
- **Server URL**: Enter the complete URL endpoint of the MCP server (e.g., `https://example.com/mcp-sse`)
- **Transport Type**: Select the connection protocol (Streamable HTTP is recommended for modern servers)
3. Click "Add Server" to initiate the connection
4. Cline will attempt to connect to the server and display the connection status
> **Note**: When connecting to a remote server, ensure you trust the source, as MCP servers can execute code in your environment.
#### Transport Types
Cline supports two transport protocols for remote MCP servers:
- **Streamable HTTP (Recommended)**: The modern MCP transport protocol with better performance, reliability, and full OAuth 2.1 authentication support. Use this for most remote servers.
- **SSE (Legacy)**: Server-Sent Events transport. Use this only if the server specifically requires SSE or doesn't support Streamable HTTP.
#### OAuth Authentication
Some MCP servers (like Vercel's MCP) require OAuth authentication to access your data securely. When connecting to an OAuth-enabled server:
1. Add the server as usual with its URL
2. If the server requires authentication, you'll see an error message asking to authenticate.
3. Click the **"Authenticate"** button that appears
4. Your browser will open to the server's authorization page
5. Sign in and grant permission
6. You'll be redirected back to Cline automatically
7. The server will connect and show a green status dot
Once authenticated, your credentials are securely stored and the server will reconnect automatically when you reload Cline. You won't need to authenticate again unless you delete the server or your credentials expire.
### Remote Server Discovery
If you're looking for MCP servers to connect to, several third-party marketplaces provide directories of available servers with various capabilities.
@@ -112,20 +90,9 @@ Toggle the switch next to each server to enable or disable it:
If a server fails to connect:
1. An error message will be displayed with details about the failure
2. **For OAuth errors**: Click the "Authenticate" button to complete the authorization flow
3. Check that the server URL is correct and the server is running
4. Try selecting a different transport type (Streamable HTTP vs SSE)
5. Use the "Restart Server" button to attempt reconnection
6. If problems persist, you can delete the server and try adding it again
#### OAuth-Specific Issues
If you're having trouble authenticating with an OAuth-enabled server:
- **"Authentication required" persists**: Make sure you completed the authorization flow in your browser and didn't cancel it
- **Browser doesn't open**: Check your system's default browser settings and ensure external URLs can be opened
- **Redirect errors**: Verify you're using the latest version of Cline - older versions may not support OAuth
- **Reset authentication**: Delete the server and re-add it to start fresh with a new OAuth flow
2. Check that the server URL is correct and the server is running
3. Use the "Restart Server" button to attempt reconnection
4. If problems persist, you can delete the server and try adding it again
### Advanced Configuration
@@ -138,11 +105,10 @@ For advanced users, Cline stores MCP server configurations in a JSON file that c
{
"mcpServers": {
"exampleServer": {
"url": "https://example.com/mcp-server",
"type": "streamableHttp",
"url": "https://example.com/mcp-sse",
"disabled": false,
"autoApprove": ["tool1", "tool2"],
"timeout": 60
"timeout": 30
}
}
}
@@ -151,10 +117,9 @@ For advanced users, Cline stores MCP server configurations in a JSON file that c
Key configuration options:
- **url**: The endpoint URL (for remote servers)
- **type**: Transport protocol - `"streamableHttp"` (recommended) or `"sse"` (legacy)
- **disabled**: Whether the server is currently enabled (true/false)
- **autoApprove**: List of tool names that don't require confirmation
- **timeout**: Maximum time in seconds to wait for server responses (default: 60)
- **timeout**: Maximum time in seconds to wait for server responses
For additional MCP settings, click the "Advanced MCP Settings" link to access VSCode settings.
@@ -165,20 +130,3 @@ Once connected, Cline can use the tools and resources provided by the MCP server
1. A tool approval prompt will appear (unless auto-approved)
2. Review the tool details and parameters before approving
3. The tool will execute and return results to Cline
### Example: Connecting to Vercel MCP
[Vercel MCP](https://vercel.com/docs/mcp/vercel-mcp) is an OAuth-enabled server that provides tools for managing your Vercel projects and deployments:
1. Click "Remote Servers" tab
2. Enter:
- **Server Name**: `vercel`
- **Server URL**: `https://mcp.vercel.com`
- **Transport Type**: Streamable HTTP (pre-selected)
3. Click "Add Server"
4. You'll see "Authentication required" - click the **"Authenticate"** button
5. Sign in to Vercel in your browser and authorize Cline
6. Return to Cline - the server will automatically connect
7. Vercel's tools (deploy, logs, projects) are now available to Cline!
Your Vercel authentication persists across sessions, so you won't need to re-authenticate each time you use Cline.
@@ -0,0 +1,886 @@
# Creating Cline Plugin Extensions
## Overview
Cline plugins are VS Code extensions that extend Cline's capabilities by registering custom tools and functions. This guide shows you how to create plugins that integrate with other VS Code extensions' APIs, using Python environment intelligence as a practical example.
## Why Create Cline Plugins?
Cline plugins bridge the gap between VS Code extensions and Cline's AI capabilities. Common use cases:
- **Environment Intelligence**: Access runtime environment data (Python interpreters, Node versions, etc.)
- **Tool Integration**: Connect Cline to language servers, debuggers, test runners
- **External APIs**: Integrate third-party services (databases, cloud providers, etc.)
- **Custom Workflows**: Add domain-specific operations tailored to your team
## Quick Start
### 1. Prerequisites
- Node.js 18+ and npm
- VS Code 1.84+
- Basic TypeScript knowledge
- Cline extension installed
### 2. Create Your Extension
```bash
npm install -g yo generator-code
yo code
# Choose: New Extension (TypeScript)
# Extension name: cline-python-env
# Description: Python environment intelligence for Cline
# Initialize git: Yes
```
### 3. Add Cline as Dependency
Edit `package.json`:
```json
{
"name": "cline-python-env",
"displayName": "Cline Python Environment Plugin",
"version": "0.1.0",
"engines": {
"vscode": "^1.84.0"
},
"extensionDependencies": [
"saoudrizwan.claude-dev"
],
"dependencies": {
"@vscode/python-extension": "^1.0.5"
}
}
```
### 4. Install Dependencies
```bash
npm install @vscode/python-extension
```
## Plugin Structure
### Core Interface
Every Cline plugin must implement the `ClinePlugin` interface:
```typescript
interface ClinePlugin {
// Unique identifier (use your extension ID)
readonly id: string
// Display name
readonly name: string
// Semantic version
readonly version: string
// Optional description
readonly description?: string
// Return available capabilities/tools
getCapabilities(): Promise<PluginCapability[]>
// Execute a specific capability
executeCapability(
capabilityName: string,
parameters: Record<string, any>,
context: PluginContext
): Promise<any>
// Optional cleanup
dispose?(): Promise<void>
}
```
### Capability Definition
Each tool/function your plugin provides:
```typescript
interface PluginCapability {
// Unique capability name (within your plugin)
name: string
// Description for the LLM
description: string
// Parameter definitions
parameters: ParameterDefinition[]
// Optional return type description
returns?: string
// Optional usage guidance for the LLM
prompt?: string
// Optional usage examples
examples?: string[]
}
interface ParameterDefinition {
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
required: boolean
description?: string
defaultValue?: any
}
```
### Plugin Context
Your plugin receives a limited context for security:
```typescript
interface PluginContext {
// Current task information
taskId: string
taskMode: 'plan' | 'act'
workingDirectory: string
// Safe services
logger: PluginLogger // Scoped logging
storage: PluginStorage // Plugin-specific storage
http: PluginHttpClient // Rate-limited HTTP
// Communication methods
notify(message: string): void
requestInput(prompt: string): Promise<string>
}
```
## Complete Example: Python Environment Plugin
This plugin integrates with the VS Code Python extension to provide environment intelligence.
### src/plugin.ts
```typescript
import * as vscode from 'vscode'
import { PythonExtension } from '@vscode/python-extension'
import { ClinePlugin, PluginCapability, PluginContext } from './types'
export class PythonEnvPlugin implements ClinePlugin {
readonly id = 'cline-python-env'
readonly name = 'Python Environment Intelligence'
readonly version = '1.0.0'
readonly description = 'Provides Python environment and package information'
private pythonApi?: Awaited<ReturnType<typeof PythonExtension.api>>
async initialize() {
// Get Python extension API
this.pythonApi = await PythonExtension.api()
}
async getCapabilities(): Promise<PluginCapability[]> {
return [
{
name: 'getPythonEnvironment',
description: 'Get detailed information about the active Python environment including version, installed packages, and environment type',
parameters: [],
returns: 'Environment details including Python version, environment type, installed packages with versions, and environment path',
prompt: 'Use this to understand what Python packages are available before suggesting code. Check package versions to generate compatible code.',
examples: [
'Get the current Python environment to check if TensorFlow is installed',
'Check Python version before using version-specific syntax',
'Verify pandas version before generating DataFrame code'
]
},
{
name: 'getPythonVersion',
description: 'Get the Python version of the active environment',
parameters: [],
returns: 'Python version string (e.g., "3.11.2")'
},
{
name: 'checkPackageInstalled',
description: 'Check if a specific package is installed and get its version',
parameters: [
{
name: 'packageName',
type: 'string',
required: true,
description: 'Name of the package to check (e.g., "pandas", "tensorflow")'
}
],
returns: 'Package version if installed, null if not installed',
examples: [
'Check if numpy is installed before suggesting array operations',
'Verify Django version to generate compatible view code'
]
},
{
name: 'getInstallCommand',
description: 'Get the appropriate package installation command for the current environment type',
parameters: [
{
name: 'packageName',
type: 'string',
required: true,
description: 'Name of the package to install'
},
{
name: 'version',
type: 'string',
required: false,
description: 'Optional specific version (e.g., "2.0.0")'
}
],
returns: 'Installation command appropriate for the environment (pip, conda, poetry, etc.)',
prompt: 'Use this to provide correct installation commands. Different environments (venv, conda, poetry) require different commands.'
}
]
}
async executeCapability(
capabilityName: string,
parameters: Record<string, any>,
context: PluginContext
): Promise<any> {
if (!this.pythonApi) {
throw new Error('Python extension API not available')
}
context.logger.info(`Executing ${capabilityName}`, { parameters })
switch (capabilityName) {
case 'getPythonEnvironment':
return await this.getPythonEnvironment(context)
case 'getPythonVersion':
return await this.getPythonVersion(context)
case 'checkPackageInstalled':
return await this.checkPackageInstalled(
parameters.packageName as string,
context
)
case 'getInstallCommand':
return await this.getInstallCommand(
parameters.packageName as string,
parameters.version as string | undefined,
context
)
default:
throw new Error(`Unknown capability: ${capabilityName}`)
}
}
// Core implementation: resolveEnvironment()
private async getPythonEnvironment(context: PluginContext) {
try {
// Get active environment path
const envPath = this.pythonApi!.environments.getActiveEnvironmentPath()
context.logger.debug('Active environment path', { path: envPath.path })
// Resolve full environment details - THE KEY FUNCTION!
const envDetails = await this.pythonApi!.environments.resolveEnvironment(envPath)
if (!envDetails) {
return {
error: 'Could not resolve Python environment',
path: envPath.path
}
}
// Extract and format relevant information
const result = {
path: envPath.path,
version: envDetails.version?.major && envDetails.version?.minor && envDetails.version?.micro
? `${envDetails.version.major}.${envDetails.version.minor}.${envDetails.version.micro}`
: 'unknown',
environmentType: this.detectEnvironmentType(envPath.path),
packages: this.formatPackages(envDetails),
pythonExecutable: envDetails.executable?.uri?.fsPath || envPath.path
}
context.logger.info('Environment resolved successfully', {
version: result.version,
packageCount: result.packages.length
})
return result
} catch (error) {
context.logger.error('Failed to get Python environment', { error })
throw new Error(`Failed to resolve Python environment: ${error}`)
}
}
private async getPythonVersion(context: PluginContext) {
const env = await this.getPythonEnvironment(context)
return env.version
}
private async checkPackageInstalled(
packageName: string,
context: PluginContext
) {
const env = await this.getPythonEnvironment(context)
const pkg = env.packages.find(
p => p.name.toLowerCase() === packageName.toLowerCase()
)
if (pkg) {
context.logger.info(`Package ${packageName} found`, { version: pkg.version })
return pkg.version
}
context.logger.info(`Package ${packageName} not found`)
return null
}
private async getInstallCommand(
packageName: string,
version: string | undefined,
context: PluginContext
) {
const env = await this.getPythonEnvironment(context)
const packageSpec = version ? `${packageName}==${version}` : packageName
// Detect environment type and return appropriate command
switch (env.environmentType) {
case 'conda':
return `conda install ${packageSpec}`
case 'poetry':
return version
? `poetry add ${packageName}@${version}`
: `poetry add ${packageName}`
case 'pipenv':
return `pipenv install ${packageSpec}`
case 'venv':
case 'virtualenv':
return `pip install ${packageSpec}`
default:
// System Python - recommend user flag
context.notify('Using system Python - consider creating a virtual environment')
return `pip install --user ${packageSpec}`
}
}
// Helper methods
private detectEnvironmentType(path: string): string {
if (path.includes('conda') || path.includes('miniconda') || path.includes('anaconda')) {
return 'conda'
} else if (path.includes('poetry')) {
return 'poetry'
} else if (path.includes('pipenv')) {
return 'pipenv'
} else if (path.includes('.venv') || path.includes('venv')) {
return 'venv'
} else if (path.includes('virtualenv')) {
return 'virtualenv'
} else {
return 'system'
}
}
private formatPackages(envDetails: any): Array<{ name: string; version: string }> {
// Note: The actual structure depends on the Python extension API version
// This is a simplified example
const packages: Array<{ name: string; version: string }> = []
// Extract packages from environment details
// The exact property path may vary - check Python extension docs
if (envDetails.packages) {
for (const pkg of envDetails.packages) {
packages.push({
name: pkg.name || 'unknown',
version: pkg.version || 'unknown'
})
}
}
return packages
}
async dispose() {
// Cleanup if needed
this.pythonApi = undefined
}
}
```
### src/extension.ts
```typescript
import * as vscode from 'vscode'
import { PythonEnvPlugin } from './plugin'
export async function activate(context: vscode.ExtensionContext) {
console.log('Python Environment Plugin activating...')
// Get Cline API
const clineExtension = vscode.extensions.getExtension('saoudrizwan.claude-dev')
if (!clineExtension) {
vscode.window.showErrorMessage('Cline extension not found')
return
}
// Activate Cline if not already active
if (!clineExtension.isActive) {
await clineExtension.activate()
}
const clineApi = clineExtension.exports
if (!clineApi || !clineApi.plugins) {
vscode.window.showErrorMessage('Cline plugin API not available')
return
}
// Create and register plugin
const plugin = new PythonEnvPlugin()
await plugin.initialize()
try {
await clineApi.plugins.registerPlugin(plugin)
console.log('Python Environment Plugin registered successfully')
// Cleanup on deactivation
context.subscriptions.push({
dispose: async () => {
await plugin.dispose()
await clineApi.plugins.unregisterPlugin(plugin.id)
}
})
} catch (error) {
vscode.window.showErrorMessage(`Failed to register plugin: ${error}`)
}
}
export function deactivate() {
console.log('Python Environment Plugin deactivated')
}
```
### src/types.ts
```typescript
// Type definitions for Cline plugin interface
// (These would typically be provided by Cline or installed from npm)
export interface ClinePlugin {
readonly id: string
readonly name: string
readonly version: string
readonly description?: string
getCapabilities(): Promise<PluginCapability[]>
executeCapability(
capabilityName: string,
parameters: Record<string, any>,
context: PluginContext
): Promise<any>
dispose?(): Promise<void>
}
export interface PluginCapability {
name: string
description: string
parameters: ParameterDefinition[]
returns?: string
prompt?: string
examples?: string[]
}
export interface ParameterDefinition {
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
required: boolean
description?: string
defaultValue?: any
}
export interface PluginContext {
taskId: string
taskMode: 'plan' | 'act'
workingDirectory: string
logger: PluginLogger
storage: PluginStorage
http: PluginHttpClient
notify(message: string): void
requestInput(prompt: string): Promise<string>
}
export interface PluginLogger {
debug(message: string, data?: any): void
info(message: string, data?: any): void
warn(message: string, data?: any): void
error(message: string, data?: any): void
}
export interface PluginStorage {
get<T>(key: string): Promise<T | undefined>
set<T>(key: string, value: T): Promise<void>
delete(key: string): Promise<void>
clear(): Promise<void>
}
export interface PluginHttpClient {
get(url: string, options?: RequestOptions): Promise<HttpResponse>
post(url: string, data?: any, options?: RequestOptions): Promise<HttpResponse>
}
export interface RequestOptions {
headers?: Record<string, string>
timeout?: number
}
export interface HttpResponse {
status: number
data: any
headers: Record<string, string>
}
```
## How the Plugin Works
### 1. Registration Flow
```
Extension Activates
Get Cline Extension API
Create Plugin Instance
Call initialize() (get Python API)
Register with Cline
Plugin Available to LLM
```
### 2. Execution Flow
```
User asks: "Check if pandas is installed"
LLM generates: plugin_execute tool call
Cline routes to your plugin
executeCapability('checkPackageInstalled', {packageName: 'pandas'})
Your code calls resolveEnvironment()
Return result to LLM
LLM uses result in response
```
### 3. What the LLM Sees
When your plugin is registered, Cline adds it to the system prompt:
```
# Plugin Extensions
## Python Environment Intelligence (cline-python-env)
Provides Python environment and package information
### Available Capabilities:
**getPythonEnvironment**
Description: Get detailed information about the active Python environment including
version, installed packages, and environment type
Usage: Use this to understand what Python packages are available before suggesting code.
Check package versions to generate compatible code.
Examples:
- Get the current Python environment to check if TensorFlow is installed
- Check Python version before using version-specific syntax
- Verify pandas version before generating DataFrame code
Returns: Environment details including Python version, environment type, installed
packages with versions, and environment path
**checkPackageInstalled**
Description: Check if a specific package is installed and get its version
Parameters:
- packageName (string, required): Name of the package to check
Returns: Package version if installed, null if not installed
[... other capabilities ...]
```
## Best Practices
### 1. Error Handling
Always handle errors gracefully:
```typescript
async executeCapability(name: string, params: any, context: PluginContext) {
try {
// Your logic
const result = await this.doSomething(params)
return result
} catch (error) {
// Log the error
context.logger.error(`Failed to execute ${name}`, { error, params })
// Return user-friendly error
throw new Error(
`Failed to ${name}: ${error.message}. ` +
`Please check that the required extension is installed.`
)
}
}
```
### 2. Validate Parameters
```typescript
private validatePackageName(name: string) {
if (!name || typeof name !== 'string') {
throw new Error('Package name must be a non-empty string')
}
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
throw new Error('Invalid package name format')
}
}
```
### 3. Use Context Logging
```typescript
async getPythonEnvironment(context: PluginContext) {
context.logger.info('Fetching Python environment')
const start = Date.now()
const result = await this.pythonApi.environments.resolveEnvironment(path)
context.logger.debug('Environment resolved', {
duration: Date.now() - start,
packageCount: result.packages?.length
})
return result
}
```
### 4. Cache Expensive Operations
```typescript
private envCache?: {
path: string
data: any
timestamp: number
}
async getPythonEnvironment(context: PluginContext) {
const envPath = this.pythonApi!.environments.getActiveEnvironmentPath()
// Check cache (5 minute expiry)
if (this.envCache &&
this.envCache.path === envPath.path &&
Date.now() - this.envCache.timestamp < 300000) {
context.logger.debug('Using cached environment data')
return this.envCache.data
}
// Fetch fresh data
const data = await this.pythonApi!.environments.resolveEnvironment(envPath)
this.envCache = {
path: envPath.path,
data,
timestamp: Date.now()
}
return data
}
```
### 5. Provide Helpful Prompts
Guide the LLM on when and how to use your capabilities:
```typescript
{
name: 'checkPackageInstalled',
description: 'Check if a specific package is installed',
prompt: `
IMPORTANT: Always check if packages are installed before suggesting code that uses them.
Examples of when to use:
- Before generating import statements
- When user mentions a package name
- Before suggesting package-specific solutions
If package is not installed, suggest the installation command using getInstallCommand.
`,
parameters: [...]
}
```
## Testing Your Plugin
### 1. Unit Tests
```typescript
// test/plugin.test.ts
import { PythonEnvPlugin } from '../src/plugin'
import { PluginContext } from '../src/types'
describe('PythonEnvPlugin', () => {
let plugin: PythonEnvPlugin
let mockContext: PluginContext
beforeEach(() => {
plugin = new PythonEnvPlugin()
mockContext = createMockContext()
})
it('should detect conda environment', () => {
const path = '/Users/test/miniconda3/envs/myenv/bin/python'
const type = plugin['detectEnvironmentType'](path)
expect(type).toBe('conda')
})
it('should handle missing package', async () => {
// Mock Python API response
mockPythonApi.environments.resolveEnvironment.mockResolvedValue({
packages: []
})
const result = await plugin.executeCapability(
'checkPackageInstalled',
{ packageName: 'nonexistent' },
mockContext
)
expect(result).toBeNull()
})
})
```
### 2. Integration Testing
Test with Cline running:
1. Install your extension in development mode (`F5` in VS Code)
2. Ask Cline: "What Python packages do I have installed?"
3. Check Cline calls your plugin
4. Verify the response is useful
### 3. Debug Logging
Enable verbose logging in your plugin:
```typescript
if (process.env.DEBUG === 'true') {
context.logger.debug('Full environment details', { envDetails })
}
```
## Publishing Your Plugin
### 1. Prepare for Publication
Update `package.json`:
```json
{
"name": "cline-python-env",
"displayName": "Cline Python Environment Plugin",
"description": "Provides Python environment intelligence to Cline",
"version": "1.0.0",
"publisher": "your-username",
"repository": {
"type": "git",
"url": "https://github.com/your-username/cline-python-env"
},
"keywords": ["cline", "python", "environment", "plugin"],
"categories": ["Other"],
"icon": "icon.png"
}
```
### 2. Add Documentation
Create `README.md`:
```markdown
# Cline Python Environment Plugin
Provides Python environment intelligence to Cline, enabling it to understand
your Python setup and generate more accurate code.
## Features
- Detect Python version and environment type
- Check installed packages and versions
- Generate correct installation commands
- Provide environment-aware code suggestions
## Usage
Install this extension, then ask Cline questions like:
- "What Python packages do I have installed?"
- "Check if TensorFlow is installed"
- "What version of pandas am I using?"
Cline will automatically use this plugin to provide accurate information.
## Requirements
- Cline extension installed
- Python extension for VS Code installed
```
### 3. Publish
```bash
# Install vsce
npm install -g vsce
# Package extension
vsce package
# Publish to marketplace
vsce publish
```
## Troubleshooting
### Plugin Not Registering
**Problem**: Plugin doesn't appear in Cline
**Solutions**:
1. Check `extensionDependencies` includes Cline
2. Verify Cline is active before registration
3. Check console for error messages
4. Ensure plugin implements all required methods
### API Not Available
**Problem**: External extension API returns undefined
**Solutions**:
1. Check external extension is installed
2. Ensure external extension activated first
3. Add activation event: `"onLanguage:python"`
4. Wait for activation: `await extension.activate()`
### Execution Timeouts
**Problem**: Plugin operations take too long
**Solutions**:
1. Cache expensive operations
2. Make operations asynchronous
3. Add progress notifications
4. Implement timeouts with
+9 -5
View File
@@ -21,16 +21,20 @@ 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/
- `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
**Reasoning Models:**
- `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.
+2 -1
View File
@@ -18,7 +18,8 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
Cline supports the following Cerebras models:
- `zai-glm-4.6` - Intelligent general purpose model with 1,500 tokens/s
- `qwen-3-coder-480b-free` (Free tier) - High-performance coding model at no cost
- `qwen-3-coder-480b` - Flagship 480B parameter coding model
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
- `llama-3.3-70b` - Meta's Llama 3.3 model optimized for speed
+10 -2
View File
@@ -34,10 +34,18 @@ 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
- **macOS / Linux / WSL / Git Bash**: `which claude`
- **Windows Command Prompt**: `where claude`
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`
## Supported Models
+1 -2
View File
@@ -42,8 +42,7 @@ h5,
h6,
img {
opacity: 1 !important;
font-family:
"Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !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 */
+1 -5
View File
@@ -123,11 +123,7 @@ const copyWasmFiles = {
},
}
const buildEnvVars = {
"import.meta.url": "_importMetaUrl",
"process.env.IS_STANDALONE": JSON.stringify(standalone),
}
const buildEnvVars = { "import.meta.url": "_importMetaUrl" }
if (production) {
// IS_DEV is always disable in production builds.
buildEnvVars["process.env.IS_DEV"] = "false"
+3 -3
View File
@@ -1,6 +1,6 @@
repositories
temp-files
results
results/evals.db
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
+70 -32
View File
@@ -15,32 +15,48 @@ The Cline Evaluation System allows you to:
The evaluation system consists of two main components:
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.
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.
## Directory Structure
```
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
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
└── ...
```
## Getting Started
@@ -51,14 +67,25 @@ evals/ # Main directory for evaluation system
- 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
cd evals/cli
npm install
npm run build:cli
npm run build
```
### Usage
@@ -79,14 +106,13 @@ node dist/index.js setup --benchmarks exercism
#### Running Evaluations
```bash
node dist/index.js run --benchmark exercism --count 10
node dist/index.js run --model claude-3-opus-20240229 --benchmark exercism
```
Options:
- `--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.
- `--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)
#### Generating Reports
@@ -98,11 +124,24 @@ 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 [polyglot-benchmark](https://github.com/Aider-AI/polyglot-benchmark) repository. These are small, focused programming exercises in various languages.
Modified Exercism exercises from the [pashpashpash/evals](https://github.com/pashpashpash/evals) repository. These are small, focused programming exercises in various languages.
### SWE-Bench (Coming Soon)
@@ -311,8 +350,7 @@ 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
- **Test Success Rate**: Percentage of tests passed
- **Functional Correctness**: Ratio of tests passed to total tests
- **Functional Correctness**: Percentage of tests passed
## Reports
+40 -471
View File
@@ -1,7 +1,6 @@
import chalk from "chalk"
import execa from "execa"
import * as fs from "fs"
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, "../../../")
@@ -21,12 +20,8 @@ export class ExercismAdapter implements BenchmarkAdapter {
if (!fs.existsSync(exercismDir)) {
console.log(`Cloning Exercism repository to ${exercismDir}...`)
await execa("git", ["clone", "https://github.com/Aider-AI/polyglot-benchmark.git", exercismDir])
await execa("git", ["clone", "https://github.com/pashpashpash/evals.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}`)
@@ -34,10 +29,6 @@ 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)
}
}
@@ -60,7 +51,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, "exercises", "practice")
const languageDir = path.join(exercisesDir, language)
// Read exercise directories
const exercises = fs.readdirSync(languageDir).filter((dir) => fs.statSync(path.join(languageDir, dir)).isDirectory())
@@ -70,7 +61,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")
}
@@ -78,23 +69,20 @@ 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 -- --testNamePattern=."]
testCommands = ["npm install", "npm test"]
break
case "python":
testCommands = ["python3 -m pytest -o markers=task *_test.py"]
testCommands = ["python -m pytest -o markers=task *_test.py"]
break
case "go":
testCommands = ["GOWORK=off go test -v"]
testCommands = ["go test"]
break
case "java":
testCommands = ["./gradlew test"]
break
case "rust":
testCommands = ["cargo test -- --include-ignored"]
testCommands = ["cargo test"]
break
default:
testCommands = []
@@ -130,117 +118,53 @@ export class ExercismAdapter implements BenchmarkAdapter {
throw new Error(`Task ${taskId} not found`)
}
// Create temp directory outside workspace for hiding files
const tempDir = path.join(EVALS_DIR, "temp-files", task.id)
fs.mkdirSync(tempDir, { recursive: true })
// Check if Git repository is already initialized
const gitDirExists = fs.existsSync(path.join(task.workspacePath, ".git"))
// Read config.json to get solution and test files
const configPath = path.join(task.workspacePath, ".meta", "config.json")
let config: any = { files: { solution: [], test: [] } }
try {
// Initialize Git repository if needed
if (!gitDirExists) {
await execa("git", ["init"], { cwd: task.workspacePath })
}
if (fs.existsSync(configPath)) {
config = JSON.parse(fs.readFileSync(configPath, "utf-8"))
}
// 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())
// 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")
// Add all files and commit
await execa("git", ["add", "."], { cwd: task.workspacePath })
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)
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
}
}
})
return {
...task,
description,
metadata: {
...task.metadata,
solutionFiles,
tempDir,
config,
},
} catch (error: any) {
console.warn(`Warning: Git operations failed: ${error.message}`)
console.warn("Continuing without Git initialization")
}
return task
}
/**
* Cleanup after task execution (restores hidden files from temp directory)
* Verify the result of a task execution
* @param task The task that was executed
* @param result The result of the task execution
*/
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> {
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
// Run verification commands
let success = true
let output = ""
for (const command of task.verificationCommands) {
try {
const { stdout, stderr } = await execa(command, {
cwd: task.workspacePath,
shell: true,
})
const [cmd, ...args] = command.split(" ")
const { stdout } = await execa(cmd, args, { cwd: task.workspacePath })
output += stdout + "\n"
if (stderr) {
output += stderr + "\n"
}
} catch (error: any) {
success = false
if (error.stdout) {
@@ -252,92 +176,13 @@ export class ExercismAdapter implements BenchmarkAdapter {
}
}
// 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
}
// Parse test results
const testsPassed = (output.match(/PASS/g) || []).length
const testsFailed = (output.match(/FAIL/g) || []).length
const testsTotal = testsPassed + testsFailed
return {
success,
rawOutput: output,
metrics: {
testsPassed,
testsFailed,
@@ -346,280 +191,4 @@ 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}`))
}
}
}
}
}
+10 -1
View File
@@ -1,9 +1,18 @@
import { ExercismAdapter } from "./exercism"
import { BenchmarkAdapter } from "./types"
import { ExercismAdapter } from "./exercism"
import { SWEBenchAdapter } from "./swe-bench"
import { SWELancerAdapter } from "./swelancer"
import { MultiSWEAdapter } from "./multi-swe"
// 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(),
}
/**
+192
View File
@@ -0,0 +1,192 @@
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
},
}
}
}
+125
View File
@@ -0,0 +1,125 @@
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
},
}
}
}
+143
View File
@@ -0,0 +1,143 @@
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
},
}
}
}
+1 -4
View File
@@ -17,7 +17,6 @@ export interface Task {
export interface VerificationResult {
success: boolean
metrics: Record<string, any>
rawOutput?: string
}
/**
@@ -28,7 +27,5 @@ export interface BenchmarkAdapter {
setup(): Promise<void>
listTasks(): Promise<Task[]>
prepareTask(taskId: string): Promise<Task>
cleanupTask(task: Task): Promise<void>
verifyResult(task: Task): Promise<VerificationResult>
runTask(task: Task): Promise<VerificationResult | null>
verifyResult(task: Task, result: any): Promise<VerificationResult>
}
+53
View File
@@ -0,0 +1,53 @@
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
}
}
+57 -43
View File
@@ -1,7 +1,7 @@
import chalk from "chalk"
import * as fs from "fs"
import ora from "ora"
import * as path from "path"
import chalk from "chalk"
import ora from "ora"
import { ResultsDatabase } from "../db"
import { generateMarkdownReport } from "../utils/markdown"
@@ -34,6 +34,7 @@ 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,
@@ -44,10 +45,6 @@ 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
@@ -57,9 +54,6 @@ 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)
@@ -79,14 +73,6 @@ 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
@@ -113,12 +99,6 @@ 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
@@ -132,15 +112,12 @@ 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
@@ -148,9 +125,6 @@ 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)
@@ -169,14 +143,6 @@ 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
}
}
@@ -185,14 +151,60 @@ 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 })
@@ -205,12 +217,14 @@ 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, outputPath)
generateMarkdownReport(summary, benchmarkReports, modelReports, outputPath)
spinner.succeed(`Markdown report generated at ${outputPath}`)
}
+50 -31
View File
@@ -1,13 +1,18 @@
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
}
/**
@@ -16,10 +21,12 @@ 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
const benchmarks = options.benchmark ? [options.benchmark] : ["exercism"] // Default to exercism for now
const model = options.model
const count = options.count || Infinity
console.log(chalk.blue(`Running evaluations for the following benchmarks: ${benchmarks.join(", ")}`))
console.log(chalk.blue(`Running evaluations for model: ${model}`))
console.log(chalk.blue(`Benchmarks: ${benchmarks.join(", ")}`))
// Create a run for each benchmark
for (const benchmark of benchmarks) {
@@ -29,7 +36,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, benchmark)
db.createRun(runId, model, benchmark)
// Get adapter for this benchmark
try {
@@ -56,47 +63,58 @@ export async function runHandler(options: RunOptions): Promise<void> {
const preparedTask = await adapter.prepareTask(task.id)
prepareSpinner.succeed("Task prepared")
let cleanedUp = false
// Spawn VSCode
console.log("Spawning VSCode...")
await spawnVSCode(preparedTask.workspacePath)
// Send task to server
const sendSpinner = ora("Sending task to server...").start()
try {
// Run task using adapter's execution strategy
const finalVerification = await adapter.runTask(preparedTask)
const result = await sendTaskToServer(preparedTask.description, options.apiKey)
sendSpinner.succeed("Task completed")
// 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))
// Verify result
const verifySpinner = ora("Verifying result...").start()
const verification = await adapter.verifyResult(preparedTask, result)
if (verification.success) {
console.log(
chalk.green(`Tests passed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`),
verifySpinner.succeed(
`Verification successful: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
)
} else {
console.log(
chalk.red(`Tests failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`),
verifySpinner.fail(
`Verification failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
)
}
// Store result
const storeSpinner = ora("Storing result...").start()
await storeTaskResult(runId, preparedTask, {}, verification)
await storeTaskResult(runId, preparedTask, result, 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) {
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}`))
}
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))
}
}
}
@@ -107,6 +125,7 @@ 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)
}
}
+7 -6
View File
@@ -1,6 +1,6 @@
import Database from "better-sqlite3"
import * as fs from "fs"
import * as path from "path"
import * as fs from "fs"
import Database from "better-sqlite3"
import { SCHEMA } from "./schema"
const EVALS_DIR = path.resolve(__dirname, "../../../")
@@ -34,15 +34,16 @@ export class ResultsDatabase {
/**
* Create a new evaluation run
* @param id Run ID
* @param model Model name
* @param benchmark Benchmark name
*/
createRun(id: string, benchmark: string): void {
createRun(id: string, model: string, benchmark: string): void {
const stmt = this.db.prepare(`
INSERT INTO runs (id, timestamp, benchmark)
VALUES (?, ?, ?)
INSERT INTO runs (id, timestamp, model, benchmark)
VALUES (?, ?, ?, ?)
`)
stmt.run(id, Date.now(), benchmark)
stmt.run(id, Date.now(), model, benchmark)
}
/**
+1
View File
@@ -5,6 +5,7 @@ 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
);
+29 -11
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env node
import chalk from "chalk"
import { Command } from "commander"
import { reportHandler } from "./commands/report"
import { runHandler } from "./commands/run"
import { runDiffEvalHandler } from "./commands/runDiffEval"
import chalk from "chalk"
import { setupHandler } from "./commands/setup"
import { runHandler } from "./commands/run"
import { reportHandler } from "./commands/report"
import { evalsEnvHandler } from "./commands/evals-env"
import { runDiffEvalHandler } from "./commands/runDiffEval"
// Create the CLI program
const program = new Command()
@@ -16,7 +17,11 @@ 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")
.option(
"-b, --benchmarks <benchmarks>",
"Comma-separated list of benchmarks to set up",
"exercism,swe-bench,swelancer,multi-swe",
)
.action(async (options) => {
try {
await setupHandler(options)
@@ -31,7 +36,9 @@ 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)
@@ -56,6 +63,21 @@ 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")
@@ -64,15 +86,11 @@ 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", "diff-06-26-25")
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
.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)
+79
View File
@@ -0,0 +1,79 @@
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
}
+131
View File
@@ -0,0 +1,131 @@
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")
}
+40 -11
View File
@@ -1,24 +1,28 @@
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>, outputPath: string): void {
export function generateMarkdownReport(
summary: any,
benchmarkReports: Record<string, any>,
modelReports: 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 += `- **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 += `- **Success Rate:** ${(summary.successRate * 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`
@@ -44,12 +48,23 @@ export function generateMarkdownReport(summary: any, benchmarkReports: Record<st
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 += `- **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 += `- **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 += `- **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`
@@ -72,6 +87,20 @@ export function generateMarkdownReport(summary: any, benchmarkReports: Record<st
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`
+52
View File
@@ -0,0 +1,52 @@
import fetch from "node-fetch"
import chalk from "chalk"
/**
* Send a task to the Cline test server
* @param task The task description to send
* @param apiKey Optional Cline API key to use for the task
* @returns The result of the task execution
*/
export async function sendTaskToServer(task: string, apiKey?: string): Promise<any> {
const SERVER_URL = "http://localhost:9876/task"
try {
console.log(chalk.blue(`Sending task to server: ${task.substring(0, 100)}${task.length > 100 ? "..." : ""}`))
const response = await fetch(SERVER_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
task,
apiKey,
}),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Server responded with status ${response.status}: ${errorText}`)
}
const result = await response.json()
if (!result.success) {
throw new Error(`Task execution failed: ${result.error || "Unknown error"}`)
}
if (result.timeout) {
throw new Error("Task execution timed out")
}
return result
} catch (error: any) {
if (error.code === "ECONNREFUSED") {
throw new Error(
"Could not connect to the test server. Make sure VSCode is running with the Cline extension and the test server is active.",
)
}
throw error
}
}
+598
View File
@@ -0,0 +1,598 @@
import execa from "execa"
import * as path from "path"
import * as fs from "fs"
import fetch from "node-fetch"
import * as os from "os"
import { installRequiredExtensions, configureExtensionSettings } from "./extensions"
// Store temporary directories for cleanup
interface VSCodeResources {
tempUserDataDir: string
tempExtensionsDir: string
vscodePid?: number
}
// Global map to track resources for each workspace
const workspaceResources = new Map<string, VSCodeResources>()
/**
* Spawn a VSCode instance with the Cline extension
* @param workspacePath The workspace path to open
* @param vsixPath Optional path to a VSIX file to install
* @returns The resources created for this VS Code instance
*/
export async function spawnVSCode(workspacePath: string, vsixPath?: string): Promise<VSCodeResources> {
// Ensure the workspace path exists
if (!fs.existsSync(workspacePath)) {
throw new Error(`Workspace path does not exist: ${workspacePath}`)
}
// If no VSIX path is provided, build one with IS_TEST=true
if (!vsixPath) {
try {
// Build the VSIX (no longer need to set IS_TEST=true as we'll use evals.env file)
console.log("Building VSIX...")
const clineRoot = path.resolve(process.cwd(), "..", "..")
await execa("npx", ["vsce", "package"], {
cwd: clineRoot,
stdio: "inherit",
})
// Find the generated VSIX file(s)
const files = fs.readdirSync(clineRoot)
const vsixFiles = files.filter((file) => file.endsWith(".vsix"))
if (vsixFiles.length > 0) {
// Get file stats to find the most recent one
const vsixFilesWithStats = vsixFiles.map((file) => {
const filePath = path.join(clineRoot, file)
return {
file,
path: filePath,
mtime: fs.statSync(filePath).mtime,
}
})
// Sort by modification time (most recent first)
vsixFilesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
// Use the most recent VSIX
vsixPath = vsixFilesWithStats[0].path
console.log(`Using most recent VSIX: ${vsixPath} (modified ${vsixFilesWithStats[0].mtime.toISOString()})`)
// Log all found VSIX files for debugging
if (vsixFiles.length > 1) {
console.log(`Found ${vsixFiles.length} VSIX files:`)
vsixFilesWithStats.forEach((f) => {
console.log(` - ${f.file} (modified ${f.mtime.toISOString()})`)
})
}
} else {
console.warn("Could not find generated VSIX file")
}
} catch (error) {
console.warn("Failed to build test VSIX:", error)
}
}
// Create a temporary user data directory for this VS Code instance
const tempUserDataDir = path.join(os.tmpdir(), `vscode-cline-eval-${Date.now()}`)
fs.mkdirSync(tempUserDataDir, { recursive: true })
console.log(`Created temporary user data directory: ${tempUserDataDir}`)
// Create a temporary extensions directory to ensure no other extensions are loaded
const tempExtensionsDir = path.join(os.tmpdir(), `vscode-cline-eval-ext-${Date.now()}`)
fs.mkdirSync(tempExtensionsDir, { recursive: true })
console.log(`Created temporary extensions directory: ${tempExtensionsDir}`)
// Create evals.env file in the workspace to trigger test mode
console.log(`Creating evals.env file in workspace: ${workspacePath}`)
const evalsEnvPath = path.join(workspacePath, "evals.env")
fs.writeFileSync(
evalsEnvPath,
`# 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.
`,
)
// Create settings.json in the temporary user data directory to disable workspace trust
// and configure Cline to auto-open on startup
const settingsDir = path.join(tempUserDataDir, "User")
fs.mkdirSync(settingsDir, { recursive: true })
const settingsPath = path.join(settingsDir, "settings.json")
const settings = {
// Disable workspace trust
"security.workspace.trust.enabled": false,
"security.workspace.trust.startupPrompt": "never",
"security.workspace.trust.banner": "never",
"security.workspace.trust.emptyWindow": true,
// Configure startup behavior
"workbench.startupEditor": "none",
// Auto-open Cline on startup
"cline.autoOpenOnStartup": true,
// Show the activity bar and sidebar
"workbench.activityBar.visible": true,
"workbench.sideBar.visible": true,
"workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.visible": true,
"workbench.view.alwaysShowHeaderActions": true,
"workbench.editor.openSideBySideDirection": "right",
// Disable GitLens from opening automatically
"gitlens.views.repositories.autoReveal": false,
"gitlens.views.fileHistory.autoReveal": false,
"gitlens.views.lineHistory.autoReveal": false,
"gitlens.views.compare.autoReveal": false,
"gitlens.views.search.autoReveal": false,
"gitlens.showWelcomeOnInstall": false,
"gitlens.showWhatsNewAfterUpgrades": false,
// Disable other extensions that might compete for startup focus
"extensions.autoUpdate": false,
}
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
console.log(`Created settings.json to disable workspace trust and auto-open Cline`)
// Create keybindings.json to automatically open Cline on startup
const keybindingsPath = path.join(settingsDir, "keybindings.json")
const keybindings = [
{
key: "alt+c",
command: "workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
},
]
fs.writeFileSync(keybindingsPath, JSON.stringify(keybindings, null, 2))
console.log(`Created keybindings.json to help with Cline activation`)
// Build the command arguments with custom user data directory
const args = [
// Use a custom user data directory to isolate this instance
"--user-data-dir",
tempUserDataDir,
// Use a custom extensions directory to ensure only our extension is loaded
"--extensions-dir",
tempExtensionsDir,
// Disable workspace trust
"--disable-workspace-trust",
"-n",
workspacePath,
// Force the extension to be activated on startup
"--start-up-extension",
"saoudrizwan.claude-dev",
// Run a command on startup to open Cline
"--command",
"workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
// Additional flags to help with extension activation
"--disable-gpu=false",
"--max-memory=4096",
]
// Create a startup script to run commands after VS Code launches
const startupScriptPath = path.join(settingsDir, "startup.js")
const startupScript = `
// This script will be executed when VS Code starts
setTimeout(() => {
// Try to open Cline in the sidebar
require('vscode').commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
}, 5000);
`
fs.writeFileSync(startupScriptPath, startupScript)
console.log(`Created startup script to activate Cline`)
// If a VSIX is provided, install it
if (vsixPath) {
if (!fs.existsSync(vsixPath)) {
throw new Error(`VSIX file does not exist: ${vsixPath}`)
}
args.unshift("--install-extension", vsixPath)
}
// Install required extensions
console.log("Installing required VSCode extensions...")
await installRequiredExtensions(tempExtensionsDir)
// Configure extension settings
console.log("Configuring extension settings...")
configureExtensionSettings(tempUserDataDir)
// Execute the command
try {
// We don't need to install extensions globally anymore since we're using a custom user data directory
// The VSIX will be installed in the isolated environment if provided in the args
// Launch VS Code
console.log("Launching VS Code...")
await execa("code", args, {
stdio: "inherit",
})
// Wait longer for VSCode to initialize and extension to load
console.log("Waiting for VS Code to initialize...")
await new Promise((resolve) => setTimeout(resolve, 30000))
// Create a JavaScript file that will be loaded as a VS Code extension
const extensionDir = path.join(tempExtensionsDir, "cline-activator")
fs.mkdirSync(extensionDir, { recursive: true })
// Create package.json for the extension
const packageJsonPath = path.join(extensionDir, "package.json")
const packageJson = {
name: "cline-activator",
displayName: "Cline Activator",
description: "Activates Cline and starts the test server",
version: "0.0.1",
engines: {
vscode: "^1.60.0",
},
main: "./extension.js",
activationEvents: ["*"],
contributes: {
commands: [
{
command: "cline-activator.activate",
title: "Activate Cline",
},
],
},
}
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
// Create extension.js
const extensionJsPath = path.join(extensionDir, "extension.js")
const extensionJs = `
const vscode = require('vscode');
/**
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
console.log('Cline Activator is now active!');
// Register the command to activate Cline
let disposable = vscode.commands.registerCommand('cline-activator.activate', async function () {
try {
// Make sure the Cline extension is activated
const extension = vscode.extensions.getExtension('saoudrizwan.claude-dev');
if (!extension) {
console.error('Cline extension not found');
return;
}
if (!extension.isActive) {
console.log('Activating Cline extension...');
await extension.activate();
}
// Show the Cline sidebar
console.log('Opening Cline sidebar...');
await vscode.commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
// Wait a moment for the sidebar to initialize
await new Promise(resolve => setTimeout(resolve, 2000));
// Create the test server if it doesn't exist
console.log('Creating test server...');
// Get the visible webview instance
const clineRootPath = '${path.resolve(process.cwd(), "..", "..")}';
const visibleWebview = require(path.join(clineRootPath, 'src', 'core', 'webview')).WebviewProvider.getVisibleInstance();
if (visibleWebview) {
require(path.join(clineRootPath, 'src', 'services', 'test', 'TestServer')).createTestServer(visibleWebview);
console.log('Test server created successfully');
} else {
console.error('No visible webview instance found');
}
} catch (error) {
console.error('Error activating Cline:', error);
}
});
context.subscriptions.push(disposable);
// Automatically run the command after a delay
setTimeout(() => {
vscode.commands.executeCommand('cline-activator.activate');
}, 5000);
}
function deactivate() {}
module.exports = {
activate,
deactivate
}
`
fs.writeFileSync(extensionJsPath, extensionJs)
console.log(`Created Cline Activator extension`)
// Try multiple approaches to activate the extension
let serverStarted = false
// Create an activation script to run in VS Code
const activationScriptPath = path.join(settingsDir, "activate-cline.js")
const activationScript = `
// This script will be executed to activate Cline and start the test server
const vscode = require('vscode');
// Execute the cline-activator.activate command
vscode.commands.executeCommand('cline-activator.activate');
`
fs.writeFileSync(activationScriptPath, activationScript)
console.log(`Created activation script to run in VS Code`)
// Execute the activation script
try {
console.log("Executing activation script to start Cline and test server...")
await execa(
"code",
[
"--user-data-dir",
tempUserDataDir,
"--extensions-dir",
tempExtensionsDir,
"--folder-uri",
`file://${workspacePath}`,
"--execute",
activationScriptPath,
],
{
stdio: "inherit",
},
)
// Wait for the test server to start
console.log("Waiting for test server to start...")
for (let i = 0; i < 30; i++) {
try {
// Try to connect to the test server
const response = await fetch("http://localhost:9876/task", {
method: "OPTIONS",
headers: {
"Content-Type": "application/json",
},
})
if (response.status === 204) {
console.log("Test server is running!")
serverStarted = true
break
}
} catch (error) {
// Server not started yet, wait and try again
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
} catch (error) {
console.warn("Failed to execute activation script:", error)
}
if (!serverStarted) {
console.warn("Test server did not start after multiple attempts")
console.log("You may need to manually open the Cline extension in VS Code")
}
// Store the resources for this workspace
const resources: VSCodeResources = {
tempUserDataDir,
tempExtensionsDir,
}
// Store in the global map
workspaceResources.set(workspacePath, resources)
// Return the resources
return resources
} catch (error: any) {
throw new Error(`Failed to spawn VSCode: ${error.message}`)
}
}
/**
* Clean up VS Code resources and shut down the test server
* @param workspacePath The workspace path to clean up resources for
*/
export async function cleanupVSCode(workspacePath: string): Promise<void> {
console.log(`Cleaning up VS Code resources for workspace: ${workspacePath}`)
// Get the resources for this workspace
const resources = workspaceResources.get(workspacePath)
if (!resources) {
console.log(`No resources found for workspace: ${workspacePath}`)
return
}
// Try to shut down the test server
try {
console.log("Shutting down test server...")
await fetch("http://localhost:9876/shutdown", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
}).catch(() => {
// Ignore errors, the server might already be down
})
} catch (error) {
console.warn(`Error shutting down test server: ${error}`)
}
// Try to gracefully close VS Code instead of killing it
try {
console.log("Attempting to gracefully close VS Code...")
// Create a settings file that will disable the crash reporter and the exit confirmation dialog
const settingsDir = path.join(resources.tempUserDataDir, "User")
const settingsPath = path.join(settingsDir, "settings.json")
// 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}`)
}
}
// Update settings to disable crash reporter and exit confirmation
settings = {
...settings,
"window.confirmBeforeClose": "never",
"telemetry.enableCrashReporter": false,
"window.restoreWindows": "none",
"window.newWindowDimensions": "default",
}
// Write updated settings
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
// On macOS, use AppleScript to quit VS Code gracefully
if (process.platform === "darwin") {
try {
// First try AppleScript to quit VS Code gracefully
await execa("osascript", ["-e", 'tell application "Visual Studio Code" to quit'])
// Wait a moment for VS Code to close
await new Promise((resolve) => setTimeout(resolve, 2000))
} catch (appleScriptError) {
console.warn(`Error using AppleScript to quit VS Code: ${appleScriptError}`)
}
} else if (process.platform === "win32") {
// On Windows, try to use taskkill without /F first
try {
await execa("taskkill", ["/IM", "code.exe"])
// Wait a moment for VS Code to close
await new Promise((resolve) => setTimeout(resolve, 2000))
} catch (taskkillError) {
console.warn(`Error using taskkill to quit VS Code: ${taskkillError}`)
}
} else {
// On Linux, try to use SIGTERM first
try {
// Find VS Code processes
const { stdout } = await execa("ps", ["aux"])
const lines = stdout.split("\n")
for (const line of lines) {
if (line.includes(resources.tempUserDataDir)) {
const parts = line.trim().split(/\s+/)
const pid = parseInt(parts[1])
if (pid && !isNaN(pid)) {
console.log(`Sending SIGTERM to VS Code process with PID: ${pid}`)
try {
// Use SIGTERM instead of SIGKILL for a graceful shutdown
process.kill(pid, "SIGTERM")
} catch (killError) {
console.warn(`Failed to terminate process ${pid}: ${killError}`)
}
}
}
}
// Wait a moment for VS Code to close
await new Promise((resolve) => setTimeout(resolve, 2000))
} catch (psError) {
console.warn(`Error listing processes: ${psError}`)
}
}
// If graceful methods failed, fall back to forceful termination as a last resort
// Check if VS Code is still running with the temp user data dir
let vsCodeStillRunning = false
if (process.platform !== "win32") {
try {
const { stdout } = await execa("ps", ["aux"])
vsCodeStillRunning = stdout.split("\n").some((line) => line.includes(resources.tempUserDataDir))
} catch (error) {
console.warn(`Error checking if VS Code is still running: ${error}`)
}
} else {
try {
const { stdout } = await execa("tasklist", ["/FI", `IMAGENAME eq code.exe`])
vsCodeStillRunning = stdout.includes("code.exe")
} catch (error) {
console.warn(`Error checking if VS Code is still running: ${error}`)
}
}
// If VS Code is still running, use forceful termination as a last resort
if (vsCodeStillRunning) {
console.log("Graceful shutdown failed, falling back to forceful termination...")
if (process.platform === "win32") {
try {
await execa("taskkill", ["/IM", "code.exe", "/F"])
} catch (error) {
console.warn(`Error forcefully terminating VS Code: ${error}`)
}
} else {
try {
const { stdout } = await execa("ps", ["aux"])
const lines = stdout.split("\n")
for (const line of lines) {
if (line.includes(resources.tempUserDataDir)) {
const parts = line.trim().split(/\s+/)
const pid = parseInt(parts[1])
if (pid && !isNaN(pid)) {
console.log(`Forcefully killing VS Code process with PID: ${pid}`)
try {
process.kill(pid, "SIGKILL")
} catch (killError) {
console.warn(`Failed to kill process ${pid}: ${killError}`)
}
}
}
}
} catch (error) {
console.warn(`Error forcefully terminating VS Code: ${error}`)
}
}
}
} catch (error) {
console.warn(`Error closing VS Code: ${error}`)
}
// Clean up temporary directories and evals.env file
try {
console.log(`Removing temporary user data directory: ${resources.tempUserDataDir}`)
fs.rmSync(resources.tempUserDataDir, { recursive: true, force: true })
} catch (error) {
console.warn(`Error removing temporary user data directory: ${error}`)
}
try {
console.log(`Removing temporary extensions directory: ${resources.tempExtensionsDir}`)
fs.rmSync(resources.tempExtensionsDir, { recursive: true, force: true })
} catch (error) {
console.warn(`Error removing temporary extensions directory: ${error}`)
}
// Remove the evals.env file
try {
const evalsEnvPath = path.join(workspacePath, "evals.env")
if (fs.existsSync(evalsEnvPath)) {
console.log(`Removing evals.env file: ${evalsEnvPath}`)
fs.unlinkSync(evalsEnvPath)
}
} catch (error) {
console.warn(`Error removing evals.env file: ${error}`)
}
// Remove from the global map
workspaceResources.delete(workspacePath)
console.log("Cleanup completed")
}
+59 -1694
View File
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -20,7 +20,7 @@
"license": "MIT",
"dependencies": {
"axios": "^1.12.0",
"better-sqlite3": "^12.4.1",
"better-sqlite3": "^11.10.0",
"chalk": "5.6.2",
"dotenv": "^16.5.0",
"commander": "^9.4.1",
@@ -40,8 +40,5 @@
"@types/yargs": "^17.0.19",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"overrides": {
"tar-fs": "^3.1.1"
}
}
+264
View File
@@ -0,0 +1,264 @@
# Implementation Plan
## Overview
Create a parallel VS Code extension plugin system for Cline that allows third-party extensions to register tools and capabilities through a JavaScript API, providing dynamic capability discovery similar to MCP but optimized for direct VS Code extension integration without protocol overhead.
This implementation will create a new plugin system alongside the existing MCP infrastructure, allowing VS Code extensions to declare Cline as a dependency and register tools during their activation. The system will provide limited context access for plugins while maintaining security boundaries, and ensure the LLM always has visibility into available plugin capabilities through system prompts.
### Reference Documentation
**This implementation MUST follow the architectural patterns and API specifications defined in:**
1. **Architecture Documentation**: `docs/development/plugin-system-architecture.md`
- Defines the complete system architecture and component hierarchy
- Specifies integration patterns with existing tool infrastructure
- Details security boundaries and isolation requirements
- Provides error handling and testing strategies
2. **Plugin Development Guide**: `docs/plugin-development/creating-cline-plugins.md`
- Defines the complete plugin interface that extensions must implement
- Shows practical integration patterns (e.g., Python environment example)
- Specifies the context API that plugins receive
- Documents expected behavior and best practices
**All code must be fully compatible with the interfaces and patterns documented in these guides.**
## Types
Define comprehensive TypeScript interfaces for plugin registration, tool definitions, and execution context.
```typescript
// Core plugin interface that extensions must implement
interface ClinePlugin {
readonly id: string
readonly name: string
readonly version: string
readonly description?: string
getCapabilities(): Promise<PluginCapability[]>
executeCapability(capabilityName: string, parameters: Record<string, any>, context: PluginContext): Promise<any>
dispose?(): Promise<void>
}
// Individual capability/tool definition
interface PluginCapability {
name: string
description: string
parameters: ParameterDefinition[]
returns?: string
prompt?: string
examples?: string[]
}
// Parameter schema definition
interface ParameterDefinition {
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
required: boolean
description?: string
defaultValue?: any
}
// Limited context provided to plugins
interface PluginContext {
taskId: string
taskMode: 'plan' | 'act'
workingDirectory: string
// Safe services
logger: PluginLogger
storage: PluginStorage
http: PluginHttpClient
// Communication methods
notify(message: string): void
requestInput(prompt: string): Promise<string>
}
// Plugin registration in Cline's exported API
interface ClinePluginAPI {
registerPlugin(plugin: ClinePlugin): Promise<void>
unregisterPlugin(pluginId: string): Promise<void>
}
// Internal plugin registry types
interface RegisteredPlugin {
plugin: ClinePlugin
extensionId: string
capabilities: Map<string, PluginCapability>
isActive: boolean
lastError?: string
}
```
## Files
Create new plugin system files and modify existing tool infrastructure for integration.
**New Files:**
- `src/services/plugins/PluginHub.ts` - Main plugin management service
- `src/services/plugins/PluginContext.ts` - Limited context implementation for plugins
- `src/services/plugins/types.ts` - Plugin type definitions
- `src/core/task/tools/handlers/PluginToolHandler.ts` - Tool handler for plugin capabilities
- `src/exports/plugin-api.ts` - API exported for plugin extensions
- `src/shared/plugins.ts` - Shared plugin enums and constants
**Modified Files:**
- `src/exports/index.ts` - Add plugin API to main export
- `src/extension.ts` - Initialize PluginHub service
- `src/core/controller/index.ts` - Add plugin hub reference
- `src/core/task/index.ts` - Pass plugin hub to task
- `src/core/task/ToolExecutor.ts` - Register plugin tool handlers
- `src/core/prompts/system-prompt/components/plugins.ts` - Plugin system prompt section
- `src/core/prompts/system-prompt/components/index.ts` - Include plugin section
- `src/shared/tools.ts` - Add plugin tool enum values
## Functions
Implement core plugin management and execution functions.
**New Functions:**
- `PluginHub.discoverPlugins()` - Scan VS Code extensions for Cline plugins
- `PluginHub.registerPlugin(plugin, extensionId)` - Register plugin and capabilities
- `PluginHub.executePluginCapability(pluginId, capabilityName, params)` - Execute plugin tool
- `PluginHub.getPluginPrompts()` - Get all plugin prompts for system prompt
- `PluginContext.createContext(taskConfig)` - Create limited plugin context
- `PluginToolHandler.execute()` - Handle plugin tool execution in coordinator
- `createPluginAPI(controller)` - Create plugin API for export
**Modified Functions:**
- `createClineAPI()` - Include plugin registration API
- `ToolExecutor.registerToolHandlers()` - Register plugin handlers
- `getSystemPrompt()` - Include plugin capabilities in prompt
## Classes
Define plugin management and execution classes.
**New Classes:**
- `PluginHub` - Central plugin registry and management
- `PluginContext` - Limited execution context for plugins
- `PluginLogger` - Scoped logging for plugins
- `PluginStorage` - Plugin-scoped storage interface
- `PluginHttpClient` - Rate-limited HTTP client for plugins
- `PluginToolHandler` - Tool handler implementing IFullyManagedTool
**Modified Classes:**
- `Controller` - Add pluginHub property and initialization
- `Task` - Pass plugin hub to tool executor
- `ToolExecutor` - Include plugin tool registration
## Dependencies
No new external dependencies required - leverages existing VS Code API and Cline infrastructure.
All functionality built on existing dependencies:
- VS Code Extension API for extension discovery and management
- Existing Cline tool infrastructure and coordinator pattern
- Current TypeScript and Zod validation patterns
- Existing error handling and logging systems
### Optional Dependencies for Plugin Developers
Plugin extensions may add their own dependencies to integrate with other VS Code extensions:
- `@vscode/python-extension` - For Python environment integration
- Other VS Code extension APIs as needed for specific integrations
## Testing
Create comprehensive test coverage for plugin system functionality.
**Test Files:**
- `src/core/task/tools/handlers/__tests__/PluginToolHandler.test.ts` - Plugin tool handler tests
- `src/services/plugins/__tests__/PluginHub.test.ts` - Plugin hub functionality tests
- `src/services/plugins/__tests__/PluginContext.test.ts` - Plugin context isolation tests
- `src/exports/__tests__/plugin-api.test.ts` - Plugin API export tests
**Test Coverage:**
- Plugin discovery and registration workflows
- Capability execution with error handling
- Context isolation and security boundaries
- System prompt integration
- Tool coordinator integration
## Implementation Order
Structured implementation sequence to minimize conflicts and enable incremental testing.
### Phase 1: Core Infrastructure (Steps 1-4)
1. **Core Types and Interfaces**
- Define all TypeScript interfaces in `src/services/plugins/types.ts`
- Must match interfaces in `docs/plugin-development/creating-cline-plugins.md`
- Include: `ClinePlugin`, `PluginCapability`, `PluginContext`, `PluginLogger`, `PluginStorage`, `PluginHttpClient`
2. **Plugin Context Implementation**
- Create `PluginContext.ts` with limited context and safe services
- Implement security boundaries as specified in `docs/development/plugin-system-architecture.md`
- Services: `PluginLogger` (scoped logging), `PluginStorage` (plugin-scoped), `PluginHttpClient` (rate-limited)
- Communication: `notify()` and `requestInput()` methods
3. **Plugin Hub Service**
- Implement `PluginHub.ts` with discovery, registration, and execution logic
- Follow architecture defined in `docs/development/plugin-system-architecture.md`
- Key methods:
- `discoverPlugins()` - Initial discovery during activation
- `registerPlugin(plugin, extensionId)` - Active registration
- `executePluginCapability(pluginId, capabilityName, params)` - Execution with error isolation
- `getPluginPrompts()` - Generate system prompt sections
- Maintain `Map<string, RegisteredPlugin>` for registry
4. **Plugin API Export**
- Create `src/exports/plugin-api.ts`
- Integrate into main exports in `src/exports/index.ts`
- API must match specification in plugin development guide:
- `registerPlugin(plugin: ClinePlugin): Promise<void>`
- `unregisterPlugin(pluginId: string): Promise<void>`
### Phase 2: Tool System Integration (Steps 5-6)
5. **Tool Handler Integration**
- Implement `PluginToolHandler.ts` following `IFullyManagedTool` pattern
- Register in `ToolExecutor.registerToolHandlers()`
- Handle tool execution with proper error boundaries
- Format results using `formatResponse.pluginSuccess()` and `formatResponse.pluginError()`
6. **System Prompt Integration**
- Create `src/core/prompts/system-prompt/components/plugins.ts`
- Follow prompt format shown in architecture documentation
- Include plugin capabilities with descriptions, parameters, prompts, and examples
- Add section to main system prompt generation
### Phase 3: Core Integration (Steps 7-8)
7. **Controller Integration**
- Add `pluginHub` property to Controller
- Initialize plugin hub in controller constructor
- Pass plugin hub reference to tasks
8. **Extension Integration**
- Update `extension.ts` activation to initialize plugin system
- Call `pluginHub.discoverPlugins()` during activation
- Ensure proper cleanup on deactivation
### Phase 4: Quality Assurance (Steps 9-10)
9. **Testing Implementation**
- Create comprehensive test suite matching architecture doc testing strategy
- Unit tests: PluginHub, PluginContext, PluginToolHandler
- Integration tests: End-to-end registration and execution
- Mock plugin pattern for testing
- Test error isolation and security boundaries
10. **Documentation Validation**
- Verify implementation matches both documentation files
- Ensure all interfaces are compatible with plugin development guide
- Validate architecture matches architecture documentation
- Create example plugin (Python environment integration recommended)
- **Note: Documentation already complete - validate implementation against it**
### Implementation Guidelines
**Critical Requirements:**
1. All interfaces MUST match `docs/plugin-development/creating-cline-plugins.md` exactly
2. Architecture MUST follow patterns in `docs/development/plugin-system-architecture.md`
3. Security boundaries MUST be enforced as documented
4. Error handling MUST follow isolation principles from architecture doc
5. System prompt format MUST match documented format
**Testing Checkpoints:**
- After Phase 1: Test plugin registration and context creation
- After Phase 2: Test tool execution through coordinator
- After Phase 3: Test end-to-end flow from extension activation
- After Phase 4: Validate against documentation and run full test suite
+288
View File
@@ -0,0 +1,288 @@
# Python Extension API Integration Guide
## Why Integrate with the VS Code Python Extension API?
The Python extension (`ms-python.python`) for VS Code exposes a powerful API that provides **computed intelligence about Python environments** - data that is expensive or impossible to obtain by simply reading source code files.
## The Gold Mine: Environment Intelligence
### What Makes This a Gold Mine?
When building AI-powered code generation tools, understanding the **runtime environment** is just as critical as understanding the code itself. The Python extension has spent years solving the complex problem of:
- **Discovering Python installations** across Windows, macOS, and Linux
- **Detecting virtual environments** (venv, conda, poetry, pipenv, virtualenv)
- **Tracking installed packages** and their versions
- **Managing environment activation** with correct paths and environment variables
- **Monitoring environment changes** in real-time
You get all of this intelligence for FREE through the API.
### The Problem: Reading Code Isn't Enough
Consider this simple Python file:
```python
import pandas as pd
import tensorflow as tf
import requests
df = pd.read_csv('data.csv')
model = tf.keras.Sequential([...])
```
**What you CAN see by reading the file:**
- ✅ The code imports `pandas`, `tensorflow`, and `requests`
- ✅ It uses pandas DataFrames and TensorFlow Keras API
**What you CANNOT see by reading the file:**
- ❌ Which Python interpreter will actually run this code?
- ❌ Are these packages actually installed?
- ❌ What versions are installed? (TensorFlow 1.x vs 2.x is drastically different!)
- ❌ Is this using a virtual environment or system Python?
- ❌ What Python version is being used? (affects available syntax features)
- ❌ What other packages are available for suggestions?
- ❌ Where are packages installed?
- ❌ Is CUDA/GPU support available?
### Why This Data is Valuable for Development
#### 1. **Accurate Code Generation**
Without environment knowledge, you're guessing. With it, you can:
- Generate code using the correct API version (TensorFlow 1.x vs 2.x)
- Use Python version-specific syntax (f-strings in 3.6+, walrus operator in 3.8+)
- Suggest only packages that are actually installed
- Generate environment-appropriate installation commands
#### 2. **Better Error Prevention**
- Warn about missing dependencies BEFORE code execution
- Suggest correct package versions for the Python version in use
- Detect incompatible package combinations
- Prevent suggesting code that won't work in the user's environment
#### 3. **Smart Autocomplete & Suggestions**
- Only suggest APIs from installed package versions
- Recommend packages that work with the current Python version
- Suggest compatible dependency versions
- Provide environment-specific code snippets
#### 4. **Proper Development Workflow**
- Know whether to use `pip`, `conda`, `poetry`, or `pipenv` for installations
- Generate correct activation commands for the environment type
- Understand project structure through environment location
- Respect virtual environment isolation
## Why Environment Data is Expensive to Obtain
### The Hidden Complexity
Getting accurate Python environment information is deceptively difficult:
#### 1. **Cross-Platform Differences**
- Windows: `C:\Python39\python.exe`, `%USERPROFILE%\.virtualenvs\`, registry entries
- macOS: `/usr/local/bin/python3`, homebrew paths, framework builds
- Linux: `/usr/bin/python3`, multiple system versions, various package managers
#### 2. **Environment Type Detection**
Different virtual environment tools have different structures:
- **venv**: `pyvenv.cfg` file
- **conda**: `conda-meta/` directory
- **poetry**: `poetry.lock` + `pyproject.toml`
- **pipenv**: `Pipfile` + `Pipfile.lock`
- **virtualenv**: Similar to venv but older structure
Each requires different detection logic!
#### 3. **Package Discovery**
Finding installed packages isn't trivial:
- Parse `site-packages/` directories
- Read `.dist-info` or `.egg-info` metadata
- Handle different package formats
- Deal with editable installs (`pip install -e`)
- Check multiple potential locations
#### 4. **Environment Activation**
Each environment type activates differently:
```bash
# venv
source .venv/bin/activate # Unix
.venv\Scripts\activate.bat # Windows
# conda
conda activate myenv
# poetry
poetry shell
```
#### 5. **Real-Time Monitoring**
Tracking when users:
- Create new environments
- Switch between environments
- Install/uninstall packages
- Change Python interpreter settings
### The Cost of DIY Implementation
If you tried to implement this yourself:
**Time Investment:**
- 2-4 weeks just for basic cross-platform environment discovery
- 1-2 weeks for package detection and parsing
- 1 week for activation script generation
- Ongoing maintenance for edge cases and new environment tools
**Complexity:**
- Handle all OS-specific quirks
- Parse various metadata formats
- Deal with symlinks and junction points
- Handle spaces and special characters in paths
- Support new environment tools as they emerge
**The Python extension has already done this!** Years of development, bug fixes, and edge case handling are available through a simple API.
## Getting Started: The First Function to Implement
### Recommended: `getActiveEnvironmentPath()`
Start with the simplest and most fundamental function:
```typescript
const pythonApi = await PythonExtension.api();
const envPath = pythonApi.environments.getActiveEnvironmentPath();
console.log(envPath.path);
// Output: "/Users/username/project/.venv/bin/python"
```
#### Why Start Here?
1. **Simple to integrate** - Just one function call
2. **Immediate value** - Tells you which Python the user is actually using
3. **Foundation for more** - Other functions build on this
4. **No complex parsing** - Returns a clean path string
#### What You Get
The active environment path tells you:
- **Environment type detection**: Is it in `.venv/`, `conda/`, or system location?
- **Project context**: Virtual env paths often reveal the project root
- **Isolation awareness**: Know if user is in isolated env (safe) vs system Python (careful!)
- **Interpreter location**: Exact binary that will execute the code
#### Example Usage in Code Generation
```typescript
async function generateInstallCommand(packageName: string) {
const pythonApi = await PythonExtension.api();
const envPath = pythonApi.environments.getActiveEnvironmentPath().path;
// Detect environment type from path
if (envPath.includes('conda')) {
return `conda install ${packageName}`;
} else if (envPath.includes('.venv') || envPath.includes('virtualenv')) {
return `pip install ${packageName}`;
} else if (envPath.includes('poetry')) {
return `poetry add ${packageName}`;
} else {
// System Python - be cautious!
return `pip install --user ${packageName}`;
}
}
```
### Next Level: `resolveEnvironment()`
Once you have the basic integration working, level up with:
```typescript
const envPath = pythonApi.environments.getActiveEnvironmentPath().path;
const details = await pythonApi.environments.resolveEnvironment(envPath);
```
#### What This Unlocks
This function returns **rich environment details**:
- **Python version**: "3.11.2" - Know what syntax features are available
- **Environment type**: "Venv", "Conda", "Poetry", etc.
- **Installed packages**: Complete list with versions
- **Environment variables**: Variables needed for activation
- **Package locations**: Where to find installed libraries
#### Powerful Example: Version-Aware Code Generation
```typescript
async function generateTensorFlowCode() {
const envPath = pythonApi.environments.getActiveEnvironmentPath().path;
const details = await pythonApi.environments.resolveEnvironment(envPath);
// Check TensorFlow version
const tfVersion = details.packages.find(p => p.name === 'tensorflow')?.version;
if (!tfVersion) {
return {
error: "TensorFlow not installed",
suggestion: "Run: pip install tensorflow"
};
}
if (tfVersion.startsWith('1.')) {
// Generate TensorFlow 1.x code
return `
import tensorflow as tf
session = tf.Session()
# TensorFlow 1.x style code
`.trim();
} else {
// Generate TensorFlow 2.x code
return `
import tensorflow as tf
# TensorFlow 2.x style - eager execution by default
model = tf.keras.Sequential([...])
`.trim();
}
}
```
## Implementation Strategy
### Phase 1: Basic Integration
1. ✅ Import `@vscode/python-extension` npm module
2. ✅ Get Python extension API instance
3. ✅ Call `getActiveEnvironmentPath()`
4. ✅ Display environment path in your UI
### Phase 2: Environment Intelligence
1. ✅ Call `resolveEnvironment()` with active path
2. ✅ Cache environment details
3. ✅ Use Python version for syntax decisions
4. ✅ Check installed packages before suggesting imports
### Phase 3: Real-Time Awareness
1. ✅ Subscribe to `onDidChangeActiveEnvironment`
2. ✅ Subscribe to `onDidEnvironmentsChanged`
3. ✅ Update your tool's state when environment changes
4. ✅ Invalidate caches appropriately
## Key Takeaways
🎯 **The Python extension API provides environment intelligence that is:**
- ✨ **Impossible to get cheaply** by reading source files
- 🚀 **Years of development** already done for you
- 🔄 **Real-time and accurate** through native integration
- 🌍 **Cross-platform and battle-tested** across millions of users
🎯 **Start simple with `getActiveEnvironmentPath()`** then expand to `resolveEnvironment()` for maximum value
🎯 **This data transforms your code generation** from guessing to knowing
## Resources
- [Python Extension API Documentation](https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs)
- [@vscode/python-extension NPM Module](https://www.npmjs.com/package/@vscode/python-extension)
- [Python Extension GitHub Repository](https://github.com/microsoft/vscode-python)
---
**Remember:** The Python extension has already solved the hard problems. Your job is to leverage that intelligence to build smarter tools!
+1107 -1667
View File
File diff suppressed because it is too large Load Diff
+10 -28
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.37.1",
"version": "3.33.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -149,12 +149,6 @@
"category": "Cline",
"when": "cline.isDevMode"
},
{
"command": "cline.dev.expireMcpOAuthTokens",
"title": "Expire MCP OAuth Tokens (for testing)",
"category": "Cline",
"when": "cline.isDevMode"
},
{
"command": "cline.addToChat",
"title": "Add to Cline",
@@ -284,11 +278,11 @@
"commandPalette": [
{
"command": "cline.generateGitCommitMessage",
"when": "config.git.enabled && !cline.isGeneratingCommit"
"when": "config.git.enabled && scmProvider == git && !cline.isGeneratingCommit"
},
{
"command": "cline.abortGitCommitMessage",
"when": "config.git.enabled && cline.isGeneratingCommit"
"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
}
]
},
@@ -306,20 +300,16 @@
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
"build:npm": "scripts/build-npm-package.sh",
"build:docker:dev": "node scripts/build-docker-dev.mjs",
"docker:shell": "node scripts/docker-shell.mjs",
"test:install": "bash scripts/test-install.sh",
"dev:cli:watch": "node scripts/dev-cli-watch.mjs",
"postcompile-standalone": "node scripts/package-standalone.mjs",
"postcompile-standalone-npm": "node scripts/package-standalone.mjs --target=npm",
"dev": "npm run protos && npm run watch",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.mjs --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
"protos": "node scripts/build-proto.mjs",
"protos-go": "node scripts/build-go-proto.mjs",
"protos-python": "node scripts/build-python-proto.mjs",
"cli-providers": "node scripts/cli-providers.mjs",
"download-ripgrep": "node scripts/download-ripgrep.mjs",
"postprotos": "biome format src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --no-errors-on-unmatched",
@@ -329,8 +319,7 @@
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc -b --noEmit",
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
"lint:proto": "bash ./scripts/proto-lint.sh",
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && buf lint",
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
"format:fix": "biome check --changed --no-errors-on-unmatched --files-ignore-unknown=true --write",
"fix:all": "biome check --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe",
@@ -339,7 +328,7 @@
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
"test": "npm-run-all test:unit test:integration",
"test:integration": "vscode-test",
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha # Use `UPDATE_SNAPSHOTS=true npm run test:unit` to rebuild prompt snapshots",
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha",
"test:coverage": "vscode-test --coverage",
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
"test:tp-orchestrator": "npx tsx scripts/testing-platform-orchestrator.ts",
@@ -414,8 +403,8 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.922.0",
"@aws-sdk/credential-providers": "^3.922.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/credential-providers": "^3.840.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
@@ -443,12 +432,11 @@
"@opentelemetry/sdk-trace-base": "^2.1.0",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@playwright/test": "^1.55.1",
"@playwright/test": "^1.53.2",
"@sap-ai-sdk/ai-api": "^1.17.0",
"@sap-ai-sdk/orchestration": "^1.17.0",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@tailwindcss/vite": "^4.1.14",
"@types/uuid": "^10.0.0",
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
@@ -468,14 +456,14 @@
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"grpc-health-check": "^2.0.2",
"https-proxy-agent": "^7.0.6",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"jwt-decode": "^4.0.0",
"mammoth": "^1.11.0",
"nanoid": "^5.1.6",
"mammoth": "^1.8.0",
"nice-grpc": "^2.1.12",
"node-machine-id": "^1.1.12",
"ollama": "^0.5.13",
@@ -483,7 +471,6 @@
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
"p-mutex": "^1.0.0",
"p-timeout": "^6.1.4",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
@@ -494,20 +481,15 @@
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"strip-ansi": "^7.1.2",
"tailwindcss": "^4.1.14",
"tree-sitter-wasms": "^0.1.11",
"ts-morph": "^25.0.1",
"turndown": "^7.2.0",
"ulid": "^2.4.0",
"undici": "^7.16.0",
"uuid": "^11.1.0",
"vscode-uri": "^3.1.0",
"web-tree-sitter": "^0.22.6",
"zod": "^3.24.2"
},
"overrides": {
"tar-fs": ">=3.1.1"
},
"c8": {
"reporter": [
"lcov",
+8 -10
View File
@@ -1,12 +1,10 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
// Service for account-related operations
service AccountService {
@@ -14,18 +12,20 @@ service AccountService {
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc accountLoginClicked(EmptyRequest) returns (String);
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
// Subscribe to auth status update events (when authentication state changes)
rpc subscribeToAuthStatusUpdate(EmptyRequest) returns (stream AuthState);
rpc subscribeToAuthStatusUpdate(EmptyRequest)
returns (stream AuthState);
// Handles authentication state changes from the Firebase context.
// Updates the user info in global state and returns the updated value.
rpc authStateChanged(AuthStateChangedRequest) returns (AuthState);
rpc authStateChanged(AuthStateChangedRequest)
returns (AuthState);
// Fetches all user credits data
// (balance, usage transactions, payment transactions)
rpc getUserCredits(EmptyRequest) returns (UserCreditsData);
@@ -40,8 +40,6 @@ service AccountService {
rpc openrouterAuthClicked(EmptyRequest) returns (Empty);
rpc requestyAuthClicked(StringRequest) returns (Empty);
// Returns a link the webview can use to redirect back to the user's IDE.
rpc getRedirectUrl(EmptyRequest) returns (String);
}
+1 -3
View File
@@ -1,12 +1,10 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
service BrowserService {
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
+3 -5
View File
@@ -1,13 +1,11 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
service CheckpointsService {
rpc checkpointDiff(Int64Request) returns (Empty);
@@ -33,13 +31,13 @@ message CheckpointEvent {
CHECKPOINT_COMMIT = 1;
CHECKPOINT_RESTORE = 2;
}
OperationType operation = 1;
string cwd_hash = 2;
bool is_active = 3;
google.protobuf.Timestamp timestamp = 4;
optional string task_id = 5;
optional string commit_hash = 6;
optional string commit_hash = 6;
}
message PathHashMap {
+2 -4
View File
@@ -1,14 +1,12 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
// Service for running IDE commands, for example context menu actions,
// Service for running IDE commands, for example context menu actions,
// commands, etc.
// In contrast to the rest of the ProtoBus services, these are
// intended to be called by the IDE directly instead of through the webview,
+7 -5
View File
@@ -1,16 +1,18 @@
syntax = "proto3";
package cline;
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
message Metadata {}
message Metadata {
}
message EmptyRequest {}
message EmptyRequest {
}
message Empty {}
message Empty {
}
message StringRequest {
string value = 2;
+1 -3
View File
@@ -1,12 +1,10 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
service DictationService {
rpc startRecording(EmptyRequest) returns (RecordingResult);
+37 -58
View File
@@ -1,12 +1,10 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
// Service for file-related operations
service FileService {
@@ -15,10 +13,10 @@ service FileService {
// Opens a file in the editor
rpc openFile(StringRequest) returns (Empty);
// Opens an image in the system viewer
rpc openImage(StringRequest) returns (Empty);
// Opens a mention (file, path, git commit, problem, terminal, or URL)
rpc openMention(StringRequest) returns (Empty);
@@ -27,37 +25,34 @@ service FileService {
// Creates a rule file from either global or workspace rules directory
rpc createRuleFile(RuleFileRequest) returns (RuleFile);
// Search git commits in the workspace
rpc searchCommits(StringRequest) returns (GitCommits);
// Select images and other files from the file system and returns as data URLs & paths respectively
rpc selectFiles(BooleanRequest) returns (StringArrays);
// Convert URIs to workspace-relative paths
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
// Search for files in the workspace with fuzzy matching
rpc searchFiles(FileSearchRequest) returns (FileSearchResults);
// Toggle a Cline rule (enable or disable)
rpc toggleClineRule(ToggleClineRuleRequest) returns (ToggleClineRules);
// Toggle a Cursor rule (enable or disable)
rpc toggleCursorRule(ToggleCursorRuleRequest) returns (ClineRulesToggles);
// Toggle a Windsurf rule (enable or disable)
rpc toggleWindsurfRule(ToggleWindsurfRuleRequest) returns (ClineRulesToggles);
// Toggle an Agents rule (enable or disable)
rpc toggleAgentsRule(ToggleAgentsRuleRequest) returns (ClineRulesToggles);
// Refreshes all rule toggles (Cline, External, and Workflows)
rpc refreshRules(EmptyRequest) returns (RefreshedRules);
// Opens a task's conversation history file on disk
rpc openDiskConversationHistory(StringRequest) returns (Empty);
// Toggles a workflow on or off
rpc toggleWorkflow(ToggleWorkflowRequest) returns (ClineRulesToggles);
@@ -66,7 +61,7 @@ service FileService {
// Open a file in editor by a relative path
rpc openFileRelativePath(StringRequest) returns (Empty);
// Opens or creates a focus chain checklist markdown file for editing
rpc openFocusChainFile(StringRequest) returns (Empty);
}
@@ -77,23 +72,15 @@ message RefreshedRules {
ClineRulesToggles local_cline_rules_toggles = 2;
ClineRulesToggles local_cursor_rules_toggles = 3;
ClineRulesToggles local_windsurf_rules_toggles = 4;
ClineRulesToggles local_agents_rules_toggles = 5;
ClineRulesToggles local_workflow_toggles = 6;
ClineRulesToggles global_workflow_toggles = 7;
ClineRulesToggles local_workflow_toggles = 5;
ClineRulesToggles global_workflow_toggles = 6;
}
// Request to toggle a Windsurf rule
message ToggleWindsurfRuleRequest {
Metadata metadata = 1;
string rule_path = 2; // Path to the rule file
bool enabled = 3; // Whether to enable or disable the rule
}
// Request to toggle an Agents rule
message ToggleAgentsRuleRequest {
Metadata metadata = 1;
string rule_path = 2; // Path to the rule file
bool enabled = 3; // Whether to enable or disable the rule
string rule_path = 2; // Path to the rule file
bool enabled = 3; // Whether to enable or disable the rule
}
// Request to convert a list of URIs to relative paths
@@ -116,25 +103,25 @@ enum FileSearchType {
// Request for file search operations
message FileSearchRequest {
Metadata metadata = 1;
string query = 2; // Search query string
optional string mentions_request_id = 3; // Optional request ID for tracking requests
optional int32 limit = 4; // Optional limit for results (default: 20)
optional FileSearchType selected_type = 5; // Optional selected type filter
optional string workspace_hint = 6; // Optional workspace name to search in
string query = 2; // Search query string
optional string mentions_request_id = 3; // Optional request ID for tracking requests
optional int32 limit = 4; // Optional limit for results (default: 20)
optional FileSearchType selected_type = 5; // Optional selected type filter
optional string workspace_hint = 6; // Optional workspace name to search in
}
// Result for file search operations
message FileSearchResults {
repeated FileInfo results = 1; // Array of file/folder results
optional string mentions_request_id = 2; // Echo of the request ID for tracking
repeated FileInfo results = 1; // Array of file/folder results
optional string mentions_request_id = 2; // Echo of the request ID for tracking
}
// File information structure for search results
message FileInfo {
string path = 1; // Relative path from workspace root
string type = 2; // "file" or "folder"
optional string label = 3; // Display name (usually basename)
optional string workspace_name = 4; // Workspace this result came from
string path = 1; // Relative path from workspace root
string type = 2; // "file" or "folder"
optional string label = 3; // Display name (usually basename)
optional string workspace_name = 4; // Workspace this result came from
}
// Response for searchCommits
@@ -154,32 +141,25 @@ message GitCommit {
// Unified request for all rule file operations
message RuleFileRequest {
Metadata metadata = 1;
bool is_global = 2; // Common field for all operations
bool is_global = 2; // Common field for all operations
optional string rule_path = 3; // Path field for deleteRuleFile (optional)
optional string filename = 4; // Filename field for createRuleFile (optional)
optional string type = 5; // Type of the file to create (optional)
optional string filename = 4; // Filename field for createRuleFile (optional)
optional string type = 5; // Type of the file to create (optional)
}
// Result for rule file operations with meaningful data only
message RuleFile {
string file_path = 1; // Path to the rule file
string display_name = 2; // Filename for display purposes
bool already_exists = 3; // For createRuleFile, indicates if file already existed
}
// Enum for rule scope (local, global, or remote)
enum RuleScope {
LOCAL = 0;
GLOBAL = 1;
REMOTE = 2;
string file_path = 1; // Path to the rule file
string display_name = 2; // Filename for display purposes
bool already_exists = 3; // For createRuleFile, indicates if file already existed
}
// Request to toggle a Cline rule
message ToggleClineRuleRequest {
Metadata metadata = 1;
RuleScope scope = 2; // Scope of the rule (local, global, or remote)
string rule_path = 3; // Path to the rule file
bool enabled = 4; // Whether to enable or disable the rule
bool is_global = 2; // Whether this is a global rule or workspace rule
string rule_path = 3; // Path to the rule file
bool enabled = 4; // Whether to enable or disable the rule
}
// Maps from filepath to enabled/disabled status, matching app's ClineRulesToggles type
@@ -191,14 +171,13 @@ message ClineRulesToggles {
message ToggleClineRules {
ClineRulesToggles global_cline_rules_toggles = 1;
ClineRulesToggles local_cline_rules_toggles = 2;
ClineRulesToggles remote_rules_toggles = 3;
}
// Request to toggle a Cursor rule
message ToggleCursorRuleRequest {
Metadata metadata = 1;
string rule_path = 2; // Path to the rule file
bool enabled = 3; // Whether to enable or disable the rule
string rule_path = 2; // Path to the rule file
bool enabled = 3; // Whether to enable or disable the rule
}
// Request to toggle a workflow on or off
@@ -206,5 +185,5 @@ message ToggleWorkflowRequest {
Metadata metadata = 1;
string workflow_path = 2;
bool enabled = 3;
RuleScope scope = 4; // Scope of the workflow (local, global, or remote)
bool is_global = 4;
}
+2 -3
View File
@@ -1,10 +1,9 @@
syntax = "proto3";
package cline;
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
// Input message for all hooks
message HookInput {
@@ -29,7 +28,7 @@ message HookInput {
// Output message for all hooks
message HookOutput {
string context_modification = 1;
bool cancel = 2;
bool should_continue = 2;
string error_message = 3;
}
+4 -10
View File
@@ -1,12 +1,10 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
option go_package = "github.com/cline/grpc-go/cline";
option java_multiple_files = true;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
service McpService {
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
@@ -18,12 +16,11 @@ service McpService {
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog);
rpc openMcpSettings(EmptyRequest) returns (Empty);
rpc authenticateMcpServer(StringRequest) returns (Empty);
// Subscribe to MCP marketplace catalog updates
rpc subscribeToMcpMarketplaceCatalog(EmptyRequest) returns (stream McpMarketplaceCatalog);
rpc getLatestMcpServers(Empty) returns (McpServers);
// Subscribe to MCP server updates
rpc subscribeToMcpServers(EmptyRequest) returns (stream McpServers);
}
@@ -44,7 +41,6 @@ message AddRemoteMcpServerRequest {
Metadata metadata = 1;
string server_name = 2;
string server_url = 3;
optional string transport_type = 4;
}
message ToggleToolAutoApproveRequest {
@@ -76,7 +72,7 @@ message McpResourceTemplate {
}
enum McpServerStatus {
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
// To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
MCP_SERVER_STATUS_DISCONNECTED = 0; // default
MCP_SERVER_STATUS_CONNECTED = 1;
@@ -93,8 +89,6 @@ message McpServer {
repeated McpResourceTemplate resource_templates = 7;
optional bool disabled = 8;
optional int32 timeout = 9;
optional bool oauth_required = 10;
optional string oauth_auth_status = 11;
}
message McpServers {

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