mirror of
https://github.com/cline/cline.git
synced 2026-09-17 17:45:33 +08:00
Compare commits
79
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1136f25bdf | ||
|
|
1630f37b3f | ||
|
|
6ed664942a | ||
|
|
6dffa3f6ed | ||
|
|
8b1ed0113c | ||
|
|
ba98b44504 | ||
|
|
fdeb639f75 | ||
|
|
76410b42b0 | ||
|
|
e4e347a8a4 | ||
|
|
59e7c2b7b9 | ||
|
|
5371377b21 | ||
|
|
5a444dc30a | ||
|
|
34c48264c0 | ||
|
|
daf14ef181 | ||
|
|
f1bf9b3f90 | ||
|
|
6e20047c3e | ||
|
|
1d4fe88bec | ||
|
|
74dbbb9d6b | ||
|
|
3073cf40be | ||
|
|
50056768d6 | ||
|
|
e7cd32b830 | ||
|
|
d983b451e1 | ||
|
|
bb32aac8e7 | ||
|
|
e6d5f55fb2 | ||
|
|
93f60ebe5b | ||
|
|
a3e000d5dd | ||
|
|
0076be531d | ||
|
|
9de4fc2a2f | ||
|
|
8a4b87a133 | ||
|
|
571480e3b6 | ||
|
|
5c2c264a1d | ||
|
|
04549cf1e3 | ||
|
|
f2a93b6201 | ||
|
|
2f09b994dd | ||
|
|
8aac67bf75 | ||
|
|
10422dd3fc | ||
|
|
6191c681ce | ||
|
|
9ab7b41c24 | ||
|
|
bddfb93b38 | ||
|
|
a219eb8000 | ||
|
|
515b4e5d6a | ||
|
|
aaa3846b82 | ||
|
|
8229dcfe88 | ||
|
|
b4ea1a17a5 | ||
|
|
e263c74325 | ||
|
|
08ca115c66 | ||
|
|
68ffe20b7d | ||
|
|
4517408537 | ||
|
|
42c947afd4 | ||
|
|
d4736afce2 | ||
|
|
b00053ff16 | ||
|
|
2ab8fcb54f | ||
|
|
e697c12e94 | ||
|
|
64dcc3fc89 | ||
|
|
8215d3fdb9 | ||
|
|
59260cefab | ||
|
|
a767f844b9 | ||
|
|
bb7c3556d6 | ||
|
|
8b80c337ae | ||
|
|
d0cac53d5b | ||
|
|
8aedc90214 | ||
|
|
0c118086df | ||
|
|
261a7dd505 | ||
|
|
9abf2f567a | ||
|
|
42de7c81b4 | ||
|
|
06b710f62b | ||
|
|
ea9b8fe0b1 | ||
|
|
05213f2a71 | ||
|
|
d5bad1357d | ||
|
|
a4b1549dac | ||
|
|
e015ce94c0 | ||
|
|
be076cf407 | ||
|
|
580db36476 | ||
|
|
8f8b98bb58 | ||
|
|
36022438cb | ||
|
|
c7afb61e28 | ||
|
|
dfd113a6e5 | ||
|
|
3698d2356c | ||
|
|
ff20c4addc |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
added zai-glm-4.6 as a Cerebras model
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Created GPT5 family specific system prompt template
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added change to hide the context window usage message from env details when using next gen models and before the usage has reached an elevated state
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix: show reasoning budget slider to models with valid thinking config
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Telemtry change: collect domain of openai compatible endpoints when telemetry is enabled
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Requesty base URL, and API key fixes
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Delete all Auth Tokens when logging out
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Support for <think> tags for models that prefer that over <thinking>
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
This PR introduces a Python gRPC codegen flow analogous to the existing Go flow
|
||||
@@ -1,54 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse Hook Example
|
||||
#
|
||||
# This hook runs AFTER a tool is executed. It can:
|
||||
# 1. Observe tool results and outcomes
|
||||
# 2. Add context for FUTURE tool uses via contextModification
|
||||
# 3. Log or track tool usage patterns
|
||||
#
|
||||
# IMPORTANT: Context injection affects FUTURE AI decisions, not the current tool execution.
|
||||
# The tool has already completed when this hook runs.
|
||||
|
||||
# Read the hook input (JSON via stdin)
|
||||
echo "PostToolUse running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
# Extract tool information
|
||||
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName // "unknown"')
|
||||
parameters=$(echo "$input" | jq -r '.postToolUse.parameters // {}')
|
||||
result=$(echo "$input" | jq -r '.postToolUse.result // ""')
|
||||
success=$(echo "$input" | jq -r '.postToolUse.success // false')
|
||||
execution_time=$(echo "$input" | jq -r '.postToolUse.executionTimeMs // 0')
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
# Example 1: Learning from file operations
|
||||
# Track successful file creations to build context about project structure
|
||||
# if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
|
||||
# path=$(echo "$parameters" | jq -r '.path // ""')
|
||||
# cat <<EOF
|
||||
# {
|
||||
# "shouldContinue": true,
|
||||
# "contextModification": "FILE_OPERATIONS: Successfully created '$path'. Future operations should maintain consistency with this file's patterns and structure."
|
||||
# }
|
||||
# EOF
|
||||
# exit 0
|
||||
# fi
|
||||
|
||||
# Example 2: Performance monitoring
|
||||
# Warn about slow operations
|
||||
# if [[ "$execution_time" -gt 5000 ]]; then
|
||||
# cat <<EOF
|
||||
# {
|
||||
# "shouldContinue": true,
|
||||
# "contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms to complete. Consider optimizing future similar operations or breaking them into smaller steps."
|
||||
# }
|
||||
# EOF
|
||||
# exit 0
|
||||
# fi
|
||||
|
||||
# Example 3: Context injection for future tool uses
|
||||
# The context will be available in the NEXT API request
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "TOOL_RESULT: The tool '$tool_name' completed with success=$success. Consider validating the results before proceeding to the next step."
|
||||
"cancel": false,
|
||||
"contextModification": "PostToolUse response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "PostToolUse hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse Hook Example
|
||||
#
|
||||
# This hook runs BEFORE a tool is executed. It can:
|
||||
# 1. Block execution by returning {"shouldContinue": false}
|
||||
# 2. Add context for FUTURE tool uses via contextModification
|
||||
# 3. Validate tool parameters
|
||||
#
|
||||
# IMPORTANT: Context injection affects FUTURE AI decisions, not the current tool execution.
|
||||
# The tool parameters are already determined when this hook runs.
|
||||
|
||||
# Read the hook input (JSON via stdin)
|
||||
echo "PreToolUse running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
# Extract tool information
|
||||
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName // "unknown"')
|
||||
parameters=$(echo "$input" | jq -r '.preToolUse.parameters // {}')
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
# Example 1: Validation - Block invalid operations
|
||||
# Uncomment to prevent creating .js files in a TypeScript project
|
||||
# if [[ "$tool_name" == "write_to_file" ]]; then
|
||||
# path=$(echo "$parameters" | jq -r '.path // ""')
|
||||
# if [[ "$path" == *.js ]]; then
|
||||
# cat <<EOF
|
||||
# {
|
||||
# "shouldContinue": false,
|
||||
# "errorMessage": "VALIDATION FAILED: Cannot create .js files in TypeScript project. Please use .ts extension instead.",
|
||||
# "contextModification": "WORKSPACE_RULES: This is a strict TypeScript project. All new files must use .ts or .tsx extensions."
|
||||
# }
|
||||
# EOF
|
||||
# exit 0
|
||||
# fi
|
||||
# fi
|
||||
|
||||
# Example 2: Context injection for future tool uses
|
||||
# The context will be available in the NEXT API request after this tool completes
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "WORKSPACE_RULES: [For future tool uses] This is a TypeScript React project. When creating files, use .ts/.tsx extensions and include detailed comments explaining the purpose and usage of each function."
|
||||
"cancel": false,
|
||||
"contextModification": "PreToolUse response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "PreToolUse hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
|
||||
+124
-76
@@ -3,8 +3,8 @@
|
||||
## Overview
|
||||
|
||||
Cline hooks allow you to execute custom scripts at specific points in the agentic workflow. Hooks can be placed in either:
|
||||
- **Global hooks directory**: `~/Documents/Cline/Rules/Hooks/` (applies to all workspaces)
|
||||
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to specific workspace)
|
||||
- **Global hooks directory**: `~/Documents/Cline/Hooks/` (applies to all workspaces)
|
||||
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to the workspace the repo is part of)
|
||||
|
||||
Hooks run automatically when enabled.
|
||||
|
||||
@@ -17,17 +17,54 @@ Hooks run automatically when enabled.
|
||||
|
||||
## Available Hooks
|
||||
|
||||
### TaskStart Hook
|
||||
- **When**: Runs when a NEW task is started (not when resuming)
|
||||
- **Purpose**: Initialize task context, validate task requirements, set up environment
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/TaskStart`
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskStart`
|
||||
|
||||
### TaskResume Hook
|
||||
- **When**: Runs when an EXISTING task is resumed (after user clicks resume button)
|
||||
- **Purpose**: Validate resumed task state, restore context, check for changes since last run
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/TaskResume`
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskResume`
|
||||
|
||||
### TaskCancel Hook
|
||||
- **When**: Runs when a task is cancelled or a hook is aborted by the user (only if there's actual active work or work was started)
|
||||
- **Purpose**: Clean up resources, log cancellation, save state
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/TaskCancel`
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskCancel`
|
||||
- **Note**: This hook is NOT cancellable
|
||||
|
||||
### TaskComplete Hook (coming soon!)
|
||||
- **When**: Runs when a task is marked as complete
|
||||
- **Purpose**: Log completion status, perform final cleanup, generate reports
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/TaskComplete`
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskComplete`
|
||||
|
||||
### UserPromptSubmit Hook
|
||||
- **When**: Runs when the user submits a prompt/message (initial task, resume, or feedback)
|
||||
- **Purpose**: Validate user input, preprocess prompts, add context to user messages
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/UserPromptSubmit`
|
||||
- **Workspace Location**: `.clinerules/hooks/UserPromptSubmit`
|
||||
|
||||
### PreToolUse Hook
|
||||
- **When**: Runs BEFORE a tool is executed
|
||||
- **Purpose**: Validate parameters, block execution, or add context
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/PreToolUse` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/PreToolUse` (all platforms)
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/PreToolUse`
|
||||
- **Workspace Location**: `.clinerules/hooks/PreToolUse`
|
||||
|
||||
### PostToolUse Hook
|
||||
- **When**: Runs AFTER a tool completes
|
||||
- **Purpose**: Observe results, track patterns, or add context
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/PostToolUse` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/PostToolUse` (all platforms)
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/PostToolUse`
|
||||
- **Workspace Location**: `.clinerules/hooks/PostToolUse`
|
||||
|
||||
### PreCompact Hook (coming soon!)
|
||||
- **When**: Runs BEFORE the conversation context is compacted/truncated
|
||||
- **Purpose**: Observe compaction events, log context management, track token usage
|
||||
- **Global Location**: `~/Documents/Cline/Hooks/PreCompact`
|
||||
- **Workspace Location**: `.clinerules/hooks/PreCompact`
|
||||
|
||||
## Cross-Platform Hook Format
|
||||
|
||||
@@ -37,13 +74,12 @@ Cline uses a git-style approach for hooks that works consistently across all pla
|
||||
- **No file extensions**: Hooks are named exactly `PreToolUse` or `PostToolUse` (no `.bat`, `.cmd`, `.sh` etc.)
|
||||
- **Shebang required**: First line must be a shebang (e.g., `#!/usr/bin/env bash` or `#!/usr/bin/env node`)
|
||||
- **Executable on Unix**: On Unix/Linux/macOS, hooks must be executable: `chmod +x PreToolUse`
|
||||
- **Windows**: No special permissions needed - hooks are executed through the shell
|
||||
- **Windows**: Not currently supported.
|
||||
|
||||
### How It Works
|
||||
|
||||
Like git hooks, Cline executes hook files through a shell that interprets the shebang line:
|
||||
- On Unix/Linux/macOS: Native shell execution with shebang support
|
||||
- On Windows: Shell execution handles shebang interpretation
|
||||
|
||||
This means:
|
||||
- ✅ Same hook script works on all platforms
|
||||
@@ -55,16 +91,10 @@ This means:
|
||||
**On Unix/Linux/macOS:**
|
||||
```bash
|
||||
# Create hook file
|
||||
nano ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
nano ~/Documents/Cline/Hooks/PreToolUse
|
||||
|
||||
# Make executable
|
||||
chmod +x ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
```
|
||||
|
||||
**On Windows:**
|
||||
```batch
|
||||
REM Create hook file (note: no file extension)
|
||||
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
|
||||
chmod +x ~/Documents/Cline/Hooks/PreToolUse
|
||||
```
|
||||
|
||||
## Context Injection Timing
|
||||
@@ -107,11 +137,46 @@ All hooks receive:
|
||||
```json
|
||||
{
|
||||
"clineVersion": "string",
|
||||
"hookName": "PreToolUse" | "PostToolUse",
|
||||
"hookName": "TaskStart" | "TaskResume" | "TaskCancel" | "TaskComplete" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PreCompact",
|
||||
"timestamp": "string",
|
||||
"taskId": "string",
|
||||
"workspaceRoots": ["string"],
|
||||
"userId": "string",
|
||||
"taskStart": { // Only for TaskStart
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string",
|
||||
"initialTask": "string"
|
||||
}
|
||||
},
|
||||
"taskResume": { // Only for TaskResume
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string"
|
||||
},
|
||||
"previousState": {
|
||||
"lastMessageTs": "string",
|
||||
"messageCount": "string",
|
||||
"conversationHistoryDeleted": "string"
|
||||
}
|
||||
},
|
||||
"taskCancel": { // Only for TaskCancel
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string",
|
||||
"completionStatus": "string"
|
||||
}
|
||||
},
|
||||
"taskComplete": { // Only for TaskComplete
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string"
|
||||
}
|
||||
},
|
||||
"userPromptSubmit": { // Only for UserPromptSubmit
|
||||
"prompt": "string",
|
||||
"attachments": ["string"]
|
||||
},
|
||||
"preToolUse": { // Only for PreToolUse
|
||||
"toolName": "string",
|
||||
"parameters": {}
|
||||
@@ -122,6 +187,11 @@ All hooks receive:
|
||||
"result": "string",
|
||||
"success": boolean,
|
||||
"executionTimeMs": number
|
||||
},
|
||||
"preCompact": { // Only for PreCompact
|
||||
"contextSize": number,
|
||||
"messagesToCompact": number,
|
||||
"compactionStrategy": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -131,38 +201,21 @@ All hooks receive:
|
||||
All hooks must return:
|
||||
```json
|
||||
{
|
||||
"shouldContinue": boolean, // Required: Allow or block execution
|
||||
"contextModification": "string", // Optional: Context for future tool uses
|
||||
"cancel": boolean, // Required: false to continue, true to block execution
|
||||
"contextModification": "string", // Optional: Context for future AI decisions
|
||||
"errorMessage": "string" // Optional: Error details if blocking
|
||||
}
|
||||
```
|
||||
|
||||
## Context Modification Format
|
||||
|
||||
Use structured prefixes to help the AI understand context type:
|
||||
|
||||
- `WORKSPACE_RULES:` - Project conventions and requirements
|
||||
- `FILE_OPERATIONS:` - File creation/modification patterns
|
||||
- `TOOL_RESULT:` - Outcomes of tool executions
|
||||
- `PERFORMANCE:` - Performance concerns
|
||||
- `VALIDATION:` - Validation results
|
||||
- Custom prefixes as needed
|
||||
|
||||
Example:
|
||||
```bash
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "WORKSPACE_RULES: This is a TypeScript project. All new files must use .ts or .tsx extensions."
|
||||
}
|
||||
EOF
|
||||
```
|
||||
**Note**: The `cancel` field works as follows:
|
||||
- `false` (or omitted): Allow execution to continue
|
||||
- `true`: Block execution and show error message to user
|
||||
|
||||
## Hook Execution Limits
|
||||
|
||||
- **Timeout**: Hooks must complete within 30 seconds
|
||||
- **Context Size**: Context modifications are limited to 50KB
|
||||
- **Error Handling**: Unexpected file system errors are propagated; expected errors (file not found, permission denied) are handled silently
|
||||
- **Timeout**: Hooks must complete within 30 seconds (configurable via `HOOK_EXECUTION_TIMEOUT_MS`)
|
||||
- **Context Size**: Context modifications are limited to 50KB (configurable via `MAX_CONTEXT_MODIFICATION_SIZE`)
|
||||
- **Error Handling**: Expected errors (file not found, permission denied, not a directory) are handled silently; unexpected file system errors are propagated
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
@@ -177,15 +230,15 @@ path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
|
||||
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": false,
|
||||
"cancel": true,
|
||||
"errorMessage": "Cannot create .js files in TypeScript project",
|
||||
"contextModification": "WORKSPACE_RULES: Use .ts/.tsx extensions only"
|
||||
"contextModification": "Use .ts/.tsx extensions only"
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
### 2. Context Building - Learn from Operations
|
||||
@@ -200,12 +253,12 @@ path=$(echo "$input" | jq -r '.postToolUse.parameters.path // ""')
|
||||
if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "FILE_OPERATIONS: Created '$path'. Maintain consistency with this file's patterns in future operations."
|
||||
"cancel": false,
|
||||
"contextModification": "Created '$path'. Maintain consistency with this file's patterns in future operations."
|
||||
}
|
||||
EOF
|
||||
else
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -220,12 +273,12 @@ tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
|
||||
if [[ "$execution_time" -gt 5000 ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
|
||||
"cancel": false,
|
||||
"contextModification": "Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
|
||||
}
|
||||
EOF
|
||||
else
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -239,7 +292,7 @@ input=$(cat)
|
||||
echo "$input" >> ~/.cline/hook-logs/tool-usage.jsonl
|
||||
|
||||
# Allow execution
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
## Global vs Workspace Hooks
|
||||
@@ -247,44 +300,40 @@ echo '{"shouldContinue": true}'
|
||||
Cline supports two levels of hooks:
|
||||
|
||||
### Global Hooks
|
||||
- **Location**: `~/Documents/Cline/Rules/Hooks/` (macOS/Linux) or `%USERPROFILE%\Documents\Cline\Rules\Hooks\` (Windows)
|
||||
- **Location**: `~/Documents/Cline/Hooks/` (macOS/Linux)
|
||||
- **Scope**: Apply to ALL workspaces and projects
|
||||
- **Use Case**: Organization-wide policies, personal preferences, universal validations
|
||||
- **Priority**: Execute FIRST, before workspace hooks
|
||||
- **Priority**: Order not guaranteed when combined with workspace hooks
|
||||
|
||||
### Workspace Hooks
|
||||
- **Location**: `.clinerules/hooks/` in each workspace root
|
||||
- **Scope**: Apply only to the specific workspace
|
||||
- **Use Case**: Project-specific rules, team conventions, repository requirements
|
||||
- **Priority**: Execute AFTER global hooks
|
||||
- **Priority**: Order not guaranteed when combined with global hooks
|
||||
|
||||
### Hook Execution
|
||||
|
||||
When multiple hooks exist (global and/or workspace):
|
||||
- All hooks for a given step (PreToolUse or PostToolUse) are executed
|
||||
- **Execution order is not guaranteed** - hooks may run concurrently
|
||||
- If ALL hooks allow execution (`shouldContinue: true`), the tool proceeds
|
||||
- If ANY hook blocks (`shouldContinue: false`), execution is blocked
|
||||
- All hooks for a given step are executed **concurrently** using `Promise.all`
|
||||
- **Execution order is not guaranteed** - hooks run in parallel
|
||||
- If ALL hooks allow execution (`cancel: false`), the tool proceeds
|
||||
- If ANY hook blocks (`cancel: true`), execution is blocked
|
||||
|
||||
**Result Combination:**
|
||||
- `shouldContinue`: Must be `true` from ALL hooks for execution to proceed
|
||||
- `contextModification`: All context strings are concatenated
|
||||
- `errorMessage`: All error messages are concatenated
|
||||
- `cancel`: If ANY hook returns `true`, execution is blocked
|
||||
- `contextModification`: All context strings are concatenated with double newlines (`\n\n`)
|
||||
- `errorMessage`: All error messages are concatenated with single newlines (`\n`)
|
||||
|
||||
### Setting Up Global Hooks
|
||||
|
||||
1. The global hooks directory is automatically created at:
|
||||
- macOS/Linux: `~/Documents/Cline/Rules/Hooks/`
|
||||
- Windows: `%USERPROFILE%\Documents\Cline\Rules\Hooks\`
|
||||
- macOS/Linux: `~/Documents/Cline/Hooks/`
|
||||
|
||||
2. Add your hook script:
|
||||
```bash
|
||||
# Unix/Linux/macOS
|
||||
nano ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
chmod +x ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
|
||||
# Windows
|
||||
notepad %USERPROFILE%\Documents\Cline\Rules\Hooks\PreToolUse
|
||||
nano ~/Documents/Cline/Hooks/PreToolUse
|
||||
chmod +x ~/Documents/Cline/Hooks/PreToolUse
|
||||
```
|
||||
|
||||
3. Enable hooks in Cline settings
|
||||
@@ -294,18 +343,18 @@ When multiple hooks exist (global and/or workspace):
|
||||
**Global Hook** (applies to all projects):
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# ~/Documents/Cline/Rules/Hooks/PreToolUse
|
||||
# ~/Documents/Cline/Hooks/PreToolUse
|
||||
# Universal rule: Never delete package.json
|
||||
input=$(cat)
|
||||
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
|
||||
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
|
||||
|
||||
if [[ "$tool_name" == "write_to_file" && "$path" == *"package.json"* ]]; then
|
||||
echo '{"shouldContinue": false, "errorMessage": "Global policy: Cannot modify package.json"}'
|
||||
echo '{"cancel": true, "errorMessage": "Global policy: Cannot modify package.json"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
**Workspace Hook** (applies to specific project):
|
||||
@@ -318,11 +367,11 @@ tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
|
||||
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
|
||||
|
||||
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
|
||||
echo '{"shouldContinue": false, "errorMessage": "Project rule: Use .ts files only"}'
|
||||
echo '{"cancel": true, "errorMessage": "Project rule: Use .ts files only"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
**All hooks must allow execution for the tool to proceed.** Hooks may execute concurrently.
|
||||
@@ -331,7 +380,7 @@ echo '{"shouldContinue": true}'
|
||||
|
||||
If you have multiple workspace roots, you can place hooks in each root's `.clinerules/hooks/` directory. All hooks (global and workspace) may execute concurrently. Their results will be combined:
|
||||
|
||||
- **shouldContinue**: If ANY hook returns false, execution is blocked
|
||||
- **cancel**: If ANY hook returns `true`, execution is blocked
|
||||
- **contextModification**: All context modifications are concatenated
|
||||
- **errorMessage**: All error messages are concatenated
|
||||
|
||||
@@ -352,7 +401,6 @@ If you have multiple workspace roots, you can place hooks in each root's `.cline
|
||||
|
||||
### Context Not Affecting Behavior
|
||||
- Remember: context affects FUTURE decisions, not the current tool
|
||||
- Use PreToolUse for validation (blocking) if you need immediate effect
|
||||
- Ensure context modifications are clear and actionable
|
||||
- Check that context isn't being truncated (50KB limit)
|
||||
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "TaskCancel running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "TaskCancel response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "TaskCancel hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "TaskResume running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "TaskResume response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "TaskResume hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "TaskStart running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "TaskStart response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "TaskStart hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "UserPromptSubmit running inside local cline/.clinerules/hooks/ directory"
|
||||
|
||||
input=$(cat)
|
||||
echo $input | jq .
|
||||
|
||||
for i in {1..5}; do
|
||||
sleep 1
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "UserPromptSubmit response from the local cline/.clinerules/hooks/ directory.",
|
||||
"errorMessage": "UserPromptSubmit hook custom errorMessage"
|
||||
}
|
||||
EOF
|
||||
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
@@ -0,0 +1,556 @@
|
||||
# Discuss Mode Implementation Plan
|
||||
|
||||
> **Voice-enabled interactive planning conversations for Cline**
|
||||
> Last Updated: November 8, 2025
|
||||
> Status: 🚧 In Progress
|
||||
|
||||
## 📋 Quick Status
|
||||
|
||||
- **Start Date:** November 8, 2025
|
||||
- **Target Completion:** TBD
|
||||
- **Current Phase:** Backend Infrastructure Complete (Phases 1-3)
|
||||
- **Overall Progress:** 35% (15/43 tasks complete)
|
||||
|
||||
## 🎯 Project Goals
|
||||
|
||||
Enable natural voice conversations with Cline during Plan Mode to create a collaborative planning experience where users can discuss requirements before implementation begins.
|
||||
|
||||
### Core Features
|
||||
- ✅ Voice input (already exists)
|
||||
- ⬜ Voice output via ElevenLabs TTS
|
||||
- ⬜ Auto-continue conversation flow
|
||||
- ⬜ Interactive question-asking behavior
|
||||
- ⬜ Plan completion detection
|
||||
- ⬜ Smooth transition to Act Mode
|
||||
|
||||
### Scope
|
||||
- **In Scope:** Voice conversations in Plan Mode only
|
||||
- **Out of Scope:** Voice during Act Mode (tool execution)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
|
||||
### System Components
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Discuss Mode Stack │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Frontend (React/TypeScript) │
|
||||
│ ├── DiscussModeToggle.tsx (UI control) │
|
||||
│ ├── AudioPlayer.tsx (TTS playback) │
|
||||
│ ├── VoiceConversationControls.tsx (speaking indicators) │
|
||||
│ └── VoiceSettingsSection.tsx (settings UI) │
|
||||
│ │
|
||||
│ Backend (Node.js/TypeScript) │
|
||||
│ ├── TextToSpeechService.ts (TTS orchestration) │
|
||||
│ ├── ElevenLabsProvider.ts (ElevenLabs API) │
|
||||
│ ├── Task.say() modifications (TTS trigger) │
|
||||
│ └── System Prompt additions (discuss behavior) │
|
||||
│ │
|
||||
│ Infrastructure │
|
||||
│ ├── proto/tts.proto (gRPC definitions) │
|
||||
│ ├── Controller handlers (gRPC endpoints) │
|
||||
│ └── State management (discuss mode state) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User Speech → VoiceRecorder → Transcription → Cline (Plan Mode)
|
||||
↓
|
||||
[Processes with
|
||||
"Discuss Mode" prompt]
|
||||
↓
|
||||
Response Text → TextToSpeechService
|
||||
↓
|
||||
Audio Buffer → AudioPlayer → Speaker
|
||||
↓
|
||||
[On playback complete]
|
||||
↓
|
||||
Auto-start VoiceRecorder (if enabled)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Implementation Checklist
|
||||
|
||||
### Phase 1: System Prompt & Behavior (Est: 2-3 hours)
|
||||
- [ ] 1.1 Read existing Plan Mode system prompt
|
||||
- [ ] 1.2 Design "Discuss Mode" prompt additions
|
||||
- [ ] 1.3 Add conversational guidelines
|
||||
- [ ] 1.4 Add proactive questioning instructions
|
||||
- [ ] 1.5 Add plan completion signal instructions
|
||||
- [ ] 1.6 Test prompt changes with existing Plan Mode
|
||||
|
||||
**Files to Modify:**
|
||||
- `src/core/prompts/system-prompt.ts`
|
||||
- `src/core/prompts/system-prompt-legacy/` (if needed)
|
||||
|
||||
### Phase 2: TTS Service Backend (Est: 4-6 hours)
|
||||
- [ ] 2.1 Create `src/services/tts/` directory structure
|
||||
- [ ] 2.2 Implement `BaseTTSProvider` interface
|
||||
- [ ] 2.3 Implement `ElevenLabsProvider.ts`
|
||||
- [ ] API client setup
|
||||
- [ ] Voice list endpoint
|
||||
- [ ] Text-to-speech synthesis
|
||||
- [ ] Error handling
|
||||
- [ ] 2.4 Implement `TextToSpeechService.ts`
|
||||
- [ ] Provider factory pattern
|
||||
- [ ] Audio buffer management
|
||||
- [ ] Streaming support (optional)
|
||||
- [ ] 2.5 Add TTS configuration to `src/shared/api.ts`
|
||||
- [ ] 2.6 Add ElevenLabs API key to secrets storage
|
||||
- [ ] 2.7 Unit tests for TTS service
|
||||
|
||||
**New Files:**
|
||||
```
|
||||
src/services/tts/
|
||||
├── TextToSpeechService.ts
|
||||
├── providers/
|
||||
│ ├── BaseTTSProvider.ts
|
||||
│ ├── ElevenLabsProvider.ts
|
||||
│ └── OpenAITTSProvider.ts (future)
|
||||
└── __tests__/
|
||||
└── TextToSpeechService.test.ts
|
||||
```
|
||||
|
||||
### Phase 3: Protobuf & gRPC (Est: 2-3 hours)
|
||||
- [ ] 3.1 Create `proto/tts.proto` definition
|
||||
- [ ] 3.2 Define `TtsService` with methods:
|
||||
- [ ] `synthesizeSpeech()`
|
||||
- [ ] `getAvailableVoices()`
|
||||
- [ ] `updateVoiceSettings()`
|
||||
- [ ] 3.3 Run proto compilation: `npm run protos`
|
||||
- [ ] 3.4 Implement gRPC handlers in `src/core/controller/tts/`
|
||||
- [ ] `synthesizeSpeech.ts`
|
||||
- [ ] `getAvailableVoices.ts`
|
||||
- [ ] 3.5 Generate client in `webview-ui/src/services/grpc-client.ts`
|
||||
|
||||
**New Files:**
|
||||
```
|
||||
proto/tts.proto
|
||||
src/core/controller/tts/
|
||||
├── synthesizeSpeech.ts
|
||||
└── getAvailableVoices.ts
|
||||
```
|
||||
|
||||
### Phase 4: Audio Player Component (Est: 3-4 hours)
|
||||
- [ ] 4.1 Create `AudioPlayer.tsx` component
|
||||
- [ ] Audio element management
|
||||
- [ ] Playback controls
|
||||
- [ ] Loading states
|
||||
- [ ] Error handling
|
||||
- [ ] 4.2 Create audio queue system
|
||||
- [ ] 4.3 Add speaking animation/indicator
|
||||
- [ ] 4.4 Integrate into chat message component
|
||||
- [ ] 4.5 Add auto-play functionality
|
||||
- [ ] 4.6 Add callback for playback completion
|
||||
|
||||
**New Files:**
|
||||
```
|
||||
webview-ui/src/components/chat/
|
||||
├── AudioPlayer.tsx
|
||||
├── VoiceConversationControls.tsx
|
||||
└── SpeakingIndicator.tsx
|
||||
```
|
||||
|
||||
### Phase 5: Discuss Mode Integration (Est: 4-5 hours)
|
||||
- [ ] 5.1 Add `discussModeEnabled` to global state
|
||||
- [ ] 5.2 Add `voiceModeSettings` to state
|
||||
- [ ] 5.3 Modify `Task.say()` in `src/core/task/index.ts`:
|
||||
- [ ] Check if discuss mode is enabled
|
||||
- [ ] Check if current mode is "plan"
|
||||
- [ ] Trigger TTS for assistant text
|
||||
- [ ] Queue audio for playback
|
||||
- [ ] 5.4 Implement auto-continue logic:
|
||||
- [ ] Detect audio playback completion
|
||||
- [ ] Auto-start voice recorder
|
||||
- [ ] Handle errors gracefully
|
||||
- [ ] 5.5 Add mode switching guard (auto-disable on Act Mode)
|
||||
|
||||
**Files to Modify:**
|
||||
```
|
||||
src/core/task/index.ts
|
||||
src/core/controller/index.ts
|
||||
src/core/storage/StateManager.ts
|
||||
```
|
||||
|
||||
### Phase 6: UI Components (Est: 3-4 hours)
|
||||
- [ ] 6.1 Create `DiscussModeToggle.tsx`
|
||||
- [ ] Toggle button with icon
|
||||
- [ ] Mode indicator badge
|
||||
- [ ] Tooltip with description
|
||||
- [ ] Disable in Act Mode
|
||||
- [ ] 6.2 Add discuss mode controls to chat header
|
||||
- [ ] 6.3 Create plan completion UI:
|
||||
- [ ] "Plan Ready" indicator
|
||||
- [ ] "Switch to Act Mode" button
|
||||
- [ ] "Continue Discussing" option
|
||||
- [ ] 6.4 Add voice conversation status indicators:
|
||||
- [ ] "🎤 Listening..." when recording
|
||||
- [ ] "🗣️ Speaking..." when playing audio
|
||||
- [ ] "💭 Thinking..." when processing
|
||||
|
||||
**New Files:**
|
||||
```
|
||||
webview-ui/src/components/discuss-mode/
|
||||
├── DiscussModeToggle.tsx
|
||||
├── PlanCompletionCard.tsx
|
||||
└── ConversationStatusIndicator.tsx
|
||||
```
|
||||
|
||||
### Phase 7: Settings Panel (Est: 2-3 hours)
|
||||
- [ ] 7.1 Extend `VoiceSettingsSection.tsx`
|
||||
- [ ] 7.2 Add TTS provider selection (ElevenLabs)
|
||||
- [ ] 7.3 Add ElevenLabs API key input
|
||||
- [ ] 7.4 Add voice selection dropdown:
|
||||
- [ ] Fetch voices from ElevenLabs
|
||||
- [ ] Voice preview button
|
||||
- [ ] Voice descriptions
|
||||
- [ ] 7.5 Add speech rate slider
|
||||
- [ ] 7.6 Add auto-speak toggle
|
||||
- [ ] 7.7 Add auto-listen toggle
|
||||
- [ ] 7.8 Save settings to state
|
||||
|
||||
**Files to Modify:**
|
||||
```
|
||||
webview-ui/src/components/settings/sections/VoiceSettingsSection.tsx
|
||||
```
|
||||
|
||||
### Phase 8: Testing & Polish (Est: 3-4 hours)
|
||||
- [ ] 8.1 End-to-end testing:
|
||||
- [ ] Voice input → TTS output flow
|
||||
- [ ] Auto-continue conversation
|
||||
- [ ] Plan completion detection
|
||||
- [ ] Mode switching behavior
|
||||
- [ ] 8.2 Error handling:
|
||||
- [ ] API key missing
|
||||
- [ ] Network failures
|
||||
- [ ] Audio playback errors
|
||||
- [ ] Rate limiting (ElevenLabs)
|
||||
- [ ] 8.3 Edge cases:
|
||||
- [ ] Empty responses
|
||||
- [ ] Very long responses (chunking)
|
||||
- [ ] Interrupted speech
|
||||
- [ ] Rapid mode switching
|
||||
- [ ] 8.4 Performance optimization:
|
||||
- [ ] Audio caching
|
||||
- [ ] Queue management
|
||||
- [ ] Memory cleanup
|
||||
- [ ] 8.5 UX polish:
|
||||
- [ ] Smooth animations
|
||||
- [ ] Clear status indicators
|
||||
- [ ] Helpful error messages
|
||||
- [ ] Onboarding tooltip
|
||||
|
||||
---
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
```
|
||||
cline/
|
||||
├── proto/
|
||||
│ └── tts.proto [NEW]
|
||||
│
|
||||
├── src/
|
||||
│ ├── core/
|
||||
│ │ ├── controller/
|
||||
│ │ │ └── tts/ [NEW]
|
||||
│ │ │ ├── synthesizeSpeech.ts
|
||||
│ │ │ └── getAvailableVoices.ts
|
||||
│ │ ├── prompts/
|
||||
│ │ │ └── system-prompt.ts [MODIFY]
|
||||
│ │ └── task/
|
||||
│ │ └── index.ts [MODIFY]
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ └── tts/ [NEW]
|
||||
│ │ ├── TextToSpeechService.ts
|
||||
│ │ └── providers/
|
||||
│ │ ├── BaseTTSProvider.ts
|
||||
│ │ └── ElevenLabsProvider.ts
|
||||
│ │
|
||||
│ └── shared/
|
||||
│ └── api.ts [MODIFY]
|
||||
│
|
||||
└── webview-ui/
|
||||
└── src/
|
||||
├── components/
|
||||
│ ├── chat/
|
||||
│ │ ├── AudioPlayer.tsx [NEW]
|
||||
│ │ └── VoiceConversationControls.tsx [NEW]
|
||||
│ ├── discuss-mode/ [NEW]
|
||||
│ │ ├── DiscussModeToggle.tsx
|
||||
│ │ └── PlanCompletionCard.tsx
|
||||
│ └── settings/
|
||||
│ └── VoiceSettingsSection.tsx [MODIFY]
|
||||
│
|
||||
└── services/
|
||||
└── grpc-client.ts [MODIFY]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Details
|
||||
|
||||
### ElevenLabs Integration
|
||||
|
||||
**API Endpoints:**
|
||||
- Text-to-Speech: `POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id}`
|
||||
- Get Voices: `GET https://api.elevenlabs.io/v1/voices`
|
||||
|
||||
**Authentication:**
|
||||
```typescript
|
||||
headers: {
|
||||
'xi-api-key': API_KEY,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
```
|
||||
|
||||
**Request Format:**
|
||||
```typescript
|
||||
{
|
||||
text: string,
|
||||
model_id: "eleven_multilingual_v2",
|
||||
voice_settings: {
|
||||
stability: 0.5,
|
||||
similarity_boost: 0.75
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Audio file (MP3 format)
|
||||
|
||||
### State Management
|
||||
|
||||
**New Global State Keys:**
|
||||
```typescript
|
||||
interface GlobalState {
|
||||
discussModeEnabled: boolean
|
||||
voiceModeSettings: {
|
||||
ttsProvider: "elevenlabs" | "openai"
|
||||
elevenLabsApiKey?: string
|
||||
selectedVoice: string
|
||||
speechRate: number
|
||||
autoSpeak: boolean
|
||||
autoListen: boolean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Protobuf Schema
|
||||
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
|
||||
package cline.tts;
|
||||
|
||||
service TtsService {
|
||||
rpc synthesizeSpeech(SynthesizeRequest) returns (SynthesizeResponse);
|
||||
rpc getAvailableVoices(EmptyRequest) returns (VoicesResponse);
|
||||
}
|
||||
|
||||
message SynthesizeRequest {
|
||||
string text = 1;
|
||||
string voice_id = 2;
|
||||
optional float speech_rate = 3;
|
||||
}
|
||||
|
||||
message SynthesizeResponse {
|
||||
bytes audio_data = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message Voice {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string description = 3;
|
||||
}
|
||||
|
||||
message VoicesResponse {
|
||||
repeated Voice voices = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- [ ] TTS Service provider selection
|
||||
- [ ] Audio buffer handling
|
||||
- [ ] Error handling for API failures
|
||||
- [ ] State management for discuss mode
|
||||
|
||||
### Integration Tests
|
||||
- [ ] gRPC endpoint communication
|
||||
- [ ] Full voice input → TTS output flow
|
||||
- [ ] Mode switching behavior
|
||||
- [ ] Settings persistence
|
||||
|
||||
### Manual Testing Scenarios
|
||||
1. **Happy Path:**
|
||||
- Enable Discuss Mode
|
||||
- Ask initial question via voice
|
||||
- Cline asks clarifying questions
|
||||
- Iterative discussion
|
||||
- Plan completion and approval
|
||||
- Switch to Act Mode
|
||||
|
||||
2. **Error Cases:**
|
||||
- Missing API key
|
||||
- Network failure during TTS
|
||||
- Invalid voice selection
|
||||
- Audio playback failure
|
||||
|
||||
3. **Edge Cases:**
|
||||
- Mode switching during playback
|
||||
- Rapid successive voice inputs
|
||||
- Very long responses (>1000 chars)
|
||||
- Empty or nonsense responses
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Known Issues & Future Improvements
|
||||
|
||||
### Known Limitations
|
||||
- ElevenLabs rate limits (adjust queue as needed)
|
||||
- No offline mode (requires internet for TTS)
|
||||
- Audio latency depends on network speed
|
||||
|
||||
### Future Enhancements
|
||||
- [ ] Multiple TTS provider support (OpenAI, Azure)
|
||||
- [ ] Voice cloning integration
|
||||
- [ ] Conversation history playback
|
||||
- [ ] Export voice conversations
|
||||
- [ ] Voice command shortcuts
|
||||
- [ ] Ambient mode (minimal UI, voice-first)
|
||||
- [ ] Multi-language support
|
||||
- [ ] Emotion/tone control for TTS
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progress Tracking
|
||||
|
||||
### Time Estimates vs Actual
|
||||
| Phase | Estimated | Actual | Status |
|
||||
|-------|-----------|--------|--------|
|
||||
| Phase 1: System Prompt | 2-3h | ~1h | ✅ Complete |
|
||||
| Phase 2: TTS Backend | 4-6h | ~2h | ✅ Complete |
|
||||
| Phase 3: Protobuf/gRPC | 2-3h | ~1h | ✅ Complete |
|
||||
| Phase 4: Audio Player | 3-4h | - | ⬜ Not Started |
|
||||
| Phase 5: Integration | 4-5h | - | ⬜ Not Started |
|
||||
| Phase 6: UI Components | 3-4h | - | ⬜ Not Started |
|
||||
| Phase 7: Settings | 2-3h | - | ⬜ Not Started |
|
||||
| Phase 8: Testing | 3-4h | - | ⬜ Not Started |
|
||||
| **Total** | **23-32h** | **~4h** | 🟡 35% Complete |
|
||||
|
||||
### Sprint Log
|
||||
_Add daily progress notes here as development proceeds_
|
||||
|
||||
**November 8, 2025 - Session 1:**
|
||||
- ✅ Completed: Initial planning and architecture design
|
||||
- ✅ Completed: Created comprehensive implementation plan document
|
||||
- ✅ Completed: **Phase 1 - System Prompt & Behavior**
|
||||
- Modified `src/core/prompts/system-prompt/components/act_vs_plan_mode.ts`
|
||||
- Added Discuss Mode conversational guidelines
|
||||
- Implemented proactive questioning behavior
|
||||
- Added plan completion signal instructions
|
||||
- ✅ Completed: **Phase 2 - TTS Service Backend**
|
||||
- Created complete TTS service architecture in `src/services/tts/`
|
||||
- Implemented `BaseTTSProvider.ts` abstract base class
|
||||
- Implemented `ElevenLabsProvider.ts` with full API integration
|
||||
- Implemented `TextToSpeechService.ts` orchestration layer
|
||||
- Added voice synthesis, voice selection, and validation
|
||||
- ✅ Completed: **Phase 3 - Protobuf & gRPC**
|
||||
- Created `proto/tts.proto` with complete service definitions
|
||||
- Successfully compiled protobuf definitions
|
||||
- Implemented gRPC handlers in `src/core/controller/tts/`:
|
||||
- `synthesizeSpeech.ts` for text-to-speech synthesis
|
||||
- `getAvailableVoices.ts` for voice listing
|
||||
- Integrated TTS service into Controller class with getter method
|
||||
- 📝 Status: Backend infrastructure complete (35% overall progress)
|
||||
- 🎯 Next: Begin Phase 4 (Audio Player Component implementation)
|
||||
|
||||
---
|
||||
|
||||
## 📚 References
|
||||
|
||||
- [ElevenLabs API Docs](https://elevenlabs.io/docs)
|
||||
- [Cline Architecture Guide](.clinerules/cline-overview.md)
|
||||
- [Protobuf Development Guide](.clinerules/protobuf-development.md)
|
||||
- [Existing Voice Input Implementation](src/services/dictation/)
|
||||
|
||||
---
|
||||
|
||||
## 👥 Team Notes
|
||||
|
||||
_Use this section for brainstorming, decisions, and Q&A during development_
|
||||
|
||||
### Design Decisions
|
||||
- **Why ElevenLabs?** High-quality, natural-sounding voices with proven API reliability
|
||||
- **Why Plan Mode only?** Clean separation of concerns - discussion vs execution. This creates a natural workflow where users discuss in Plan Mode then execute silently in Act Mode
|
||||
- **Why auto-continue?** Enables natural conversation flow without manual button clicking between exchanges
|
||||
|
||||
### Open Questions
|
||||
- [ ] Should we support offline TTS? (browser TTS API as fallback?)
|
||||
- [ ] Max response length before chunking audio?
|
||||
- [ ] Voice selection defaults for different languages?
|
||||
- [ ] Should we cache frequently used phrases/responses?
|
||||
|
||||
### Example Discuss Mode Session
|
||||
|
||||
```
|
||||
User clicks "Discuss Mode" button in Plan Mode
|
||||
|
||||
👤 User: [Speaks] "I need to add authentication to my app"
|
||||
|
||||
🤖 Cline: [Speaks] "I'd be happy to help with that! Let me ask a few questions
|
||||
so I can create the right solution for you. First, what kind of
|
||||
authentication are you thinking - email/password, OAuth with
|
||||
providers like Google or GitHub, or something else?"
|
||||
|
||||
👤 User: [Speaks] "Email and password for now, but I might add OAuth later"
|
||||
|
||||
🤖 Cline: [Speaks] "Perfect. And do you already have a backend API, or should
|
||||
I create one? Also, what framework is your frontend using?"
|
||||
|
||||
👤 User: [Speaks] "I have a Node.js backend with Express, and React on the frontend"
|
||||
|
||||
🤖 Cline: [Speaks] "Great! A couple more things - do you need features like
|
||||
password reset, email verification, or 'remember me' functionality?"
|
||||
|
||||
👤 User: [Speaks] "Yes, all of those would be helpful"
|
||||
|
||||
🤖 Cline: [Speaks] "Perfect! I have a clear plan now. Here's what I'll do:
|
||||
|
||||
I'll create:
|
||||
1. Backend authentication routes with JWT tokens
|
||||
2. Password hashing with bcrypt
|
||||
3. Email service for verification and password reset
|
||||
4. React components for login, signup, and password reset pages
|
||||
5. Protected route wrapper for authenticated pages
|
||||
|
||||
This will integrate with your existing Express backend and React
|
||||
frontend. The authentication will be secure and production-ready.
|
||||
|
||||
Does this sound like what you need? If so, I'm ready to switch to
|
||||
Act Mode and start building!"
|
||||
|
||||
👤 User: [Speaks] "Yes, that sounds perfect!"
|
||||
|
||||
[UI shows: ✅ Plan Complete - Switch to Act Mode?]
|
||||
[User clicks button → Switches to Act Mode]
|
||||
[Discuss Mode auto-disables]
|
||||
[Cline begins implementation silently]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*This document will be updated throughout implementation. Last updated: November 8, 2025*
|
||||
@@ -2,7 +2,7 @@
|
||||
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
|
||||
</sub></div>
|
||||
|
||||
# Cline – \#1 on OpenRouter
|
||||
# Cline
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
|
||||
@@ -182,7 +182,7 @@ see the manual page: man cline`,
|
||||
rootCmd.AddCommand(cli.NewVersionCommand())
|
||||
rootCmd.AddCommand(cli.NewAuthCommand())
|
||||
rootCmd.AddCommand(cli.NewLogsCommand())
|
||||
rootCmd.AddCommand(cli.NewDoctorCommand())
|
||||
// rootCmd.AddCommand(cli.NewDoctorCommand()) // Disabled for now
|
||||
|
||||
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
|
||||
os.Exit(1)
|
||||
@@ -345,4 +345,4 @@ func getContentFromStdinAndArgs(args []string) (string, error) {
|
||||
}
|
||||
|
||||
return content.String(), nil
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.0-nightly.18",
|
||||
"version": "1.0.3",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
@@ -20,7 +20,7 @@
|
||||
"vscode-uri"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
@@ -28,7 +29,7 @@ func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL
|
||||
}
|
||||
|
||||
// Create task manager for state operations
|
||||
manager, err := task.NewManagerForDefault(ctx)
|
||||
manager, err := createTaskManager(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create task manager: %w", err)
|
||||
}
|
||||
@@ -75,6 +76,11 @@ func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL
|
||||
}
|
||||
}
|
||||
|
||||
// WORKAROUND: Wait for debounced state persistence to complete
|
||||
// Fixes `cline auth` issue when ran in docker environments
|
||||
// TODO: implement better solution w/ changes in StateManager
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
|
||||
// Success message
|
||||
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
|
||||
fmt.Printf(" Model: %s\n", finalModelID)
|
||||
|
||||
@@ -112,6 +112,7 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
cline.ApiProvider_OLLAMA,
|
||||
cline.ApiProvider_CEREBRAS,
|
||||
cline.ApiProvider_OCA,
|
||||
cline.ApiProvider_HICAP,
|
||||
}
|
||||
|
||||
// Check each provider to see if it's ready to use
|
||||
@@ -212,13 +213,15 @@ func extractProviderFromState(stateData map[string]interface{}, mode string) *Pr
|
||||
// mapProviderStringToEnum converts provider string from state to ApiProvider enum
|
||||
// Returns (provider, ok) where ok is false if the provider is unknown
|
||||
func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
|
||||
normalizedStr := strings.ToLower(providerStr)
|
||||
|
||||
// Map string values to enum values
|
||||
switch providerStr {
|
||||
switch normalizedStr {
|
||||
case "anthropic":
|
||||
return cline.ApiProvider_ANTHROPIC, true
|
||||
case "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
|
||||
case "openai", "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
|
||||
return cline.ApiProvider_OPENAI, true
|
||||
case "openai", "openai-native": // This is the native, official Open AI provider
|
||||
case "openai-native": // This is the native, official Open AI provider
|
||||
return cline.ApiProvider_OPENAI_NATIVE, true
|
||||
case "openrouter":
|
||||
return cline.ApiProvider_OPENROUTER, true
|
||||
@@ -236,6 +239,8 @@ func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
|
||||
return cline.ApiProvider_CLINE, true
|
||||
case "oca":
|
||||
return cline.ApiProvider_OCA, true
|
||||
case "hicap":
|
||||
return cline.ApiProvider_HICAP, true
|
||||
default:
|
||||
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
|
||||
}
|
||||
@@ -267,6 +272,8 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
|
||||
return "cline"
|
||||
case cline.ApiProvider_OCA:
|
||||
return "oca"
|
||||
case cline.ApiProvider_HICAP:
|
||||
return "hicap"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -344,6 +351,8 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
|
||||
return "Cline (Official)"
|
||||
case cline.ApiProvider_OCA:
|
||||
return "Oracle Code Assist"
|
||||
case cline.ApiProvider_HICAP:
|
||||
return "Hicap"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -465,6 +474,7 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
{cline.ApiProvider_GEMINI, "geminiApiKey"},
|
||||
{cline.ApiProvider_OLLAMA, "ollamaBaseUrl"}, // Ollama uses baseUrl instead of API key
|
||||
{cline.ApiProvider_CEREBRAS, "cerebrasApiKey"},
|
||||
{cline.ApiProvider_HICAP, "hicapApiKey"},
|
||||
}
|
||||
|
||||
for _, providerCheck := range providersToCheck {
|
||||
|
||||
@@ -154,6 +154,14 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
|
||||
PlanModeProviderSpecificModelIDField: "planModeOcaModelId",
|
||||
ActModeProviderSpecificModelIDField: "actModeOcaModelId",
|
||||
}, nil
|
||||
case cline.ApiProvider_HICAP:
|
||||
return ProviderFields{
|
||||
APIKeyField: "hicapApiKey",
|
||||
PlanModeModelInfoField: "planModeHicapModelInfo",
|
||||
ActModeModelInfoField: "actModeHicapModelInfo",
|
||||
PlanModeProviderSpecificModelIDField: "planModeHicapModelId",
|
||||
ActModeProviderSpecificModelIDField: "actModeHicapModelId",
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
|
||||
@@ -268,6 +276,8 @@ func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, v
|
||||
apiConfig.ClineApiKey = value
|
||||
case "ocaApiKey":
|
||||
apiConfig.OcaApiKey = value
|
||||
case "hicapApiKey":
|
||||
apiConfig.HicapApiKey = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +299,9 @@ func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldNa
|
||||
case "planModeOcaModelId":
|
||||
apiConfig.PlanModeOcaModelId = value
|
||||
apiConfig.ActModeOcaModelId = value
|
||||
case "planModeHicapModelId":
|
||||
apiConfig.PlanModeHicapModelId = value
|
||||
apiConfig.ActModeHicapModelId = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,14 @@ func (tr *ToolRenderer) generateToolHeader(tool *types.ToolMessage, verbTense st
|
||||
}
|
||||
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
|
||||
|
||||
case string(types.ToolTypeFileDeleted):
|
||||
if verbTense == "wants to" {
|
||||
action = "wants to delete"
|
||||
} else {
|
||||
action = "is deleting"
|
||||
}
|
||||
return fmt.Sprintf("### Cline %s `%s`", action, tool.Path)
|
||||
|
||||
case string(types.ToolTypeListFilesTopLevel):
|
||||
if verbTense == "wants to" {
|
||||
action = "wants to list files in"
|
||||
@@ -199,7 +207,7 @@ func (tr *ToolRenderer) GenerateToolContentPreview(tool *types.ToolMessage) stri
|
||||
previewMd := fmt.Sprintf("```\n%s\n```", preview)
|
||||
return tr.renderMarkdown(previewMd)
|
||||
|
||||
case string(types.ToolTypeReadFile), string(types.ToolTypeWebFetch):
|
||||
case string(types.ToolTypeReadFile), string(types.ToolTypeWebFetch), string(types.ToolTypeFileDeleted):
|
||||
// No preview for read/fetch operations
|
||||
return ""
|
||||
|
||||
@@ -226,7 +234,8 @@ func (tr *ToolRenderer) GenerateToolContentBody(tool *types.ToolMessage) string
|
||||
toolParser := NewToolResultParser(tr.mdRenderer)
|
||||
|
||||
switch tool.Tool {
|
||||
case string(types.ToolTypeReadFile):
|
||||
case string(types.ToolTypeReadFile),
|
||||
string(types.ToolTypeFileDeleted):
|
||||
// readFile: show header only, no body
|
||||
return ""
|
||||
|
||||
|
||||
+3
-3
@@ -208,9 +208,9 @@ func listLogFiles(logsDir string) ([]logFileInfo, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by created time (newest first)
|
||||
// Sort by created time (oldest first)
|
||||
sort.Slice(logs, func(i, j int) bool {
|
||||
return logs[i].created.After(logs[j].created)
|
||||
return logs[i].created.Before(logs[j].created)
|
||||
})
|
||||
|
||||
return logs, nil
|
||||
@@ -379,4 +379,4 @@ func renderLogsTable(logs []logFileInfo, markForDeletion bool) error {
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,8 @@ 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)
|
||||
}
|
||||
|
||||
@@ -180,8 +180,6 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
|
||||
settings.PlanModeHuaweiCloudMaasModelId = strPtr(value)
|
||||
case "plan_mode_oca_model_id":
|
||||
settings.PlanModeOcaModelId = strPtr(value)
|
||||
case "plan_mode_vercel_ai_gateway_model_id":
|
||||
settings.PlanModeVercelAiGatewayModelId = strPtr(value)
|
||||
case "act_mode_api_model_id":
|
||||
settings.ActModeApiModelId = strPtr(value)
|
||||
case "act_mode_reasoning_effort":
|
||||
@@ -218,8 +216,6 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
|
||||
settings.ActModeHuaweiCloudMaasModelId = strPtr(value)
|
||||
case "act_mode_oca_model_id":
|
||||
settings.ActModeOcaModelId = strPtr(value)
|
||||
case "act_mode_vercel_ai_gateway_model_id":
|
||||
settings.ActModeVercelAiGatewayModelId = strPtr(value)
|
||||
|
||||
// Boolean fields
|
||||
case "aws_use_cross_region_inference":
|
||||
@@ -736,14 +732,14 @@ func setSecretField(secrets *cline.Secrets, key, value string) error {
|
||||
secrets.HuaweiCloudMaasApiKey = strPtr(value)
|
||||
case "baseten_api_key":
|
||||
secrets.BasetenApiKey = strPtr(value)
|
||||
case "vercel_ai_gateway_api_key":
|
||||
secrets.VercelAiGatewayApiKey = strPtr(value)
|
||||
case "dify_api_key":
|
||||
secrets.DifyApiKey = strPtr(value)
|
||||
case "oca_api_key":
|
||||
secrets.OcaApiKey = strPtr(value)
|
||||
case "oca_refresh_token":
|
||||
secrets.OcaRefreshToken = strPtr(value)
|
||||
case "hicap_api_key":
|
||||
secrets.HicapApiKey = strPtr(value)
|
||||
default:
|
||||
return fmt.Errorf("unsupported secret field '%s'", key)
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ const (
|
||||
ToolTypeEditedExistingFile ToolType = "editedExistingFile"
|
||||
ToolTypeNewFileCreated ToolType = "newFileCreated"
|
||||
ToolTypeReadFile ToolType = "readFile"
|
||||
ToolTypeFileDeleted ToolType = "fileDeleted"
|
||||
ToolTypeListFilesTopLevel ToolType = "listFilesTopLevel"
|
||||
ToolTypeListFilesRecursive ToolType = "listFilesRecursive"
|
||||
ToolTypeListCodeDefinitionNames ToolType = "listCodeDefinitionNames"
|
||||
|
||||
@@ -375,7 +375,7 @@ func showFailureMessage(channel string) {
|
||||
|
||||
func getCacheFilePath() string {
|
||||
configDir := filepath.Join(os.Getenv("HOME"), ".cline", "data")
|
||||
return filepath.Join(configDir, ".update-cache")
|
||||
return filepath.Join(configDir, "cli-update-cache")
|
||||
}
|
||||
|
||||
func loadCache() (cacheData, error) {
|
||||
@@ -406,4 +406,4 @@ func saveCache(cache cacheData) error {
|
||||
}
|
||||
|
||||
return os.WriteFile(cacheFile, data, 0644)
|
||||
}
|
||||
}
|
||||
+27
-1
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
@@ -124,6 +126,16 @@ func NormalizeAddressForGRPC(address string) (string, error) {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// GetNodeVersion returns the current Node.js version, or "unknown" if unable to detect
|
||||
func GetNodeVersion() string {
|
||||
cmd := exec.Command("node", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
// RetryOperation performs an operation with retry logic
|
||||
func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation func() error) error {
|
||||
var lastErr error
|
||||
@@ -155,5 +167,19 @@ func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation f
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("operation failed after %d attempts: %w", maxRetries, lastErr)
|
||||
return fmt.Errorf(`operation failed to after %d attempts: %w
|
||||
|
||||
This is usually caused by an incompatible Node.js version
|
||||
|
||||
REQUIREMENTS:
|
||||
• Node.js version 20+ is required
|
||||
• Current Node.js version: %s
|
||||
|
||||
DEBUGGING STEPS:
|
||||
1. View recent logs: cline log list
|
||||
2. Logs are available in: ~/.cline/logs/
|
||||
3. The most recent cline-core log file is usually valuable
|
||||
|
||||
For additional help, visit: https://github.com/cline/cline/issues
|
||||
`, maxRetries, lastErr, GetNodeVersion())
|
||||
}
|
||||
|
||||
@@ -443,7 +443,16 @@ var rawConfigFields = ` [
|
||||
"required": false,
|
||||
"fieldType": "string",
|
||||
"placeholder": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "hicapApiKey",
|
||||
"type": "string",
|
||||
"comment": "",
|
||||
"category": "general",
|
||||
"required": true,
|
||||
"fieldType": "password",
|
||||
"placeholder": "Enter your API key"
|
||||
},
|
||||
]`
|
||||
|
||||
// Raw model definitions data (parsed from TypeScript)
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
---
|
||||
title: "GitHub Actions Integration"
|
||||
description: "Automatically respond to GitHub issues by mentioning @cline in comments using Cline CLI in GitHub Actions."
|
||||
---
|
||||
|
||||
# GitHub Integration Sample
|
||||
|
||||
Automate GitHub issue analysis with AI. Mention `@cline` in any issue comment to trigger an autonomous investigation that reads files, analyzes code, and provides actionable insights - all running automatically in GitHub Actions.
|
||||
|
||||
|
||||
<Note>
|
||||
**New to Cline CLI?** This sample assumes you understand Cline CLI basics and have completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation). If you're new to Cline CLI, we recommend starting with the [GitHub RCA sample](../github-issue-rca) first, as it's simpler and will help you understand the fundamentals before setting up GitHub Actions.
|
||||
</Note>
|
||||
|
||||
## The Workflow
|
||||
|
||||
Trigger Cline by mentioning `@cline` in any issue comment:
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss0a-comment.png" alt="Issue comment with @cline mention" width="600" />
|
||||
</Frame>
|
||||
|
||||
Cline's automated analysis appears as a new comment, with insights drawn from your actual codebase:
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss0b-final.png" alt="Automated analysis response from Cline" width="600" />
|
||||
</Frame>
|
||||
|
||||
The entire investigation runs autonomously in GitHub Actions - from file exploration to posting results.
|
||||
|
||||
Let's configure your repository.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, you'll need:
|
||||
|
||||
- **Cline CLI knowledge** - Completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation) and understand basic usage
|
||||
- **GitHub repository** - With admin access to configure Actions and secrets
|
||||
- **GitHub Actions familiarity** - Basic understanding of workflows and CI/CD
|
||||
- **API provider account** - OpenRouter, Anthropic, or similar with API key
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Copy the Workflow File
|
||||
|
||||
|
||||
|
||||
Copy the workflow file from this sample to your repository. The workflow file must be placed in the `.github/workflows/` directory in your repository root for GitHub Actions to detect and run it. In this case, we'll name it `cline-responder.yml`.
|
||||
|
||||
```bash
|
||||
# In your repository root
|
||||
mkdir -p .github/workflows
|
||||
curl -o .github/workflows/cline-responder.yml https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-integration/cline-responder.yml
|
||||
```
|
||||
|
||||
Alternatively, you can copy the full workflow file directly into `.github/workflows/cline-responder.yml`:
|
||||
|
||||
<Accordion title="Click to view the complete cline-responder.yml workflow">
|
||||
```yaml
|
||||
name: Cline Issue Assistant
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
respond:
|
||||
runs-on: ubuntu-latest
|
||||
environment: cline-actions
|
||||
steps:
|
||||
- name: Check for @cline mention
|
||||
id: detect
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const body = context.payload.comment?.body || "";
|
||||
const isPR = !!context.payload.issue?.pull_request;
|
||||
const hit = body.toLowerCase().includes("@cline");
|
||||
core.setOutput("hit", (!isPR && hit) ? "true" : "false");
|
||||
core.setOutput("issue_number", String(context.payload.issue?.number || ""));
|
||||
core.setOutput("issue_url", context.payload.issue?.html_url || "");
|
||||
core.setOutput("comment_body", body);
|
||||
|
||||
- name: Checkout repository
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Node v20 is needed for Cline CLI on GitHub Actions Linux
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup Cline CLI
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
run: |
|
||||
# Install the Cline CLI
|
||||
sudo npm install -g cline
|
||||
|
||||
- name: Create Cline Instance
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
env:
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
CLINE_DIR: ${{ runner.temp }}/cline
|
||||
run: |
|
||||
# Create instance and capture output
|
||||
INSTANCE_OUTPUT=$(cline instance new 2>&1)
|
||||
|
||||
# Parse address from output (format: " Address: 127.0.0.1:36733")
|
||||
CLINE_ADDRESS=$(echo "$INSTANCE_OUTPUT" | grep "Address:" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]+')
|
||||
echo "CLINE_ADDRESS=$CLINE_ADDRESS" >> $GITHUB_ENV
|
||||
|
||||
# Configure API key
|
||||
cline config set open-router-api-key=$OPENROUTER_API_KEY --address $CLINE_ADDRESS -v
|
||||
|
||||
- name: Download analyze script
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
run: |
|
||||
export GITORG="YOUR-GITHUB-ORG"
|
||||
export GITREPO="YOUR-GITHUB-REPO"
|
||||
|
||||
curl -L https://raw.githubusercontent.com/${GITORG}/${GITREPO}/refs/heads/main/git-scripts/analyze-issue.sh -o analyze-issue.sh
|
||||
chmod +x analyze-issue.sh
|
||||
|
||||
- name: Run analysis
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
id: analyze
|
||||
env:
|
||||
ISSUE_URL: ${{ steps.detect.outputs.issue_url }}
|
||||
COMMENT: ${{ steps.detect.outputs.comment_body }}
|
||||
CLINE_ADDRESS: ${{ env.CLINE_ADDRESS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
RESULT=$(./analyze-issue.sh "${ISSUE_URL}" "Analyze this issue. The user asked: ${COMMENT}" "$CLINE_ADDRESS")
|
||||
|
||||
{
|
||||
echo 'result<<EOF'
|
||||
printf "%s\n" "$RESULT"
|
||||
echo 'EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Post response
|
||||
if: steps.detect.outputs.hit == 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ steps.detect.outputs.issue_number }}
|
||||
RESULT: ${{ steps.analyze.outputs.result }}
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: Number(process.env.ISSUE_NUMBER),
|
||||
body: process.env.RESULT || "(no output)"
|
||||
});
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Warning>
|
||||
**You MUST edit the workflow file before committing!**
|
||||
|
||||
Open `.github/workflows/cline-responder.yml` and update the "Download analyze script" step within the workflow to specify your GitHub organization and repository where the analysis script is stored:
|
||||
|
||||
```yaml
|
||||
export GITORG="YOUR-GITHUB-ORG" # Change this!
|
||||
export GITREPO="YOUR-GITHUB-REPO" # Change this!
|
||||
```
|
||||
|
||||
**Example:** If your repository is `github.com/acme/myproject`, set:
|
||||
```yaml
|
||||
export GITORG="acme"
|
||||
export GITREPO="myproject"
|
||||
```
|
||||
|
||||
This tells the workflow where to download the analysis script from your repository after you commit it in step 3.
|
||||
</Warning>
|
||||
|
||||
The workflow will look for new or updated issues, check for `@cline` mentions, and then
|
||||
start up an instance of the Cline CLI to dig into the issue, providing feedback
|
||||
as a reply to the issue.
|
||||
|
||||
### 2. Configure API Keys
|
||||
|
||||
Add your AI provider API keys as repository secrets:
|
||||
|
||||
1. Go to your GitHub repository
|
||||
2. Navigate to **Settings** → **Environment** and Add a new environment.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss01-environment.png" alt="Navigate to Actions secrets" width="600" />
|
||||
</Frame>
|
||||
|
||||
Make sure to name it "cline-actions" so that it matches the `environment`
|
||||
value at the top of the `cline-responder.yml` file.
|
||||
|
||||
3. Click **New repository secret**
|
||||
4. Add a secret for the `OPENROUTER_API_KEY` with a value of an API key from
|
||||
[openrouter.com](https://openrouter.com).
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss02-api-key.png" alt="Add API key secret" width="600" />
|
||||
</Frame>
|
||||
|
||||
5. Verify your secret is configured:
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/ss03-ready.png" alt="API key configured" width="600" />
|
||||
</Frame>
|
||||
|
||||
Now you're ready to supply Cline with the credentials it needs in a GitHub Action.
|
||||
|
||||
### 3. Add Analysis Script
|
||||
|
||||
Add the analysis script from the `github-issue-rca` sample to your repository. **First, you'll need to create a `git-scripts` directory in your repository root where the script will be located.** Choose one of these options:
|
||||
|
||||
**Option A: Download directly (Recommended)**
|
||||
|
||||
```bash
|
||||
# In your repository root, create the directory and download the script
|
||||
mkdir -p git-scripts
|
||||
curl -o git-scripts/analyze-issue.sh https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-issue-rca/analyze-issue.sh
|
||||
chmod +x git-scripts/analyze-issue.sh
|
||||
```
|
||||
|
||||
**Option B: Manual copy-paste**
|
||||
|
||||
Create the directory and file manually, then paste the script content:
|
||||
|
||||
```bash
|
||||
# In your repository root
|
||||
mkdir -p git-scripts
|
||||
# Create and edit the file with your preferred editor
|
||||
nano git-scripts/analyze-issue.sh # or use vim, code, etc.
|
||||
```
|
||||
|
||||
<Accordion title="Click to view the complete analyze-issue.sh script">
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Analyze a GitHub issue using Cline CLI
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <github-issue-url> [prompt] [address]"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?'"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?' 127.0.0.1:46529"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Gather the args
|
||||
ISSUE_URL="$1"
|
||||
PROMPT="${2:-What is the root cause of this issue?}"
|
||||
if [ -n "$3" ]; then
|
||||
ADDRESS="--address $3"
|
||||
fi
|
||||
|
||||
# Ask Cline for its analysis, showing only the summary
|
||||
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
|
||||
sed -n '/^{/,$p' | \
|
||||
jq -r 'select(.say == "completion_result") | .text' | \
|
||||
sed 's/\\n/\n/g'
|
||||
```
|
||||
|
||||
After pasting the script content, make it executable:
|
||||
```bash
|
||||
chmod +x git-scripts/analyze-issue.sh
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
This analysis script calls Cline to execute a prompt on a GitHub issue,
|
||||
summarizing the output to populate the reply to the issue.
|
||||
|
||||
### 4. Commit and Push
|
||||
|
||||
```bash
|
||||
git add .github/workflows/cline-responder.yml
|
||||
git add git-scripts/analyze-issue.sh
|
||||
git commit -m "Add Cline issue assistant workflow"
|
||||
git push
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once set up, simply mention `@cline` in any issue comment:
|
||||
|
||||
```
|
||||
@cline what's causing this error?
|
||||
|
||||
@cline analyze the root cause
|
||||
|
||||
@cline what are the security implications?
|
||||
```
|
||||
|
||||
GitHub Actions will:
|
||||
1. Detect the `@cline` mention
|
||||
2. Start a Cline CLI instance
|
||||
3. Download the analysis script
|
||||
4. Analyze the issue using act mode with yolo (fully autonomous)
|
||||
5. Post Cline's analysis as a new comment
|
||||
|
||||
**Note**: The workflow only triggers on issue comments, not pull request
|
||||
comments.
|
||||
|
||||
## How It Works
|
||||
|
||||
The workflow (`cline-responder.yml`):
|
||||
|
||||
1. **Triggers** on issue comments (created or edited)
|
||||
2. **Detects** `@cline` mentions (case-insensitive)
|
||||
3. **Installs** Cline CLI globally using npm
|
||||
4. **Creates** a Cline instance using `cline instance new`
|
||||
5. **Configures** authentication using `cline config set open-router-api-key=...
|
||||
--address ...`
|
||||
6. **Downloads** the reusable `analyze-issue.sh` script from the
|
||||
`github-issue-rca` sample
|
||||
7. **Runs** analysis with the instance address
|
||||
8. **Posts** the analysis result as a comment
|
||||
|
||||
## Related Samples
|
||||
|
||||
- **[github-issue-rca](./github-issue-rca)**: The reusable script that powers this integration
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
title: "GitHub Issue RCA Sample"
|
||||
description: "Automated GitHub issue analysis using Cline CLI to identify root causes."
|
||||
---
|
||||
|
||||
# GitHub Root Cause Analysis
|
||||
|
||||
Automated GitHub issue analysis using Cline CLI. This script uses Cline's autonomous AI capabilities to fetch, analyze, and identify root causes of GitHub issues, outputting clean, parseable results that can be easily integrated into your development workflows.
|
||||
|
||||
<Note>
|
||||
**New to Cline CLI?** This sample assumes you have already completed the [Installation Guide](https://docs.cline.bot/cline-cli/installation) and authenticated with `cline auth`. If you haven't set up Cline CLI yet, please start there first.
|
||||
</Note>
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/cli-rca.gif" alt="CLI Root Cause Analysis Demo" width="600" />
|
||||
</Frame>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This sample assumes you have already:
|
||||
|
||||
- **Cline CLI** installed and authenticated ([Installation Guide](https://docs.cline.bot/cline-cli/installation))
|
||||
- **At least one AI model provider** configured (e.g., OpenRouter, Anthropic, OpenAI)
|
||||
- **Basic familiarity** with Cline CLI commands
|
||||
|
||||
Additionally, you'll need:
|
||||
|
||||
- **GitHub CLI** (`gh`) installed and authenticated
|
||||
- **jq** installed for JSON parsing
|
||||
- **bash** shell (or compatible shell)
|
||||
|
||||
### Installation Instructions
|
||||
|
||||
#### macOS
|
||||
|
||||
<Note>
|
||||
These instructions require [Homebrew](https://brew.sh/) to be installed. If you don't have Homebrew, install it first by running:
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
</Note>
|
||||
|
||||
```bash
|
||||
# Install GitHub CLI
|
||||
brew install gh
|
||||
|
||||
# Install jq
|
||||
brew install jq
|
||||
|
||||
# Authenticate with GitHub
|
||||
gh auth login
|
||||
```
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
# Install GitHub CLI (Debian/Ubuntu)
|
||||
sudo apt install gh
|
||||
|
||||
# Or for other Linux distributions, see: https://cli.github.com/manual/installation
|
||||
|
||||
# Install jq (Debian/Ubuntu)
|
||||
sudo apt install jq
|
||||
|
||||
# Authenticate with GitHub
|
||||
gh auth login
|
||||
```
|
||||
|
||||
## Getting the Script
|
||||
|
||||
**Option 1: Download directly with curl**
|
||||
```bash
|
||||
curl -O https://raw.githubusercontent.com/cline/cline/main/src/samples/cli/github-issue-rca/analyze-issue.sh
|
||||
```
|
||||
|
||||
**Option 2: Copy the full script**
|
||||
|
||||
<Accordion title="Click to view the complete analyze-issue.sh script">
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Analyze a GitHub issue using Cline CLI
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <github-issue-url> [prompt] [address]"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?'"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause of this issue?' 127.0.0.1:46529"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Gather the args
|
||||
ISSUE_URL="$1"
|
||||
PROMPT="${2:-What is the root cause of this issue?}"
|
||||
if [ -n "$3" ]; then
|
||||
ADDRESS="--address $3"
|
||||
fi
|
||||
|
||||
# Ask Cline for its analysis, showing only the summary
|
||||
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
|
||||
sed -n '/^{/,$p' | \
|
||||
jq -r 'select(.say == "completion_result") | .text' | \
|
||||
sed 's/\\n/\n/g'
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Note>
|
||||
**After downloading or creating the script**, make it executable by running:
|
||||
```bash
|
||||
chmod +x analyze-issue.sh
|
||||
```
|
||||
</Note>
|
||||
|
||||
## Quick Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Run this command in your terminal from the directory where you saved the script to analyze an issue with the default root cause prompt:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/owner/repo/issues/123
|
||||
```
|
||||
|
||||
This will:
|
||||
- Fetch issue #123 from the repository
|
||||
- Analyze the issue to identify root causes
|
||||
- Provide detailed analysis with recommendations
|
||||
|
||||
### Custom Analysis Prompt
|
||||
|
||||
Ask specific questions about the issue:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/owner/repo/issues/456 "What is the security impact?"
|
||||
```
|
||||
|
||||
### Using Specific Cline Instance
|
||||
|
||||
Target a particular Cline instance by address:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/owner/repo/issues/123 \
|
||||
"What is the root cause of this issue?" \
|
||||
127.0.0.1:46529
|
||||
```
|
||||
|
||||
<Warning>
|
||||
This is useful when:
|
||||
- Running multiple Cline instances
|
||||
- Using a remote Cline server
|
||||
- Testing with specific configurations
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
The script will automatically handle everything: fetching the issue, analyzing it with Cline, and displaying the results. The analysis typically takes 30-60 seconds depending on the issue complexity.
|
||||
</Note>
|
||||
|
||||
## How It Works
|
||||
|
||||
Let's analyze each component of the script to understand how it works.
|
||||
|
||||
### Argument Validation
|
||||
|
||||
The script validates input and provides usage instructions:
|
||||
|
||||
```bash
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <github-issue-url> [prompt] [address]"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'What is the root cause?'"
|
||||
echo "Example: $0 https://github.com/owner/repo/issues/123 'Analyze security impact' 127.0.0.1:46529"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Validates required GitHub issue URL
|
||||
- Shows clear usage examples
|
||||
- Supports optional custom prompt
|
||||
- Supports optional Cline instance address
|
||||
|
||||
### Argument Parsing
|
||||
|
||||
The script extracts and sets up the arguments:
|
||||
|
||||
```bash
|
||||
# Gather the args
|
||||
ISSUE_URL="$1"
|
||||
PROMPT="${2:-What is the root cause of this issue?}"
|
||||
if [ -n "$3" ]; then
|
||||
ADDRESS="--address $3"
|
||||
fi
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
- `ISSUE_URL="$1"` - First argument is always the issue URL
|
||||
- `PROMPT="${2:-...}"` - Second argument is optional, defaults to root cause analysis
|
||||
- `ADDRESS` - Third argument is optional, only set if provided
|
||||
|
||||
### The Core Analysis Pipeline
|
||||
|
||||
This is where the magic happens:
|
||||
|
||||
```bash
|
||||
# Ask Cline for his analysis, showing only the summary
|
||||
cline -y "$PROMPT: $ISSUE_URL" --mode act $ADDRESS -F json | \
|
||||
sed -n '/^{/,$p' | \
|
||||
jq -r 'select(.say == "completion_result") | .text' | \
|
||||
sed 's/\\n/\n/g'
|
||||
```
|
||||
|
||||
<Accordion title="Pipeline Breakdown: Understanding Each Component">
|
||||
|
||||
**1. `cline -y "$PROMPT: $ISSUE_URL"`**
|
||||
- `-y` enables yolo mode (no user interaction)
|
||||
- Constructs prompt with issue URL
|
||||
|
||||
**2. `--mode act`**
|
||||
- Enables act mode for active investigation
|
||||
- Allows Cline to use tools (read files, run commands, etc.)
|
||||
|
||||
**3. `$ADDRESS`**
|
||||
- Optional address flag for specific instance
|
||||
- Expands to `--address <ip:port>` if set
|
||||
|
||||
**4. `-F json`**
|
||||
- Outputs in JSON format for parsing
|
||||
|
||||
**5. `sed -n '/^{/,$p'`**
|
||||
- Extracts JSON from output
|
||||
- Skips any non-JSON prefix lines
|
||||
|
||||
**6. `jq -r 'select(.say == "completion_result") | .text'`**
|
||||
- Filters for completion result messages
|
||||
- Extracts the text field
|
||||
- `-r` outputs raw strings (no JSON quotes)
|
||||
|
||||
**7. `sed 's/\\n/\n/g'`**
|
||||
- Converts escaped newlines to actual newlines
|
||||
- Makes output readable
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Sample Output
|
||||
|
||||
Here's an example analyzing a real Flutter issue:
|
||||
|
||||
```bash
|
||||
$ ./analyze-issue.sh https://github.com/csells/flutter_counter/issues/2
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```markdown
|
||||
**Root Cause Analysis of Issue #2: "setState isn't cutting it"**
|
||||
|
||||
After examining the GitHub issue and analyzing the Flutter counter codebase,
|
||||
I've identified the root cause of why setState() is insufficient for this
|
||||
project's needs:
|
||||
|
||||
## Current Implementation Problems
|
||||
|
||||
The current Flutter counter app uses setState() for state management, which
|
||||
has several limitations:
|
||||
|
||||
1. **Local State Only**: setState() only works within a single widget, making
|
||||
it difficult to share state across the app
|
||||
2. **Rebuild Overhead**: Every setState() call rebuilds the entire widget tree,
|
||||
causing performance issues with complex UIs
|
||||
3. **No State Persistence**: State is lost when the widget is disposed
|
||||
4. **Testing Challenges**: setState-based logic is tightly coupled to the UI,
|
||||
making unit testing difficult
|
||||
|
||||
## Why This Matters
|
||||
|
||||
As the app grows beyond a simple counter, these limitations become critical:
|
||||
- Multiple screens need to access the count
|
||||
- State needs to persist across navigation
|
||||
- Business logic should be testable independently
|
||||
- UI should only rebuild when necessary
|
||||
|
||||
## Recommended Solutions
|
||||
|
||||
The issue mentions "Provider or Bloc" - both are excellent alternatives:
|
||||
|
||||
1. **Provider**: Simple, lightweight state management using InheritedWidget
|
||||
- Easy migration path from setState
|
||||
- Good for small to medium apps
|
||||
- Official Flutter recommendation
|
||||
|
||||
2. **Bloc**: More structured approach with clear separation between events,
|
||||
states, and business logic
|
||||
- Better for complex apps
|
||||
- Excellent testability
|
||||
- Clear architectural patterns
|
||||
|
||||
3. **Riverpod**: Modern alternative to Provider with better performance and
|
||||
developer experience
|
||||
- Compile-time safety
|
||||
- Better testing support
|
||||
- More flexible than Provider
|
||||
|
||||
4. **GetX**: Full-featured solution with state management, routing, and
|
||||
dependency injection
|
||||
- Minimal boilerplate
|
||||
- Fast and lightweight
|
||||
- All-in-one solution
|
||||
|
||||
## Next Steps
|
||||
|
||||
The current codebase needs refactoring to implement proper state management
|
||||
architecture to handle more complex state scenarios effectively. Provider
|
||||
would be the easiest migration path while Bloc provides better long-term
|
||||
scalability.
|
||||
```
|
||||
|
||||
## When to Use This Pattern
|
||||
|
||||
This script pattern is ideal for various development scenarios where automated GitHub issue analysis can accelerate your workflow.
|
||||
|
||||
### Bug Investigation
|
||||
|
||||
Quickly analyze bug reports and identify root causes without manual code exploration:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/123 \
|
||||
"What is the root cause of this bug?"
|
||||
```
|
||||
|
||||
### Feature Request Analysis
|
||||
|
||||
Understand context and implications of feature requests:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/456 \
|
||||
"What are the implementation challenges?"
|
||||
```
|
||||
|
||||
### Security Audits
|
||||
|
||||
Assess security implications of reported issues:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/789 \
|
||||
"What are the security implications?"
|
||||
```
|
||||
|
||||
### Documentation Generation
|
||||
|
||||
Generate detailed technical documentation from issues:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/654 \
|
||||
"Provide detailed technical documentation for this issue"
|
||||
```
|
||||
|
||||
### Code Review Assistance
|
||||
|
||||
Get second opinions on proposed changes:
|
||||
|
||||
```bash
|
||||
./analyze-issue.sh https://github.com/project/repo/issues/987 \
|
||||
"Review the proposed solution approach"
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This sample demonstrates how to build an autonomous GitHub issue analysis tool using Cline CLI:
|
||||
|
||||
1. **Building autonomous CLI tools** using Cline's capabilities
|
||||
2. **Parsing structured JSON output** from Cline CLI
|
||||
3. **Creating flexible automation scripts** with custom prompting
|
||||
4. **Integrating with GitHub** for issue analysis
|
||||
5. **Handling command-line arguments** effectively
|
||||
|
||||
This pattern can be adapted for many other automation scenarios, from pull request reviews to documentation generation to code quality analysis.
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [CLI Installation Guide](https://docs.cline.bot/cline-cli/installation)
|
||||
- [CLI Reference Documentation](https://docs.cline.bot/cline-cli/cli-reference)
|
||||
- [Three Core Flows](https://docs.cline.bot/cline-cli/three-core-flows)
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: "Samples Overview"
|
||||
description: Example implementations demonstrating Cline CLI capabilities
|
||||
---
|
||||
|
||||
This section provides sample implementations that demonstrate various Cline CLI features and capabilities. Each sample includes complete code, detailed explanations, and real-world usage examples.
|
||||
|
||||
## Available Samples
|
||||
|
||||
<CardGroup cols={1}>
|
||||
<Card
|
||||
title="GitHub Root Cause Analysis"
|
||||
icon="magnifying-glass-chart"
|
||||
href="/cline-cli/samples/github-issue-rca"
|
||||
>
|
||||
A command-line script that uses Cline's autonomous AI capabilities to fetch, analyze, and identify root causes of GitHub issues. Features JSON output parsing and non-interactive execution.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="GitHub Integration (Actions)"
|
||||
icon="github"
|
||||
href="/cline-cli/samples/github-integration"
|
||||
>
|
||||
Automatically respond to GitHub issues by mentioning @cline in comments. Uses Cline CLI in GitHub Actions to create an AI-powered issue assistant that analyzes and responds autonomously.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [CLI Installation Guide](/cline-cli/installation)
|
||||
- [CLI Reference Documentation](/cline-cli/cli-reference)
|
||||
- [Three Core Flows](/cline-cli/three-core-flows)
|
||||
@@ -88,6 +88,14 @@
|
||||
"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"
|
||||
]
|
||||
},
|
||||
@@ -130,6 +138,7 @@
|
||||
"features/drag-and-drop",
|
||||
"features/editing-messages",
|
||||
"features/focus-chain",
|
||||
"features/hooks",
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
{
|
||||
@@ -315,6 +324,10 @@
|
||||
{
|
||||
"source": "/getting-started/your-first-task",
|
||||
"destination": "/getting-started/your-first-project"
|
||||
},
|
||||
{
|
||||
"source": "/cline-cli/samples",
|
||||
"destination": "/cline-cli/samples/overview"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
---
|
||||
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>
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
## What You Can Build
|
||||
|
||||
Once you understand the basics, hooks open up creative possibilities:
|
||||
|
||||
- **Intelligent Code Review**:
|
||||
Run linters or custom validators before files get saved. Block commits that don't pass checks. Track code quality metrics over time.
|
||||
|
||||
- **Security Enforcement**:
|
||||
Prevent operations that violate security policies. Detect when sensitive data might be exposed. Audit all file access for compliance.
|
||||
|
||||
- **Development Analytics**: Measure how long different operations take. Identify patterns in how the AI works. Generate productivity reports from hook data.
|
||||
|
||||
- **Integration Hub**: Connect to issue trackers when certain keywords appear. Update project management tools. Sync with external APIs at the right moments.
|
||||
|
||||
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.
|
||||
|
||||
## 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: context affects FUTURE decisions, not the current tool
|
||||
- The current AI behavior is based on the previous "API Request..." block
|
||||
- Your `contextModification` gets injected into the NEXT "API Request..." block
|
||||
- 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)
|
||||
|
||||
<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
|
||||
+5
-1
@@ -123,7 +123,11 @@ const copyWasmFiles = {
|
||||
},
|
||||
}
|
||||
|
||||
const buildEnvVars = { "import.meta.url": "_importMetaUrl" }
|
||||
const buildEnvVars = {
|
||||
"import.meta.url": "_importMetaUrl",
|
||||
"process.env.IS_STANDALONE": JSON.stringify(standalone),
|
||||
}
|
||||
|
||||
if (production) {
|
||||
// IS_DEV is always disable in production builds.
|
||||
buildEnvVars["process.env.IS_DEV"] = "false"
|
||||
|
||||
Generated
+774
-1307
File diff suppressed because it is too large
Load Diff
+6
-4
@@ -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.34.1",
|
||||
"version": "3.36.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -405,8 +405,8 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
|
||||
"@aws-sdk/credential-providers": "^3.840.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.922.0",
|
||||
"@aws-sdk/credential-providers": "^3.922.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
@@ -459,7 +459,6 @@
|
||||
"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",
|
||||
@@ -467,6 +466,7 @@
|
||||
"jschardet": "^3.1.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mammoth": "^1.11.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"nice-grpc": "^2.1.12",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"ollama": "^0.5.13",
|
||||
@@ -474,6 +474,7 @@
|
||||
"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",
|
||||
@@ -489,6 +490,7 @@
|
||||
"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",
|
||||
|
||||
@@ -29,7 +29,7 @@ message HookInput {
|
||||
// Output message for all hooks
|
||||
message HookOutput {
|
||||
string context_modification = 1;
|
||||
bool should_continue = 2;
|
||||
bool cancel = 2;
|
||||
string error_message = 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ service ModelsService {
|
||||
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns OpenAI models
|
||||
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
|
||||
// Refreshes and returns Vercel AI Gateway models
|
||||
rpc refreshVercelAiGatewayModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Requesty models
|
||||
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Hicap models
|
||||
rpc refreshHicapModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Subscribe to OpenRouter models updates
|
||||
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
|
||||
// Updates API configuration (legacy - uses combined configuration)
|
||||
@@ -43,6 +43,8 @@ service ModelsService {
|
||||
rpc getSapAiCoreModels(SapAiCoreModelsRequest) returns (SapAiCoreModelsResponse);
|
||||
// Fetches available models from OCA
|
||||
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
|
||||
// Fetches available models from AIhubmix
|
||||
rpc getAihubmixModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -94,6 +96,7 @@ message OpenRouterModelInfo {
|
||||
optional ThinkingConfig thinking_config = 10;
|
||||
optional bool supports_global_endpoint = 11;
|
||||
repeated ModelTier tiers = 12;
|
||||
optional string name = 13;
|
||||
}
|
||||
|
||||
// Shared response message for model information
|
||||
@@ -170,6 +173,7 @@ message ModelsApiSecrets {
|
||||
optional string oca_api_key = 36;
|
||||
optional string oca_refresh_token = 37;
|
||||
optional string minimax_api_key = 38;
|
||||
optional string aihubmix_api_key = 39;
|
||||
}
|
||||
|
||||
// API configuration options (non-secret settings)
|
||||
@@ -216,6 +220,8 @@ message ModelsApiOptions {
|
||||
optional string oca_mode = 39;
|
||||
optional bool aws_use_global_inference = 40;
|
||||
optional string minimax_api_line = 41;
|
||||
optional string aihubmix_base_url = 42;
|
||||
optional string aihubmix_app_code = 43;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
@@ -251,6 +257,8 @@ message ModelsApiOptions {
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
|
||||
optional string plan_mode_oca_model_id = 131;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 132;
|
||||
optional string plan_mode_aihubmix_model_id = 133;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 134;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -286,6 +294,8 @@ message ModelsApiOptions {
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
|
||||
optional string act_mode_oca_model_id = 231;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 232;
|
||||
optional string act_mode_aihubmix_model_id = 233;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 234;
|
||||
}
|
||||
|
||||
// Request for updating API configuration (legacy - uses combined configuration)
|
||||
@@ -294,11 +304,24 @@ message UpdateApiConfigurationRequest {
|
||||
ModelsApiConfiguration api_configuration = 2;
|
||||
}
|
||||
|
||||
// Combined API configuration containing both options and secrets
|
||||
message ApiConfiguration {
|
||||
ModelsApiOptions options = 1;
|
||||
ModelsApiSecrets secrets = 2;
|
||||
}
|
||||
|
||||
// Request for updating API configuration (new - uses separate options and secrets)
|
||||
message UpdateApiConfigurationRequestNew {
|
||||
Metadata metadata = 1;
|
||||
ModelsApiOptions options = 2;
|
||||
ModelsApiSecrets secrets = 3;
|
||||
ApiConfiguration updates = 2;
|
||||
|
||||
// Required field mask specifying which fields to update.
|
||||
// Field paths use dot notation with camelCase field names:
|
||||
// - "options.ulid" (for options fields)
|
||||
// - "options.openAiHeaders" (for options fields)
|
||||
// - "secrets.apiKey" (for secrets fields)
|
||||
// - "secrets.openRouterApiKey" (for secrets fields)
|
||||
repeated string update_mask = 3;
|
||||
}
|
||||
|
||||
// Request for partially updating API configuration using FieldMask
|
||||
@@ -397,6 +420,8 @@ enum ApiProvider {
|
||||
DIFY = 34;
|
||||
OCA = 35;
|
||||
MINIMAX = 36;
|
||||
HICAP = 37;
|
||||
AIHUBMIX = 38;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -516,6 +541,11 @@ message ModelsApiConfiguration {
|
||||
optional bool aws_use_global_inference = 77;
|
||||
optional string minimax_api_key = 78;
|
||||
optional string minimax_api_line = 79;
|
||||
optional string hicap_model_id = 80;
|
||||
optional string hicap_api_key = 81;
|
||||
optional string aihubmix_api_key = 82;
|
||||
optional string aihubmix_base_url = 83;
|
||||
optional string aihubmix_app_code = 84;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
@@ -551,6 +581,10 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
|
||||
optional string plan_mode_oca_model_id = 131;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 132;
|
||||
optional string plan_mode_hicap_model_id = 133;
|
||||
optional OpenRouterModelInfo plan_mode_hicap_model_info = 134;
|
||||
optional string plan_mode_aihubmix_model_id = 135;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 136;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -586,4 +620,8 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
|
||||
optional string act_mode_oca_model_id = 231;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 232;
|
||||
optional string act_mode_hicap_model_id = 233;
|
||||
optional OpenRouterModelInfo act_mode_hicap_model_info = 234;
|
||||
optional string act_mode_aihubmix_model_id = 235;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 236;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ service StateService {
|
||||
rpc updateSettingsCli(UpdateSettingsRequestCli) returns (Empty);
|
||||
rpc updateTaskSettings(UpdateTaskSettingsRequest) returns (Empty);
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
rpc captureOnboardingProgress(OnboardingProgressRequest) returns (Empty);
|
||||
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
|
||||
rpc updateInfoBannerVersion(Int64Request) returns (Empty);
|
||||
rpc updateModelBannerVersion(Int64Request) returns (Empty);
|
||||
@@ -87,6 +88,7 @@ message Secrets {
|
||||
optional string dify_api_key = 36;
|
||||
optional string oca_api_key = 37;
|
||||
optional string oca_refresh_token = 38;
|
||||
optional string hicap_api_key = 39;
|
||||
}
|
||||
|
||||
message Settings {
|
||||
@@ -214,6 +216,13 @@ message Settings {
|
||||
optional int32 max_consecutive_mistakes = 124;
|
||||
optional bool subagents_enabled = 125;
|
||||
optional int32 subagent_terminal_output_line_limit = 126;
|
||||
optional string aihubmix_api_key = 127;
|
||||
optional string aihubmix_base_url = 128;
|
||||
optional string aihubmix_app_code = 129;
|
||||
optional string plan_mode_aihubmix_model_id = 130;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
|
||||
optional string act_mode_aihubmix_model_id = 132;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -350,6 +359,7 @@ message UpdateSettingsRequest {
|
||||
optional bool subagents_enabled = 29;
|
||||
optional int32 subagent_terminal_output_line_limit = 30;
|
||||
optional string cline_env = 31;
|
||||
optional bool native_tool_call_enabled = 32;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
@@ -370,3 +380,10 @@ message ProcessInfo {
|
||||
optional string version = 2;
|
||||
optional int64 uptime_ms = 3;
|
||||
}
|
||||
|
||||
message OnboardingProgressRequest {
|
||||
int32 step = 1;
|
||||
optional string action = 2;
|
||||
optional bool completed = 3;
|
||||
optional string model_selected = 4;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// TTS Service - Handles text-to-speech synthesis
|
||||
service TtsService {
|
||||
// Synthesize speech from text
|
||||
rpc SynthesizeSpeech(SynthesizeRequest) returns (SynthesizeResponse);
|
||||
|
||||
// Get list of available voices for current provider
|
||||
rpc GetAvailableVoices(GetVoicesRequest) returns (VoicesResponse);
|
||||
|
||||
// Validate API key for TTS provider
|
||||
rpc ValidateApiKey(ValidateApiKeyRequest) returns (ValidateApiKeyResponse);
|
||||
|
||||
// Check if API key is configured
|
||||
rpc CheckApiKeyConfigured(GetVoicesRequest) returns (ValidateApiKeyResponse);
|
||||
}
|
||||
|
||||
// Request to synthesize speech
|
||||
message SynthesizeRequest {
|
||||
string text = 1;
|
||||
string voice_id = 2;
|
||||
optional float speed = 3;
|
||||
optional float stability = 4;
|
||||
optional float similarity_boost = 5;
|
||||
}
|
||||
|
||||
// Response containing synthesized audio
|
||||
message SynthesizeResponse {
|
||||
bytes audio_data = 1;
|
||||
string content_type = 2;
|
||||
optional string error = 3;
|
||||
}
|
||||
|
||||
// Request to get available voices
|
||||
message GetVoicesRequest {
|
||||
// Empty for now, could add filtering options later
|
||||
}
|
||||
|
||||
// Information about a voice
|
||||
message Voice {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
optional string description = 3;
|
||||
optional string preview_url = 4;
|
||||
}
|
||||
|
||||
// Response containing list of voices
|
||||
message VoicesResponse {
|
||||
repeated Voice voices = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
// Request to validate API key
|
||||
message ValidateApiKeyRequest {
|
||||
string api_key = 1; // The API key to validate
|
||||
}
|
||||
|
||||
// Response from API key validation
|
||||
message ValidateApiKeyResponse {
|
||||
bool is_valid = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
@@ -77,6 +77,7 @@ enum ClineSayToolType {
|
||||
LIST_CODE_DEFINITION_NAMES = 5;
|
||||
SEARCH_FILES = 6;
|
||||
WEB_FETCH = 7;
|
||||
FILE_DELETED = 8;
|
||||
}
|
||||
|
||||
// Enum for browser actions
|
||||
@@ -263,4 +264,18 @@ service UiService {
|
||||
|
||||
// Opens the Cline walkthrough
|
||||
rpc openWalkthrough(EmptyRequest) returns (Empty);
|
||||
|
||||
// Toggle discuss mode on/off
|
||||
rpc setDiscussModeEnabled(BooleanRequest) returns (BooleanRequest);
|
||||
|
||||
// Update discuss mode voice settings
|
||||
rpc updateDiscussVoiceSettings(DiscussVoiceSettingsRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// Request to update discuss mode voice settings
|
||||
message DiscussVoiceSettingsRequest {
|
||||
optional string selected_voice = 1;
|
||||
optional double speech_speed = 2;
|
||||
optional bool auto_speak = 3;
|
||||
optional bool auto_listen = 4;
|
||||
}
|
||||
|
||||
@@ -328,6 +328,7 @@ export function generateApiKeyDisplayName(fieldName) {
|
||||
sapAiCoreClientId: "SAP AI Core Client ID",
|
||||
sapAiCoreClientSecret: "SAP AI Core Client Secret",
|
||||
huaweiCloudMaasApiKey: "Huawei Cloud MaaS API Key",
|
||||
hicapApiKey: "Hicap API Key",
|
||||
}
|
||||
|
||||
if (specialCases[fieldName]) {
|
||||
|
||||
@@ -119,7 +119,8 @@ async function generateVscodeProtobusServers(protobusServices) {
|
||||
imports.push(`// ${domain} Service`)
|
||||
servers.push(`const ${serviceName}Handlers: serviceTypes.${serviceName}Handlers = {`)
|
||||
for (const [rpcName, _rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
|
||||
const filePath = toFilePath(rpcName)
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${filePath}"`)
|
||||
servers.push(` ${rpcName}: ${rpcName},`)
|
||||
}
|
||||
servers.push(`} \n`)
|
||||
@@ -156,7 +157,8 @@ async function generateStandaloneProtobusServiceSetup(protobusServices) {
|
||||
handlerSetup.push(` // ${domain} Service`)
|
||||
handlerSetup.push(` server.addService(cline.${name}Service, {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
|
||||
const filePath = toFilePath(rpcName)
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${filePath}"`)
|
||||
const requestType = "cline." + rpc.requestType.type.name
|
||||
const responseType = "cline." + rpc.responseType.type.name
|
||||
if (rpc.requestStream) {
|
||||
@@ -204,6 +206,10 @@ function getDirName(serviceName) {
|
||||
const domain = getDomainName(serviceName)
|
||||
return domain.charAt(0).toLowerCase() + domain.slice(1)
|
||||
}
|
||||
function toFilePath(rpcName) {
|
||||
// Convert PascalCase to camelCase for file paths
|
||||
return rpcName.charAt(0).toLowerCase() + rpcName.slice(1)
|
||||
}
|
||||
|
||||
// Only run main if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler } from "../../core/api/index"
|
||||
import { ApiStream } from "../../core/api/transform/stream"
|
||||
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ class ClineEndpoint {
|
||||
environment: Environment.staging,
|
||||
appBaseUrl: "https://staging-app.cline.bot",
|
||||
apiBaseUrl: "https://core-api.staging.int.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
mcpBaseUrl: "https://core-api.staging.int.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
|
||||
authDomain: "cline-staging.firebaseapp.com",
|
||||
|
||||
+22
-4
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { AIhubmixHandler } from "./providers/aihubmix"
|
||||
import { AnthropicHandler } from "./providers/anthropic"
|
||||
import { AskSageHandler } from "./providers/asksage"
|
||||
import { BasetenHandler } from "./providers/baseten"
|
||||
@@ -15,6 +16,7 @@ import { DoubaoHandler } from "./providers/doubao"
|
||||
import { FireworksHandler } from "./providers/fireworks"
|
||||
import { GeminiHandler } from "./providers/gemini"
|
||||
import { GroqHandler } from "./providers/groq"
|
||||
import { HicapHandler } from "./providers/hicap"
|
||||
import { HuaweiCloudMaaSHandler } from "./providers/huawei-cloud-maas"
|
||||
import { HuggingFaceHandler } from "./providers/huggingface"
|
||||
import { LiteLlmHandler } from "./providers/litellm"
|
||||
@@ -365,10 +367,10 @@ function createHandlerForProvider(
|
||||
return new VercelAIGatewayHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
vercelAiGatewayApiKey: options.vercelAiGatewayApiKey,
|
||||
vercelAiGatewayModelId:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelId : options.actModeVercelAiGatewayModelId,
|
||||
vercelAiGatewayModelInfo:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelInfo : options.actModeVercelAiGatewayModelInfo,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "zai":
|
||||
return new ZAiHandler({
|
||||
@@ -391,6 +393,16 @@ function createHandlerForProvider(
|
||||
: options.actModeOcaModelInfo?.supportsPromptCache,
|
||||
taskId: options.ulid,
|
||||
})
|
||||
case "aihubmix":
|
||||
return new AIhubmixHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
apiKey: options.aihubmixApiKey,
|
||||
baseURL: options.aihubmixBaseUrl,
|
||||
appCode: options.aihubmixAppCode,
|
||||
modelId: mode === "plan" ? (options as any).planModeAihubmixModelId : (options as any).actModeAihubmixModelId,
|
||||
modelInfo:
|
||||
mode === "plan" ? (options as any).planModeAihubmixModelInfo : (options as any).actModeAihubmixModelInfo,
|
||||
})
|
||||
case "minimax":
|
||||
return new MinimaxHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
@@ -398,6 +410,12 @@ function createHandlerForProvider(
|
||||
minimaxApiLine: options.minimaxApiLine,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "hicap":
|
||||
return new HicapHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
hicapApiKey: options.hicapApiKey,
|
||||
hicapModelId: mode === "plan" ? options.planModeHicapModelId : options.actModeHicapModelId,
|
||||
})
|
||||
default:
|
||||
return new AnthropicHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { LiteLlmHandler, type LiteLlmModelInfoResponse } from "@core/api/provide
|
||||
import { convertToOpenAiMessages } from "@core/api/transform/openai-format"
|
||||
import { expect } from "chai"
|
||||
import sinon from "sinon"
|
||||
import { mockFetchForTesting } from "@/shared/net"
|
||||
|
||||
const fakeClient = {
|
||||
chat: {
|
||||
@@ -10,12 +11,12 @@ const fakeClient = {
|
||||
create: sinon.stub(),
|
||||
},
|
||||
},
|
||||
baseURL: "fake",
|
||||
baseURL: "https://fake.example",
|
||||
}
|
||||
|
||||
describe("LiteLlmHandler", () => {
|
||||
const originalFetch = global.fetch
|
||||
const mockFetch = sinon.stub()
|
||||
let doneMockingFetch: (value: any) => void = () => {}
|
||||
|
||||
const mockModelFetch = (modelInfo: LiteLlmModelInfoResponse["data"][number]) => {
|
||||
mockFetch.resolves({
|
||||
@@ -45,7 +46,11 @@ describe("LiteLlmHandler", () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
global.fetch = mockFetch
|
||||
mockFetchForTesting(mockFetch, () => {
|
||||
return new Promise((resolve) => {
|
||||
doneMockingFetch = resolve
|
||||
})
|
||||
})
|
||||
|
||||
// Configure the stub to return a stream that closes immediately with usage data
|
||||
fakeClient.chat.completions.create.resolves(
|
||||
@@ -68,8 +73,7 @@ describe("LiteLlmHandler", () => {
|
||||
|
||||
afterEach(() => {
|
||||
sinon.reset()
|
||||
|
||||
global.fetch = originalFetch
|
||||
doneMockingFetch(void 0)
|
||||
})
|
||||
|
||||
const createAsyncIterable = (data: any[] = []) => {
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { GenerateContentConfig, GoogleGenAI } from "@google/genai"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface AIhubmixHandlerOptions extends CommonApiHandlerOptions {
|
||||
apiKey?: string
|
||||
baseURL?: string
|
||||
appCode?: string
|
||||
modelId?: string
|
||||
modelInfo?: ModelInfo
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
export class AIhubmixHandler implements ApiHandler {
|
||||
private options: AIhubmixHandlerOptions
|
||||
private anthropicClient: Anthropic | undefined
|
||||
private openaiClient: OpenAI | undefined
|
||||
private geminiClient: GoogleGenAI | undefined
|
||||
|
||||
constructor(options: AIhubmixHandlerOptions) {
|
||||
const { baseURL, appCode, ...rest } = options
|
||||
this.options = {
|
||||
baseURL: baseURL ?? "https://aihubmix.com",
|
||||
appCode: appCode ?? "KUWF9311",
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
private ensureAnthropicClient(): Anthropic {
|
||||
if (!this.anthropicClient) {
|
||||
if (!this.options.apiKey) {
|
||||
throw new Error("AIhubmix API key is required")
|
||||
}
|
||||
try {
|
||||
this.anthropicClient = new Anthropic({
|
||||
apiKey: this.options.apiKey,
|
||||
baseURL: this.options.baseURL,
|
||||
defaultHeaders: {
|
||||
"APP-Code": this.options.appCode,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Anthropic client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.anthropicClient
|
||||
}
|
||||
|
||||
private ensureOpenaiClient(): OpenAI {
|
||||
if (!this.openaiClient) {
|
||||
if (!this.options.apiKey) {
|
||||
throw new Error("AIhubmix API key is required")
|
||||
}
|
||||
try {
|
||||
this.openaiClient = new OpenAI({
|
||||
apiKey: this.options.apiKey,
|
||||
baseURL: `${this.options.baseURL}/v1`,
|
||||
defaultHeaders: {
|
||||
"APP-Code": this.options.appCode,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating OpenAI client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.openaiClient
|
||||
}
|
||||
|
||||
private ensureGeminiClient(): GoogleGenAI {
|
||||
if (!this.geminiClient) {
|
||||
if (!this.options.apiKey) {
|
||||
throw new Error("AIhubmix API key is required")
|
||||
}
|
||||
try {
|
||||
this.geminiClient = new GoogleGenAI({
|
||||
apiKey: this.options.apiKey,
|
||||
httpOptions: {
|
||||
baseUrl: `${this.options.baseURL}/gemini`,
|
||||
headers: {
|
||||
// @ts-expect-error
|
||||
"APP-Code": this.options.appCode,
|
||||
Authorization: `Bearer ${this.options.apiKey ?? ""}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Gemini client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.geminiClient
|
||||
}
|
||||
|
||||
private routeModel(modelName: string): "anthropic" | "openai" | "gemini" | "openai-response" {
|
||||
const id = modelName || ""
|
||||
if (id.startsWith("claude")) {
|
||||
return "anthropic"
|
||||
}
|
||||
if (id.startsWith("gemini") && !id.endsWith("-nothink") && !id.endsWith("-search")) {
|
||||
return "gemini"
|
||||
}
|
||||
if (id === "gpt-5-pro" || id === "gpt-5-codex") {
|
||||
return "openai-response"
|
||||
}
|
||||
return "openai"
|
||||
}
|
||||
|
||||
private fixToolChoice(requestBody: any): any {
|
||||
if (requestBody.tools?.length === 0 && requestBody.tool_choice) {
|
||||
delete requestBody.tool_choice
|
||||
}
|
||||
return requestBody
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: any[]): ApiStream {
|
||||
const modelId = this.options.modelId || ""
|
||||
const route = this.routeModel(modelId)
|
||||
|
||||
switch (route) {
|
||||
case "anthropic":
|
||||
yield* this.createAnthropicMessage(systemPrompt, messages)
|
||||
break
|
||||
case "gemini":
|
||||
yield* this.createGeminiMessage(systemPrompt, messages)
|
||||
break
|
||||
case "openai-response":
|
||||
yield* this.createOpenaiResponseMessage(systemPrompt, messages)
|
||||
break
|
||||
case "openai":
|
||||
yield* this.createOpenaiMessage(systemPrompt, messages)
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported model route: ${route}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async *createAnthropicMessage(systemPrompt: string, messages: any[]): ApiStream {
|
||||
const client = this.ensureAnthropicClient()
|
||||
const modelId = this.options.modelId || "claude-3-5-sonnet-20241022"
|
||||
|
||||
const stream = await client.messages.create({
|
||||
model: modelId,
|
||||
temperature: 0,
|
||||
max_tokens: this.options.modelInfo?.maxTokens || 8192,
|
||||
system: [{ text: systemPrompt, type: "text" }],
|
||||
messages,
|
||||
stream: true,
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk?.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens || undefined,
|
||||
}
|
||||
break
|
||||
case "message_delta":
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
case "content_block_start":
|
||||
if (chunk.content_block.type === "text") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.content_block.text,
|
||||
}
|
||||
}
|
||||
break
|
||||
case "content_block_delta":
|
||||
if (chunk.delta.type === "text_delta") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async *createOpenaiResponseMessage(systemPrompt: string, messages: any[]): ApiStream {
|
||||
const client = this.ensureOpenaiClient()
|
||||
const modelId = this.options.modelId || "gpt-4o-mini"
|
||||
|
||||
const input = (messages || []).map((m: any) => {
|
||||
const role = m.role || "user"
|
||||
const contentArray = Array.isArray(m.content) ? m.content : [{ type: "text", text: m.content }]
|
||||
const content = contentArray
|
||||
.filter((c: any) => c != null)
|
||||
.map((c: any) => {
|
||||
if (c.type === "image" || c.type === "input_image" || c.type === "image_url") {
|
||||
return { type: "input_image", image_url: c.image_url || c.url || c.source?.url }
|
||||
}
|
||||
const text = c.text ?? (typeof c === "string" ? c : "")
|
||||
return { type: role === "assistant" ? "output_text" : "input_text", text }
|
||||
})
|
||||
return { role, content }
|
||||
})
|
||||
|
||||
const stream = await (client as any).responses.stream({
|
||||
model: modelId,
|
||||
instructions: systemPrompt,
|
||||
input,
|
||||
})
|
||||
|
||||
for await (const event of stream as any) {
|
||||
if (event?.type === "response.output_text.delta") {
|
||||
yield { type: "text", text: event.delta || "" }
|
||||
continue
|
||||
}
|
||||
if (event?.type === "response.completed") {
|
||||
const usage = event.response?.usage || {}
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (event?.type === "response.error") {
|
||||
throw new Error(event.error?.message || "responses error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async *createOpenaiMessage(systemPrompt: string, messages: any[]): ApiStream {
|
||||
const client = this.ensureOpenaiClient()
|
||||
const modelId = this.options.modelId || "gpt-4o-mini"
|
||||
|
||||
const openaiMessages = [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
|
||||
|
||||
const requestBody = {
|
||||
model: modelId,
|
||||
messages: openaiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
}
|
||||
|
||||
const fixedRequestBody = this.fixToolChoice(requestBody)
|
||||
|
||||
const stream = await client.chat.completions.create(fixedRequestBody)
|
||||
|
||||
for await (const chunk of stream as any) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async *createGeminiMessage(systemPrompt: string, messages: any[]): ApiStream {
|
||||
const client = this.ensureGeminiClient()
|
||||
const modelId = this.options.modelId || "gemini-2.0-flash-exp"
|
||||
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
const requestConfig: GenerateContentConfig = {
|
||||
systemInstruction: systemPrompt,
|
||||
temperature: 0,
|
||||
}
|
||||
|
||||
if (this.options.thinkingBudgetTokens) {
|
||||
requestConfig.thinkingConfig = {
|
||||
thinkingBudget: this.options.thinkingBudgetTokens,
|
||||
includeThoughts: true,
|
||||
}
|
||||
}
|
||||
|
||||
const stream = await client.models.generateContentStream({
|
||||
model: modelId,
|
||||
contents,
|
||||
config: requestConfig,
|
||||
})
|
||||
|
||||
for await (const chunk of stream as any) {
|
||||
if (chunk?.text) {
|
||||
yield { type: "text", text: chunk.text }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.modelId || "gpt-4o-mini",
|
||||
info: this.options.modelInfo || {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
description: "AIhubmix unified model provider",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Tool as AnthropicTool, MessageParam } from "@anthropic-ai/sdk/resources/index"
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface AnthropicHandlerOptions extends CommonApiHandlerOptions {
|
||||
@@ -31,6 +33,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.apiKey,
|
||||
baseURL: this.options.anthropicBaseUrl || undefined,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Anthropic client: ${error.message}`)
|
||||
@@ -61,6 +64,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
switch (modelId) {
|
||||
// 'latest' alias does not support cache_control
|
||||
case "claude-haiku-4-5-20251001":
|
||||
case "claude-sonnet-4-5-20250929:1m":
|
||||
case "claude-sonnet-4-5-20250929":
|
||||
case "claude-sonnet-4-20250514":
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
@@ -82,35 +86,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
const anthropicMessages: Array<MessageParam> = messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
const anthropicMessages = sanitizeAnthropicMessages(messages, lastUserMsgIndex, secondLastMsgUserIndex)
|
||||
|
||||
stream = await client.messages.create(
|
||||
{
|
||||
@@ -159,7 +135,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: [{ text: systemPrompt, type: "text" }],
|
||||
messages,
|
||||
messages: sanitizeAnthropicMessages(messages),
|
||||
// tools,
|
||||
// tool_choice: { type: "auto" },
|
||||
stream: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { BasetenModelId, basetenDefaultModelId, basetenModels, ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -31,6 +32,7 @@ export class BasetenHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://inference.baseten.co/v1",
|
||||
apiKey: this.options.basetenApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Baseten client: ${error.message}`)
|
||||
|
||||
@@ -270,8 +270,12 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add proxy support for AWS SDK
|
||||
// AWS SDK uses a different architecture than fetch-based SDKs.
|
||||
// To add proxy support, we need to provide a custom requestHandler.
|
||||
return new BedrockRuntimeClient({
|
||||
defaultUserAgentProvider: () => Promise.resolve([["cline", ExtensionRegistryInfo.version]]),
|
||||
userAgentAppId: `cline#${ExtensionRegistryInfo.version}`,
|
||||
region: this.getRegion(),
|
||||
...auth,
|
||||
...(this.options.awsBedrockEndpoint && { endpoint: this.options.awsBedrockEndpoint }),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import Cerebras from "@cerebras/cerebras_cloud_sdk"
|
||||
import { CerebrasModelId, cerebrasDefaultModelId, cerebrasModels, ModelInfo } from "@shared/api"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -31,6 +32,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
this.client = new Cerebras({
|
||||
apiKey: cleanApiKey,
|
||||
timeout: 30000, // 30 second timeout
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Cerebras client: ${error.message}`)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { buildClineExtraHeaders } from "@/services/EnvUtils"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -239,6 +240,7 @@ export class ClineHandler implements ApiHandler {
|
||||
const response = await axios.get(`${this.clineAccountService.baseUrl}/generation?id=${this.lastGenerationId}`, {
|
||||
headers,
|
||||
timeout: 15_000, // this request hangs sometimes
|
||||
...getAxiosSettings(),
|
||||
})
|
||||
|
||||
const generation = response.data
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } fr
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -32,6 +33,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
apiKey: this.options.deepSeekApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating DeepSeek client: ${error.message}`)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ModelInfo } from "../../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { DoubaoModelId, doubaoDefaultModelId, doubaoModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -27,6 +28,7 @@ export class DoubaoHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
|
||||
apiKey: this.options.doubaoApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Doubao client: ${error.message}`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { FireworksModelId, fireworksDefaultModelId, fireworksModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -30,6 +31,7 @@ export class FireworksHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.fireworks.ai/inference/v1",
|
||||
apiKey: this.options.fireworksApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Fireworks client: ${error.message}`)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { GroqModelId, groqDefaultModelId, groqModels, ModelInfo } from "@shared/
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -102,6 +103,7 @@ export class GroqHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
apiKey: this.options.groqApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Groq client: ${error.message}`)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { hicapModelInfoSaneDefaults, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface OpenAiHandlerOptions extends CommonApiHandlerOptions {
|
||||
hicapApiKey?: string
|
||||
hicapModelId?: string
|
||||
}
|
||||
|
||||
export class HicapHandler implements ApiHandler {
|
||||
private options: OpenAiHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: OpenAiHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.hicapApiKey) {
|
||||
throw new Error("Hicap API key is required")
|
||||
}
|
||||
if (!this.options.hicapModelId) {
|
||||
throw new Error("Model ID is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.hicap.ai/v2/openai",
|
||||
apiKey: this.options.hicapApiKey,
|
||||
defaultHeaders: {
|
||||
"api-key": this.options.hicapApiKey,
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating OpenAI client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.hicapModelId ?? ""
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const temperature: number = 1
|
||||
let reasoningEffort: ChatCompletionReasoningEffort | undefined
|
||||
let maxTokens: number | undefined
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
reasoning_effort: reasoningEffort,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta.reasoning_content as string | undefined) || "",
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.hicapModelId ?? "",
|
||||
info: hicapModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { HuaweiCloudMaasModelId, huaweiCloudMaasDefaultModelId, huaweiCloudMaasModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -30,6 +31,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.modelarts-maas.com/v1/",
|
||||
apiKey: this.options.huaweiCloudMaasApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { HuggingFaceModelId, huggingFaceDefaultModelId, huggingFaceModels, Model
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -37,6 +38,7 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
defaultHeaders: {
|
||||
"User-Agent": "Cline/1.0",
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Hugging Face client: ${error.message}`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { isAnthropicModelId } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
@@ -55,6 +56,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
|
||||
apiKey: this.options.liteLlmApiKey || "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LiteLLM client: ${error.message}`)
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -29,6 +30,7 @@ export class LmStudioHandler implements ApiHandler {
|
||||
// Docs on the new v0 api endpoint: https://lmstudio.ai/docs/app/api/endpoints/rest
|
||||
baseURL: new URL("api/v0", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
|
||||
apiKey: "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LM Studio client: ${error.message}`)
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { MinimaxModelId, ModelInfo, minimaxDefaultModelId, minimaxModels } from "@/shared/api"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface MinimaxHandlerOptions extends CommonApiHandlerOptions {
|
||||
minimaxApiKey?: string
|
||||
minimaxApiLine?: string
|
||||
apiModelId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
export class MinimaxHandler implements ApiHandler {
|
||||
private client: OpenAI | undefined
|
||||
private options: MinimaxHandlerOptions
|
||||
private client: Anthropic | undefined
|
||||
|
||||
constructor(private readonly options: MinimaxHandlerOptions) {}
|
||||
constructor(options: MinimaxHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
private ensureClient(): Anthropic {
|
||||
if (!this.client) {
|
||||
if (!this.options.minimaxApiKey) {
|
||||
throw new Error("MiniMax API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL:
|
||||
this.options.minimaxApiLine === "china" ? "https://api.minimaxi.com/v1" : "https://api.minimax.io/v1",
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.minimaxApiKey,
|
||||
baseURL:
|
||||
this.options.minimaxApiLine === "china"
|
||||
? "https://api.minimaxi.com/anthropic"
|
||||
: "https://api.minimax.io/anthropic",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating MiniMax client: ${error.message}`)
|
||||
@@ -38,60 +45,157 @@ export class MinimaxHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
tools?: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
// Tools are available only when native tools are enabled
|
||||
const nativeToolsOn = tools?.length && tools?.length > 0
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
// MiniMax M2 uses Anthropic API format
|
||||
// Note: According to MiniMax docs, some Anthropic parameters like 'thinking' are ignored
|
||||
// but we'll include the standard Anthropic streaming pattern for consistency
|
||||
const stream: AnthropicStream<Anthropic.RawMessageStreamEvent> = await client.messages.create({
|
||||
model: model.id,
|
||||
messages: openAiMessages,
|
||||
max_tokens: model.info.maxTokens,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 1.0, // MiniMax recommends 1.0, range is (0.0, 1.0]
|
||||
system: [{ text: systemPrompt, type: "text" }],
|
||||
messages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...getOpenAIToolParams(tools),
|
||||
tools: nativeToolsOn ? (tools as AnthropicTool[]) : undefined,
|
||||
tool_choice: nativeToolsOn ? { type: "any" } : undefined,
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
let thinkingDeltaAccumulator = ""
|
||||
const lastStartedToolCall = { id: "", name: "", arguments: "" }
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
switch (chunk?.type) {
|
||||
case "message_start":
|
||||
// tells us cache reads/writes/input/output
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens || undefined,
|
||||
}
|
||||
break
|
||||
case "message_delta":
|
||||
// tells us stop_reason, stop_sequence, and output tokens along the way and at the end of the message
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
case "message_stop":
|
||||
// no usage data, just an indicator that the message is done
|
||||
break
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "thinking":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.content_block.thinking || "",
|
||||
}
|
||||
const thinking = chunk.content_block.thinking
|
||||
const signature = chunk.content_block.signature
|
||||
if (thinking && signature) {
|
||||
yield {
|
||||
type: "ant_thinking",
|
||||
thinking,
|
||||
signature,
|
||||
}
|
||||
}
|
||||
break
|
||||
case "redacted_thinking":
|
||||
// Content is encrypted, and we don't want to pass placeholder text back to the API
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: "[Redacted thinking block]",
|
||||
}
|
||||
yield {
|
||||
type: "ant_redacted_thinking",
|
||||
data: chunk.content_block.data,
|
||||
}
|
||||
break
|
||||
case "tool_use":
|
||||
if (chunk.content_block.id && chunk.content_block.name) {
|
||||
// Store tool call information for streaming
|
||||
lastStartedToolCall.id = chunk.content_block.id
|
||||
lastStartedToolCall.name = chunk.content_block.name
|
||||
lastStartedToolCall.arguments = ""
|
||||
}
|
||||
break
|
||||
case "text":
|
||||
// we may receive multiple text blocks, in which case just insert a line break between them
|
||||
if (chunk.index > 0) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: "\n",
|
||||
}
|
||||
}
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.content_block.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "thinking_delta":
|
||||
// 'reasoning' type just displays in the UI, but ant_thinking will be used to send the thinking traces back to the API
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.delta.thinking,
|
||||
}
|
||||
thinkingDeltaAccumulator += chunk.delta.thinking
|
||||
break
|
||||
case "signature_delta":
|
||||
// It's used when sending the thinking block back to the API
|
||||
// API expects this in completed form, not as array of deltas
|
||||
if (thinkingDeltaAccumulator && chunk.delta.signature) {
|
||||
yield {
|
||||
type: "ant_thinking",
|
||||
thinking: thinkingDeltaAccumulator,
|
||||
signature: chunk.delta.signature,
|
||||
}
|
||||
}
|
||||
break
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
break
|
||||
case "input_json_delta":
|
||||
if (lastStartedToolCall.id && lastStartedToolCall.name && chunk.delta.partial_json) {
|
||||
// Convert Anthropic tool_use to OpenAI-compatible format for internal processing
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
...lastStartedToolCall,
|
||||
function: {
|
||||
...lastStartedToolCall,
|
||||
id: lastStartedToolCall.id,
|
||||
name: lastStartedToolCall.name,
|
||||
arguments: chunk.delta.partial_json,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta.reasoning_content as string | undefined) || "",
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
case "content_block_stop":
|
||||
lastStartedToolCall.id = ""
|
||||
lastStartedToolCall.name = ""
|
||||
lastStartedToolCall.arguments = ""
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Mistral } from "@mistralai/mistralai"
|
||||
import { HTTPClient } from "@mistralai/mistralai/lib/http"
|
||||
import { Tool as MistralTool } from "@mistralai/mistralai/models/components/tool"
|
||||
import { MistralModelId, ModelInfo, mistralDefaultModelId, mistralModels } from "@shared/api"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToMistralMessages } from "../transform/mistral-format"
|
||||
@@ -27,8 +29,16 @@ export class MistralHandler implements ApiHandler {
|
||||
throw new Error("Mistral API key is required")
|
||||
}
|
||||
try {
|
||||
// Create HTTP client with custom fetch for proxy support
|
||||
const httpClient = new HTTPClient({
|
||||
fetcher: (request) => {
|
||||
return fetch(request)
|
||||
},
|
||||
})
|
||||
|
||||
this.client = new Mistral({
|
||||
apiKey: this.options.mistralApiKey,
|
||||
httpClient,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Mistral client: ${error.message}`)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ModelInfo, MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "@/shared/api"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -29,6 +30,7 @@ export class MoonshotHandler implements ApiHandler {
|
||||
baseURL:
|
||||
this.options.moonshotApiLine === "china" ? "https://api.moonshot.cn/v1" : "https://api.moonshot.ai/v1",
|
||||
apiKey: this.options.moonshotApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Moonshot client: ${error.message}`)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { type ModelInfo, type NebiusModelId, nebiusDefaultModelId, nebiusModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -28,6 +29,7 @@ export class NebiusHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.studio.nebius.ai/v1",
|
||||
apiKey: this.options.nebiusApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Nebius client: ${error.message}`)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -81,6 +82,7 @@ export class OcaHandler implements ApiHandler {
|
||||
options.ocaBaseUrl ||
|
||||
(options.ocaMode === "internal" ? DEFAULT_INTERNAL_OCA_BASE_URL : DEFAULT_EXTERNAL_OCA_BASE_URL),
|
||||
apiKey: "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ModelInfo, OpenAiNativeModelId, openAiNativeDefaultModelId, openAiNativ
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -31,6 +32,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
apiKey: this.options.openAiNativeApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating OpenAI client: ${error.message}`)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { azureOpenAiDefaultApiVersion, ModelInfo, OpenAiCompatibleModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI, { AzureOpenAI } from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -46,12 +47,14 @@ export class OpenAiHandler implements ApiHandler {
|
||||
apiKey: this.options.openAiApiKey,
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} else {
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import axios from "axios"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -43,6 +44,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
|
||||
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating OpenRouter client: ${error.message}`)
|
||||
@@ -201,6 +203,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
Authorization: `Bearer ${this.options.openRouterApiKey}`,
|
||||
},
|
||||
timeout: 15_000, // this request hangs sometimes
|
||||
...getAxiosSettings(),
|
||||
})
|
||||
yield response.data?.data
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -52,6 +53,7 @@ export class QwenHandler implements ApiHandler {
|
||||
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: this.options.qwenApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Alibaba client: ${error.message}`)
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, requestyDefaultModelId, requestyDefaultModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import { toRequestyServiceStringUrl } from "@/shared/providers/requesty"
|
||||
import { toRequestyServiceStringUrl } from "@/shared/clients/requesty"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -48,6 +49,7 @@ export class RequestyHandler implements ApiHandler {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Requesty client: ${error.message}`)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -31,6 +32,7 @@ export class SambanovaHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.sambanova.ai/v1",
|
||||
apiKey: this.options.sambanovaApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating SambaNova client: ${error.message}`)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ChatMessages, LlmModuleConfig, OrchestrationClient, TemplatingModuleCon
|
||||
import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels } from "@shared/api"
|
||||
import axios from "axios"
|
||||
import OpenAI from "openai"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -384,6 +385,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
const tokenUrl = this.options.sapAiCoreTokenUrl!.replace(/\/+$/, "") + "/oauth/token"
|
||||
const response = await axios.post(tokenUrl, payload, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
...getAxiosSettings(),
|
||||
})
|
||||
const token = response.data as Token
|
||||
token.expires_at = Date.now() + token.expires_in * 1000
|
||||
@@ -410,7 +412,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
const url = `${this.options.sapAiCoreBaseUrl}/v2/lm/deployments?$top=10000&$skip=0`
|
||||
|
||||
try {
|
||||
const response = await axios.get(url, { headers })
|
||||
const response = await axios.get(url, { headers, ...getAxiosSettings() })
|
||||
const deployments = response.data.resources
|
||||
|
||||
return deployments
|
||||
@@ -555,6 +557,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const anthropicModels = [
|
||||
"anthropic--claude-4.5-sonnet",
|
||||
"anthropic--claude-4-sonnet",
|
||||
"anthropic--claude-4-opus",
|
||||
"anthropic--claude-3.7-sonnet",
|
||||
@@ -599,6 +602,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
if (
|
||||
model.id === "anthropic--claude-4.5-sonnet" ||
|
||||
model.id === "anthropic--claude-4-sonnet" ||
|
||||
model.id === "anthropic--claude-4-opus" ||
|
||||
model.id === "anthropic--claude-3.7-sonnet"
|
||||
@@ -677,10 +681,11 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
const response = await axios.post(url, JSON.stringify(payload, null, 2), {
|
||||
headers,
|
||||
responseType: "stream",
|
||||
...getAxiosSettings(),
|
||||
})
|
||||
|
||||
if (model.id === "o3-mini") {
|
||||
const response = await axios.post(url, JSON.stringify(payload, null, 2), { headers })
|
||||
const response = await axios.post(url, JSON.stringify(payload, null, 2), { headers, ...getAxiosSettings() })
|
||||
|
||||
// Yield the usage information
|
||||
if (response.data.usage) {
|
||||
@@ -710,6 +715,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
} else if (openAIModels.includes(model.id)) {
|
||||
yield* this.streamCompletionGPT(response.data, model)
|
||||
} else if (
|
||||
model.id === "anthropic--claude-4.5-sonnet" ||
|
||||
model.id === "anthropic--claude-4-sonnet" ||
|
||||
model.id === "anthropic--claude-4-opus" ||
|
||||
model.id === "anthropic--claude-3.7-sonnet"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -31,6 +32,7 @@ export class TogetherHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.together.xyz/v1",
|
||||
apiKey: this.options.togetherApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Together client: ${error.message}`)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, vercelAiGatewayDefaultModelId, vercelAiGatewayDefaultModelInfo } from "@shared/api"
|
||||
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -10,8 +11,9 @@ import { createVercelAIGatewayStream } from "../transform/vercel-ai-gateway-stre
|
||||
|
||||
interface VercelAIGatewayHandlerOptions extends CommonApiHandlerOptions {
|
||||
vercelAiGatewayApiKey?: string
|
||||
vercelAiGatewayModelId?: string
|
||||
vercelAiGatewayModelInfo?: ModelInfo
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
export class VercelAIGatewayHandler implements ApiHandler {
|
||||
@@ -35,6 +37,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
"http-referer": "https://cline.bot",
|
||||
"x-title": "Cline",
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Vercel AI Gateway client: ${error.message}`)
|
||||
@@ -55,6 +58,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
systemPrompt,
|
||||
messages,
|
||||
{ id: modelId, info: modelInfo },
|
||||
this.options.thinkingBudgetTokens,
|
||||
tools,
|
||||
)
|
||||
let didOutputUsage: boolean = false
|
||||
@@ -74,6 +78,28 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
reasoning: delta.reasoning,
|
||||
}
|
||||
}
|
||||
|
||||
// Reasoning details that can be passed back in API requests to preserve reasoning traces
|
||||
if (
|
||||
"reasoning_details" in delta &&
|
||||
delta.reasoning_details &&
|
||||
// @ts-ignore-next-line
|
||||
delta.reasoning_details.length // exists and non-0
|
||||
) {
|
||||
yield {
|
||||
type: "reasoning_details",
|
||||
reasoning_details: delta.reasoning_details,
|
||||
}
|
||||
}
|
||||
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
const inputTokens = chunk.usage.prompt_tokens || 0
|
||||
const outputTokens =
|
||||
@@ -107,11 +133,11 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.vercelAiGatewayModelId
|
||||
const modelInfo = this.options.vercelAiGatewayModelInfo
|
||||
const modelId = this.options.openRouterModelId
|
||||
const modelInfo = this.options.openRouterModelInfo
|
||||
if (modelId && modelInfo) {
|
||||
return { id: modelId, info: modelInfo }
|
||||
}
|
||||
return { id: vercelAiGatewayDefaultModelId, info: vercelAiGatewayDefaultModelInfo }
|
||||
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ModelInfo, VertexModelId, vertexDefaultModelId, vertexModels } from "@s
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { GeminiHandler } from "./gemini"
|
||||
|
||||
@@ -122,46 +123,7 @@ export class VertexHandler implements ApiHandler {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
}
|
||||
}),
|
||||
messages: sanitizeAnthropicMessages(messages, lastUserMsgIndex, secondLastMsgUserIndex),
|
||||
stream: true,
|
||||
tools: tools?.length ? (tools as AnthropicTool[]) : undefined,
|
||||
// tool_choice options:
|
||||
@@ -187,18 +149,7 @@ export class VertexHandler implements ApiHandler {
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
messages: messages.map((message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
})),
|
||||
messages: sanitizeAnthropicMessages(messages),
|
||||
stream: true,
|
||||
tools: tools?.length ? (tools as AnthropicTool[]) : undefined,
|
||||
// tool_choice options:
|
||||
|
||||
@@ -4,6 +4,7 @@ import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -33,6 +34,7 @@ export class XAIHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.x.ai/v1",
|
||||
apiKey: this.options.xaiApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating xAI client: ${error.message}`)
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { version as extensionVersion } from "../../../../package.json"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
@@ -48,6 +49,7 @@ export class ZAiHandler implements ApiHandler {
|
||||
"X-Title": "Cline",
|
||||
"X-Cline-Version": extensionVersion,
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Z AI client: ${error.message}`)
|
||||
|
||||
@@ -1,19 +1,77 @@
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { MessageParam } from "@anthropic-ai/sdk/resources/index"
|
||||
|
||||
/**
|
||||
* Converts an OpenAI ChatCompletionTool into an Anthropic Tool definition
|
||||
* Sanitize Anthropic messages by removing reasoning details and adding ephemeral cache control
|
||||
* to the last two user messages to prevent them from being stored in Anthropic's cache.
|
||||
*/
|
||||
export function openAIToolToAnthropic(openAITool: OpenAITool): AnthropicTool {
|
||||
const func = openAITool.function
|
||||
export function sanitizeAnthropicMessages(
|
||||
messages: Array<MessageParam>,
|
||||
lastUserMsgIndex?: number,
|
||||
secondLastMsgUserIndex?: number,
|
||||
): Array<MessageParam> {
|
||||
return messages.map((_message, index) => {
|
||||
const message = removeReasoningDetails(_message)
|
||||
const addCacheControl = lastUserMsgIndex !== undefined && secondLastMsgUserIndex !== undefined
|
||||
|
||||
return {
|
||||
name: func.name,
|
||||
description: func.description || "",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: func.parameters?.properties || {},
|
||||
required: func.parameters?.required || [],
|
||||
},
|
||||
if (addCacheControl && (index === lastUserMsgIndex || index === secondLastMsgUserIndex)) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove reasoning details from a single Anthropic message parameter
|
||||
*/
|
||||
function removeReasoningDetails(param: MessageParam): MessageParam {
|
||||
if (Array.isArray(param.content)) {
|
||||
return {
|
||||
...param,
|
||||
content: param.content.map((item) => {
|
||||
if (item.type === "text") {
|
||||
return {
|
||||
...item,
|
||||
reasoning_details: undefined,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}),
|
||||
}
|
||||
}
|
||||
return param
|
||||
}
|
||||
|
||||
@@ -5,40 +5,44 @@ export function convertAnthropicContentToGemini(content: string | Anthropic.Cont
|
||||
if (typeof content === "string") {
|
||||
return [{ text: content }]
|
||||
}
|
||||
return content.flatMap((block): Part => {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
return { text: block.text }
|
||||
case "image":
|
||||
if (block.source.type !== "base64") {
|
||||
throw new Error("Unsupported image source type")
|
||||
}
|
||||
return {
|
||||
inlineData: {
|
||||
data: block.source.data,
|
||||
mimeType: block.source.media_type,
|
||||
},
|
||||
}
|
||||
case "tool_use":
|
||||
return {
|
||||
functionCall: {
|
||||
name: block.name,
|
||||
args: block.input as Record<string, unknown>,
|
||||
},
|
||||
}
|
||||
case "tool_result":
|
||||
return {
|
||||
functionResponse: {
|
||||
name: block.tool_use_id,
|
||||
response: {
|
||||
result: block.content,
|
||||
return content
|
||||
.flatMap((block): Part | undefined => {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
return { text: block.text }
|
||||
case "image":
|
||||
if (block.source.type !== "base64") {
|
||||
throw new Error("Unsupported image source type")
|
||||
}
|
||||
return {
|
||||
inlineData: {
|
||||
data: block.source.data,
|
||||
mimeType: block.source.media_type,
|
||||
},
|
||||
},
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported content block type: ${block.type}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
case "tool_use":
|
||||
return {
|
||||
functionCall: {
|
||||
name: block.name,
|
||||
args: block.input as Record<string, unknown>,
|
||||
},
|
||||
}
|
||||
case "tool_result":
|
||||
return {
|
||||
functionResponse: {
|
||||
name: block.tool_use_id,
|
||||
response: {
|
||||
result: block.content,
|
||||
},
|
||||
},
|
||||
}
|
||||
case "thinking":
|
||||
return { text: block.thinking, thought: true, thoughtSignature: block.signature }
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
.filter((part): part is Part => part !== undefined) // Filter out unsupported blocks
|
||||
}
|
||||
|
||||
export function convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam): Content {
|
||||
|
||||
@@ -156,9 +156,14 @@ export function convertToOpenAiMessages(
|
||||
},
|
||||
}))
|
||||
|
||||
// Set content to blank when tool_calls are present but content has no text, per OpenAI API spec
|
||||
const hasToolCalls = tool_calls.length > 0
|
||||
const hasMeaningfulContent = content !== undefined && content.trim() !== ""
|
||||
const finalContent = hasMeaningfulContent ? content : hasToolCalls ? null : undefined
|
||||
|
||||
openAiMessages.push({
|
||||
role: "assistant",
|
||||
content,
|
||||
content: finalContent,
|
||||
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
|
||||
tool_calls: tool_calls?.length > 0 ? tool_calls : undefined,
|
||||
// @ts-ignore-next-line
|
||||
|
||||
@@ -32,7 +32,7 @@ const ESCAPE_MAP: Record<string, string> = {
|
||||
const ESCAPE_PATTERN = /\\[ntr"\\]/g
|
||||
|
||||
/**
|
||||
* Handles streaming tool use blocks and converts them to Anthropic.ToolUseBlockParam format
|
||||
* Handles streaming native tool use blocks and converts them to Anthropic.ToolUseBlockParam format
|
||||
*/
|
||||
export class ToolUseHandler {
|
||||
private pendingToolUses = new Map<string, PendingToolUse>()
|
||||
@@ -130,6 +130,7 @@ export class ToolUseHandler {
|
||||
arguments: JSON.stringify(input),
|
||||
},
|
||||
partial: true,
|
||||
isNativeToolCall: true,
|
||||
})
|
||||
} else {
|
||||
const params: Record<string, string> = {}
|
||||
@@ -143,6 +144,7 @@ export class ToolUseHandler {
|
||||
name: pending.name as ClineDefaultTool,
|
||||
params: params as any,
|
||||
partial: true,
|
||||
isNativeToolCall: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import {
|
||||
CLAUDE_SONNET_1M_SUFFIX,
|
||||
ModelInfo,
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -10,6 +15,7 @@ export async function createVercelAIGatewayStream(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
model: { id: string; info: ModelInfo },
|
||||
thinkingBudgetTokens?: number,
|
||||
tools?: OpenAITool[],
|
||||
) {
|
||||
// Convert Anthropic messages to OpenAI format
|
||||
@@ -18,9 +24,15 @@ export async function createVercelAIGatewayStream(
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
|
||||
if (isClaudeSonnet1m) {
|
||||
// remove the custom :1m suffix, to create the model id openrouter API expects
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
}
|
||||
|
||||
const isAnthropicModel = model.id.startsWith("anthropic/")
|
||||
|
||||
if (isAnthropicModel && model.info.supportsPromptCache) {
|
||||
if (isAnthropicModel) {
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
@@ -46,12 +58,32 @@ export async function createVercelAIGatewayStream(
|
||||
})
|
||||
}
|
||||
|
||||
// Configure reasoning parameters similar to OpenRouter
|
||||
let temperature: number | undefined = 0
|
||||
let reasoning: { max_tokens: number } | undefined
|
||||
|
||||
if (isAnthropicModel) {
|
||||
const budget_tokens = thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0
|
||||
if (reasoningOn) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: budget_tokens }
|
||||
}
|
||||
} else if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: thinkingBudgetTokens }
|
||||
}
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens,
|
||||
temperature: 0.7,
|
||||
temperature: temperature,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
include_reasoning: true,
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
|
||||
@@ -51,4 +51,6 @@ export interface ToolUse {
|
||||
// params is a partial record, allowing only some or none of the possible parameters to be used
|
||||
params: Partial<Record<ToolParamName, string>>
|
||||
partial: boolean
|
||||
// Whether this tool use was initiated by a native tool call
|
||||
isNativeToolCall?: boolean
|
||||
}
|
||||
|
||||
@@ -434,13 +434,6 @@ export class ContextManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Add any orphaned tool_results not in toolUseIds (shouldn't happen, but be safe)
|
||||
for (const [toolUseId, toolResult] of toolResultMap) {
|
||||
if (!processedToolResults.has(toolUseId)) {
|
||||
newContent.push(toolResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Add all other blocks
|
||||
newContent.push(...otherBlocks)
|
||||
|
||||
@@ -464,6 +457,21 @@ export class ContextManager {
|
||||
const secondChunk = messages.slice(startFromIndex) // get remaining messages within context
|
||||
const messagesToUpdate = [...firstChunk, ...secondChunk]
|
||||
|
||||
// Remove orphaned tool_results from the first message after truncation (if it's a user message)
|
||||
if (startFromIndex > 2 && messagesToUpdate.length > 2) {
|
||||
const firstMessageAfterTruncation = messagesToUpdate[2]
|
||||
if (firstMessageAfterTruncation.role === "user" && Array.isArray(firstMessageAfterTruncation.content)) {
|
||||
const hasToolResults = firstMessageAfterTruncation.content.some((block) => block.type === "tool_result")
|
||||
if (hasToolResults) {
|
||||
// Clone and filter out all tool_result blocks
|
||||
messagesToUpdate[2] = cloneDeep(firstMessageAfterTruncation)
|
||||
;(messagesToUpdate[2].content as Anthropic.Messages.ContentBlockParam[]) = (
|
||||
firstMessageAfterTruncation.content as Anthropic.Messages.ContentBlockParam[]
|
||||
).filter((block) => block.type !== "tool_result")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we need the mapping from the local indices in messagesToUpdate to the global array of updates in this.contextHistoryUpdates
|
||||
const originalIndices = [
|
||||
...Array(2).keys(),
|
||||
|
||||
@@ -153,5 +153,48 @@ describe("ContextManager", () => {
|
||||
expect(result[1].role).to.equal("assistant")
|
||||
expect(result[2].role).to.equal("user")
|
||||
})
|
||||
|
||||
it("removes orphaned tool_results after truncation", () => {
|
||||
// Create messages with tool_use and tool_result blocks
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: "Initial task" },
|
||||
{ role: "assistant", content: "Response 1" },
|
||||
// Assistant message with tool_use that will be truncated
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "Using a tool" },
|
||||
{ type: "tool_use", id: "tool_123", name: "read_file", input: { path: "test.ts" } },
|
||||
],
|
||||
},
|
||||
// User message with tool_result - should have tool_result removed after truncation
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "tool_result", tool_use_id: "tool_123", content: "file content here" },
|
||||
{ type: "text", text: "Additional user text" },
|
||||
],
|
||||
},
|
||||
{ role: "assistant", content: "Response 2" },
|
||||
]
|
||||
|
||||
// Truncate to remove the assistant message with tool_use
|
||||
const range: [number, number] = [2, 2]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
// Should have 4 messages (original 5 minus 1 truncated)
|
||||
expect(result).to.have.lengthOf(4)
|
||||
|
||||
// The user message at index 2 should have tool_result removed but text preserved
|
||||
const userMessageAfterTruncation = result[2]
|
||||
expect(userMessageAfterTruncation.role).to.equal("user")
|
||||
expect(Array.isArray(userMessageAfterTruncation.content)).to.be.true
|
||||
|
||||
const content = userMessageAfterTruncation.content as Anthropic.Messages.ContentBlockParam[]
|
||||
// Should only have the text block, not the tool_result
|
||||
expect(content).to.have.lengthOf(1)
|
||||
expect(content[0].type).to.equal("text")
|
||||
expect((content[0] as Anthropic.Messages.TextBlockParam).text).to.equal("Additional user text")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -40,7 +40,10 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
|
||||
if (await fileExistsAtPath(clineRulesFilePath)) {
|
||||
if (await isDirectory(clineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath, [[".clinerules", "workflows"]])
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath, [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "hooks"],
|
||||
])
|
||||
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
if (rulesFilesTotalContent) {
|
||||
@@ -84,6 +87,7 @@ export async function refreshClineRulesToggles(
|
||||
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "hooks"],
|
||||
])
|
||||
controller.stateManager.setWorkspaceState("localClineRulesToggles", updatedLocalToggles)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { toRequestyServiceUrl } from "@shared/providers/requesty"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { toRequestyServiceUrl } from "@/shared/clients/requesty"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { Controller } from ".."
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ export const cancelRecording = async (controller: Controller): Promise<Recording
|
||||
let errorMessage = ""
|
||||
let isSuccess = true
|
||||
try {
|
||||
// Clear the silence detection callback
|
||||
audioRecordingService.setSilenceDetectedCallback(null)
|
||||
|
||||
const result = await audioRecordingService.cancelRecording()
|
||||
isSuccess = !!result?.success
|
||||
errorMessage = result?.error ?? ""
|
||||
|
||||
@@ -117,6 +117,12 @@ export const startRecording = async (controller: Controller): Promise<RecordingR
|
||||
throw new Error("Please sign in to your Cline Account to use Dictation.")
|
||||
}
|
||||
|
||||
// Set up silence detection callback to auto-stop recording
|
||||
audioRecordingService.setSilenceDetectedCallback(async () => {
|
||||
// Automatically stop recording when silence is detected
|
||||
await audioRecordingService.stopRecording()
|
||||
})
|
||||
|
||||
// Attempt to start recording
|
||||
const result = await audioRecordingService.startRecording()
|
||||
|
||||
@@ -129,6 +135,9 @@ export const startRecording = async (controller: Controller): Promise<RecordingR
|
||||
})
|
||||
}
|
||||
|
||||
// Clear callback on failure
|
||||
audioRecordingService.setSilenceDetectedCallback(null)
|
||||
|
||||
// Check if the error is due to missing dependencies
|
||||
const platform = os.platform() as keyof typeof AUDIO_PROGRAM_CONFIG
|
||||
const config = AUDIO_PROGRAM_CONFIG[platform]
|
||||
|
||||
@@ -14,6 +14,9 @@ export const stopRecording = async (controller: Controller): Promise<RecordedAud
|
||||
const recordingDuration = recordingStatus.durationSeconds * 1000 // Convert to milliseconds
|
||||
|
||||
try {
|
||||
// Clear the silence detection callback
|
||||
audioRecordingService.setSilenceDetectedCallback(null)
|
||||
|
||||
const result = await audioRecordingService.stopRecording()
|
||||
|
||||
telemetryService.captureVoiceRecordingStopped(taskId, recordingDuration, result.success, process.platform)
|
||||
|
||||
@@ -19,8 +19,36 @@ export const transcribeAudio = async (controller: Controller, request: Transcrib
|
||||
telemetryService.captureVoiceTranscriptionStarted(taskId, request.language ?? "en")
|
||||
|
||||
try {
|
||||
// Transcribe the audio
|
||||
const result = await getVoiceTranscriptionService().transcribeAudio(request.audioBase64, request.language ?? "en")
|
||||
// Try ElevenLabs STT first if API key is available
|
||||
const apiKey = await controller.context.secrets.get("elevenLabsApiKey")
|
||||
let result: { text?: string; error?: string } | null = null
|
||||
|
||||
if (apiKey) {
|
||||
try {
|
||||
const { ElevenLabsProvider } = await import("@/services/tts/providers/ElevenLabsProvider")
|
||||
const provider = new ElevenLabsProvider(apiKey)
|
||||
|
||||
// Convert base64 to Buffer
|
||||
const audioBuffer = Buffer.from(request.audioBase64, "base64")
|
||||
result = await provider.transcribeAudio(audioBuffer, request.language ?? "en")
|
||||
|
||||
if (result.text) {
|
||||
console.log("ElevenLabs transcription successful")
|
||||
} else if (result.error) {
|
||||
console.warn("ElevenLabs transcription failed, falling back to Cline service:", result.error)
|
||||
result = null // Fall back to Cline service
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("ElevenLabs STT error, falling back to Cline service:", error)
|
||||
result = null // Fall back to Cline service
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to Cline transcription service if ElevenLabs failed or no API key
|
||||
if (!result) {
|
||||
result = await getVoiceTranscriptionService().transcribeAudio(request.audioBase64, request.language ?? "en")
|
||||
}
|
||||
|
||||
const durationMs = Date.now() - startTime
|
||||
|
||||
if (result.error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user