mirror of
https://github.com/cline/cline.git
synced 2026-09-11 16:42:40 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7190d0fe10 | ||
|
|
b5d0f6faf9 | ||
|
|
7c3909d8e8 | ||
|
|
48abfd4536 | ||
|
|
9c4a18b170 | ||
|
|
e99e5b990e | ||
|
|
9ed7ef54dd | ||
|
|
59bcb08466 | ||
|
|
72d8d53d1f | ||
|
|
309e9546f1 | ||
|
|
dd3fda8a3b | ||
|
|
51cfd464cd | ||
|
|
7ced24fd13 | ||
|
|
21dae24b0a | ||
|
|
92eb399c33 | ||
|
|
b4590f2a21 | ||
|
|
ce72bb6b3a | ||
|
|
dfd113a6e5 | ||
|
|
3698d2356c | ||
|
|
ff20c4addc | ||
|
|
8eeeabb966 | ||
|
|
9b1dc5bd92 | ||
|
|
1cfff0a45f | ||
|
|
e7d00dec2d | ||
|
|
2c5748ccfd | ||
|
|
5196adce33 | ||
|
|
d95d86f329 | ||
|
|
f6eb3aa386 | ||
|
|
5c4b9e54c2 | ||
|
|
29a1d08685 | ||
|
|
9664ddd106 | ||
|
|
3c1327b115 | ||
|
|
bb993e4a9a | ||
|
|
473b3d0204 | ||
|
|
d6529a81e8 | ||
|
|
7e68614631 | ||
|
|
52e621bfa1 | ||
|
|
a19e9907d5 | ||
|
|
19f74cac01 | ||
|
|
b877abc708 | ||
|
|
c90e64e763 | ||
|
|
e02e1eca7f | ||
|
|
f8925fb8fd | ||
|
|
ac81aeaf4e | ||
|
|
f12b5a1573 | ||
|
|
e110259167 | ||
|
|
8ef3a3b735 | ||
|
|
a66b57ef51 | ||
|
|
944ed41f4a | ||
|
|
970e941e57 | ||
|
|
45abe977e4 | ||
|
|
5755b30bce | ||
|
|
5fde21dffd | ||
|
|
cf9f2a8630 | ||
|
|
5a3416ff09 | ||
|
|
826b2b1276 |
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added support for reinitializing the telemetry service and all providers inside of it.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Fixed a bug and set up how environmental variables are preceded and overwritten by the remote configuration settings.
|
||||
@@ -1,54 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse Hook Example
|
||||
#
|
||||
# This hook runs AFTER a tool is executed. It can:
|
||||
# 1. Observe tool results and outcomes
|
||||
# 2. Add context for FUTURE tool uses via contextModification
|
||||
# 3. Log or track tool usage patterns
|
||||
#
|
||||
# IMPORTANT: Context injection affects FUTURE AI decisions, not the current tool execution.
|
||||
# The tool has already completed when this hook runs.
|
||||
|
||||
# Read the hook input (JSON via stdin)
|
||||
input=$(cat)
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
# Extract tool information
|
||||
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName // "unknown"')
|
||||
parameters=$(echo "$input" | jq -r '.postToolUse.parameters // {}')
|
||||
result=$(echo "$input" | jq -r '.postToolUse.result // ""')
|
||||
success=$(echo "$input" | jq -r '.postToolUse.success // false')
|
||||
execution_time=$(echo "$input" | jq -r '.postToolUse.executionTimeMs // 0')
|
||||
sleep 3
|
||||
|
||||
# Example 1: Learning from file operations
|
||||
# Track successful file creations to build context about project structure
|
||||
# if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
|
||||
# path=$(echo "$parameters" | jq -r '.path // ""')
|
||||
# cat <<EOF
|
||||
# {
|
||||
# "shouldContinue": true,
|
||||
# "contextModification": "FILE_OPERATIONS: Successfully created '$path'. Future operations should maintain consistency with this file's patterns and structure."
|
||||
# }
|
||||
# EOF
|
||||
# exit 0
|
||||
# fi
|
||||
|
||||
# Example 2: Performance monitoring
|
||||
# Warn about slow operations
|
||||
# if [[ "$execution_time" -gt 5000 ]]; then
|
||||
# cat <<EOF
|
||||
# {
|
||||
# "shouldContinue": true,
|
||||
# "contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms to complete. Consider optimizing future similar operations or breaking them into smaller steps."
|
||||
# }
|
||||
# EOF
|
||||
# exit 0
|
||||
# fi
|
||||
|
||||
# Example 3: Context injection for future tool uses
|
||||
# The context will be available in the NEXT API request
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "TOOL_RESULT: The tool '$tool_name' completed with success=$success. Consider validating the results before proceeding to the next step."
|
||||
"cancel": false,
|
||||
"contextModification": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "PostToolUse hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -1,42 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse Hook Example
|
||||
#
|
||||
# This hook runs BEFORE a tool is executed. It can:
|
||||
# 1. Block execution by returning {"shouldContinue": false}
|
||||
# 2. Add context for FUTURE tool uses via contextModification
|
||||
# 3. Validate tool parameters
|
||||
#
|
||||
# IMPORTANT: Context injection affects FUTURE AI decisions, not the current tool execution.
|
||||
# The tool parameters are already determined when this hook runs.
|
||||
|
||||
# Read the hook input (JSON via stdin)
|
||||
input=$(cat)
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
# Extract tool information
|
||||
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName // "unknown"')
|
||||
parameters=$(echo "$input" | jq -r '.preToolUse.parameters // {}')
|
||||
sleep 3
|
||||
|
||||
# Example 1: Validation - Block invalid operations
|
||||
# Uncomment to prevent creating .js files in a TypeScript project
|
||||
# if [[ "$tool_name" == "write_to_file" ]]; then
|
||||
# path=$(echo "$parameters" | jq -r '.path // ""')
|
||||
# if [[ "$path" == *.js ]]; then
|
||||
# cat <<EOF
|
||||
# {
|
||||
# "shouldContinue": false,
|
||||
# "errorMessage": "VALIDATION FAILED: Cannot create .js files in TypeScript project. Please use .ts extension instead.",
|
||||
# "contextModification": "WORKSPACE_RULES: This is a strict TypeScript project. All new files must use .ts or .tsx extensions."
|
||||
# }
|
||||
# EOF
|
||||
# exit 0
|
||||
# fi
|
||||
# fi
|
||||
|
||||
# Example 2: Context injection for future tool uses
|
||||
# The context will be available in the NEXT API request after this tool completes
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"contextModification": "WORKSPACE_RULES: [For future tool uses] This is a TypeScript React project. When creating files, use .ts/.tsx extensions and include detailed comments explaining the purpose and usage of each function."
|
||||
"cancel": false,
|
||||
"contextModification": "Make sure that all variables in python code in this repo always start with the string 'EVIEDEBUG_'. This is important.",
|
||||
"errorMessage": "PreToolUse hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
|
||||
+109
-28
@@ -17,6 +17,37 @@ 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/Rules/Hooks/TaskStart` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskStart` (all platforms)
|
||||
|
||||
### TaskResume Hook
|
||||
- **When**: Runs when an EXISTING task is resumed (after user clicks resume button)
|
||||
- **Purpose**: Validate resumed task state, restore context, check for changes since last run
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/TaskResume` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskResume` (all platforms)
|
||||
|
||||
### TaskCancel Hook
|
||||
- **When**: Runs when a task is cancelled by the user (only if there's actual active work or work was started)
|
||||
- **Purpose**: Clean up resources, log cancellation, save state
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/TaskCancel` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskCancel` (all platforms)
|
||||
- **Note**: This hook is NOT cancellable and will complete even if the task is being aborted
|
||||
|
||||
### TaskComplete Hook
|
||||
- **When**: Runs when a task is marked as complete
|
||||
- **Purpose**: Log completion status, perform final cleanup, generate reports
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/TaskComplete` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/TaskComplete` (all platforms)
|
||||
|
||||
### UserPromptSubmit Hook
|
||||
- **When**: Runs when the user submits a prompt/message (initial task, resume, or feedback)
|
||||
- **Purpose**: Validate user input, preprocess prompts, add context to user messages
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/UserPromptSubmit` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/UserPromptSubmit` (all platforms)
|
||||
|
||||
### PreToolUse Hook
|
||||
- **When**: Runs BEFORE a tool is executed
|
||||
- **Purpose**: Validate parameters, block execution, or add context
|
||||
@@ -29,6 +60,12 @@ Hooks run automatically when enabled.
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/PostToolUse` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/PostToolUse` (all platforms)
|
||||
|
||||
### PreCompact Hook
|
||||
- **When**: Runs BEFORE the conversation context is compacted/truncated
|
||||
- **Purpose**: Observe compaction events, log context management, track token usage
|
||||
- **Global Location**: `~/Documents/Cline/Rules/Hooks/PreCompact` (all platforms)
|
||||
- **Workspace Location**: `.clinerules/hooks/PreCompact` (all platforms)
|
||||
|
||||
## Cross-Platform Hook Format
|
||||
|
||||
Cline uses a git-style approach for hooks that works consistently across all platforms:
|
||||
@@ -107,11 +144,46 @@ All hooks receive:
|
||||
```json
|
||||
{
|
||||
"clineVersion": "string",
|
||||
"hookName": "PreToolUse" | "PostToolUse",
|
||||
"hookName": "TaskStart" | "TaskResume" | "TaskCancel" | "TaskComplete" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PreCompact",
|
||||
"timestamp": "string",
|
||||
"taskId": "string",
|
||||
"workspaceRoots": ["string"],
|
||||
"userId": "string",
|
||||
"taskStart": { // Only for TaskStart
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string",
|
||||
"initialTask": "string"
|
||||
}
|
||||
},
|
||||
"taskResume": { // Only for TaskResume
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string"
|
||||
},
|
||||
"previousState": {
|
||||
"lastMessageTs": "string",
|
||||
"messageCount": "string",
|
||||
"conversationHistoryDeleted": "string"
|
||||
}
|
||||
},
|
||||
"taskCancel": { // Only for TaskCancel
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string",
|
||||
"completionStatus": "string"
|
||||
}
|
||||
},
|
||||
"taskComplete": { // Only for TaskComplete
|
||||
"taskMetadata": {
|
||||
"taskId": "string",
|
||||
"ulid": "string"
|
||||
}
|
||||
},
|
||||
"userPromptSubmit": { // Only for UserPromptSubmit
|
||||
"prompt": "string",
|
||||
"attachments": ["string"]
|
||||
},
|
||||
"preToolUse": { // Only for PreToolUse
|
||||
"toolName": "string",
|
||||
"parameters": {}
|
||||
@@ -122,6 +194,11 @@ All hooks receive:
|
||||
"result": "string",
|
||||
"success": boolean,
|
||||
"executionTimeMs": number
|
||||
},
|
||||
"preCompact": { // Only for PreCompact
|
||||
"contextSize": number,
|
||||
"messagesToCompact": number,
|
||||
"compactionStrategy": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -131,12 +208,16 @@ All hooks receive:
|
||||
All hooks must return:
|
||||
```json
|
||||
{
|
||||
"shouldContinue": boolean, // Required: Allow or block execution
|
||||
"contextModification": "string", // Optional: Context for future tool uses
|
||||
"cancel": boolean, // Required: false to continue, true to block execution
|
||||
"contextModification": "string", // Optional: Context for future AI decisions
|
||||
"errorMessage": "string" // Optional: Error details if blocking
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `cancel` field works as follows:
|
||||
- `false` (or omitted): Allow execution to continue
|
||||
- `true`: Block execution and show error message to user
|
||||
|
||||
## Context Modification Format
|
||||
|
||||
Use structured prefixes to help the AI understand context type:
|
||||
@@ -152,7 +233,7 @@ Example:
|
||||
```bash
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"cancel": false,
|
||||
"contextModification": "WORKSPACE_RULES: This is a TypeScript project. All new files must use .ts or .tsx extensions."
|
||||
}
|
||||
EOF
|
||||
@@ -160,9 +241,9 @@ EOF
|
||||
|
||||
## Hook Execution Limits
|
||||
|
||||
- **Timeout**: Hooks must complete within 30 seconds
|
||||
- **Context Size**: Context modifications are limited to 50KB
|
||||
- **Error Handling**: Unexpected file system errors are propagated; expected errors (file not found, permission denied) are handled silently
|
||||
- **Timeout**: Hooks must complete within 30 seconds (configurable via `HOOK_EXECUTION_TIMEOUT_MS`)
|
||||
- **Context Size**: Context modifications are limited to 50KB (configurable via `MAX_CONTEXT_MODIFICATION_SIZE`)
|
||||
- **Error Handling**: Expected errors (file not found, permission denied, not a directory) are handled silently; unexpected file system errors are propagated
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
@@ -177,7 +258,7 @@ path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
|
||||
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": false,
|
||||
"cancel": true,
|
||||
"errorMessage": "Cannot create .js files in TypeScript project",
|
||||
"contextModification": "WORKSPACE_RULES: Use .ts/.tsx extensions only"
|
||||
}
|
||||
@@ -185,7 +266,7 @@ EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
### 2. Context Building - Learn from Operations
|
||||
@@ -200,12 +281,12 @@ path=$(echo "$input" | jq -r '.postToolUse.parameters.path // ""')
|
||||
if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"cancel": false,
|
||||
"contextModification": "FILE_OPERATIONS: Created '$path'. Maintain consistency with this file's patterns in future operations."
|
||||
}
|
||||
EOF
|
||||
else
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -220,12 +301,12 @@ tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
|
||||
if [[ "$execution_time" -gt 5000 ]]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"shouldContinue": true,
|
||||
"cancel": false,
|
||||
"contextModification": "PERFORMANCE: Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
|
||||
}
|
||||
EOF
|
||||
else
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -239,7 +320,7 @@ input=$(cat)
|
||||
echo "$input" >> ~/.cline/hook-logs/tool-usage.jsonl
|
||||
|
||||
# Allow execution
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
## Global vs Workspace Hooks
|
||||
@@ -250,26 +331,26 @@ Cline supports two levels of hooks:
|
||||
- **Location**: `~/Documents/Cline/Rules/Hooks/` (macOS/Linux) or `%USERPROFILE%\Documents\Cline\Rules\Hooks\` (Windows)
|
||||
- **Scope**: Apply to ALL workspaces and projects
|
||||
- **Use Case**: Organization-wide policies, personal preferences, universal validations
|
||||
- **Priority**: 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
|
||||
|
||||
@@ -301,11 +382,11 @@ tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
|
||||
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
|
||||
|
||||
if [[ "$tool_name" == "write_to_file" && "$path" == *"package.json"* ]]; then
|
||||
echo '{"shouldContinue": false, "errorMessage": "Global policy: Cannot modify package.json"}'
|
||||
echo '{"cancel": true, "errorMessage": "Global policy: Cannot modify package.json"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
**Workspace Hook** (applies to specific project):
|
||||
@@ -318,11 +399,11 @@ tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
|
||||
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
|
||||
|
||||
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
|
||||
echo '{"shouldContinue": false, "errorMessage": "Project rule: Use .ts files only"}'
|
||||
echo '{"cancel": true, "errorMessage": "Project rule: Use .ts files only"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo '{"shouldContinue": true}'
|
||||
echo '{"cancel": false}'
|
||||
```
|
||||
|
||||
**All hooks must allow execution for the tool to proceed.** Hooks may execute concurrently.
|
||||
@@ -331,7 +412,7 @@ echo '{"shouldContinue": true}'
|
||||
|
||||
If you have multiple workspace roots, you can place hooks in each root's `.clinerules/hooks/` directory. All hooks (global and workspace) may execute concurrently. Their results will be combined:
|
||||
|
||||
- **shouldContinue**: If ANY hook returns false, execution is blocked
|
||||
- **cancel**: If ANY hook returns `true`, execution is blocked
|
||||
- **contextModification**: All context modifications are concatenated
|
||||
- **errorMessage**: All error messages are concatenated
|
||||
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
sleep 3
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel", false,
|
||||
"contextModification": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "TaskCancel hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
sleep 3
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "TaskResume hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
sleep 1
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "TaskStart hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for i in {1..100}; do
|
||||
echo "$i"
|
||||
done
|
||||
|
||||
sleep 3
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cancel": false,
|
||||
"contextModification": "WORKSPACE_RULES: This is from the local bar/ workspace.",
|
||||
"errorMessage": "UserPromptSubmit hook custom errorMessage: foo"
|
||||
}
|
||||
EOF
|
||||
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
@@ -123,7 +123,10 @@ func setCommand() *cobra.Command {
|
||||
Use: "set <key=value> [key=value...]",
|
||||
Aliases: []string{"s"},
|
||||
Short: "Set configuration variables",
|
||||
Long: `Set one or more global configuration variables using key=value format.`,
|
||||
Long: `Set one or more global configuration variables using key=value format.
|
||||
|
||||
This command merges the provided settings with existing values, preserving
|
||||
unspecified fields. Only the fields you explicitly set will be updated.`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
@@ -139,7 +142,7 @@ func setCommand() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update settings
|
||||
// Update settings (server-side merge handles preserving existing values)
|
||||
return configManager.UpdateSettings(ctx, settings, secrets)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ func renderAutoApprovalSettings(value interface{}, censor bool) error {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Print other fields normally (enabled, maxRequests, enableNotifications, favorites)
|
||||
// Print other fields normally (enabled, enableNotifications, favorites)
|
||||
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ func (h *AskHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
return h.handleResumeCompletedTask(msg, dc)
|
||||
case string(types.AskTypeMistakeLimitReached):
|
||||
return h.handleMistakeLimitReached(msg, dc)
|
||||
case string(types.AskTypeAutoApprovalMaxReached):
|
||||
return h.handleAutoApprovalMaxReached(msg, dc)
|
||||
case string(types.AskTypeBrowserActionLaunch):
|
||||
return h.handleBrowserActionLaunch(msg, dc)
|
||||
case string(types.AskTypeUseMcpServer):
|
||||
@@ -255,25 +253,6 @@ func (h *AskHandler) handleMistakeLimitReached(msg *types.ClineMessage, dc *Disp
|
||||
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("Mistake Limit Reached: %s. Approval required.", msg.Text), true)
|
||||
}
|
||||
|
||||
// handleAutoApprovalMaxReached handles auto-approval max reached
|
||||
func (h *AskHandler) handleAutoApprovalMaxReached(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
if dc.SystemRenderer != nil {
|
||||
details := make(map[string]string)
|
||||
if msg.Text != "" {
|
||||
details["reason"] = msg.Text
|
||||
}
|
||||
dc.SystemRenderer.RenderError(
|
||||
"warning",
|
||||
"Auto-Approval Limit Reached",
|
||||
"The maximum number of auto-approved requests has been reached. Manual approval is now required.",
|
||||
details,
|
||||
)
|
||||
fmt.Printf("\n**Approval required to continue.**\n")
|
||||
return nil
|
||||
}
|
||||
return dc.Renderer.RenderMessage("WARNING", fmt.Sprintf("Auto-approval limit reached: %s. Approval required.", msg.Text), true)
|
||||
}
|
||||
|
||||
// handleBrowserActionLaunch handles browser action launch requests
|
||||
func (h *AskHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
url := strings.TrimSpace(msg.Text)
|
||||
|
||||
@@ -282,7 +282,6 @@ func (m *Manager) CheckSendEnabled(ctx context.Context) error {
|
||||
errorTypes := []string{
|
||||
string(types.AskTypeAPIReqFailed), // "api_req_failed"
|
||||
string(types.AskTypeMistakeLimitReached), // "mistake_limit_reached"
|
||||
string(types.AskTypeAutoApprovalMaxReached), // "auto_approval_max_req_reached"
|
||||
}
|
||||
|
||||
isError := false
|
||||
@@ -1239,16 +1238,16 @@ func (m *Manager) updateMode(stateJson string) {
|
||||
|
||||
// UpdateTaskAutoApprovalAction enables a specific auto-approval action for the current task
|
||||
func (m *Manager) UpdateTaskAutoApprovalAction(ctx context.Context, actionKey string) error {
|
||||
boolPtr := func(b bool) *bool { return &b }
|
||||
|
||||
settings := &cline.Settings{
|
||||
AutoApprovalSettings: &cline.AutoApprovalSettings{
|
||||
Enabled: true,
|
||||
MaxRequests: 20, // Important: avoid maxRequests=0 bug
|
||||
Actions: &cline.AutoApprovalActions{},
|
||||
Actions: &cline.AutoApprovalActions{},
|
||||
},
|
||||
}
|
||||
|
||||
// Set the specific action to true based on actionKey
|
||||
truePtr := func() *bool { b := true; return &b }()
|
||||
truePtr := boolPtr(true)
|
||||
|
||||
switch actionKey {
|
||||
case "read_files":
|
||||
|
||||
@@ -416,24 +416,12 @@ func setNestedField(settings *cline.Settings, parentField string, childFields ma
|
||||
func setAutoApprovalSettings(settings *cline.AutoApprovalSettings, fields map[string]string) error {
|
||||
for key, value := range fields {
|
||||
switch key {
|
||||
case "enabled":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.Enabled = val
|
||||
case "max_requests":
|
||||
val, err := parseInt32(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.MaxRequests = val
|
||||
case "enable_notifications":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.EnableNotifications = val
|
||||
settings.EnableNotifications = boolPtr(val)
|
||||
case "actions":
|
||||
return fmt.Errorf("auto_approval_settings.actions requires nested dot notation (e.g., auto-approval-settings.actions.read-files=true)")
|
||||
default:
|
||||
|
||||
@@ -37,17 +37,16 @@ const (
|
||||
type AskType string
|
||||
|
||||
const (
|
||||
AskTypeFollowup AskType = "followup"
|
||||
AskTypePlanModeRespond AskType = "plan_mode_respond"
|
||||
AskTypeCommand AskType = "command"
|
||||
AskTypeCommandOutput AskType = "command_output"
|
||||
AskTypeCompletionResult AskType = "completion_result"
|
||||
AskTypeTool AskType = "tool"
|
||||
AskTypeAPIReqFailed AskType = "api_req_failed"
|
||||
AskTypeResumeTask AskType = "resume_task"
|
||||
AskTypeResumeCompletedTask AskType = "resume_completed_task"
|
||||
AskTypeMistakeLimitReached AskType = "mistake_limit_reached"
|
||||
AskTypeAutoApprovalMaxReached AskType = "auto_approval_max_req_reached"
|
||||
AskTypeFollowup AskType = "followup"
|
||||
AskTypePlanModeRespond AskType = "plan_mode_respond"
|
||||
AskTypeCommand AskType = "command"
|
||||
AskTypeCommandOutput AskType = "command_output"
|
||||
AskTypeCompletionResult AskType = "completion_result"
|
||||
AskTypeTool AskType = "tool"
|
||||
AskTypeAPIReqFailed AskType = "api_req_failed"
|
||||
AskTypeResumeTask AskType = "resume_task"
|
||||
AskTypeResumeCompletedTask AskType = "resume_completed_task"
|
||||
AskTypeMistakeLimitReached AskType = "mistake_limit_reached"
|
||||
AskTypeBrowserActionLaunch AskType = "browser_action_launch"
|
||||
AskTypeUseMcpServer AskType = "use_mcp_server"
|
||||
AskTypeNewTask AskType = "new_task"
|
||||
@@ -247,8 +246,6 @@ func convertProtoAskType(askType cline.ClineAsk) string {
|
||||
return string(AskTypeResumeCompletedTask)
|
||||
case cline.ClineAsk_MISTAKE_LIMIT_REACHED:
|
||||
return string(AskTypeMistakeLimitReached)
|
||||
case cline.ClineAsk_AUTO_APPROVAL_MAX_REQ_REACHED:
|
||||
return string(AskTypeAutoApprovalMaxReached)
|
||||
case cline.ClineAsk_BROWSER_ACTION_LAUNCH:
|
||||
return string(AskTypeBrowserActionLaunch)
|
||||
case cline.ClineAsk_USE_MCP_SERVER:
|
||||
|
||||
@@ -42,17 +42,17 @@ To open Cline in the right sidebar:
|
||||
4. Set the value to `vertical`
|
||||
5. Restart Cursor for the changes to take effect
|
||||
</Step>
|
||||
<Step title="Open Agent Panel">
|
||||
Click the Cursor cube icon button that opens Cursor's agent (right side view panel)
|
||||
<Step title="Open the AI Pane">
|
||||
Click the Cursor cube icon button (AI Pane) that opens Cursor's agent (right side view panel)
|
||||
</Step>
|
||||
<Step title="Drag to Three Dots">
|
||||
Drag the Cline icon directly onto the three dots button - it doesn't work if you just drag it to the top, it has to be the three dots
|
||||
<Step title="Drag Cline to the AI Pane Sidebar">
|
||||
Drag the Cline icon directly into the AI Pane sidebar.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/cursor-side-bar.gif"
|
||||
src="https://storage.googleapis.com/cline_public_images/Cursor-sidebar.gif"
|
||||
alt="Cursor Right Sidebar Setup"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
Generated
+272
-59
@@ -10,7 +10,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.0",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"chalk": "5.6.2",
|
||||
"cline": "^1.0.1",
|
||||
"commander": "^9.4.1",
|
||||
@@ -211,6 +211,111 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
|
||||
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react-native-b4a": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-b4a": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz",
|
||||
"integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.4.11",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.11.tgz",
|
||||
"integrity": "sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-stream": "^2.6.4",
|
||||
"bare-url": "^2.2.2",
|
||||
"fast-fifo": "^1.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-os": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
|
||||
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"bare": ">=1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-stream": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
|
||||
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"streamx": "^2.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-events": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-events": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-url": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.1.tgz",
|
||||
"integrity": "sha512-v2yl0TnaZTdEnelkKtXZGnotiV6qATBlnNuUMrHl6v9Lmmrh9mw9RYyImPU7/4RahumSwQS1k2oKXcRfXcbjJw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -231,13 +336,17 @@
|
||||
]
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
|
||||
"version": "12.4.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
|
||||
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x || 22.x || 23.x || 24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
@@ -305,11 +414,6 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -1450,6 +1554,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events-universal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
@@ -1480,6 +1593,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@@ -1520,11 +1639,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -2131,6 +2245,17 @@
|
||||
"resolved": "https://registry.npmjs.org/sqlite/-/sqlite-4.2.1.tgz",
|
||||
"integrity": "sha512-Tll0Ndvnwkuv5Hn6WIbh26rZiYQORuH1t5m/or9LUpSmDmmyFG89G9fKrSeugMPxwmEIXoVxqTun4LbizTs4uw=="
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
|
||||
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events-universal": "^1.0.0",
|
||||
"fast-fifo": "^1.3.2",
|
||||
"text-decoder": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -2192,29 +2317,37 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
||||
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
"tar-stream": "^3.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-fs": "^4.0.1",
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"b4a": "^1.6.4",
|
||||
"fast-fifo": "^1.2.0",
|
||||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/text-decoder": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
||||
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tiktoken": {
|
||||
@@ -2577,15 +2710,73 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"b4a": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
|
||||
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"bare-events": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz",
|
||||
"integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==",
|
||||
"requires": {}
|
||||
},
|
||||
"bare-fs": {
|
||||
"version": "4.4.11",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.11.tgz",
|
||||
"integrity": "sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-stream": "^2.6.4",
|
||||
"bare-url": "^2.2.2",
|
||||
"fast-fifo": "^1.3.2"
|
||||
}
|
||||
},
|
||||
"bare-os": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
|
||||
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
|
||||
"optional": true
|
||||
},
|
||||
"bare-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"bare-stream": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
|
||||
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"streamx": "^2.21.0"
|
||||
}
|
||||
},
|
||||
"bare-url": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.1.tgz",
|
||||
"integrity": "sha512-v2yl0TnaZTdEnelkKtXZGnotiV6qATBlnNuUMrHl6v9Lmmrh9mw9RYyImPU7/4RahumSwQS1k2oKXcRfXcbjJw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"better-sqlite3": {
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
|
||||
"version": "12.4.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
|
||||
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
@@ -2632,11 +2823,6 @@
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -3328,6 +3514,14 @@
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="
|
||||
},
|
||||
"events-universal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||
"requires": {
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
@@ -3349,6 +3543,11 @@
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
|
||||
},
|
||||
"fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@@ -3371,11 +3570,6 @@
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -3659,7 +3853,7 @@
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tar-fs": "^3.1.1",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
}
|
||||
},
|
||||
@@ -3760,6 +3954,16 @@
|
||||
"resolved": "https://registry.npmjs.org/sqlite/-/sqlite-4.2.1.tgz",
|
||||
"integrity": "sha512-Tll0Ndvnwkuv5Hn6WIbh26rZiYQORuH1t5m/or9LUpSmDmmyFG89G9fKrSeugMPxwmEIXoVxqTun4LbizTs4uw=="
|
||||
},
|
||||
"streamx": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
|
||||
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
|
||||
"requires": {
|
||||
"events-universal": "^1.0.0",
|
||||
"fast-fifo": "^1.3.2",
|
||||
"text-decoder": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -3805,26 +4009,32 @@
|
||||
}
|
||||
},
|
||||
"tar-fs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
||||
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"bare-fs": "^4.0.1",
|
||||
"bare-path": "^3.0.0",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
"tar-stream": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||
"requires": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
"b4a": "^1.6.4",
|
||||
"fast-fifo": "^1.2.0",
|
||||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"text-decoder": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
||||
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
|
||||
"requires": {
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"tiktoken": {
|
||||
@@ -3969,5 +4179,8 @@
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"tar-fs": ">=2.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
+45
-43
@@ -1,45 +1,47 @@
|
||||
{
|
||||
"name": "cline-evals",
|
||||
"version": "0.1.0",
|
||||
"description": "Evaluation scripts and tools for Cline",
|
||||
"main": "cli/dist/index.js",
|
||||
"scripts": {
|
||||
"build:cli": "cd cli && tsc",
|
||||
"start:cli": "cd cli && node dist/index.js",
|
||||
"dev:cli": "cd cli && ts-node src/index.ts",
|
||||
"diff-eval": "./diff-edits/run_and_open_dashboard.sh",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"evaluation",
|
||||
"benchmark",
|
||||
"diff-edits"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.0",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"chalk": "5.6.2",
|
||||
"dotenv": "^16.5.0",
|
||||
"commander": "^9.4.1",
|
||||
"execa": "^5.1.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"ora": "^5.4.1",
|
||||
"sqlite": "^4.1.2",
|
||||
"tiktoken": "^1.0.21",
|
||||
"uuid": "^9.0.0",
|
||||
"yargs": "^17.6.2",
|
||||
"cline": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.3",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/yargs": "^17.0.19",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
"name": "cline-evals",
|
||||
"version": "0.1.0",
|
||||
"description": "Evaluation scripts and tools for Cline",
|
||||
"main": "cli/dist/index.js",
|
||||
"scripts": {
|
||||
"build:cli": "cd cli && tsc",
|
||||
"start:cli": "cd cli && node dist/index.js",
|
||||
"dev:cli": "cd cli && ts-node src/index.ts",
|
||||
"diff-eval": "./diff-edits/run_and_open_dashboard.sh",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"evaluation",
|
||||
"benchmark",
|
||||
"diff-edits"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"chalk": "5.6.2",
|
||||
"dotenv": "^16.5.0",
|
||||
"commander": "^9.4.1",
|
||||
"execa": "^5.1.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"ora": "^5.4.1",
|
||||
"sqlite": "^4.1.2",
|
||||
"tiktoken": "^1.0.21",
|
||||
"uuid": "^9.0.0",
|
||||
"yargs": "^17.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.3",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/yargs": "^17.0.19",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"overrides": {
|
||||
"tar-fs": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+30
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.34.1",
|
||||
"version": "3.35.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.34.1",
|
||||
"version": "3.35.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -80,6 +80,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",
|
||||
@@ -15190,6 +15191,33 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/p-mutex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-mutex/-/p-mutex-1.0.0.tgz",
|
||||
"integrity": "sha512-UlthGzEMsg2VnZAR58wkzL7muskxtNamoTR1Q6/VYBUKqPaMM+YtSncjWIvyjfUvVECKck1SYC/4XIWWJU3gBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yocto-queue": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-mutex/node_modules/yocto-queue": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
|
||||
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-timeout": {
|
||||
"version": "6.1.4",
|
||||
"license": "MIT",
|
||||
|
||||
+4
-2
@@ -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.35.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -310,6 +310,7 @@
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
|
||||
"protos": "node scripts/build-proto.mjs",
|
||||
"protos-go": "node scripts/build-go-proto.mjs",
|
||||
"protos-python": "node scripts/build-python-proto.mjs",
|
||||
"cli-providers": "node scripts/cli-providers.mjs",
|
||||
"download-ripgrep": "node scripts/download-ripgrep.mjs",
|
||||
"postprotos": "biome format src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --no-errors-on-unmatched",
|
||||
@@ -329,7 +330,7 @@
|
||||
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha # Use `npm run test:unit --update-snapshots` to rebuild prompt snapshots",
|
||||
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
|
||||
"test:tp-orchestrator": "npx tsx scripts/testing-platform-orchestrator.ts",
|
||||
@@ -473,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",
|
||||
|
||||
@@ -40,6 +40,8 @@ service AccountService {
|
||||
|
||||
rpc openrouterAuthClicked(EmptyRequest) returns (Empty);
|
||||
|
||||
rpc requestyAuthClicked(StringRequest) returns (Empty);
|
||||
|
||||
// Returns a link the webview can use to redirect back to the user's IDE.
|
||||
rpc getRedirectUrl(EmptyRequest) returns (String);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+170
-2
@@ -29,8 +29,10 @@ service ModelsService {
|
||||
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Subscribe to OpenRouter models updates
|
||||
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
|
||||
// Updates API configuration
|
||||
// Updates API configuration (legacy - uses combined configuration)
|
||||
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
|
||||
// Updates API configuration (new - uses separate options and secrets)
|
||||
rpc updateApiConfiguration(UpdateApiConfigurationRequestNew) returns (Empty);
|
||||
// Updates API configuration with partial values (only updates fields that are explicitly set)
|
||||
rpc updateApiConfigurationPartial(UpdateApiConfigurationPartialRequest) returns (Empty);
|
||||
// Refreshes and returns Groq models
|
||||
@@ -92,6 +94,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
|
||||
@@ -128,12 +131,177 @@ message SapAiCoreModelsResponse {
|
||||
bool orchestration_available = 2;
|
||||
}
|
||||
|
||||
// Request for updating API configuration
|
||||
// API secrets (credentials, API keys)
|
||||
message ModelsApiSecrets {
|
||||
optional string api_key = 1;
|
||||
optional string cline_api_key = 2;
|
||||
optional string lite_llm_api_key = 3;
|
||||
optional string open_router_api_key = 4;
|
||||
optional string aws_access_key = 5;
|
||||
optional string aws_secret_key = 6;
|
||||
optional string aws_session_token = 7;
|
||||
optional string aws_bedrock_api_key = 8;
|
||||
optional string open_ai_api_key = 9;
|
||||
optional string ollama_api_key = 10;
|
||||
optional string gemini_api_key = 11;
|
||||
optional string open_ai_native_api_key = 12;
|
||||
optional string deep_seek_api_key = 13;
|
||||
optional string requesty_api_key = 14;
|
||||
optional string together_api_key = 15;
|
||||
optional string fireworks_api_key = 16;
|
||||
optional string qwen_api_key = 17;
|
||||
optional string doubao_api_key = 18;
|
||||
optional string mistral_api_key = 19;
|
||||
optional string nebius_api_key = 20;
|
||||
optional string asksage_api_key = 21;
|
||||
optional string xai_api_key = 22;
|
||||
optional string sambanova_api_key = 23;
|
||||
optional string cerebras_api_key = 24;
|
||||
optional string sap_ai_core_client_id = 25;
|
||||
optional string sap_ai_core_client_secret = 26;
|
||||
optional string moonshot_api_key = 27;
|
||||
optional string cline_account_id = 28;
|
||||
optional string groq_api_key = 29;
|
||||
optional string hugging_face_api_key = 30;
|
||||
optional string huawei_cloud_maas_api_key = 31;
|
||||
optional string baseten_api_key = 32;
|
||||
optional string zai_api_key = 33;
|
||||
optional string vercel_ai_gateway_api_key = 34;
|
||||
optional string dify_api_key = 35;
|
||||
optional string oca_api_key = 36;
|
||||
optional string oca_refresh_token = 37;
|
||||
optional string minimax_api_key = 38;
|
||||
}
|
||||
|
||||
// API configuration options (non-secret settings)
|
||||
message ModelsApiOptions {
|
||||
// Global configuration fields (not mode-specific)
|
||||
optional string ulid = 1;
|
||||
optional string lite_llm_base_url = 2;
|
||||
optional bool lite_llm_use_prompt_cache = 3;
|
||||
map<string, string> open_ai_headers = 4;
|
||||
optional string anthropic_base_url = 5;
|
||||
optional string open_router_provider_sorting = 6;
|
||||
optional string aws_region = 7;
|
||||
optional bool aws_use_cross_region_inference = 8;
|
||||
optional bool aws_bedrock_use_prompt_cache = 9;
|
||||
optional bool aws_use_profile = 10;
|
||||
optional string aws_profile = 11;
|
||||
optional string aws_bedrock_endpoint = 12;
|
||||
optional string claude_code_path = 13;
|
||||
optional string vertex_project_id = 14;
|
||||
optional string vertex_region = 15;
|
||||
optional string open_ai_base_url = 16;
|
||||
optional string ollama_base_url = 17;
|
||||
optional string ollama_api_options_ctx_num = 18;
|
||||
optional string lm_studio_base_url = 19;
|
||||
optional string gemini_base_url = 20;
|
||||
optional string requesty_base_url = 21;
|
||||
optional int64 fireworks_model_max_completion_tokens = 22;
|
||||
optional int64 fireworks_model_max_tokens = 23;
|
||||
optional string azure_api_version = 24;
|
||||
optional string qwen_api_line = 25;
|
||||
optional string asksage_api_url = 26;
|
||||
optional int64 request_timeout_ms = 27;
|
||||
optional string sap_ai_resource_group = 28;
|
||||
optional string sap_ai_core_token_url = 29;
|
||||
optional string sap_ai_core_base_url = 30;
|
||||
optional bool sap_ai_core_use_orchestration_mode = 31;
|
||||
optional string moonshot_api_line = 32;
|
||||
optional string aws_authentication = 33;
|
||||
optional string zai_api_line = 34;
|
||||
optional string lm_studio_max_tokens = 35;
|
||||
optional string qwen_code_oauth_path = 36;
|
||||
optional string dify_base_url = 37;
|
||||
optional string oca_base_url = 38;
|
||||
optional string oca_mode = 39;
|
||||
optional bool aws_use_global_inference = 40;
|
||||
optional string minimax_api_line = 41;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
optional string plan_mode_api_model_id = 101;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 102;
|
||||
optional string plan_mode_reasoning_effort = 103;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 105;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
|
||||
optional string plan_mode_open_router_model_id = 107;
|
||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
|
||||
optional string plan_mode_open_ai_model_id = 109;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
|
||||
optional string plan_mode_ollama_model_id = 111;
|
||||
optional string plan_mode_lm_studio_model_id = 112;
|
||||
optional string plan_mode_lite_llm_model_id = 113;
|
||||
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 114;
|
||||
optional string plan_mode_requesty_model_id = 115;
|
||||
optional OpenRouterModelInfo plan_mode_requesty_model_info = 116;
|
||||
optional string plan_mode_together_model_id = 117;
|
||||
optional string plan_mode_fireworks_model_id = 118;
|
||||
optional string plan_mode_sap_ai_core_model_id = 119;
|
||||
optional string plan_mode_sap_ai_core_deployment_id = 120;
|
||||
optional string plan_mode_groq_model_id = 121;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 122;
|
||||
optional string plan_mode_hugging_face_model_id = 123;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 124;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 125;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 126;
|
||||
optional string plan_mode_baseten_model_id = 127;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 128;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 129;
|
||||
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;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
optional string act_mode_api_model_id = 201;
|
||||
optional int64 act_mode_thinking_budget_tokens = 202;
|
||||
optional string act_mode_reasoning_effort = 203;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 205;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
|
||||
optional string act_mode_open_router_model_id = 207;
|
||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
|
||||
optional string act_mode_open_ai_model_id = 209;
|
||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
|
||||
optional string act_mode_ollama_model_id = 211;
|
||||
optional string act_mode_lm_studio_model_id = 212;
|
||||
optional string act_mode_lite_llm_model_id = 213;
|
||||
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 214;
|
||||
optional string act_mode_requesty_model_id = 215;
|
||||
optional OpenRouterModelInfo act_mode_requesty_model_info = 216;
|
||||
optional string act_mode_together_model_id = 217;
|
||||
optional string act_mode_fireworks_model_id = 218;
|
||||
optional string act_mode_sap_ai_core_model_id = 219;
|
||||
optional string act_mode_sap_ai_core_deployment_id = 220;
|
||||
optional string act_mode_groq_model_id = 221;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 222;
|
||||
optional string act_mode_hugging_face_model_id = 223;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 224;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 225;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 226;
|
||||
optional string act_mode_baseten_model_id = 227;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 228;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 229;
|
||||
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;
|
||||
}
|
||||
|
||||
// Request for updating API configuration (legacy - uses combined configuration)
|
||||
message UpdateApiConfigurationRequest {
|
||||
Metadata metadata = 1;
|
||||
ModelsApiConfiguration api_configuration = 2;
|
||||
}
|
||||
|
||||
// Request for updating API configuration (new - uses separate options and secrets)
|
||||
message UpdateApiConfigurationRequestNew {
|
||||
Metadata metadata = 1;
|
||||
ModelsApiOptions options = 2;
|
||||
ModelsApiSecrets secrets = 3;
|
||||
}
|
||||
|
||||
// Request for partially updating API configuration using FieldMask
|
||||
// Only fields specified in update_mask will be updated from api_configuration
|
||||
message UpdateApiConfigurationPartialRequest {
|
||||
|
||||
+5
-10
@@ -46,11 +46,8 @@ message AutoApprovalActions {
|
||||
// Auto approval settings for task execution
|
||||
message AutoApprovalSettings {
|
||||
int32 version = 1;
|
||||
bool enabled = 2;
|
||||
AutoApprovalActions actions = 3;
|
||||
int32 max_requests = 4;
|
||||
bool enable_notifications = 5;
|
||||
repeated string favorites = 6;
|
||||
AutoApprovalActions actions = 2;
|
||||
optional bool enable_notifications = 3;
|
||||
}
|
||||
|
||||
message Secrets {
|
||||
@@ -283,11 +280,8 @@ message ResetStateRequest {
|
||||
message AutoApprovalSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
int32 version = 2;
|
||||
bool enabled = 3;
|
||||
AutoApprovalActions actions = 4;
|
||||
int32 max_requests = 5;
|
||||
bool enable_notifications = 6;
|
||||
repeated string favorites = 7;
|
||||
AutoApprovalActions actions = 3;
|
||||
bool enable_notifications = 4;
|
||||
}
|
||||
|
||||
enum TelemetrySettingEnum {
|
||||
@@ -356,6 +350,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 {
|
||||
|
||||
@@ -26,13 +26,12 @@ enum ClineAsk {
|
||||
RESUME_TASK = 7;
|
||||
RESUME_COMPLETED_TASK = 8;
|
||||
MISTAKE_LIMIT_REACHED = 9;
|
||||
AUTO_APPROVAL_MAX_REQ_REACHED = 10;
|
||||
BROWSER_ACTION_LAUNCH = 11;
|
||||
USE_MCP_SERVER = 12;
|
||||
NEW_TASK = 13;
|
||||
CONDENSE = 14;
|
||||
REPORT_BUG = 15;
|
||||
SUMMARIZE_TASK = 16;
|
||||
BROWSER_ACTION_LAUNCH = 10;
|
||||
USE_MCP_SERVER = 11;
|
||||
NEW_TASK = 12;
|
||||
CONDENSE = 13;
|
||||
REPORT_BUG = 14;
|
||||
SUMMARIZE_TASK = 15;
|
||||
}
|
||||
|
||||
// Enum for ClineSay types
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import chalk from "chalk"
|
||||
import { execSync } from "child_process"
|
||||
import * as fs from "fs/promises"
|
||||
import { globby } from "globby"
|
||||
import * as path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
|
||||
const PROTO_DIR = path.join(ROOT_DIR, "proto")
|
||||
const PY_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-python")
|
||||
const PY_CLIENT_DIR = path.join(PY_OUT_DIR, "client")
|
||||
|
||||
function hasCommand(cmd) {
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
execSync(`where ${cmd}`, { stdio: "pipe" })
|
||||
} else {
|
||||
execSync(`which ${cmd}`, { stdio: "pipe" })
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePython() {
|
||||
// Allow override via env.PYTHON pointing to a specific interpreter
|
||||
const envPy = process.env.PYTHON
|
||||
if (envPy) {
|
||||
try {
|
||||
execSync(`"${envPy}" --version`, { stdio: "pipe" })
|
||||
return envPy
|
||||
} catch {
|
||||
console.warn(chalk.yellow(`Warning: PYTHON override "${envPy}" is not usable, falling back to discovery.`))
|
||||
}
|
||||
}
|
||||
const candidates = ["python3", "python"]
|
||||
for (const c of candidates) {
|
||||
if (hasCommand(c)) {
|
||||
try {
|
||||
execSync(`${c} --version`, { stdio: "pipe" })
|
||||
return c
|
||||
} catch {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function checkGrpcTools(pythonExe) {
|
||||
try {
|
||||
execSync(`"${pythonExe}" -c "import grpc_tools"`, { stdio: "pipe" })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDir(dir) {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
}
|
||||
|
||||
async function ensureInitPy(dir) {
|
||||
try {
|
||||
await fs.writeFile(path.join(dir, "__init__.py"), "", { flag: "wx" })
|
||||
} catch {
|
||||
// exists
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse proto files to extract service names with their source file and package.
|
||||
* Returns array of:
|
||||
* { serviceName: string, serviceKey: string, protoPackage: "cline"|"host", moduleBase: string }
|
||||
*/
|
||||
async function parseServicesWithFiles(protoDir, protoFiles) {
|
||||
const services = []
|
||||
for (const relPath of protoFiles) {
|
||||
const full = path.join(protoDir, relPath)
|
||||
const content = await fs.readFile(full, "utf8")
|
||||
const pkg = relPath.startsWith("host/") ? "host" : "cline"
|
||||
const moduleBase = path.basename(relPath, ".proto")
|
||||
const serviceRe = /service\s+(\w+Service)\s*\{([\s\S]*?)\}/g
|
||||
for (const m of content.matchAll(serviceRe)) {
|
||||
const serviceName = m[1] // e.g., TaskService
|
||||
const serviceKey = serviceName.replace(/Service$/, "").toLowerCase() // task
|
||||
const body = m[2]
|
||||
const methodRe = /rpc\s+(\w+)\s*\((stream\s)?([\w.]+)\)\s*returns\s*\((stream\s)?([\w.]+)\)/g
|
||||
const methods = []
|
||||
for (const mm of body.matchAll(methodRe)) {
|
||||
methods.push({
|
||||
name: mm[1],
|
||||
isRequestStreaming: !!mm[2],
|
||||
requestType: mm[3],
|
||||
isResponseStreaming: !!mm[4],
|
||||
responseType: mm[5],
|
||||
})
|
||||
}
|
||||
services.push({ serviceName, serviceKey, protoPackage: pkg, moduleBase, methods })
|
||||
}
|
||||
}
|
||||
return services
|
||||
}
|
||||
|
||||
function upperFirst(s) {
|
||||
return s.length ? s[0].toUpperCase() + s.slice(1) : s
|
||||
}
|
||||
|
||||
async function generateConnectionPy(outDir) {
|
||||
const content = `# AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
# Generated by scripts/build-python-proto.mjs
|
||||
|
||||
import grpc
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self, address: str, timeout: float = 30.0):
|
||||
self.address = address
|
||||
self.timeout = timeout
|
||||
self._channel: Optional[grpc.Channel] = None
|
||||
|
||||
def connect(self) -> None:
|
||||
if self._channel is not None:
|
||||
return
|
||||
self._channel = grpc.insecure_channel(self.address)
|
||||
# Wait for channel to be ready within timeout
|
||||
grpc.channel_ready_future(self._channel).result(timeout=self.timeout)
|
||||
|
||||
def disconnect(self) -> None:
|
||||
if self._channel is not None:
|
||||
self._channel.close()
|
||||
self._channel = None
|
||||
|
||||
@property
|
||||
def channel(self) -> Optional[grpc.Channel]:
|
||||
return self._channel
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return self._channel is not None
|
||||
`
|
||||
await fs.mkdir(outDir, { recursive: true })
|
||||
await fs.writeFile(path.join(outDir, "connection.py"), content)
|
||||
await ensureInitPy(outDir)
|
||||
}
|
||||
|
||||
async function generateClineClientPy(outDir, services) {
|
||||
// Import per-service wrapper clients
|
||||
const importLines = []
|
||||
const seen = new Set()
|
||||
for (const s of services) {
|
||||
const fileBase = `${s.serviceKey}_client`
|
||||
const className = `${s.serviceName.replace(/Service$/, "")}Client`
|
||||
const importKey = `${fileBase}:${className}`
|
||||
if (!seen.has(importKey)) {
|
||||
importLines.push(`from .services.${fileBase} import ${className}`)
|
||||
seen.add(importKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Build wrapper initializations on connect (like Go New<Service>Client)
|
||||
const initLines = services.map((s) => {
|
||||
const shortName = s.serviceName.replace(/Service$/, "") // Task
|
||||
const className = `${shortName}Client`
|
||||
return ` self.${shortName} = ${className}(self._conn.channel)`
|
||||
})
|
||||
|
||||
// Build attribute resets on disconnect
|
||||
const nilLines = services.map((s) => {
|
||||
const shortName = s.serviceName.replace(/Service$/, "")
|
||||
return ` self.${shortName} = None`
|
||||
})
|
||||
|
||||
const content = `# AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
# Generated by scripts/build-python-proto.mjs
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import grpc
|
||||
from .connection import ConnectionManager
|
||||
${importLines.join("\n")}
|
||||
|
||||
class ClineClient:
|
||||
"""
|
||||
Unified Python client analogous to src/generated/grpc-go/client/ClineClient.
|
||||
|
||||
Usage:
|
||||
client = ClineClient("localhost:17611")
|
||||
client.connect()
|
||||
# Call wrappers, e.g.: client.Task.SomeRpc(...)
|
||||
client.disconnect()
|
||||
"""
|
||||
|
||||
def __init__(self, address: str, timeout: float = 30.0):
|
||||
self._conn = ConnectionManager(address, timeout=timeout)
|
||||
self._connected = False
|
||||
|
||||
${services.map((s) => ` self.${s.serviceName.replace(/Service$/, "")}: Optional[object] = None`).join("\n")}
|
||||
|
||||
def connect(self) -> None:
|
||||
if self._connected:
|
||||
return
|
||||
self._conn.connect()
|
||||
${initLines.join("\n")}
|
||||
self._connected = True
|
||||
|
||||
def disconnect(self) -> None:
|
||||
if not self._connected:
|
||||
return
|
||||
self._conn.disconnect()
|
||||
${nilLines.join("\n")}
|
||||
self._connected = False
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return self._connected
|
||||
|
||||
@property
|
||||
def channel(self) -> Optional[grpc.Channel]:
|
||||
return self._conn.channel
|
||||
`
|
||||
const clientDir = outDir
|
||||
await fs.mkdir(clientDir, { recursive: true })
|
||||
await fs.writeFile(path.join(clientDir, "cline_client.py"), content)
|
||||
}
|
||||
|
||||
async function generatePythonClient(protoDir, pyOutDir, clientDir, protoFiles) {
|
||||
// Ensure package structure for client
|
||||
await fs.mkdir(clientDir, { recursive: true })
|
||||
await ensureInitPy(pyOutDir)
|
||||
await ensureInitPy(clientDir)
|
||||
|
||||
const services = await parseServicesWithFiles(protoDir, protoFiles)
|
||||
|
||||
// connection.py
|
||||
await generateConnectionPy(clientDir)
|
||||
|
||||
// services/ per-service wrappers (mirror Go client/services)
|
||||
const servicesDir = path.join(clientDir, "services")
|
||||
await fs.mkdir(servicesDir, { recursive: true })
|
||||
await ensureInitPy(servicesDir)
|
||||
await generateServiceClientsPy(servicesDir, services)
|
||||
|
||||
// cline_client.py (unified that composes service wrappers)
|
||||
await generateClineClientPy(clientDir, services)
|
||||
}
|
||||
|
||||
async function generateServiceClientsPy(outDir, services) {
|
||||
await fs.mkdir(outDir, { recursive: true })
|
||||
await ensureInitPy(outDir)
|
||||
|
||||
for (const s of services) {
|
||||
const shortName = s.serviceName.replace(/Service$/, "") // Task
|
||||
const className = `${shortName}Client`
|
||||
const fileName = `${s.serviceKey}_client.py`
|
||||
|
||||
const aliasPb2 = `${s.protoPackage}_${s.moduleBase}_pb2`
|
||||
const aliasGrpc = `${s.protoPackage}_${s.moduleBase}_pb2_grpc`
|
||||
|
||||
const methodLines = s.methods
|
||||
.map((m) => {
|
||||
const reqTypeName = m.requestType.split(".").pop()
|
||||
const respTypeName = m.responseType.split(".").pop()
|
||||
if (m.isResponseStreaming) {
|
||||
return `
|
||||
def ${m.name}(self, req):
|
||||
"""
|
||||
Server-streaming RPC.
|
||||
:param req: ${aliasPb2}.${reqTypeName}
|
||||
:return: iterator of ${aliasPb2}.${respTypeName}
|
||||
"""
|
||||
return self._stub.${m.name}(req)`
|
||||
} else {
|
||||
return `
|
||||
def ${m.name}(self, req):
|
||||
"""
|
||||
Unary RPC.
|
||||
:param req: ${aliasPb2}.${reqTypeName}
|
||||
:return: ${aliasPb2}.${respTypeName}
|
||||
"""
|
||||
return self._stub.${m.name}(req)`
|
||||
}
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
const content = `# AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
# Generated by scripts/build-python-proto.mjs
|
||||
|
||||
import grpc
|
||||
from ${s.protoPackage} import ${s.moduleBase}_pb2 as ${aliasPb2}
|
||||
from ${s.protoPackage} import ${s.moduleBase}_pb2_grpc as ${aliasGrpc}
|
||||
|
||||
class ${className}:
|
||||
def __init__(self, channel: grpc.Channel):
|
||||
self._stub = ${aliasGrpc}.${s.serviceName}Stub(channel)
|
||||
${methodLines}
|
||||
`
|
||||
await fs.writeFile(path.join(outDir, fileName), content)
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePyproject(outDir) {
|
||||
const content = `[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "cline-grpc-python"
|
||||
version = "0.1.0"
|
||||
description = "Generated Python gRPC stubs and client wrappers for Cline protos"
|
||||
license = { text: "Apache-2.0" }
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"grpcio>=1.56.0",
|
||||
"protobuf>=4.21.0"
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
`
|
||||
await fs.writeFile(path.join(outDir, "pyproject.toml"), content)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.cyan("Starting Python protobuf code generation..."))
|
||||
|
||||
// Verify proto dir exists
|
||||
try {
|
||||
const stat = await fs.stat(PROTO_DIR)
|
||||
if (!stat.isDirectory()) {
|
||||
console.error(chalk.red(`Proto directory is not a folder: ${PROTO_DIR}`))
|
||||
process.exit(1)
|
||||
}
|
||||
} catch {
|
||||
console.error(chalk.red(`Proto directory not found: ${PROTO_DIR}`))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Resolve Python
|
||||
const python = resolvePython()
|
||||
if (!python) {
|
||||
console.error(
|
||||
chalk.red("Python not found on PATH. Please install Python 3 and ensure it is available (python3 or python)."),
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(chalk.green(`✓ Using Python executable: ${python}`))
|
||||
|
||||
// Check grpcio-tools
|
||||
if (!checkGrpcTools(python)) {
|
||||
console.error(chalk.red("Missing dependency: grpcio-tools"))
|
||||
console.log(chalk.yellow("Install with:"))
|
||||
console.log(chalk.yellow(` ${python} -m pip install grpcio-tools --user --break-system-packages`))
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(chalk.green("✓ grpcio-tools available"))
|
||||
|
||||
// Discover proto files
|
||||
const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR })
|
||||
if (!protoFiles.length) {
|
||||
console.error(chalk.red("No .proto files found under ./proto"))
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(chalk.cyan(`Found ${protoFiles.length} proto files`))
|
||||
|
||||
// Ensure output directory
|
||||
await ensureDir(PY_OUT_DIR)
|
||||
|
||||
// Build and run protoc command via grpc_tools
|
||||
const quoted = (s) => `"${s}"`
|
||||
const pythonCmd = quoted(python)
|
||||
const cmd =
|
||||
`${pythonCmd} -m grpc_tools.protoc ` +
|
||||
`-I ${quoted(PROTO_DIR)} ` +
|
||||
`--python_out=${quoted(PY_OUT_DIR)} ` +
|
||||
`--grpc_python_out=${quoted(PY_OUT_DIR)} ` +
|
||||
protoFiles.map((f) => quoted(f)).join(" ")
|
||||
|
||||
try {
|
||||
console.log(chalk.cyan(`Generating Python code into ${PY_OUT_DIR}...`))
|
||||
execSync(cmd, { cwd: ROOT_DIR, stdio: "inherit", env: process.env })
|
||||
} catch (error) {
|
||||
console.error(chalk.red("Error generating Python code:"), error?.message || error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Ensure package structure (__init__.py) for imports
|
||||
await ensureInitPy(PY_OUT_DIR)
|
||||
try {
|
||||
const clineDir = path.join(PY_OUT_DIR, "cline")
|
||||
const hostDir = path.join(PY_OUT_DIR, "host")
|
||||
// These may or may not exist depending on which protos are present
|
||||
await fs
|
||||
.stat(clineDir)
|
||||
.then(() => ensureInitPy(clineDir))
|
||||
.catch(() => {})
|
||||
await fs
|
||||
.stat(hostDir)
|
||||
.then(() => ensureInitPy(hostDir))
|
||||
.catch(() => {})
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Generate Python client structure analogous to src/generated/grpc-go/client
|
||||
await generatePythonClient(PROTO_DIR, PY_OUT_DIR, PY_CLIENT_DIR, protoFiles)
|
||||
|
||||
// Generate a minimal pyproject.toml in the generated output so it can be pip-installed if desired
|
||||
await generatePyproject(PY_OUT_DIR)
|
||||
|
||||
console.log(chalk.green("✓ Python protobuf and client code generation completed successfully!"))
|
||||
console.log(chalk.cyan(`Output directory: ${PY_OUT_DIR}`))
|
||||
console.log(chalk.cyan(`Client directory: ${PY_CLIENT_DIR}`))
|
||||
console.log(chalk.cyan(`PyProject: ${path.join(PY_OUT_DIR, "pyproject.toml")}`))
|
||||
console.log(chalk.gray("Note: To import, add the output dir to your PYTHONPATH or pip install -e src/generated/grpc-python"))
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((err) => {
|
||||
console.error(chalk.red("Unexpected error in build-python-proto.mjs:"), err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { HostProvider } from "@/hosts/host-provider"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { StateManager } from "./core/storage/StateManager"
|
||||
import { ExtensionRegistryInfo } from "./registry"
|
||||
import { BannerService } from "./services/banner/BannerService"
|
||||
import { audioRecordingService } from "./services/dictation/AudioRecordingService"
|
||||
import { ErrorService } from "./services/error"
|
||||
import { featureFlagsService } from "./services/feature-flags"
|
||||
@@ -71,6 +72,20 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
|
||||
await showVersionUpdateAnnouncement(context)
|
||||
|
||||
// Initialize banner service
|
||||
BannerService.initialize(webview.controller)
|
||||
BannerService.get()
|
||||
.fetchActiveBanners()
|
||||
.then((banners) => {
|
||||
if (banners.length > 0) {
|
||||
Logger.log(`BannerService: ${banners.length} active banner(s) fetched.`)
|
||||
// Banners are now cached and can be accessed by the frontend when needed
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error("BannerService: Failed to fetch banners on startup", error)
|
||||
})
|
||||
|
||||
telemetryService.captureExtensionActivated()
|
||||
|
||||
return webview
|
||||
|
||||
@@ -1,6 +1,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 { AnthropicHandler } from "./providers/anthropic"
|
||||
import { AskSageHandler } from "./providers/asksage"
|
||||
import { BasetenHandler } from "./providers/baseten"
|
||||
@@ -45,7 +46,7 @@ export type CommonApiHandlerOptions = {
|
||||
}
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream
|
||||
getModel(): ApiHandlerModel
|
||||
getApiStreamUsage?(): Promise<ApiStreamUsageChunk | undefined>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Tool as AnthropicTool, MessageParam } 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 { ClineTool } from "@/shared/tools"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -38,7 +40,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
|
||||
const model = this.getModel()
|
||||
@@ -48,6 +50,9 @@ export class AnthropicHandler implements ApiHandler {
|
||||
const enable1mContextWindow = model.id.endsWith(CLAUDE_SONNET_1M_SUFFIX)
|
||||
|
||||
const budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
|
||||
// Tools are available only when native tools are enabled.
|
||||
const nativeToolsOn = tools?.length && tools?.length > 0
|
||||
const reasoningOn = !!(
|
||||
(modelId.includes("3-7") || modelId.includes("4-") || modelId.includes("4-5")) &&
|
||||
budget_tokens !== 0
|
||||
@@ -68,12 +73,45 @@ export class AnthropicHandler implements ApiHandler {
|
||||
/*
|
||||
The latest message will be the new user message, one before will be the assistant message from a previous request, and the user message before that will be a previously cached user message. So we need to mark the latest user message as ephemeral to cache it for the next request, and mark the second to last user message as ephemeral to let the server know the last message to retrieve from the cache for the current request..
|
||||
*/
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
const userMsgIndices = messages.reduce((acc, msg, index) => {
|
||||
if (msg.role === "user") {
|
||||
acc.push(index)
|
||||
}
|
||||
return acc
|
||||
}, [] as number[])
|
||||
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
|
||||
})
|
||||
|
||||
stream = await client.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
@@ -89,39 +127,16 @@ export class AnthropicHandler implements ApiHandler {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
], // setting cache breakpoint for system prompt so new tasks can reuse it
|
||||
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
|
||||
}),
|
||||
messages: anthropicMessages,
|
||||
// tools, // cache breakpoints go from tools > system > messages, and since tools dont change, we can just set the breakpoint at the end of system (this avoids having to set a breakpoint at the end of tools which by itself does not meet min requirements for haiku caching)
|
||||
// tool_choice: { type: "auto" },
|
||||
// tools: tools,
|
||||
stream: true,
|
||||
tools: nativeToolsOn ? (tools as AnthropicTool[]) : undefined,
|
||||
// tool_choice options:
|
||||
// - none: disables tool use, even if tools are provided. Claude will not call any tools.
|
||||
// - auto: allows Claude to decide whether to call any provided tools or not. This is the default value when tools are provided.
|
||||
// - any: tells Claude that it must use one of the provided tools, but doesn’t force a particular tool.
|
||||
// NOTE: Forcing tool use when tools are provided will result in error when thinking is also enabled.
|
||||
tool_choice: nativeToolsOn && !reasoningOn ? { type: "any" } : undefined,
|
||||
},
|
||||
(() => {
|
||||
// 1m context window beta header
|
||||
@@ -154,6 +169,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
let thinkingDeltaAccumulator = ""
|
||||
const lastStartedToolCall = { id: "", name: "", arguments: "" }
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk?.type) {
|
||||
@@ -208,6 +224,14 @@ export class AnthropicHandler implements ApiHandler {
|
||||
data: chunk.content_block.data,
|
||||
}
|
||||
break
|
||||
case "tool_use":
|
||||
if (chunk.content_block.id && chunk.content_block.name) {
|
||||
// Convert Anthropic tool_use to OpenAI-compatible format
|
||||
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) {
|
||||
@@ -250,9 +274,30 @@ export class AnthropicHandler implements ApiHandler {
|
||||
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
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
...lastStartedToolCall,
|
||||
function: {
|
||||
...lastStartedToolCall,
|
||||
id: lastStartedToolCall.id,
|
||||
name: lastStartedToolCall.name,
|
||||
arguments: chunk.delta.partial_json,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case "content_block_stop":
|
||||
lastStartedToolCall.id = ""
|
||||
lastStartedToolCall.name = ""
|
||||
lastStartedToolCall.arguments = ""
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from
|
||||
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 { ClineEnv } from "@/config"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
@@ -12,6 +13,7 @@ import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
|
||||
interface ClineHandlerOptions extends CommonApiHandlerOptions {
|
||||
@@ -93,7 +95,7 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
try {
|
||||
const client = await this.ensureClient()
|
||||
|
||||
@@ -110,8 +112,11 @@ export class ClineHandler implements ApiHandler {
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
this.options.openRouterProviderSorting,
|
||||
tools,
|
||||
)
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
if ("error" in chunk) {
|
||||
@@ -150,6 +155,10 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
@@ -194,8 +203,7 @@ export class ClineHandler implements ApiHandler {
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost: totalCost,
|
||||
totalCost,
|
||||
}
|
||||
didOutputUsage = true
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface DeepSeekHandlerOptions extends CommonApiHandlerOptions {
|
||||
deepSeekApiKey?: string
|
||||
@@ -71,7 +73,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
@@ -94,8 +96,11 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
stream_options: { include_usage: true },
|
||||
// Only set temperature for non-reasoner models
|
||||
...(model.id === "deepseek-reasoner" ? {} : { temperature: 0 }),
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -105,6 +110,10 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
|
||||
import { ApiError, type GenerateContentConfig, type GenerateContentResponseUsageMetadata, GoogleGenAI, Part } from "@google/genai"
|
||||
import {
|
||||
ApiError,
|
||||
FunctionCallingConfigMode,
|
||||
type GenerateContentConfig,
|
||||
type GenerateContentResponseUsageMetadata,
|
||||
GoogleGenAI,
|
||||
FunctionDeclaration as GoogleTool,
|
||||
Part,
|
||||
} from "@google/genai"
|
||||
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -102,7 +110,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
baseDelay: 2000,
|
||||
maxDelay: 15000,
|
||||
})
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: GoogleTool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const { id: modelId, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
@@ -140,6 +148,16 @@ export class GeminiHandler implements ApiHandler {
|
||||
let thoughtsTokenCount = 0 // Initialize thought token counts
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
|
||||
if (tools?.length) {
|
||||
requestConfig.tools = [{ functionDeclarations: tools }]
|
||||
requestConfig.toolConfig = {
|
||||
// Force the model to call 'any' function.
|
||||
functionCallingConfig: {
|
||||
mode: FunctionCallingConfigMode.ANY,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await client.models.generateContentStream({
|
||||
model: modelId,
|
||||
@@ -189,6 +207,24 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (tools && chunk.functionCalls && chunk.functionCalls?.length > 0) {
|
||||
for (const functionCall of chunk.functionCalls) {
|
||||
if (functionCall.args) {
|
||||
console.log("[GeminiHandler] tool call received:", functionCall)
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: functionCall.id || functionCall.name,
|
||||
name: functionCall.name,
|
||||
arguments: JSON.stringify(functionCall.args),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usageMetadata) {
|
||||
lastUsageMetadata = chunk.usageMetadata
|
||||
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
|
||||
|
||||
@@ -2,10 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { GroqModelId, groqDefaultModelId, groqModels, ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface GroqHandlerOptions extends CommonApiHandlerOptions {
|
||||
groqApiKey?: string
|
||||
@@ -188,7 +190,7 @@ export class GroqHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const modelFamily = this.detectModelFamily(model.id)
|
||||
@@ -213,6 +215,7 @@ export class GroqHandler implements ApiHandler {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature,
|
||||
...getOpenAIToolParams(tools),
|
||||
}
|
||||
|
||||
// Add any special parameters for specific model families
|
||||
@@ -220,6 +223,7 @@ export class GroqHandler implements ApiHandler {
|
||||
Object.assign(requestParams, modelFamily.specialParams)
|
||||
}
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
const stream = await client.chat.completions.create(requestParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
@@ -235,6 +239,10 @@ export class GroqHandler implements ApiHandler {
|
||||
continue
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
// Handle content field - trust the parsed output from Groq
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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 { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface HuaweiCloudMaaSHandlerOptions extends CommonApiHandlerOptions {
|
||||
huaweiCloudMaasApiKey?: string
|
||||
@@ -58,7 +60,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
@@ -72,12 +74,15 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
let reasoning: string | null = null
|
||||
let didOutputUsage: boolean = false
|
||||
let finalUsage: any = null
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
@@ -93,6 +98,10 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
// Handle reasoning output
|
||||
if (reasoning || (delta && "reasoning_content" in delta && delta.reasoning_content)) {
|
||||
const reasoningContent = delta?.content || ((delta as any)?.reasoning_content as string | undefined) || ""
|
||||
|
||||
@@ -2,10 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { HuggingFaceModelId, huggingFaceDefaultModelId, huggingFaceModels, ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface HuggingFaceHandlerOptions extends CommonApiHandlerOptions {
|
||||
huggingFaceApiKey?: string
|
||||
@@ -65,7 +67,7 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
try {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
@@ -82,8 +84,10 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
...getOpenAIToolParams(tools),
|
||||
}
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
const stream = (await client.chat.completions.create(requestParams)) as any
|
||||
|
||||
let _chunkCount = 0
|
||||
@@ -101,6 +105,10 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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 type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import type { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface LmStudioHandlerOptions extends CommonApiHandlerOptions {
|
||||
lmStudioBaseUrl?: string
|
||||
@@ -36,7 +38,7 @@ export class LmStudioHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry({ retryAllErrors: true })
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
@@ -50,7 +52,11 @@ export class LmStudioHandler implements ApiHandler {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
max_completion_tokens: this.options.lmStudioMaxTokens ? Number(this.options.lmStudioMaxTokens) : undefined,
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const choice = chunk.choices[0]
|
||||
const delta = choice?.delta
|
||||
@@ -66,6 +72,11 @@ export class LmStudioHandler implements ApiHandler {
|
||||
reasoning: (delta.reasoning_content as string | undefined) || "",
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { MinimaxModelId, ModelInfo, minimaxDefaultModelId, minimaxModels } from "@/shared/api"
|
||||
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
|
||||
@@ -36,7 +38,11 @@ export class MinimaxHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
tools?: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
@@ -51,8 +57,11 @@ export class MinimaxHandler implements ApiHandler {
|
||||
max_tokens: model.info.maxTokens,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -69,6 +78,10 @@ export class MinimaxHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Mistral } from "@mistralai/mistralai"
|
||||
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 { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToMistralMessages } from "../transform/mistral-format"
|
||||
@@ -36,7 +38,7 @@ export class MistralHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const stream = await client.chat
|
||||
.stream({
|
||||
@@ -45,6 +47,8 @@ export class MistralHandler implements ApiHandler {
|
||||
temperature: 0,
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToMistralMessages(messages)],
|
||||
stream: true,
|
||||
tools: tools?.length ? (tools as MistralTool[]) : undefined,
|
||||
toolChoice: tools?.length ? "any" : undefined,
|
||||
})
|
||||
.catch((err) => {
|
||||
// The Mistal SDK uses statusCode instead of status
|
||||
@@ -58,7 +62,20 @@ export class MistralHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.data.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
if (delta.toolCalls) {
|
||||
for (const toolCall of delta.toolCalls) {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: toolCall.id,
|
||||
name: toolCall.function.name,
|
||||
arguments: JSON.stringify(toolCall.function.arguments),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
} else if (delta?.content) {
|
||||
let content: string = ""
|
||||
if (typeof delta.content === "string") {
|
||||
content = delta.content
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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 { 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 MoonshotHandlerOptions extends CommonApiHandlerOptions {
|
||||
moonshotApiKey?: string
|
||||
@@ -36,7 +38,7 @@ export class MoonshotHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
@@ -52,7 +54,11 @@ export class MoonshotHandler implements ApiHandler {
|
||||
max_tokens: model.info.maxTokens,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -62,6 +68,10 @@ export class MoonshotHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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 { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface NebiusHandlerOptions extends CommonApiHandlerOptions {
|
||||
nebiusApiKey?: string
|
||||
@@ -35,7 +37,7 @@ export class NebiusHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
@@ -49,7 +51,9 @@ export class NebiusHandler implements ApiHandler {
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -59,6 +63,10 @@ export class NebiusHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI, { APIError, OpenAIError } from "openai"
|
||||
import type { FinalRequestOptions, Headers as OpenAIHeaders } from "openai/core"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import {
|
||||
DEFAULT_EXTERNAL_OCA_BASE_URL,
|
||||
@@ -14,6 +15,7 @@ import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
export interface OcaHandlerOptions extends CommonApiHandlerOptions {
|
||||
ocaBaseUrl?: string
|
||||
@@ -135,7 +137,7 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const formattedMessages = convertToOpenAiMessages(messages)
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
@@ -187,6 +189,8 @@ export class OcaHandler implements ApiHandler {
|
||||
return message
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: this.options.ocaModelId || liteLlmDefaultModelId,
|
||||
messages: [enhancedSystemMessage, ...enhancedMessages],
|
||||
@@ -198,6 +202,7 @@ export class OcaHandler implements ApiHandler {
|
||||
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
|
||||
...(this.options.taskId && {
|
||||
litellm_session_id: `cline-${this.options.taskId}`,
|
||||
...getOpenAIToolParams(tools),
|
||||
}), // Add session ID for LiteLLM tracking
|
||||
})
|
||||
|
||||
@@ -228,6 +233,10 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
// Handle token usage information
|
||||
if (chunk.usage) {
|
||||
const totalCost =
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, OpenAiNativeModelId, openAiNativeDefaultModelId, openAiNativeModels } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface OpenAiNativeHandlerOptions extends CommonApiHandlerOptions {
|
||||
openAiNativeApiKey?: string
|
||||
@@ -56,9 +57,14 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
tools?: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
switch (model.id) {
|
||||
case "o1":
|
||||
@@ -114,6 +120,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium",
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
@@ -124,8 +131,17 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
try {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
} catch (error) {
|
||||
console.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
// Only last chunk contains usage
|
||||
// Only last chunk contains usage - stream is ending
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
@@ -138,6 +154,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
@@ -148,8 +165,13 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
// Only last chunk contains usage
|
||||
// Only last chunk contains usage - stream is ending
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { azureOpenAiDefaultApiVersion, ModelInfo, OpenAiCompatibleModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI, { AzureOpenAI } from "openai"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface OpenAiHandlerOptions extends CommonApiHandlerOptions {
|
||||
openAiApiKey?: string
|
||||
@@ -61,16 +62,17 @@ export class OpenAiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
tools?: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.openAiModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
|
||||
const isReasoningModelFamily =
|
||||
modelId.includes("o1") ||
|
||||
modelId.includes("o3") ||
|
||||
modelId.includes("o4") ||
|
||||
(modelId.includes("gpt-5") && !modelId.includes("chat"))
|
||||
["o1", "o3", "o4", "gpt-5"].some((prefix) => modelId.includes(prefix)) && !modelId.includes("chat")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
@@ -104,7 +106,11 @@ export class OpenAiHandler implements ApiHandler {
|
||||
reasoning_effort: reasoningEffort,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -121,12 +127,15 @@ export class OpenAiHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
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_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
|
||||
@@ -4,10 +4,12 @@ import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from
|
||||
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 { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
|
||||
interface OpenRouterHandlerOptions extends CommonApiHandlerOptions {
|
||||
@@ -50,7 +52,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
this.lastGenerationId = undefined
|
||||
|
||||
@@ -62,9 +64,11 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
this.options.openRouterProviderSorting,
|
||||
tools,
|
||||
)
|
||||
|
||||
let didOutputUsage: boolean = false
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
@@ -112,6 +116,10 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
|
||||
@@ -2,12 +2,14 @@ import { promises as fs } from "node:fs"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, QwenCodeModelId, qwenCodeDefaultModelId, qwenCodeModels } from "@shared/api"
|
||||
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 { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
// --- Constants for Qwen OAuth2 ---
|
||||
const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"
|
||||
@@ -174,7 +176,7 @@ export class QwenCodeHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
await this.ensureAuthenticated()
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
@@ -193,10 +195,12 @@ export class QwenCodeHandler implements ApiHandler {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
...getOpenAIToolParams(tools),
|
||||
}
|
||||
|
||||
const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
let fullContent = ""
|
||||
|
||||
for await (const apiChunk of stream) {
|
||||
@@ -240,6 +244,10 @@ export class QwenCodeHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
// Handle reasoning content (o1-style)
|
||||
if ("reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
|
||||
@@ -10,11 +10,13 @@ import {
|
||||
QwenApiRegions,
|
||||
} from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface QwenHandlerOptions extends CommonApiHandlerOptions {
|
||||
qwenApiKey?: string
|
||||
@@ -77,7 +79,7 @@ export class QwenHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-r1")
|
||||
@@ -112,8 +114,11 @@ export class QwenHandler implements ApiHandler {
|
||||
stream_options: { include_usage: true },
|
||||
temperature,
|
||||
...thinkingArgs,
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -123,6 +128,14 @@ export class QwenHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
try {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
} catch (error) {
|
||||
console.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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 { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface SambanovaHandlerOptions extends CommonApiHandlerOptions {
|
||||
sambanovaApiKey?: string
|
||||
@@ -38,7 +40,7 @@ export class SambanovaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
@@ -53,12 +55,14 @@ export class SambanovaHandler implements ApiHandler {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
const stream = await client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
@@ -70,6 +74,10 @@ export class SambanovaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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 { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface TogetherHandlerOptions extends CommonApiHandlerOptions {
|
||||
togetherApiKey?: string
|
||||
@@ -38,7 +40,7 @@ export class TogetherHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.togetherModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
@@ -58,7 +60,9 @@ export class TogetherHandler implements ApiHandler {
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -68,6 +72,10 @@ export class TogetherHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, vercelAiGatewayDefaultModelId, vercelAiGatewayDefaultModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
import { createVercelAIGatewayStream } from "../transform/vercel-ai-gateway-stream"
|
||||
|
||||
interface VercelAIGatewayHandlerOptions extends CommonApiHandlerOptions {
|
||||
@@ -42,15 +44,23 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.getModel().id
|
||||
const modelInfo = this.getModel().info
|
||||
|
||||
try {
|
||||
const stream = await createVercelAIGatewayStream(client, systemPrompt, messages, { id: modelId, info: modelInfo })
|
||||
const stream = await createVercelAIGatewayStream(
|
||||
client,
|
||||
systemPrompt,
|
||||
messages,
|
||||
{ id: modelId, info: modelInfo },
|
||||
tools,
|
||||
)
|
||||
let didOutputUsage: boolean = false
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -60,6 +70,10 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
const inputTokens = chunk.usage.prompt_tokens || 0
|
||||
const outputTokens =
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
|
||||
import { FunctionDeclaration as GoogleTool } from "@google/genai"
|
||||
import { ModelInfo, VertexModelId, vertexDefaultModelId, vertexModels } from "@shared/api"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -63,14 +66,14 @@ export class VertexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: ClineTool[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
const modelId = model.id
|
||||
|
||||
// For Gemini models, use the GeminiHandler
|
||||
if (!modelId.includes("claude")) {
|
||||
const geminiHandler = this.ensureGeminiHandler()
|
||||
yield* geminiHandler.createMessage(systemPrompt, messages)
|
||||
yield* geminiHandler.createMessage(systemPrompt, messages, tools as GoogleTool[])
|
||||
return
|
||||
}
|
||||
|
||||
@@ -160,6 +163,12 @@ export class VertexHandler implements ApiHandler {
|
||||
}
|
||||
}),
|
||||
stream: true,
|
||||
tools: tools?.length ? (tools as AnthropicTool[]) : undefined,
|
||||
// tool_choice options:
|
||||
// - none: disables tool use, even if tools are provided. Claude will not call any tools.
|
||||
// - auto: allows Claude to decide whether to call any provided tools or not. This is the default value when tools are provided.
|
||||
// - any: tells Claude that it must use one of the provided tools, but doesn’t force a particular tool.
|
||||
tool_choice: tools ? { type: "any" } : undefined,
|
||||
},
|
||||
{
|
||||
headers: {},
|
||||
@@ -191,11 +200,19 @@ export class VertexHandler implements ApiHandler {
|
||||
: message.content,
|
||||
})),
|
||||
stream: true,
|
||||
tools: tools?.length ? (tools as AnthropicTool[]) : undefined,
|
||||
// tool_choice options:
|
||||
// - none: disables tool use, even if tools are provided. Claude will not call any tools.
|
||||
// - auto: allows Claude to decide whether to call any provided tools or not. This is the default value when tools are provided.
|
||||
// - any: tells Claude that it must use one of the provided tools, but doesn’t force a particular tool.
|
||||
tool_choice: tools ? { type: "any" } : undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const lastStartedToolCall = { id: "", name: "", arguments: "" }
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk?.type) {
|
||||
case "message_start":
|
||||
@@ -233,6 +250,14 @@ export class VertexHandler implements ApiHandler {
|
||||
reasoning: "[Redacted thinking block]",
|
||||
}
|
||||
break
|
||||
case "tool_use":
|
||||
if (chunk.content_block.id && chunk.content_block.name) {
|
||||
// Convert Anthropic tool_use to OpenAI-compatible format
|
||||
lastStartedToolCall.id = chunk.content_block.id
|
||||
lastStartedToolCall.name = chunk.content_block.name
|
||||
lastStartedToolCall.arguments = ""
|
||||
}
|
||||
break
|
||||
case "text":
|
||||
if (chunk.index > 0) {
|
||||
yield {
|
||||
@@ -255,6 +280,22 @@ export class VertexHandler implements ApiHandler {
|
||||
reasoning: chunk.delta.thinking,
|
||||
}
|
||||
break
|
||||
case "input_json_delta":
|
||||
if (lastStartedToolCall.id && lastStartedToolCall.name && chunk.delta.partial_json) {
|
||||
// // Convert Anthropic tool_use to OpenAI-compatible format
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
...lastStartedToolCall,
|
||||
function: {
|
||||
id: lastStartedToolCall.id,
|
||||
name: lastStartedToolCall.name,
|
||||
arguments: chunk.delta.partial_json,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
break
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -264,6 +305,9 @@ export class VertexHandler implements ApiHandler {
|
||||
}
|
||||
break
|
||||
case "content_block_stop":
|
||||
lastStartedToolCall.id = ""
|
||||
lastStartedToolCall.name = ""
|
||||
lastStartedToolCall.arguments = ""
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, XAIModelId, xaiDefaultModelId, xaiModels } from "@shared/api"
|
||||
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 { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface XAIHandlerOptions extends CommonApiHandlerOptions {
|
||||
xaiApiKey?: string
|
||||
@@ -40,7 +42,7 @@ export class XAIHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.getModel().id
|
||||
// ensure reasoning effort is either "low" or "high" for grok-3-mini
|
||||
@@ -59,8 +61,11 @@ export class XAIHandler implements ApiHandler {
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: reasoningEffort,
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -70,6 +75,10 @@ export class XAIHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if (!shouldSkipReasoningForModel(modelId)) {
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
mainlandZAiModels,
|
||||
} from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { version as extensionVersion } from "../../../../package.json"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface ZAiHandlerOptions extends CommonApiHandlerOptions {
|
||||
zaiApiLine?: string
|
||||
@@ -72,7 +74,7 @@ export class ZAiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
@@ -85,8 +87,11 @@ export class ZAiHandler implements ApiHandler {
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -96,6 +101,10 @@ export class ZAiHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
|
||||
/**
|
||||
* Converts an OpenAI ChatCompletionTool into an Anthropic Tool definition
|
||||
*/
|
||||
export function openAIToolToAnthropic(openAITool: OpenAITool): AnthropicTool {
|
||||
const func = openAITool.function
|
||||
|
||||
return {
|
||||
name: func.name,
|
||||
description: func.description || "",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: func.parameters?.properties || {},
|
||||
required: func.parameters?.required || [],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,22 @@ export function convertAnthropicContentToGemini(content: string | Anthropic.Cont
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported content block type: ${block.type}`)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function convertToOpenAiMessages(
|
||||
|
||||
if (typeof toolMessage.content === "string") {
|
||||
content = toolMessage.content
|
||||
} else {
|
||||
} else if (Array.isArray(toolMessage.content)) {
|
||||
content =
|
||||
toolMessage.content
|
||||
?.map((part) => {
|
||||
@@ -56,6 +56,9 @@ export function convertToOpenAiMessages(
|
||||
return part.text
|
||||
})
|
||||
.join("\n") ?? ""
|
||||
} else {
|
||||
// Handle undefined content
|
||||
content = ""
|
||||
}
|
||||
openAiMessages.push({
|
||||
role: "tool",
|
||||
@@ -70,15 +73,15 @@ export function convertToOpenAiMessages(
|
||||
// Therefore we need to send these images after the tool result messages
|
||||
// NOTE: it's actually okay to have multiple user messages in a row, the model will treat them as a continuation of the same input (this way works better than combining them into one message, since the tool result specifically mentions (see following user message for image)
|
||||
// UPDATE v2.0: we don't use tools anymore, but if we did it's important to note that the openrouter prompt caching mechanism requires one user message at a time, so we would need to add these images to the user content array instead.
|
||||
// if (toolResultImages.length > 0) {
|
||||
// openAiMessages.push({
|
||||
// role: "user",
|
||||
// content: toolResultImages.map((part) => ({
|
||||
// type: "image_url",
|
||||
// image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
|
||||
// })),
|
||||
// })
|
||||
// }
|
||||
if (toolResultImages.length > 0) {
|
||||
openAiMessages.push({
|
||||
role: "user",
|
||||
content: toolResultImages.map((part) => ({
|
||||
type: "image_url",
|
||||
image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
// Process non-tool messages
|
||||
if (nonToolMessages.length > 0) {
|
||||
@@ -157,7 +160,7 @@ export function convertToOpenAiMessages(
|
||||
role: "assistant",
|
||||
content,
|
||||
// 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,
|
||||
tool_calls: tool_calls?.length > 0 ? tool_calls : undefined,
|
||||
// @ts-ignore-next-line
|
||||
reasoning_details: reasoningDetails.length > 0 ? consolidateReasoningDetails(reasoningDetails) : undefined,
|
||||
})
|
||||
@@ -272,6 +275,9 @@ function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[]): Reaso
|
||||
return consolidated
|
||||
}
|
||||
|
||||
// Unique name to use to filter out tool call that cannot be parsed correctly
|
||||
const UNIQUE_ERROR_TOOL_NAME = "_cline_error_unknown_function_"
|
||||
|
||||
// Convert OpenAI response to Anthropic format
|
||||
export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.ChatCompletion): Anthropic.Messages.Message {
|
||||
const openAiMessage = completion.choices[0].message
|
||||
@@ -308,24 +314,32 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
cache_read_input_tokens: null,
|
||||
},
|
||||
}
|
||||
|
||||
if (openAiMessage.tool_calls && openAiMessage.tool_calls.length > 0) {
|
||||
anthropicMessage.content.push(
|
||||
...openAiMessage.tool_calls.map((toolCall): Anthropic.ToolUseBlock => {
|
||||
let parsedInput = {}
|
||||
try {
|
||||
parsedInput = JSON.parse(toolCall.function.arguments || "{}")
|
||||
} catch (error) {
|
||||
console.error("Failed to parse tool arguments:", error)
|
||||
}
|
||||
return {
|
||||
type: "tool_use",
|
||||
id: toolCall.id,
|
||||
name: toolCall.function.name,
|
||||
input: parsedInput,
|
||||
}
|
||||
}),
|
||||
)
|
||||
try {
|
||||
if (openAiMessage?.tool_calls?.length) {
|
||||
anthropicMessage.content.push(
|
||||
...openAiMessage.tool_calls
|
||||
.map((toolCall): Anthropic.ToolUseBlock => {
|
||||
const parsedName = toolCall.type === "function" && toolCall.function.name
|
||||
let parsedInput = toolCall.function.arguments
|
||||
try {
|
||||
parsedInput = JSON.parse(toolCall.function.arguments || "{}")
|
||||
} catch (error) {
|
||||
console.error("Failed to parse tool arguments:", error)
|
||||
}
|
||||
return {
|
||||
type: "tool_use",
|
||||
id: toolCall.id,
|
||||
name: parsedName || UNIQUE_ERROR_TOOL_NAME,
|
||||
input: parsedInput,
|
||||
}
|
||||
})
|
||||
// Filter out any tool uses with the UNIQUE_ERROR_TOOL_NAME, which indicates a parsing error
|
||||
.filter((toolUse) => toolUse.name !== UNIQUE_ERROR_TOOL_NAME),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process tool calls:", error)
|
||||
}
|
||||
|
||||
return anthropicMessage
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { convertToOpenAiMessages } from "./openai-format"
|
||||
import { convertToR1Format } from "./r1-format"
|
||||
import { getOpenAIToolParams } from "./tool-call-processor"
|
||||
|
||||
export async function createOpenRouterStream(
|
||||
client: OpenAI,
|
||||
@@ -18,6 +20,7 @@ export async function createOpenRouterStream(
|
||||
reasoningEffort?: string,
|
||||
thinkingBudgetTokens?: number,
|
||||
openRouterProviderSorting?: string,
|
||||
tools?: Array<ChatCompletionTool>,
|
||||
) {
|
||||
// Convert Anthropic messages to OpenAI format
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
@@ -185,6 +188,7 @@ export async function createOpenRouterStream(
|
||||
...(openRouterProviderSorting && !providerPreferences ? { provider: { sort: openRouterProviderSorting } } : {}),
|
||||
...(providerPreferences ? { provider: providerPreferences } : {}),
|
||||
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -6,6 +6,7 @@ export type ApiStreamChunk =
|
||||
| ApiStreamAnthropicThinkingChunk
|
||||
| ApiStreamAnthropicRedactedThinkingChunk
|
||||
| ApiStreamUsageChunk
|
||||
| ApiStreamToolCallsChunk
|
||||
|
||||
export interface ApiStreamTextChunk {
|
||||
type: "text"
|
||||
@@ -42,3 +43,18 @@ export interface ApiStreamUsageChunk {
|
||||
thoughtsTokenCount?: number // openrouter
|
||||
totalCost?: number // openrouter
|
||||
}
|
||||
|
||||
export interface ApiStreamToolCallsChunk {
|
||||
type: "tool_calls"
|
||||
tool_call: ApiStreamToolCall
|
||||
}
|
||||
|
||||
export interface ApiStreamToolCall {
|
||||
call_id?: string // The call / request ID associated with this tool call
|
||||
// Information about the tool being called
|
||||
function: {
|
||||
id?: string // The tool call ID
|
||||
name?: string
|
||||
arguments?: any
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import type {
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionToolChoiceOption,
|
||||
ChatCompletionTool as OpenAITool,
|
||||
} from "openai/resources/chat/completions"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import type { ApiStreamToolCallsChunk } from "./stream"
|
||||
|
||||
/**
|
||||
* Helper class to process tool call deltas from OpenAI-compatible streaming responses.
|
||||
* Handles accumulating tool call ID and name across multiple delta chunks,
|
||||
* and yields properly formatted tool call chunks when arguments are received.
|
||||
*/
|
||||
export class ToolCallProcessor {
|
||||
private lastToolCall: { id: string; name: string }
|
||||
|
||||
constructor() {
|
||||
this.lastToolCall = { id: "", name: "" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Process tool call deltas from a chunk and yield formatted tool call chunks.
|
||||
* @param toolCallDeltas - Array of tool call deltas from the chunk
|
||||
* @yields Formatted tool call chunks ready to be yielded in the API stream
|
||||
*/
|
||||
*processToolCallDeltas(
|
||||
toolCallDeltas: ChatCompletionChunk.Choice.Delta.ToolCall[] | undefined,
|
||||
): Generator<ApiStreamToolCallsChunk> {
|
||||
if (!toolCallDeltas) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const toolCallDelta of toolCallDeltas) {
|
||||
// Accumulate the tool call ID if present
|
||||
if (toolCallDelta.id) {
|
||||
this.lastToolCall.id = toolCallDelta.id
|
||||
}
|
||||
|
||||
// Accumulate the function name if present
|
||||
if (toolCallDelta.function?.name) {
|
||||
Logger.debug(`[ToolCallProcessor] Native Tool Called: ${toolCallDelta.function.name}`)
|
||||
this.lastToolCall.name = toolCallDelta.function.name
|
||||
}
|
||||
|
||||
// Only yield when we have all required fields: id, name, and arguments
|
||||
if (this.lastToolCall.id && this.lastToolCall.name && toolCallDelta.function?.arguments) {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
...toolCallDelta,
|
||||
function: {
|
||||
...toolCallDelta.function,
|
||||
id: this.lastToolCall.id,
|
||||
name: this.lastToolCall.name,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the internal state. Call this when starting a new message.
|
||||
*/
|
||||
reset(): void {
|
||||
this.lastToolCall = { id: "", name: "" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current accumulated tool call state (useful for debugging).
|
||||
*/
|
||||
getState(): { id: string; name: string } {
|
||||
return { ...this.lastToolCall }
|
||||
}
|
||||
}
|
||||
|
||||
export function getOpenAIToolParams(tools?: OpenAITool[]) {
|
||||
return tools?.length
|
||||
? {
|
||||
tools,
|
||||
tool_choice: tools ? ("auto" as ChatCompletionToolChoiceOption) : undefined,
|
||||
parallel_tool_calls: tools ? true : undefined,
|
||||
}
|
||||
: {
|
||||
tools: undefined,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { JSONParser } from "@streamparser/json"
|
||||
import { McpHub } from "@/services/mcp/McpHub"
|
||||
import { CLINE_MCP_TOOL_IDENTIFIER } from "@/shared/mcp"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
|
||||
export interface PendingToolUse {
|
||||
id: string
|
||||
name: string
|
||||
input: string
|
||||
parsedInput?: unknown
|
||||
jsonParser?: JSONParser
|
||||
call_id?: string
|
||||
}
|
||||
|
||||
interface ToolUseDeltaBlock {
|
||||
id?: string
|
||||
type?: string
|
||||
name?: string
|
||||
input?: string
|
||||
}
|
||||
|
||||
const ESCAPE_MAP: Record<string, string> = {
|
||||
"\\n": "\n",
|
||||
"\\t": "\t",
|
||||
"\\r": "\r",
|
||||
'\\"': '"',
|
||||
"\\\\": "\\",
|
||||
}
|
||||
|
||||
const ESCAPE_PATTERN = /\\[ntr"\\]/g
|
||||
|
||||
/**
|
||||
* Handles streaming tool use blocks and converts them to Anthropic.ToolUseBlockParam format
|
||||
*/
|
||||
export class ToolUseHandler {
|
||||
private pendingToolUses = new Map<string, PendingToolUse>()
|
||||
|
||||
processToolUseDelta(delta: ToolUseDeltaBlock, call_id?: string): void {
|
||||
if (delta.type !== "tool_use" || !delta.id) {
|
||||
return
|
||||
}
|
||||
|
||||
let pending = this.pendingToolUses.get(delta.id)
|
||||
if (!pending) {
|
||||
pending = this.createPendingToolUse(delta.id, delta.name || "", call_id)
|
||||
}
|
||||
|
||||
if (delta.name) {
|
||||
pending.name = delta.name
|
||||
}
|
||||
if (delta.input) {
|
||||
pending.input += delta.input
|
||||
try {
|
||||
pending.jsonParser?.write(delta.input)
|
||||
} catch {
|
||||
// Expected during streaming
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFinalizedToolUse(id: string): Anthropic.ToolUseBlockParam | undefined {
|
||||
const pending = this.pendingToolUses.get(id)
|
||||
if (!pending?.name) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let input: unknown = {}
|
||||
if (pending.parsedInput != null) {
|
||||
input = pending.parsedInput
|
||||
} else if (pending.input) {
|
||||
try {
|
||||
input = JSON.parse(pending.input)
|
||||
} catch {
|
||||
input = this.extractPartialJsonFields(pending.input)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "tool_use",
|
||||
id: pending.id,
|
||||
name: pending.name,
|
||||
input,
|
||||
}
|
||||
}
|
||||
|
||||
getAllFinalizedToolUses(): Anthropic.ToolUseBlockParam[] {
|
||||
const results: Anthropic.ToolUseBlockParam[] = []
|
||||
for (const id of this.pendingToolUses.keys()) {
|
||||
const toolUse = this.getFinalizedToolUse(id)
|
||||
if (toolUse) {
|
||||
results.push(toolUse)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
hasToolUse(id: string): boolean {
|
||||
return this.pendingToolUses.has(id)
|
||||
}
|
||||
|
||||
getPartialToolUsesAsContent(): ToolUse[] {
|
||||
const results: ToolUse[] = []
|
||||
|
||||
for (const pending of this.pendingToolUses.values()) {
|
||||
if (!pending.name) {
|
||||
continue
|
||||
}
|
||||
|
||||
let input: any = {}
|
||||
if (pending.parsedInput != null) {
|
||||
input = pending.parsedInput
|
||||
} else if (pending.input) {
|
||||
try {
|
||||
input = JSON.parse(pending.input)
|
||||
} catch {
|
||||
input = this.extractPartialJsonFields(pending.input)
|
||||
}
|
||||
}
|
||||
|
||||
if (pending.name.includes(CLINE_MCP_TOOL_IDENTIFIER)) {
|
||||
const [key, toolName] = pending.name.split(CLINE_MCP_TOOL_IDENTIFIER)
|
||||
results.push({
|
||||
type: "tool_use",
|
||||
name: ClineDefaultTool.MCP_USE,
|
||||
params: {
|
||||
server_name: McpHub.getMcpServerByKey(key),
|
||||
tool_name: toolName,
|
||||
arguments: JSON.stringify(input),
|
||||
},
|
||||
partial: true,
|
||||
})
|
||||
} else {
|
||||
const params: Record<string, string> = {}
|
||||
if (typeof input === "object") {
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
params[key] = typeof value === "string" ? value : JSON.stringify(value)
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
type: "tool_use",
|
||||
name: pending.name as ClineDefaultTool,
|
||||
params: params as any,
|
||||
partial: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.pendingToolUses.clear()
|
||||
}
|
||||
|
||||
private createPendingToolUse(id: string, name: string, call_id?: string): PendingToolUse {
|
||||
const jsonParser = new JSONParser()
|
||||
const pending: PendingToolUse = {
|
||||
id,
|
||||
name,
|
||||
input: "",
|
||||
parsedInput: undefined,
|
||||
jsonParser,
|
||||
call_id,
|
||||
}
|
||||
|
||||
jsonParser.onValue = (info: any) => {
|
||||
if (info.stack.length === 0 && info.value && typeof info.value === "object") {
|
||||
pending.parsedInput = info.value
|
||||
}
|
||||
}
|
||||
|
||||
jsonParser.onError = () => {}
|
||||
|
||||
this.pendingToolUses.set(id, pending)
|
||||
return pending
|
||||
}
|
||||
|
||||
private extractPartialJsonFields(partialJson: string): Record<string, any> {
|
||||
const result: Record<string, any> = {}
|
||||
const pattern = /"(\w+)":\s*"((?:[^"\\]|\\.)*)(?:")?/g
|
||||
|
||||
for (const match of partialJson.matchAll(pattern)) {
|
||||
result[match[1]] = match[2].replace(ESCAPE_PATTERN, (m) => ESCAPE_MAP[m])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { getOpenAIToolParams } from "./tool-call-processor"
|
||||
|
||||
export async function createVercelAIGatewayStream(
|
||||
client: OpenAI,
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
model: { id: string; info: ModelInfo },
|
||||
tools?: OpenAITool[],
|
||||
) {
|
||||
// Convert Anthropic messages to OpenAI format
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
@@ -49,6 +52,7 @@ export async function createVercelAIGatewayStream(
|
||||
temperature: 0.7,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
...getOpenAIToolParams(tools),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -13,6 +13,7 @@ export const toolParamNames = [
|
||||
"command",
|
||||
"requires_approval",
|
||||
"path",
|
||||
"absolutePath",
|
||||
"content",
|
||||
"diff",
|
||||
"regex",
|
||||
@@ -39,6 +40,7 @@ export const toolParamNames = [
|
||||
"needs_more_exploration",
|
||||
"task_progress",
|
||||
"timeout",
|
||||
"input",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
||||
@@ -329,10 +329,128 @@ export class ContextManager {
|
||||
|
||||
const updatedMessages = this.applyContextHistoryUpdates(messages, deletedRange ? deletedRange[1] + 1 : 2)
|
||||
|
||||
// Validate and fix tool_use/tool_result pairing
|
||||
this.ensureToolResultsFollowToolUse(updatedMessages)
|
||||
|
||||
// OLD NOTE: if you try to console log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
|
||||
return updatedMessages
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that every tool_use block in assistant messages has a corresponding tool_result in the next user message,
|
||||
* and that tool_result blocks immediately follow their corresponding tool_use blocks
|
||||
*/
|
||||
private ensureToolResultsFollowToolUse(messages: Anthropic.Messages.MessageParam[]): void {
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
const message = messages[i]
|
||||
|
||||
// Only process assistant messages with content
|
||||
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract tool_use IDs in order
|
||||
const toolUseIds: string[] = []
|
||||
for (const block of message.content) {
|
||||
if (block.type === "tool_use" && block.id) {
|
||||
toolUseIds.push(block.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if no tool_use blocks found
|
||||
if (toolUseIds.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const nextMessage = messages[i + 1]
|
||||
|
||||
// Skip if next message is not a user message
|
||||
if (nextMessage.role !== "user") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure content is an array
|
||||
if (!Array.isArray(nextMessage.content)) {
|
||||
nextMessage.content = []
|
||||
}
|
||||
|
||||
// Separate tool_results from other blocks in a single pass
|
||||
const toolResultMap = new Map<string, Anthropic.Messages.ToolResultBlockParam>()
|
||||
const otherBlocks: Anthropic.Messages.ContentBlockParam[] = []
|
||||
let needsUpdate = false
|
||||
|
||||
for (const block of nextMessage.content) {
|
||||
if (block.type === "tool_result" && block.tool_use_id) {
|
||||
toolResultMap.set(block.tool_use_id, block)
|
||||
} else {
|
||||
otherBlocks.push(block)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if reordering is needed (tool_results not at start in correct order)
|
||||
if (toolResultMap.size > 0) {
|
||||
let expectedIndex = 0
|
||||
for (let j = 0; j < nextMessage.content.length && expectedIndex < toolUseIds.length; j++) {
|
||||
const block = nextMessage.content[j]
|
||||
if (block.type === "tool_result" && block.tool_use_id === toolUseIds[expectedIndex]) {
|
||||
expectedIndex++
|
||||
} else if (block.type === "tool_result" || expectedIndex < toolUseIds.length) {
|
||||
needsUpdate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!needsUpdate && expectedIndex < toolResultMap.size) {
|
||||
needsUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing tool_results
|
||||
for (const toolUseId of toolUseIds) {
|
||||
if (!toolResultMap.has(toolUseId)) {
|
||||
toolResultMap.set(toolUseId, {
|
||||
type: "tool_result",
|
||||
tool_use_id: toolUseId,
|
||||
content: "result missing",
|
||||
})
|
||||
needsUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
// Only modify if changes are needed
|
||||
if (!needsUpdate) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build new content: tool_results first (in toolUseIds order), then other blocks
|
||||
const newContent: Anthropic.Messages.ContentBlockParam[] = []
|
||||
|
||||
// Add tool_results in the order of toolUseIds
|
||||
const processedToolResults = new Set<string>()
|
||||
for (const toolUseId of toolUseIds) {
|
||||
const toolResult = toolResultMap.get(toolUseId)
|
||||
if (toolResult) {
|
||||
newContent.push(toolResult)
|
||||
processedToolResults.add(toolUseId)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// Clone and update the message
|
||||
const clonedMessage = cloneDeep(nextMessage)
|
||||
clonedMessage.content = newContent
|
||||
messages[i + 1] = clonedMessage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* applies deletedRange truncation and other alterations based on changes in this.contextHistoryUpdates
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { toRequestyServiceUrl } from "@shared/providers/requesty"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Initiates Requesty auth with optional custom base URL
|
||||
*/
|
||||
export async function requestyAuthClicked(_: Controller, req: StringRequest): Promise<Empty> {
|
||||
const customBaseUrl = req.value || undefined
|
||||
const callbackUrl = await HostProvider.get().getCallbackUrl()
|
||||
const baseUrl = toRequestyServiceUrl(customBaseUrl, "app")
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error("Invalid Requesty base URL")
|
||||
}
|
||||
|
||||
const authUrl = new URL(`oauth/authorize?callback_url=${callbackUrl}/requesty`, baseUrl)
|
||||
|
||||
await openExternal(authUrl.toString())
|
||||
|
||||
return {}
|
||||
}
|
||||
+107
-94
@@ -75,6 +75,9 @@ export class Controller {
|
||||
private backgroundCommandRunning = false
|
||||
private backgroundCommandTaskId?: string
|
||||
|
||||
// Flag to prevent duplicate cancellations from spam clicking
|
||||
private cancelInProgress = false
|
||||
|
||||
// Shell integration warning tracker
|
||||
private shellIntegrationWarningTracker: {
|
||||
timestamps: number[]
|
||||
@@ -333,6 +336,12 @@ export class Controller {
|
||||
taskLockAcquired,
|
||||
})
|
||||
|
||||
if (historyItem) {
|
||||
this.task.resumeTaskFromHistory()
|
||||
} else if (task || images || files) {
|
||||
this.task.startTask(task, images, files)
|
||||
}
|
||||
|
||||
return this.task.taskId
|
||||
}
|
||||
|
||||
@@ -410,14 +419,28 @@ export class Controller {
|
||||
}
|
||||
|
||||
async cancelTask() {
|
||||
if (this.task) {
|
||||
// Prevent duplicate cancellations from spam clicking
|
||||
if (this.cancelInProgress) {
|
||||
console.log(`[Controller.cancelTask] Cancellation already in progress, ignoring duplicate request`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.task) {
|
||||
return
|
||||
}
|
||||
|
||||
// Set flag to prevent concurrent cancellations
|
||||
this.cancelInProgress = true
|
||||
|
||||
try {
|
||||
this.updateBackgroundCommandState(false)
|
||||
const { historyItem } = await this.getTaskWithId(this.task.taskId)
|
||||
|
||||
try {
|
||||
await this.task.abortTask()
|
||||
} catch (error) {
|
||||
console.error("Failed to abort task", error)
|
||||
}
|
||||
|
||||
await pWaitFor(
|
||||
() =>
|
||||
this.task === undefined ||
|
||||
@@ -430,13 +453,38 @@ export class Controller {
|
||||
).catch(() => {
|
||||
console.error("Failed to abort task")
|
||||
})
|
||||
|
||||
if (this.task) {
|
||||
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
|
||||
this.task.taskState.abandoned = true
|
||||
}
|
||||
await this.initTask(undefined, undefined, undefined, historyItem) // clears task again, so we need to abortTask manually above
|
||||
// Dont send the state to the webview, the new Cline instance will send state when it's ready.
|
||||
// Sending the state here sent an empty messages array to webview leading to virtuoso having to reload the entire list
|
||||
|
||||
// Small delay to ensure state manager has persisted the history update
|
||||
//await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// NOW try to get history after abort has finished (hook may have saved messages)
|
||||
let historyItem: HistoryItem | undefined
|
||||
try {
|
||||
const result = await this.getTaskWithId(this.task.taskId)
|
||||
historyItem = result.historyItem
|
||||
} catch (error) {
|
||||
// Task not in history yet (new task with no messages); catch the
|
||||
// error to enable the agent to continue making progress.
|
||||
console.log(`[Controller.cancelTask] Task not found in history: ${error}`)
|
||||
}
|
||||
|
||||
// Only re-initialize if we found a history item, otherwise just clear
|
||||
if (historyItem) {
|
||||
// Re-initialize task to keep it visible in UI with resume button
|
||||
await this.initTask(undefined, undefined, undefined, historyItem, undefined)
|
||||
} else {
|
||||
await this.clearTask()
|
||||
}
|
||||
|
||||
await this.postStateToWebview()
|
||||
} finally {
|
||||
// Always clear the flag, even if cancellation fails
|
||||
this.cancelInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,99 +645,41 @@ export class Controller {
|
||||
}
|
||||
|
||||
// MCP Marketplace
|
||||
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
|
||||
try {
|
||||
const response = await axios.get(`${ClineEnv.config().mcpBaseUrl}/marketplace`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
private async fetchMcpMarketplaceFromApi(): Promise<McpMarketplaceCatalog> {
|
||||
const response = await axios.get(`${ClineEnv.config().mcpBaseUrl}/marketplace`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "cline-vscode-extension",
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error("Invalid response from MCP marketplace API")
|
||||
}
|
||||
|
||||
const catalog: McpMarketplaceCatalog = {
|
||||
items: (response.data || []).map((item: any) => ({
|
||||
...item,
|
||||
githubStars: item.githubStars ?? 0,
|
||||
downloadCount: item.downloadCount ?? 0,
|
||||
tags: item.tags ?? [],
|
||||
})),
|
||||
}
|
||||
|
||||
// Store in cache file
|
||||
await writeMcpMarketplaceCatalogToCache(catalog)
|
||||
return catalog
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch MCP marketplace:", error)
|
||||
if (!silent) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
return undefined
|
||||
if (!response.data) {
|
||||
throw new Error("Invalid response from MCP marketplace API")
|
||||
}
|
||||
|
||||
const catalog: McpMarketplaceCatalog = {
|
||||
items: (response.data || []).map((item: any) => ({
|
||||
...item,
|
||||
githubStars: item.githubStars ?? 0,
|
||||
downloadCount: item.downloadCount ?? 0,
|
||||
tags: item.tags ?? [],
|
||||
})),
|
||||
}
|
||||
|
||||
// Store in cache file
|
||||
await writeMcpMarketplaceCatalogToCache(catalog)
|
||||
return catalog
|
||||
}
|
||||
|
||||
private async fetchMcpMarketplaceFromApiRPC(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
|
||||
async refreshMcpMarketplace(sendCatalogEvent: boolean): Promise<McpMarketplaceCatalog | undefined> {
|
||||
try {
|
||||
const response = await axios.get(`${ClineEnv.config().mcpBaseUrl}/marketplace`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "cline-vscode-extension",
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error("Invalid response from MCP marketplace API")
|
||||
}
|
||||
|
||||
const catalog: McpMarketplaceCatalog = {
|
||||
items: (response.data || []).map((item: any) => ({
|
||||
...item,
|
||||
githubStars: item.githubStars ?? 0,
|
||||
downloadCount: item.downloadCount ?? 0,
|
||||
tags: item.tags ?? [],
|
||||
})),
|
||||
}
|
||||
|
||||
// Store in cache file
|
||||
await writeMcpMarketplaceCatalogToCache(catalog)
|
||||
return catalog
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch MCP marketplace:", error)
|
||||
if (!silent) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async silentlyRefreshMcpMarketplace() {
|
||||
try {
|
||||
const catalog = await this.fetchMcpMarketplaceFromApi(true)
|
||||
if (catalog) {
|
||||
const catalog = await this.fetchMcpMarketplaceFromApi()
|
||||
if (catalog && sendCatalogEvent) {
|
||||
await sendMcpMarketplaceCatalogEvent(catalog)
|
||||
}
|
||||
return catalog
|
||||
} catch (error) {
|
||||
console.error("Failed to silently refresh MCP marketplace:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC variant that silently refreshes the MCP marketplace catalog and returns the result
|
||||
* Unlike silentlyRefreshMcpMarketplace, this doesn't send a message to the webview
|
||||
* @returns MCP marketplace catalog or undefined if refresh failed
|
||||
*/
|
||||
async silentlyRefreshMcpMarketplaceRPC() {
|
||||
try {
|
||||
return await this.fetchMcpMarketplaceFromApiRPC(true)
|
||||
} catch (error) {
|
||||
console.error("Failed to silently refresh MCP marketplace (RPC):", error)
|
||||
console.error("Failed to refresh MCP marketplace:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -730,6 +720,25 @@ export class Controller {
|
||||
// Dont send settingsButtonClicked because its bad ux if user is on welcome
|
||||
}
|
||||
|
||||
// Requesty
|
||||
|
||||
async handleRequestyCallback(code: string) {
|
||||
const requesty: ApiProvider = "requesty"
|
||||
const currentMode = this.stateManager.getGlobalSettingsKey("mode")
|
||||
const currentApiConfiguration = this.stateManager.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...currentApiConfiguration,
|
||||
planModeApiProvider: requesty,
|
||||
actModeApiProvider: requesty,
|
||||
requestyApiKey: code,
|
||||
}
|
||||
this.stateManager.setApiConfiguration(updatedConfig)
|
||||
await this.postStateToWebview()
|
||||
if (this.task) {
|
||||
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
|
||||
}
|
||||
}
|
||||
|
||||
// Read OpenRouter models from disk cache
|
||||
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
@@ -846,9 +855,9 @@ export class Controller {
|
||||
const vscodeTerminalExecutionMode = this.stateManager.getGlobalStateKey("vscodeTerminalExecutionMode")
|
||||
const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile")
|
||||
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
|
||||
const welcomeViewCompleted = Boolean(
|
||||
this.stateManager.getGlobalStateKey("welcomeViewCompleted") || this.authService.getInfo()?.user?.uid,
|
||||
)
|
||||
// Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts
|
||||
const welcomeViewCompleted = !!this.stateManager.getGlobalStateKey("welcomeViewCompleted")
|
||||
|
||||
const customPrompt = this.stateManager.getGlobalSettingsKey("customPrompt")
|
||||
const mcpResponsesCollapsed = this.stateManager.getGlobalStateKey("mcpResponsesCollapsed")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
|
||||
@@ -925,7 +934,7 @@ export class Controller {
|
||||
vscodeTerminalExecutionMode: vscodeTerminalExecutionMode,
|
||||
defaultTerminalProfile,
|
||||
isNewUser,
|
||||
welcomeViewCompleted: welcomeViewCompleted as boolean, // Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts
|
||||
welcomeViewCompleted,
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
maxConsecutiveMistakes,
|
||||
@@ -954,6 +963,10 @@ export class Controller {
|
||||
remoteConfigSettings: this.stateManager.getRemoteConfigSettings(),
|
||||
lastDismissedCliBannerVersion,
|
||||
subagentsEnabled,
|
||||
nativeToolCallSetting: {
|
||||
user: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
featureFlag: featureFlagsService.getNativeToolCallEnabled(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,14 @@ import type { Controller } from "../index"
|
||||
export async function refreshMcpMarketplace(controller: Controller, _request: EmptyRequest): Promise<McpMarketplaceCatalog> {
|
||||
try {
|
||||
// Call the RPC variant which returns the result directly
|
||||
const catalog = await controller.silentlyRefreshMcpMarketplaceRPC()
|
||||
|
||||
const catalog = await controller.refreshMcpMarketplace(false /* sendCatalogEvent */)
|
||||
if (catalog) {
|
||||
// Types are structurally identical, use direct type assertion
|
||||
return catalog as McpMarketplaceCatalog
|
||||
}
|
||||
|
||||
// Return empty catalog if nothing was fetched
|
||||
return McpMarketplaceCatalog.create({ items: [] })
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh MCP marketplace:", error)
|
||||
return McpMarketplaceCatalog.create({ items: [] })
|
||||
}
|
||||
// Return empty catalog if nothing was fetched
|
||||
return { items: [] }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import axios from "axios"
|
||||
import cloneDeep from "clone-deep"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { CLAUDE_SONNET_1M_TIERS, openRouterClaudeSonnet41mModelId, openRouterClaudeSonnet451mModelId } from "@/shared/api"
|
||||
import { Controller } from ".."
|
||||
import type { Controller } from ".."
|
||||
|
||||
type OpenRouterSupportedParams =
|
||||
| "frequency_penalty"
|
||||
@@ -59,7 +59,7 @@ interface OpenRouterRawModelInfo {
|
||||
input_cache_read: string
|
||||
input_cache_write: string
|
||||
} | null
|
||||
thinking_config: any | null
|
||||
thinking_config: Record<string, unknown> | null
|
||||
supports_global_endpoint: boolean | null
|
||||
tiers: any[] | null
|
||||
supported_parameters?: OpenRouterSupportedParams[] | null
|
||||
@@ -88,6 +88,7 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
|
||||
for (const rawModel of rawModels as OpenRouterRawModelInfo[]) {
|
||||
const supportThinking = rawModel.supported_parameters?.some((p) => p === "include_reasoning")
|
||||
const modelInfo: ModelInfo = {
|
||||
name: rawModel.name,
|
||||
maxTokens: rawModel.top_provider?.max_completion_tokens ?? 0,
|
||||
contextWindow: rawModel.context_length ?? 0,
|
||||
supportsImages: rawModel.architecture?.modality?.includes("image") ?? false,
|
||||
@@ -97,7 +98,7 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
|
||||
cacheWritesPrice: parsePrice(rawModel.pricing?.input_cache_write),
|
||||
cacheReadsPrice: parsePrice(rawModel.pricing?.input_cache_read),
|
||||
description: rawModel.description ?? "",
|
||||
thinkingConfig: supportThinking ? (rawModel.thinking_config ?? {}) : undefined,
|
||||
thinkingConfig: (supportThinking && rawModel.thinking_config) || undefined,
|
||||
supportsGlobalEndpoint: rawModel.supports_global_endpoint ?? undefined,
|
||||
tiers: rawModel.tiers ?? undefined,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
import { buildApiHandler } from "@/core/api"
|
||||
import { ApiHandlerOptions, ApiHandlerSecrets, ApiProvider } from "@/shared/api"
|
||||
import { UpdateApiConfigurationRequestNew } from "@/shared/proto/index.cline"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Updates API configuration
|
||||
* @param controller The controller instance
|
||||
* @param request The update API configuration request
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function updateApiConfiguration(controller: Controller, request: UpdateApiConfigurationRequestNew): Promise<Empty> {
|
||||
try {
|
||||
const { options: protoOptions, secrets: protoSecrets } = request
|
||||
|
||||
const secrets: Partial<ApiHandlerSecrets> = {}
|
||||
if (protoSecrets) {
|
||||
const filteredSecrets = Object.fromEntries(Object.entries(protoSecrets).filter(([_, value]) => value !== undefined))
|
||||
Object.assign(secrets, filteredSecrets)
|
||||
}
|
||||
|
||||
const options: Partial<ApiHandlerOptions> & { planModeApiProvider?: ApiProvider; actModeApiProvider?: ApiProvider } = {}
|
||||
if (protoOptions) {
|
||||
// Extract fields requiring conversion or special handling
|
||||
const {
|
||||
// Fields requiring enum conversion
|
||||
planModeApiProvider,
|
||||
actModeApiProvider,
|
||||
|
||||
// Fields requiring special handling
|
||||
openAiHeaders,
|
||||
...simpleOptions
|
||||
} = protoOptions
|
||||
|
||||
// Batch update for simple pass-through fields
|
||||
const filteredOptions = Object.fromEntries(Object.entries(simpleOptions).filter(([_, value]) => value !== undefined))
|
||||
Object.assign(options, filteredOptions)
|
||||
|
||||
// Handle openAiHeaders (skip empty objects)
|
||||
if (openAiHeaders && Object.keys(openAiHeaders).length > 0) {
|
||||
options.openAiHeaders = openAiHeaders
|
||||
}
|
||||
|
||||
// Convert proto ApiProvider enums to native string types
|
||||
if (planModeApiProvider !== undefined) {
|
||||
options.planModeApiProvider = convertProtoToApiProvider(planModeApiProvider)
|
||||
}
|
||||
if (actModeApiProvider !== undefined) {
|
||||
options.actModeApiProvider = convertProtoToApiProvider(actModeApiProvider)
|
||||
}
|
||||
}
|
||||
|
||||
// Update storage using batch methods
|
||||
if (Object.keys(secrets).length > 0) {
|
||||
controller.stateManager.setSecretsBatch(secrets)
|
||||
}
|
||||
if (Object.keys(options).length > 0) {
|
||||
controller.stateManager.setGlobalStateBatch(options)
|
||||
}
|
||||
|
||||
// Update the task's API handler if there's an active task
|
||||
if (controller.task) {
|
||||
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
|
||||
// Combine secrets and options for the API handler
|
||||
const apiConfigForHandler = { ...secrets, ...options, ulid: controller.task.ulid }
|
||||
controller.task.api = buildApiHandler(apiConfigForHandler, currentMode)
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error(`Failed to update API configuration: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,7 @@ export async function updateAutoApprovalSettings(controller: Controller, request
|
||||
const settings = {
|
||||
...currentSettings,
|
||||
...(request.version !== undefined && { version: request.version }),
|
||||
...(request.enabled !== undefined && { enabled: request.enabled }),
|
||||
...(request.maxRequests !== undefined && { maxRequests: request.maxRequests }),
|
||||
...(request.enableNotifications !== undefined && { enableNotifications: request.enableNotifications }),
|
||||
...(request.favorites && request.favorites.length > 0 && { favorites: request.favorites }),
|
||||
actions: {
|
||||
...currentSettings.actions,
|
||||
...(request.actions
|
||||
@@ -31,16 +28,6 @@ export async function updateAutoApprovalSettings(controller: Controller, request
|
||||
},
|
||||
}
|
||||
|
||||
if (controller.task) {
|
||||
const maxRequestsChanged =
|
||||
controller.stateManager.getGlobalSettingsKey("autoApprovalSettings").maxRequests !== settings.maxRequests
|
||||
|
||||
// Reset counter if max requests limit changed
|
||||
if (maxRequestsChanged) {
|
||||
controller.task.resetConsecutiveAutoApprovedRequestsCount()
|
||||
}
|
||||
}
|
||||
|
||||
controller.stateManager.setGlobalState("autoApprovalSettings", settings)
|
||||
|
||||
await controller.postStateToWebview()
|
||||
|
||||
@@ -327,7 +327,14 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
}
|
||||
|
||||
if (request.hooksEnabled !== undefined) {
|
||||
controller.stateManager.setGlobalState("hooksEnabled", !!request.hooksEnabled)
|
||||
const isEnabled = !!request.hooksEnabled
|
||||
|
||||
// Platform validation: Only allow enabling hooks on macOS and Linux
|
||||
if (isEnabled && process.platform === "win32") {
|
||||
throw new Error("Hooks are not yet supported on Windows")
|
||||
}
|
||||
|
||||
controller.stateManager.setGlobalState("hooksEnabled", isEnabled)
|
||||
}
|
||||
|
||||
if (request.subagentsEnabled !== undefined) {
|
||||
@@ -349,6 +356,10 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("subagentsEnabled", !!request.subagentsEnabled)
|
||||
}
|
||||
|
||||
if (request.nativeToolCallEnabled !== undefined) {
|
||||
controller.stateManager.setGlobalState("nativeToolCallEnabled", !!request.nativeToolCallEnabled)
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
|
||||
@@ -87,13 +87,9 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
|
||||
const mergedSettings = {
|
||||
...currentAutoApprovalSettings,
|
||||
...(autoApprovalSettings.version !== undefined && { version: autoApprovalSettings.version }),
|
||||
...(autoApprovalSettings.enabled !== undefined && { enabled: autoApprovalSettings.enabled }),
|
||||
...(autoApprovalSettings.maxRequests !== undefined && { maxRequests: autoApprovalSettings.maxRequests }),
|
||||
...(autoApprovalSettings.enableNotifications !== undefined && {
|
||||
enableNotifications: autoApprovalSettings.enableNotifications,
|
||||
}),
|
||||
...(autoApprovalSettings.favorites &&
|
||||
autoApprovalSettings.favorites.length > 0 && { favorites: autoApprovalSettings.favorites }),
|
||||
actions: {
|
||||
...currentAutoApprovalSettings.actions,
|
||||
...(autoApprovalSettings.actions
|
||||
@@ -212,7 +208,7 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
|
||||
}
|
||||
|
||||
// Update default terminal profile (requires terminal manager updates and notifications)
|
||||
if (defaultTerminalProfile !== undefined) {
|
||||
if (defaultTerminalProfile !== undefined && defaultTerminalProfile !== "") {
|
||||
const profileId = defaultTerminalProfile
|
||||
|
||||
// Update the terminal profile in the state
|
||||
@@ -223,6 +219,11 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
|
||||
|
||||
// Update the terminal manager of the current task if it exists
|
||||
if (controller.task) {
|
||||
// Terminal manager must exist when task is active
|
||||
if (!controller.task.terminalManager) {
|
||||
throw new Error("Cannot update terminal profile: Terminal manager missing from active task")
|
||||
}
|
||||
|
||||
// Call the updated setDefaultTerminalProfile method that returns closed terminal info
|
||||
const result = controller.task.terminalManager.setDefaultTerminalProfile(profileId)
|
||||
closedCount = result.closedCount
|
||||
|
||||
@@ -76,13 +76,9 @@ export async function updateTaskSettings(controller: Controller, request: Update
|
||||
const mergedSettings = {
|
||||
...currentAutoApprovalSettings,
|
||||
...(autoApprovalSettings.version !== undefined && { version: autoApprovalSettings.version }),
|
||||
...(autoApprovalSettings.enabled !== undefined && { enabled: autoApprovalSettings.enabled }),
|
||||
...(autoApprovalSettings.maxRequests !== undefined && { maxRequests: autoApprovalSettings.maxRequests }),
|
||||
...(autoApprovalSettings.enableNotifications !== undefined && {
|
||||
enableNotifications: autoApprovalSettings.enableNotifications,
|
||||
}),
|
||||
...(autoApprovalSettings.favorites &&
|
||||
autoApprovalSettings.favorites.length > 0 && { favorites: autoApprovalSettings.favorites }),
|
||||
actions: {
|
||||
...currentAutoApprovalSettings.actions,
|
||||
...(autoApprovalSettings.actions
|
||||
|
||||
@@ -43,13 +43,9 @@ export async function newTask(controller: Controller, request: NewTaskRequest):
|
||||
return {
|
||||
...globalSettings,
|
||||
...(incomingSettings.version !== undefined && { version: incomingSettings.version }),
|
||||
...(incomingSettings.enabled !== undefined && { enabled: incomingSettings.enabled }),
|
||||
...(incomingSettings.maxRequests !== undefined && { maxRequests: incomingSettings.maxRequests }),
|
||||
...(incomingSettings.enableNotifications !== undefined && {
|
||||
enableNotifications: incomingSettings.enableNotifications,
|
||||
}),
|
||||
...(incomingSettings.favorites &&
|
||||
incomingSettings.favorites.length > 0 && { favorites: incomingSettings.favorites }),
|
||||
actions: {
|
||||
...globalSettings.actions,
|
||||
...(incomingSettings.actions
|
||||
|
||||
@@ -209,7 +209,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
}
|
||||
|
||||
// Silently refresh MCP marketplace catalog
|
||||
controller.silentlyRefreshMcpMarketplace()
|
||||
controller.refreshMcpMarketplace(true /* sendCatalogEvent */)
|
||||
|
||||
// Initialize telemetry service with user's current setting
|
||||
controller.getStateToPostToWebview().then((state) => {
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import { getAllHooksDirs } from "../storage/disk"
|
||||
import { HookFactory, Hooks } from "./hook-factory"
|
||||
|
||||
type HookName = keyof Hooks
|
||||
|
||||
/**
|
||||
* Cached hook discovery results
|
||||
*/
|
||||
interface HookCacheEntry {
|
||||
scriptPaths: string[] // Paths to hook scripts for this hook name
|
||||
timestamp: number // When this was last scanned
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic disposable interface for resource cleanup
|
||||
*/
|
||||
interface Disposable {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic file watcher interface
|
||||
*/
|
||||
interface FileWatcher extends Disposable {
|
||||
onDidCreate(listener: () => void): void
|
||||
onDidChange(listener: () => void): void
|
||||
onDidDelete(listener: () => void): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic context interface for managing subscriptions
|
||||
*/
|
||||
interface ExtensionContext {
|
||||
subscriptions: Disposable[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton cache for hook script discovery with lazy file system watching.
|
||||
*
|
||||
* Features:
|
||||
* - Lazy watcher initialization (only when directories are accessed)
|
||||
* - Per-directory caching
|
||||
* - Automatic invalidation on file changes
|
||||
* - Graceful error handling
|
||||
* - Optional debug logging
|
||||
*/
|
||||
export class HookDiscoveryCache {
|
||||
private static instance: HookDiscoveryCache | null = null
|
||||
|
||||
// Cache: hookName -> discovered script paths
|
||||
private cache = new Map<HookName, HookCacheEntry>()
|
||||
|
||||
// Watchers: directory path -> file watcher
|
||||
private watchers = new Map<string, FileWatcher>()
|
||||
|
||||
// Directories we've tried to watch (even if watcher creation failed)
|
||||
private watchedDirs = new Set<string>()
|
||||
|
||||
// Currently scanning (to prevent concurrent scans)
|
||||
private scanning = new Set<HookName>()
|
||||
|
||||
// For disposal
|
||||
private context: ExtensionContext | null = null
|
||||
private createFileWatcher: ((dir: string) => FileWatcher | null) | null = null
|
||||
private disposed = false
|
||||
|
||||
// Debug logging (enabled via DEBUG_HOOKS env var)
|
||||
private debug = process.env.DEBUG_HOOKS === "true"
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): HookDiscoveryCache {
|
||||
if (!HookDiscoveryCache.instance) {
|
||||
HookDiscoveryCache.instance = new HookDiscoveryCache()
|
||||
}
|
||||
return HookDiscoveryCache.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with extension context for proper cleanup
|
||||
*/
|
||||
initialize(
|
||||
context: ExtensionContext,
|
||||
createFileWatcher?: (dir: string) => FileWatcher | null,
|
||||
onWorkspaceFoldersChanged?: (callback: () => void) => Disposable,
|
||||
): void {
|
||||
this.context = context
|
||||
this.createFileWatcher = createFileWatcher || null
|
||||
|
||||
// Watch for workspace changes to invalidate cache (if callback provided)
|
||||
if (onWorkspaceFoldersChanged) {
|
||||
context.subscriptions.push(
|
||||
onWorkspaceFoldersChanged(() => {
|
||||
this.log("Workspace folders changed, invalidating cache")
|
||||
this.invalidateAll()
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached hook scripts or scan if not cached
|
||||
*/
|
||||
async get(hookName: HookName): Promise<string[]> {
|
||||
this.log(`Getting hooks for ${hookName}`)
|
||||
|
||||
const cached = this.cache.get(hookName)
|
||||
if (cached) {
|
||||
this.log(`Cache hit for ${hookName}: ${cached.scriptPaths.length} scripts`)
|
||||
return cached.scriptPaths
|
||||
}
|
||||
|
||||
this.log(`Cache miss for ${hookName}, scanning...`)
|
||||
return this.scan(hookName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan for hook scripts and cache the result
|
||||
*/
|
||||
private async scan(hookName: HookName): Promise<string[]> {
|
||||
// Prevent concurrent scans of the same hook
|
||||
if (this.scanning.has(hookName)) {
|
||||
this.log(`Already scanning ${hookName}, waiting...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
return this.get(hookName)
|
||||
}
|
||||
|
||||
this.scanning.add(hookName)
|
||||
|
||||
try {
|
||||
// Get all current hooks directories
|
||||
const hooksDirs = await getAllHooksDirs()
|
||||
this.log(`Scanning ${hooksDirs.length} directories for ${hookName}`)
|
||||
|
||||
// Ensure watchers are set up for each directory (lazy initialization)
|
||||
for (const dir of hooksDirs) {
|
||||
this.ensureWatcher(dir)
|
||||
}
|
||||
|
||||
// Scan each directory for this hook
|
||||
const scriptPromises = hooksDirs.map((dir) => HookFactory.findHookInHooksDir(hookName, dir))
|
||||
|
||||
const results = await Promise.all(scriptPromises)
|
||||
const scripts = results.filter((path): path is string => path !== undefined)
|
||||
|
||||
this.log(`Found ${scripts.length} scripts for ${hookName}`)
|
||||
|
||||
// Cache the result
|
||||
this.cache.set(hookName, {
|
||||
scriptPaths: scripts,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
return scripts
|
||||
} catch (error) {
|
||||
console.error(`Error scanning for ${hookName} hooks:`, error)
|
||||
// Return empty array on error - don't break the whole system
|
||||
return []
|
||||
} finally {
|
||||
this.scanning.delete(hookName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a file watcher exists for the given directory
|
||||
*/
|
||||
private ensureWatcher(dir: string): void {
|
||||
// Skip if already watching or tried to watch
|
||||
if (this.watchedDirs.has(dir)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.watchedDirs.add(dir)
|
||||
|
||||
if (!this.context) {
|
||||
this.log(`No context available, skipping watcher for ${dir}`)
|
||||
return
|
||||
}
|
||||
|
||||
// If no watcher creation function provided, skip watching
|
||||
if (!this.createFileWatcher) {
|
||||
this.log(`No watcher creator available, skipping watcher for ${dir}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Create watcher using the provided function
|
||||
const watcher = this.createFileWatcher(dir)
|
||||
|
||||
if (!watcher) {
|
||||
this.log(`Watcher creation returned null for ${dir}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Invalidate cache on any change
|
||||
const invalidate = () => {
|
||||
this.log(`File change detected in ${dir}, invalidating cache`)
|
||||
this.invalidateDirectory(dir)
|
||||
}
|
||||
|
||||
watcher.onDidCreate(invalidate)
|
||||
watcher.onDidChange(invalidate)
|
||||
watcher.onDidDelete(invalidate)
|
||||
|
||||
// Add to context subscriptions for proper cleanup
|
||||
if (this.context) {
|
||||
this.context.subscriptions.push(watcher)
|
||||
}
|
||||
this.watchers.set(dir, watcher)
|
||||
|
||||
this.log(`Created watcher for ${dir}`)
|
||||
} catch (error) {
|
||||
// Log but don't fail - directory might not exist yet
|
||||
this.log(`Failed to create watcher for ${dir}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all cached hooks that have scripts in this directory
|
||||
*/
|
||||
private invalidateDirectory(dir: string): void {
|
||||
let invalidated = 0
|
||||
|
||||
for (const [hookName, entry] of this.cache) {
|
||||
if (entry.scriptPaths.some((scriptPath) => scriptPath.startsWith(dir))) {
|
||||
this.cache.delete(hookName)
|
||||
invalidated++
|
||||
}
|
||||
}
|
||||
|
||||
this.log(`Invalidated ${invalidated} hooks for directory ${dir}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate entire cache
|
||||
*/
|
||||
invalidateAll(): void {
|
||||
const size = this.cache.size
|
||||
this.cache.clear()
|
||||
this.log(`Invalidated entire cache (${size} entries)`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics (for debugging/monitoring)
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
cacheSize: this.cache.size,
|
||||
watcherCount: this.watchers.size,
|
||||
watchedDirs: this.watchedDirs.size,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug message if debug mode is enabled
|
||||
*/
|
||||
private log(message: string): void {
|
||||
if (this.debug) {
|
||||
console.log(`[HookCache] ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this.disposed) {
|
||||
return
|
||||
}
|
||||
|
||||
this.log(`Disposing cache (${this.watchers.size} watchers)`)
|
||||
|
||||
for (const watcher of this.watchers.values()) {
|
||||
watcher.dispose()
|
||||
}
|
||||
|
||||
this.watchers.clear()
|
||||
this.watchedDirs.clear()
|
||||
this.cache.clear()
|
||||
this.disposed = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset singleton instance (for testing)
|
||||
*/
|
||||
static resetForTesting(): void {
|
||||
if (HookDiscoveryCache.instance) {
|
||||
HookDiscoveryCache.instance.dispose()
|
||||
HookDiscoveryCache.instance = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Types of errors that can occur during hook execution
|
||||
*/
|
||||
export enum HookErrorType {
|
||||
/** Hook execution exceeded the timeout limit */
|
||||
TIMEOUT = "timeout",
|
||||
/** Hook output failed JSON validation */
|
||||
VALIDATION = "validation",
|
||||
/** Hook script execution failed (non-zero exit, crash, etc.) */
|
||||
EXECUTION = "execution",
|
||||
/** Hook was cancelled by user */
|
||||
CANCELLATION = "cancellation",
|
||||
}
|
||||
|
||||
/**
|
||||
* Structured error information for hook failures.
|
||||
* Provides both user-friendly messages and technical details.
|
||||
*/
|
||||
export interface HookErrorInfo {
|
||||
/** Type of error that occurred */
|
||||
type: HookErrorType
|
||||
/** User-friendly error message */
|
||||
message: string
|
||||
/** Technical details for debugging (optional, shown in expansion) */
|
||||
details?: string
|
||||
/** Path to the hook script that failed */
|
||||
scriptPath?: string
|
||||
/** Exit code if available */
|
||||
exitCode?: number
|
||||
/** Stderr output if available */
|
||||
stderr?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown during hook execution with structured information.
|
||||
* This allows proper error handling without string parsing.
|
||||
*/
|
||||
export class HookExecutionError extends Error {
|
||||
constructor(
|
||||
public readonly errorInfo: HookErrorInfo,
|
||||
message?: string,
|
||||
) {
|
||||
super(message || errorInfo.message)
|
||||
this.name = "HookExecutionError"
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is a HookExecutionError
|
||||
*/
|
||||
static isHookError(error: unknown): error is HookExecutionError {
|
||||
return error instanceof HookExecutionError
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timeout error
|
||||
*/
|
||||
static timeout(scriptPath: string, timeoutMs: number, stderr?: string, hookName?: string): HookExecutionError {
|
||||
const hookPrefix = hookName ? `${hookName} hook` : "Hook"
|
||||
return new HookExecutionError({
|
||||
type: HookErrorType.TIMEOUT,
|
||||
message: `${hookPrefix} execution timed out after ${timeoutMs}ms`,
|
||||
details:
|
||||
`The hook took longer than ${timeoutMs / 1000} seconds to complete.\n\n` +
|
||||
`Common causes:\n` +
|
||||
`• Infinite loop in hook script\n` +
|
||||
`• Network request hanging without timeout\n` +
|
||||
`• File I/O operation stuck\n` +
|
||||
`• Heavy computation taking too long\n\n` +
|
||||
`Recommendations:\n` +
|
||||
`1. Check your hook script for infinite loops\n` +
|
||||
`2. Add timeouts to network requests\n` +
|
||||
`3. Use background jobs for long operations\n` +
|
||||
`4. Test your hook script independently`,
|
||||
scriptPath,
|
||||
stderr,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validation error
|
||||
*/
|
||||
static validation(validationError: string, scriptPath: string, stdoutPreview: string): HookExecutionError {
|
||||
return new HookExecutionError({
|
||||
type: HookErrorType.VALIDATION,
|
||||
message: "Hook output validation failed",
|
||||
details: `${validationError}\n\nOutput preview:\n${stdoutPreview}`,
|
||||
scriptPath,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an execution error
|
||||
*/
|
||||
static execution(scriptPath: string, exitCode: number, stderr?: string, hookName?: string): HookExecutionError {
|
||||
const hookPrefix = hookName ? `${hookName} hook` : "Hook script"
|
||||
const message = `${hookPrefix} exited with code ${exitCode}`
|
||||
return new HookExecutionError(
|
||||
{
|
||||
type: HookErrorType.EXECUTION,
|
||||
message,
|
||||
details: stderr ? `stderr:\n${stderr}` : undefined,
|
||||
scriptPath,
|
||||
exitCode,
|
||||
stderr,
|
||||
},
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cancellation error
|
||||
*/
|
||||
static cancellation(scriptPath: string, hookName?: string): HookExecutionError {
|
||||
const hookPrefix = hookName ? `${hookName} hook` : "Hook"
|
||||
return new HookExecutionError({
|
||||
type: HookErrorType.CANCELLATION,
|
||||
message: `${hookPrefix} execution was cancelled`,
|
||||
details: "The hook was cancelled by the user before completion",
|
||||
scriptPath,
|
||||
exitCode: 130, // Standard SIGINT exit code
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
import { ChildProcess, spawn } from "child_process"
|
||||
import { EventEmitter } from "events"
|
||||
import { HookProcessRegistry } from "./HookProcessRegistry"
|
||||
|
||||
// Maximum total output size (stdout + stderr combined)
|
||||
const MAX_HOOK_OUTPUT_SIZE = 1024 * 1024 // 1MB
|
||||
|
||||
/**
|
||||
* HookProcess manages the execution of a hook script with streaming output capabilities.
|
||||
* Similar to StandaloneTerminalProcess but specialized for hook execution.
|
||||
*
|
||||
* Key features:
|
||||
* - Real-time stdout/stderr streaming via line events
|
||||
* - Separate handling of visual output vs. JSON response
|
||||
* - 30-second execution timeout
|
||||
* - 1MB output size limit (prevents memory issues)
|
||||
* - Process lifecycle management with abort support
|
||||
*/
|
||||
export class HookProcess extends EventEmitter {
|
||||
private childProcess: ChildProcess | null = null
|
||||
private buffer = ""
|
||||
private fullOutput = ""
|
||||
private lastRetrievedIndex = 0
|
||||
private exitCode: number | null = null
|
||||
private isCompleted = false
|
||||
private timeoutHandle: NodeJS.Timeout | null = null // 30-second execution timeout
|
||||
|
||||
// Separate buffers for stdout and stderr
|
||||
private stdoutBuffer = ""
|
||||
private stderrBuffer = ""
|
||||
|
||||
// Output size tracking
|
||||
private stdoutSize = 0
|
||||
private stderrSize = 0
|
||||
private outputTruncated = false
|
||||
|
||||
// Track registration state to prevent leaks and ensure cleanup
|
||||
private isRegistered = false
|
||||
|
||||
constructor(
|
||||
private readonly scriptPath: string,
|
||||
private readonly timeoutMs: number = 30000,
|
||||
private readonly abortSignal?: AbortSignal,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the hook script with the given JSON input
|
||||
* @param inputJson The JSON string to pass to the hook via stdin
|
||||
*/
|
||||
async run(inputJson: string): Promise<void> {
|
||||
// Wrap in try/finally to guarantee cleanup even if errors occur
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
// Register this process for tracking
|
||||
HookProcessRegistry.register(this)
|
||||
this.isRegistered = true
|
||||
|
||||
// Check if already aborted
|
||||
if (this.abortSignal?.aborted) {
|
||||
this.safeUnregister()
|
||||
reject(new Error("Hook execution cancelled"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set up abort handler
|
||||
const abortHandler = () => {
|
||||
if (this.childProcess && !this.isCompleted) {
|
||||
this.isCompleted = true // Mark as completed immediately
|
||||
|
||||
// Remove abort listener immediately to prevent double-rejection
|
||||
if (this.abortSignal) {
|
||||
this.abortSignal.removeEventListener("abort", abortHandler)
|
||||
}
|
||||
|
||||
// Clean up execution timeout timer
|
||||
if (this.timeoutHandle) {
|
||||
clearTimeout(this.timeoutHandle)
|
||||
this.timeoutHandle = null
|
||||
}
|
||||
|
||||
// Unregister from active processes
|
||||
this.safeUnregister()
|
||||
|
||||
// Kill the process (async, fire-and-forget)
|
||||
if (this.childProcess.pid) {
|
||||
this.childProcess.kill("SIGTERM")
|
||||
}
|
||||
|
||||
// Reject immediately - don't wait for process to die
|
||||
reject(new Error("Hook execution cancelled by user"))
|
||||
}
|
||||
}
|
||||
|
||||
if (this.abortSignal) {
|
||||
this.abortSignal.addEventListener("abort", abortHandler, { once: true })
|
||||
}
|
||||
|
||||
// Spawn the hook process through shell on all platforms
|
||||
// This is the git-style approach: the shell interprets the shebang line
|
||||
// and executes the appropriate interpreter (bash, node, python, etc.)
|
||||
// On Unix: detached=true creates a process group, allowing us to kill all children
|
||||
this.childProcess = spawn(this.scriptPath, [], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
shell: true, // Use shell on all platforms for shebang interpretation
|
||||
detached: process.platform !== "win32", // Create process group on Unix
|
||||
})
|
||||
|
||||
let didEmitEmptyLine = false
|
||||
|
||||
// Set up timeout
|
||||
this.timeoutHandle = setTimeout(() => {
|
||||
if (this.childProcess && !this.isCompleted) {
|
||||
this.childProcess.kill("SIGTERM")
|
||||
reject(
|
||||
new Error(
|
||||
`Hook execution timed out after ${this.timeoutMs}ms. The hook script at '${this.scriptPath}' took too long to complete.`,
|
||||
),
|
||||
)
|
||||
}
|
||||
}, this.timeoutMs)
|
||||
|
||||
// Handle stdout
|
||||
this.childProcess.stdout?.on("data", (data) => {
|
||||
const output = data.toString()
|
||||
this.stdoutBuffer += output
|
||||
this.handleOutput(output, didEmitEmptyLine, "stdout")
|
||||
if (!didEmitEmptyLine && output) {
|
||||
this.emit("line", "", "stdout") // Signal start of output
|
||||
didEmitEmptyLine = true
|
||||
}
|
||||
})
|
||||
|
||||
// Handle stderr
|
||||
this.childProcess.stderr?.on("data", (data) => {
|
||||
const output = data.toString()
|
||||
this.stderrBuffer += output
|
||||
this.handleOutput(output, didEmitEmptyLine, "stderr")
|
||||
if (!didEmitEmptyLine && output) {
|
||||
this.emit("line", "", "stderr") // Signal start of output
|
||||
didEmitEmptyLine = true
|
||||
}
|
||||
})
|
||||
|
||||
// Handle process completion
|
||||
this.childProcess.on("close", (code, signal) => {
|
||||
this.exitCode = code
|
||||
this.isCompleted = true
|
||||
this.emitRemainingBuffer()
|
||||
|
||||
// Unregister from active processes
|
||||
this.safeUnregister()
|
||||
|
||||
// Clear execution timeout timer
|
||||
if (this.timeoutHandle) {
|
||||
clearTimeout(this.timeoutHandle)
|
||||
this.timeoutHandle = null
|
||||
}
|
||||
|
||||
// Remove abort listener
|
||||
if (this.abortSignal) {
|
||||
this.abortSignal.removeEventListener("abort", abortHandler)
|
||||
}
|
||||
|
||||
this.emit("completed", code, signal)
|
||||
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`Hook exited with code ${code}${signal ? `, signal ${signal}` : ""}`))
|
||||
}
|
||||
})
|
||||
|
||||
// Handle process errors
|
||||
this.childProcess.on("error", (error) => {
|
||||
// Unregister from active processes
|
||||
this.safeUnregister()
|
||||
|
||||
if (this.timeoutHandle) {
|
||||
clearTimeout(this.timeoutHandle)
|
||||
this.timeoutHandle = null
|
||||
}
|
||||
// Remove abort listener
|
||||
if (this.abortSignal) {
|
||||
this.abortSignal.removeEventListener("abort", abortHandler)
|
||||
}
|
||||
this.emit("error", error)
|
||||
reject(error)
|
||||
})
|
||||
|
||||
// Send input to the process
|
||||
try {
|
||||
this.childProcess.stdin?.write(inputJson)
|
||||
this.childProcess.stdin?.end()
|
||||
} catch (error) {
|
||||
reject(new Error(`Failed to write input to hook: ${error}`))
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
// Guaranteed cleanup even if process setup fails or throws
|
||||
this.safeUnregister()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely unregister from the process registry.
|
||||
* This is idempotent and prevents double-unregistration issues.
|
||||
*/
|
||||
private safeUnregister(): void {
|
||||
if (this.isRegistered) {
|
||||
HookProcessRegistry.unregister(this)
|
||||
this.isRegistered = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle output data and emit line events.
|
||||
* Enforces 1MB total output limit to prevent memory issues.
|
||||
*/
|
||||
private handleOutput(data: string, _didEmitEmptyLine: boolean, stream: "stdout" | "stderr"): void {
|
||||
// Check output size limit
|
||||
const dataSize = Buffer.byteLength(data)
|
||||
const currentTotalSize = this.stdoutSize + this.stderrSize
|
||||
|
||||
if (currentTotalSize + dataSize > MAX_HOOK_OUTPUT_SIZE) {
|
||||
if (!this.outputTruncated) {
|
||||
this.outputTruncated = true
|
||||
const truncationMsg = "\n\n[Output truncated: exceeded 1MB limit]"
|
||||
this.emit("line", truncationMsg, stream)
|
||||
console.warn(`[HookProcess] Output exceeded ${MAX_HOOK_OUTPUT_SIZE} bytes, truncating`)
|
||||
}
|
||||
return // Drop further output
|
||||
}
|
||||
|
||||
// Track size by stream
|
||||
if (stream === "stdout") {
|
||||
this.stdoutSize += dataSize
|
||||
} else {
|
||||
this.stderrSize += dataSize
|
||||
}
|
||||
|
||||
// Store full output
|
||||
this.fullOutput += data
|
||||
|
||||
// Emit lines immediately
|
||||
this.emitLines(data, stream)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit complete lines from buffered output
|
||||
*/
|
||||
private emitLines(chunk: string, stream: "stdout" | "stderr"): void {
|
||||
this.buffer += chunk
|
||||
let lineEndIndex
|
||||
while ((lineEndIndex = this.buffer.indexOf("\n")) !== -1) {
|
||||
const line = this.buffer.slice(0, lineEndIndex).trimEnd()
|
||||
this.emit("line", line, stream)
|
||||
this.buffer = this.buffer.slice(lineEndIndex + 1)
|
||||
}
|
||||
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit any remaining buffered output when process completes
|
||||
*/
|
||||
private emitRemainingBuffer(): void {
|
||||
if (this.buffer) {
|
||||
const remainingBuffer = this.buffer.trimEnd()
|
||||
if (remainingBuffer) {
|
||||
// Determine which stream this came from based on content
|
||||
// This is a fallback; in practice, line events should capture most output
|
||||
this.emit("line", remainingBuffer, "stdout")
|
||||
}
|
||||
this.buffer = ""
|
||||
this.lastRetrievedIndex = this.fullOutput.length
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unretrieved output (for compatibility with terminal process interface)
|
||||
*/
|
||||
getUnretrievedOutput(): string {
|
||||
const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
|
||||
this.lastRetrievedIndex = this.fullOutput.length
|
||||
return unretrieved.trimEnd()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the complete stdout buffer (for JSON parsing)
|
||||
*/
|
||||
getStdout(): string {
|
||||
return this.stdoutBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the complete stderr buffer (for error reporting)
|
||||
*/
|
||||
getStderr(): string {
|
||||
return this.stderrBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the exit code
|
||||
*/
|
||||
getExitCode(): number | null {
|
||||
return this.exitCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if process has completed
|
||||
*/
|
||||
hasCompleted(): boolean {
|
||||
return this.isCompleted
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate the process and its entire process tree.
|
||||
* Uses process groups on Unix to kill child processes.
|
||||
* Implements graceful shutdown with 2-second timeout before force kill.
|
||||
*/
|
||||
async terminate(): Promise<void> {
|
||||
if (!this.childProcess || this.isCompleted) {
|
||||
// Still ensure unregistration even if process already completed
|
||||
this.safeUnregister()
|
||||
return
|
||||
}
|
||||
|
||||
const pid = this.childProcess.pid
|
||||
if (!pid) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// On Unix, kill process group (negative PID kills all children)
|
||||
// On Windows, just kill the process (tree-kill would be better but adds dependency)
|
||||
if (process.platform !== "win32") {
|
||||
// Kill process group with SIGTERM for graceful shutdown
|
||||
process.kill(-pid, "SIGTERM")
|
||||
} else {
|
||||
// On Windows, just kill the process
|
||||
this.childProcess.kill("SIGTERM")
|
||||
}
|
||||
|
||||
// Wait up to 2 seconds for graceful shutdown
|
||||
const gracefulTimeout = new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
const processExit = new Promise((resolve) => {
|
||||
this.childProcess?.once("exit", resolve)
|
||||
})
|
||||
|
||||
await Promise.race([processExit, gracefulTimeout])
|
||||
|
||||
// Force kill if still running
|
||||
if (!this.isCompleted) {
|
||||
if (process.platform !== "win32") {
|
||||
process.kill(-pid, "SIGKILL")
|
||||
} else {
|
||||
this.childProcess?.kill("SIGKILL")
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Process might already be dead, which is fine
|
||||
console.debug(`[HookProcess] Error during termination: ${error}`)
|
||||
} finally {
|
||||
// Clear timeout regardless
|
||||
if (this.timeoutHandle) {
|
||||
clearTimeout(this.timeoutHandle)
|
||||
this.timeoutHandle = null
|
||||
}
|
||||
// Ensure unregistration even if termination fails
|
||||
this.safeUnregister()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { HookProcess } from "./HookProcess"
|
||||
|
||||
/**
|
||||
* Global registry for tracking active hook processes.
|
||||
*
|
||||
* Purpose:
|
||||
* - Prevents zombie processes by tracking all running hooks
|
||||
* - Enables cleanup on extension deactivation
|
||||
* - Provides visibility into active hook executions
|
||||
*
|
||||
* Usage:
|
||||
* - HookProcess automatically registers/unregisters itself
|
||||
* - Extension deactivation calls terminateAll()
|
||||
* - Can query active count for monitoring/debugging
|
||||
*/
|
||||
export class HookProcessRegistry {
|
||||
private static activeProcesses = new Set<HookProcess>()
|
||||
|
||||
/**
|
||||
* Register a hook process as active.
|
||||
* Called by HookProcess when execution starts.
|
||||
*/
|
||||
static register(process: HookProcess): void {
|
||||
HookProcessRegistry.activeProcesses.add(process)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a hook process (completed or failed).
|
||||
* Called by HookProcess when execution ends.
|
||||
*/
|
||||
static unregister(process: HookProcess): void {
|
||||
HookProcessRegistry.activeProcesses.delete(process)
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate all active hook processes.
|
||||
* Called during extension deactivation to prevent zombie processes.
|
||||
*/
|
||||
static async terminateAll(): Promise<void> {
|
||||
const processes = Array.from(HookProcessRegistry.activeProcesses)
|
||||
if (processes.length > 0) {
|
||||
console.log(`[HookProcessRegistry] Terminating ${processes.length} active hook process(es)`)
|
||||
await Promise.all(processes.map((p) => p.terminate()))
|
||||
HookProcessRegistry.activeProcesses.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of currently active hook processes.
|
||||
* Useful for monitoring and debugging.
|
||||
*/
|
||||
static getActiveCount(): number {
|
||||
return HookProcessRegistry.activeProcesses.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the registry (for testing only).
|
||||
* @internal
|
||||
*/
|
||||
static resetForTesting(): void {
|
||||
HookProcessRegistry.activeProcesses.clear()
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ it("should work with real hook", async () => {
|
||||
const runner = await factory.create("PreToolUse")
|
||||
const result = await runner.run(buildPreToolUseInput({ toolName: "test_tool" }))
|
||||
|
||||
result.shouldContinue.should.be.true()
|
||||
result.cancel.should.be.false()
|
||||
})
|
||||
```
|
||||
|
||||
@@ -50,15 +50,15 @@ For more control, you can also manually copy fixture files.
|
||||
### PreToolUse Hooks
|
||||
|
||||
#### `hooks/pretooluse/success`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "PreToolUse hook executed successfully", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "PreToolUse hook executed successfully", errorMessage: "" }`
|
||||
- **Use for**: Testing happy path scenarios
|
||||
|
||||
#### `hooks/pretooluse/blocking`
|
||||
- **Returns**: `{ shouldContinue: false, contextModification: "", errorMessage: "Tool execution blocked by hook" }`
|
||||
- **Returns**: `{ cancel: true, contextModification: "", errorMessage: "Tool execution blocked by hook" }`
|
||||
- **Use for**: Testing tool execution blocking
|
||||
|
||||
#### `hooks/pretooluse/context-injection`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "WORKSPACE_RULES: Tool [toolName] requires review", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "WORKSPACE_RULES: Tool [toolName] requires review", errorMessage: "" }`
|
||||
- **Use for**: Testing context injection with type prefixes
|
||||
- **Note**: Dynamically includes tool name from input
|
||||
|
||||
@@ -69,7 +69,7 @@ For more control, you can also manually copy fixture files.
|
||||
### PostToolUse Hooks
|
||||
|
||||
#### `hooks/posttooluse/success`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "PostToolUse hook executed successfully", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "PostToolUse hook executed successfully", errorMessage: "" }`
|
||||
- **Use for**: Testing PostToolUse execution
|
||||
|
||||
#### `hooks/posttooluse/error`
|
||||
@@ -79,34 +79,34 @@ For more control, you can also manually copy fixture files.
|
||||
### UserPromptSubmit Hooks
|
||||
|
||||
#### `hooks/userpromptsubmit/success`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "Prompt approved", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "Prompt approved", errorMessage: "" }`
|
||||
- **Use for**: Testing successful prompt submission
|
||||
|
||||
#### `hooks/userpromptsubmit/blocking`
|
||||
- **Returns**: `{ shouldContinue: false, contextModification: "", errorMessage: "Prompt violates policy" }`
|
||||
- **Returns**: `{ cancel: true, contextModification: "", errorMessage: "Prompt violates policy" }`
|
||||
- **Use for**: Testing prompt submission blocking
|
||||
|
||||
#### `hooks/userpromptsubmit/context-injection`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "CONTEXT_INJECTION: User is in plan mode", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "CONTEXT_INJECTION: User is in plan mode", errorMessage: "" }`
|
||||
- **Use for**: Testing context injection into task request
|
||||
|
||||
#### `hooks/userpromptsubmit/multiline`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "Line count: N", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "Line count: N", errorMessage: "" }`
|
||||
- **Use for**: Testing multiline prompt handling
|
||||
- **Note**: Dynamically counts newlines in the prompt
|
||||
|
||||
#### `hooks/userpromptsubmit/large-prompt`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "Prompt size: N", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "Prompt size: N", errorMessage: "" }`
|
||||
- **Use for**: Testing large prompt handling
|
||||
- **Note**: Dynamically reports prompt character count
|
||||
|
||||
#### `hooks/userpromptsubmit/special-chars`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "Special chars preserved" | "Missing special chars", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "Special chars preserved" | "Missing special chars", errorMessage: "" }`
|
||||
- **Use for**: Testing special character preservation
|
||||
- **Note**: Checks for @, #, and $ characters
|
||||
|
||||
#### `hooks/userpromptsubmit/empty-prompt`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "Prompt length: 0", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "Prompt length: 0", errorMessage: "" }`
|
||||
- **Use for**: Testing empty prompt handling
|
||||
- **Note**: Safely handles undefined or empty prompts
|
||||
|
||||
@@ -121,11 +121,11 @@ For more control, you can also manually copy fixture files.
|
||||
### TaskStart Hooks
|
||||
|
||||
#### `hooks/taskstart/success`
|
||||
- **Returns**: `{ shouldContinue: true, contextModification: "TaskStart hook executed successfully", errorMessage: "" }`
|
||||
- **Returns**: `{ cancel: false, contextModification: "TaskStart hook executed successfully", errorMessage: "" }`
|
||||
- **Use for**: Testing TaskStart hook success path, allowing task to proceed
|
||||
|
||||
#### `hooks/taskstart/blocking`
|
||||
- **Returns**: `{ shouldContinue: false, contextModification: "", errorMessage: "Task execution blocked by hook" }`
|
||||
- **Returns**: `{ cancel: true, contextModification: "", errorMessage: "Task execution blocked by hook" }`
|
||||
- **Use for**: Testing task blocking at start (e.g., policy enforcement)
|
||||
|
||||
#### `hooks/taskstart/error`
|
||||
@@ -158,7 +158,7 @@ cat > src/core/hooks/__tests__/fixtures/hooks/pretooluse/my-new-scenario/PreTool
|
||||
#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: "My custom context",
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: "PostToolUse hook executed successfully",
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: false,
|
||||
cancel: true,
|
||||
contextModification: "",
|
||||
errorMessage: "Tool execution blocked by hook"
|
||||
}));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
const toolName = input.preToolUse?.toolName || 'unknown';
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: `WORKSPACE_RULES: Tool ${toolName} requires review`,
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: "PreToolUse hook executed successfully",
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// Note: For TaskCancel, contextModification is completely ignored.
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: false,
|
||||
cancel: true,
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// Note: For TaskCancel, contextModification is completely ignored.
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: false,
|
||||
cancel: true,
|
||||
errorMessage: "some error happened"
|
||||
}));
|
||||
@@ -2,6 +2,6 @@
|
||||
// Note: For TaskCancel, contextModification is completely ignored.
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// Note: For TaskCancel, contextModification is completely ignored.
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
errorMessage: "some error happened"
|
||||
}));
|
||||
@@ -3,7 +3,7 @@ const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
const deleted = input.taskResume?.previousState?.conversationHistoryDeleted === 'true';
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: deleted
|
||||
? "TASK_CONTEXT: Some conversation history was truncated due to context window limits"
|
||||
: "",
|
||||
|
||||
@@ -3,7 +3,7 @@ const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
const taskId = input.taskResume?.taskMetadata?.taskId || 'unknown';
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: `WORKSPACE_RULES: Task ${taskId} resumed - review previous context`,
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,7 @@ const now = Date.now();
|
||||
const hoursAgo = Math.floor((now - lastMessageTs) / 3600000);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: hoursAgo >= 1
|
||||
? `TASK_CONTEXT: Task was paused ${hoursAgo} hours ago - you may need to re-familiarize yourself with the context`
|
||||
: "",
|
||||
|
||||
@@ -3,7 +3,7 @@ const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
const messageCount = parseInt(input.taskResume?.previousState?.messageCount || '0');
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: `TASK_CONTEXT: Resuming task with ${messageCount} previous messages`,
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,7 @@ const now = Date.now();
|
||||
const minutesAgo = Math.floor((now - lastMessageTs) / 60000);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: minutesAgo < 5
|
||||
? "TASK_CONTEXT: Recently paused task - context is still fresh"
|
||||
: "",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: "TaskResume hook executed successfully",
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: false,
|
||||
cancel: true,
|
||||
contextModification: "",
|
||||
errorMessage: "Task execution blocked by hook"
|
||||
}));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: "TaskStart hook executed successfully",
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: false,
|
||||
cancel: true,
|
||||
contextModification: "",
|
||||
errorMessage: "Prompt violates policy"
|
||||
}));
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: "CONTEXT_INJECTION: User is in plan mode",
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
const promptLength = typeof input.userPromptSubmit.prompt === 'string' ? input.userPromptSubmit.prompt.length : 0;
|
||||
console.log(JSON.stringify({
|
||||
shouldContinue: true,
|
||||
cancel: false,
|
||||
contextModification: "Prompt length: " + promptLength,
|
||||
errorMessage: ""
|
||||
}));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user