mirror of
https://github.com/cline/cline.git
synced 2026-09-12 00:50:27 +08:00
Compare commits
86
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 | ||
|
|
8da38b2a2e | ||
|
|
cb121170cb | ||
|
|
e9d2d344c6 | ||
|
|
69fb954a6c | ||
|
|
e9e616e317 | ||
|
|
545ac29e07 | ||
|
|
d38489aebc | ||
|
|
8d47026640 | ||
|
|
268cd5c527 | ||
|
|
c7c4e43322 | ||
|
|
aae9d432fd | ||
|
|
604dbd7bb0 | ||
|
|
062a32f93d | ||
|
|
535b29f465 | ||
|
|
a8027dc570 | ||
|
|
978a8a0aa6 | ||
|
|
0cd462a414 | ||
|
|
65dbd85a92 | ||
|
|
ee1bb2f788 | ||
|
|
6f69ffb16f | ||
|
|
f91769bda7 | ||
|
|
7692adacf5 | ||
|
|
a98faf5af4 | ||
|
|
e3f4ce618f | ||
|
|
dcf519d2f7 | ||
|
|
3ef4aea0f7 | ||
|
|
a820026e0b | ||
|
|
29d1b0507c | ||
|
|
929d13a4dd | ||
|
|
c729e8c7c6 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix remote config
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add Qwen3 models to Amazon Bedrock provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add GLM 4.6 to Baseten provider
|
||||
@@ -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
|
||||
@@ -122,9 +122,9 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
title="Previous Updates:"
|
||||
classNames={{
|
||||
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
|
||||
title: "font-bold text-[var(--vscode-foreground)]",
|
||||
title: "font-bold text-(--vscode-foreground)",
|
||||
indicator:
|
||||
"text-[var(--vscode-foreground)] mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
|
||||
"text-(--vscode-foreground) mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
|
||||
}}>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
|
||||
@@ -74,10 +74,9 @@ jobs:
|
||||
CLINE_ENVIRONMENT: production
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
|
||||
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
|
||||
OTEL_LOGS_EXPORTER: console,otlp
|
||||
OTEL_METRICS_EXPORTER: console,otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
OTEL_METRIC_EXPORT_INTERVAL: ${{ secrets.OTEL_METRIC_EXPORT_INTERVAL }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
|
||||
@@ -99,12 +99,11 @@ jobs:
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
|
||||
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
|
||||
OTEL_LOGS_EXPORTER: console,otlp
|
||||
OTEL_METRICS_EXPORTER: console,otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
OTEL_METRIC_EXPORT_INTERVAL: ${{ secrets.OTEL_METRIC_EXPORT_INTERVAL }}
|
||||
run: |
|
||||
# Required to generate the .vsix
|
||||
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
+23
-1
@@ -1,12 +1,34 @@
|
||||
# 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
|
||||
- Remove Cline/code-supernova-1-million model
|
||||
- Changes to allow users to manually enter model names (eg. presets) when using OpenRouter
|
||||
|
||||
## [3.34.0]
|
||||
|
||||
- Cline Teams is now free through 2025 for unlimited users. Includes Jetbrains, RBAC, centralized billing and more.
|
||||
- Use the “exacto” versions of GLM-4.6, Kimi-K2, and Qwen3-Coder in the Cline provider for the best balance of cost, speed, accuracy and tool-calling.
|
||||
|
||||
## [3.33.1]
|
||||
|
||||
- Fix CLI installation copy text
|
||||
|
||||
## [3.33.0]
|
||||
|
||||
- Added Cline CLI (Preview)
|
||||
- Added Cline CLI (Preview)
|
||||
- Added Subagent support (Experimental)
|
||||
- Added Multi-Root Workspaces support (Enable in feature settings)
|
||||
- Add auto-retry with exponential backof for failed API requests
|
||||
|
||||
@@ -43,7 +43,7 @@ Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.c
|
||||
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
|
||||
|
||||
> [!TIP]
|
||||
> Use the `CMD/CTRL + Shift + P` shortcut to open the command palette and type "Cline: Open In New Tab" to open the extension as a tab in your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
|
||||
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+13
-13
@@ -70,7 +70,7 @@
|
||||
"noControlCharactersInRegex": "off",
|
||||
"noShadowRestrictedNames": "off",
|
||||
"noArrayIndexKey": "info",
|
||||
"noAssignInExpressions": "warn"
|
||||
"noAssignInExpressions": "info"
|
||||
},
|
||||
"complexity": {
|
||||
"noUselessConstructor": "off",
|
||||
@@ -82,7 +82,7 @@
|
||||
"noStaticOnlyClass": "off"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "warn"
|
||||
"noDangerouslySetInnerHtml": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -114,17 +114,17 @@
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/dist/**",
|
||||
"!**/dist-*/**",
|
||||
"!**/out/**",
|
||||
"!**/evals/**",
|
||||
"!**/playwright/**",
|
||||
"!**/test-results/**",
|
||||
"!**/node_modules/**",
|
||||
"!**/webview-ui/build/**",
|
||||
"!**/generated/**",
|
||||
"!**/proto/**",
|
||||
"!**/tests/specs/**"
|
||||
"!**/dist",
|
||||
"!**/dist-*",
|
||||
"!**/out",
|
||||
"!**/evals",
|
||||
"!**/playwright",
|
||||
"!**/test-results",
|
||||
"!**/node_modules",
|
||||
"!**/webview-ui/build",
|
||||
"!**/generated",
|
||||
"!**/proto",
|
||||
"!**/tests/specs"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
|
||||
+25
-5
@@ -6,18 +6,38 @@ import (
|
||||
)
|
||||
|
||||
func NewAuthCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Authenticate a provider and configure model used",
|
||||
Long: `Authenticate a provider and configure model used
|
||||
Short: "Authenticate a provider and configure what model is used",
|
||||
Long: `Authenticate a provider and configure what model is used
|
||||
|
||||
This command opens an interactive menu where you can:
|
||||
Interactive Mode:
|
||||
Run without flags to open an interactive menu where you can:
|
||||
- Sign in to your Cline account
|
||||
- Configure other LLM providers (Anthropic, OpenAI, etc.)
|
||||
- Select and switch between AI models
|
||||
- Manage provider settings`,
|
||||
- Manage provider settings
|
||||
|
||||
Quick Setup Mode:
|
||||
Use flags to quickly configure a BYO provider non-interactively:
|
||||
|
||||
Examples:
|
||||
cline auth --provider openai-native --apikey sk-xxx --modelid gpt-5
|
||||
cline auth -p anthropic -k sk-ant-xxx -m claude-sonnet-4-5-20250929
|
||||
cline auth -p openai-compatible -k xxx -m gpt-4 -b https://api.example.com/v1
|
||||
|
||||
Supported providers: openai-native, openai, anthropic, gemini, openrouter, xai, cerebras, ollama
|
||||
Note: Bedrock provider requires interactive setup due to complex auth fields`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return auth.RunAuthFlow(cmd.Context(), args)
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags for quick setup mode
|
||||
cmd.Flags().StringVarP(&auth.QuickProvider, "provider", "p", "", "Provider ID for quick setup (e.g., openai-native, anthropic)")
|
||||
cmd.Flags().StringVarP(&auth.QuickAPIKey, "apikey", "k", "", "API key for the provider")
|
||||
cmd.Flags().StringVarP(&auth.QuickModelID, "modelid", "m", "", "Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)")
|
||||
cmd.Flags().StringVarP(&auth.QuickBaseURL, "baseurl", "b", "", "Base URL (optional, only for openai provider)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ const (
|
||||
// ┃ Change Cline model (only if authenticated) - hidden if not authenticated
|
||||
// ┃ Authenticate with Cline account / Sign out of Cline - changes based on auth status
|
||||
// ┃ Select active provider (Cline or BYO) - always shown. Used to switch between Cline and BYO providers
|
||||
// ┃ Configure API provider - always shown. Launches provider setup wizard
|
||||
// ┃ Configure BYO API providers - always shown. Launches provider setup wizard
|
||||
// ┃ Exit authorization wizard - always shown. Exits the auth menu
|
||||
|
||||
// RunAuthFlow is the entry point for the entire auth flow with instance management
|
||||
@@ -69,18 +69,25 @@ func RunAuthFlow(ctx context.Context, args []string) error {
|
||||
// Main entry point for handling the `cline auth` command
|
||||
// HandleAuthCommand routes the auth command based on the number of arguments
|
||||
func HandleAuthCommand(ctx context.Context, args []string) error {
|
||||
|
||||
// Check if flags are provided for quick setup
|
||||
if QuickProvider != "" || QuickAPIKey != "" || QuickModelID != "" || QuickBaseURL != "" {
|
||||
if QuickProvider == "" || QuickAPIKey == "" || QuickModelID == "" {
|
||||
return fmt.Errorf("quick setup requires --provider, --apikey, and --modelid flags. Use 'cline auth --help' for more information")
|
||||
}
|
||||
return QuickSetupFromFlags(ctx, QuickProvider, QuickAPIKey, QuickModelID, QuickBaseURL)
|
||||
}
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// No args: Show menu (ShowAuthMenuNoArgs)
|
||||
// No args: Show uth wizard
|
||||
return HandleAuthMenuNoArgs(ctx)
|
||||
case 1:
|
||||
// One arg: Provider ID only, prompt for API key
|
||||
return QuickAPISetup(args[0], "")
|
||||
case 2:
|
||||
// Two args: Provider ID and API key
|
||||
return QuickAPISetup(args[0], args[1])
|
||||
case 1, 2, 3, 4:
|
||||
fmt.Println("Invalid positional arguments. Correct usage:")
|
||||
fmt.Println(" cline auth --provider <provider> --apikey <key> --modelid <model> --baseurl <optional>")
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("quick BYO API setup is currently stubbed - not yet implemented")
|
||||
return fmt.Errorf("too many arguments. Use flags for quick setup: --provider, --apikey, --modelid --baseurl(optional)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,14 +173,14 @@ func ShowAuthMenuWithStatus(isClineAuthenticated bool, hasOrganizations bool, cu
|
||||
options = append(options,
|
||||
huh.NewOption("Sign out of Cline", AuthActionClineLogin),
|
||||
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
|
||||
huh.NewOption("Configure API provider", AuthActionBYOSetup),
|
||||
huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
|
||||
huh.NewOption("Exit authorization wizard", AuthActionExit),
|
||||
)
|
||||
} else {
|
||||
options = []huh.Option[AuthAction]{
|
||||
huh.NewOption("Authenticate with Cline account", AuthActionClineLogin),
|
||||
huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
|
||||
huh.NewOption("Configure API provider", AuthActionBYOSetup),
|
||||
huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
|
||||
huh.NewOption("Exit authorization wizard", AuthActionExit),
|
||||
}
|
||||
}
|
||||
@@ -261,11 +268,6 @@ func HandleSelectProvider(ctx context.Context) error {
|
||||
return HandleAuthMenuNoArgs(ctx)
|
||||
}
|
||||
|
||||
if len(providerOptions) == 1 {
|
||||
fmt.Println("Only one provider is configured. Configure another provider to switch between them.")
|
||||
return HandleAuthMenuNoArgs(ctx)
|
||||
}
|
||||
|
||||
providerOptions = append(providerOptions, huh.NewOption("(Cancel)", "cancel"))
|
||||
|
||||
// Show selection menu
|
||||
|
||||
@@ -1,13 +1,240 @@
|
||||
package auth
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
// QuickAPISetup performs quick provider setup with provider ID and optional API key
|
||||
func QuickAPISetup(providerID, apiKey string) error {
|
||||
fmt.Println("Quick BYO API setup is currently stubbed - not yet implemented.")
|
||||
fmt.Printf("Requested provider: %s\n", providerID)
|
||||
if apiKey != "" {
|
||||
fmt.Println("Provided API key:", "<jk redacted>")
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
// Package-level variables for command-line flags
|
||||
var (
|
||||
QuickProvider string // Provider ID (e.g., "openai", "anthropic")
|
||||
QuickAPIKey string // API key for the provider
|
||||
QuickModelID string // Model ID to configure
|
||||
QuickBaseURL string // Base URL (optional, for openai compatible only)
|
||||
)
|
||||
|
||||
// QuickSetupFromFlags performs quick setup using command-line flags
|
||||
// Returns error if validation fails or configuration cannot be applied
|
||||
func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL string) error {
|
||||
// Validate all input parameters
|
||||
providerEnum, err := validateQuickSetupInputs(provider, apiKey, modelID, baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create task manager for state operations
|
||||
manager, err := task.NewManagerForDefault(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create task manager: %w", err)
|
||||
}
|
||||
|
||||
// Validate and fetch model information if needed
|
||||
finalModelID, modelInfo, err := validateAndFetchModel(ctx, manager, providerEnum, modelID, apiKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("model validation failed: %w", err)
|
||||
}
|
||||
|
||||
// For Ollama, baseURL is stored in the API key field
|
||||
finalAPIKey := apiKey
|
||||
finalBaseURL := baseURL
|
||||
if providerEnum == cline.ApiProvider_OLLAMA {
|
||||
if baseURL != "" {
|
||||
finalAPIKey = baseURL
|
||||
finalBaseURL = ""
|
||||
} else if apiKey != "" {
|
||||
// User provided API key for Ollama - treat it as baseURL
|
||||
finalAPIKey = apiKey
|
||||
finalBaseURL = ""
|
||||
} else {
|
||||
// Use default Ollama baseURL
|
||||
finalAPIKey = "http://localhost:11434"
|
||||
finalBaseURL = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the provider using existing AddProviderPartial function
|
||||
if err := AddProviderPartial(ctx, manager, providerEnum, finalModelID, finalAPIKey, finalBaseURL, modelInfo); err != nil {
|
||||
return fmt.Errorf("failed to configure provider: %w", err)
|
||||
}
|
||||
|
||||
// Set the provider as active for both Plan and Act modes
|
||||
if err := UpdateProviderPartial(ctx, manager, providerEnum, ProviderUpdatesPartial{}, true); err != nil {
|
||||
return fmt.Errorf("failed to set provider as active: %w", err)
|
||||
}
|
||||
|
||||
// Mark welcome view as completed
|
||||
if err := markWelcomeViewCompleted(ctx, manager); err != nil {
|
||||
// Non-fatal error, just log it
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("[DEBUG] Warning: failed to mark welcome view as completed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Success message
|
||||
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
|
||||
fmt.Printf(" Model: %s\n", finalModelID)
|
||||
if providerEnum == cline.ApiProvider_OLLAMA {
|
||||
fmt.Printf(" Base URL: %s\n", finalAPIKey)
|
||||
} else {
|
||||
fmt.Println(" API Key: Configured")
|
||||
}
|
||||
if finalBaseURL != "" {
|
||||
fmt.Printf(" Custom Base URL: %s\n", finalBaseURL)
|
||||
}
|
||||
fmt.Println("\nYou can now use Cline with this provider.")
|
||||
fmt.Println("Run 'cline start' to begin a new task.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateQuickSetupInputs validates all input parameters for quick setup
|
||||
// Returns the validated provider enum or an error if validation fails
|
||||
func validateQuickSetupInputs(provider, apiKey, modelID, baseURL string) (cline.ApiProvider, error) {
|
||||
// Validate required parameters
|
||||
if provider == "" {
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("provider is required. Use --provider or -p flag")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(apiKey) == "" && provider != "ollama" {
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("API key is required for %s provider. Use --apikey or -k flag", provider)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(modelID) == "" {
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("model ID is required. Use --modelid or -m flag")
|
||||
}
|
||||
|
||||
// Validate and map provider string to enum
|
||||
providerEnum, err := validateQuickSetupProvider(provider)
|
||||
if err != nil {
|
||||
return cline.ApiProvider_ANTHROPIC, err
|
||||
}
|
||||
|
||||
// Validate that baseURL is only provided for OpenAI-compatible providers
|
||||
if err := validateBaseURL(baseURL, providerEnum); err != nil {
|
||||
return cline.ApiProvider_ANTHROPIC, err
|
||||
}
|
||||
|
||||
return providerEnum, nil
|
||||
}
|
||||
|
||||
// validateBaseURL checks if the user's input includes a baseURL for a provider other than OpenAI (compatible)
|
||||
// Returns error if baseURL is provided for unsupported providers
|
||||
func validateBaseURL(baseURL string, providerEnum cline.ApiProvider) error {
|
||||
if providerEnum != cline.ApiProvider_OPENAI {
|
||||
if baseURL != "" {
|
||||
return fmt.Errorf("base URL is only supported for OpenAI and OpenAI-compatible providers")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// validateQuickSetupProvider validates the provider ID and returns the enum value
|
||||
// Returns error if provider is invalid or not supported for quick setup
|
||||
func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
|
||||
// Normalize provider ID (trim whitespace, lowercase)
|
||||
normalizedID := strings.TrimSpace(strings.ToLower(providerID))
|
||||
|
||||
// Explicitly block Bedrock
|
||||
if normalizedID == "bedrock" {
|
||||
return cline.ApiProvider_BEDROCK, fmt.Errorf("bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup: cline auth")
|
||||
}
|
||||
|
||||
// Map provider string to enum using existing function
|
||||
provider, ok := mapProviderStringToEnum(normalizedID)
|
||||
if !ok {
|
||||
// Provider not found - provide helpful error message
|
||||
supportedProviders := []string{
|
||||
"openai-native", "openai", "anthropic", "gemini",
|
||||
"openrouter", "xai", "cerebras", "ollama",
|
||||
}
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf(
|
||||
"invalid provider '%s'. Supported providers: %s",
|
||||
providerID,
|
||||
strings.Join(supportedProviders, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
// Validate against supported quick setup providers
|
||||
supportedProviders := map[cline.ApiProvider]bool{
|
||||
cline.ApiProvider_OPENAI_NATIVE: true,
|
||||
cline.ApiProvider_OPENAI: true,
|
||||
cline.ApiProvider_ANTHROPIC: true,
|
||||
cline.ApiProvider_GEMINI: true,
|
||||
cline.ApiProvider_OPENROUTER: true,
|
||||
cline.ApiProvider_XAI: true,
|
||||
cline.ApiProvider_CEREBRAS: true,
|
||||
cline.ApiProvider_OLLAMA: true,
|
||||
}
|
||||
|
||||
if !supportedProviders[provider] {
|
||||
return provider, fmt.Errorf(
|
||||
"provider '%s' is not supported for quick setup. Please use interactive setup: cline auth",
|
||||
providerID,
|
||||
)
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// validateAndFetchModel validates the model ID or fetches from provider if needed
|
||||
// Returns the final model ID and optional model info
|
||||
// For providers with static models, validates against the list
|
||||
// For providers with dynamic models, fetches the list if possible
|
||||
func validateAndFetchModel(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID, apiKey string) (string, interface{}, error) {
|
||||
// Normalize model ID
|
||||
modelID = strings.TrimSpace(modelID)
|
||||
if modelID == "" {
|
||||
return "", nil, fmt.Errorf("model ID cannot be empty")
|
||||
}
|
||||
|
||||
// For most providers, we trust the user's input since we can't easily validate without making API calls
|
||||
// The actual validation will happen when the model is used
|
||||
switch provider {
|
||||
case cline.ApiProvider_OPENROUTER:
|
||||
// OpenRouter supports model info fetching, but it requires an API call
|
||||
// For quick setup, we'll trust the user's input and return nil for model info
|
||||
// The actual model info will be fetched when needed
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("[DEBUG] OpenRouter model ID: %s (will be validated on first use)\n", modelID)
|
||||
}
|
||||
return modelID, nil, nil
|
||||
|
||||
case cline.ApiProvider_OLLAMA:
|
||||
// Ollama models can be validated by fetching the list, but this requires the server to be running
|
||||
// For quick setup, we'll trust the user's input
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("[DEBUG] Ollama model ID: %s (will be validated when server is accessible)\n", modelID)
|
||||
}
|
||||
return modelID, nil, nil
|
||||
|
||||
default:
|
||||
// For other providers (Anthropic, OpenAI, Gemini, XAI, Cerebras), trust user input
|
||||
// Model validation will occur when the model is actually used
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("[DEBUG] %s model ID: %s (will be validated on first use)\n", GetProviderDisplayName(provider), modelID)
|
||||
}
|
||||
return modelID, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// markWelcomeViewCompleted marks the welcome view as completed in the state
|
||||
// This prevents the welcome view from showing up after quick setup
|
||||
func markWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
|
||||
// Use the State service to update the welcome view flag
|
||||
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark welcome view as completed: %w", err)
|
||||
}
|
||||
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("[DEBUG] Marked welcome view as completed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package auth
|
||||
@@ -14,13 +14,33 @@ import (
|
||||
|
||||
// FetchOpenRouterModels fetches available OpenRouter models from Cline Core
|
||||
func FetchOpenRouterModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
|
||||
resp, err := manager.GetClient().Models.RefreshOpenRouterModelsRPC(ctx, &cline.EmptyRequest{})
|
||||
resp, err := manager.GetClient().Models.RefreshOpenRouterModelsRpc(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch OpenRouter models: %w", err)
|
||||
}
|
||||
return resp.Models, nil
|
||||
}
|
||||
|
||||
// FetchOcaModels fetches available Oca models from Cline Core
|
||||
func FetchOcaModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OcaModelInfo, error) {
|
||||
resp, err := manager.GetClient().Models.RefreshOcaModels(ctx, &cline.StringRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch Oca models: %w", err)
|
||||
}
|
||||
return resp.Models, nil
|
||||
}
|
||||
|
||||
// ConvertOpenRouterModelsToInterface converts OpenRouter model map to generic interface map.
|
||||
// This allows OpenRouter and Cline models to be used with the generic fetching utilities.
|
||||
func ConvertOpenRouterModelsToInterface(models map[string]*cline.OpenRouterModelInfo) map[string]interface{} {
|
||||
result := make(map[string]interface{}, len(models))
|
||||
for k, v := range models {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// FetchOpenAiModels fetches available OpenAI models from Cline Core
|
||||
// Takes the API key and returns a list of model IDs
|
||||
func FetchOpenAiModels(ctx context.Context, manager *task.Manager, baseURL, apiKey string) ([]string, error) {
|
||||
@@ -100,9 +120,9 @@ func ConvertModelsMapToSlice(models map[string]interface{}) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// ConvertOpenRouterModelsToInterface converts OpenRouter model map to generic interface map.
|
||||
// This allows OpenRouter and Cline models to be used with the generic fetching utilities.
|
||||
func ConvertOpenRouterModelsToInterface(models map[string]*cline.OpenRouterModelInfo) map[string]interface{} {
|
||||
// ConvertOcaModelsToInterface converts Oca model map to generic interface map.
|
||||
// This allows Oca and Cline models to be used with the generic fetching utilities.
|
||||
func ConvertOcaModelsToInterface(models map[string]*cline.OcaModelInfo) map[string]interface{} {
|
||||
result := make(map[string]interface{}, len(models))
|
||||
for k, v := range models {
|
||||
result[k] = v
|
||||
|
||||
@@ -18,14 +18,15 @@ type BYOProviderOption struct {
|
||||
func GetBYOProviderList() []BYOProviderOption {
|
||||
return []BYOProviderOption{
|
||||
{Name: "Anthropic", Provider: cline.ApiProvider_ANTHROPIC},
|
||||
{Name: "OpenAI", Provider: cline.ApiProvider_OPENAI},
|
||||
{Name: "OpenAI Native", Provider: cline.ApiProvider_OPENAI_NATIVE},
|
||||
{Name: "OpenAI Compatible", Provider: cline.ApiProvider_OPENAI},
|
||||
{Name: "OpenAI (Official)", Provider: cline.ApiProvider_OPENAI_NATIVE},
|
||||
{Name: "OpenRouter", Provider: cline.ApiProvider_OPENROUTER},
|
||||
{Name: "X AI (Grok)", Provider: cline.ApiProvider_XAI},
|
||||
{Name: "AWS Bedrock", Provider: cline.ApiProvider_BEDROCK},
|
||||
{Name: "Google Gemini", Provider: cline.ApiProvider_GEMINI},
|
||||
{Name: "Ollama", Provider: cline.ApiProvider_OLLAMA},
|
||||
{Name: "Cerebras", Provider: cline.ApiProvider_CEREBRAS},
|
||||
{Name: "Oracle Code Assist", Provider: cline.ApiProvider_OCA},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +72,8 @@ func SupportsBYOModelFetching(provider cline.ApiProvider) bool {
|
||||
return true
|
||||
case cline.ApiProvider_OLLAMA:
|
||||
return true
|
||||
case cline.ApiProvider_OCA:
|
||||
return true
|
||||
}
|
||||
|
||||
return SupportsStaticModelList(provider)
|
||||
@@ -82,9 +85,9 @@ func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
|
||||
case cline.ApiProvider_ANTHROPIC:
|
||||
return "e.g., claude-sonnet-4-5-20250929"
|
||||
case cline.ApiProvider_OPENAI:
|
||||
return "e.g., gpt-5-2025-08-07"
|
||||
case cline.ApiProvider_OPENAI_NATIVE:
|
||||
return "e.g., openai/gpt-oss-120b"
|
||||
case cline.ApiProvider_OPENAI_NATIVE:
|
||||
return "e.g., gpt-5-2025-08-07"
|
||||
case cline.ApiProvider_OPENROUTER:
|
||||
return "e.g., google/gemini-2.0-flash-exp:free"
|
||||
case cline.ApiProvider_XAI:
|
||||
@@ -97,6 +100,8 @@ func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
|
||||
return "e.g., qwen3-coder:30b"
|
||||
case cline.ApiProvider_CEREBRAS:
|
||||
return "e.g., gpt-oss-120b"
|
||||
case cline.ApiProvider_OCA:
|
||||
return "e.g., oca/llama4"
|
||||
default:
|
||||
return "Enter model ID"
|
||||
}
|
||||
@@ -127,8 +132,8 @@ func GetBYOAPIKeyFieldConfig(provider cline.ApiProvider) APIKeyFieldConfig {
|
||||
}
|
||||
|
||||
// PromptForAPIKey prompts the user to enter an API key (or base URL for Ollama).
|
||||
// For OpenAI Native provider, also prompts for an optional base URL.
|
||||
func PromptForAPIKey(provider cline.ApiProvider) (string, error) {
|
||||
// For OpenAI (Compatible) provider, also prompts for an optional base URL.
|
||||
func PromptForAPIKey(provider cline.ApiProvider) (string, string, error) {
|
||||
var apiKey string
|
||||
config := GetBYOAPIKeyFieldConfig(provider)
|
||||
|
||||
@@ -149,11 +154,11 @@ func PromptForAPIKey(provider cline.ApiProvider) (string, error) {
|
||||
form := huh.NewForm(huh.NewGroup(apiKeyField))
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return "", fmt.Errorf("failed to get API key: %w", err)
|
||||
return "", "", fmt.Errorf("failed to get API key: %w", err)
|
||||
}
|
||||
|
||||
// For OpenAI Native provider, also prompt for base URL
|
||||
if provider == cline.ApiProvider_OPENAI_NATIVE {
|
||||
// For OpenAI (Compatible) provider, prompt for base URL
|
||||
if provider == cline.ApiProvider_OPENAI {
|
||||
var baseURL string
|
||||
baseURLForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
@@ -166,12 +171,11 @@ func PromptForAPIKey(provider cline.ApiProvider) (string, error) {
|
||||
)
|
||||
|
||||
if err := baseURLForm.Run(); err != nil {
|
||||
return "", fmt.Errorf("failed to get base URL: %w", err)
|
||||
return "", "", fmt.Errorf("failed to get base URL: %w", err)
|
||||
}
|
||||
|
||||
// TODO - connect baseURL
|
||||
_ = baseURL
|
||||
return apiKey, baseURL, nil
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
return apiKey, "", nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
@@ -110,6 +111,7 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
cline.ApiProvider_GEMINI,
|
||||
cline.ApiProvider_OLLAMA,
|
||||
cline.ApiProvider_CEREBRAS,
|
||||
cline.ApiProvider_OCA,
|
||||
}
|
||||
|
||||
// Check each provider to see if it's ready to use
|
||||
@@ -120,16 +122,23 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this provider has an API key
|
||||
hasAPIKey := checkAPIKeyExists(r.apiConfig, provider)
|
||||
if !hasAPIKey {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this provider has a model configured
|
||||
modelID := getProviderSpecificModelID(r.apiConfig, "plan", provider)
|
||||
if modelID == "" {
|
||||
continue
|
||||
|
||||
// Determine if credentials exist
|
||||
hasCreds := checkAPIKeyExists(r.apiConfig, provider)
|
||||
|
||||
// Determine readiness: OCA uses auth state presence; others need creds and model
|
||||
if provider == cline.ApiProvider_OCA {
|
||||
state, _ := GetLatestOCAState(context.Background(), 2 *time.Second)
|
||||
if state == nil || state.User == nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Provider is not ready unless it has credentials AND a model configured
|
||||
if !hasCreds || modelID == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Get base URL for Ollama
|
||||
@@ -145,7 +154,7 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
Mode: "Ready",
|
||||
Provider: provider,
|
||||
ModelID: modelID,
|
||||
HasAPIKey: hasAPIKey,
|
||||
HasAPIKey: checkAPIKeyExists(r.apiConfig, provider),
|
||||
BaseURL: baseURL,
|
||||
})
|
||||
seenProviders[provider] = true
|
||||
@@ -207,9 +216,9 @@ func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
|
||||
switch providerStr {
|
||||
case "anthropic":
|
||||
return cline.ApiProvider_ANTHROPIC, true
|
||||
case "openai":
|
||||
case "openai-compatible": // internal name is 'openai', but this is actually the openai-compatible provider
|
||||
return cline.ApiProvider_OPENAI, true
|
||||
case "openai-native":
|
||||
case "openai", "openai-native": // This is the native, official Open AI provider
|
||||
return cline.ApiProvider_OPENAI_NATIVE, true
|
||||
case "openrouter":
|
||||
return cline.ApiProvider_OPENROUTER, true
|
||||
@@ -225,6 +234,8 @@ func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
|
||||
return cline.ApiProvider_CEREBRAS, true
|
||||
case "cline":
|
||||
return cline.ApiProvider_CLINE, true
|
||||
case "oca":
|
||||
return cline.ApiProvider_OCA, true
|
||||
default:
|
||||
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
|
||||
}
|
||||
@@ -237,7 +248,7 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
|
||||
case cline.ApiProvider_ANTHROPIC:
|
||||
return "anthropic"
|
||||
case cline.ApiProvider_OPENAI:
|
||||
return "openai"
|
||||
return "openai-compatible"
|
||||
case cline.ApiProvider_OPENAI_NATIVE:
|
||||
return "openai-native"
|
||||
case cline.ApiProvider_OPENROUTER:
|
||||
@@ -254,6 +265,8 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
|
||||
return "cerebras"
|
||||
case cline.ApiProvider_CLINE:
|
||||
return "cline"
|
||||
case cline.ApiProvider_OCA:
|
||||
return "oca"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -312,9 +325,9 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
|
||||
case cline.ApiProvider_ANTHROPIC:
|
||||
return "Anthropic"
|
||||
case cline.ApiProvider_OPENAI:
|
||||
return "OpenAI"
|
||||
return "OpenAI Compatible"
|
||||
case cline.ApiProvider_OPENAI_NATIVE:
|
||||
return "OpenAI Native"
|
||||
return "OpenAI (Official)"
|
||||
case cline.ApiProvider_OPENROUTER:
|
||||
return "OpenRouter"
|
||||
case cline.ApiProvider_XAI:
|
||||
@@ -329,6 +342,8 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
|
||||
return "Cerebras"
|
||||
case cline.ApiProvider_CLINE:
|
||||
return "Cline (Official)"
|
||||
case cline.ApiProvider_OCA:
|
||||
return "Oracle Code Assist"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -378,7 +393,7 @@ func FormatProviderList(result *ProviderListResult) string {
|
||||
} else {
|
||||
output.WriteString(" Base URL: (default)\n")
|
||||
}
|
||||
} else if display.Provider == cline.ApiProvider_CLINE {
|
||||
} else if display.Provider == cline.ApiProvider_CLINE || display.Provider == cline.ApiProvider_OCA {
|
||||
output.WriteString(" Status: Authenticated\n")
|
||||
} else {
|
||||
output.WriteString(" API Key: Configured\n")
|
||||
@@ -430,6 +445,12 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
verboseLog("[DEBUG] Cline provider is authenticated")
|
||||
}
|
||||
|
||||
// Check OCA provider via global auth subscription (state presence)
|
||||
if state, _ := GetLatestOCAState(context.Background(), 2*time.Second); state != nil && state.User != nil {
|
||||
configuredProviders = append(configuredProviders, cline.ApiProvider_OCA)
|
||||
verboseLog("[DEBUG] OCA provider has active auth state")
|
||||
}
|
||||
|
||||
// Check each BYO provider for API key presence
|
||||
providersToCheck := []struct {
|
||||
provider cline.ApiProvider
|
||||
@@ -459,6 +480,7 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
verboseLog("[DEBUG] Total configured providers: %d", len(configuredProviders))
|
||||
for _, p := range configuredProviders {
|
||||
verboseLog("[DEBUG] - %s", GetProviderDisplayName(p))
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// updateApiConfigurationPartial is a helper that calls the gRPC method with optional verbose logging.
|
||||
// This replaces the Manager.UpdateApiConfigurationPartial method to keep auth-specific code in the auth package.
|
||||
// This replaces the Manager.updateApiConfigurationPartial method to keep auth-specific code in the auth package.
|
||||
func updateApiConfigurationPartial(ctx context.Context, manager *task.Manager, request *cline.UpdateApiConfigurationPartialRequest) error {
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("[DEBUG] Updating API configuration (partial)")
|
||||
@@ -46,6 +46,7 @@ func updateApiConfigurationPartial(ctx context.Context, manager *task.Manager, r
|
||||
// ProviderFields defines all the field names associated with a specific provider
|
||||
type ProviderFields struct {
|
||||
APIKeyField string // API key field name (e.g., "apiKey", "openAiApiKey")
|
||||
BaseURLField string // Base URL field name (optional, empty if not applicable)
|
||||
PlanModeModelIDField string // Plan mode model ID field (e.g., "planModeApiModelId")
|
||||
ActModeModelIDField string // Act mode model ID field (e.g., "actModeApiModelId")
|
||||
PlanModeModelInfoField string // Plan mode model info field (optional, empty if not applicable)
|
||||
@@ -68,6 +69,7 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
|
||||
case cline.ApiProvider_OPENAI:
|
||||
return ProviderFields{
|
||||
APIKeyField: "openAiApiKey",
|
||||
BaseURLField: "openAiBaseUrl",
|
||||
PlanModeModelIDField: "planModeApiModelId",
|
||||
ActModeModelIDField: "actModeApiModelId",
|
||||
PlanModeProviderSpecificModelIDField: "planModeOpenAiModelId",
|
||||
@@ -142,6 +144,17 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
|
||||
ActModeProviderSpecificModelIDField: "actModeOpenRouterModelId",
|
||||
}, nil
|
||||
|
||||
case cline.ApiProvider_OCA:
|
||||
return ProviderFields{
|
||||
APIKeyField: "ocaApiKey",
|
||||
PlanModeModelIDField: "planModeApiModelId",
|
||||
ActModeModelIDField: "actModeApiModelId",
|
||||
PlanModeModelInfoField: "planModeOcaModelInfo",
|
||||
ActModeModelInfoField: "actModeOcaModelInfo",
|
||||
PlanModeProviderSpecificModelIDField: "planModeOcaModelId",
|
||||
ActModeProviderSpecificModelIDField: "actModeOcaModelId",
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
|
||||
}
|
||||
@@ -150,9 +163,12 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
|
||||
// ProviderUpdatesPartial defines optional fields for partial provider updates
|
||||
// Uses pointers to distinguish between "not provided" and "set to empty"
|
||||
type ProviderUpdatesPartial struct {
|
||||
ModelID *string // New model ID (optional)
|
||||
APIKey *string // New API key (optional)
|
||||
ModelInfo interface{} // New model info (optional, provider-specific)
|
||||
ModelID *string // New model ID (optional)
|
||||
APIKey *string // New API key (optional)
|
||||
ModelInfo interface{} // New model info (optional, provider-specific)
|
||||
BaseURL *string // New base URL (optional, e.g., for OCA, Ollama)
|
||||
RefreshToken *string // New refresh token (optional, e.g., for OCA)
|
||||
Mode *string // New mode (optional, e.g., "internal" or "external" for OCA)
|
||||
}
|
||||
|
||||
// GetModelIDFieldName returns the appropriate model ID field name for a provider and mode.
|
||||
@@ -182,7 +198,7 @@ func GetModelIDFieldName(provider cline.ApiProvider, mode string) (string, error
|
||||
// buildProviderFieldMask builds a list of camelCase field paths for the field mask.
|
||||
// When includeProviderEnums is true, the provider enum fields are included (for setting active provider).
|
||||
// When false, only the data fields are included (for configuring without activating).
|
||||
func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeModelID bool, includeModelInfo bool, includeProviderEnums bool) []string {
|
||||
func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeModelID bool, includeModelInfo bool, includeBaseURL bool, includeProviderEnums bool) []string {
|
||||
var fieldPaths []string
|
||||
|
||||
// Include provider enums if requested (used when setting active provider)
|
||||
@@ -199,6 +215,11 @@ func buildProviderFieldMask(fields ProviderFields, includeAPIKey bool, includeMo
|
||||
}
|
||||
}
|
||||
|
||||
// Add base URL field if requested and applicable
|
||||
if includeBaseURL && fields.BaseURLField != "" {
|
||||
fieldPaths = append(fieldPaths, fields.BaseURLField)
|
||||
}
|
||||
|
||||
// Add model ID fields if requested
|
||||
if includeModelID {
|
||||
// Only include provider-specific fields if they exist, otherwise use generic fields
|
||||
@@ -245,6 +266,8 @@ func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, v
|
||||
apiConfig.CerebrasApiKey = value
|
||||
case "clineApiKey":
|
||||
apiConfig.ClineApiKey = value
|
||||
case "ocaApiKey":
|
||||
apiConfig.OcaApiKey = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,11 +286,14 @@ func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldNa
|
||||
case "planModeAwsBedrockCustomModelBaseId":
|
||||
apiConfig.PlanModeAwsBedrockCustomModelBaseId = value
|
||||
apiConfig.ActModeAwsBedrockCustomModelBaseId = value
|
||||
case "planModeOcaModelId":
|
||||
apiConfig.PlanModeOcaModelId = value
|
||||
apiConfig.ActModeOcaModelId = value
|
||||
}
|
||||
}
|
||||
|
||||
// AddProviderPartial configures a new provider with all necessary fields using partial updates.
|
||||
func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID string, apiKey string, modelInfo interface{}) error {
|
||||
func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID string, apiKey string, baseURL string, modelInfo interface{}) error {
|
||||
// Get field mapping for this provider
|
||||
fields, err := GetProviderFields(provider)
|
||||
if err != nil {
|
||||
@@ -282,6 +308,13 @@ func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cli
|
||||
setAPIKeyField(apiConfig, fields.APIKeyField, proto.String(apiKey))
|
||||
}
|
||||
|
||||
// Set base URL field if provided and applicable
|
||||
includeBaseURL := false
|
||||
if baseURL != "" && fields.BaseURLField != "" {
|
||||
setBaseURLField(apiConfig, fields.BaseURLField, proto.String(baseURL))
|
||||
includeBaseURL = true
|
||||
}
|
||||
|
||||
// Set model ID fields
|
||||
apiConfig.PlanModeApiModelId = proto.String(modelID)
|
||||
apiConfig.ActModeApiModelId = proto.String(modelID)
|
||||
@@ -301,7 +334,7 @@ func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cli
|
||||
|
||||
// Build field mask including all fields we're setting (without provider enums)
|
||||
includeModelInfo := fields.PlanModeModelInfoField != "" && modelInfo != nil
|
||||
fieldPaths := buildProviderFieldMask(fields, true, true, includeModelInfo, false)
|
||||
fieldPaths := buildProviderFieldMask(fields, true, true, includeModelInfo, includeBaseURL, false)
|
||||
|
||||
// Create field mask
|
||||
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
|
||||
@@ -368,7 +401,7 @@ func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider
|
||||
}
|
||||
|
||||
// Build field mask for only the fields being updated
|
||||
fieldPaths := buildProviderFieldMask(fields, includeAPIKey, includeModelID, includeModelInfo, setAsActive)
|
||||
fieldPaths := buildProviderFieldMask(fields, includeAPIKey, includeModelID, includeModelInfo, false, setAsActive)
|
||||
|
||||
// Create field mask
|
||||
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
|
||||
@@ -421,6 +454,46 @@ func RemoveProviderPartial(ctx context.Context, manager *task.Manager, provider
|
||||
return nil
|
||||
}
|
||||
|
||||
// setBaseURLField sets the appropriate base URL field in the config based on the field name
|
||||
func setBaseURLField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
|
||||
switch fieldName {
|
||||
case "ocaBaseUrl":
|
||||
apiConfig.OcaBaseUrl = value
|
||||
case "ollamaBaseUrl":
|
||||
apiConfig.OllamaBaseUrl = value
|
||||
case "openAiBaseUrl":
|
||||
apiConfig.OpenAiBaseUrl = value
|
||||
case "geminiBaseUrl":
|
||||
apiConfig.GeminiBaseUrl = value
|
||||
case "liteLlmBaseUrl":
|
||||
apiConfig.LiteLlmBaseUrl = value
|
||||
case "anthropicBaseUrl":
|
||||
apiConfig.AnthropicBaseUrl = value
|
||||
case "requestyBaseUrl":
|
||||
apiConfig.RequestyBaseUrl = value
|
||||
case "lmStudioBaseUrl":
|
||||
apiConfig.LmStudioBaseUrl = value
|
||||
case "oca":
|
||||
apiConfig.OcaBaseUrl = value
|
||||
}
|
||||
}
|
||||
|
||||
// setRefreshTokenField sets the appropriate refresh token field in the config
|
||||
func setRefreshTokenField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
|
||||
switch fieldName {
|
||||
case "ocaRefreshToken":
|
||||
apiConfig.OcaRefreshToken = value
|
||||
}
|
||||
}
|
||||
|
||||
// setModeField sets the appropriate mode field in the config
|
||||
func setModeField(apiConfig *cline.ModelsApiConfiguration, fieldName string, value *string) {
|
||||
switch fieldName {
|
||||
case "ocaMode":
|
||||
apiConfig.OcaMode = value
|
||||
}
|
||||
}
|
||||
|
||||
// BedrockOptionalFields holds optional configuration fields for AWS Bedrock
|
||||
type BedrockOptionalFields struct {
|
||||
SessionToken *string // Optional: AWS session token for temporary credentials
|
||||
@@ -434,6 +507,12 @@ type BedrockOptionalFields struct {
|
||||
Endpoint *string // Optional: Custom endpoint URL
|
||||
}
|
||||
|
||||
// OcaOptionalFields holds optional configuration fields for Oracle Code Assist
|
||||
type OcaOptionalFields struct {
|
||||
BaseURL *string // Optional: Base URL
|
||||
Mode *string // Optional: Mode ("internal" or "external")
|
||||
}
|
||||
|
||||
// setBedrockOptionalFields sets optional Bedrock-specific fields in the API configuration
|
||||
func setBedrockOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *BedrockOptionalFields) {
|
||||
if fields == nil {
|
||||
@@ -469,6 +548,20 @@ func setBedrockOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *B
|
||||
}
|
||||
}
|
||||
|
||||
// setOcaOptionalFields sets optional Oca-specific fields in the API configuration
|
||||
func setOcaOptionalFields(apiConfig *cline.ModelsApiConfiguration, fields *OcaOptionalFields) {
|
||||
if fields == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if fields.Mode != nil {
|
||||
apiConfig.OcaMode = fields.Mode
|
||||
}
|
||||
if fields.BaseURL != nil {
|
||||
apiConfig.OcaBaseUrl = fields.BaseURL
|
||||
}
|
||||
}
|
||||
|
||||
// buildBedrockOptionalFieldMask builds field mask paths for Bedrock optional fields that have values
|
||||
func buildBedrockOptionalFieldMask(fields *BedrockOptionalFields) []string {
|
||||
if fields == nil {
|
||||
@@ -507,3 +600,21 @@ func buildBedrockOptionalFieldMask(fields *BedrockOptionalFields) []string {
|
||||
|
||||
return fieldPaths
|
||||
}
|
||||
|
||||
// buildOcaOptionalFieldMask builds field mask paths for Bedrock optional fields that have values
|
||||
func buildOcaOptionalFieldMask(fields *OcaOptionalFields) []string {
|
||||
if fields == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var fieldPaths []string
|
||||
|
||||
if fields.Mode != nil {
|
||||
fieldPaths = append(fieldPaths, "ocaMode")
|
||||
}
|
||||
if fields.BaseURL != nil {
|
||||
fieldPaths = append(fieldPaths, "ocaBaseUrl")
|
||||
}
|
||||
|
||||
return fieldPaths
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
@@ -40,7 +41,7 @@ func (pw *ProviderWizard) showMainMenu() (string, error) {
|
||||
huh.NewSelect[string]().
|
||||
Title("What would you like to do?").
|
||||
Options(
|
||||
huh.NewOption("Configure a new provider", "add"),
|
||||
huh.NewOption("Add or change an API provider", "add"),
|
||||
huh.NewOption("Change model for API provider", "change-model"),
|
||||
huh.NewOption("Remove a provider", "remove"),
|
||||
huh.NewOption("List configured providers", "list"),
|
||||
@@ -107,8 +108,13 @@ func (pw *ProviderWizard) handleAddProvider() error {
|
||||
return pw.handleAddBedrockProvider()
|
||||
}
|
||||
|
||||
// Step 2b: Special handling for OCA provider
|
||||
if provider == cline.ApiProvider_OCA {
|
||||
return pw.handleAddOcaProvider()
|
||||
}
|
||||
|
||||
// Step 3: Get API key first (for non-Bedrock providers)
|
||||
apiKey, err := PromptForAPIKey(provider)
|
||||
apiKey, baseURL, err := PromptForAPIKey(provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API key: %w", err)
|
||||
}
|
||||
@@ -120,7 +126,7 @@ func (pw *ProviderWizard) handleAddProvider() error {
|
||||
}
|
||||
|
||||
// Step 5: Apply configuration using AddProviderPartial
|
||||
if err := AddProviderPartial(pw.ctx, pw.manager, provider, modelID, apiKey, modelInfo); err != nil {
|
||||
if err := AddProviderPartial(pw.ctx, pw.manager, provider, modelID, apiKey, baseURL, modelInfo); err != nil {
|
||||
return fmt.Errorf("failed to save configuration: %w", err)
|
||||
}
|
||||
|
||||
@@ -162,6 +168,51 @@ func (pw *ProviderWizard) handleAddBedrockProvider() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleAddOcaProvider handles adding Oracle Code Assist provider with optional settings and auth
|
||||
func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
// Step 1: Get OCA configuration (base URL and mode)
|
||||
config, err := PromptForOcaConfig(pw.ctx, pw.manager)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "user aborted") || strings.Contains(err.Error(), "cancelled") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
// Apply OCA configuration (base URL and mode)
|
||||
if err := ApplyOcaConfig(pw.ctx, pw.manager, config); err != nil {
|
||||
return fmt.Errorf("failed to save OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Ensure OCA authentication
|
||||
if err := ensureOcaAuthenticated(pw.ctx); err != nil {
|
||||
return fmt.Errorf("failed to authenticate with OCA: %w", err)
|
||||
}
|
||||
|
||||
// Step 3: Select model
|
||||
modelID, _, err := pw.selectModel(cline.ApiProvider_OCA, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("model selection failed: %w", err)
|
||||
}
|
||||
|
||||
// Step 4: Apply the OCA model configuration and set as active
|
||||
updates := ProviderUpdatesPartial{
|
||||
ModelID: &modelID,
|
||||
ModelInfo: nil,
|
||||
}
|
||||
|
||||
if err := UpdateProviderPartial(pw.ctx, pw.manager, cline.ApiProvider_OCA, updates, true); err != nil {
|
||||
return fmt.Errorf("failed to save OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
if err := setWelcomeViewCompleted(pw.ctx, pw.manager); err != nil {
|
||||
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ OCA provider configured successfully!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleListProviders retrieves and displays configured providers
|
||||
func (pw *ProviderWizard) handleListProviders() error {
|
||||
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
|
||||
@@ -259,6 +310,15 @@ func (pw *ProviderWizard) fetchModelsForProvider(provider cline.ApiProvider, api
|
||||
}
|
||||
// Ollama returns just model IDs without additional info, so modelInfo map is nil
|
||||
return modelIDs, nil, nil
|
||||
|
||||
case cline.ApiProvider_OCA:
|
||||
// OCA supports dynamic model fetching
|
||||
models, err := FetchOcaModels(pw.ctx, pw.manager)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
interfaceMap := ConvertOcaModelsToInterface(models)
|
||||
return ConvertModelsMapToSlice(interfaceMap), interfaceMap, nil
|
||||
}
|
||||
|
||||
// Fall back to static models for providers that don't support dynamic fetching
|
||||
@@ -525,8 +585,17 @@ func getProviderModelIDFromState(stateData map[string]interface{}, provider clin
|
||||
return ""
|
||||
}
|
||||
|
||||
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
|
||||
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
|
||||
func getProviderAPIKeyFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
|
||||
// OCA uses account authentication, not API keys. Consider it "present" if authenticated.
|
||||
if provider == cline.ApiProvider_OCA {
|
||||
if state, _ := GetLatestOCAState(context.TODO(), 2 * time.Second); state != nil && state.User != nil {
|
||||
// Return a sentinel non-empty string so upstream checks pass.
|
||||
return "OCA_AUTH_VERIFIED"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fields, err := GetProviderFields(provider)
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -656,7 +725,16 @@ func (pw *ProviderWizard) handleRemoveProvider() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 7: Clear the API key for the selected provider
|
||||
// Step 7: If removing OCA, sign out first
|
||||
if selectedProvider.Provider == cline.ApiProvider_OCA {
|
||||
if err := signOutOca(pw.ctx); err != nil {
|
||||
fmt.Printf("Warning: Failed to sign out of OCA: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Signed out of OCA.")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: Clear the API key for the selected provider
|
||||
if err := pw.clearProviderAPIKey(selectedProvider.Provider); err != nil {
|
||||
return fmt.Errorf("failed to remove provider: %w", err)
|
||||
}
|
||||
@@ -670,6 +748,16 @@ func (pw *ProviderWizard) clearProviderAPIKey(provider cline.ApiProvider) error
|
||||
return RemoveProviderPartial(pw.ctx, pw.manager, provider)
|
||||
}
|
||||
|
||||
|
||||
func signOutOca(ctx context.Context) error {
|
||||
client, err := global.GetDefaultClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.Ocaaccount.OcaAccountLogoutClicked(ctx, &cline.EmptyRequest{})
|
||||
return err
|
||||
}
|
||||
|
||||
func setWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
|
||||
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
)
|
||||
|
||||
// OcaConfig holds Oracle Code Assist (OCA) configuration fields
|
||||
type OcaConfig struct {
|
||||
BaseURL string
|
||||
Mode string
|
||||
}
|
||||
|
||||
// PromptForOcaConfig displays a form for OCA configuration (base URL and mode)
|
||||
func PromptForOcaConfig(ctx context.Context, manager *task.Manager) (*OcaConfig, error) {
|
||||
config := &OcaConfig{}
|
||||
var mode string
|
||||
|
||||
// Collect optional settings
|
||||
configForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Base URL").
|
||||
Value(&config.BaseURL).
|
||||
Description("Leave empty to use default Base URL"),
|
||||
|
||||
huh.NewSelect[string]().
|
||||
Title("Choose OCA mode (used for authentication)").
|
||||
Description("Select 'Internal' to use Cline's internal OCA, or 'External' for your own OCA instance").
|
||||
Options(
|
||||
huh.NewOption("Internal", "internal"),
|
||||
huh.NewOption("External", "external"),
|
||||
).
|
||||
Value(&mode),
|
||||
),
|
||||
)
|
||||
|
||||
if err := configForm.Run(); err != nil {
|
||||
return nil, fmt.Errorf("failed to get OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
// Trim whitespace from string fields
|
||||
config.BaseURL = strings.TrimSpace(config.BaseURL)
|
||||
config.Mode = strings.TrimSpace(mode)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// ApplyOcaConfig applies OCA configuration using partial updates
|
||||
func ApplyOcaConfig(ctx context.Context, manager *task.Manager, config *OcaConfig) error {
|
||||
// Build the API configuration with all OCA fields
|
||||
apiConfig := &cline.ModelsApiConfiguration{}
|
||||
|
||||
// Set profile authentication fields (always required)
|
||||
optionalFields := &OcaOptionalFields{}
|
||||
|
||||
// Set profile name (can be empty for default profile)
|
||||
if config.BaseURL != "" {
|
||||
optionalFields.BaseURL = proto.String(config.BaseURL)
|
||||
}
|
||||
|
||||
// Set optional fields if provided
|
||||
if config.Mode != "" {
|
||||
optionalFields.Mode = proto.String(config.Mode)
|
||||
}
|
||||
|
||||
// Apply all fields to the config
|
||||
setOcaOptionalFields(apiConfig, optionalFields)
|
||||
|
||||
// Add profile authentication field paths
|
||||
optionalPaths := buildOcaOptionalFieldMask(optionalFields)
|
||||
|
||||
// Create field mask
|
||||
fieldMask := &fieldmaskpb.FieldMask{Paths: optionalPaths}
|
||||
|
||||
// Apply the partial update
|
||||
request := &cline.UpdateApiConfigurationPartialRequest{
|
||||
ApiConfiguration: apiConfig,
|
||||
UpdateMask: fieldMask,
|
||||
}
|
||||
|
||||
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
|
||||
return fmt.Errorf("failed to apply OCA configuration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// OCA Auth Listener Singleton
|
||||
// ===========================
|
||||
|
||||
type ocaAuthStream interface {
|
||||
Recv() (*cline.OcaAuthState, error)
|
||||
}
|
||||
|
||||
// OcaAuthStatusListener manages subscription to OCA auth status updates
|
||||
type OcaAuthStatusListener struct {
|
||||
stream ocaAuthStream
|
||||
updatesCh chan *cline.OcaAuthState
|
||||
errCh chan error
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
lastState *cline.OcaAuthState
|
||||
firstEventCh chan struct{}
|
||||
firstEventOnce sync.Once
|
||||
}
|
||||
|
||||
// NewOcaAuthStatusListener creates a new OCA auth status listener
|
||||
func NewOcaAuthStatusListener(parentCtx context.Context) (*OcaAuthStatusListener, error) {
|
||||
client, err := global.GetDefaultClient(parentCtx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get client: %w", err)
|
||||
}
|
||||
|
||||
// Keep the listener alive independently of short-lived caller contexts
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Subscribe to OCA auth status updates
|
||||
stream, err := client.Ocaaccount.OcaSubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to subscribe to OCA auth updates: %w", err)
|
||||
}
|
||||
|
||||
return &OcaAuthStatusListener{
|
||||
stream: stream,
|
||||
updatesCh: make(chan *cline.OcaAuthState, 10),
|
||||
errCh: make(chan error, 1),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
firstEventCh: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start begins listening to the auth status update stream
|
||||
func (l *OcaAuthStatusListener) Start() error {
|
||||
go l.readStream()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *OcaAuthStatusListener) readStream() {
|
||||
defer close(l.updatesCh)
|
||||
defer close(l.errCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
return
|
||||
default:
|
||||
state, err := l.stream.Recv()
|
||||
if err != nil {
|
||||
// Propagate error and exit
|
||||
if err == io.EOF {
|
||||
// Treat as error to notify waiters
|
||||
err = fmt.Errorf("OCA auth status stream closed")
|
||||
}
|
||||
select {
|
||||
case l.errCh <- err:
|
||||
case <-l.ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
l.lastState = state
|
||||
l.mu.Unlock()
|
||||
|
||||
// Notify first event waiters
|
||||
l.firstEventOnce.Do(func() { close(l.firstEventCh) })
|
||||
|
||||
select {
|
||||
case l.updatesCh <- state:
|
||||
case <-l.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForFirstEvent blocks until the first event is received or timeout occurs
|
||||
func (l *OcaAuthStatusListener) WaitForFirstEvent(timeout time.Duration) error {
|
||||
// Fast-path if already have a state
|
||||
l.mu.RLock()
|
||||
ready := l.lastState != nil
|
||||
l.mu.RUnlock()
|
||||
if ready {
|
||||
return nil
|
||||
}
|
||||
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-l.firstEventCh:
|
||||
return nil
|
||||
case <-timer.C:
|
||||
return fmt.Errorf("timeout waiting for initial OCA auth event")
|
||||
case <-l.ctx.Done():
|
||||
return fmt.Errorf("OCA auth listener cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
// IsAuthenticated returns true if the last known OCA auth state is authenticated
|
||||
func (l *OcaAuthStatusListener) IsAuthenticated() bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return isOCAStateAuthenticated(l.lastState)
|
||||
}
|
||||
|
||||
// WaitForAuthentication waits until OCA authentication succeeds or timeout occurs
|
||||
func (l *OcaAuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
// If already authenticated, return immediately
|
||||
if l.IsAuthenticated() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
return fmt.Errorf("OCA authentication timeout after %v - please try again", timeout)
|
||||
case <-l.ctx.Done():
|
||||
return fmt.Errorf("OCA authentication cancelled")
|
||||
case err := <-l.errCh:
|
||||
return fmt.Errorf("OCA authentication stream error: %w", err)
|
||||
case state := <-l.updatesCh:
|
||||
if isOCAStateAuthenticated(state) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop closes the stream and cleans up resources
|
||||
func (l *OcaAuthStatusListener) Stop() {
|
||||
l.cancel()
|
||||
}
|
||||
|
||||
func isOCAStateAuthenticated(state *cline.OcaAuthState) bool {
|
||||
return state != nil && state.User != nil
|
||||
}
|
||||
|
||||
// Singleton holder
|
||||
var (
|
||||
ocaListener *OcaAuthStatusListener
|
||||
ocaListenerOnce sync.Once
|
||||
ocaListenerErr error
|
||||
)
|
||||
|
||||
// GetOcaAuthListener returns the OCA auth listener singleton
|
||||
func GetOcaAuthListener(ctx context.Context) (*OcaAuthStatusListener, error) {
|
||||
// Allow optional ctx: if nil, use context.TODO(). If already initialized, return singleton.
|
||||
if ctx == nil {
|
||||
ctx = context.TODO()
|
||||
}
|
||||
|
||||
ocaListenerOnce.Do(func() {
|
||||
l, err := NewOcaAuthStatusListener(ctx)
|
||||
if err != nil {
|
||||
ocaListenerErr = err
|
||||
return
|
||||
}
|
||||
if err := l.Start(); err != nil {
|
||||
ocaListenerErr = err
|
||||
return
|
||||
}
|
||||
ocaListener = l
|
||||
})
|
||||
return ocaListener, ocaListenerErr
|
||||
}
|
||||
|
||||
// IsOCAAuthenticated returns true if the global OCA auth status is authenticated.
|
||||
// It attempts a brief wait for the first event to avoid stale reads.
|
||||
func IsOCAAuthenticated(ctx context.Context) bool {
|
||||
l, err := GetOcaAuthListener(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = l.WaitForFirstEvent(1 * time.Second) // best-effort
|
||||
return l.IsAuthenticated()
|
||||
}
|
||||
|
||||
// LatestState returns the last received OCA auth state (may be nil)
|
||||
func (l *OcaAuthStatusListener) LatestState() *cline.OcaAuthState {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return l.lastState
|
||||
}
|
||||
|
||||
// GetLatestOCAState returns the latest known OCA auth state, optionally waiting for the first event
|
||||
func GetLatestOCAState(ctx context.Context, timeout time.Duration) (*cline.OcaAuthState, error) {
|
||||
l, err := GetOcaAuthListener(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if timeout > 0 {
|
||||
if err := l.WaitForFirstEvent(timeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return l.LatestState(), nil
|
||||
}
|
||||
|
||||
// ensureOcaAuthenticated initiates OCA login (if needed) and waits for success using the singleton listener
|
||||
func ensureOcaAuthenticated(ctx context.Context) error {
|
||||
// Ensure listener exists
|
||||
listener, err := GetOcaAuthListener(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize OCA auth listener: %w", err)
|
||||
}
|
||||
|
||||
// Briefly wait for first event to know current state
|
||||
_ = listener.WaitForFirstEvent(1 * time.Second)
|
||||
|
||||
// If already authenticated, nothing to do
|
||||
if listener.IsAuthenticated() {
|
||||
fmt.Println("✓ OCA authentication already active.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create gRPC client for initiating login
|
||||
client, err := global.GetDefaultClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to obtain client: %w", err)
|
||||
}
|
||||
|
||||
// Start login and wait for authentication
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Initiate login (opens the browser with a callback URL from Cline Core)
|
||||
response, err := client.Ocaaccount.OcaAccountLoginClicked(waitCtx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initiate OCA login: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\nOpening browser for OCA authentication...")
|
||||
if response != nil && response.Value != "" {
|
||||
fmt.Printf("If the browser doesn't open automatically, visit this URL:\n%s\n\n", response.Value)
|
||||
}
|
||||
fmt.Println("Waiting for you to complete OCA authentication in your browser...")
|
||||
fmt.Println("(This may take a few moments. Timeout: 5 minutes)")
|
||||
|
||||
// Block until authenticated or timeout
|
||||
if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("✓ OCA authentication successful!")
|
||||
return nil
|
||||
}
|
||||
@@ -123,7 +123,10 @@ func setCommand() *cobra.Command {
|
||||
Use: "set <key=value> [key=value...]",
|
||||
Aliases: []string{"s"},
|
||||
Short: "Set configuration variables",
|
||||
Long: `Set one or more global configuration variables using key=value format.`,
|
||||
Long: `Set one or more global configuration variables using key=value format.
|
||||
|
||||
This command merges the provided settings with existing values, preserving
|
||||
unspecified fields. Only the fields you explicitly set will be updated.`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
@@ -139,7 +142,7 @@ func setCommand() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update settings
|
||||
// Update settings (server-side merge handles preserving existing values)
|
||||
return configManager.UpdateSettings(ctx, settings, secrets)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ func renderAutoApprovalSettings(value interface{}, censor bool) error {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Print other fields normally (enabled, maxRequests, enableNotifications, favorites)
|
||||
// Print other fields normally (enabled, enableNotifications, favorites)
|
||||
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -672,6 +660,8 @@ func parseApiProvider(value string) (cline.ApiProvider, error) {
|
||||
return cline.ApiProvider_DIFY, nil
|
||||
case "oca":
|
||||
return cline.ApiProvider_OCA, nil
|
||||
case "minimax":
|
||||
return cline.ApiProvider_MINIMAX, nil
|
||||
default:
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("invalid api_provider '%s'", value)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -144,6 +144,7 @@ const (
|
||||
OPENAI_NATIVE = "openai-native"
|
||||
XAI = "xai"
|
||||
CEREBRAS = "cerebras"
|
||||
OCA = "oca"
|
||||
)
|
||||
|
||||
// AllProviders returns a slice of enabled provider IDs for the CLI build.
|
||||
@@ -159,6 +160,7 @@ var AllProviders = []string{
|
||||
"openai-native",
|
||||
"xai",
|
||||
"cerebras",
|
||||
"oca",
|
||||
}
|
||||
|
||||
// ConfigField represents a configuration field requirement
|
||||
@@ -467,6 +469,16 @@ var rawModelDefinitions = ` {
|
||||
"supportsImages": true,
|
||||
"supportsPromptCache": true
|
||||
},
|
||||
"claude-haiku-4-5-20251001": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 200000,
|
||||
"inputPrice": 1,
|
||||
"outputPrice": 5,
|
||||
"cacheWritesPrice": 1,
|
||||
"cacheReadsPrice": 0,
|
||||
"supportsImages": true,
|
||||
"supportsPromptCache": true
|
||||
},
|
||||
"claude-sonnet-4-20250514": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 200000,
|
||||
@@ -579,6 +591,16 @@ var rawModelDefinitions = ` {
|
||||
"supportsImages": true,
|
||||
"supportsPromptCache": true
|
||||
},
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 200000,
|
||||
"inputPrice": 1,
|
||||
"outputPrice": 5,
|
||||
"cacheWritesPrice": 1,
|
||||
"cacheReadsPrice": 0,
|
||||
"supportsImages": true,
|
||||
"supportsPromptCache": true
|
||||
},
|
||||
"anthropic.claude-sonnet-4-20250514-v1:0": {
|
||||
"maxTokens": 8192,
|
||||
"contextWindow": 200000,
|
||||
@@ -1389,6 +1411,18 @@ func GetProviderDefinitions() (map[string]ProviderDefinition, error) {
|
||||
HasDynamicModels: false,
|
||||
SetupInstructions: `Get your API key from https://cloud.cerebras.ai/`,
|
||||
}
|
||||
|
||||
// Oca
|
||||
definitions["oca"] = ProviderDefinition{
|
||||
ID: "oca",
|
||||
Name: "Oca",
|
||||
RequiredFields: getFieldsByProvider("oca", configFields, true),
|
||||
OptionalFields: getFieldsByProvider("oca", configFields, false),
|
||||
Models: modelDefinitions["oca"],
|
||||
DefaultModelID: "",
|
||||
HasDynamicModels: false,
|
||||
SetupInstructions: `Configure Oca API credentials`,
|
||||
}
|
||||
|
||||
return definitions, nil
|
||||
}
|
||||
@@ -1415,6 +1449,7 @@ func GetProviderDisplayName(providerID string) string {
|
||||
"openai-native": "OpenAI",
|
||||
"xai": "X AI (Grok)",
|
||||
"cerebras": "Cerebras",
|
||||
"oca": "Oca",
|
||||
}
|
||||
|
||||
if name, exists := displayNames[providerID]; exists {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -35,7 +35,7 @@ Create a simple website in a single HTML file. It should have:
|
||||
```
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/installation/chat-prompt.png" alt="Cline Chat Prompt"/>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/chat-prompt.png" alt="Cline Chat Prompt"/>
|
||||
</Frame>
|
||||
|
||||
Press Enter and watch Cline work!
|
||||
|
||||
@@ -34,18 +34,10 @@ First, you'll need to install and authenticate Claude Code on your system:
|
||||
|
||||
<br />
|
||||
|
||||
<Accordion title="Windows Setup">
|
||||
Anthropic introduced full support for Claude Code on Windows. Follow the [instructions on how to set up Claude Code
|
||||
normally](#setup) and make sure you have the latest Claude Code and Cline versions.
|
||||
</Accordion>
|
||||
|
||||
### Finding your Claude Code path
|
||||
|
||||
If you're not sure where Claude Code is installed:
|
||||
|
||||
- **macOS / Linux**: Run `which claude` in your terminal
|
||||
- **Windows (Command Prompt)**: Run `where claude`
|
||||
- **Windows (PowerShell)**: Run `Get-Command claude`
|
||||
- **macOS / Linux / WSL / Git Bash**: `which claude`
|
||||
- **Windows Command Prompt**: `where claude`
|
||||
|
||||
## Supported Models
|
||||
|
||||
|
||||
+2
-1
@@ -42,7 +42,8 @@ h5,
|
||||
h6,
|
||||
img {
|
||||
opacity: 1 !important;
|
||||
font-family: "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
|
||||
font-family:
|
||||
"Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Also apply to any h1 elements within content areas */
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
repositories
|
||||
|
||||
results/evals.db
|
||||
temp-files
|
||||
results
|
||||
|
||||
diff-edits/cases/
|
||||
diff-edits/results/
|
||||
@@ -21,4 +21,4 @@ diff_editing/test_outputs/
|
||||
# Python bytecode cache
|
||||
*__pycache__/
|
||||
|
||||
diff-edits/cases.zip
|
||||
diff-edits/cases.zip
|
||||
|
||||
+32
-70
@@ -15,48 +15,32 @@ The Cline Evaluation System allows you to:
|
||||
|
||||
The evaluation system consists of two main components:
|
||||
|
||||
1. **Test Server**: Enhanced HTTP server in `src/services/test/TestServer.ts` that provides detailed task results
|
||||
2. **CLI Tool**: Command-line interface in `evals/cli/` for orchestrating evaluations
|
||||
3. **Diff Edit Benchmark**: Separate command using the CLI tool that runs a comprehensive diff editing benchmark suite on real world cases, along with a streamlit dashboard displaying the results. For more details, see the [Diff Edit Benchmark README](./diff-edits/README.md). Make sure you add a `evals/diff-edits/cases` folder with all the conversation jsons.
|
||||
1. **CLI Tool**: Command-line interface in `evals/cli/` for orchestrating evaluations
|
||||
2. **Diff Edit Benchmark**: Separate command using the CLI tool that runs a comprehensive diff editing benchmark suite on real world cases, along with a streamlit dashboard displaying the results. For more details, see the Diff Edit Benchmark [README](./diff-edits/README.md). Make sure you add a `evals/diff-edits/cases` folder with all the conversation jsons.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
cline-repo/
|
||||
├── src/
|
||||
│ ├── services/
|
||||
│ │ ├── test/
|
||||
│ │ │ ├── TestServer.ts # Enhanced HTTP server for task execution
|
||||
│ │ │ ├── GitHelper.ts # Git utilities for file tracking
|
||||
│ │ │ └── ...
|
||||
│ │ └── ...
|
||||
│ └── ...
|
||||
├── evals/ # Main directory for evaluation system
|
||||
│ ├── cli/ # CLI tool for orchestrating evaluations
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── index.ts # CLI entry point
|
||||
│ │ │ ├── commands/ # CLI commands (setup, run, report)
|
||||
│ │ │ ├── adapters/ # Benchmark adapters
|
||||
│ │ │ ├── db/ # Database management
|
||||
│ │ │ └── utils/ # Utility functions
|
||||
│ │ ├── package.json
|
||||
│ │ └── tsconfig.json
|
||||
│ ├── diff-edits/ # Diff editing evaluation suite
|
||||
│ │ ├── cases/ # Test case JSON files
|
||||
│ │ ├── results/ # Evaluation results
|
||||
│ │ ├── diff-apply/ # Diff application logic
|
||||
│ │ ├── parsing/ # Assistant message parsing
|
||||
│ │ └── prompts/ # System prompts
|
||||
│ ├── repositories/ # Cloned benchmark repositories
|
||||
│ │ ├── exercism/ # Modified Exercism (from pashpashpash/evals)
|
||||
│ │ ├── swe-bench/ # SWE-Bench repository
|
||||
│ │ ├── swelancer/ # SWELancer repository
|
||||
│ │ └── multi-swe/ # Multi-SWE-Bench repository
|
||||
│ ├── results/ # Evaluation results storage
|
||||
│ │ ├── runs/ # Individual run results
|
||||
│ │ └── reports/ # Generated reports
|
||||
│ └── README.md # This file
|
||||
└── ...
|
||||
evals/ # Main directory for evaluation system
|
||||
├── cli/ # CLI tool for orchestrating evaluations
|
||||
│ └── src/
|
||||
│ ├── index.ts # CLI entry point
|
||||
│ ├── commands/ # CLI commands (setup, run, report)
|
||||
│ ├── adapters/ # Benchmark adapters
|
||||
│ ├── db/ # Database management
|
||||
│ └── utils/ # Utility functions
|
||||
├── diff-edits/ # Diff editing evaluation suite
|
||||
│ ├── cases/ # Test case JSON files
|
||||
│ ├── results/ # Evaluation results
|
||||
│ ├── diff-apply/ # Diff application logic
|
||||
│ ├── parsing/ # Assistant message parsing
|
||||
│ └── prompts/ # System prompts
|
||||
├── repositories/ # Cloned benchmark repositories
|
||||
│ └── exercism/ # Exercism (Aider Polyglot)
|
||||
├── results/ # Evaluation results storage
|
||||
│ ├── runs/ # Individual run results
|
||||
│ └── reports/ # Generated reports
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
@@ -67,25 +51,14 @@ cline-repo/
|
||||
- VSCode with Cline extension installed
|
||||
- Git
|
||||
|
||||
### Activation Mechanism
|
||||
|
||||
The evaluation system uses an `evals.env` file approach to activate test mode in the Cline extension. When an evaluation is run:
|
||||
|
||||
1. The CLI creates an `evals.env` file in the workspace directory
|
||||
2. The Cline extension activates due to the `workspaceContains:evals.env` activation event
|
||||
3. The extension detects this file and automatically enters test mode
|
||||
4. After evaluation completes, the file is automatically removed
|
||||
|
||||
This approach eliminates the need for environment variables during the build process and allows for targeted activation only when needed for evaluations. The extension remains dormant during normal use, only activating when an evals.env file is present. For more details, see [Evals Env Activation](./docs/evals-env-activation.md).
|
||||
|
||||
### Installation
|
||||
|
||||
1. Build the CLI tool:
|
||||
|
||||
```bash
|
||||
cd evals/cli
|
||||
cd evals
|
||||
npm install
|
||||
npm run build
|
||||
npm run build:cli
|
||||
```
|
||||
|
||||
### Usage
|
||||
@@ -106,13 +79,14 @@ node dist/index.js setup --benchmarks exercism
|
||||
#### Running Evaluations
|
||||
|
||||
```bash
|
||||
node dist/index.js run --model claude-3-opus-20240229 --benchmark exercism
|
||||
node dist/index.js run --benchmark exercism --count 10
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--model`: The model to evaluate (default: claude-3-opus-20240229)
|
||||
- `--benchmark`: Specific benchmark to run (default: all)
|
||||
- `--count`: Number of tasks to run (default: all)
|
||||
- `--benchmark`: Specific benchmark to run (default: exercism)
|
||||
- `--count`: Number of tasks to run (default: all available tasks)
|
||||
|
||||
**Note:** Model selection is currently configured through the Cline CLI itself, not through evaluation flags.
|
||||
|
||||
#### Generating Reports
|
||||
|
||||
@@ -124,24 +98,11 @@ Options:
|
||||
- `--format`: Report format (json, markdown) (default: markdown)
|
||||
- `--output`: Output path for the report
|
||||
|
||||
#### Managing Test Mode Activation
|
||||
|
||||
The CLI provides a command to manually manage the evals.env file for test mode activation:
|
||||
|
||||
```bash
|
||||
node dist/index.js evals-env create # Create evals.env file in current directory
|
||||
node dist/index.js evals-env remove # Remove evals.env file from current directory
|
||||
node dist/index.js evals-env check # Check if evals.env file exists in current directory
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--directory`: Specify a directory other than the current one
|
||||
|
||||
## Benchmarks
|
||||
|
||||
### Exercism
|
||||
|
||||
Modified Exercism exercises from the [pashpashpash/evals](https://github.com/pashpashpash/evals) repository. These are small, focused programming exercises in various languages.
|
||||
Modified Exercism exercises from the [polyglot-benchmark](https://github.com/Aider-AI/polyglot-benchmark) repository. These are small, focused programming exercises in various languages.
|
||||
|
||||
### SWE-Bench (Coming Soon)
|
||||
|
||||
@@ -350,7 +311,8 @@ The evaluation system collects the following metrics:
|
||||
- **Duration**: Time taken to complete tasks
|
||||
- **Tool Usage**: Number of tool calls and failures
|
||||
- **Success Rate**: Percentage of tasks completed successfully
|
||||
- **Functional Correctness**: Percentage of tests passed
|
||||
- **Test Success Rate**: Percentage of tests passed
|
||||
- **Functional Correctness**: Ratio of tests passed to total tests
|
||||
|
||||
## Reports
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import chalk from "chalk"
|
||||
import execa from "execa"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
@@ -20,8 +21,12 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
|
||||
if (!fs.existsSync(exercismDir)) {
|
||||
console.log(`Cloning Exercism repository to ${exercismDir}...`)
|
||||
await execa("git", ["clone", "https://github.com/pashpashpash/evals.git", exercismDir])
|
||||
await execa("git", ["clone", "https://github.com/Aider-AI/polyglot-benchmark.git", exercismDir])
|
||||
console.log("Exercism repository cloned successfully")
|
||||
|
||||
// Unskip all JavaScript and Java tests after cloning
|
||||
this.unskipAllJavaScriptTests(exercismDir)
|
||||
this.unskipAllJavaTests(exercismDir)
|
||||
} else {
|
||||
console.log(`Exercism repository already exists at ${exercismDir}`)
|
||||
|
||||
@@ -29,6 +34,10 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
console.log("Pulling latest changes...")
|
||||
await execa("git", ["pull"], { cwd: exercismDir })
|
||||
console.log("Repository updated successfully")
|
||||
|
||||
// Unskip tests again after pulling
|
||||
this.unskipAllJavaScriptTests(exercismDir)
|
||||
this.unskipAllJavaTests(exercismDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +60,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
.filter((dir) => !dir.startsWith(".") && !["node_modules", ".git"].includes(dir))
|
||||
|
||||
for (const language of languages) {
|
||||
const languageDir = path.join(exercisesDir, language)
|
||||
const languageDir = path.join(exercisesDir, language, "exercises", "practice")
|
||||
|
||||
// Read exercise directories
|
||||
const exercises = fs.readdirSync(languageDir).filter((dir) => fs.statSync(path.join(languageDir, dir)).isDirectory())
|
||||
@@ -61,7 +70,7 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
|
||||
// Read instructions
|
||||
let description = ""
|
||||
const instructionsPath = path.join(exerciseDir, "docs", "instructions.md")
|
||||
const instructionsPath = path.join(exerciseDir, ".docs", "instructions.md")
|
||||
if (fs.existsSync(instructionsPath)) {
|
||||
description = fs.readFileSync(instructionsPath, "utf-8")
|
||||
}
|
||||
@@ -69,20 +78,23 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
// Determine test commands based on language
|
||||
let testCommands: string[] = []
|
||||
switch (language) {
|
||||
case "cpp":
|
||||
testCommands = ["cmake -DEXERCISM_RUN_ALL_TESTS=1 .", "make"]
|
||||
break
|
||||
case "javascript":
|
||||
testCommands = ["npm install", "npm test"]
|
||||
testCommands = ["npm install", "npm test -- --testNamePattern=."]
|
||||
break
|
||||
case "python":
|
||||
testCommands = ["python -m pytest -o markers=task *_test.py"]
|
||||
testCommands = ["python3 -m pytest -o markers=task *_test.py"]
|
||||
break
|
||||
case "go":
|
||||
testCommands = ["go test"]
|
||||
testCommands = ["GOWORK=off go test -v"]
|
||||
break
|
||||
case "java":
|
||||
testCommands = ["./gradlew test"]
|
||||
break
|
||||
case "rust":
|
||||
testCommands = ["cargo test"]
|
||||
testCommands = ["cargo test -- --include-ignored"]
|
||||
break
|
||||
default:
|
||||
testCommands = []
|
||||
@@ -118,53 +130,117 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Check if Git repository is already initialized
|
||||
const gitDirExists = fs.existsSync(path.join(task.workspacePath, ".git"))
|
||||
// Create temp directory outside workspace for hiding files
|
||||
const tempDir = path.join(EVALS_DIR, "temp-files", task.id)
|
||||
fs.mkdirSync(tempDir, { recursive: true })
|
||||
|
||||
try {
|
||||
// Initialize Git repository if needed
|
||||
if (!gitDirExists) {
|
||||
await execa("git", ["init"], { cwd: task.workspacePath })
|
||||
}
|
||||
// Read config.json to get solution and test files
|
||||
const configPath = path.join(task.workspacePath, ".meta", "config.json")
|
||||
let config: any = { files: { solution: [], test: [] } }
|
||||
|
||||
// Create a dummy file to ensure there's something to commit
|
||||
const dummyFilePath = path.join(task.workspacePath, ".eval-timestamp")
|
||||
fs.writeFileSync(dummyFilePath, new Date().toISOString())
|
||||
|
||||
// Add all files and commit
|
||||
await execa("git", ["add", "."], { cwd: task.workspacePath })
|
||||
|
||||
try {
|
||||
await execa("git", ["commit", "-m", "Initial commit"], { cwd: task.workspacePath })
|
||||
} catch (error: any) {
|
||||
// If commit fails because there are no changes, that's okay
|
||||
if (!error.stderr?.includes("nothing to commit")) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(`Warning: Git operations failed: ${error.message}`)
|
||||
console.warn("Continuing without Git initialization")
|
||||
if (fs.existsSync(configPath)) {
|
||||
config = JSON.parse(fs.readFileSync(configPath, "utf-8"))
|
||||
}
|
||||
|
||||
return task
|
||||
// Build enhanced description with instructions
|
||||
let description = ""
|
||||
const instructionsPath = path.join(task.workspacePath, ".docs", "instructions.md")
|
||||
const appendPath = path.join(task.workspacePath, ".docs", "instructions.append.md")
|
||||
|
||||
if (fs.existsSync(instructionsPath)) {
|
||||
description = fs.readFileSync(instructionsPath, "utf-8")
|
||||
}
|
||||
|
||||
if (fs.existsSync(appendPath)) {
|
||||
description += "\n\n" + fs.readFileSync(appendPath, "utf-8")
|
||||
}
|
||||
|
||||
// Add solution files constraint to description
|
||||
const solutionFiles = config.files.solution || []
|
||||
const fileList = solutionFiles.join(", ")
|
||||
description += `\n\nUse the above instructions to modify the supplied files: ${fileList}. Don't change the names of existing functions or classes, as they may be referenced from other code like unit tests, etc. Only use standard libraries, don't suggest installing any packages.`
|
||||
description +=
|
||||
" You should ignore all test or test related files in this directory. The final test file has been removed and will be used to evaluate your work after your implementation is complete. Think deeply about the problem prior to working on the implementation. Consider all edge cases and test your solution prior to finalizing."
|
||||
|
||||
// Move test files to temp directory
|
||||
if (config.files.test) {
|
||||
config.files.test.forEach((testFile: string) => {
|
||||
const src = path.join(task.workspacePath, testFile)
|
||||
if (fs.existsSync(src)) {
|
||||
const dest = path.join(tempDir, testFile)
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Move all dot directories (except .git) to temp directory
|
||||
const items = fs.readdirSync(task.workspacePath)
|
||||
items.forEach((item) => {
|
||||
if (item.startsWith(".") && item !== ".git") {
|
||||
const src = path.join(task.workspacePath, item)
|
||||
const stat = fs.statSync(src)
|
||||
if (stat.isDirectory()) {
|
||||
const dest = path.join(tempDir, item)
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...task,
|
||||
description,
|
||||
metadata: {
|
||||
...task.metadata,
|
||||
solutionFiles,
|
||||
tempDir,
|
||||
config,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution
|
||||
* Cleanup after task execution (restores hidden files from temp directory)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
async cleanupTask(task: Task): Promise<void> {
|
||||
const tempDir = path.join(EVALS_DIR, "temp-files", task.id)
|
||||
|
||||
if (fs.existsSync(tempDir)) {
|
||||
const items = fs.readdirSync(tempDir)
|
||||
items.forEach((item) => {
|
||||
const src = path.join(tempDir, item)
|
||||
const dest = path.join(task.workspacePath, item)
|
||||
// Only move if destination doesn't exist (keeps newer test artifacts like .pytest_cache)
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up temp directory
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution by running tests
|
||||
* @param task The task that was executed
|
||||
*/
|
||||
async verifyResult(task: Task): Promise<VerificationResult> {
|
||||
// Run verification commands
|
||||
let success = true
|
||||
let output = ""
|
||||
|
||||
for (const command of task.verificationCommands) {
|
||||
try {
|
||||
const [cmd, ...args] = command.split(" ")
|
||||
const { stdout } = await execa(cmd, args, { cwd: task.workspacePath })
|
||||
const { stdout, stderr } = await execa(command, {
|
||||
cwd: task.workspacePath,
|
||||
shell: true,
|
||||
})
|
||||
output += stdout + "\n"
|
||||
if (stderr) {
|
||||
output += stderr + "\n"
|
||||
}
|
||||
} catch (error: any) {
|
||||
success = false
|
||||
if (error.stdout) {
|
||||
@@ -176,13 +252,92 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse test results
|
||||
const testsPassed = (output.match(/PASS/g) || []).length
|
||||
const testsFailed = (output.match(/FAIL/g) || []).length
|
||||
// Log the raw output
|
||||
// console.log("\n=== TEST OUTPUT START ===")
|
||||
// console.log(output)
|
||||
// console.log("=== TEST OUTPUT END ===\n")
|
||||
|
||||
// Parse test results based on language
|
||||
const language = task.metadata.language
|
||||
let testsPassed = 0
|
||||
let testsFailed = 0
|
||||
|
||||
switch (language) {
|
||||
case "python":
|
||||
const pyPassMatch = output.match(/(\d+) passed/)
|
||||
const pyFailMatch = output.match(/(\d+) failed/)
|
||||
testsPassed = pyPassMatch ? parseInt(pyPassMatch[1]) : 0
|
||||
testsFailed = pyFailMatch ? parseInt(pyFailMatch[1]) : 0
|
||||
break
|
||||
|
||||
case "javascript":
|
||||
const jestMatch = output.match(/Tests:\s+(?:\d+ skipped,\s+)?(\d+) passed(?:,\s+(\d+) failed)?/)
|
||||
if (jestMatch) {
|
||||
testsPassed = parseInt(jestMatch[1])
|
||||
testsFailed = jestMatch[2] ? parseInt(jestMatch[2]) : 0
|
||||
} else {
|
||||
// Fallback to counting test suites
|
||||
testsPassed = (output.match(/PASS/g) || []).length
|
||||
testsFailed = (output.match(/FAIL/g) || []).length
|
||||
}
|
||||
break
|
||||
|
||||
case "go":
|
||||
// This incorrectly counts the parent, but minor and doesn't affect final boolean metric
|
||||
testsPassed = (output.match(/--- PASS:/g) || []).length
|
||||
testsFailed = (output.match(/--- FAIL:/g) || []).length
|
||||
break
|
||||
|
||||
case "rust":
|
||||
// Rust runs multiple test suites (unit, integration, doc tests)
|
||||
// Sum results across all test result lines
|
||||
const resultLines = output.match(/test result:.*?(\d+) passed; (\d+) failed/g)
|
||||
if (resultLines) {
|
||||
testsPassed = 0
|
||||
testsFailed = 0
|
||||
for (const line of resultLines) {
|
||||
const match = line.match(/(\d+) passed; (\d+) failed/)
|
||||
if (match) {
|
||||
testsPassed += parseInt(match[1])
|
||||
testsFailed += parseInt(match[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case "java":
|
||||
testsPassed = (output.match(/PASSED/g) || []).length
|
||||
testsFailed = (output.match(/FAILED/g) || []).length
|
||||
break
|
||||
|
||||
case "cpp":
|
||||
const cppAllPassedMatch = output.match(/All tests passed \(.*?(\d+) test cases?\)/)
|
||||
const cppTestCasesMatch = output.match(/test cases?: (\d+) \| (\d+) passed/)
|
||||
const cppFailedMatch = output.match(/(\d+) failed/)
|
||||
|
||||
if (cppAllPassedMatch) {
|
||||
// All tests passed - extract total test cases
|
||||
testsPassed = parseInt(cppAllPassedMatch[1])
|
||||
testsFailed = 0
|
||||
} else if (cppTestCasesMatch) {
|
||||
// Mixed results - extract passed count and calculate failed
|
||||
const totalTests = parseInt(cppTestCasesMatch[1])
|
||||
testsPassed = parseInt(cppTestCasesMatch[2])
|
||||
testsFailed = cppFailedMatch ? parseInt(cppFailedMatch[1]) : totalTests - testsPassed
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
// Fallback to generic PASS/FAIL counting
|
||||
testsPassed = (output.match(/PASS/g) || []).length
|
||||
testsFailed = (output.match(/FAIL/g) || []).length
|
||||
}
|
||||
|
||||
const testsTotal = testsPassed + testsFailed
|
||||
|
||||
return {
|
||||
success,
|
||||
rawOutput: output,
|
||||
metrics: {
|
||||
testsPassed,
|
||||
testsFailed,
|
||||
@@ -191,4 +346,280 @@ export class ExercismAdapter implements BenchmarkAdapter {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide test files by moving them to temp directory
|
||||
* @param task The task to hide test files for
|
||||
*/
|
||||
private hideTestFiles(task: Task): void {
|
||||
const tempDir = task.metadata.tempDir
|
||||
const config = task.metadata.config
|
||||
|
||||
if (config?.files?.test) {
|
||||
config.files.test.forEach((testFile: string) => {
|
||||
const src = path.join(task.workspacePath, testFile)
|
||||
if (fs.existsSync(src)) {
|
||||
const dest = path.join(tempDir, testFile)
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Hide dot directories again (except .git)
|
||||
const items = fs.readdirSync(task.workspacePath)
|
||||
items.forEach((item) => {
|
||||
if (item.startsWith(".") && item !== ".git") {
|
||||
const src = path.join(task.workspacePath, item)
|
||||
if (fs.existsSync(src)) {
|
||||
const stat = fs.statSync(src)
|
||||
if (stat.isDirectory()) {
|
||||
const dest = path.join(tempDir, item)
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore test files by moving them from temp directory
|
||||
* @param task The task to restore test files for
|
||||
*/
|
||||
private restoreTestFiles(task: Task): void {
|
||||
const tempDir = task.metadata.tempDir
|
||||
const config = task.metadata.config
|
||||
|
||||
if (config?.files?.test) {
|
||||
config.files.test.forEach((testFile: string) => {
|
||||
const src = path.join(tempDir, testFile)
|
||||
if (fs.existsSync(src)) {
|
||||
const dest = path.join(task.workspacePath, testFile)
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Restore dot directories (except .git)
|
||||
if (fs.existsSync(tempDir)) {
|
||||
const items = fs.readdirSync(tempDir)
|
||||
items.forEach((item) => {
|
||||
if (item.startsWith(".") && item !== ".git") {
|
||||
const src = path.join(tempDir, item)
|
||||
const dest = path.join(task.workspacePath, item)
|
||||
if (fs.existsSync(src) && !fs.existsSync(dest)) {
|
||||
fs.renameSync(src, dest)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds retry message with test errors and fix instructions
|
||||
* @param testOutput The raw test output showing errors
|
||||
* @param solutionFiles List of solution files to fix
|
||||
* @returns Formatted retry message
|
||||
*/
|
||||
private buildRetryMessage(testOutput: string, solutionFiles: string[]): string {
|
||||
const fileList = solutionFiles.join(", ")
|
||||
return `${testOutput}\n\nSee the testing errors above. The tests are correct, don't try and change them. Fix the code in ${fileList} to resolve the errors.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Unskip all JavaScript tests in the repository by replacing xtest with test
|
||||
* @param repoPath Path to the exercism repository
|
||||
*/
|
||||
private unskipAllJavaScriptTests(repoPath: string): void {
|
||||
const jsDir = path.join(repoPath, "javascript", "exercises", "practice")
|
||||
|
||||
if (!fs.existsSync(jsDir)) {
|
||||
console.log("JavaScript exercises directory not found, skipping test unskipping")
|
||||
return
|
||||
}
|
||||
|
||||
// Walk through all exercise directories
|
||||
const exercises = fs.readdirSync(jsDir).filter((dir) => {
|
||||
const fullPath = path.join(jsDir, dir)
|
||||
return fs.statSync(fullPath).isDirectory()
|
||||
})
|
||||
|
||||
let filesModified = 0
|
||||
for (const exercise of exercises) {
|
||||
const exerciseDir = path.join(jsDir, exercise)
|
||||
|
||||
// Find all .spec.js files
|
||||
const files = fs.readdirSync(exerciseDir).filter((file) => file.endsWith(".spec.js"))
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(exerciseDir, file)
|
||||
let content = fs.readFileSync(filePath, "utf-8")
|
||||
const originalContent = content
|
||||
|
||||
// Replace xtest with test to unskip tests
|
||||
content = content.replace(/xtest\(/g, "test(")
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(filePath, content)
|
||||
filesModified++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Unskipped tests in ${filesModified} JavaScript test files`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unskip all Java tests in the repository by removing @Disabled annotations
|
||||
* @param repoPath Path to the exercism repository
|
||||
*/
|
||||
private unskipAllJavaTests(repoPath: string): void {
|
||||
const javaDir = path.join(repoPath, "java", "exercises", "practice")
|
||||
|
||||
if (!fs.existsSync(javaDir)) {
|
||||
console.log("Java exercises directory not found, skipping test unskipping")
|
||||
return
|
||||
}
|
||||
|
||||
// Walk through all exercise directories
|
||||
const exercises = fs.readdirSync(javaDir).filter((dir) => {
|
||||
const fullPath = path.join(javaDir, dir)
|
||||
return fs.statSync(fullPath).isDirectory()
|
||||
})
|
||||
|
||||
let filesModified = 0
|
||||
for (const exercise of exercises) {
|
||||
const testDir = path.join(javaDir, exercise, "src", "test", "java")
|
||||
|
||||
if (!fs.existsSync(testDir)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find all .java test files
|
||||
const files = fs.readdirSync(testDir).filter((file) => file.endsWith(".java"))
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(testDir, file)
|
||||
let content = fs.readFileSync(filePath, "utf-8")
|
||||
const originalContent = content
|
||||
|
||||
// Remove @Disabled("Remove to run test") annotations
|
||||
content = content.replace(/@Disabled\("Remove to run test"\)\s*\n/g, "")
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(filePath, content)
|
||||
filesModified++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Unskipped tests in ${filesModified} Java test files`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a Cline task with automatic retry on test failure
|
||||
* Creates a new Cline instance, runs the task, verifies with tests,
|
||||
* and retries once if tests fail
|
||||
* @param task The task to execute
|
||||
* @returns The final verification result, or null
|
||||
*/
|
||||
async runTask(task: Task): Promise<VerificationResult | null> {
|
||||
const startTime = Date.now()
|
||||
let instanceAddress: string | null = null
|
||||
let attempts = 0
|
||||
let finalVerification: VerificationResult | null = null
|
||||
|
||||
try {
|
||||
// Step 1: Start a new Cline instance in the working directory
|
||||
const instanceResult = await execa("cline", ["instance", "new"], {
|
||||
cwd: task.workspacePath,
|
||||
stdin: "ignore",
|
||||
})
|
||||
|
||||
// Step 2: Parse the instance address from output
|
||||
const addressMatch = instanceResult.stdout.match(/Address:\s*([\d.]+:\d+)/)
|
||||
if (!addressMatch) {
|
||||
throw new Error("Failed to parse instance address from output")
|
||||
}
|
||||
instanceAddress = addressMatch[1]
|
||||
|
||||
// Step 3: Create the initial task on this specific instance
|
||||
await execa("cline", ["task", "new", "--yolo", "--address", instanceAddress, task.description], {
|
||||
cwd: task.workspacePath,
|
||||
stdin: "ignore",
|
||||
})
|
||||
|
||||
// Step 4: Wait for initial implementation to complete
|
||||
console.log(chalk.blue(`Waiting for first attempt to complete...`))
|
||||
await execa("cline", ["task", "view", "--follow-complete", "--address", instanceAddress], {
|
||||
cwd: task.workspacePath,
|
||||
stdin: "ignore",
|
||||
})
|
||||
|
||||
// Step 5: Run first test attempt
|
||||
console.log(chalk.blue(`Running tests (attempt 1)...`))
|
||||
this.restoreTestFiles(task)
|
||||
attempts = 1
|
||||
const firstVerification = await this.verifyResult(task)
|
||||
finalVerification = firstVerification
|
||||
|
||||
// Step 6: Retry if tests failed
|
||||
if (!firstVerification.success) {
|
||||
console.log(chalk.blue(`Tests failed on first attempt. Retrying...`))
|
||||
|
||||
// Hide test files again for retry
|
||||
this.hideTestFiles(task)
|
||||
|
||||
attempts = 2
|
||||
const solutionFiles = task.metadata.solutionFiles || []
|
||||
const retryMessage = this.buildRetryMessage(firstVerification.rawOutput || "", solutionFiles)
|
||||
|
||||
// Send retry task message
|
||||
await execa("cline", ["task", "send", "--yolo", "--address", instanceAddress], {
|
||||
cwd: task.workspacePath,
|
||||
input: retryMessage,
|
||||
})
|
||||
|
||||
// Follow retry until complete
|
||||
await execa("cline", ["task", "view", "--follow-complete", "--address", instanceAddress], {
|
||||
cwd: task.workspacePath,
|
||||
stdin: "ignore",
|
||||
})
|
||||
|
||||
// Run second test attempt (final)
|
||||
console.log(chalk.blue(`Running tests (attempt 2)...`))
|
||||
this.restoreTestFiles(task)
|
||||
const secondVerification = await this.verifyResult(task)
|
||||
finalVerification = secondVerification
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Task completed in ${(duration / 1000).toFixed(1)}s after ${attempts} attempt${attempts > 1 ? "s" : ""}`,
|
||||
),
|
||||
)
|
||||
|
||||
return finalVerification
|
||||
} catch (error: any) {
|
||||
const duration = Date.now() - startTime
|
||||
console.error(chalk.red(`Task failed after ${(duration / 1000).toFixed(1)}s: ${error.message}`))
|
||||
|
||||
return finalVerification
|
||||
} finally {
|
||||
// Step 7: Always clean up the instance, even if task failed
|
||||
if (instanceAddress) {
|
||||
try {
|
||||
await execa("cline", ["instance", "kill", instanceAddress], {
|
||||
stdin: "ignore",
|
||||
})
|
||||
} catch (cleanupError: any) {
|
||||
console.error(chalk.yellow(`Warning: Failed to kill instance ${instanceAddress}: ${cleanupError.message}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import { BenchmarkAdapter } from "./types"
|
||||
import { ExercismAdapter } from "./exercism"
|
||||
import { SWEBenchAdapter } from "./swe-bench"
|
||||
import { SWELancerAdapter } from "./swelancer"
|
||||
import { MultiSWEAdapter } from "./multi-swe"
|
||||
import { BenchmarkAdapter } from "./types"
|
||||
|
||||
// Registry of all available adapters
|
||||
const adapters: Record<string, BenchmarkAdapter> = {
|
||||
// Exercism is the primary adapter with real implementation
|
||||
exercism: new ExercismAdapter(),
|
||||
|
||||
// Dummy adapters for testing
|
||||
"swe-bench": new SWEBenchAdapter(),
|
||||
swelancer: new SWELancerAdapter(),
|
||||
"multi-swe": new MultiSWEAdapter(),
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Dummy adapter for the Multi-SWE-Bench benchmark
|
||||
*/
|
||||
export class MultiSWEAdapter implements BenchmarkAdapter {
|
||||
name = "multi-swe"
|
||||
|
||||
/**
|
||||
* Set up the Multi-SWE-Bench benchmark repository (dummy implementation)
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
console.log("Multi-SWE-Bench dummy setup completed")
|
||||
|
||||
// Create repositories directory if it doesn't exist
|
||||
const repoDir = path.join(EVALS_DIR, "repositories", "multi-swe")
|
||||
if (!fs.existsSync(repoDir)) {
|
||||
fs.mkdirSync(repoDir, { recursive: true })
|
||||
console.log(`Created dummy Multi-SWE-Bench directory at ${repoDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the Multi-SWE-Bench benchmark (dummy implementation)
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
return [
|
||||
{
|
||||
id: "multi-swe-task-1",
|
||||
name: "Multi-Language API Integration",
|
||||
description:
|
||||
"Implement a system that integrates a Python backend with a TypeScript frontend and a Rust processing service.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
languages: ["python", "typescript", "rust"],
|
||||
complexity: "high",
|
||||
type: "multi-swe",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "multi-swe-task-2",
|
||||
name: "Cross-Platform Mobile App",
|
||||
description: "Create a cross-platform mobile app using React Native with native modules in Swift and Kotlin.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
languages: ["javascript", "swift", "kotlin"],
|
||||
complexity: "medium",
|
||||
type: "multi-swe",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "multi-swe-task-3",
|
||||
name: "Microservice Architecture",
|
||||
description: "Design and implement a microservice architecture with services written in Go, Node.js, and Java.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
languages: ["go", "javascript", "java"],
|
||||
complexity: "high",
|
||||
type: "multi-swe",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution (dummy implementation)
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Create a dummy workspace for the task
|
||||
const taskDir = path.join(task.workspacePath, taskId)
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
|
||||
// Create a dummy file for the task
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "README.md"),
|
||||
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
|
||||
)
|
||||
|
||||
// Create additional dummy files based on task type
|
||||
if (task.id === "multi-swe-task-1") {
|
||||
// Python backend
|
||||
fs.mkdirSync(path.join(taskDir, "backend"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "backend", "app.py"),
|
||||
`# TODO: Implement Python backend\nfrom flask import Flask\n\napp = Flask(__name__)\n\n@app.route('/')\ndef hello():\n return "Hello, World!"\n`,
|
||||
)
|
||||
|
||||
// TypeScript frontend
|
||||
fs.mkdirSync(path.join(taskDir, "frontend"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "frontend", "app.ts"),
|
||||
`// TODO: Implement TypeScript frontend\nconsole.log('Frontend starting...');\n`,
|
||||
)
|
||||
|
||||
// Rust processing service
|
||||
fs.mkdirSync(path.join(taskDir, "processor"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "processor", "main.rs"),
|
||||
`// TODO: Implement Rust processing service\nfn main() {\n println!("Processor starting...");\n}\n`,
|
||||
)
|
||||
} else if (task.id === "multi-swe-task-2") {
|
||||
// React Native app
|
||||
fs.mkdirSync(path.join(taskDir, "app"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "app", "App.js"),
|
||||
`// TODO: Implement React Native app\nimport React from 'react';\nimport { View, Text } from 'react-native';\n\nexport default function App() {\n return (\n <View>\n <Text>Hello, World!</Text>\n </View>\n );\n}\n`,
|
||||
)
|
||||
|
||||
// Swift native module
|
||||
fs.mkdirSync(path.join(taskDir, "ios"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "ios", "NativeModule.swift"),
|
||||
`// TODO: Implement Swift native module\nimport Foundation\n\n@objc(NativeModule)\nclass NativeModule: NSObject {\n @objc\n func hello() -> String {\n return "Hello from Swift"\n }\n}\n`,
|
||||
)
|
||||
|
||||
// Kotlin native module
|
||||
fs.mkdirSync(path.join(taskDir, "android"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "android", "NativeModule.kt"),
|
||||
`// TODO: Implement Kotlin native module\npackage com.example.app\n\nclass NativeModule {\n fun hello(): String {\n return "Hello from Kotlin"\n }\n}\n`,
|
||||
)
|
||||
} else if (task.id === "multi-swe-task-3") {
|
||||
// Go service
|
||||
fs.mkdirSync(path.join(taskDir, "service-go"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "service-go", "main.go"),
|
||||
`// TODO: Implement Go service\npackage main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Go service starting...")\n}\n`,
|
||||
)
|
||||
|
||||
// Node.js service
|
||||
fs.mkdirSync(path.join(taskDir, "service-node"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "service-node", "server.js"),
|
||||
`// TODO: Implement Node.js service\nconsole.log('Node.js service starting...');\n`,
|
||||
)
|
||||
|
||||
// Java service
|
||||
fs.mkdirSync(path.join(taskDir, "service-java"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "service-java", "Main.java"),
|
||||
`// TODO: Implement Java service\npublic class Main {\n public static void main(String[] args) {\n System.out.println("Java service starting...");\n }\n}\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the task's workspace path to the task-specific directory
|
||||
return {
|
||||
...task,
|
||||
workspacePath: taskDir,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution (dummy implementation)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Always return success for dummy implementation
|
||||
return {
|
||||
success: true,
|
||||
metrics: {
|
||||
testsPassed: 1,
|
||||
testsFailed: 0,
|
||||
testsTotal: 1,
|
||||
functionalCorrectness: 1.0,
|
||||
crossLanguageIntegration: 0.9, // Dummy metric specific to Multi-SWE
|
||||
architectureQuality: 0.85, // Dummy metric specific to Multi-SWE
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Dummy adapter for the SWE-Bench benchmark
|
||||
*/
|
||||
export class SWEBenchAdapter implements BenchmarkAdapter {
|
||||
name = "swe-bench"
|
||||
|
||||
/**
|
||||
* Set up the SWE-Bench benchmark repository (dummy implementation)
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
console.log("SWE-Bench dummy setup completed")
|
||||
|
||||
// Create repositories directory if it doesn't exist
|
||||
const repoDir = path.join(EVALS_DIR, "repositories", "swe-bench")
|
||||
if (!fs.existsSync(repoDir)) {
|
||||
fs.mkdirSync(repoDir, { recursive: true })
|
||||
console.log(`Created dummy SWE-Bench directory at ${repoDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the SWE-Bench benchmark (dummy implementation)
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
return [
|
||||
{
|
||||
id: "swe-bench-task-1",
|
||||
name: "Fix React Component Bug",
|
||||
description: "Fix a bug in a React component where the state is not properly updated.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
repository: "facebook/react",
|
||||
issue: "#12345",
|
||||
type: "swe-bench",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swe-bench-task-2",
|
||||
name: "Optimize Database Query",
|
||||
description: "Optimize a slow database query in a Django application.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
repository: "django/django",
|
||||
issue: "#6789",
|
||||
type: "swe-bench",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swe-bench-task-3",
|
||||
name: "Fix Memory Leak",
|
||||
description: "Fix a memory leak in a Node.js application.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
repository: "nodejs/node",
|
||||
issue: "#9876",
|
||||
type: "swe-bench",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution (dummy implementation)
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Create a dummy workspace for the task
|
||||
const taskDir = path.join(task.workspacePath, taskId)
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
|
||||
// Create a dummy file for the task
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "README.md"),
|
||||
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Update the task's workspace path to the task-specific directory
|
||||
return {
|
||||
...task,
|
||||
workspacePath: taskDir,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution (dummy implementation)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Always return success for dummy implementation
|
||||
return {
|
||||
success: true,
|
||||
metrics: {
|
||||
testsPassed: 1,
|
||||
testsFailed: 0,
|
||||
testsTotal: 1,
|
||||
functionalCorrectness: 1.0,
|
||||
performanceImprovement: 0.25, // Dummy metric specific to SWE-Bench
|
||||
codeQuality: 0.9, // Dummy metric specific to SWE-Bench
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Dummy adapter for the SWELancer benchmark
|
||||
*/
|
||||
export class SWELancerAdapter implements BenchmarkAdapter {
|
||||
name = "swelancer"
|
||||
|
||||
/**
|
||||
* Set up the SWELancer benchmark repository (dummy implementation)
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
console.log("SWELancer dummy setup completed")
|
||||
|
||||
// Create repositories directory if it doesn't exist
|
||||
const repoDir = path.join(EVALS_DIR, "repositories", "swelancer")
|
||||
if (!fs.existsSync(repoDir)) {
|
||||
fs.mkdirSync(repoDir, { recursive: true })
|
||||
console.log(`Created dummy SWELancer directory at ${repoDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the SWELancer benchmark (dummy implementation)
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
return [
|
||||
{
|
||||
id: "swelancer-task-1",
|
||||
name: "Create Landing Page",
|
||||
description: "Create a responsive landing page for a new product using HTML, CSS, and JavaScript.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
client: "TechStartup Inc.",
|
||||
difficulty: "medium",
|
||||
type: "swelancer",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swelancer-task-2",
|
||||
name: "Build REST API",
|
||||
description: "Create a RESTful API for a blog application using Node.js and Express.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
client: "BlogCo",
|
||||
difficulty: "hard",
|
||||
type: "swelancer",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swelancer-task-3",
|
||||
name: "Fix CSS Layout Issues",
|
||||
description: "Fix layout issues in a responsive website across different screen sizes.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
client: "DesignAgency",
|
||||
difficulty: "easy",
|
||||
type: "swelancer",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution (dummy implementation)
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Create a dummy workspace for the task
|
||||
const taskDir = path.join(task.workspacePath, taskId)
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
|
||||
// Create a dummy file for the task
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "README.md"),
|
||||
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
|
||||
)
|
||||
|
||||
// Create additional dummy files based on task type
|
||||
if (task.id === "swelancer-task-1") {
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "index.html"),
|
||||
`<!DOCTYPE html>\n<html>\n<head>\n <title>Landing Page</title>\n</head>\n<body>\n <!-- TODO: Implement landing page -->\n</body>\n</html>`,
|
||||
)
|
||||
} else if (task.id === "swelancer-task-2") {
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "server.js"),
|
||||
`// TODO: Implement REST API\nconsole.log('Server starting...');`,
|
||||
)
|
||||
} else if (task.id === "swelancer-task-3") {
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "styles.css"),
|
||||
`/* TODO: Fix layout issues */\nbody {\n margin: 0;\n padding: 0;\n}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the task's workspace path to the task-specific directory
|
||||
return {
|
||||
...task,
|
||||
workspacePath: taskDir,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution (dummy implementation)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Always return success for dummy implementation
|
||||
return {
|
||||
success: true,
|
||||
metrics: {
|
||||
testsPassed: 1,
|
||||
testsFailed: 0,
|
||||
testsTotal: 1,
|
||||
functionalCorrectness: 1.0,
|
||||
clientSatisfaction: 0.95, // Dummy metric specific to SWELancer
|
||||
timeEfficiency: 0.85, // Dummy metric specific to SWELancer
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export interface Task {
|
||||
export interface VerificationResult {
|
||||
success: boolean
|
||||
metrics: Record<string, any>
|
||||
rawOutput?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,5 +28,7 @@ export interface BenchmarkAdapter {
|
||||
setup(): Promise<void>
|
||||
listTasks(): Promise<Task[]>
|
||||
prepareTask(taskId: string): Promise<Task>
|
||||
verifyResult(task: Task, result: any): Promise<VerificationResult>
|
||||
cleanupTask(task: Task): Promise<void>
|
||||
verifyResult(task: Task): Promise<VerificationResult>
|
||||
runTask(task: Task): Promise<VerificationResult | null>
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import * as path from "path"
|
||||
import chalk from "chalk"
|
||||
import { createEvalsEnvFile, removeEvalsEnvFile, checkEvalsEnvFile } from "../utils/evals-env"
|
||||
|
||||
interface EvalsEnvOptions {
|
||||
action: "create" | "remove" | "check"
|
||||
directory?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the evals-env command
|
||||
* @param options Command options
|
||||
*/
|
||||
export async function evalsEnvHandler(options: EvalsEnvOptions): Promise<void> {
|
||||
// Determine the directory to use - default to repository root instead of current directory
|
||||
const currentDir = process.cwd()
|
||||
const repoRoot = path.resolve(currentDir, "..", "..") // Navigate up from evals/cli to root
|
||||
const directory = options.directory || repoRoot
|
||||
|
||||
console.log(chalk.blue(`Working with directory: ${directory}`))
|
||||
|
||||
// Perform the requested action
|
||||
switch (options.action) {
|
||||
case "create":
|
||||
console.log(chalk.blue("Creating evals.env file..."))
|
||||
createEvalsEnvFile(directory)
|
||||
console.log(chalk.green("The Cline extension should now detect this file and enter test mode."))
|
||||
console.log(chalk.yellow("Note: You may need to reload VSCode for the changes to take effect."))
|
||||
break
|
||||
|
||||
case "remove":
|
||||
console.log(chalk.blue("Removing evals.env file..."))
|
||||
removeEvalsEnvFile(directory)
|
||||
console.log(chalk.green("The Cline extension should now exit test mode."))
|
||||
console.log(chalk.yellow("Note: You may need to reload VSCode for the changes to take effect."))
|
||||
break
|
||||
|
||||
case "check":
|
||||
console.log(chalk.blue("Checking for evals.env file..."))
|
||||
const exists = checkEvalsEnvFile(directory)
|
||||
if (exists) {
|
||||
console.log(chalk.green("The Cline extension should be in test mode."))
|
||||
} else {
|
||||
console.log(chalk.yellow("The Cline extension should not be in test mode."))
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
console.error(chalk.red(`Unknown action: ${options.action}`))
|
||||
console.log(chalk.yellow("Valid actions are: create, remove, check"))
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import chalk from "chalk"
|
||||
import * as fs from "fs"
|
||||
import ora from "ora"
|
||||
import * as path from "path"
|
||||
import { ResultsDatabase } from "../db"
|
||||
import { generateMarkdownReport } from "../utils/markdown"
|
||||
|
||||
@@ -34,7 +34,6 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
// Generate summary report
|
||||
const summary = {
|
||||
runs: runs.length,
|
||||
models: [...new Set(runs.map((run) => run.model))],
|
||||
benchmarks: [...new Set(runs.map((run) => run.benchmark))],
|
||||
tasks: 0,
|
||||
successRate: 0,
|
||||
@@ -45,6 +44,10 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
totalToolFailures: 0,
|
||||
toolSuccessRate: 0,
|
||||
toolUsage: {} as Record<string, { calls: number; failures: number }>,
|
||||
totalTests: 0,
|
||||
totalTestsPassed: 0,
|
||||
totalTestsFailed: 0,
|
||||
testSuccessRate: 0,
|
||||
}
|
||||
|
||||
let totalTasks = 0
|
||||
@@ -54,6 +57,9 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
let totalDuration = 0
|
||||
let totalToolCalls = 0
|
||||
let totalToolFailures = 0
|
||||
let totalTests = 0
|
||||
let totalTestsPassed = 0
|
||||
let totalTestsFailed = 0
|
||||
|
||||
for (const run of runs) {
|
||||
const tasks = db.getRunTasks(run.id)
|
||||
@@ -73,6 +79,14 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
totalCost += metrics.find((m) => m.name === "cost")?.value || 0
|
||||
totalDuration += metrics.find((m) => m.name === "duration")?.value || 0
|
||||
|
||||
// Collect test metrics
|
||||
const testsPassed = metrics.find((m) => m.name === "testsPassed")?.value || 0
|
||||
const testsFailed = metrics.find((m) => m.name === "testsFailed")?.value || 0
|
||||
const testsTotal = metrics.find((m) => m.name === "testsTotal")?.value || 0
|
||||
totalTestsPassed += testsPassed
|
||||
totalTestsFailed += testsFailed
|
||||
totalTests += testsTotal
|
||||
|
||||
// Collect tool call metrics
|
||||
totalToolCalls += task.total_tool_calls || 0
|
||||
totalToolFailures += task.total_tool_failures || 0
|
||||
@@ -99,6 +113,12 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
summary.totalToolFailures = totalToolFailures
|
||||
summary.toolSuccessRate = totalToolCalls > 0 ? 1 - totalToolFailures / totalToolCalls : 1.0
|
||||
|
||||
// Calculate test metrics
|
||||
summary.totalTests = totalTests
|
||||
summary.totalTestsPassed = totalTestsPassed
|
||||
summary.totalTestsFailed = totalTestsFailed
|
||||
summary.testSuccessRate = totalTests > 0 ? totalTestsPassed / totalTests : 0
|
||||
|
||||
summary.tasks = totalTasks
|
||||
summary.successRate = totalTasks > 0 ? successfulTasks / totalTasks : 0
|
||||
summary.averageTokens = totalTasks > 0 ? totalTokens / totalTasks : 0
|
||||
@@ -112,12 +132,15 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
const benchmarkRuns = runs.filter((run) => run.benchmark === benchmark)
|
||||
const benchmarkSummary = {
|
||||
runs: benchmarkRuns.length,
|
||||
models: [...new Set(benchmarkRuns.map((run) => run.model))],
|
||||
tasks: 0,
|
||||
successRate: 0,
|
||||
averageTokens: 0,
|
||||
averageCost: 0,
|
||||
averageDuration: 0,
|
||||
totalTests: 0,
|
||||
totalTestsPassed: 0,
|
||||
totalTestsFailed: 0,
|
||||
testSuccessRate: 0,
|
||||
}
|
||||
|
||||
let benchmarkTasks = 0
|
||||
@@ -125,6 +148,9 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
let benchmarkTotalTokens = 0
|
||||
let benchmarkTotalCost = 0
|
||||
let benchmarkTotalDuration = 0
|
||||
let benchmarkTotalTests = 0
|
||||
let benchmarkTotalTestsPassed = 0
|
||||
let benchmarkTotalTestsFailed = 0
|
||||
|
||||
for (const run of benchmarkRuns) {
|
||||
const tasks = db.getRunTasks(run.id)
|
||||
@@ -143,6 +169,14 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
|
||||
benchmarkTotalCost += metrics.find((m) => m.name === "cost")?.value || 0
|
||||
benchmarkTotalDuration += metrics.find((m) => m.name === "duration")?.value || 0
|
||||
|
||||
// Collect test metrics
|
||||
const testsPassed = metrics.find((m) => m.name === "testsPassed")?.value || 0
|
||||
const testsFailed = metrics.find((m) => m.name === "testsFailed")?.value || 0
|
||||
const testsTotal = metrics.find((m) => m.name === "testsTotal")?.value || 0
|
||||
benchmarkTotalTestsPassed += testsPassed
|
||||
benchmarkTotalTestsFailed += testsFailed
|
||||
benchmarkTotalTests += testsTotal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,60 +185,14 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
benchmarkSummary.averageTokens = benchmarkTasks > 0 ? benchmarkTotalTokens / benchmarkTasks : 0
|
||||
benchmarkSummary.averageCost = benchmarkTasks > 0 ? benchmarkTotalCost / benchmarkTasks : 0
|
||||
benchmarkSummary.averageDuration = benchmarkTasks > 0 ? benchmarkTotalDuration / benchmarkTasks : 0
|
||||
benchmarkSummary.totalTests = benchmarkTotalTests
|
||||
benchmarkSummary.totalTestsPassed = benchmarkTotalTestsPassed
|
||||
benchmarkSummary.totalTestsFailed = benchmarkTotalTestsFailed
|
||||
benchmarkSummary.testSuccessRate = benchmarkTotalTests > 0 ? benchmarkTotalTestsPassed / benchmarkTotalTests : 0
|
||||
|
||||
benchmarkReports[benchmark] = benchmarkSummary
|
||||
}
|
||||
|
||||
// Generate model-specific reports
|
||||
const modelReports: Record<string, any> = {}
|
||||
|
||||
for (const model of summary.models) {
|
||||
const modelRuns = runs.filter((run) => run.model === model)
|
||||
const modelSummary = {
|
||||
runs: modelRuns.length,
|
||||
benchmarks: [...new Set(modelRuns.map((run) => run.benchmark))],
|
||||
tasks: 0,
|
||||
successRate: 0,
|
||||
averageTokens: 0,
|
||||
averageCost: 0,
|
||||
averageDuration: 0,
|
||||
}
|
||||
|
||||
let modelTasks = 0
|
||||
let modelSuccessfulTasks = 0
|
||||
let modelTotalTokens = 0
|
||||
let modelTotalCost = 0
|
||||
let modelTotalDuration = 0
|
||||
|
||||
for (const run of modelRuns) {
|
||||
const tasks = db.getRunTasks(run.id)
|
||||
modelTasks += tasks.length
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.success) {
|
||||
modelSuccessfulTasks++
|
||||
}
|
||||
|
||||
const metrics = db.getTaskMetrics(task.id)
|
||||
|
||||
const tokensIn = metrics.find((m) => m.name === "tokensIn")?.value || 0
|
||||
const tokensOut = metrics.find((m) => m.name === "tokensOut")?.value || 0
|
||||
modelTotalTokens += tokensIn + tokensOut
|
||||
|
||||
modelTotalCost += metrics.find((m) => m.name === "cost")?.value || 0
|
||||
modelTotalDuration += metrics.find((m) => m.name === "duration")?.value || 0
|
||||
}
|
||||
}
|
||||
|
||||
modelSummary.tasks = modelTasks
|
||||
modelSummary.successRate = modelTasks > 0 ? modelSuccessfulTasks / modelTasks : 0
|
||||
modelSummary.averageTokens = modelTasks > 0 ? modelTotalTokens / modelTasks : 0
|
||||
modelSummary.averageCost = modelTasks > 0 ? modelTotalCost / modelTasks : 0
|
||||
modelSummary.averageDuration = modelTasks > 0 ? modelTotalDuration / modelTasks : 0
|
||||
|
||||
modelReports[model] = modelSummary
|
||||
}
|
||||
|
||||
// Save reports
|
||||
const reportDir = path.join(path.resolve(__dirname, "../../../"), "results", "reports")
|
||||
fs.mkdirSync(reportDir, { recursive: true })
|
||||
@@ -217,14 +205,12 @@ export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
|
||||
fs.writeFileSync(path.join(reportDir, `benchmarks-${timestamp}.json`), JSON.stringify(benchmarkReports, null, 2))
|
||||
|
||||
fs.writeFileSync(path.join(reportDir, `models-${timestamp}.json`), JSON.stringify(modelReports, null, 2))
|
||||
|
||||
spinner.succeed(`JSON reports generated in ${reportDir}`)
|
||||
} else {
|
||||
// Generate markdown report
|
||||
const outputPath = options.output || path.join(reportDir, `report-${timestamp}.md`)
|
||||
|
||||
generateMarkdownReport(summary, benchmarkReports, modelReports, outputPath)
|
||||
generateMarkdownReport(summary, benchmarkReports, outputPath)
|
||||
|
||||
spinner.succeed(`Markdown report generated at ${outputPath}`)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import * as path from "path"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import chalk from "chalk"
|
||||
import ora from "ora"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { getAdapter } from "../adapters"
|
||||
import { ResultsDatabase } from "../db"
|
||||
import { spawnVSCode, cleanupVSCode } from "../utils/vscode"
|
||||
import { sendTaskToServer } from "../utils/task"
|
||||
import { storeTaskResult } from "../utils/results"
|
||||
|
||||
interface RunOptions {
|
||||
benchmark?: string
|
||||
model: string
|
||||
count?: number
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,12 +16,10 @@ interface RunOptions {
|
||||
*/
|
||||
export async function runHandler(options: RunOptions): Promise<void> {
|
||||
// Determine which benchmarks to run
|
||||
const benchmarks = options.benchmark ? [options.benchmark] : ["exercism"] // Default to exercism for now
|
||||
const model = options.model
|
||||
const benchmarks = options.benchmark ? [options.benchmark] : ["exercism"] // Default to exercism
|
||||
const count = options.count || Infinity
|
||||
|
||||
console.log(chalk.blue(`Running evaluations for model: ${model}`))
|
||||
console.log(chalk.blue(`Benchmarks: ${benchmarks.join(", ")}`))
|
||||
console.log(chalk.blue(`Running evaluations for the following benchmarks: ${benchmarks.join(", ")}`))
|
||||
|
||||
// Create a run for each benchmark
|
||||
for (const benchmark of benchmarks) {
|
||||
@@ -36,7 +29,7 @@ export async function runHandler(options: RunOptions): Promise<void> {
|
||||
console.log(chalk.green(`\nStarting run for benchmark: ${benchmark}`))
|
||||
|
||||
// Create run in database
|
||||
db.createRun(runId, model, benchmark)
|
||||
db.createRun(runId, benchmark)
|
||||
|
||||
// Get adapter for this benchmark
|
||||
try {
|
||||
@@ -63,58 +56,47 @@ export async function runHandler(options: RunOptions): Promise<void> {
|
||||
const preparedTask = await adapter.prepareTask(task.id)
|
||||
prepareSpinner.succeed("Task prepared")
|
||||
|
||||
// Spawn VSCode
|
||||
console.log("Spawning VSCode...")
|
||||
await spawnVSCode(preparedTask.workspacePath)
|
||||
let cleanedUp = false
|
||||
|
||||
// Send task to server
|
||||
const sendSpinner = ora("Sending task to server...").start()
|
||||
try {
|
||||
const result = await sendTaskToServer(preparedTask.description, options.apiKey)
|
||||
sendSpinner.succeed("Task completed")
|
||||
// Run task using adapter's execution strategy
|
||||
const finalVerification = await adapter.runTask(preparedTask)
|
||||
|
||||
// Verify result
|
||||
const verifySpinner = ora("Verifying result...").start()
|
||||
const verification = await adapter.verifyResult(preparedTask, result)
|
||||
// Cleanup task
|
||||
const cleanupSpinner = ora("Cleaning up task...").start()
|
||||
await adapter.cleanupTask(preparedTask)
|
||||
cleanedUp = true
|
||||
cleanupSpinner.succeed("Cleanup complete")
|
||||
|
||||
// Use final verification from runTask
|
||||
const verification = finalVerification || (await adapter.verifyResult(preparedTask))
|
||||
|
||||
if (verification.success) {
|
||||
verifySpinner.succeed(
|
||||
`Verification successful: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
|
||||
console.log(
|
||||
chalk.green(`Tests passed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`),
|
||||
)
|
||||
} else {
|
||||
verifySpinner.fail(
|
||||
`Verification failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
|
||||
console.log(
|
||||
chalk.red(`Tests failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal}`),
|
||||
)
|
||||
}
|
||||
|
||||
// Store result
|
||||
const storeSpinner = ora("Storing result...").start()
|
||||
await storeTaskResult(runId, preparedTask, result, verification)
|
||||
await storeTaskResult(runId, preparedTask, {}, verification)
|
||||
storeSpinner.succeed("Result stored")
|
||||
|
||||
console.log(chalk.green(`Task completed. Success: ${verification.success}`))
|
||||
|
||||
// Clean up VS Code and temporary files
|
||||
const cleanupSpinner = ora("Cleaning up...").start()
|
||||
try {
|
||||
await cleanupVSCode(preparedTask.workspacePath)
|
||||
cleanupSpinner.succeed("Cleanup completed")
|
||||
} catch (cleanupError: any) {
|
||||
cleanupSpinner.fail(`Cleanup failed: ${cleanupError.message}`)
|
||||
console.error(chalk.yellow(cleanupError.stack))
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendSpinner.fail(`Task failed: ${error.message}`)
|
||||
console.error(chalk.red(error.stack))
|
||||
|
||||
// Clean up VS Code and temporary files even if the task failed
|
||||
const cleanupSpinner = ora("Cleaning up...").start()
|
||||
try {
|
||||
await cleanupVSCode(preparedTask.workspacePath)
|
||||
cleanupSpinner.succeed("Cleanup completed")
|
||||
} catch (cleanupError: any) {
|
||||
cleanupSpinner.fail(`Cleanup failed: ${cleanupError.message}`)
|
||||
console.error(chalk.yellow(cleanupError.stack))
|
||||
console.error(chalk.red(`Task failed: ${error.message}`))
|
||||
} finally {
|
||||
// Ensure cleanup always happens
|
||||
if (!cleanedUp) {
|
||||
try {
|
||||
const finalCleanupSpinner = ora("Performing cleanup...").start()
|
||||
await adapter.cleanupTask(preparedTask)
|
||||
finalCleanupSpinner.succeed("Cleanup complete")
|
||||
} catch (cleanupError: any) {
|
||||
console.error(chalk.red(`Cleanup failed: ${cleanupError.message}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +107,6 @@ export async function runHandler(options: RunOptions): Promise<void> {
|
||||
console.log(chalk.green(`\nRun complete for benchmark: ${benchmark}`))
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Error running benchmark ${benchmark}: ${error.message}`))
|
||||
console.error(error.stack)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import Database from "better-sqlite3"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { SCHEMA } from "./schema"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
@@ -34,16 +34,15 @@ export class ResultsDatabase {
|
||||
/**
|
||||
* Create a new evaluation run
|
||||
* @param id Run ID
|
||||
* @param model Model name
|
||||
* @param benchmark Benchmark name
|
||||
*/
|
||||
createRun(id: string, model: string, benchmark: string): void {
|
||||
createRun(id: string, benchmark: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO runs (id, timestamp, model, benchmark)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO runs (id, timestamp, benchmark)
|
||||
VALUES (?, ?, ?)
|
||||
`)
|
||||
|
||||
stmt.run(id, Date.now(), model, benchmark)
|
||||
stmt.run(id, Date.now(), benchmark)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ export const SCHEMA = `
|
||||
CREATE TABLE IF NOT EXISTS runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
timestamp INTEGER NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
benchmark TEXT NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
+10
-28
@@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
import { Command } from "commander"
|
||||
import chalk from "chalk"
|
||||
import { setupHandler } from "./commands/setup"
|
||||
import { runHandler } from "./commands/run"
|
||||
import { Command } from "commander"
|
||||
import { reportHandler } from "./commands/report"
|
||||
import { evalsEnvHandler } from "./commands/evals-env"
|
||||
import { runHandler } from "./commands/run"
|
||||
import { runDiffEvalHandler } from "./commands/runDiffEval"
|
||||
import { setupHandler } from "./commands/setup"
|
||||
|
||||
// Create the CLI program
|
||||
const program = new Command()
|
||||
@@ -17,11 +16,7 @@ program.name("cline-eval").description("CLI tool for orchestrating Cline evaluat
|
||||
program
|
||||
.command("setup")
|
||||
.description("Clone and set up benchmark repositories")
|
||||
.option(
|
||||
"-b, --benchmarks <benchmarks>",
|
||||
"Comma-separated list of benchmarks to set up",
|
||||
"exercism,swe-bench,swelancer,multi-swe",
|
||||
)
|
||||
.option("-b, --benchmarks <benchmarks>", "Comma-separated list of benchmarks to set up", "exercism")
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await setupHandler(options)
|
||||
@@ -36,9 +31,7 @@ program
|
||||
.command("run")
|
||||
.description("Run evaluations")
|
||||
.option("-b, --benchmark <benchmark>", "Specific benchmark to run")
|
||||
.option("-m, --model <model>", "Model to evaluate", "claude-3-opus-20240229")
|
||||
.option("-c, --count <count>", "Number of tasks to run", parseInt)
|
||||
.option("-k, --api-key <apiKey>", "Cline API key to use for evaluations")
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await runHandler(options)
|
||||
@@ -63,21 +56,6 @@ program
|
||||
}
|
||||
})
|
||||
|
||||
// Evals-env command
|
||||
program
|
||||
.command("evals-env")
|
||||
.description("Manage evals.env files for test mode activation")
|
||||
.argument("<action>", "Action to perform: create, remove, or check")
|
||||
.option("-d, --directory <directory>", "Directory to create/remove/check evals.env file in (defaults to current directory)")
|
||||
.action(async (action, options) => {
|
||||
try {
|
||||
await evalsEnvHandler({ action, ...options })
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error managing evals.env file: ${error instanceof Error ? error.message : String(error)}`))
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Run-diff-eval command
|
||||
program
|
||||
.command("run-diff-eval")
|
||||
@@ -86,11 +64,15 @@ program
|
||||
.option("--output-path <path>", "Path to the directory to save the test output JSON files")
|
||||
.option("--model-ids <model_ids>", "Comma-separated list of model IDs to test")
|
||||
.option("--system-prompt-name <name>", "The name of the system prompt to use", "basicSystemPrompt")
|
||||
.option("-n, --valid-attempts-per-case <number>", "Number of valid attempts per test case per model (will retry until this many valid attempts are collected)", "1")
|
||||
.option(
|
||||
"-n, --valid-attempts-per-case <number>",
|
||||
"Number of valid attempts per test case per model (will retry until this many valid attempts are collected)",
|
||||
"1",
|
||||
)
|
||||
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
|
||||
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
|
||||
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
|
||||
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
|
||||
.option("--diff-edit-function <name>", "The diff editing function to use", "diff-06-26-25")
|
||||
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
|
||||
.option("--provider <provider>", "API provider to use (openrouter, openai)", "openrouter")
|
||||
.option("--parallel", "Run tests in parallel", false)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import chalk from "chalk"
|
||||
|
||||
/**
|
||||
* Creates an evals.env file in the specified directory
|
||||
* @param directory The directory where the evals.env file should be created
|
||||
* @returns True if the file was created, false if it already exists
|
||||
*/
|
||||
export function createEvalsEnvFile(directory: string): boolean {
|
||||
const evalsEnvPath = path.join(directory, "evals.env")
|
||||
|
||||
// Check if the file already exists
|
||||
if (fs.existsSync(evalsEnvPath)) {
|
||||
console.log(chalk.yellow(`evals.env file already exists at ${evalsEnvPath}`))
|
||||
return false
|
||||
}
|
||||
|
||||
// Create the file
|
||||
try {
|
||||
const content = `# This file activates Cline test mode
|
||||
# Created at: ${new Date().toISOString()}
|
||||
#
|
||||
# This file is automatically detected by the Cline extension
|
||||
# and enables test mode for automated evaluations.
|
||||
#
|
||||
# Delete this file to deactivate test mode.
|
||||
`
|
||||
fs.writeFileSync(evalsEnvPath, content)
|
||||
console.log(chalk.green(`Created evals.env file at ${evalsEnvPath}`))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error creating evals.env file: ${error}`))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an evals.env file from the specified directory
|
||||
* @param directory The directory where the evals.env file should be removed
|
||||
* @returns True if the file was removed, false if it doesn't exist
|
||||
*/
|
||||
export function removeEvalsEnvFile(directory: string): boolean {
|
||||
const evalsEnvPath = path.join(directory, "evals.env")
|
||||
|
||||
// Check if the file exists
|
||||
if (!fs.existsSync(evalsEnvPath)) {
|
||||
console.log(chalk.yellow(`No evals.env file found at ${evalsEnvPath}`))
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove the file
|
||||
try {
|
||||
fs.unlinkSync(evalsEnvPath)
|
||||
console.log(chalk.green(`Removed evals.env file from ${evalsEnvPath}`))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error removing evals.env file: ${error}`))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an evals.env file exists in the specified directory
|
||||
* @param directory The directory to check for an evals.env file
|
||||
* @returns True if the file exists, false otherwise
|
||||
*/
|
||||
export function checkEvalsEnvFile(directory: string): boolean {
|
||||
const evalsEnvPath = path.join(directory, "evals.env")
|
||||
const exists = fs.existsSync(evalsEnvPath)
|
||||
|
||||
if (exists) {
|
||||
console.log(chalk.green(`evals.env file found at ${evalsEnvPath}`))
|
||||
} else {
|
||||
console.log(chalk.yellow(`No evals.env file found at ${evalsEnvPath}`))
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import execa from "execa"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
|
||||
/**
|
||||
* List of VSCode extensions to install for evaluation environments
|
||||
* These extensions provide language support and other useful features
|
||||
*/
|
||||
export const REQUIRED_EXTENSIONS = [
|
||||
"golang.go", // Go language support
|
||||
"dbaeumer.vscode-eslint", // ESLint support
|
||||
"redhat.java", // Java support
|
||||
"ms-python.python", // Python support
|
||||
"rust-lang.rust-analyzer", // Rust support
|
||||
"ms-vscode.cpptools", // C/C++ support
|
||||
]
|
||||
|
||||
/**
|
||||
* Install required VSCode extensions in the specified extensions directory
|
||||
* @param extensionsDir The directory where extensions should be installed
|
||||
* @returns Promise that resolves when all extensions are installed
|
||||
*/
|
||||
export async function installRequiredExtensions(extensionsDir: string): Promise<void> {
|
||||
console.log("Installing required VSCode extensions...")
|
||||
|
||||
// Create the extensions directory if it doesn't exist
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
fs.mkdirSync(extensionsDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Install each extension
|
||||
for (const extension of REQUIRED_EXTENSIONS) {
|
||||
try {
|
||||
console.log(`Installing extension: ${extension}...`)
|
||||
await execa("code", ["--extensions-dir", extensionsDir, "--install-extension", extension, "--force"])
|
||||
console.log(`✅ Extension ${extension} installed successfully`)
|
||||
} catch (error: any) {
|
||||
console.warn(`⚠️ Failed to install extension ${extension}: ${error.message}`)
|
||||
// Continue with other extensions even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ All required extensions installed")
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a VSCode extension is installed in the specified directory
|
||||
* @param extensionsDir The directory to check for installed extensions
|
||||
* @param extensionId The ID of the extension to check
|
||||
* @returns True if the extension is installed, false otherwise
|
||||
*/
|
||||
export function isExtensionInstalled(extensionsDir: string, extensionId: string): boolean {
|
||||
// Extensions are installed in directories named publisher.name-version
|
||||
// We need to check if any directory starts with the extensionId
|
||||
const extensionPrefix = extensionId.toLowerCase() + "-"
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(extensionsDir)
|
||||
return files.some((file) => {
|
||||
const lowerCaseFile = file.toLowerCase()
|
||||
return lowerCaseFile === extensionId.toLowerCase() || lowerCaseFile.startsWith(extensionPrefix)
|
||||
})
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the VSCode settings file in the specified user data directory
|
||||
* @param userDataDir The VSCode user data directory
|
||||
* @returns The path to the settings.json file
|
||||
*/
|
||||
export function getSettingsPath(userDataDir: string): string {
|
||||
const settingsDir = path.join(userDataDir, "User")
|
||||
fs.mkdirSync(settingsDir, { recursive: true })
|
||||
return path.join(settingsDir, "settings.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure extension settings in the VSCode user data directory
|
||||
* @param userDataDir The VSCode user data directory
|
||||
*/
|
||||
export function configureExtensionSettings(userDataDir: string): void {
|
||||
const settingsPath = getSettingsPath(userDataDir)
|
||||
|
||||
// Read existing settings if they exist
|
||||
let settings = {}
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"))
|
||||
} catch (error) {
|
||||
console.warn(`Error reading settings file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update extension-specific settings
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
// Go extension settings
|
||||
"go.toolsManagement.autoUpdate": false,
|
||||
"go.survey.prompt": false,
|
||||
|
||||
// ESLint settings
|
||||
"eslint.enable": true,
|
||||
"eslint.run": "onSave",
|
||||
|
||||
// Java settings
|
||||
"java.configuration.checkProjectSettingsExclusions": false,
|
||||
"java.configure.checkForOutdatedExtensions": false,
|
||||
"java.help.firstView": false,
|
||||
|
||||
// Python settings
|
||||
"python.experiments.enabled": false,
|
||||
"python.showStartPage": false,
|
||||
|
||||
// Rust settings
|
||||
"rust-analyzer.checkOnSave.command": "check",
|
||||
|
||||
// C/C++ settings
|
||||
"C_Cpp.intelliSenseEngine": "default",
|
||||
|
||||
// General extension settings
|
||||
"extensions.autoUpdate": false,
|
||||
"extensions.ignoreRecommendations": true,
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(updatedSettings, null, 2))
|
||||
console.log("✅ Extension settings configured")
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
|
||||
/**
|
||||
* Generate a markdown report from evaluation results
|
||||
* @param summary Overall summary
|
||||
* @param benchmarkReports Benchmark-specific reports
|
||||
* @param modelReports Model-specific reports
|
||||
* @param outputPath Output file path
|
||||
*/
|
||||
export function generateMarkdownReport(
|
||||
summary: any,
|
||||
benchmarkReports: Record<string, any>,
|
||||
modelReports: Record<string, any>,
|
||||
outputPath: string,
|
||||
): void {
|
||||
export function generateMarkdownReport(summary: any, benchmarkReports: Record<string, any>, outputPath: string): void {
|
||||
let markdown = `# Cline Evaluation Report\n\n`
|
||||
|
||||
// Generate summary section
|
||||
markdown += `## Summary\n\n`
|
||||
markdown += `- **Total Runs:** ${summary.runs}\n`
|
||||
markdown += `- **Models:** ${summary.models.join(", ")}\n`
|
||||
markdown += `- **Benchmarks:** ${summary.benchmarks.join(", ")}\n`
|
||||
markdown += `- **Total Tasks:** ${summary.tasks}\n`
|
||||
markdown += `- **Success Rate:** ${(summary.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Task Success Rate:** ${(summary.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Total Tests:** ${summary.totalTests}\n`
|
||||
markdown += `- **Tests Passed:** ${summary.totalTestsPassed}\n`
|
||||
markdown += `- **Tests Failed:** ${summary.totalTestsFailed}\n`
|
||||
markdown += `- **Test Success Rate:** ${(summary.testSuccessRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Average Tokens:** ${Math.round(summary.averageTokens)}\n`
|
||||
markdown += `- **Average Cost:** $${summary.averageCost.toFixed(4)}\n`
|
||||
markdown += `- **Average Duration:** ${(summary.averageDuration / 1000).toFixed(2)}s\n`
|
||||
@@ -48,23 +44,12 @@ export function generateMarkdownReport(
|
||||
for (const [benchmark, report] of Object.entries(benchmarkReports)) {
|
||||
markdown += `### ${benchmark}\n\n`
|
||||
markdown += `- **Runs:** ${report.runs}\n`
|
||||
markdown += `- **Models:** ${report.models.join(", ")}\n`
|
||||
markdown += `- **Tasks:** ${report.tasks}\n`
|
||||
markdown += `- **Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Average Tokens:** ${Math.round(report.averageTokens)}\n`
|
||||
markdown += `- **Average Cost:** $${report.averageCost.toFixed(4)}\n`
|
||||
markdown += `- **Average Duration:** ${(report.averageDuration / 1000).toFixed(2)}s\n\n`
|
||||
}
|
||||
|
||||
// Generate model results section
|
||||
markdown += `## Model Results\n\n`
|
||||
|
||||
for (const [model, report] of Object.entries(modelReports)) {
|
||||
markdown += `### ${model}\n\n`
|
||||
markdown += `- **Runs:** ${report.runs}\n`
|
||||
markdown += `- **Benchmarks:** ${report.benchmarks.join(", ")}\n`
|
||||
markdown += `- **Tasks:** ${report.tasks}\n`
|
||||
markdown += `- **Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Task Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Total Tests:** ${report.totalTests}\n`
|
||||
markdown += `- **Tests Passed:** ${report.totalTestsPassed}\n`
|
||||
markdown += `- **Tests Failed:** ${report.totalTestsFailed}\n`
|
||||
markdown += `- **Test Success Rate:** ${(report.testSuccessRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Average Tokens:** ${Math.round(report.averageTokens)}\n`
|
||||
markdown += `- **Average Cost:** $${report.averageCost.toFixed(4)}\n`
|
||||
markdown += `- **Average Duration:** ${(report.averageDuration / 1000).toFixed(2)}s\n\n`
|
||||
@@ -87,20 +72,6 @@ export function generateMarkdownReport(
|
||||
|
||||
markdown += "```\n\n"
|
||||
|
||||
// Success rate by model chart
|
||||
markdown += `### Success Rate by Model\n\n`
|
||||
markdown += "```mermaid\n"
|
||||
markdown += "graph TD\n"
|
||||
markdown += " title[Success Rate by Model]\n"
|
||||
markdown += " style title fill:none,stroke:none\n\n"
|
||||
|
||||
for (const [model, report] of Object.entries(modelReports)) {
|
||||
const successRate = (report.successRate * 100).toFixed(2)
|
||||
markdown += ` ${model.replace(/[-\.]/g, "_")}[${model}: ${successRate}%]\n`
|
||||
}
|
||||
|
||||
markdown += "```\n\n"
|
||||
|
||||
// Add timestamp
|
||||
markdown += `\n\n---\n\nReport generated on ${new Date().toISOString()}\n`
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import fetch from "node-fetch"
|
||||
import chalk from "chalk"
|
||||
|
||||
/**
|
||||
* Send a task to the Cline test server
|
||||
* @param task The task description to send
|
||||
* @param apiKey Optional Cline API key to use for the task
|
||||
* @returns The result of the task execution
|
||||
*/
|
||||
export async function sendTaskToServer(task: string, apiKey?: string): Promise<any> {
|
||||
const SERVER_URL = "http://localhost:9876/task"
|
||||
|
||||
try {
|
||||
console.log(chalk.blue(`Sending task to server: ${task.substring(0, 100)}${task.length > 100 ? "..." : ""}`))
|
||||
|
||||
const response = await fetch(SERVER_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
task,
|
||||
apiKey,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Server responded with status ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Task execution failed: ${result.error || "Unknown error"}`)
|
||||
}
|
||||
|
||||
if (result.timeout) {
|
||||
throw new Error("Task execution timed out")
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error: any) {
|
||||
if (error.code === "ECONNREFUSED") {
|
||||
throw new Error(
|
||||
"Could not connect to the test server. Make sure VSCode is running with the Cline extension and the test server is active.",
|
||||
)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,598 +0,0 @@
|
||||
import execa from "execa"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import fetch from "node-fetch"
|
||||
import * as os from "os"
|
||||
import { installRequiredExtensions, configureExtensionSettings } from "./extensions"
|
||||
|
||||
// Store temporary directories for cleanup
|
||||
interface VSCodeResources {
|
||||
tempUserDataDir: string
|
||||
tempExtensionsDir: string
|
||||
vscodePid?: number
|
||||
}
|
||||
|
||||
// Global map to track resources for each workspace
|
||||
const workspaceResources = new Map<string, VSCodeResources>()
|
||||
|
||||
/**
|
||||
* Spawn a VSCode instance with the Cline extension
|
||||
* @param workspacePath The workspace path to open
|
||||
* @param vsixPath Optional path to a VSIX file to install
|
||||
* @returns The resources created for this VS Code instance
|
||||
*/
|
||||
export async function spawnVSCode(workspacePath: string, vsixPath?: string): Promise<VSCodeResources> {
|
||||
// Ensure the workspace path exists
|
||||
if (!fs.existsSync(workspacePath)) {
|
||||
throw new Error(`Workspace path does not exist: ${workspacePath}`)
|
||||
}
|
||||
|
||||
// If no VSIX path is provided, build one with IS_TEST=true
|
||||
if (!vsixPath) {
|
||||
try {
|
||||
// Build the VSIX (no longer need to set IS_TEST=true as we'll use evals.env file)
|
||||
console.log("Building VSIX...")
|
||||
const clineRoot = path.resolve(process.cwd(), "..", "..")
|
||||
await execa("npx", ["vsce", "package"], {
|
||||
cwd: clineRoot,
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
||||
// Find the generated VSIX file(s)
|
||||
const files = fs.readdirSync(clineRoot)
|
||||
const vsixFiles = files.filter((file) => file.endsWith(".vsix"))
|
||||
|
||||
if (vsixFiles.length > 0) {
|
||||
// Get file stats to find the most recent one
|
||||
const vsixFilesWithStats = vsixFiles.map((file) => {
|
||||
const filePath = path.join(clineRoot, file)
|
||||
return {
|
||||
file,
|
||||
path: filePath,
|
||||
mtime: fs.statSync(filePath).mtime,
|
||||
}
|
||||
})
|
||||
|
||||
// Sort by modification time (most recent first)
|
||||
vsixFilesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
|
||||
|
||||
// Use the most recent VSIX
|
||||
vsixPath = vsixFilesWithStats[0].path
|
||||
console.log(`Using most recent VSIX: ${vsixPath} (modified ${vsixFilesWithStats[0].mtime.toISOString()})`)
|
||||
|
||||
// Log all found VSIX files for debugging
|
||||
if (vsixFiles.length > 1) {
|
||||
console.log(`Found ${vsixFiles.length} VSIX files:`)
|
||||
vsixFilesWithStats.forEach((f) => {
|
||||
console.log(` - ${f.file} (modified ${f.mtime.toISOString()})`)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.warn("Could not find generated VSIX file")
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to build test VSIX:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a temporary user data directory for this VS Code instance
|
||||
const tempUserDataDir = path.join(os.tmpdir(), `vscode-cline-eval-${Date.now()}`)
|
||||
fs.mkdirSync(tempUserDataDir, { recursive: true })
|
||||
console.log(`Created temporary user data directory: ${tempUserDataDir}`)
|
||||
|
||||
// Create a temporary extensions directory to ensure no other extensions are loaded
|
||||
const tempExtensionsDir = path.join(os.tmpdir(), `vscode-cline-eval-ext-${Date.now()}`)
|
||||
fs.mkdirSync(tempExtensionsDir, { recursive: true })
|
||||
console.log(`Created temporary extensions directory: ${tempExtensionsDir}`)
|
||||
|
||||
// Create evals.env file in the workspace to trigger test mode
|
||||
console.log(`Creating evals.env file in workspace: ${workspacePath}`)
|
||||
const evalsEnvPath = path.join(workspacePath, "evals.env")
|
||||
fs.writeFileSync(
|
||||
evalsEnvPath,
|
||||
`# This file activates Cline test mode
|
||||
# Created at: ${new Date().toISOString()}
|
||||
#
|
||||
# This file is automatically detected by the Cline extension
|
||||
# and enables test mode for automated evaluations.
|
||||
#
|
||||
# Delete this file to deactivate test mode.
|
||||
`,
|
||||
)
|
||||
|
||||
// Create settings.json in the temporary user data directory to disable workspace trust
|
||||
// and configure Cline to auto-open on startup
|
||||
const settingsDir = path.join(tempUserDataDir, "User")
|
||||
fs.mkdirSync(settingsDir, { recursive: true })
|
||||
const settingsPath = path.join(settingsDir, "settings.json")
|
||||
const settings = {
|
||||
// Disable workspace trust
|
||||
"security.workspace.trust.enabled": false,
|
||||
"security.workspace.trust.startupPrompt": "never",
|
||||
"security.workspace.trust.banner": "never",
|
||||
"security.workspace.trust.emptyWindow": true,
|
||||
|
||||
// Configure startup behavior
|
||||
"workbench.startupEditor": "none",
|
||||
|
||||
// Auto-open Cline on startup
|
||||
"cline.autoOpenOnStartup": true,
|
||||
|
||||
// Show the activity bar and sidebar
|
||||
"workbench.activityBar.visible": true,
|
||||
"workbench.sideBar.visible": true,
|
||||
"workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.visible": true,
|
||||
"workbench.view.alwaysShowHeaderActions": true,
|
||||
"workbench.editor.openSideBySideDirection": "right",
|
||||
|
||||
// Disable GitLens from opening automatically
|
||||
"gitlens.views.repositories.autoReveal": false,
|
||||
"gitlens.views.fileHistory.autoReveal": false,
|
||||
"gitlens.views.lineHistory.autoReveal": false,
|
||||
"gitlens.views.compare.autoReveal": false,
|
||||
"gitlens.views.search.autoReveal": false,
|
||||
"gitlens.showWelcomeOnInstall": false,
|
||||
"gitlens.showWhatsNewAfterUpgrades": false,
|
||||
|
||||
// Disable other extensions that might compete for startup focus
|
||||
"extensions.autoUpdate": false,
|
||||
}
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
||||
console.log(`Created settings.json to disable workspace trust and auto-open Cline`)
|
||||
|
||||
// Create keybindings.json to automatically open Cline on startup
|
||||
const keybindingsPath = path.join(settingsDir, "keybindings.json")
|
||||
const keybindings = [
|
||||
{
|
||||
key: "alt+c",
|
||||
command: "workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
|
||||
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
|
||||
},
|
||||
]
|
||||
fs.writeFileSync(keybindingsPath, JSON.stringify(keybindings, null, 2))
|
||||
console.log(`Created keybindings.json to help with Cline activation`)
|
||||
|
||||
// Build the command arguments with custom user data directory
|
||||
const args = [
|
||||
// Use a custom user data directory to isolate this instance
|
||||
"--user-data-dir",
|
||||
tempUserDataDir,
|
||||
// Use a custom extensions directory to ensure only our extension is loaded
|
||||
"--extensions-dir",
|
||||
tempExtensionsDir,
|
||||
// Disable workspace trust
|
||||
"--disable-workspace-trust",
|
||||
"-n",
|
||||
workspacePath,
|
||||
// Force the extension to be activated on startup
|
||||
"--start-up-extension",
|
||||
"saoudrizwan.claude-dev",
|
||||
// Run a command on startup to open Cline
|
||||
"--command",
|
||||
"workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
|
||||
// Additional flags to help with extension activation
|
||||
"--disable-gpu=false",
|
||||
"--max-memory=4096",
|
||||
]
|
||||
|
||||
// Create a startup script to run commands after VS Code launches
|
||||
const startupScriptPath = path.join(settingsDir, "startup.js")
|
||||
const startupScript = `
|
||||
// This script will be executed when VS Code starts
|
||||
setTimeout(() => {
|
||||
// Try to open Cline in the sidebar
|
||||
require('vscode').commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
|
||||
}, 5000);
|
||||
`
|
||||
fs.writeFileSync(startupScriptPath, startupScript)
|
||||
console.log(`Created startup script to activate Cline`)
|
||||
|
||||
// If a VSIX is provided, install it
|
||||
if (vsixPath) {
|
||||
if (!fs.existsSync(vsixPath)) {
|
||||
throw new Error(`VSIX file does not exist: ${vsixPath}`)
|
||||
}
|
||||
args.unshift("--install-extension", vsixPath)
|
||||
}
|
||||
|
||||
// Install required extensions
|
||||
console.log("Installing required VSCode extensions...")
|
||||
await installRequiredExtensions(tempExtensionsDir)
|
||||
|
||||
// Configure extension settings
|
||||
console.log("Configuring extension settings...")
|
||||
configureExtensionSettings(tempUserDataDir)
|
||||
|
||||
// Execute the command
|
||||
try {
|
||||
// We don't need to install extensions globally anymore since we're using a custom user data directory
|
||||
// The VSIX will be installed in the isolated environment if provided in the args
|
||||
|
||||
// Launch VS Code
|
||||
console.log("Launching VS Code...")
|
||||
await execa("code", args, {
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
||||
// Wait longer for VSCode to initialize and extension to load
|
||||
console.log("Waiting for VS Code to initialize...")
|
||||
await new Promise((resolve) => setTimeout(resolve, 30000))
|
||||
|
||||
// Create a JavaScript file that will be loaded as a VS Code extension
|
||||
const extensionDir = path.join(tempExtensionsDir, "cline-activator")
|
||||
fs.mkdirSync(extensionDir, { recursive: true })
|
||||
|
||||
// Create package.json for the extension
|
||||
const packageJsonPath = path.join(extensionDir, "package.json")
|
||||
const packageJson = {
|
||||
name: "cline-activator",
|
||||
displayName: "Cline Activator",
|
||||
description: "Activates Cline and starts the test server",
|
||||
version: "0.0.1",
|
||||
engines: {
|
||||
vscode: "^1.60.0",
|
||||
},
|
||||
main: "./extension.js",
|
||||
activationEvents: ["*"],
|
||||
contributes: {
|
||||
commands: [
|
||||
{
|
||||
command: "cline-activator.activate",
|
||||
title: "Activate Cline",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
||||
|
||||
// Create extension.js
|
||||
const extensionJsPath = path.join(extensionDir, "extension.js")
|
||||
const extensionJs = `
|
||||
const vscode = require('vscode');
|
||||
|
||||
/**
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
console.log('Cline Activator is now active!');
|
||||
|
||||
// Register the command to activate Cline
|
||||
let disposable = vscode.commands.registerCommand('cline-activator.activate', async function () {
|
||||
try {
|
||||
// Make sure the Cline extension is activated
|
||||
const extension = vscode.extensions.getExtension('saoudrizwan.claude-dev');
|
||||
if (!extension) {
|
||||
console.error('Cline extension not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!extension.isActive) {
|
||||
console.log('Activating Cline extension...');
|
||||
await extension.activate();
|
||||
}
|
||||
|
||||
// Show the Cline sidebar
|
||||
console.log('Opening Cline sidebar...');
|
||||
await vscode.commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
|
||||
|
||||
// Wait a moment for the sidebar to initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Create the test server if it doesn't exist
|
||||
console.log('Creating test server...');
|
||||
|
||||
// Get the visible webview instance
|
||||
const clineRootPath = '${path.resolve(process.cwd(), "..", "..")}';
|
||||
const visibleWebview = require(path.join(clineRootPath, 'src', 'core', 'webview')).WebviewProvider.getVisibleInstance();
|
||||
if (visibleWebview) {
|
||||
require(path.join(clineRootPath, 'src', 'services', 'test', 'TestServer')).createTestServer(visibleWebview);
|
||||
console.log('Test server created successfully');
|
||||
} else {
|
||||
console.error('No visible webview instance found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error activating Cline:', error);
|
||||
}
|
||||
});
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
|
||||
// Automatically run the command after a delay
|
||||
setTimeout(() => {
|
||||
vscode.commands.executeCommand('cline-activator.activate');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
}
|
||||
`
|
||||
fs.writeFileSync(extensionJsPath, extensionJs)
|
||||
console.log(`Created Cline Activator extension`)
|
||||
|
||||
// Try multiple approaches to activate the extension
|
||||
let serverStarted = false
|
||||
|
||||
// Create an activation script to run in VS Code
|
||||
const activationScriptPath = path.join(settingsDir, "activate-cline.js")
|
||||
const activationScript = `
|
||||
// This script will be executed to activate Cline and start the test server
|
||||
const vscode = require('vscode');
|
||||
|
||||
// Execute the cline-activator.activate command
|
||||
vscode.commands.executeCommand('cline-activator.activate');
|
||||
`
|
||||
fs.writeFileSync(activationScriptPath, activationScript)
|
||||
console.log(`Created activation script to run in VS Code`)
|
||||
|
||||
// Execute the activation script
|
||||
try {
|
||||
console.log("Executing activation script to start Cline and test server...")
|
||||
await execa(
|
||||
"code",
|
||||
[
|
||||
"--user-data-dir",
|
||||
tempUserDataDir,
|
||||
"--extensions-dir",
|
||||
tempExtensionsDir,
|
||||
"--folder-uri",
|
||||
`file://${workspacePath}`,
|
||||
"--execute",
|
||||
activationScriptPath,
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
)
|
||||
|
||||
// Wait for the test server to start
|
||||
console.log("Waiting for test server to start...")
|
||||
for (let i = 0; i < 30; i++) {
|
||||
try {
|
||||
// Try to connect to the test server
|
||||
const response = await fetch("http://localhost:9876/task", {
|
||||
method: "OPTIONS",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 204) {
|
||||
console.log("Test server is running!")
|
||||
serverStarted = true
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
// Server not started yet, wait and try again
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to execute activation script:", error)
|
||||
}
|
||||
|
||||
if (!serverStarted) {
|
||||
console.warn("Test server did not start after multiple attempts")
|
||||
console.log("You may need to manually open the Cline extension in VS Code")
|
||||
}
|
||||
|
||||
// Store the resources for this workspace
|
||||
const resources: VSCodeResources = {
|
||||
tempUserDataDir,
|
||||
tempExtensionsDir,
|
||||
}
|
||||
|
||||
// Store in the global map
|
||||
workspaceResources.set(workspacePath, resources)
|
||||
|
||||
// Return the resources
|
||||
return resources
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to spawn VSCode: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up VS Code resources and shut down the test server
|
||||
* @param workspacePath The workspace path to clean up resources for
|
||||
*/
|
||||
export async function cleanupVSCode(workspacePath: string): Promise<void> {
|
||||
console.log(`Cleaning up VS Code resources for workspace: ${workspacePath}`)
|
||||
|
||||
// Get the resources for this workspace
|
||||
const resources = workspaceResources.get(workspacePath)
|
||||
if (!resources) {
|
||||
console.log(`No resources found for workspace: ${workspacePath}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to shut down the test server
|
||||
try {
|
||||
console.log("Shutting down test server...")
|
||||
await fetch("http://localhost:9876/shutdown", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).catch(() => {
|
||||
// Ignore errors, the server might already be down
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Error shutting down test server: ${error}`)
|
||||
}
|
||||
|
||||
// Try to gracefully close VS Code instead of killing it
|
||||
try {
|
||||
console.log("Attempting to gracefully close VS Code...")
|
||||
|
||||
// Create a settings file that will disable the crash reporter and the exit confirmation dialog
|
||||
const settingsDir = path.join(resources.tempUserDataDir, "User")
|
||||
const settingsPath = path.join(settingsDir, "settings.json")
|
||||
|
||||
// Read existing settings if they exist
|
||||
let settings = {}
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"))
|
||||
} catch (error) {
|
||||
console.warn(`Error reading settings file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Update settings to disable crash reporter and exit confirmation
|
||||
settings = {
|
||||
...settings,
|
||||
"window.confirmBeforeClose": "never",
|
||||
"telemetry.enableCrashReporter": false,
|
||||
"window.restoreWindows": "none",
|
||||
"window.newWindowDimensions": "default",
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
||||
|
||||
// On macOS, use AppleScript to quit VS Code gracefully
|
||||
if (process.platform === "darwin") {
|
||||
try {
|
||||
// First try AppleScript to quit VS Code gracefully
|
||||
await execa("osascript", ["-e", 'tell application "Visual Studio Code" to quit'])
|
||||
|
||||
// Wait a moment for VS Code to close
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
} catch (appleScriptError) {
|
||||
console.warn(`Error using AppleScript to quit VS Code: ${appleScriptError}`)
|
||||
}
|
||||
} else if (process.platform === "win32") {
|
||||
// On Windows, try to use taskkill without /F first
|
||||
try {
|
||||
await execa("taskkill", ["/IM", "code.exe"])
|
||||
|
||||
// Wait a moment for VS Code to close
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
} catch (taskkillError) {
|
||||
console.warn(`Error using taskkill to quit VS Code: ${taskkillError}`)
|
||||
}
|
||||
} else {
|
||||
// On Linux, try to use SIGTERM first
|
||||
try {
|
||||
// Find VS Code processes
|
||||
const { stdout } = await execa("ps", ["aux"])
|
||||
const lines = stdout.split("\n")
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes(resources.tempUserDataDir)) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
const pid = parseInt(parts[1])
|
||||
|
||||
if (pid && !isNaN(pid)) {
|
||||
console.log(`Sending SIGTERM to VS Code process with PID: ${pid}`)
|
||||
try {
|
||||
// Use SIGTERM instead of SIGKILL for a graceful shutdown
|
||||
process.kill(pid, "SIGTERM")
|
||||
} catch (killError) {
|
||||
console.warn(`Failed to terminate process ${pid}: ${killError}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a moment for VS Code to close
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
} catch (psError) {
|
||||
console.warn(`Error listing processes: ${psError}`)
|
||||
}
|
||||
}
|
||||
|
||||
// If graceful methods failed, fall back to forceful termination as a last resort
|
||||
// Check if VS Code is still running with the temp user data dir
|
||||
let vsCodeStillRunning = false
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
const { stdout } = await execa("ps", ["aux"])
|
||||
vsCodeStillRunning = stdout.split("\n").some((line) => line.includes(resources.tempUserDataDir))
|
||||
} catch (error) {
|
||||
console.warn(`Error checking if VS Code is still running: ${error}`)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { stdout } = await execa("tasklist", ["/FI", `IMAGENAME eq code.exe`])
|
||||
vsCodeStillRunning = stdout.includes("code.exe")
|
||||
} catch (error) {
|
||||
console.warn(`Error checking if VS Code is still running: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// If VS Code is still running, use forceful termination as a last resort
|
||||
if (vsCodeStillRunning) {
|
||||
console.log("Graceful shutdown failed, falling back to forceful termination...")
|
||||
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
await execa("taskkill", ["/IM", "code.exe", "/F"])
|
||||
} catch (error) {
|
||||
console.warn(`Error forcefully terminating VS Code: ${error}`)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { stdout } = await execa("ps", ["aux"])
|
||||
const lines = stdout.split("\n")
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes(resources.tempUserDataDir)) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
const pid = parseInt(parts[1])
|
||||
|
||||
if (pid && !isNaN(pid)) {
|
||||
console.log(`Forcefully killing VS Code process with PID: ${pid}`)
|
||||
try {
|
||||
process.kill(pid, "SIGKILL")
|
||||
} catch (killError) {
|
||||
console.warn(`Failed to kill process ${pid}: ${killError}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error forcefully terminating VS Code: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error closing VS Code: ${error}`)
|
||||
}
|
||||
|
||||
// Clean up temporary directories and evals.env file
|
||||
try {
|
||||
console.log(`Removing temporary user data directory: ${resources.tempUserDataDir}`)
|
||||
fs.rmSync(resources.tempUserDataDir, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
console.warn(`Error removing temporary user data directory: ${error}`)
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Removing temporary extensions directory: ${resources.tempExtensionsDir}`)
|
||||
fs.rmSync(resources.tempExtensionsDir, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
console.warn(`Error removing temporary extensions directory: ${error}`)
|
||||
}
|
||||
|
||||
// Remove the evals.env file
|
||||
try {
|
||||
const evalsEnvPath = path.join(workspacePath, "evals.env")
|
||||
if (fs.existsSync(evalsEnvPath)) {
|
||||
console.log(`Removing evals.env file: ${evalsEnvPath}`)
|
||||
fs.unlinkSync(evalsEnvPath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error removing evals.env file: ${error}`)
|
||||
}
|
||||
|
||||
// Remove from the global map
|
||||
workspaceResources.delete(workspacePath)
|
||||
|
||||
console.log("Cleanup completed")
|
||||
}
|
||||
Generated
+1694
-59
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -20,7 +20,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.0",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"chalk": "5.6.2",
|
||||
"dotenv": "^16.5.0",
|
||||
"commander": "^9.4.1",
|
||||
@@ -40,5 +40,8 @@
|
||||
"@types/yargs": "^17.0.19",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"overrides": {
|
||||
"tar-fs": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
-321
@@ -1,321 +0,0 @@
|
||||
---
|
||||
title: "Hooks System MVP - Phase 1"
|
||||
description: "Technical specification for Phase 1 hooks implementation with protobuf-based interfaces"
|
||||
date: 2025-09-09
|
||||
draft: false
|
||||
---
|
||||
|
||||
# Hooks System MVP - Phase 1
|
||||
|
||||
This page documents the minimum viable product (MVP) implementation for Cline's hooks system, focusing on the seven Phase 1 hooks identified by client requirements. Each hook provides standardized input/output interfaces using protobuf-based data structures for consistency with Cline's existing gRPC architecture.
|
||||
|
||||
## Phase 1 Hook Overview
|
||||
|
||||
The MVP focuses on essential lifecycle and tool execution hooks that provide the highest value for automation and integration workflows:
|
||||
|
||||
| Hook Name | Category | Trigger Point | Implementation Hours |
|
||||
|-----------|----------|---------------|---------------------|
|
||||
| `PreToolUse` | Tool Execution | Before any tool execution | 8-12 hours |
|
||||
| `PostToolUse` | Tool Execution | After successful tool execution | 6-10 hours |
|
||||
| `UserPromptSubmit` | User Interaction | When user submits a message | 4-6 hours |
|
||||
| `TaskStart` | Task Lifecycle | When a new task begins | 6-8 hours |
|
||||
| `TaskResume` | Task Lifecycle | When resuming an existing task | 8-10 hours |
|
||||
| `TaskCancel` | Task Lifecycle | User cancels task | 4-6 hours |
|
||||
| `TaskComplete` | Task Lifecycle | When attempt_completion succeeds | 4-6 hours |
|
||||
| `PreCompact` | System Events | Before context compaction | 10-14 hours |
|
||||
|
||||
**Total estimated effort: 46-66 hours**
|
||||
|
||||
## Addressing Amazon's Requirements:
|
||||
|
||||
| Req | Judgement |
|
||||
|--|--|
|
||||
| Hooks that inject context should support blocking/synchronous behavior with timeouts | all hooks blocking & timeout should be implemented by hook |
|
||||
| Hooks that do not inject context can run Asynchronously | up to hook: start a background process & return no changes to context |
|
||||
| Hook failures should be communicated clearly to the user and logged | supported: error field in hook return |
|
||||
| Hooks should support both parallel and sequential execution to minimize latency | sequential only, single hook entrypoint only, up to implementers |
|
||||
| Hooks should support both synchronous and asynchronous execution | up to hook: start a background process & return no changes to context |
|
||||
| Hooks should support a timeout in order to not block the agent if failing | up to hook implementation |
|
||||
| Configuration should be simple and flexible | same as git hooks |
|
||||
| Hooks should have access to relevant context about the triggering event | included in spec |
|
||||
| Context retention should be configurable - some hooks need persistent context, others should avoid consuming context window | we support persistent context only, use a subagent (cline-cli) in hook |
|
||||
| Hook actions should be instrumented, and observable to see exactly what hooks are doing to help debug / iterate. | up to hook implementation |
|
||||
|
||||
| Req | Judgement |
|
||||
|--|--|
|
||||
| Configuration Format | Git hooks style instead of claude style |
|
||||
| Context scope | Support global hooks in `~/.cline` and folder level hooks at `MyRepo/.clinerules` |
|
||||
| Multiple hooks | Single entry point executable, manage multiple hooks however you want |
|
||||
| Async vs sync | We only support sync & permanent context |
|
||||
| Error handling | We support returning errors from hooks |
|
||||
| Telemetry | Up to hook implementation |
|
||||
| Toggling hooks | like git hooks, use `chmod` to change executable bit |
|
||||
|
||||
## Data Structures
|
||||
|
||||
|
||||
### Hook Directory Structure
|
||||
|
||||
Implemented the same way as git hooks: a single entry point that can be any executable. Toggling hooks is done via `chmod +x` or `-x`
|
||||
|
||||
```
|
||||
.clinerules/ (or .cline)
|
||||
├── hooks/
|
||||
│ ├── TaskStart*
|
||||
│ ├── TaskComplete*
|
||||
│ ├── PreFileWrite*
|
||||
│ ├── PostFileWrite*
|
||||
│ └── ...
|
||||
└── logs/
|
||||
├── TaskStart.log
|
||||
└── ...
|
||||
```
|
||||
|
||||
All hooks use protobuf-based data structures converted to JSON for consistency with Cline's gRPC architecture:
|
||||
|
||||
### Base Hook Input
|
||||
```protobuf
|
||||
message HookInput {
|
||||
string hook_name = 1;
|
||||
string timestamp = 2;
|
||||
string task_id = 3;
|
||||
repeated string workspace_roots = 4;
|
||||
string user_id = 5;
|
||||
oneof data {
|
||||
PreToolUseData pre_tool_use = 10;
|
||||
PostToolUseData post_tool_use = 11;
|
||||
UserPromptSubmitData user_prompt_submit = 12;
|
||||
TaskStartData task_start = 13;
|
||||
TaskResumeData task_resume = 14;
|
||||
TaskCancelData task_complete = 15;
|
||||
TaskCompleteData task_complete = 16;
|
||||
PreCompactData pre_compact = 17;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Base Hook Output
|
||||
```protobuf
|
||||
message HookOutput {
|
||||
string context_modification = 1;
|
||||
bool should_continue = 2;
|
||||
string error_message = 3;
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Specifications
|
||||
|
||||
### PreToolUse Hook
|
||||
|
||||
**Trigger:** Before any tool execution
|
||||
**Purpose:** Validation, permission checks, parameter modification
|
||||
|
||||
**Input Data:**
|
||||
```protobuf
|
||||
message PreToolUseData {
|
||||
string tool_name = 1;
|
||||
map<string, string> parameters = 2;
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Validate tool parameters before execution
|
||||
- Implement custom permission checks
|
||||
- Log tool usage for audit trails
|
||||
- Modify parameters based on workspace context
|
||||
- Block dangerous operations in production environments
|
||||
|
||||
**Implementation Notes:**
|
||||
- Hook can prevent tool execution by setting `should_continue = false`
|
||||
- Context modifications can add warnings or guidance to the AI
|
||||
- Parameter validation should be comprehensive but fast
|
||||
|
||||
---
|
||||
|
||||
### PostToolUse Hook
|
||||
|
||||
**Trigger:** After successful tool execution
|
||||
**Purpose:** Logging, backup creation, result processing
|
||||
|
||||
**Input Data:**
|
||||
```protobuf
|
||||
message PostToolUseData {
|
||||
string tool_name = 1;
|
||||
map<string, string> parameters = 2;
|
||||
string result = 3;
|
||||
bool success = 4;
|
||||
int64 execution_time_ms = 5;
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Create automatic backups after file modifications
|
||||
- Log successful operations for debugging
|
||||
- Trigger downstream automation workflows
|
||||
- Update external systems with operation results
|
||||
- Generate metrics and performance data
|
||||
|
||||
**Implementation Notes:**
|
||||
- Hook receives full tool execution context
|
||||
- Can add context about operation success/failure
|
||||
- Should handle errors gracefully to avoid breaking workflows
|
||||
|
||||
---
|
||||
|
||||
### UserPromptSubmit Hook
|
||||
|
||||
**Trigger:** When user submits a message
|
||||
**Purpose:** Input validation, preprocessing, context enhancement
|
||||
|
||||
**Input Data:**
|
||||
```protobuf
|
||||
message UserPromptSubmitData {
|
||||
string prompt = 1;
|
||||
repeated string attachments = 2;
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Validate user input for security concerns
|
||||
- Preprocess prompts to add context or formatting
|
||||
- Log user interactions for analysis
|
||||
- Implement custom prompt templates
|
||||
- Add workspace-specific context automatically
|
||||
|
||||
**Implementation Notes:**
|
||||
- Can modify user prompt before AI processing
|
||||
- Should preserve user intent while enhancing context
|
||||
- Fast execution critical for user experience
|
||||
|
||||
---
|
||||
|
||||
### TaskStart Hook
|
||||
|
||||
**Trigger:** When a new task begins
|
||||
**Purpose:** Initialize logging, setup workspace, prepare environment
|
||||
|
||||
**Input Data:**
|
||||
```protobuf
|
||||
message TaskStartData {
|
||||
map<string, string> task_metadata = 1;
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Initialize task-specific logging systems
|
||||
- Set up workspace environment variables
|
||||
- Create task directories and scaffolding
|
||||
- Notify external systems of new task
|
||||
- Load task-specific configuration
|
||||
|
||||
**Implementation Notes:**
|
||||
- First hook called in task lifecycle
|
||||
- Can set up persistent context for entire task
|
||||
- Should handle workspace initialization robustly
|
||||
|
||||
---
|
||||
|
||||
### TaskResume Hook
|
||||
|
||||
**Trigger:** When resuming an existing task
|
||||
**Purpose:** Restore context, validate state, prepare for continuation
|
||||
|
||||
**Input Data:**
|
||||
```protobuf
|
||||
message TaskResumeData {
|
||||
map<string, string> task_metadata = 1;
|
||||
map<string, string> previous_state = 2;
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Restore workspace state from previous session
|
||||
- Validate that environment is ready for continuation
|
||||
- Load cached data or intermediate results
|
||||
- Notify team members of task resumption
|
||||
- Reconcile changes made outside of Cline
|
||||
|
||||
**Implementation Notes:**
|
||||
- More complex than TaskStart due to state restoration
|
||||
- Should validate workspace consistency
|
||||
- Can provide context about what changed since last session
|
||||
|
||||
---
|
||||
|
||||
### TaskCancel Hook
|
||||
|
||||
**Trigger:** When user cancels the task manually
|
||||
**Purpose:** Cleanup, notifications, metrics collection
|
||||
|
||||
**Input Data:**
|
||||
```protobuf
|
||||
message TaskCancelData {
|
||||
map<string, string> task_metadata = 1;
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Clean up temporary files and resources
|
||||
- Send completion notifications to stakeholders
|
||||
- Generate task completion reports
|
||||
- Update project management systems
|
||||
- Archive task artifacts
|
||||
|
||||
**Implementation Notes:**
|
||||
- Final hook in successful task lifecycle
|
||||
- Should handle cleanup even if other operations fail
|
||||
- Can provide summary context about task completion
|
||||
|
||||
---
|
||||
|
||||
### TaskComplete Hook
|
||||
|
||||
**Trigger:** When attempt_completion succeeds
|
||||
**Purpose:** Cleanup, notifications, metrics collection
|
||||
|
||||
**Input Data:**
|
||||
```protobuf
|
||||
message TaskCompleteData {
|
||||
map<string, string> task_metadata = 1;
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Clean up temporary files and resources
|
||||
- Send completion notifications to stakeholders
|
||||
- Generate task completion reports
|
||||
- Update project management systems
|
||||
- Archive task artifacts
|
||||
|
||||
**Implementation Notes:**
|
||||
- Final hook in successful task lifecycle
|
||||
- Should handle cleanup even if other operations fail
|
||||
- Can provide summary context about task completion
|
||||
|
||||
---
|
||||
|
||||
### PreCompact Hook
|
||||
|
||||
**Trigger:** Before context compaction occurs
|
||||
**Purpose:** Archive conversation history, preserve important context
|
||||
|
||||
**Input Data:**
|
||||
```protobuf
|
||||
message PreCompactData {
|
||||
int64 context_size = 1;
|
||||
int32 messages_to_compact = 2;
|
||||
string compaction_strategy = 3;
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Archive full conversation history before compaction
|
||||
- Extract and preserve critical information
|
||||
- Generate summaries of compacted content
|
||||
- Update external knowledge bases
|
||||
- Implement custom compaction strategies
|
||||
|
||||
**Implementation Notes:**
|
||||
- Most complex hook due to context management requirements
|
||||
- Should execute quickly to avoid delaying AI responses
|
||||
- Can influence compaction strategy through context modifications
|
||||
Generated
+1203
-38
File diff suppressed because it is too large
Load Diff
+7
-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.33.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",
|
||||
@@ -319,7 +320,8 @@
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc -b --noEmit",
|
||||
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && buf lint",
|
||||
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
|
||||
"lint:proto": "bash ./scripts/proto-lint.sh",
|
||||
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
|
||||
"format:fix": "biome check --changed --no-errors-on-unmatched --files-ignore-unknown=true --write",
|
||||
"fix:all": "biome check --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe",
|
||||
@@ -437,6 +439,7 @@
|
||||
"@sap-ai-sdk/orchestration": "^1.17.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
@@ -471,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",
|
||||
@@ -481,6 +485,7 @@
|
||||
"serialize-error": "^11.0.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"ts-morph": "^25.0.1",
|
||||
"turndown": "^7.2.0",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// Service for account-related operations
|
||||
service AccountService {
|
||||
@@ -12,20 +14,18 @@ service AccountService {
|
||||
// Generates a secure nonce for state validation, stores it in secrets,
|
||||
// and opens the authentication URL in the external browser.
|
||||
rpc accountLoginClicked(EmptyRequest) returns (String);
|
||||
|
||||
|
||||
// Handles the user clicking the logout button in the UI.
|
||||
// Clears API keys and user state.
|
||||
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
|
||||
|
||||
// Subscribe to auth status update events (when authentication state changes)
|
||||
rpc subscribeToAuthStatusUpdate(EmptyRequest)
|
||||
returns (stream AuthState);
|
||||
|
||||
rpc subscribeToAuthStatusUpdate(EmptyRequest) returns (stream AuthState);
|
||||
|
||||
// Handles authentication state changes from the Firebase context.
|
||||
// Updates the user info in global state and returns the updated value.
|
||||
rpc authStateChanged(AuthStateChangedRequest)
|
||||
returns (AuthState);
|
||||
|
||||
rpc authStateChanged(AuthStateChangedRequest) returns (AuthState);
|
||||
|
||||
// Fetches all user credits data
|
||||
// (balance, usage transactions, payment transactions)
|
||||
rpc getUserCredits(EmptyRequest) returns (UserCreditsData);
|
||||
@@ -40,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);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
service BrowserService {
|
||||
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
service CheckpointsService {
|
||||
rpc checkpointDiff(Int64Request) returns (Empty);
|
||||
@@ -31,13 +33,13 @@ message CheckpointEvent {
|
||||
CHECKPOINT_COMMIT = 1;
|
||||
CHECKPOINT_RESTORE = 2;
|
||||
}
|
||||
|
||||
|
||||
OperationType operation = 1;
|
||||
string cwd_hash = 2;
|
||||
bool is_active = 3;
|
||||
google.protobuf.Timestamp timestamp = 4;
|
||||
optional string task_id = 5;
|
||||
optional string commit_hash = 6;
|
||||
optional string commit_hash = 6;
|
||||
}
|
||||
|
||||
message PathHashMap {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Service for running IDE commands, for example context menu actions,
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// Service for running IDE commands, for example context menu actions,
|
||||
// commands, etc.
|
||||
// In contrast to the rest of the ProtoBus services, these are
|
||||
// intended to be called by the IDE directly instead of through the webview,
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
message Metadata {
|
||||
}
|
||||
message Metadata {}
|
||||
|
||||
message EmptyRequest {
|
||||
}
|
||||
message EmptyRequest {}
|
||||
|
||||
message Empty {
|
||||
}
|
||||
message Empty {}
|
||||
|
||||
message StringRequest {
|
||||
string value = 2;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
service DictationService {
|
||||
rpc startRecording(EmptyRequest) returns (RecordingResult);
|
||||
|
||||
+36
-34
@@ -1,10 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// Service for file-related operations
|
||||
service FileService {
|
||||
@@ -13,10 +15,10 @@ service FileService {
|
||||
|
||||
// Opens a file in the editor
|
||||
rpc openFile(StringRequest) returns (Empty);
|
||||
|
||||
|
||||
// Opens an image in the system viewer
|
||||
rpc openImage(StringRequest) returns (Empty);
|
||||
|
||||
|
||||
// Opens a mention (file, path, git commit, problem, terminal, or URL)
|
||||
rpc openMention(StringRequest) returns (Empty);
|
||||
|
||||
@@ -25,34 +27,34 @@ service FileService {
|
||||
|
||||
// Creates a rule file from either global or workspace rules directory
|
||||
rpc createRuleFile(RuleFileRequest) returns (RuleFile);
|
||||
|
||||
|
||||
// Search git commits in the workspace
|
||||
rpc searchCommits(StringRequest) returns (GitCommits);
|
||||
|
||||
// Select images and other files from the file system and returns as data URLs & paths respectively
|
||||
rpc selectFiles(BooleanRequest) returns (StringArrays);
|
||||
|
||||
|
||||
// Convert URIs to workspace-relative paths
|
||||
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
|
||||
|
||||
// Search for files in the workspace with fuzzy matching
|
||||
rpc searchFiles(FileSearchRequest) returns (FileSearchResults);
|
||||
|
||||
|
||||
// Toggle a Cline rule (enable or disable)
|
||||
rpc toggleClineRule(ToggleClineRuleRequest) returns (ToggleClineRules);
|
||||
|
||||
// Toggle a Cursor rule (enable or disable)
|
||||
rpc toggleCursorRule(ToggleCursorRuleRequest) returns (ClineRulesToggles);
|
||||
|
||||
|
||||
// Toggle a Windsurf rule (enable or disable)
|
||||
rpc toggleWindsurfRule(ToggleWindsurfRuleRequest) returns (ClineRulesToggles);
|
||||
|
||||
|
||||
// Refreshes all rule toggles (Cline, External, and Workflows)
|
||||
rpc refreshRules(EmptyRequest) returns (RefreshedRules);
|
||||
|
||||
// Opens a task's conversation history file on disk
|
||||
rpc openDiskConversationHistory(StringRequest) returns (Empty);
|
||||
|
||||
|
||||
// Toggles a workflow on or off
|
||||
rpc toggleWorkflow(ToggleWorkflowRequest) returns (ClineRulesToggles);
|
||||
|
||||
@@ -61,7 +63,7 @@ service FileService {
|
||||
|
||||
// Open a file in editor by a relative path
|
||||
rpc openFileRelativePath(StringRequest) returns (Empty);
|
||||
|
||||
|
||||
// Opens or creates a focus chain checklist markdown file for editing
|
||||
rpc openFocusChainFile(StringRequest) returns (Empty);
|
||||
}
|
||||
@@ -79,8 +81,8 @@ message RefreshedRules {
|
||||
// Request to toggle a Windsurf rule
|
||||
message ToggleWindsurfRuleRequest {
|
||||
Metadata metadata = 1;
|
||||
string rule_path = 2; // Path to the rule file
|
||||
bool enabled = 3; // Whether to enable or disable the rule
|
||||
string rule_path = 2; // Path to the rule file
|
||||
bool enabled = 3; // Whether to enable or disable the rule
|
||||
}
|
||||
|
||||
// Request to convert a list of URIs to relative paths
|
||||
@@ -103,25 +105,25 @@ enum FileSearchType {
|
||||
// Request for file search operations
|
||||
message FileSearchRequest {
|
||||
Metadata metadata = 1;
|
||||
string query = 2; // Search query string
|
||||
optional string mentions_request_id = 3; // Optional request ID for tracking requests
|
||||
optional int32 limit = 4; // Optional limit for results (default: 20)
|
||||
optional FileSearchType selected_type = 5; // Optional selected type filter
|
||||
optional string workspace_hint = 6; // Optional workspace name to search in
|
||||
string query = 2; // Search query string
|
||||
optional string mentions_request_id = 3; // Optional request ID for tracking requests
|
||||
optional int32 limit = 4; // Optional limit for results (default: 20)
|
||||
optional FileSearchType selected_type = 5; // Optional selected type filter
|
||||
optional string workspace_hint = 6; // Optional workspace name to search in
|
||||
}
|
||||
|
||||
// Result for file search operations
|
||||
message FileSearchResults {
|
||||
repeated FileInfo results = 1; // Array of file/folder results
|
||||
optional string mentions_request_id = 2; // Echo of the request ID for tracking
|
||||
repeated FileInfo results = 1; // Array of file/folder results
|
||||
optional string mentions_request_id = 2; // Echo of the request ID for tracking
|
||||
}
|
||||
|
||||
// File information structure for search results
|
||||
message FileInfo {
|
||||
string path = 1; // Relative path from workspace root
|
||||
string type = 2; // "file" or "folder"
|
||||
optional string label = 3; // Display name (usually basename)
|
||||
optional string workspace_name = 4; // Workspace this result came from
|
||||
string path = 1; // Relative path from workspace root
|
||||
string type = 2; // "file" or "folder"
|
||||
optional string label = 3; // Display name (usually basename)
|
||||
optional string workspace_name = 4; // Workspace this result came from
|
||||
}
|
||||
|
||||
// Response for searchCommits
|
||||
@@ -141,25 +143,25 @@ message GitCommit {
|
||||
// Unified request for all rule file operations
|
||||
message RuleFileRequest {
|
||||
Metadata metadata = 1;
|
||||
bool is_global = 2; // Common field for all operations
|
||||
bool is_global = 2; // Common field for all operations
|
||||
optional string rule_path = 3; // Path field for deleteRuleFile (optional)
|
||||
optional string filename = 4; // Filename field for createRuleFile (optional)
|
||||
optional string type = 5; // Type of the file to create (optional)
|
||||
optional string filename = 4; // Filename field for createRuleFile (optional)
|
||||
optional string type = 5; // Type of the file to create (optional)
|
||||
}
|
||||
|
||||
// Result for rule file operations with meaningful data only
|
||||
message RuleFile {
|
||||
string file_path = 1; // Path to the rule file
|
||||
string display_name = 2; // Filename for display purposes
|
||||
bool already_exists = 3; // For createRuleFile, indicates if file already existed
|
||||
string file_path = 1; // Path to the rule file
|
||||
string display_name = 2; // Filename for display purposes
|
||||
bool already_exists = 3; // For createRuleFile, indicates if file already existed
|
||||
}
|
||||
|
||||
// Request to toggle a Cline rule
|
||||
message ToggleClineRuleRequest {
|
||||
Metadata metadata = 1;
|
||||
bool is_global = 2; // Whether this is a global rule or workspace rule
|
||||
string rule_path = 3; // Path to the rule file
|
||||
bool enabled = 4; // Whether to enable or disable the rule
|
||||
bool is_global = 2; // Whether this is a global rule or workspace rule
|
||||
string rule_path = 3; // Path to the rule file
|
||||
bool enabled = 4; // Whether to enable or disable the rule
|
||||
}
|
||||
|
||||
// Maps from filepath to enabled/disabled status, matching app's ClineRulesToggles type
|
||||
@@ -176,8 +178,8 @@ message ToggleClineRules {
|
||||
// Request to toggle a Cursor rule
|
||||
message ToggleCursorRuleRequest {
|
||||
Metadata metadata = 1;
|
||||
string rule_path = 2; // Path to the rule file
|
||||
bool enabled = 3; // Whether to enable or disable the rule
|
||||
string rule_path = 2; // Path to the rule file
|
||||
bool enabled = 3; // Whether to enable or disable the rule
|
||||
}
|
||||
|
||||
// Request to toggle a workflow on or off
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// Input message for all hooks
|
||||
message HookInput {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
service McpService {
|
||||
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
|
||||
@@ -16,11 +18,11 @@ service McpService {
|
||||
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
|
||||
rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog);
|
||||
rpc openMcpSettings(EmptyRequest) returns (Empty);
|
||||
|
||||
|
||||
// Subscribe to MCP marketplace catalog updates
|
||||
rpc subscribeToMcpMarketplaceCatalog(EmptyRequest) returns (stream McpMarketplaceCatalog);
|
||||
rpc getLatestMcpServers(Empty) returns (McpServers);
|
||||
|
||||
|
||||
// Subscribe to MCP server updates
|
||||
rpc subscribeToMcpServers(EmptyRequest) returns (stream McpServers);
|
||||
}
|
||||
@@ -72,7 +74,7 @@ message McpResourceTemplate {
|
||||
}
|
||||
|
||||
enum McpServerStatus {
|
||||
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
|
||||
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
|
||||
// To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
|
||||
MCP_SERVER_STATUS_DISCONNECTED = 0; // default
|
||||
MCP_SERVER_STATUS_CONNECTED = 1;
|
||||
|
||||
+190
-19
@@ -1,11 +1,13 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
import "google/protobuf/field_mask.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// Service for model-related operations
|
||||
service ModelsService {
|
||||
@@ -16,25 +18,27 @@ service ModelsService {
|
||||
// Fetches available models from VS Code LM API
|
||||
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
|
||||
// Refreshes and returns OpenRouter models
|
||||
rpc refreshOpenRouterModelsRPC(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
rpc refreshOpenRouterModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Hugging Face models
|
||||
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns OpenAI models
|
||||
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
|
||||
// Refreshes and returns Vercel AI Gateway models
|
||||
rpc refreshVercelAiGatewayModelsRPC(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
rpc refreshVercelAiGatewayModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Requesty models
|
||||
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
|
||||
rpc refreshGroqModelsRPC(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
rpc refreshGroqModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Baseten models
|
||||
rpc refreshBasetenModelsRPC(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
rpc refreshBasetenModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Fetches available models from SAP AI Core
|
||||
rpc getSapAiCoreModels(SapAiCoreModelsRequest) returns (SapAiCoreModelsResponse);
|
||||
// Fetches available models from OCA
|
||||
@@ -56,15 +60,15 @@ message LanguageModelChatSelector {
|
||||
|
||||
// Price tier for tiered pricing models
|
||||
message PriceTier {
|
||||
int64 token_limit = 1; // Upper limit (inclusive) of input tokens for this price
|
||||
double price = 2; // Price per million tokens for this tier
|
||||
int64 token_limit = 1; // Upper limit (inclusive) of input tokens for this price
|
||||
double price = 2; // Price per million tokens for this tier
|
||||
}
|
||||
|
||||
// Thinking configuration for models that support thinking/reasoning
|
||||
message ThinkingConfig {
|
||||
optional int64 max_budget = 1; // Max allowed thinking budget tokens
|
||||
optional double output_price = 2; // Output price per million tokens when budget > 0
|
||||
repeated PriceTier output_price_tiers = 3; // Optional: Tiered output price when budget > 0
|
||||
optional int64 max_budget = 1; // Max allowed thinking budget tokens
|
||||
optional double output_price = 2; // Output price per million tokens when budget > 0
|
||||
repeated PriceTier output_price_tiers = 3; // Optional: Tiered output price when budget > 0
|
||||
}
|
||||
|
||||
// Model tier for tiered pricing structures
|
||||
@@ -90,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
|
||||
@@ -120,35 +125,199 @@ message SapAiCoreModelDeployment {
|
||||
string deployment_id = 2;
|
||||
}
|
||||
|
||||
|
||||
// Response for SAP AI Core models with orchestration availability
|
||||
message SapAiCoreModelsResponse {
|
||||
repeated SapAiCoreModelDeployment deployments = 1;
|
||||
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 {
|
||||
Metadata metadata = 1;
|
||||
|
||||
|
||||
// The API configuration with values to update.
|
||||
// Only fields listed in update_mask will be applied from this configuration.
|
||||
ModelsApiConfiguration api_configuration = 2;
|
||||
|
||||
|
||||
// Mask specifying which top-level fields from api_configuration to update.
|
||||
// Field names should use camelCase (e.g., "apiKey", "planModeApiProvider").
|
||||
// If a field is in the mask but not set in api_configuration, it will be cleared (set to undefined).
|
||||
google.protobuf.FieldMask update_mask = 3;
|
||||
}
|
||||
|
||||
// Model info for OCA (OpenAI-compatible) models exposed by the OCA provider
|
||||
// Model info for OCA (OpenAI-compatible) models exposed by the OCA provider
|
||||
message OcaModelInfo {
|
||||
// Maximum completion tokens per request supported by this model
|
||||
optional int64 max_tokens = 1;
|
||||
@@ -182,7 +351,7 @@ message OcaModelInfo {
|
||||
string model_name = 17;
|
||||
}
|
||||
|
||||
// Aggregated OCA model catalog keyed by model identifier
|
||||
// Aggregated OCA model catalog keyed by model identifier
|
||||
message OcaCompatibleModelInfo {
|
||||
// key: canonical model id as reported by OCA (e.g., "openai/gpt-4o-mini")
|
||||
// value: OcaModelInfo describing that model
|
||||
@@ -228,6 +397,7 @@ enum ApiProvider {
|
||||
QWEN_CODE = 33;
|
||||
DIFY = 34;
|
||||
OCA = 35;
|
||||
MINIMAX = 36;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -345,6 +515,8 @@ message ModelsApiConfiguration {
|
||||
optional string oca_refresh_token = 75;
|
||||
optional string oca_mode = 76;
|
||||
optional bool aws_use_global_inference = 77;
|
||||
optional string minimax_api_key = 78;
|
||||
optional string minimax_api_line = 79;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
@@ -373,7 +545,7 @@ message ModelsApiConfiguration {
|
||||
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 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;
|
||||
@@ -381,7 +553,6 @@ message ModelsApiConfiguration {
|
||||
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;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// Service for account-related operations
|
||||
service OcaAccountService {
|
||||
@@ -12,18 +14,15 @@ service OcaAccountService {
|
||||
// Generates a secure nonce for state validation, stores it in secrets,
|
||||
// and opens the authentication URL in the external browser.
|
||||
rpc ocaAccountLoginClicked(EmptyRequest) returns (String);
|
||||
|
||||
|
||||
// Handles the user clicking the logout button in the UI.
|
||||
// Clears API keys and user state.
|
||||
rpc ocaAccountLogoutClicked(EmptyRequest) returns (Empty);
|
||||
|
||||
// Subscribe to auth status update events (when authentication state changes)
|
||||
rpc ocaSubscribeToAuthStatusUpdate(EmptyRequest)
|
||||
returns (stream OcaAuthState);
|
||||
|
||||
rpc ocaSubscribeToAuthStatusUpdate(EmptyRequest) returns (stream OcaAuthState);
|
||||
}
|
||||
|
||||
|
||||
message OcaAuthState {
|
||||
optional OcaUserInfo user = 1;
|
||||
optional string api_key = 2;
|
||||
@@ -34,4 +33,4 @@ message OcaUserInfo {
|
||||
string uid = 1;
|
||||
optional string display_name = 2;
|
||||
optional string email = 3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// SlashService provides methods for managing slash
|
||||
service SlashService {
|
||||
|
||||
+132
-136
@@ -1,11 +1,13 @@
|
||||
syntax = "proto3";
|
||||
package cline;
|
||||
|
||||
import "cline/browser.proto";
|
||||
import "cline/common.proto";
|
||||
import "cline/models.proto";
|
||||
import "cline/browser.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
@@ -44,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 {
|
||||
@@ -91,130 +90,130 @@ message Secrets {
|
||||
}
|
||||
|
||||
message Settings {
|
||||
optional string aws_region = 1;
|
||||
optional bool aws_use_cross_region_inference = 2;
|
||||
optional bool aws_bedrock_use_prompt_cache = 3;
|
||||
optional string aws_bedrock_endpoint = 4;
|
||||
optional string aws_profile = 5;
|
||||
optional string aws_authentication = 6;
|
||||
optional bool aws_use_profile = 7;
|
||||
optional string vertex_project_id = 8;
|
||||
optional string vertex_region = 9;
|
||||
optional string requesty_base_url = 10;
|
||||
optional string open_ai_base_url = 11;
|
||||
optional string aws_region = 1;
|
||||
optional bool aws_use_cross_region_inference = 2;
|
||||
optional bool aws_bedrock_use_prompt_cache = 3;
|
||||
optional string aws_bedrock_endpoint = 4;
|
||||
optional string aws_profile = 5;
|
||||
optional string aws_authentication = 6;
|
||||
optional bool aws_use_profile = 7;
|
||||
optional string vertex_project_id = 8;
|
||||
optional string vertex_region = 9;
|
||||
optional string requesty_base_url = 10;
|
||||
optional string open_ai_base_url = 11;
|
||||
// map<string, string> open_ai_headers = 12;
|
||||
optional string ollama_base_url = 13;
|
||||
optional string ollama_api_options_ctx_num = 14;
|
||||
optional string lm_studio_base_url = 15;
|
||||
optional string lm_studio_max_tokens = 16;
|
||||
optional string anthropic_base_url = 17;
|
||||
optional string gemini_base_url = 18;
|
||||
optional string azure_api_version = 19;
|
||||
optional string open_router_provider_sorting = 20;
|
||||
optional AutoApprovalSettings auto_approval_settings = 21;
|
||||
optional BrowserSettings browser_settings = 24;
|
||||
optional string lite_llm_base_url = 25;
|
||||
optional bool lite_llm_use_prompt_cache = 26;
|
||||
optional int32 fireworks_model_max_completion_tokens = 27;
|
||||
optional int32 fireworks_model_max_tokens = 28;
|
||||
optional string qwen_api_line = 29;
|
||||
optional string moonshot_api_line = 30;
|
||||
optional string zai_api_line = 31;
|
||||
optional string telemetry_setting = 32;
|
||||
optional string asksage_api_url = 33;
|
||||
optional bool plan_act_separate_models_setting = 34;
|
||||
optional bool enable_checkpoints_setting = 35;
|
||||
optional int32 request_timeout_ms = 36;
|
||||
optional int32 shell_integration_timeout = 37;
|
||||
optional string default_terminal_profile = 38;
|
||||
optional int32 terminal_output_line_limit = 39;
|
||||
optional string sap_ai_core_token_url = 40;
|
||||
optional string sap_ai_core_base_url = 41;
|
||||
optional string sap_ai_resource_group = 42;
|
||||
optional bool sap_ai_core_use_orchestration_mode = 43;
|
||||
optional string claude_code_path = 44;
|
||||
optional string qwen_code_oauth_path = 45;
|
||||
optional bool strict_plan_mode_enabled = 46;
|
||||
optional bool yolo_mode_toggled = 47;
|
||||
optional bool use_auto_condense = 48;
|
||||
optional string preferred_language = 49;
|
||||
optional OpenaiReasoningEffort openai_reasoning_effort = 50;
|
||||
optional PlanActMode mode = 51;
|
||||
optional DictationSettings dictation_settings = 52;
|
||||
optional FocusChainSettings focus_chain_settings = 53;
|
||||
optional string custom_prompt = 54;
|
||||
optional string dify_base_url = 55;
|
||||
optional double auto_condense_threshold = 56;
|
||||
optional string oca_base_url = 57;
|
||||
optional ApiProvider plan_mode_api_provider = 58;
|
||||
optional string plan_mode_api_model_id = 59;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 60;
|
||||
optional string plan_mode_reasoning_effort = 61;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 62;
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 63;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 64;
|
||||
optional string plan_mode_open_router_model_id = 65;
|
||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 66;
|
||||
optional string plan_mode_open_ai_model_id = 67;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 68;
|
||||
optional string plan_mode_ollama_model_id = 69;
|
||||
optional string plan_mode_lm_studio_model_id = 70;
|
||||
optional string plan_mode_lite_llm_model_id = 71;
|
||||
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 72;
|
||||
optional string plan_mode_requesty_model_id = 73;
|
||||
optional OpenRouterModelInfo plan_mode_requesty_model_info = 74;
|
||||
optional string plan_mode_together_model_id = 75;
|
||||
optional string plan_mode_fireworks_model_id = 76;
|
||||
optional string plan_mode_sap_ai_core_model_id = 77;
|
||||
optional string plan_mode_sap_ai_core_deployment_id = 78;
|
||||
optional string plan_mode_groq_model_id = 79;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 80;
|
||||
optional string plan_mode_baseten_model_id = 81;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 82;
|
||||
optional string plan_mode_hugging_face_model_id = 83;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 84;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 85;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 86;
|
||||
optional string plan_mode_oca_model_id = 87;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 88;
|
||||
optional ApiProvider act_mode_api_provider = 89;
|
||||
optional string act_mode_api_model_id = 90;
|
||||
optional int64 act_mode_thinking_budget_tokens = 91;
|
||||
optional string act_mode_reasoning_effort = 92;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 93;
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 94;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 95;
|
||||
optional string act_mode_open_router_model_id = 96;
|
||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 97;
|
||||
optional string act_mode_open_ai_model_id = 98;
|
||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 99;
|
||||
optional string act_mode_ollama_model_id = 100;
|
||||
optional string act_mode_lm_studio_model_id = 101;
|
||||
optional string act_mode_lite_llm_model_id = 102;
|
||||
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 103;
|
||||
optional string act_mode_requesty_model_id = 104;
|
||||
optional OpenRouterModelInfo act_mode_requesty_model_info = 105;
|
||||
optional string act_mode_together_model_id = 106;
|
||||
optional string act_mode_fireworks_model_id = 107;
|
||||
optional string act_mode_sap_ai_core_model_id = 108;
|
||||
optional string act_mode_sap_ai_core_deployment_id = 109;
|
||||
optional string act_mode_groq_model_id = 110;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 111;
|
||||
optional string act_mode_baseten_model_id = 112;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 113;
|
||||
optional string act_mode_hugging_face_model_id = 114;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 115;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 116;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 117;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 118;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 119;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 120;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 121;
|
||||
optional string act_mode_oca_model_id = 122;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 123;
|
||||
optional int32 max_consecutive_mistakes = 124;
|
||||
optional bool subagents_enabled = 125;
|
||||
optional int32 subagent_terminal_output_line_limit = 126;
|
||||
optional string ollama_base_url = 13;
|
||||
optional string ollama_api_options_ctx_num = 14;
|
||||
optional string lm_studio_base_url = 15;
|
||||
optional string lm_studio_max_tokens = 16;
|
||||
optional string anthropic_base_url = 17;
|
||||
optional string gemini_base_url = 18;
|
||||
optional string azure_api_version = 19;
|
||||
optional string open_router_provider_sorting = 20;
|
||||
optional AutoApprovalSettings auto_approval_settings = 21;
|
||||
optional BrowserSettings browser_settings = 24;
|
||||
optional string lite_llm_base_url = 25;
|
||||
optional bool lite_llm_use_prompt_cache = 26;
|
||||
optional int32 fireworks_model_max_completion_tokens = 27;
|
||||
optional int32 fireworks_model_max_tokens = 28;
|
||||
optional string qwen_api_line = 29;
|
||||
optional string moonshot_api_line = 30;
|
||||
optional string zai_api_line = 31;
|
||||
optional string telemetry_setting = 32;
|
||||
optional string asksage_api_url = 33;
|
||||
optional bool plan_act_separate_models_setting = 34;
|
||||
optional bool enable_checkpoints_setting = 35;
|
||||
optional int32 request_timeout_ms = 36;
|
||||
optional int32 shell_integration_timeout = 37;
|
||||
optional string default_terminal_profile = 38;
|
||||
optional int32 terminal_output_line_limit = 39;
|
||||
optional string sap_ai_core_token_url = 40;
|
||||
optional string sap_ai_core_base_url = 41;
|
||||
optional string sap_ai_resource_group = 42;
|
||||
optional bool sap_ai_core_use_orchestration_mode = 43;
|
||||
optional string claude_code_path = 44;
|
||||
optional string qwen_code_oauth_path = 45;
|
||||
optional bool strict_plan_mode_enabled = 46;
|
||||
optional bool yolo_mode_toggled = 47;
|
||||
optional bool use_auto_condense = 48;
|
||||
optional string preferred_language = 49;
|
||||
optional OpenaiReasoningEffort openai_reasoning_effort = 50;
|
||||
optional PlanActMode mode = 51;
|
||||
optional DictationSettings dictation_settings = 52;
|
||||
optional FocusChainSettings focus_chain_settings = 53;
|
||||
optional string custom_prompt = 54;
|
||||
optional string dify_base_url = 55;
|
||||
optional double auto_condense_threshold = 56;
|
||||
optional string oca_base_url = 57;
|
||||
optional ApiProvider plan_mode_api_provider = 58;
|
||||
optional string plan_mode_api_model_id = 59;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 60;
|
||||
optional string plan_mode_reasoning_effort = 61;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 62;
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 63;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 64;
|
||||
optional string plan_mode_open_router_model_id = 65;
|
||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 66;
|
||||
optional string plan_mode_open_ai_model_id = 67;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 68;
|
||||
optional string plan_mode_ollama_model_id = 69;
|
||||
optional string plan_mode_lm_studio_model_id = 70;
|
||||
optional string plan_mode_lite_llm_model_id = 71;
|
||||
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 72;
|
||||
optional string plan_mode_requesty_model_id = 73;
|
||||
optional OpenRouterModelInfo plan_mode_requesty_model_info = 74;
|
||||
optional string plan_mode_together_model_id = 75;
|
||||
optional string plan_mode_fireworks_model_id = 76;
|
||||
optional string plan_mode_sap_ai_core_model_id = 77;
|
||||
optional string plan_mode_sap_ai_core_deployment_id = 78;
|
||||
optional string plan_mode_groq_model_id = 79;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 80;
|
||||
optional string plan_mode_baseten_model_id = 81;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 82;
|
||||
optional string plan_mode_hugging_face_model_id = 83;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 84;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 85;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 86;
|
||||
optional string plan_mode_oca_model_id = 87;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 88;
|
||||
optional ApiProvider act_mode_api_provider = 89;
|
||||
optional string act_mode_api_model_id = 90;
|
||||
optional int64 act_mode_thinking_budget_tokens = 91;
|
||||
optional string act_mode_reasoning_effort = 92;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 93;
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 94;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 95;
|
||||
optional string act_mode_open_router_model_id = 96;
|
||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 97;
|
||||
optional string act_mode_open_ai_model_id = 98;
|
||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 99;
|
||||
optional string act_mode_ollama_model_id = 100;
|
||||
optional string act_mode_lm_studio_model_id = 101;
|
||||
optional string act_mode_lite_llm_model_id = 102;
|
||||
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 103;
|
||||
optional string act_mode_requesty_model_id = 104;
|
||||
optional OpenRouterModelInfo act_mode_requesty_model_info = 105;
|
||||
optional string act_mode_together_model_id = 106;
|
||||
optional string act_mode_fireworks_model_id = 107;
|
||||
optional string act_mode_sap_ai_core_model_id = 108;
|
||||
optional string act_mode_sap_ai_core_deployment_id = 109;
|
||||
optional string act_mode_groq_model_id = 110;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 111;
|
||||
optional string act_mode_baseten_model_id = 112;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 113;
|
||||
optional string act_mode_hugging_face_model_id = 114;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 115;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 116;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 117;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 118;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 119;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 120;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 121;
|
||||
optional string act_mode_oca_model_id = 122;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 123;
|
||||
optional int32 max_consecutive_mistakes = 124;
|
||||
optional bool subagents_enabled = 125;
|
||||
optional int32 subagent_terminal_output_line_limit = 126;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -281,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 {
|
||||
@@ -354,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 {
|
||||
@@ -369,7 +366,6 @@ message UpdateTerminalConnectionTimeoutResponse {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
|
||||
message ProcessInfo {
|
||||
int32 process_id = 1;
|
||||
optional string version = 2;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
import "cline/state.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
service TaskService {
|
||||
// Cancels the currently running task
|
||||
rpc cancelTask(EmptyRequest) returns (Empty);
|
||||
// Cancels the currently running background command
|
||||
rpc cancelBackgroundCommand(EmptyRequest) returns (Empty);
|
||||
// Cancels the currently running hook execution
|
||||
rpc cancelHookExecution(EmptyRequest) returns (Boolean);
|
||||
// Clears the current task
|
||||
rpc clearTask(EmptyRequest) returns (Empty);
|
||||
// Gets the total size of all tasks
|
||||
@@ -104,7 +104,7 @@ message TaskItem {
|
||||
// Request for ask response operation
|
||||
message AskResponseRequest {
|
||||
Metadata metadata = 1;
|
||||
string response_type = 2;
|
||||
string response_type = 2;
|
||||
string text = 3;
|
||||
repeated string images = 4;
|
||||
repeated string files = 5;
|
||||
|
||||
+25
-24
@@ -1,10 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// Enum for ClineMessage type
|
||||
enum ClineMessageType {
|
||||
@@ -24,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
|
||||
@@ -198,7 +199,7 @@ message ClineMessage {
|
||||
bool is_operation_outside_workspace = 12;
|
||||
int32 conversation_history_index = 13;
|
||||
ConversationHistoryDeletedRange conversation_history_deleted_range = 14;
|
||||
|
||||
|
||||
// Additional fields for specific ask/say types
|
||||
ClineSayTool say_tool = 15;
|
||||
ClineSayBrowserAction say_browser_action = 16;
|
||||
@@ -214,52 +215,52 @@ message ClineMessage {
|
||||
service UiService {
|
||||
// Scrolls to a specific settings section in the settings view
|
||||
rpc scrollToSettings(StringRequest) returns (KeyValuePair);
|
||||
|
||||
|
||||
// Sets the terminal execution mode (vscodeTerminal or backgroundExec)
|
||||
rpc setTerminalExecutionMode(BooleanRequest) returns (KeyValuePair);
|
||||
|
||||
|
||||
// Marks the current announcement as shown and returns whether an announcement should still be shown
|
||||
rpc onDidShowAnnouncement(EmptyRequest) returns (Boolean);
|
||||
|
||||
|
||||
// Subscribe to addToInput events (when user adds content via context menu)
|
||||
rpc subscribeToAddToInput(EmptyRequest) returns (stream String);
|
||||
|
||||
|
||||
// Subscribe to MCP button clicked events
|
||||
rpc subscribeToMcpButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
|
||||
// Subscribe to history button click events
|
||||
rpc subscribeToHistoryButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
|
||||
// Subscribe to chat button clicked events (when the chat button is clicked in VSCode)
|
||||
rpc subscribeToChatButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
|
||||
// Subscribe to account button click events
|
||||
rpc subscribeToAccountButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
|
||||
// Subscribe to settings button clicked events
|
||||
rpc subscribeToSettingsButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
|
||||
// Subscribe to partial message updates (streaming Cline messages as they're built)
|
||||
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
|
||||
|
||||
|
||||
// Initialize webview when it launches
|
||||
rpc initializeWebview(EmptyRequest) returns (Empty);
|
||||
|
||||
|
||||
// Subscribe to relinquish control events
|
||||
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
|
||||
|
||||
|
||||
// Subscribe to focus chat input events
|
||||
rpc subscribeToFocusChatInput(EmptyRequest) returns (stream Empty);
|
||||
|
||||
|
||||
// Subscribe to webview visibility change events
|
||||
rpc subscribeToDidBecomeVisible(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Returns the HTML for the webview index page. This is only used by external clients, not by the vscode webview.
|
||||
rpc getWebviewHtml(EmptyRequest) returns (String);
|
||||
|
||||
|
||||
// Opens a URL in the default browser
|
||||
rpc openUrl(StringRequest) returns (Empty);
|
||||
|
||||
|
||||
// Opens the Cline walkthrough
|
||||
rpc openWalkthrough(EmptyRequest) returns (Empty);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
service WebService {
|
||||
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
|
||||
|
||||
Binary file not shown.
@@ -1,12 +1,13 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option go_package = "github.com/cline/grpc-go/host";
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/host";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
|
||||
// Provides methods for diff views.
|
||||
service DiffService {
|
||||
// Open the diff view/editor.
|
||||
@@ -54,7 +55,7 @@ message GetDocumentTextRequest {
|
||||
}
|
||||
|
||||
message GetDocumentTextResponse {
|
||||
optional string content = 1;
|
||||
optional string content = 1;
|
||||
}
|
||||
|
||||
message ReplaceTextRequest {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option go_package = "github.com/cline/grpc-go/host";
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/host";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
|
||||
// Provides methods for working with the user's environment.
|
||||
service EnvService {
|
||||
// Writes text to the system clipboard.
|
||||
@@ -19,7 +20,7 @@ service EnvService {
|
||||
rpc getHostVersion(cline.EmptyRequest) returns (GetHostVersionResponse);
|
||||
|
||||
// Returns a URI that will redirect to the host environment.
|
||||
// e.g. vscode://saoudrizwan.claude-dev, idea://, pycharm://, etc.
|
||||
// e.g. vscode://saoudrizwan.claude-dev, idea://, pycharm://, etc.
|
||||
// If the host does not support URIs it should return empty.
|
||||
rpc getIdeRedirectUri(cline.EmptyRequest) returns (cline.String);
|
||||
|
||||
@@ -36,14 +37,14 @@ service EnvService {
|
||||
|
||||
message GetHostVersionResponse {
|
||||
// The name of the host platform, e.g VSCode, IntelliJ Ultimate Edition, etc.
|
||||
optional string platform = 1;
|
||||
optional string platform = 1;
|
||||
// The version of the host platform, e.g. 1.103.0 for VSCode, or 2025.1.1.1 for JetBrains IDEs.
|
||||
optional string version = 2;
|
||||
// The type of the cline host environment, e.g. 'VSCode Extension', 'Cline for JetBrains', 'CLI'
|
||||
// This is different from the platform because there are many JetBrains IDEs, but they all use the same
|
||||
// plugin.
|
||||
optional string cline_type = 3;
|
||||
// The version of the cline host environment, e.g. 33.2.10 for extension, or 1.0.6 for JetBrains.
|
||||
// The version of the cline host environment, e.g. 33.2.10 for extension, or 1.0.6 for JetBrains.
|
||||
optional string cline_version = 4;
|
||||
}
|
||||
|
||||
@@ -57,5 +58,5 @@ message GetTelemetrySettingsResponse {
|
||||
}
|
||||
|
||||
message TelemetrySettingsEvent {
|
||||
Setting is_enabled = 1;
|
||||
Setting is_enabled = 1;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/host";
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
|
||||
// This is for use in integration tests to get the contents of the webview.
|
||||
service TestingService {
|
||||
rpc getWebviewHtml(GetWebviewHtmlRequest) returns (GetWebviewHtmlResponse);
|
||||
}
|
||||
|
||||
message GetWebviewHtmlRequest {
|
||||
}
|
||||
message GetWebviewHtmlRequest {}
|
||||
|
||||
message GetWebviewHtmlResponse {
|
||||
optional string html = 1;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/host";
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
|
||||
// Provides methods for working with IDE windows and editors.
|
||||
service WindowService {
|
||||
@@ -86,7 +87,6 @@ message ShowMessageRequestOptions {
|
||||
repeated string items = 1;
|
||||
optional bool modal = 2;
|
||||
optional string detail = 3;
|
||||
|
||||
}
|
||||
|
||||
message SelectedResponse {
|
||||
|
||||
+13
-12
@@ -1,18 +1,19 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option go_package = "github.com/cline/grpc-go/host";
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/host";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
|
||||
// Provides methods for working with workspaces/projects.
|
||||
service WorkspaceService {
|
||||
// Returns a list of the top level directories of the workspace.
|
||||
rpc getWorkspacePaths(GetWorkspacePathsRequest) returns (GetWorkspacePathsResponse);
|
||||
|
||||
// Saves an open document if it's open in the editor and has unsaved changes.
|
||||
// Saves an open document if it's open in the editor and has unsaved changes.
|
||||
// Returns true if the document was saved, returns false if the document was not found, or did not
|
||||
// need to be saved.
|
||||
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (SaveOpenDocumentIfDirtyResponse);
|
||||
@@ -24,7 +25,7 @@ service WorkspaceService {
|
||||
rpc openProblemsPanel(OpenProblemsPanelRequest) returns (OpenProblemsPanelResponse);
|
||||
|
||||
// Opens the IDE file explorer panel and selects a file or directory.
|
||||
rpc openInFileExplorerPanel(OpenInFileExplorerPanelRequest) returns (OpenInFileExplorerPanelResponse);
|
||||
rpc openInFileExplorerPanel(OpenInFileExplorerPanelRequest) returns (OpenInFileExplorerPanelResponse);
|
||||
|
||||
// Opens and focuses the Cline sidebar panel in the host IDE.
|
||||
rpc openClineSidebarPanel(OpenClineSidebarPanelRequest) returns (OpenClineSidebarPanelResponse);
|
||||
@@ -53,7 +54,7 @@ message SaveOpenDocumentIfDirtyRequest {
|
||||
optional string file_path = 2;
|
||||
}
|
||||
message SaveOpenDocumentIfDirtyResponse {
|
||||
// Returns true if the document was saved.
|
||||
// Returns true if the document was saved.
|
||||
optional bool was_saved = 1;
|
||||
}
|
||||
|
||||
@@ -67,8 +68,8 @@ message GetDiagnosticsResponse {
|
||||
|
||||
// Request for host-side workspace search (files/folders) used by mentions autocomplete
|
||||
message SearchWorkspaceItemsRequest {
|
||||
string query = 1; // Search query string
|
||||
optional int32 limit = 2; // Optional limit for results (default decided by host)
|
||||
string query = 1; // Search query string
|
||||
optional int32 limit = 2; // Optional limit for results (default decided by host)
|
||||
// Optional selected type filter
|
||||
enum SearchItemType {
|
||||
FILE = 0;
|
||||
@@ -80,9 +81,9 @@ message SearchWorkspaceItemsRequest {
|
||||
// Response for host-side workspace search
|
||||
message SearchWorkspaceItemsResponse {
|
||||
message SearchItem {
|
||||
string path = 1; // Workspace-relative path using platform separators
|
||||
string path = 1; // Workspace-relative path using platform separators
|
||||
SearchWorkspaceItemsRequest.SearchItemType type = 2;
|
||||
optional string label = 3; // Optional display label (e.g., basename)
|
||||
optional string label = 3; // Optional display label (e.g., basename)
|
||||
}
|
||||
repeated SearchItem items = 1;
|
||||
}
|
||||
@@ -100,9 +101,9 @@ message OpenTerminalResponse {}
|
||||
|
||||
// Execute a command in the terminal
|
||||
message ExecuteCommandInTerminalRequest {
|
||||
string command = 1; // The command to execute
|
||||
string command = 1; // The command to execute
|
||||
}
|
||||
|
||||
message ExecuteCommandInTerminalResponse {
|
||||
bool success = 1; // Whether the command was successfully sent to the terminal
|
||||
bool success = 1; // Whether the command was successfully sent to the terminal
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -94,6 +94,7 @@ const ENABLED_PROVIDERS = [
|
||||
"gemini", // Google Gemini
|
||||
"ollama", // Ollama local models
|
||||
"cerebras", // Cerebras models
|
||||
"oca", // Oracle Code Assist
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
|
||||
buf lint
|
||||
|
||||
if ! buf format -w --exit-code; then
|
||||
echo Proto files were formatted
|
||||
fi
|
||||
|
||||
if grep -rn "rpc .*[A-Z][A-Z].*[(]" --include="*.proto"; then
|
||||
# See https://github.com/cline/cline/pull/7054
|
||||
echo Error: Proto RPC names cannot contain repeated capital letters
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-1
@@ -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"
|
||||
@@ -18,6 +19,7 @@ import { HuaweiCloudMaaSHandler } from "./providers/huawei-cloud-maas"
|
||||
import { HuggingFaceHandler } from "./providers/huggingface"
|
||||
import { LiteLlmHandler } from "./providers/litellm"
|
||||
import { LmStudioHandler } from "./providers/lmstudio"
|
||||
import { MinimaxHandler } from "./providers/minimax"
|
||||
import { MistralHandler } from "./providers/mistral"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { NebiusHandler } from "./providers/nebius"
|
||||
@@ -44,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>
|
||||
}
|
||||
@@ -389,6 +391,13 @@ function createHandlerForProvider(
|
||||
: options.actModeOcaModelInfo?.supportsPromptCache,
|
||||
taskId: options.ulid,
|
||||
})
|
||||
case "minimax":
|
||||
return new MinimaxHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
minimaxApiKey: options.minimaxApiKey,
|
||||
minimaxApiLine: options.minimaxApiLine,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
default:
|
||||
return new AnthropicHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
|
||||
@@ -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)) {
|
||||
@@ -184,10 +193,6 @@ export class ClineHandler implements ApiHandler {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
|
||||
if (this.getModel().id === "cline/code-supernova-1-million") {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
if (this.getModel().id === "x-ai/grok-code-fast-1") {
|
||||
totalCost = 0
|
||||
}
|
||||
@@ -198,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",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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
|
||||
minimaxApiLine?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
export class MinimaxHandler implements ApiHandler {
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(private readonly options: MinimaxHandlerOptions) {}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.minimaxApiKey) {
|
||||
throw new Error("MiniMax API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL:
|
||||
this.options.minimaxApiLine === "china" ? "https://api.minimaxi.com/v1" : "https://api.minimax.io/v1",
|
||||
apiKey: this.options.minimaxApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating MiniMax client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
tools?: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: openAiMessages,
|
||||
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) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta.reasoning_content as string | undefined) || "",
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: MinimaxModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
|
||||
if (modelId && modelId in minimaxModels) {
|
||||
const id = modelId as MinimaxModelId
|
||||
return { id, info: minimaxModels[id] }
|
||||
}
|
||||
return { id: minimaxDefaultModelId, info: minimaxModels[minimaxDefaultModelId] }
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user