mirror of
https://github.com/cline/cline.git
synced 2026-09-14 11:29:25 +08:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
527f8593f7 | ||
|
|
2a48bad28c | ||
|
|
c6f4584f7d | ||
|
|
a17b31070f | ||
|
|
cad82d518d | ||
|
|
b4d7ec187f | ||
|
|
42af8414e4 | ||
|
|
8f6b9e8362 | ||
|
|
f1430359db | ||
|
|
932695f70b | ||
|
|
489ee936c2 | ||
|
|
dff7f61175 | ||
|
|
aead42c6b8 | ||
|
|
db50a1c671 | ||
|
|
bb20f60f1d | ||
|
|
a7333b7177 | ||
|
|
f30837a850 | ||
|
|
5660b2513f | ||
|
|
64e7e5fa4c | ||
|
|
b34166e99a | ||
|
|
cd2d8f98a7 | ||
|
|
333468c9b6 | ||
|
|
0da6ddc001 | ||
|
|
612130366f | ||
|
|
85206cfded | ||
|
|
6f8ed7aa56 | ||
|
|
f2c130a69e | ||
|
|
c870bfb454 | ||
|
|
436bfdb535 | ||
|
|
e2a3652d26 | ||
|
|
632aca225c |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add zai-glm-4.7 to Cerebras model list
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Adding support for responses api to OCA provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Exclude files without extensions (and dotfiles) from getDiffSet results if they are binary
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: preserve file endings and trailing newlines across all edit tools
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Cerebras rate limiting by using conservative max_tokens (16K) instead of model maximum.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed Auto Compact not triggering when using Claude Code provider. Short model aliases like "sonnet" and "opus" are now correctly recognized as Claude 4+ models.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Workspace and Favorites history filters to work independently instead of being mutually exclusive
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed connection failures with remote MCP servers that return 404 instead of 405 for SSE stream checks. This was causing "Failed to open SSE stream: Not Found" errors after the v3.46.0 SDK upgrade.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
sap provider - in orchestration mode, use messages_history field instead of messages because of placeholder_values usage templating error
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add bash command permission system to cline
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(prompts): modify prompts for parallel tool usage in claude and gemini 3 models
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Stop automatically opening Cline sidebar on extension update - only show a notification
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Revert #8341 (0d04205dc) due to regressions in diff view/document truncation (see #8423, #8429).
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(vercel-ai-gateway): add model refresh and improve reasoning support
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add supportsReasoning property to Baseten models
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
show cline command permission denials in the CLI
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
improve cline CLI say output
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix regression that broke JSON parsing for SAP AI Core provider in native API mode for claude models
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Baseten model selector issue in model picker modal mode
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Reduce the number of network requests for the users profile
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: Verify selected index is not -1 when checking if an option is selectable in the context menu
|
||||
@@ -0,0 +1,160 @@
|
||||
name: Publish NPM Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish-npm-nightly:
|
||||
needs: test
|
||||
name: Publish Cline CLI (Nightly) to NPM
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for recent commits
|
||||
id: check_commits
|
||||
run: |
|
||||
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Found recent commits, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup Go
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache-dependency-path: cli/go.sum
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Read nightly version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
# Read version from cli/package-nightly.json
|
||||
VERSION=$(node -p "require('./cli/package-nightly.json').version")
|
||||
echo "Nightly version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup cli/package.json for build
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
# Copy nightly package.json to cli/package.json for build
|
||||
cp cli/package-nightly.json cli/package.json
|
||||
echo "Using version ${{ steps.version.outputs.version }} for build"
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run download-ripgrep
|
||||
|
||||
- name: Clean previous builds
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: rm -rf dist-standalone
|
||||
|
||||
- name: Generate Protos (First Pass)
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Compile CLI
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run compile-cli
|
||||
|
||||
- name: Compile CLI for all platforms
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run compile-cli-all-platforms
|
||||
|
||||
- name: Build standalone NPM package
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
OTEL_TELEMETRY_ENABLED: "1"
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_LOGS_EXPORTER: 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 }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Verify build output
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Verifying CLI binaries..."
|
||||
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with nightly tag
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
|
||||
cd dist-standalone
|
||||
npm publish --tag nightly --access public
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline@nightly"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
@@ -29,6 +29,8 @@ coverage-unit
|
||||
|
||||
*evals.env
|
||||
.env
|
||||
.secrets
|
||||
.github/act/.secrets
|
||||
|
||||
.worktrees
|
||||
|
||||
|
||||
+25
-1
@@ -1,12 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
## [3.47.0]
|
||||
|
||||
### Added
|
||||
- Added experimental support for Background Edits (allows editing files in background without opening the diff view)
|
||||
- Updated free model to MiniMax M2.1 (replacing MiniMax M2)
|
||||
- Added support for Azure based identity authentication in OpenAI Compatible provider and Azure OpenAI
|
||||
- Add `supportsReasoning` property to Baseten models
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent expired token usage in authenticated requests
|
||||
- Exclude binary files without extensions from diffs
|
||||
- Preserve file endings and trailing newlines
|
||||
- Fix Cerebras rate limiting
|
||||
- Fix Auto Compact for Claude Code provider
|
||||
- Make Workspace and Favorites history filters independent
|
||||
- Fix remote MCP server connection failures (404 response handling)
|
||||
- Disable native tool calling for Deepseek 3.2 speciale
|
||||
- Show notification instead of opening sidebar on update
|
||||
- Fix Baseten model selector
|
||||
|
||||
### Refactored
|
||||
|
||||
- Modify prompts for parallel tool usage in Claude and Gemini 3 models
|
||||
|
||||
## [3.46.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove GLM 4.6 from free models
|
||||
|
||||
|
||||
## [3.46.0]
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.8-nightly.29",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
"cline": "./bin/cline",
|
||||
"cline-host": "./bin/cline-host"
|
||||
},
|
||||
"man": "./man/cline.1",
|
||||
"scripts": {
|
||||
"postinstall": "node postinstall.js"
|
||||
},
|
||||
"bundleDependencies": [
|
||||
"@grpc/grpc-js",
|
||||
"@grpc/reflection",
|
||||
"better-sqlite3",
|
||||
"grpc-health-check",
|
||||
"open",
|
||||
"vscode-uri"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"claude",
|
||||
"dev",
|
||||
"mcp",
|
||||
"openrouter",
|
||||
"coding",
|
||||
"agent",
|
||||
"autonomous",
|
||||
"chatgpt",
|
||||
"sonnet",
|
||||
"ai",
|
||||
"llama",
|
||||
"cli"
|
||||
],
|
||||
"author": {
|
||||
"name": "Cline Bot Inc."
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline"
|
||||
},
|
||||
"homepage": "https://cline.bot",
|
||||
"bugs": {
|
||||
"url": "https://github.com/cline/cline/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"open": "^10.1.2",
|
||||
"vscode-uri": "^3.1.0"
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.8",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
)
|
||||
|
||||
// HookRenderer renders hook status messages in a CLI-native style.
|
||||
//
|
||||
// Goals:
|
||||
// - Match ToolRenderer’s markdown look
|
||||
// - Keep executions ungrouped
|
||||
// - Render status + high-signal metadata (script paths, error summary)
|
||||
//
|
||||
// Note: hook stdout/stderr currently arrives as separate `hook_output_stream` messages.
|
||||
// The CLI suppresses those by default and prints them only in --verbose mode.
|
||||
// Future work could group streamed output under the corresponding hook block.
|
||||
//
|
||||
// It returns markdown (or rendered markdown when enabled); callers should print the
|
||||
// returned string.
|
||||
|
||||
type HookRenderer struct {
|
||||
mdRenderer *MarkdownRenderer
|
||||
outputFormat string
|
||||
}
|
||||
|
||||
func NewHookRenderer(mdRenderer *MarkdownRenderer, outputFormat string) *HookRenderer {
|
||||
return &HookRenderer{mdRenderer: mdRenderer, outputFormat: outputFormat}
|
||||
}
|
||||
|
||||
func (hr *HookRenderer) RenderHookStatus(h types.HookMessage) string {
|
||||
statusText := strings.TrimSpace(h.Status)
|
||||
if statusText == "" {
|
||||
statusText = "unknown"
|
||||
}
|
||||
|
||||
// Header: aligned with ToolRenderer’s phrasing so transcripts scan consistently.
|
||||
// Example: "### Cline hook completed: PreToolUse (tool: read_file) (exit 0)"
|
||||
var headerBuilder strings.Builder
|
||||
headerBuilder.WriteString(fmt.Sprintf("### Cline hook %s: %s", statusText, h.HookName))
|
||||
if h.ToolName != "" {
|
||||
headerBuilder.WriteString(" ")
|
||||
headerBuilder.WriteString(fmt.Sprintf("(tool: %s)", h.ToolName))
|
||||
}
|
||||
if statusText == "failed" && h.ExitCode != 0 {
|
||||
headerBuilder.WriteString(" ")
|
||||
headerBuilder.WriteString(fmt.Sprintf("(exit %d)", h.ExitCode))
|
||||
}
|
||||
header := headerBuilder.String()
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, header)
|
||||
|
||||
// Pending tool info (PreToolUse): show one high-signal line directly under the header.
|
||||
if h.PendingToolInfo != nil {
|
||||
if pending := hr.formatPendingToolInfo(h.PendingToolInfo); pending != "" {
|
||||
lines = append(lines, fmt.Sprintf("- Pending: %s", pending))
|
||||
}
|
||||
}
|
||||
|
||||
// Script paths: one per line.
|
||||
paths := make([]string, 0, len(h.ScriptPaths))
|
||||
for _, p := range h.ScriptPaths {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
// Fallback when no script paths are provided.
|
||||
lines = append(lines, "- *(no hook scripts found)*")
|
||||
} else {
|
||||
for _, p := range paths {
|
||||
lines = append(lines, fmt.Sprintf("- Running hook: `%s`", p))
|
||||
}
|
||||
}
|
||||
|
||||
// On failure, show a minimal summary (full stderr reserved for verbose).
|
||||
if statusText == "failed" && h.Error != nil {
|
||||
if msg := strings.TrimSpace(h.Error.Message); msg != "" {
|
||||
lines = append(lines, fmt.Sprintf("- Error: %s", msg))
|
||||
}
|
||||
// If we have a specific script path, include it as a hint.
|
||||
if sp := strings.TrimSpace(h.Error.ScriptPath); sp != "" {
|
||||
lines = append(lines, fmt.Sprintf("- Script: `%s`", sp))
|
||||
}
|
||||
}
|
||||
|
||||
markdown := strings.Join(lines, "\n")
|
||||
return hr.renderMarkdown(markdown)
|
||||
}
|
||||
|
||||
func (hr *HookRenderer) formatPendingToolInfo(info *types.ToolInfo) string {
|
||||
if info == nil {
|
||||
return ""
|
||||
}
|
||||
tool := strings.TrimSpace(info.Tool)
|
||||
if tool == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Keep this intentionally compact and readable.
|
||||
// Format: "<tool> <identifier>" where identifier is the most relevant param.
|
||||
var ident string
|
||||
switch {
|
||||
case strings.TrimSpace(info.Path) != "":
|
||||
ident = strings.TrimSpace(info.Path)
|
||||
case strings.TrimSpace(info.Command) != "":
|
||||
ident = strings.TrimSpace(info.Command)
|
||||
case strings.TrimSpace(info.Url) != "":
|
||||
ident = strings.TrimSpace(info.Url)
|
||||
case strings.TrimSpace(info.McpTool) != "" && strings.TrimSpace(info.McpServer) != "":
|
||||
ident = fmt.Sprintf("%s %s", strings.TrimSpace(info.McpServer), strings.TrimSpace(info.McpTool))
|
||||
case strings.TrimSpace(info.ResourceUri) != "":
|
||||
ident = strings.TrimSpace(info.ResourceUri)
|
||||
case strings.TrimSpace(info.Regex) != "":
|
||||
ident = strings.TrimSpace(info.Regex)
|
||||
default:
|
||||
ident = ""
|
||||
}
|
||||
|
||||
if ident != "" {
|
||||
return fmt.Sprintf("%s %s", tool, ident)
|
||||
}
|
||||
return tool
|
||||
}
|
||||
|
||||
func (hr *HookRenderer) renderMarkdown(markdown string) string {
|
||||
// Align with ToolRenderer: in plain mode or non-TTY, return markdown as-is.
|
||||
if hr.outputFormat == "plain" || !isTTY() {
|
||||
return markdown
|
||||
}
|
||||
if hr.mdRenderer == nil {
|
||||
return markdown
|
||||
}
|
||||
rendered, err := hr.mdRenderer.Render(markdown)
|
||||
if err != nil {
|
||||
return markdown
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
)
|
||||
|
||||
func TestHookRenderer_RenderHookStatus_FailedShowsErrorAndScript(t *testing.T) {
|
||||
hr := NewHookRenderer(nil, "plain")
|
||||
|
||||
msg := hr.RenderHookStatus(types.HookMessage{
|
||||
HookName: "PreToolUse",
|
||||
ToolName: "execute_command",
|
||||
Status: "failed",
|
||||
ExitCode: 2,
|
||||
ScriptPaths: []string{"repo/.clinerules/hooks/PreToolUse"},
|
||||
Error: &types.HookError{
|
||||
Message: "boom",
|
||||
ScriptPath: "repo/.clinerules/hooks/PreToolUse",
|
||||
},
|
||||
})
|
||||
|
||||
if !strings.Contains(msg, "### Cline hook failed: PreToolUse") {
|
||||
t.Fatalf("expected header in rendered output, got: %q", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "- Error: boom") {
|
||||
t.Fatalf("expected error line in rendered output, got: %q", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "- Script: `repo/.clinerules/hooks/PreToolUse`") {
|
||||
t.Fatalf("expected script line in rendered output, got: %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHookRenderer_RenderHookStatus_PendingToolInfoAppearsDirectlyUnderHeader(t *testing.T) {
|
||||
hr := NewHookRenderer(nil, "plain")
|
||||
|
||||
msg := hr.RenderHookStatus(types.HookMessage{
|
||||
HookName: "PreToolUse",
|
||||
ToolName: "write_to_file",
|
||||
Status: "running",
|
||||
PendingToolInfo: &types.ToolInfo{
|
||||
Tool: "write_to_file",
|
||||
Path: "src/foo.ts",
|
||||
},
|
||||
ScriptPaths: []string{"repo/.clinerules/hooks/PreToolUse"},
|
||||
})
|
||||
|
||||
header := "### Cline hook running: PreToolUse"
|
||||
pending := "- Pending: write_to_file src/foo.ts"
|
||||
runningHook := "- Running hook: `repo/.clinerules/hooks/PreToolUse`"
|
||||
|
||||
headerIdx := strings.Index(msg, header)
|
||||
if headerIdx == -1 {
|
||||
t.Fatalf("expected header %q in output, got: %q", header, msg)
|
||||
}
|
||||
pendingIdx := strings.Index(msg, pending)
|
||||
if pendingIdx == -1 {
|
||||
t.Fatalf("expected pending line %q in output, got: %q", pending, msg)
|
||||
}
|
||||
runningIdx := strings.Index(msg, runningHook)
|
||||
if runningIdx == -1 {
|
||||
t.Fatalf("expected running hook line %q in output, got: %q", runningHook, msg)
|
||||
}
|
||||
if !(headerIdx < pendingIdx && pendingIdx < runningIdx) {
|
||||
t.Fatalf("expected header < pending < runningHook ordering, got indexes header=%d pending=%d running=%d\nfull=%q", headerIdx, pendingIdx, runningIdx, msg)
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,12 @@ func NewStreamingSegment(sayType, prefix string, mdRenderer *MarkdownRenderer, s
|
||||
// Render rich header immediately when creating segment (if in rich mode and TTY)
|
||||
if shouldMarkdown && outputFormat != "plain" && isTTY() {
|
||||
header := ss.generateRichHeader()
|
||||
rendered, _ := mdRenderer.Render(header)
|
||||
output.Println("")
|
||||
output.Print(rendered)
|
||||
// Skip empty headers.
|
||||
if strings.TrimSpace(header) != "" {
|
||||
rendered, _ := mdRenderer.Render(header)
|
||||
output.Println("")
|
||||
output.Print(rendered)
|
||||
}
|
||||
}
|
||||
|
||||
return ss
|
||||
@@ -110,6 +113,9 @@ func (ss *StreamingSegment) renderFinal(currentBuffer string) {
|
||||
if err := json.Unmarshal([]byte(currentBuffer), &tool); err == nil {
|
||||
bodyContent = ss.toolRenderer.GenerateToolContentBody(&tool)
|
||||
}
|
||||
} else if ss.sayType == string(types.SayTypeHookStatus) {
|
||||
// Hooks are rendered via the state stream; nothing to render here.
|
||||
bodyContent = ""
|
||||
} else if ss.sayType == string(types.SayTypeCommand) {
|
||||
// Command output
|
||||
bodyContent = "```shell\n" + currentBuffer + "\n```"
|
||||
@@ -160,6 +166,10 @@ func (ss *StreamingSegment) generateRichHeader() string {
|
||||
|
||||
case string(types.SayTypeTool):
|
||||
return ss.generateToolHeader()
|
||||
|
||||
case string(types.SayTypeHookStatus):
|
||||
// Hooks are rendered from the state stream; don’t emit a partial-stream header.
|
||||
return ""
|
||||
|
||||
case "ask":
|
||||
// Check the specific ask type
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
)
|
||||
|
||||
func TestStreamingSegment_generateRichHeader_HookIsEmpty(t *testing.T) {
|
||||
ss := &StreamingSegment{
|
||||
sayType: string(types.SayTypeHookStatus),
|
||||
prefix: "HOOK",
|
||||
msg: &types.ClineMessage{},
|
||||
}
|
||||
|
||||
header := ss.generateRichHeader()
|
||||
if header != "" {
|
||||
t.Fatalf("expected empty header for hook segments to avoid double-render, got: %q", header)
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,18 @@ func (s *StreamingDisplay) HandlePartialMessage(msg *types.ClineMessage) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Render hooks from the state stream only (not partial stream) to avoid duplicates.
|
||||
//
|
||||
// Rationale: hook status messages are often updated/reordered by the backend (e.g. PreToolUse
|
||||
// hooks are moved above the corresponding tool message). The state stream represents the
|
||||
// authoritative, “final” message ordering, while the partial stream is best-effort for
|
||||
// incremental display.
|
||||
//
|
||||
// Only suppress *partial* hook messages; complete ones still flow through dedupe.
|
||||
if msg.Partial && msg.Say == string(types.SayTypeHookStatus) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for deduplication
|
||||
if s.dedupe.IsDuplicate(msg) {
|
||||
return nil
|
||||
@@ -91,7 +103,11 @@ func (s *StreamingDisplay) HandlePartialMessage(msg *types.ClineMessage) error {
|
||||
|
||||
func (s *StreamingDisplay) shouldRenderMarkdown(sayType string) bool {
|
||||
switch sayType {
|
||||
case string(types.SayTypeReasoning), string(types.SayTypeText), string(types.SayTypeCompletionResult), string(types.SayTypeTool), "ask":
|
||||
case string(types.SayTypeReasoning),
|
||||
string(types.SayTypeText),
|
||||
string(types.SayTypeCompletionResult),
|
||||
string(types.SayTypeTool),
|
||||
"ask":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -110,6 +126,8 @@ func (s *StreamingDisplay) getPrefix(sayType string) string {
|
||||
return "ASK"
|
||||
case string(types.SayTypeCommand):
|
||||
return "TERMINAL"
|
||||
case string(types.SayTypeHookStatus):
|
||||
return "HOOK"
|
||||
default:
|
||||
return strings.ToUpper(sayType)
|
||||
}
|
||||
|
||||
@@ -264,6 +264,6 @@ func (sr *SystemMessageRenderer) RenderInfo(title, message string) error {
|
||||
func (sr *SystemMessageRenderer) RenderCheckpoint(timestamp string, id int64) error {
|
||||
markdown := fmt.Sprintf("## [%s] Checkpoint created `%d`", timestamp, id)
|
||||
rendered := sr.renderer.RenderMarkdown(markdown)
|
||||
fmt.Printf(rendered)
|
||||
fmt.Print(rendered)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -339,6 +339,13 @@ func (tr *ToolRenderer) RenderCommandOutput(output string) string {
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (tr *ToolRenderer) RenderCommandPermissionDenied(command string) string {
|
||||
command = strings.TrimSpace(command)
|
||||
rendered := tr.renderMarkdown("### Command was denied")
|
||||
message := fmt.Sprintf("Cline does not have permission to execute this command: `%s`", command)
|
||||
return fmt.Sprintf("\n%s\n\n%s\n", rendered, message)
|
||||
}
|
||||
|
||||
// RenderUserResponse renders user approval/rejection feedback
|
||||
func (tr *ToolRenderer) RenderUserResponse(approved bool, feedback string) string {
|
||||
var symbol, status string
|
||||
|
||||
@@ -25,6 +25,7 @@ type DisplayContext struct {
|
||||
State *types.ConversationState
|
||||
Renderer *display.Renderer
|
||||
ToolRenderer *display.ToolRenderer
|
||||
HookRenderer *display.HookRenderer
|
||||
SystemRenderer *display.SystemMessageRenderer
|
||||
IsLast bool
|
||||
IsPartial bool
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/clerror"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
"github.com/cline/cli/pkg/cli/output"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
)
|
||||
|
||||
// SayHandler handles SAY type messages
|
||||
@@ -90,6 +90,12 @@ func (h *SayHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
return h.handleInfo(msg, dc)
|
||||
case string(types.SayTypeTaskProgress):
|
||||
return h.handleTaskProgress(msg, dc)
|
||||
case string(types.SayTypeHookStatus):
|
||||
return h.handleHookStatus(msg, dc)
|
||||
case string(types.SayTypeHookOutputStream):
|
||||
return h.handleHookOutputStream(msg, dc)
|
||||
case string(types.SayTypeCommandPermissionDenied):
|
||||
return h.handleCommandPermissionDenied(msg, dc)
|
||||
default:
|
||||
return h.handleDefault(msg, dc)
|
||||
}
|
||||
@@ -242,18 +248,17 @@ func (h *SayHandler) handleCompletionResult(msg *types.ClineMessage, dc *Display
|
||||
}
|
||||
|
||||
func formatUserMessage(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
// Wrap each line in backticks
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
lines[i] = fmt.Sprintf("`%s`", line)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
// Wrap each line in backticks
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
lines[i] = fmt.Sprintf("`%s`", line)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// handleUserFeedback handles user feedback messages
|
||||
func (h *SayHandler) handleUserFeedback(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
@@ -343,6 +348,18 @@ func (h *SayHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayCon
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SayHandler) handleCommandPermissionDenied(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
if msg.Text == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use unified ToolRenderer
|
||||
rendered := dc.ToolRenderer.RenderCommandPermissionDenied(msg.Text)
|
||||
output.Print(rendered)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SayHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
var tool types.ToolMessage
|
||||
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
|
||||
@@ -517,5 +534,16 @@ func (h *SayHandler) handleTaskProgress(msg *types.ClineMessage, dc *DisplayCont
|
||||
|
||||
// handleDefault handles unknown SAY message types
|
||||
func (h *SayHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
// Debug: log unhandled say types to help identify missing cases using output.Printf for CLI consistency
|
||||
if dc.Verbose {
|
||||
output.Printf("[DEBUG] Unhandled SAY type: '%s' (text preview: %s)\n", msg.Say, truncateForDisplay(msg.Text, 50))
|
||||
}
|
||||
return dc.Renderer.RenderMessage("SAY", msg.Text, true)
|
||||
}
|
||||
|
||||
func truncateForDisplay(text string, maxLen int) string {
|
||||
if len(text) <= maxLen {
|
||||
return text
|
||||
}
|
||||
return text[:maxLen] + "..."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/output"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
)
|
||||
|
||||
// Hook-specific SAY handlers and helpers.
|
||||
// Kept in a separate file to keep say_handlers.go focused on routing.
|
||||
|
||||
// handleHookStatus handles hook execution status messages.
|
||||
func (h *SayHandler) handleHookStatus(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
hook, err := parseHookMessage(msg.Text)
|
||||
if err != nil {
|
||||
// Fallback to basic output if JSON parsing fails
|
||||
return dc.Renderer.RenderMessage("HOOK", msg.Text, true)
|
||||
}
|
||||
|
||||
logHookDebug(hook, dc)
|
||||
hook.ScriptPaths = formatHookPaths(hook.ScriptPaths)
|
||||
|
||||
return renderHookStatus(hook, dc)
|
||||
}
|
||||
|
||||
// handleHookOutputStream handles streaming output from hooks.
|
||||
//
|
||||
// Hook stdout/stderr currently arrives line-by-line from the backend as
|
||||
// `hook_output_stream` messages. The CLI intentionally suppresses these by default
|
||||
// to keep the transcript high-signal.
|
||||
//
|
||||
// In --verbose mode, we print each non-empty line prefixed with "HOOK>" for easy grepping.
|
||||
// Future work could associate these lines with a specific hook execution and render them
|
||||
// as a grouped section under the hook status header.
|
||||
func (h *SayHandler) handleHookOutputStream(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
if !dc.Verbose {
|
||||
return nil
|
||||
}
|
||||
|
||||
line := strings.TrimRight(msg.Text, "\n")
|
||||
if strings.TrimSpace(line) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
output.Printf("HOOK> %s\n", line)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseHookMessage(jsonText string) (types.HookMessage, error) {
|
||||
var hook types.HookMessage
|
||||
if err := json.Unmarshal([]byte(jsonText), &hook); err != nil {
|
||||
return types.HookMessage{}, err
|
||||
}
|
||||
return hook, nil
|
||||
}
|
||||
|
||||
func logHookDebug(hook types.HookMessage, dc *DisplayContext) {
|
||||
if dc.Verbose {
|
||||
output.Printf("[DEBUG] Hook parsed: name=%s, status=%s, toolName=%s, scriptPaths=%v\n",
|
||||
hook.HookName, hook.Status, hook.ToolName, hook.ScriptPaths)
|
||||
}
|
||||
}
|
||||
|
||||
func formatHookPaths(paths []string) []string {
|
||||
if len(paths) == 0 {
|
||||
return paths
|
||||
}
|
||||
formatted := make([]string, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
if strings.TrimSpace(p) == "" {
|
||||
continue
|
||||
}
|
||||
formatted = append(formatted, formatHookPath(p))
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
func renderHookStatus(hook types.HookMessage, dc *DisplayContext) error {
|
||||
if dc.HookRenderer != nil {
|
||||
rendered := dc.HookRenderer.RenderHookStatus(hook)
|
||||
// Match ToolRenderer’s spacing: one leading newline, one trailing newline.
|
||||
output.Print("\n")
|
||||
output.Print(rendered)
|
||||
output.Print("\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback: if HookRenderer not available
|
||||
return dc.Renderer.RenderMessage("HOOK", fmt.Sprintf("%s %s", hook.HookName, hook.Status), true)
|
||||
}
|
||||
|
||||
func formatHookPath(fullPath string) string {
|
||||
// Normalize for display and prefix checks. This is display-only; do not use for IO.
|
||||
normalized := normalizeSlashes(fullPath)
|
||||
|
||||
// If this is a repo-scoped hook script (i.e. lives under <repo>/.clinerules/hooks/),
|
||||
// always include the repo name for disambiguation even in single-repo workspaces.
|
||||
//
|
||||
// This intentionally runs before workspace-relative formatting, which would otherwise
|
||||
// collapse to ".clinerules/hooks/..." and lose the repo context.
|
||||
if p, ok := tryRepoScopedHooksPath(normalized); ok {
|
||||
return p
|
||||
}
|
||||
|
||||
// Prefer workspace-relative paths first for readability, since most hook scripts
|
||||
// live inside the current project.
|
||||
if p, ok := tryWorkspaceRelativeHookPath(normalized); ok {
|
||||
return p
|
||||
}
|
||||
|
||||
// Follow existing CLI pattern: resolve home via os.UserHomeDir.
|
||||
if p, ok := tryHomeTildePath(normalized); ok {
|
||||
return p
|
||||
}
|
||||
|
||||
// Secondary heuristic: if hook lives under <repo>/.clinerules, collapse to repo-relative.
|
||||
if p, ok := tryRepoRelativeHookPath(normalized); ok {
|
||||
return p
|
||||
}
|
||||
|
||||
return fallbackLastComponents(normalized, 3)
|
||||
}
|
||||
|
||||
func normalizeSlashes(p string) string {
|
||||
return filepath.ToSlash(p)
|
||||
}
|
||||
|
||||
func tryWorkspaceRelativeHookPath(normalizedPath string) (string, bool) {
|
||||
root, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// filepath.Rel expects OS-native paths, so we need to convert the normalized path
|
||||
// back to OS-native format before calling Rel, then normalize the result for display.
|
||||
targetOS := filepath.FromSlash(normalizedPath)
|
||||
rel, err := filepath.Rel(root, targetOS)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
// If it's not within the workspace, Rel will start with "..".
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return "", false
|
||||
}
|
||||
return normalizeSlashes(rel), true
|
||||
}
|
||||
|
||||
func tryHomeTildePath(normalizedPath string) (string, bool) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil || strings.TrimSpace(homeDir) == "" {
|
||||
return "", false
|
||||
}
|
||||
homeDir = normalizeSlashes(homeDir)
|
||||
if !strings.HasPrefix(normalizedPath, homeDir) {
|
||||
return "", false
|
||||
}
|
||||
rel := strings.TrimPrefix(normalizedPath, homeDir)
|
||||
rel = strings.TrimPrefix(rel, "/")
|
||||
return "~/" + rel, true
|
||||
}
|
||||
|
||||
func tryRepoRelativeHookPath(normalizedPath string) (string, bool) {
|
||||
parts := strings.Split(normalizedPath, "/")
|
||||
for i, part := range parts {
|
||||
if part == ".clinerules" && i > 0 {
|
||||
repoName := parts[i-1]
|
||||
return repoName + "/" + strings.Join(parts[i:], "/"), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// tryRepoScopedHooksPath returns a repo-prefixed path like
|
||||
// "myrepo/.clinerules/hooks/PreToolUse" when the given path points to a hook script
|
||||
// under a repo's .clinerules/hooks directory.
|
||||
//
|
||||
// This is more specific than tryRepoRelativeHookPath and is used to ensure hook script
|
||||
// paths always include repo context.
|
||||
func tryRepoScopedHooksPath(normalizedPath string) (string, bool) {
|
||||
// Fast path check to avoid split work.
|
||||
if !strings.Contains(normalizedPath, "/.clinerules/hooks/") {
|
||||
return "", false
|
||||
}
|
||||
return tryRepoRelativeHookPath(normalizedPath)
|
||||
}
|
||||
|
||||
func fallbackLastComponents(normalizedPath string, n int) string {
|
||||
parts := strings.Split(normalizedPath, "/")
|
||||
if len(parts) >= n {
|
||||
return strings.Join(parts[len(parts)-n:], "/")
|
||||
}
|
||||
return normalizedPath
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatHookPath_PrefersWorkspaceRelative(t *testing.T) {
|
||||
// Create a stable workspace root (avoid TempDir's nested ".../001" patterns)
|
||||
// so that workspace-relative formatting is deterministic.
|
||||
root := filepath.Join(t.TempDir(), "workspace")
|
||||
if err := os.MkdirAll(root, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
oldWd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Getwd: %v", err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(oldWd) }()
|
||||
if err := os.Chdir(root); err != nil {
|
||||
t.Fatalf("Chdir: %v", err)
|
||||
}
|
||||
|
||||
inside := filepath.Join(root, ".clinerules", "hooks", "pre.sh")
|
||||
got := formatHookPath(inside)
|
||||
// Repo-scoped hook scripts should always include the repo name (the directory
|
||||
// immediately containing .clinerules) even when running inside that repo.
|
||||
expected := "workspace/" + filepath.ToSlash(filepath.Join(".clinerules", "hooks", "pre.sh"))
|
||||
if got != expected {
|
||||
t.Fatalf("expected formatted path to be %q. got=%q", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatHookPath_FallsBackToLastComponents(t *testing.T) {
|
||||
// Use an obviously non-workspace path (relative, but not prefixed with cwd).
|
||||
got := formatHookPath("/var/tmp/foo/bar/baz.sh")
|
||||
if got != "foo/bar/baz.sh" {
|
||||
t.Fatalf("expected last 3 components fallback, got=%q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatHookPath_HomeDirToTilde(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
t.Skip("home dir not available; skipping")
|
||||
}
|
||||
|
||||
got := formatHookPath(home + "/Documents/Cline/Hooks/TaskStart")
|
||||
want := "~/Documents/Cline/Hooks/TaskStart"
|
||||
if got != want {
|
||||
t.Fatalf("expected %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatHookPath_WorkspaceRepoRelative(t *testing.T) {
|
||||
got := formatHookPath("/Users/alice/dev/repo-name/.clinerules/hooks/TaskStart")
|
||||
want := "repo-name/.clinerules/hooks/TaskStart"
|
||||
if got != want {
|
||||
t.Fatalf("expected %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatHookPath_FallbackLast3Components(t *testing.T) {
|
||||
got := formatHookPath("/a/b/c/d/e")
|
||||
want := "c/d/e"
|
||||
if got != want {
|
||||
t.Fatalf("expected %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
@@ -407,7 +407,7 @@ func newInstanceListCommand() *cobra.Command {
|
||||
|
||||
fmt.Print(strings.TrimLeft(rendered, "\n"))
|
||||
}
|
||||
fmt.Println("\n")
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,4 +503,4 @@ func newInstanceNewCommand() *cobra.Command {
|
||||
cmd.Flags().BoolVarP(&setDefault, "default", "d", false, "set as default instance")
|
||||
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type Manager struct {
|
||||
clientAddress string
|
||||
state *types.ConversationState
|
||||
renderer *display.Renderer
|
||||
hookRenderer *display.HookRenderer
|
||||
toolRenderer *display.ToolRenderer
|
||||
systemRenderer *display.SystemMessageRenderer
|
||||
streamingDisplay *display.StreamingDisplay
|
||||
@@ -48,6 +49,7 @@ func NewManager(client *client.ClineClient) *Manager {
|
||||
state := types.NewConversationState()
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
toolRenderer := display.NewToolRenderer(renderer.GetMdRenderer(), global.Config.OutputFormat)
|
||||
hookRenderer := display.NewHookRenderer(renderer.GetMdRenderer(), global.Config.OutputFormat)
|
||||
systemRenderer := display.NewSystemMessageRenderer(renderer, renderer.GetMdRenderer(), global.Config.OutputFormat)
|
||||
streamingDisplay := display.NewStreamingDisplay(state, renderer)
|
||||
|
||||
@@ -61,6 +63,7 @@ func NewManager(client *client.ClineClient) *Manager {
|
||||
clientAddress: "", // Will be set when client is provided
|
||||
state: state,
|
||||
renderer: renderer,
|
||||
hookRenderer: hookRenderer,
|
||||
toolRenderer: toolRenderer,
|
||||
systemRenderer: systemRenderer,
|
||||
streamingDisplay: streamingDisplay,
|
||||
@@ -964,6 +967,27 @@ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *Stre
|
||||
}
|
||||
|
||||
switch {
|
||||
case msg.Say == string(types.SayTypeReasoning):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
m.displayMessage(msg, false, false, i)
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeText):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
m.displayMessage(msg, false, false, i)
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeTool):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !msg.Partial && !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
m.displayMessage(msg, false, false, i)
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeUserFeedback):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
@@ -989,6 +1013,15 @@ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *Stre
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeCommandPermissionDenied):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
fmt.Println()
|
||||
m.displayMessage(msg, false, false, i)
|
||||
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeBrowserActionLaunch):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
@@ -1043,6 +1076,26 @@ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *Stre
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeHookStatus):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
fmt.Println()
|
||||
m.displayMessage(msg, false, false, i)
|
||||
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeHookOutputStream):
|
||||
// Hook stdout/stderr streaming arrives as hook_output_stream messages.
|
||||
// These are intentionally suppressed unless verbose (see SayHandler.handleHookOutputStream),
|
||||
// but we still need to route them through the normal handler pipeline in streaming/follow
|
||||
// mode so verbose users actually see `HOOK> ...` lines.
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
m.displayMessage(msg, false, false, i)
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeAPIReqStarted):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
apiInfo := types.APIRequestInfo{Cost: -1}
|
||||
@@ -1174,12 +1227,14 @@ func (m *Manager) displayMessage(msg *types.ClineMessage, isLast, isPartial bool
|
||||
m.mu.RUnlock()
|
||||
|
||||
dc := &handlers.DisplayContext{
|
||||
State: m.state,
|
||||
Renderer: m.renderer,
|
||||
ToolRenderer: m.toolRenderer,
|
||||
SystemRenderer: m.systemRenderer,
|
||||
State: m.state,
|
||||
Renderer: m.renderer,
|
||||
ToolRenderer: m.toolRenderer,
|
||||
HookRenderer: m.hookRenderer,
|
||||
SystemRenderer: m.systemRenderer,
|
||||
IsLast: isLast,
|
||||
IsPartial: isPartial,
|
||||
Verbose: global.Config.Verbose,
|
||||
MessageIndex: messageIndex,
|
||||
IsStreamingMode: isStreaming,
|
||||
IsInteractive: isInteractive,
|
||||
|
||||
@@ -296,6 +296,12 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
|
||||
return err
|
||||
}
|
||||
settings.HooksEnabled = boolPtr(val)
|
||||
case "azure_identity":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.AzureIdentity = boolPtr(val)
|
||||
|
||||
// Integer fields
|
||||
case "request_timeout_ms":
|
||||
|
||||
@@ -47,11 +47,11 @@ const (
|
||||
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"
|
||||
AskTypeCondense AskType = "condense"
|
||||
AskTypeReportBug AskType = "report_bug"
|
||||
AskTypeBrowserActionLaunch AskType = "browser_action_launch"
|
||||
AskTypeUseMcpServer AskType = "use_mcp_server"
|
||||
AskTypeNewTask AskType = "new_task"
|
||||
AskTypeCondense AskType = "condense"
|
||||
AskTypeReportBug AskType = "report_bug"
|
||||
)
|
||||
|
||||
// SayType represents different types of SAY messages
|
||||
@@ -87,6 +87,11 @@ const (
|
||||
SayTypeLoadMcpDocumentation SayType = "load_mcp_documentation"
|
||||
SayTypeInfo SayType = "info"
|
||||
SayTypeTaskProgress SayType = "task_progress"
|
||||
// Hook status streaming from the backend.
|
||||
// These values must match the backend "say" strings emitted by the extension.
|
||||
SayTypeHookStatus SayType = "hook_status"
|
||||
SayTypeHookOutputStream SayType = "hook_output_stream"
|
||||
SayTypeCommandPermissionDenied SayType = "command_permission_denied"
|
||||
)
|
||||
|
||||
// ToolMessage represents a tool-related message
|
||||
@@ -145,6 +150,42 @@ type APIRequestRetryStatus struct {
|
||||
ErrorSnippet string `json:"errorSnippet,omitempty"`
|
||||
}
|
||||
|
||||
// HookMessage represents hook execution metadata sent from the backend
|
||||
type HookMessage struct {
|
||||
HookName string `json:"hookName"` // Type of hook (TaskStart, PreToolUse, etc.)
|
||||
ToolName string `json:"toolName,omitempty"` // Optional tool name for tool-specific hooks
|
||||
Status string `json:"status"` // "running", "completed", "cancelled", or "failed"
|
||||
ScriptPaths []string `json:"scriptPaths,omitempty"` // Full paths to hook script(s)
|
||||
PendingToolInfo *ToolInfo `json:"pendingToolInfo,omitempty"` // Metadata about the pending tool execution (PreToolUse)
|
||||
ExitCode int `json:"exitCode,omitempty"` // Exit code for completed/failed hooks
|
||||
HasJsonResponse bool `json:"hasJsonResponse,omitempty"` // Whether hook returned JSON
|
||||
Error *HookError `json:"error,omitempty"` // Error details if hook failed
|
||||
}
|
||||
|
||||
// ToolInfo represents a compact subset of tool parameters for UI display.
|
||||
// This mirrors the extension's pendingToolInfo shape and is used by the CLI to
|
||||
// show what tool the PreToolUse hook is gating.
|
||||
type ToolInfo struct {
|
||||
Tool string `json:"tool"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Diff string `json:"diff,omitempty"`
|
||||
Regex string `json:"regex,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
McpTool string `json:"mcpTool,omitempty"`
|
||||
McpServer string `json:"mcpServer,omitempty"`
|
||||
ResourceUri string `json:"resourceUri,omitempty"`
|
||||
}
|
||||
|
||||
// HookError represents structured error information from a failed hook
|
||||
type HookError struct {
|
||||
Type string `json:"type"` // Error type: "execution", "timeout", "validation", etc.
|
||||
Message string `json:"message"` // Human-readable error message
|
||||
Details string `json:"details,omitempty"` // Additional error details
|
||||
ScriptPath string `json:"scriptPath,omitempty"` // Path to script that failed
|
||||
}
|
||||
|
||||
// GetTimestamp returns a formatted timestamp string
|
||||
func (m *ClineMessage) GetTimestamp() string {
|
||||
return time.Unix(m.Timestamp/1000, 0).Format("15:04:05")
|
||||
@@ -324,6 +365,12 @@ func convertProtoSayType(sayType cline.ClineSay) string {
|
||||
return string(SayTypeInfo)
|
||||
case cline.ClineSay_TASK_PROGRESS:
|
||||
return string(SayTypeTaskProgress)
|
||||
case cline.ClineSay_HOOK_STATUS:
|
||||
return string(SayTypeHookStatus)
|
||||
case cline.ClineSay_HOOK_OUTPUT_STREAM:
|
||||
return string(SayTypeHookOutputStream)
|
||||
case cline.ClineSay_COMMAND_PERMISSION_DENIED:
|
||||
return string(SayTypeCommandPermissionDenied)
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
},
|
||||
"features/auto-approve",
|
||||
"features/auto-compact",
|
||||
"features/background-edit",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
{
|
||||
@@ -149,6 +150,7 @@
|
||||
},
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
"features/web-tools",
|
||||
{
|
||||
"group": "Slash Commands",
|
||||
"pages": [
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: "Background Edit"
|
||||
sidebarTitle: "Background Edit"
|
||||
---
|
||||
|
||||
Background Edit lets Cline make file changes without opening the diff editor, so you can keep writing code while Cline works on other files in the background.
|
||||
|
||||
<Note>
|
||||
This feature is marked as experimental.
|
||||
</Note>
|
||||
|
||||
## How It Works
|
||||
|
||||
By default, Cline opens a side-by-side diff editor tab for each file it modifies. With Background Edit enabled:
|
||||
|
||||
- Edits write directly to your files without opening new tabs
|
||||
- Changes appear as collapsible diff blocks in the chat panel
|
||||
- Your editor focus stays on whatever file you had open
|
||||
|
||||
## Enabling Background Edit
|
||||
|
||||
1. Click the settings icon (gear) in the top-right corner of the Cline panel
|
||||
2. Go to "**Feature Settings**"
|
||||
3. Toggle "**Enable Background Edit**" on
|
||||
|
||||
## Viewing Changes
|
||||
|
||||
File changes display directly in the chat panel with:
|
||||
|
||||
- **File action icons** showing whether the file was added, updated, or deleted
|
||||
- **Stats** showing additions (+) and deletions (-) at a glance
|
||||
- **Collapsible diffs** you can expand or collapse by clicking the file header
|
||||
- **Real-time streaming** as changes appear line-by-line
|
||||
|
||||
Green highlights additions, red highlights deletions.
|
||||
|
||||
## When to Use It
|
||||
|
||||
This feature works well when you:
|
||||
|
||||
- Use [auto-approve mode](/features/auto-approve) and prefer reviewing changes after the fact
|
||||
- Work on tasks with many small file changes
|
||||
- Want to stay focused on your current file
|
||||
|
||||
Stick with the default diff editor if you prefer reviewing each change before it saves, or need to make inline edits to Cline's proposed changes.
|
||||
|
||||
## Relationship with Other Features
|
||||
|
||||
- **Checkpoints**: Still created after each file operation
|
||||
- **Auto-approve**: Pairs well for uninterrupted workflows
|
||||
- **Message editing**: Restoring from a previous message works as expected
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: "Web Tools"
|
||||
sidebarTitle: "Web Tools"
|
||||
description: "Search the web and fetch content from URLs directly within Cline"
|
||||
---
|
||||
|
||||
Web Tools give Cline the ability to search the internet and fetch content from specific URLs during your tasks. This is useful when you need up-to-date information, documentation lookups, or research that goes beyond your local codebase and the LLM's internal knowledge.
|
||||
|
||||
<Warning>
|
||||
Web Tools require the **Cline provider**. They are not available when using other providers like OpenRouter, Anthropic, AWS Bedrock, etc.
|
||||
</Warning>
|
||||
|
||||
## How Web Tools Work
|
||||
|
||||
Cline has two web tools:
|
||||
|
||||
- **web_search**: Searches the web and returns a list of relevant webpages based on your query
|
||||
- **web_fetch**: Fetches and analyzes content from a specific URL
|
||||
|
||||
When Cline determines that web information would help complete your task, it will use these tools automatically. The tools call Cline's backend API, which handles the search or fetch operation and returns the results.
|
||||
|
||||
## Enabling Web Tools
|
||||
|
||||
Web Tools are available when using the Cline provider. To use them:
|
||||
|
||||
1. Make sure you're signed in to Cline
|
||||
2. Ensure you're using the Cline provider
|
||||
3. Enable the Web Tools toggle in the Feature Settings menu
|
||||
|
||||
<Note>
|
||||
Web tools can be auto-approved using the "Use the browser" setting in [Auto Approve](/features/auto-approve).
|
||||
</Note>
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Looking Up Documentation
|
||||
|
||||
When working with unfamiliar libraries or APIs:
|
||||
- Search for official documentation
|
||||
- Fetch specific API reference pages
|
||||
- Get examples and usage patterns
|
||||
|
||||
### Research Before Implementation
|
||||
|
||||
Before implementing a feature:
|
||||
- Search for best practices and common patterns
|
||||
- Find recent discussions about approaches
|
||||
- Look up known issues or limitations
|
||||
|
||||
### Checking Latest Information
|
||||
|
||||
For time-sensitive information:
|
||||
- Latest release notes and changelogs
|
||||
- Recent bug fixes or security updates
|
||||
- Current recommended versions
|
||||
@@ -18,7 +18,8 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
|
||||
|
||||
Cline supports the following Cerebras models:
|
||||
|
||||
- `zai-glm-4.6` - Intelligent general purpose model with 1,500 tokens/s
|
||||
- `zai-glm-4.6` - Fast general-purpose model on Cerebras (up to 1,000 tokens/s). To be deprecated soon.
|
||||
- `zai-glm-4.7` - Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.
|
||||
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
|
||||
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
|
||||
- `llama-3.3-70b` - Meta's Llama 3.3 model optimized for speed
|
||||
@@ -89,7 +90,7 @@ Works with any OpenAI-compatible tool—Cursor, Continue.dev, Cline, or any othe
|
||||
|
||||
- **Speed Advantage:** Cerebras excels at making reasoning models practical for real-time use. Perfect for agentic workflows that require multiple LLM calls.
|
||||
- **Free Tier:** Start with the free model to experience Cerebras speed before upgrading to paid plans.
|
||||
- **Context Windows:** Models support context windows ranging from 64K to 128K tokens for including substantial code context.
|
||||
- **Context Windows:** Models support context windows ranging from 64K to 131K tokens for including substantial code context.
|
||||
- **Rate Limits:** Generous rate limits designed for development workflows. Check your dashboard for current limits.
|
||||
- **Pricing:** Competitive pricing with significant speed advantages. Visit [Cerebras Cloud](https://cloud.cerebras.ai/) for current rates.
|
||||
- **Real-Time Applications:** Ideal for applications where AI response time matters—code generation, debugging, and interactive development.
|
||||
|
||||
@@ -16,7 +16,7 @@ This document focuses on setting up providers _other than_ the official OpenAI A
|
||||
The key to using an OpenAI-compatible provider with Cline is to configure these main settings:
|
||||
|
||||
1. **Base URL:** This is the API endpoint specific to the provider. It will _not_ be `https://api.openai.com/v1` (that URL is for the official OpenAI API).
|
||||
2. **API Key:** This is the secret key you obtain from your chosen provider.
|
||||
2. **API Key:** This is the secret key you obtain from your chosen provider. (or **Use Azure Identity Authentication**)
|
||||
3. **Model ID:** This is the specific name or identifier for the model you wish to use.
|
||||
|
||||
You'll find these settings in the Cline settings panel (click the ⚙️ icon):
|
||||
@@ -25,6 +25,7 @@ You'll find these settings in the Cline settings panel (click the ⚙️ icon):
|
||||
- **Base URL:** Enter the base URL provided by your chosen provider. **This is a crucial step.**
|
||||
- **API Key:** Enter your API key from the provider.
|
||||
- **Model:** Choose or enter the model ID.
|
||||
- **Use Azure Identity Authentication:** Check the box to authenticate with your Azure managed identity (Note that this will not trigger the authentication, it will use the existing one (e.g., "az login"))
|
||||
- **Model Configuration:** This section allows you to customize advanced parameters for the model, such as:
|
||||
- Max Output Tokens
|
||||
- Context Window size
|
||||
|
||||
Generated
+139
-146
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.46.1",
|
||||
"version": "3.47.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.46.1",
|
||||
"version": "3.47.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.922.0",
|
||||
"@aws-sdk/credential-providers": "^3.922.0",
|
||||
"@azure/identity": "^4.13.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
@@ -43,6 +44,7 @@
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@sap-ai-sdk/ai-api": "^2.1.0",
|
||||
"@sap-ai-sdk/orchestration": "^2.1.0",
|
||||
"@sap-cloud-sdk/connectivity": "^4.2.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/uuid": "^10.0.0",
|
||||
@@ -87,6 +89,7 @@
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"reconnecting-eventsource": "^1.6.4",
|
||||
"serialize-error": "^11.0.3",
|
||||
"shell-quote": "^1.8.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
@@ -113,6 +116,7 @@
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^17.0.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
@@ -1005,7 +1009,6 @@
|
||||
},
|
||||
"node_modules/@azure/abort-controller": {
|
||||
"version": "2.1.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
@@ -1016,7 +1019,6 @@
|
||||
},
|
||||
"node_modules/@azure/core-auth": {
|
||||
"version": "1.9.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
@@ -1029,7 +1031,6 @@
|
||||
},
|
||||
"node_modules/@azure/core-client": {
|
||||
"version": "1.9.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
@@ -1046,7 +1047,6 @@
|
||||
},
|
||||
"node_modules/@azure/core-rest-pipeline": {
|
||||
"version": "1.21.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
@@ -1063,7 +1063,6 @@
|
||||
},
|
||||
"node_modules/@azure/core-tracing": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
@@ -1074,7 +1073,6 @@
|
||||
},
|
||||
"node_modules/@azure/core-util": {
|
||||
"version": "1.12.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
@@ -1086,8 +1084,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity": {
|
||||
"version": "4.10.2",
|
||||
"dev": true,
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz",
|
||||
"integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
@@ -1108,7 +1107,6 @@
|
||||
},
|
||||
"node_modules/@azure/logger": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typespec/ts-http-runtime": "^0.2.2",
|
||||
@@ -1120,7 +1118,6 @@
|
||||
},
|
||||
"node_modules/@azure/msal-browser": {
|
||||
"version": "4.14.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.8.0"
|
||||
@@ -1131,7 +1128,6 @@
|
||||
},
|
||||
"node_modules/@azure/msal-common": {
|
||||
"version": "15.8.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
@@ -1139,7 +1135,6 @@
|
||||
},
|
||||
"node_modules/@azure/msal-node": {
|
||||
"version": "3.6.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.8.0",
|
||||
@@ -1152,7 +1147,6 @@
|
||||
},
|
||||
"node_modules/@azure/msal-node/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
@@ -1187,6 +1181,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2065,12 +2060,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@dabh/diagnostics": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
|
||||
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
|
||||
"integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorspace": "1.1.x",
|
||||
"@so-ric/colorspace": "^1.1.6",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
@@ -2649,6 +2644,7 @@
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.9.15",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.8",
|
||||
"@types/node": ">=12.12.47"
|
||||
@@ -3232,6 +3228,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
|
||||
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -3300,6 +3297,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -4915,8 +4913,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4929,8 +4926,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4943,8 +4939,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4957,8 +4952,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4971,8 +4965,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4985,8 +4978,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -4999,8 +4991,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -5013,8 +5004,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5027,8 +5017,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5041,8 +5030,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5055,8 +5043,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5069,8 +5056,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5083,8 +5069,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5097,8 +5082,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5111,8 +5095,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5125,8 +5108,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5139,8 +5121,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5153,8 +5134,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5167,8 +5147,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5181,8 +5160,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5195,8 +5173,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5209,8 +5186,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.1.0",
|
||||
@@ -5259,17 +5235,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sap-cloud-sdk/connectivity": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@sap-cloud-sdk/connectivity/-/connectivity-4.1.2.tgz",
|
||||
"integrity": "sha512-PJNBg+yhyo4Y/PZBcqbN8eD+RFVolgOcWgqVTdHef6Wch8t6SVlDqxLzf4mJ4nTpAY2E3ERFjIwlP+MUUXcf5A==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sap-cloud-sdk/connectivity/-/connectivity-4.2.0.tgz",
|
||||
"integrity": "sha512-dvQpjYWp9EhoUip4ftvnsVOG6BHJO+fOOuVs0R7V1IRyl7F49QPdpI/YoMzk3W7Rex9NozRa/0EXAe9L0CrFbA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sap-cloud-sdk/resilience": "^4.1.2",
|
||||
"@sap-cloud-sdk/util": "^4.1.2",
|
||||
"@sap-cloud-sdk/resilience": "^4.2.0",
|
||||
"@sap-cloud-sdk/util": "^4.2.0",
|
||||
"@sap/xsenv": "^6.0.0",
|
||||
"@sap/xssec": "^4.9.2",
|
||||
"@sap/xssec": "^4.11.2",
|
||||
"async-retry": "^1.3.3",
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.13.2",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
}
|
||||
},
|
||||
@@ -5299,28 +5275,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sap-cloud-sdk/resilience": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@sap-cloud-sdk/resilience/-/resilience-4.1.2.tgz",
|
||||
"integrity": "sha512-rMcM6Sn0WswNQK9583UCBEwzqvPSETae8GzzFrTQ7+JM6RA6PU5/WiPmhqzQQw1MFQvwuX6uMDwPf4U3AdemMQ==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sap-cloud-sdk/resilience/-/resilience-4.2.0.tgz",
|
||||
"integrity": "sha512-ZR2UugUEKEawIu6SIfz9OM8nQO43s/37jnmU4L+vTQN+ptFdFg+2bJAjj+F3jyqSaBx6JbALs9r6X2eWRc+ZDQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sap-cloud-sdk/util": "^4.1.2",
|
||||
"@sap-cloud-sdk/util": "^4.2.0",
|
||||
"async-retry": "^1.3.3",
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.13.2",
|
||||
"opossum": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sap-cloud-sdk/util": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@sap-cloud-sdk/util/-/util-4.1.2.tgz",
|
||||
"integrity": "sha512-lsxsBc60pokMDCHvs0/CXTa6fjVEMzTeSyB8CerG6L0sN4sK8Ppm63VVcSh/E+X+U4aVMHmLuOmmd6XyLGAc3Q==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sap-cloud-sdk/util/-/util-4.2.0.tgz",
|
||||
"integrity": "sha512-sAyb1hWATZKKu+NZWX65oIdSI4uKj//0UOl4vtzMPNgYXhF806aAWbj2tLZAHI4UTtQWedbfcHhwO7BLl9MvQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.13.2",
|
||||
"chalk": "^4.1.0",
|
||||
"logform": "^2.7.0",
|
||||
"voca": "^1.4.1",
|
||||
"winston": "^3.17.0",
|
||||
"winston": "^3.18.3",
|
||||
"winston-transport": "^4.9.0"
|
||||
}
|
||||
},
|
||||
@@ -5367,9 +5343,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sap/xssec": {
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.10.0.tgz",
|
||||
"integrity": "sha512-6SxDorJpQRNjI0sCTwHoFkyECI5IrxnRZ3adoTY0XeGM8QcMoYmLnJIPXoa6AYSzMB89rjJ02j1PQVWACYt/Hg==",
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.12.1.tgz",
|
||||
"integrity": "sha512-LcCnFuoNKosXJ9H71Yd9vRNknB+LHsghro1eHgQ8jUmacSt/C2USL7BsJ4TDb4DxeeZB4wieUEy5HBPZM9T+Jw==",
|
||||
"license": "SAP DEVELOPER LICENSE AGREEMENT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
@@ -6247,6 +6223,16 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@so-ric/colorspace": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
|
||||
"integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^5.0.2",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@streamparser/json": {
|
||||
"version": "0.0.22",
|
||||
"license": "MIT"
|
||||
@@ -6763,8 +6749,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/get-folder-size": {
|
||||
"version": "3.0.4",
|
||||
@@ -6789,6 +6774,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
|
||||
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -6827,6 +6813,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/shell-quote": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz",
|
||||
"integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/shimmer": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
|
||||
@@ -6881,7 +6874,6 @@
|
||||
},
|
||||
"node_modules/@typespec/ts-http-runtime": {
|
||||
"version": "0.2.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
@@ -7486,6 +7478,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -7943,9 +7936,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -8252,6 +8245,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -8815,13 +8809,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
|
||||
"integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
"color-convert": "^3.1.3",
|
||||
"color-string": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
@@ -8839,13 +8836,24 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
|
||||
"integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-string/node_modules/color-name": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
|
||||
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
@@ -8856,35 +8864,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
|
||||
"integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
|
||||
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colorspace": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
|
||||
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^3.1.3",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"license": "MIT",
|
||||
@@ -9455,7 +9459,8 @@
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1342118",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
@@ -12431,6 +12436,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
@@ -12592,6 +12598,8 @@
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -12662,6 +12670,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
@@ -15542,7 +15551,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -15563,7 +15571,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -16231,7 +16238,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -16668,9 +16674,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -16857,21 +16867,6 @@
|
||||
"url": "https://github.com/steveukx/git-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sinon": {
|
||||
"version": "19.0.2",
|
||||
"dev": true,
|
||||
@@ -17668,7 +17663,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
@@ -17685,7 +17679,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -18049,6 +18042,7 @@
|
||||
"version": "5.5.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -18308,7 +18302,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -18383,7 +18376,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -18699,13 +18691,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
@@ -19079,6 +19071,7 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+6
-3
@@ -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.46.1",
|
||||
"version": "3.47.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -346,7 +346,6 @@
|
||||
"compile-cli": "scripts/build-cli.sh",
|
||||
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
|
||||
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
|
||||
"build:npm": "scripts/build-npm-package.sh",
|
||||
"test:install": "bash scripts/test-install.sh",
|
||||
"dev:cli:watch": "node scripts/dev-cli-watch.mjs",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
@@ -367,7 +366,7 @@
|
||||
"clean:all": "npm run clean:build && npm run clean:deps",
|
||||
"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",
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit",
|
||||
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
|
||||
"lint:proto": "bash ./scripts/proto-lint.sh",
|
||||
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
|
||||
@@ -421,6 +420,7 @@
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^17.0.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
@@ -456,6 +456,7 @@
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.922.0",
|
||||
"@aws-sdk/credential-providers": "^3.922.0",
|
||||
"@azure/identity": "^4.13.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
@@ -486,6 +487,7 @@
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@sap-ai-sdk/ai-api": "^2.1.0",
|
||||
"@sap-ai-sdk/orchestration": "^2.1.0",
|
||||
"@sap-cloud-sdk/connectivity": "^4.2.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/uuid": "^10.0.0",
|
||||
@@ -530,6 +532,7 @@
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"reconnecting-eventsource": "^1.6.4",
|
||||
"serialize-error": "^11.0.3",
|
||||
"shell-quote": "^1.8.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
|
||||
+22
-12
@@ -49,6 +49,8 @@ service ModelsService {
|
||||
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
|
||||
// Fetches available models from AIhubmix
|
||||
rpc getAihubmixModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Vercel AI Gateway models
|
||||
rpc refreshVercelAiGatewayModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -229,6 +231,7 @@ message ModelsApiOptions {
|
||||
optional string minimax_api_line = 41;
|
||||
optional string aihubmix_base_url = 42;
|
||||
optional string aihubmix_app_code = 43;
|
||||
optional bool azure_identity = 44;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
@@ -380,6 +383,10 @@ message OcaModelInfo {
|
||||
string model_name = 17;
|
||||
// The API format used by this model
|
||||
optional ApiFormat api_format = 18;
|
||||
// Supports reasoning
|
||||
optional bool supports_reasoning = 19;
|
||||
// reasoning effort options
|
||||
repeated string reasoning_effort_options = 20;
|
||||
}
|
||||
|
||||
// Aggregated OCA model catalog keyed by model identifier
|
||||
@@ -567,6 +574,7 @@ message ModelsApiConfiguration {
|
||||
optional string aihubmix_base_url = 83;
|
||||
optional string aihubmix_app_code = 84;
|
||||
optional string nous_research_api_key = 85;
|
||||
optional bool azure_identity = 86;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
@@ -602,12 +610,13 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
|
||||
optional string plan_mode_oca_model_id = 131;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 132;
|
||||
optional string plan_mode_hicap_model_id = 133;
|
||||
optional OpenRouterModelInfo plan_mode_hicap_model_info = 134;
|
||||
optional string plan_mode_aihubmix_model_id = 135;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 136;
|
||||
optional string plan_mode_nous_research_model_id = 137;
|
||||
optional string gemini_plan_mode_thinking_level = 138;
|
||||
optional string plan_mode_oca_reasoning_effort = 133;
|
||||
optional string plan_mode_hicap_model_id = 134;
|
||||
optional OpenRouterModelInfo plan_mode_hicap_model_info = 135;
|
||||
optional string plan_mode_aihubmix_model_id = 136;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 137;
|
||||
optional string plan_mode_nous_research_model_id = 138;
|
||||
optional string gemini_plan_mode_thinking_level = 139;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -643,10 +652,11 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
|
||||
optional string act_mode_oca_model_id = 231;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 232;
|
||||
optional string act_mode_hicap_model_id = 233;
|
||||
optional OpenRouterModelInfo act_mode_hicap_model_info = 234;
|
||||
optional string act_mode_aihubmix_model_id = 235;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 236;
|
||||
optional string act_mode_nous_research_model_id = 237;
|
||||
optional string gemini_act_mode_thinking_level = 238;
|
||||
optional string act_mode_oca_reasoning_effort = 233;
|
||||
optional string act_mode_hicap_model_id = 234;
|
||||
optional OpenRouterModelInfo act_mode_hicap_model_info = 235;
|
||||
optional string act_mode_aihubmix_model_id = 236;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 237;
|
||||
optional string act_mode_nous_research_model_id = 238;
|
||||
optional string gemini_act_mode_thinking_level = 239;
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ message Settings {
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
|
||||
optional bool cline_web_tools_enabled = 134;
|
||||
optional bool hooks_enabled = 135;
|
||||
optional bool azure_identity = 136;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -370,6 +371,7 @@ message UpdateSettingsRequest {
|
||||
optional bool cline_web_tools_enabled = 34;
|
||||
optional bool enable_parallel_tool_calling = 35;
|
||||
optional bool background_edit_enabled = 36;
|
||||
optional string oca_reasoning_effort = 37;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
|
||||
@@ -67,6 +67,9 @@ enum ClineSay {
|
||||
TASK_PROGRESS = 27;
|
||||
ERROR_RETRY = 28;
|
||||
GENERATE_EXPLANATION = 29;
|
||||
HOOK_STATUS = 30;
|
||||
HOOK_OUTPUT_STREAM = 31;
|
||||
COMMAND_PERMISSION_DENIED = 32;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
@@ -220,6 +223,10 @@ message ClineMessage {
|
||||
ClineModelInfo model_info = 23;
|
||||
}
|
||||
|
||||
message ShowWebviewEvent {
|
||||
bool preserve_editor_focus = 1; // When true, webview should not steal focus from editor
|
||||
}
|
||||
|
||||
// UiService provides methods for managing UI interactions
|
||||
service UiService {
|
||||
// Scrolls to a specific settings section in the settings view
|
||||
@@ -258,11 +265,8 @@ service UiService {
|
||||
// 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);
|
||||
// Subscribe to show webview events
|
||||
rpc subscribeToShowWebview(EmptyRequest) returns (stream ShowWebviewEvent);
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Script to build the Cline NPM package with telemetry keys injected
|
||||
# This script ensures all environment variables are properly set and builds are successful
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Required environment variables
|
||||
REQUIRED_VARS=(
|
||||
"TELEMETRY_SERVICE_API_KEY"
|
||||
"ERROR_SERVICE_API_KEY"
|
||||
)
|
||||
|
||||
# Optional but recommended environment variables
|
||||
OPTIONAL_VARS=(
|
||||
"CLINE_ENVIRONMENT"
|
||||
"POSTHOG_TELEMETRY_ENABLED"
|
||||
)
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Cline NPM Package Build Script${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 1: Verify required environment variables are set
|
||||
echo -e "${BLUE}Step 1: Verifying environment variables...${NC}"
|
||||
MISSING_VARS=()
|
||||
for VAR in "${REQUIRED_VARS[@]}"; do
|
||||
if [ -z "${!VAR}" ]; then
|
||||
MISSING_VARS+=("$VAR")
|
||||
echo -e "${RED}✗ $VAR is not set${NC}"
|
||||
else
|
||||
# Show first 10 chars for verification (don't expose full key)
|
||||
VAR_VALUE="${!VAR}"
|
||||
echo -e "${GREEN}✓ $VAR is set (${VAR_VALUE:0:10}...)${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check optional variables
|
||||
for VAR in "${OPTIONAL_VARS[@]}"; do
|
||||
if [ -z "${!VAR}" ]; then
|
||||
echo -e "${YELLOW}⚠ $VAR is not set (optional)${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✓ $VAR is set: ${!VAR}${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MISSING_VARS[@]} -gt 0 ]; then
|
||||
echo -e "\n${RED}Error: Missing required environment variables:${NC}"
|
||||
printf '%s\n' "${MISSING_VARS[@]}"
|
||||
echo -e "\n${YELLOW}Please set these variables before running the build:${NC}"
|
||||
echo -e "export TELEMETRY_SERVICE_API_KEY=\"your_posthog_api_key\""
|
||||
echo -e "export ERROR_SERVICE_API_KEY=\"your_error_tracking_api_key\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Verify Node.js can see the environment variables
|
||||
echo -e "\n${BLUE}Step 2: Verifying Node.js can access environment variables...${NC}"
|
||||
if node -e "
|
||||
const telemetryKey = process.env.TELEMETRY_SERVICE_API_KEY;
|
||||
const errorKey = process.env.ERROR_SERVICE_API_KEY;
|
||||
if (!telemetryKey || !errorKey) {
|
||||
console.error('Node.js cannot see environment variables!');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ TELEMETRY_SERVICE_API_KEY visible to Node.js');
|
||||
console.log('✓ ERROR_SERVICE_API_KEY visible to Node.js');
|
||||
"; then
|
||||
echo -e "${GREEN}✓ Node.js can access environment variables${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Node.js cannot access environment variables${NC}"
|
||||
echo -e "${YELLOW}Make sure to use 'export' when setting variables:${NC}"
|
||||
echo -e "export TELEMETRY_SERVICE_API_KEY=\"...\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 3: Clean previous builds
|
||||
echo -e "\n${BLUE}Step 3: Cleaning previous builds...${NC}"
|
||||
rm -rf dist-standalone
|
||||
echo -e "${GREEN}✓ Cleaned dist-standalone directory${NC}"
|
||||
|
||||
# Step 4: Build Go CLI binaries for all platforms
|
||||
echo -e "\n${BLUE}Step 4: Building Go CLI binaries for all platforms...${NC}"
|
||||
if npm run compile-cli-all-platforms; then
|
||||
echo -e "${GREEN}✓ Go CLI binaries built successfully${NC}"
|
||||
|
||||
# Verify binaries were created
|
||||
if ls cli/bin/cline-* 1> /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ CLI binaries verified:${NC}"
|
||||
ls -lh cli/bin/cline-* | awk '{print " " $9 " (" $5 ")"}'
|
||||
else
|
||||
echo -e "${RED}✗ No CLI binaries found in cli/bin/${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ Failed to build Go CLI binaries${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 5: Build the standalone package with esbuild
|
||||
echo -e "\n${BLUE}Step 5: Building standalone package with esbuild...${NC}"
|
||||
if npm run compile-standalone-npm; then
|
||||
echo -e "${GREEN}✓ Standalone package built successfully${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Failed to build standalone package${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 6: Verify telemetry keys were injected
|
||||
echo -e "\n${BLUE}Step 6: Verifying telemetry keys were injected...${NC}"
|
||||
|
||||
# Check if the compiled file still has process.env references (bad)
|
||||
if grep -q "process.env.TELEMETRY_SERVICE_API_KEY" dist-standalone/cline-core.js; then
|
||||
echo -e "${RED}✗ Keys were NOT injected! Found 'process.env.TELEMETRY_SERVICE_API_KEY' in compiled code${NC}"
|
||||
echo -e "${YELLOW}This means the environment variables were not replaced during build${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if actual keys are present (good)
|
||||
if grep -q "data.cline.bot" dist-standalone/cline-core.js; then
|
||||
# Extract a snippet of the PostHog config
|
||||
POSTHOG_CONFIG=$(grep -A 3 "data.cline.bot" dist-standalone/cline-core.js | head -5)
|
||||
if echo "$POSTHOG_CONFIG" | grep -q "apiKey.*phc_"; then
|
||||
echo -e "${GREEN}✓ Telemetry keys successfully injected into compiled code${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ PostHog config found but apiKey format unclear${NC}"
|
||||
echo -e "${YELLOW}Config snippet:${NC}"
|
||||
echo "$POSTHOG_CONFIG"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Could not verify PostHog config in compiled code${NC}"
|
||||
fi
|
||||
|
||||
# Step 7: Display build summary
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${GREEN}Build completed successfully!${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}Package location:${NC} dist-standalone/"
|
||||
echo -e "${GREEN}Package version:${NC} $(node -p "require('./dist-standalone/package.json').version" 2>/dev/null || echo "unknown")"
|
||||
echo ""
|
||||
echo -e "${BLUE}Next steps:${NC}"
|
||||
echo -e "1. Test locally: ${YELLOW}cd dist-standalone && npm link${NC}"
|
||||
echo -e "2. Verify: ${YELLOW}cline version${NC}"
|
||||
echo -e "3. Publish: ${YELLOW}cd dist-standalone && npm publish${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note: Check PostHog dashboard after running cline commands to verify telemetry${NC}"
|
||||
@@ -49,9 +49,18 @@ fi
|
||||
# Create installation directory
|
||||
mkdir -p "$INSTALL_DIR/bin"
|
||||
|
||||
# Copy standalone package first (includes node_modules, cline-core.js, etc.)
|
||||
# Copy standalone package first (cline-core.js, wasm files, etc.)
|
||||
rsync -a --exclude='bin' "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
|
||||
|
||||
# Install runtime dependencies (grpc-health-check, better-sqlite3, etc.)
|
||||
# These are external dependencies not bundled into cline-core.js
|
||||
echo -e "${CYAN}→${NC} ${DIM}Installing runtime dependencies...${NC}"
|
||||
cd "$PROJECT_ROOT/standalone/runtime-files"
|
||||
npm install --silent 2>/dev/null || npm install
|
||||
cp -r node_modules "$INSTALL_DIR/"
|
||||
cp -r vscode "$INSTALL_DIR/node_modules/"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Detect platform for native modules
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
arch=$(uname -m)
|
||||
|
||||
@@ -140,6 +140,7 @@ function createHandlerForProvider(
|
||||
openAiApiKey: options.openAiApiKey,
|
||||
openAiBaseUrl: options.openAiBaseUrl,
|
||||
azureApiVersion: options.azureApiVersion,
|
||||
azureIdentity: options.azureIdentity,
|
||||
openAiHeaders: options.openAiHeaders,
|
||||
openAiModelId: mode === "plan" ? options.planModeOpenAiModelId : options.actModeOpenAiModelId,
|
||||
openAiModelInfo: mode === "plan" ? options.planModeOpenAiModelInfo : options.actModeOpenAiModelInfo,
|
||||
@@ -375,10 +376,14 @@ function createHandlerForProvider(
|
||||
return new VercelAIGatewayHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
vercelAiGatewayApiKey: options.vercelAiGatewayApiKey,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
openRouterModelId:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelId : options.actModeVercelAiGatewayModelId,
|
||||
openRouterModelInfo:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelInfo : options.actModeVercelAiGatewayModelInfo,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
})
|
||||
case "zai":
|
||||
return new ZAiHandler({
|
||||
@@ -393,6 +398,7 @@ function createHandlerForProvider(
|
||||
ocaBaseUrl: options.ocaBaseUrl,
|
||||
ocaModelId: mode === "plan" ? options.planModeOcaModelId : options.actModeOcaModelId,
|
||||
ocaModelInfo: mode === "plan" ? options.planModeOcaModelInfo : options.actModeOcaModelInfo,
|
||||
ocaReasoningEffort: mode === "plan" ? options.planModeOcaReasoningEffort : options.actModeOcaReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
ocaUsePromptCache:
|
||||
|
||||
@@ -199,7 +199,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
|
||||
if (["x-ai/grok-code-fast-1", "minimax/minimax-m2"].includes(this.getModel().id)) {
|
||||
if (["x-ai/grok-code-fast-1", "minimax/minimax-m2.1"].includes(this.getModel().id)) {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults, ModelInfo } from "@shared/api"
|
||||
import OpenAI, { APIError, OpenAIError } from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
@@ -9,18 +9,23 @@ import {
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { OcaModelInfo } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/index.cline"
|
||||
import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
import { handleResponsesApiStreamResponse } from "../utils/responses_api_support"
|
||||
|
||||
export interface OcaHandlerOptions extends CommonApiHandlerOptions {
|
||||
ocaBaseUrl?: string
|
||||
ocaModelId?: string
|
||||
ocaModelInfo?: LiteLLMModelInfo
|
||||
ocaModelInfo?: OcaModelInfo
|
||||
ocaReasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
ocaUsePromptCache?: boolean
|
||||
taskId?: string
|
||||
@@ -100,7 +105,7 @@ export class OcaHandler implements ApiHandler {
|
||||
return this.client
|
||||
}
|
||||
|
||||
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
|
||||
async getApiCosts(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
|
||||
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
|
||||
@@ -138,8 +143,29 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async calculateCost(
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
_cacheWriteTokens?: number,
|
||||
_cacheReadTokens?: number,
|
||||
) {
|
||||
const inputCost = (await this.getApiCosts(1e6, 0)) || 0
|
||||
const outputCost = (await this.getApiCosts(0, 1e6)) || 0
|
||||
const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6
|
||||
return totalCost
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
if (this.options.ocaModelInfo?.apiFormat == ApiFormat.OPENAI_RESPONSES) {
|
||||
yield* this.createMessageResponsesApi(systemPrompt, messages, tools)
|
||||
} else {
|
||||
yield* this.createMessageChatApi(systemPrompt, messages, tools)
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessageChatApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const formattedMessages = convertToOpenAiMessages(messages)
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
@@ -193,7 +219,7 @@ export class OcaHandler implements ApiHandler {
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
const chatCompletionsParams: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: this.options.ocaModelId || liteLlmDefaultModelId,
|
||||
messages: [enhancedSystemMessage, ...enhancedMessages],
|
||||
temperature,
|
||||
@@ -206,10 +232,13 @@ export class OcaHandler implements ApiHandler {
|
||||
litellm_session_id: `cline-${this.options.taskId}`,
|
||||
...getOpenAIToolParams(tools),
|
||||
}), // Add session ID for LiteLLM tracking
|
||||
})
|
||||
}
|
||||
|
||||
const inputCost = (await this.calculateCost(1e6, 0)) || 0
|
||||
const outputCost = (await this.calculateCost(0, 1e6)) || 0
|
||||
if (this.options.ocaModelInfo?.supportsReasoningEffort) {
|
||||
chatCompletionsParams["reasoning_effort"] = this.options.ocaReasoningEffort || ("medium" as any)
|
||||
}
|
||||
|
||||
const stream = await client.chat.completions.create(chatCompletionsParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
@@ -241,8 +270,11 @@ export class OcaHandler implements ApiHandler {
|
||||
|
||||
// Handle token usage information
|
||||
if (chunk.usage) {
|
||||
const totalCost =
|
||||
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
|
||||
const totalCost = await this.calculateCost(
|
||||
this.options.ocaModelInfo!,
|
||||
chunk.usage.prompt_tokens,
|
||||
chunk.usage.completion_tokens,
|
||||
)
|
||||
|
||||
// Extract cache-related information if available
|
||||
// Need to use type assertion since these properties are not in the standard OpenAI types
|
||||
@@ -270,6 +302,44 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
console.log("Uses Responses API")
|
||||
const client = this.ensureClient()
|
||||
|
||||
// Convert messages to Responses API input format
|
||||
const input: OpenAI.Responses.ResponseInputItem[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAIResponsesInput(messages),
|
||||
]
|
||||
|
||||
// Convert ChatCompletion tools to Responses API format if provided
|
||||
const responseTools = tools
|
||||
?.filter((tool) => tool.type === "function")
|
||||
.map((tool: any) => ({
|
||||
type: "function" as const,
|
||||
name: tool.function.name,
|
||||
description: tool.function.description,
|
||||
parameters: tool.function.parameters,
|
||||
strict: tool.function.strict ?? true, // Responses API defaults to strict mode
|
||||
}))
|
||||
|
||||
const responsesParams: OpenAI.Responses.ResponseCreateParamsStreaming = {
|
||||
model: this.options.ocaModelId || liteLlmDefaultModelId,
|
||||
input,
|
||||
stream: true,
|
||||
tools: responseTools,
|
||||
}
|
||||
|
||||
if (this.options.ocaModelInfo && this.options.ocaModelInfo.supportsReasoning) {
|
||||
responsesParams["reasoning"] = { effort: this.options.ocaReasoningEffort as any, summary: "auto" }
|
||||
}
|
||||
|
||||
// Create the response using Responses API
|
||||
const stream = await client.responses.create(responsesParams)
|
||||
|
||||
yield* handleResponsesApiStreamResponse(stream, this.options.ocaModelInfo!, this.calculateCost.bind(this))
|
||||
}
|
||||
|
||||
getModel() {
|
||||
return {
|
||||
id: this.options.ocaModelId || liteLlmDefaultModelId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity"
|
||||
import { azureOpenAiDefaultApiVersion, ModelInfo, OpenAiCompatibleModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI, { AzureOpenAI } from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
@@ -14,6 +15,7 @@ interface OpenAiHandlerOptions extends CommonApiHandlerOptions {
|
||||
openAiApiKey?: string
|
||||
openAiBaseUrl?: string
|
||||
azureApiVersion?: string
|
||||
azureIdentity?: boolean
|
||||
openAiHeaders?: Record<string, string>
|
||||
openAiModelId?: string
|
||||
openAiModelInfo?: OpenAiCompatibleModelInfo
|
||||
@@ -30,8 +32,8 @@ export class OpenAiHandler implements ApiHandler {
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.openAiApiKey) {
|
||||
throw new Error("OpenAI API key is required")
|
||||
if (!this.options.openAiApiKey && !this.options.azureIdentity) {
|
||||
throw new Error("OpenAI API key or Azure Identity Authentication is required")
|
||||
}
|
||||
try {
|
||||
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
|
||||
@@ -42,13 +44,26 @@ export class OpenAiHandler implements ApiHandler {
|
||||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
|
||||
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
|
||||
) {
|
||||
this.client = new AzureOpenAI({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
if (this.options.azureIdentity) {
|
||||
this.client = new AzureOpenAI({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
azureADTokenProvider: getBearerTokenProvider(
|
||||
new DefaultAzureCredential(),
|
||||
"https://cognitiveservices.azure.com/.default",
|
||||
),
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} else {
|
||||
this.client = new AzureOpenAI({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type Message as BedrockMessage,
|
||||
} from "@aws-sdk/client-bedrock-runtime"
|
||||
import { ChatMessage, OrchestrationClient, OrchestrationModuleConfig } from "@sap-ai-sdk/orchestration"
|
||||
import { HttpDestination, transformServiceBindingToDestination } from "@sap-cloud-sdk/connectivity"
|
||||
import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels } from "@shared/api"
|
||||
import axios from "axios"
|
||||
import JSON5 from "json5"
|
||||
@@ -355,7 +356,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
private options: SapAiCoreHandlerOptions
|
||||
private token?: Token
|
||||
private deployments?: Deployment[]
|
||||
private isAiCoreEnvSetup: boolean = false
|
||||
private aiCoreDestination?: HttpDestination
|
||||
private destinationExpiresAt?: number
|
||||
|
||||
constructor(options: SapAiCoreHandlerOptions) {
|
||||
this.options = options
|
||||
@@ -387,6 +389,34 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async createAiCoreDestination(): Promise<HttpDestination> {
|
||||
try {
|
||||
const aiCoreServiceCredentials = {
|
||||
clientid: this.options.sapAiCoreClientId!,
|
||||
clientsecret: this.options.sapAiCoreClientSecret!,
|
||||
url: this.options.sapAiCoreTokenUrl!,
|
||||
serviceurls: {
|
||||
AI_API_URL: this.options.sapAiCoreBaseUrl!,
|
||||
},
|
||||
}
|
||||
|
||||
const destination = await transformServiceBindingToDestination({
|
||||
credentials: aiCoreServiceCredentials,
|
||||
name: "aicore",
|
||||
label: "aicore",
|
||||
tags: ["aicore"],
|
||||
})
|
||||
|
||||
return {
|
||||
...destination,
|
||||
url: destination.url || this.options.sapAiCoreBaseUrl!,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create AI Core destination:", error)
|
||||
throw new Error(`Unable to create AI Core destination: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async authenticate(): Promise<Token> {
|
||||
this.validateCredentials()
|
||||
|
||||
@@ -481,33 +511,23 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// TODO: support credentials changes after initial setup
|
||||
private ensureAiCoreEnvSetup(): void {
|
||||
// Only set up once to avoid redundant operations
|
||||
if (this.isAiCoreEnvSetup) {
|
||||
return
|
||||
private async ensureAiCoreEnvSetup() {
|
||||
if (!this.aiCoreDestination || !this.destinationExpiresAt || this.destinationExpiresAt < Date.now()) {
|
||||
this.validateCredentials()
|
||||
this.aiCoreDestination = await this.createAiCoreDestination()
|
||||
|
||||
// Extract expiration from the destination's auth token
|
||||
const expiresIn = this.aiCoreDestination.authTokens?.[0]?.expiresIn
|
||||
if (!expiresIn) {
|
||||
throw new Error("Destination is missing required authTokens with expiresIn")
|
||||
}
|
||||
this.destinationExpiresAt = Date.now() + parseInt(expiresIn, 10) * 1000
|
||||
}
|
||||
|
||||
// Validate required credentials
|
||||
this.validateCredentials()
|
||||
|
||||
const aiCoreServiceCredentials = {
|
||||
clientid: this.options.sapAiCoreClientId!,
|
||||
clientsecret: this.options.sapAiCoreClientSecret!,
|
||||
url: this.options.sapAiCoreTokenUrl!,
|
||||
serviceurls: {
|
||||
AI_API_URL: this.options.sapAiCoreBaseUrl!,
|
||||
},
|
||||
}
|
||||
process.env["AICORE_SERVICE_KEY"] = JSON.stringify(aiCoreServiceCredentials)
|
||||
|
||||
// Mark as set up to avoid redundant calls
|
||||
this.isAiCoreEnvSetup = true
|
||||
}
|
||||
|
||||
private async *createMessageWithOrchestration(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
try {
|
||||
// Ensure AI Core environment variable is set up (only runs once)
|
||||
this.ensureAiCoreEnvSetup()
|
||||
await this.ensureAiCoreEnvSetup()
|
||||
const model = this.getModel()
|
||||
|
||||
const orchestrationConfig: OrchestrationModuleConfig = {
|
||||
@@ -526,9 +546,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
},
|
||||
}
|
||||
|
||||
const orchestrationClient = new OrchestrationClient(orchestrationConfig, {
|
||||
resourceGroup: this.options.sapAiResourceGroup || "default",
|
||||
})
|
||||
const orchestrationClient = new OrchestrationClient(
|
||||
orchestrationConfig,
|
||||
{
|
||||
resourceGroup: this.options.sapAiResourceGroup || "default",
|
||||
},
|
||||
this.aiCoreDestination,
|
||||
)
|
||||
|
||||
const sapMessages = this.convertMessageParamToSAPMessages(messages)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
@@ -13,7 +14,9 @@ interface VercelAIGatewayHandlerOptions extends CommonApiHandlerOptions {
|
||||
vercelAiGatewayApiKey?: string
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
reasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
geminiThinkingLevel?: string
|
||||
}
|
||||
|
||||
export class VercelAIGatewayHandler implements ApiHandler {
|
||||
@@ -58,8 +61,10 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
systemPrompt,
|
||||
messages,
|
||||
{ id: modelId, info: modelInfo },
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
tools,
|
||||
this.options.geminiThinkingLevel,
|
||||
)
|
||||
let didOutputUsage: boolean = false
|
||||
|
||||
@@ -67,6 +72,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -79,7 +85,8 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// Skip reasoning content for models that don't support it (e.g., devstral, grok-4)
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning),
|
||||
@@ -91,7 +98,8 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
"reasoning_details" in delta &&
|
||||
delta.reasoning_details &&
|
||||
// @ts-ignore-next-line
|
||||
delta.reasoning_details.length // exists and non-0
|
||||
delta.reasoning_details.length && // exists and non-0
|
||||
!shouldSkipReasoningForModel(this.options.openRouterModelId)
|
||||
) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
|
||||
@@ -145,18 +145,13 @@ export function convertToOpenAiMessages(
|
||||
const thinkingBlock = []
|
||||
if (nonToolMessages.length > 0) {
|
||||
nonToolMessages.forEach((part) => {
|
||||
// @ts-ignore-next-line
|
||||
if (part.type === "text" && part.reasoning_details) {
|
||||
// @ts-ignore-next-line
|
||||
if (Array.isArray(part.reasoning_details)) {
|
||||
// @ts-ignore-next-line
|
||||
reasoningDetails.push(...part.reasoning_details)
|
||||
const anyPart = part as any
|
||||
if (part.type === "text" && anyPart.reasoning_details) {
|
||||
if (Array.isArray(anyPart.reasoning_details)) {
|
||||
reasoningDetails.push(...anyPart.reasoning_details)
|
||||
} else {
|
||||
// @ts-ignore-next-line
|
||||
reasoningDetails.push(part.reasoning_details)
|
||||
reasoningDetails.push(anyPart.reasoning_details)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
// delete part.reasoning_details
|
||||
}
|
||||
if (part.type === "thinking" && part.thinking) {
|
||||
// Reasoning details should have been moved to the text block
|
||||
@@ -216,7 +211,7 @@ export function convertToOpenAiMessages(
|
||||
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
|
||||
tool_calls: tool_calls?.length > 0 ? tool_calls : undefined,
|
||||
// Only include reasoning_details when non-empty; sending [] can trigger provider validation issues.
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error
|
||||
reasoning_details: consolidatedReasoningDetails.length > 0 ? consolidatedReasoningDetails : undefined,
|
||||
})
|
||||
}
|
||||
@@ -404,3 +399,60 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
|
||||
return anthropicMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes OpenAI messages for Gemini models by removing tool_calls that lack reasoning_details.
|
||||
*
|
||||
* Gemini models require thought signatures for tool calls. When switching providers mid-conversation,
|
||||
* historical tool calls may not include Gemini reasoning details, which can poison the next request.
|
||||
* This function drops tool_calls that lack reasoning_details and their paired tool messages.
|
||||
*
|
||||
* @param messages - Array of OpenAI chat completion messages
|
||||
* @param modelId - The model ID to check if sanitization is needed
|
||||
* @returns Sanitized array of messages (unchanged if not a Gemini model)
|
||||
*/
|
||||
export function sanitizeGeminiMessages(
|
||||
messages: OpenAI.Chat.ChatCompletionMessageParam[],
|
||||
modelId: string,
|
||||
): OpenAI.Chat.ChatCompletionMessageParam[] {
|
||||
if (!modelId.includes("gemini")) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const droppedToolCallIds = new Set<string>()
|
||||
const sanitized: OpenAI.Chat.ChatCompletionMessageParam[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant") {
|
||||
const anyMsg = msg as any
|
||||
const toolCalls = anyMsg.tool_calls
|
||||
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
||||
const reasoningDetails = anyMsg.reasoning_details
|
||||
const hasReasoningDetails = Array.isArray(reasoningDetails) && reasoningDetails.length > 0
|
||||
if (!hasReasoningDetails) {
|
||||
for (const tc of toolCalls) {
|
||||
if (tc?.id) {
|
||||
droppedToolCallIds.add(tc.id)
|
||||
}
|
||||
}
|
||||
// Keep any textual content, but drop the tool_calls themselves.
|
||||
if (anyMsg.content) {
|
||||
sanitized.push({ role: "assistant", content: anyMsg.content } as any)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.role === "tool") {
|
||||
const anyMsg = msg as any
|
||||
if (anyMsg.tool_call_id && droppedToolCallIds.has(anyMsg.tool_call_id)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sanitized.push(msg)
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { convertToOpenAiMessages } from "./openai-format"
|
||||
import { convertToOpenAiMessages, sanitizeGeminiMessages } from "./openai-format"
|
||||
import { convertToR1Format } from "./r1-format"
|
||||
import { getOpenAIToolParams } from "./tool-call-processor"
|
||||
|
||||
@@ -36,45 +36,8 @@ export async function createOpenRouterStream(
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
}
|
||||
|
||||
// Gemini models require thought signatures for tool calls. When switching providers mid-conversation,
|
||||
// historical tool calls may not include Gemini/OpenRouter reasoning details, which can poison the next request.
|
||||
// Bandaid: for Gemini only, drop tool_calls that lack reasoning_details and their paired tool messages.
|
||||
if (model.id.includes("gemini")) {
|
||||
const droppedToolCallIds = new Set<string>()
|
||||
const sanitized: OpenAI.Chat.ChatCompletionMessageParam[] = []
|
||||
|
||||
for (const msg of openAiMessages) {
|
||||
if (msg.role === "assistant") {
|
||||
const anyMsg = msg as any
|
||||
const toolCalls = anyMsg.tool_calls
|
||||
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
||||
const reasoningDetails = anyMsg.reasoning_details
|
||||
const hasReasoningDetails = Array.isArray(reasoningDetails) && reasoningDetails.length > 0
|
||||
if (!hasReasoningDetails) {
|
||||
for (const tc of toolCalls) {
|
||||
if (tc?.id) droppedToolCallIds.add(tc.id)
|
||||
}
|
||||
// Keep any textual content, but drop the tool_calls themselves.
|
||||
if (anyMsg.content) {
|
||||
sanitized.push({ role: "assistant", content: anyMsg.content } as any)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.role === "tool") {
|
||||
const anyMsg = msg as any
|
||||
if (anyMsg.tool_call_id && droppedToolCallIds.has(anyMsg.tool_call_id)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sanitized.push(msg)
|
||||
}
|
||||
|
||||
openAiMessages = sanitized
|
||||
}
|
||||
// Sanitize messages for Gemini models (removes tool_calls without reasoning_details)
|
||||
openAiMessages = sanitizeGeminiMessages(openAiMessages, model.id)
|
||||
|
||||
// prompt caching: https://openrouter.ai/docs/prompt-caching
|
||||
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
@@ -114,7 +77,7 @@ export async function createOpenRouterStream(
|
||||
{
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
@@ -134,7 +97,7 @@ export async function createOpenRouterStream(
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
})
|
||||
@@ -233,7 +196,7 @@ export async function createOpenRouterStream(
|
||||
// Skip reasoning for models that don't support it (e.g., devstral, grok-4)
|
||||
const includeReasoning = !shouldSkipReasoningForModel(model.id)
|
||||
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: maxTokens,
|
||||
|
||||
@@ -5,9 +5,11 @@ import {
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToOpenAiMessages, sanitizeGeminiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "./r1-format"
|
||||
import { getOpenAIToolParams } from "./tool-call-processor"
|
||||
|
||||
export async function createVercelAIGatewayStream(
|
||||
@@ -15,77 +17,131 @@ export async function createVercelAIGatewayStream(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
model: { id: string; info: ModelInfo },
|
||||
reasoningEffort?: string,
|
||||
thinkingBudgetTokens?: number,
|
||||
tools?: OpenAITool[],
|
||||
geminiThinkingLevel?: string,
|
||||
) {
|
||||
// Convert Anthropic messages to OpenAI format
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
|
||||
if (isClaudeSonnet1m) {
|
||||
// remove the custom :1m suffix, to create the model id openrouter API expects
|
||||
// remove the custom :1m suffix, to create the model id the API expects
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
}
|
||||
|
||||
// Sanitize messages for Gemini models (removes tool_calls without reasoning_details)
|
||||
openAiMessages = sanitizeGeminiMessages(openAiMessages, model.id)
|
||||
|
||||
// Prompt caching for supported models
|
||||
// This handles cache_control for Claude and MiniMax models
|
||||
const isAnthropicModel = model.id.startsWith("anthropic/")
|
||||
const isMinimaxModel = model.id.startsWith("minimax/")
|
||||
|
||||
if (isAnthropicModel || isMinimaxModel) {
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
// @ts-expect-error-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// Add cache_control to the last two user messages for conversation context caching
|
||||
// Add cache_control to the last two user messages
|
||||
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
|
||||
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
|
||||
lastTwoUserMessages.forEach((msg) => {
|
||||
if (typeof msg.content === "string" && msg.content.length > 0) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// Find the last text part in the message content
|
||||
const lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (lastTextPart && lastTextPart.text && lastTextPart.text.length > 0) {
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-expect-error-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Configure reasoning parameters similar to OpenRouter
|
||||
let temperature: number | undefined = 0
|
||||
// Use max tokens from model info (fetched from Vercel API)
|
||||
const maxTokens = model.info?.maxTokens || undefined
|
||||
|
||||
// Use temperature from model info, default to 0
|
||||
// Model-specific temperatures are derived in refreshVercelAiGatewayModels.ts
|
||||
let temperature: number | undefined = model.info?.temperature ?? 0
|
||||
let topP: number | undefined
|
||||
|
||||
// R1 format conversion for DeepSeek and similar reasoning models
|
||||
const requiresR1Format =
|
||||
model.id.startsWith("deepseek/deepseek-r1") ||
|
||||
model.id === "perplexity/sonar-reasoning" ||
|
||||
model.id === "qwen/qwq-32b:free" ||
|
||||
model.id === "qwen/qwq-32b"
|
||||
|
||||
if (requiresR1Format) {
|
||||
topP = 0.95
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
if (model.id.startsWith("google/gemini-3.0") || model.id === "google/gemini-3.0") {
|
||||
// Recommended value from google
|
||||
temperature = 1.0
|
||||
}
|
||||
|
||||
// Reasoning/thinking budget configuration
|
||||
let reasoning: { max_tokens: number } | undefined
|
||||
|
||||
if (isAnthropicModel) {
|
||||
const budget_tokens = thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0
|
||||
if (reasoningOn) {
|
||||
// Check if it's an Anthropic Claude model that supports thinking
|
||||
const isClaudeThinkingModel = model.id.startsWith("anthropic/claude") && model.info?.thinkingConfig
|
||||
|
||||
if (isClaudeThinkingModel) {
|
||||
// For Claude models, match OpenRouter behavior: check even if thinkingBudgetTokens is 0
|
||||
const budgetTokens = thinkingBudgetTokens || 0
|
||||
if (budgetTokens !== 0) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: budget_tokens }
|
||||
reasoning = { max_tokens: budgetTokens }
|
||||
}
|
||||
} else if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
|
||||
} else if (
|
||||
thinkingBudgetTokens &&
|
||||
thinkingBudgetTokens > 0 &&
|
||||
model.info?.thinkingConfig &&
|
||||
!(model.id.includes("gemini-3") && geminiThinkingLevel)
|
||||
) {
|
||||
// For other models with thinkingConfig, use the standard check
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: thinkingBudgetTokens }
|
||||
}
|
||||
|
||||
// @ts-ignore-next-line
|
||||
// Skip reasoning for models that don't support it (e.g., devstral, grok-4)
|
||||
const includeReasoning = !shouldSkipReasoningForModel(model.id)
|
||||
|
||||
// @ts-expect-error-next-line
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens,
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature,
|
||||
top_p: topP,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
include_reasoning: true,
|
||||
include_reasoning: includeReasoning,
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
...(model.id.includes("gemini") && geminiThinkingLevel
|
||||
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
|
||||
: {}),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import OpenAI from "openai"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ModelInfo } from "@/shared/api"
|
||||
|
||||
// Type that represents the OpenAI ResponseStream with its private properties
|
||||
// The #private property issue can be resolved by using the AsyncIterable interface
|
||||
export async function* handleResponsesApiStreamResponse(
|
||||
stream: AsyncIterable<OpenAI.Responses.ResponseStreamEvent> & { _request_id?: string | null },
|
||||
modelInfo: ModelInfo,
|
||||
calculateCost: (
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cacheWriteTokens: number,
|
||||
cacheReadTokens: number,
|
||||
) => Promise<number>,
|
||||
) {
|
||||
// Process the response stream
|
||||
for await (const chunk of stream) {
|
||||
// Handle different event types from Responses API
|
||||
if (chunk.type === "response.output_item.added") {
|
||||
const item = chunk.item
|
||||
if (item.type === "function_call" && item.id) {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
id: item.id,
|
||||
tool_call: {
|
||||
call_id: item.call_id,
|
||||
function: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
arguments: item.arguments,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
if (item.type === "reasoning" && item.encrypted_content && item.id) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: item.id,
|
||||
reasoning: "",
|
||||
redacted_data: item.encrypted_content,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
if (chunk.type === "response.output_item.done") {
|
||||
const item = chunk.item
|
||||
if (item.type === "function_call") {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
id: item.id || item.call_id,
|
||||
tool_call: {
|
||||
call_id: item.call_id,
|
||||
function: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
arguments: item.arguments,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
if (item.type === "reasoning") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: item.id,
|
||||
details: item.summary,
|
||||
reasoning: "",
|
||||
} as const
|
||||
}
|
||||
}
|
||||
if (chunk.type === "response.reasoning_summary_part.added") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: chunk.item_id,
|
||||
reasoning: chunk.part.text,
|
||||
} as const
|
||||
}
|
||||
if (chunk.type === "response.reasoning_summary_text.delta") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: chunk.item_id,
|
||||
reasoning: chunk.delta,
|
||||
} as const
|
||||
}
|
||||
if (chunk.type === "response.reasoning_summary_part.done") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: chunk.item_id,
|
||||
details: chunk.part,
|
||||
reasoning: "",
|
||||
} as const
|
||||
}
|
||||
if (chunk.type === "response.output_text.delta") {
|
||||
// Handle text content deltas
|
||||
if (chunk.delta) {
|
||||
yield {
|
||||
id: chunk.item_id,
|
||||
type: "text",
|
||||
text: chunk.delta,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
if (chunk.type === "response.reasoning_text.delta") {
|
||||
// Handle reasoning content deltas
|
||||
if (chunk.delta) {
|
||||
yield {
|
||||
id: chunk.item_id,
|
||||
type: "reasoning",
|
||||
reasoning: chunk.delta,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
if (chunk.type === "response.function_call_arguments.delta") {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: chunk.item_id,
|
||||
name: chunk.item_id,
|
||||
arguments: chunk.delta,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
if (chunk.type === "response.function_call_arguments.done") {
|
||||
// Handle completed function call
|
||||
if (chunk.item_id && chunk.name && chunk.arguments) {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: chunk.item_id,
|
||||
name: chunk.name,
|
||||
arguments: chunk.arguments,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
chunk.type === "response.incomplete" &&
|
||||
chunk.response?.status === "incomplete" &&
|
||||
chunk.response?.incomplete_details?.reason === "max_output_tokens"
|
||||
) {
|
||||
console.log("Ran out of tokens")
|
||||
if (chunk.response?.output_text?.length > 0) {
|
||||
console.log("Partial output:", chunk.response.output_text)
|
||||
} else {
|
||||
console.log("Ran out of tokens during reasoning")
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.completed" && chunk.response?.usage) {
|
||||
// Handle usage information when response is complete
|
||||
const usage = chunk.response.usage
|
||||
const inputTokens = usage.input_tokens || 0
|
||||
const outputTokens = usage.output_tokens || 0
|
||||
const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0
|
||||
const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0
|
||||
const totalTokens = usage.total_tokens || 0
|
||||
const totalCost = await calculateCost(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
Logger.log(`Total tokens from Responses API usage: ${totalTokens}`)
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens)
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: nonCachedInputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
id: chunk.response.id,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models"
|
||||
import { ApiFormat, OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models"
|
||||
import axios from "axios"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import { DEFAULT_EXTERNAL_OCA_BASE_URL, DEFAULT_INTERNAL_OCA_BASE_URL } from "@/services/auth/oca/utils/constants"
|
||||
import {
|
||||
CHAT_COMPLETIONS_API,
|
||||
DEFAULT_EXTERNAL_OCA_BASE_URL,
|
||||
DEFAULT_INTERNAL_OCA_BASE_URL,
|
||||
RESPONSES_API,
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
@@ -57,6 +62,11 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
defaultModelId = modelId
|
||||
}
|
||||
const modelInfo = model.model_info
|
||||
const supportedApiList = modelInfo.supported_api_list ?? [CHAT_COMPLETIONS_API]
|
||||
const apiFormat: ApiFormat = supportedApiList.includes(RESPONSES_API)
|
||||
? ApiFormat.OPENAI_RESPONSES
|
||||
: ApiFormat.OPENAI_CHAT
|
||||
console.log(modelId, supportedApiList)
|
||||
models[modelId] = OcaModelInfo.create({
|
||||
maxTokens: model.litellm_params?.max_tokens || -1,
|
||||
contextWindow: modelInfo.context_window,
|
||||
@@ -73,6 +83,9 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
temperature: modelInfo.temperature || 0,
|
||||
banner: modelInfo.banner,
|
||||
modelName: modelId,
|
||||
apiFormat: apiFormat,
|
||||
supportsReasoning: modelInfo.is_reasoning_model || false,
|
||||
reasoningEffortOptions: modelInfo.reasoning_effort_options || [],
|
||||
})
|
||||
}
|
||||
console.log("OCA models fetched", models)
|
||||
@@ -91,6 +104,25 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
? apiConfiguration.actModeOcaModelId
|
||||
: defaultModelId!
|
||||
|
||||
let planModeOcaReasoningEffort
|
||||
let actModeOcaReasoningEffort
|
||||
if (
|
||||
models[planModeSelectedModelId].supportsReasoning &&
|
||||
models[planModeSelectedModelId].reasoningEffortOptions.length > 0
|
||||
) {
|
||||
planModeOcaReasoningEffort = apiConfiguration.planModeOcaReasoningEffort
|
||||
? apiConfiguration.planModeOcaReasoningEffort
|
||||
: models[planModeSelectedModelId].reasoningEffortOptions[0]
|
||||
}
|
||||
if (
|
||||
models[actModeSelectedModelId].supportsReasoning &&
|
||||
models[actModeSelectedModelId].reasoningEffortOptions.length > 0
|
||||
) {
|
||||
actModeOcaReasoningEffort = apiConfiguration.actModeOcaReasoningEffort
|
||||
? apiConfiguration.actModeOcaReasoningEffort
|
||||
: models[actModeSelectedModelId].reasoningEffortOptions[0]
|
||||
}
|
||||
|
||||
// Build updates object based on plan/act mode setting
|
||||
const updates: Partial<GlobalStateAndSettings> = {}
|
||||
|
||||
@@ -98,15 +130,19 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
if (currentMode === "plan") {
|
||||
updates.planModeOcaModelId = planModeSelectedModelId
|
||||
updates.planModeOcaModelInfo = models[planModeSelectedModelId]
|
||||
updates.planModeOcaReasoningEffort = planModeOcaReasoningEffort
|
||||
} else {
|
||||
updates.actModeOcaModelId = actModeSelectedModelId
|
||||
updates.actModeOcaModelInfo = models[actModeSelectedModelId]
|
||||
updates.actModeOcaReasoningEffort = actModeOcaReasoningEffort
|
||||
}
|
||||
} else {
|
||||
updates.planModeOcaModelId = planModeSelectedModelId
|
||||
updates.planModeOcaModelInfo = models[planModeSelectedModelId]
|
||||
updates.planModeOcaReasoningEffort = planModeOcaReasoningEffort
|
||||
updates.actModeOcaModelId = actModeSelectedModelId
|
||||
updates.actModeOcaModelInfo = models[actModeSelectedModelId]
|
||||
updates.actModeOcaReasoningEffort = actModeOcaReasoningEffort
|
||||
}
|
||||
|
||||
// Update state directly using batch method
|
||||
|
||||
@@ -7,6 +7,73 @@ import path from "path"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Derives thinkingConfig from model ID and tags.
|
||||
* The Vercel API only provides a "reasoning" tag to indicate support,
|
||||
* so we derive the specific configuration based on model patterns.
|
||||
*/
|
||||
function deriveThinkingConfig(modelId: string, tags?: string[]): ModelInfo["thinkingConfig"] {
|
||||
if (!tags?.includes("reasoning")) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Anthropic Claude models
|
||||
if (modelId.startsWith("anthropic/claude")) {
|
||||
return { maxBudget: 8192 }
|
||||
}
|
||||
|
||||
// Google Gemini models
|
||||
if (modelId.includes("gemini-3")) {
|
||||
return {
|
||||
maxBudget: 32767,
|
||||
supportsThinkingLevel: true,
|
||||
geminiThinkingLevel: "high",
|
||||
}
|
||||
}
|
||||
|
||||
// DeepSeek R1 models
|
||||
if (modelId.startsWith("deepseek/deepseek-r1")) {
|
||||
return { maxBudget: 8192 }
|
||||
}
|
||||
|
||||
// OpenAI o-series reasoning models
|
||||
if (modelId.startsWith("openai/o1") || modelId.startsWith("openai/o3")) {
|
||||
return { maxBudget: 32000 }
|
||||
}
|
||||
|
||||
// Qwen QwQ models (specific IDs to match OpenRouter)
|
||||
if (modelId === "qwen/qwq-32b:free" || modelId === "qwen/qwq-32b") {
|
||||
return { maxBudget: 32000 }
|
||||
}
|
||||
|
||||
// Default for other reasoning models
|
||||
return { maxBudget: 32000 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives recommended temperature for specific model types.
|
||||
* Returns undefined to use the default (0).
|
||||
*/
|
||||
function deriveTemperature(modelId: string): number | undefined {
|
||||
// DeepSeek R1 and similar reasoning models recommend 0.7
|
||||
// Use specific model IDs to match OpenRouter behavior
|
||||
if (
|
||||
modelId.startsWith("deepseek/deepseek-r1") ||
|
||||
modelId === "perplexity/sonar-reasoning" ||
|
||||
modelId === "qwen/qwq-32b:free" ||
|
||||
modelId === "qwen/qwq-32b"
|
||||
) {
|
||||
return 0.7
|
||||
}
|
||||
|
||||
// Gemini 3.0 recommends temperature 1.0
|
||||
if (modelId.startsWith("google/gemini-3.0") || modelId === "google/gemini-3.0") {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Core function: Refreshes Vercel AI Gateway models and returns application types
|
||||
* @param _controller The controller instance (unused)
|
||||
@@ -18,7 +85,7 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
|
||||
try {
|
||||
const response = await axios.get("https://ai-gateway.vercel.sh/v1/models", getAxiosSettings())
|
||||
const response = await axios.get("https://ai-gateway.vercel.sh/v1/models?include_mappings=true", getAxiosSettings())
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
@@ -44,6 +111,8 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
|
||||
supportsImages: true, // assume all models support images since vercel ai doesn't give this info
|
||||
supportsPromptCache: !!(rawModel.pricing?.input_cache_read && rawModel.pricing?.input_cache_write),
|
||||
description: rawModel.description ?? "",
|
||||
thinkingConfig: deriveThinkingConfig(rawModel.id, rawModel.tags),
|
||||
temperature: deriveTemperature(rawModel.id),
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
// Keep track of active didBecomeVisible subscriptions
|
||||
const activeDidBecomeVisibleSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to didBecomeVisible events
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToDidBecomeVisible(
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
console.log(`[DEBUG] set up didBecomeVisible subscription`)
|
||||
|
||||
// Add this subscription to the active subscriptions
|
||||
activeDidBecomeVisibleSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeDidBecomeVisibleSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "didBecomeVisible_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a didBecomeVisible event to all active subscribers
|
||||
*/
|
||||
export async function sendDidBecomeVisibleEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeDidBecomeVisibleSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending didBecomeVisible event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeDidBecomeVisibleSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
// Keep track of active focus chat input subscriptions
|
||||
const focusChatInputSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to focus chat input events
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request
|
||||
*/
|
||||
export async function subscribeToFocusChatInput(
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
// Add this subscription to the active subscriptions
|
||||
focusChatInputSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
focusChatInputSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "focus_chat_input_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a focus chat input event to all active subscribers
|
||||
*/
|
||||
export async function sendFocusChatInputEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(focusChatInputSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending focus chat input event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
focusChatInputSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { ShowWebviewEvent } from "@shared/proto/cline/ui"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
// Keep track of active show webview subscriptions
|
||||
const showWebviewSubscriptions = new Set<StreamingResponseHandler<ShowWebviewEvent>>()
|
||||
|
||||
/**
|
||||
* Subscribe to show webview events
|
||||
* @param controller The controller instance
|
||||
* @param request The show webview request containing preserveEditorFocus flag
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request
|
||||
*/
|
||||
export async function subscribeToShowWebview(
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<ShowWebviewEvent>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
// Add this subscription to the active subscriptions
|
||||
showWebviewSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
showWebviewSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "show_webview_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a show webview event to all active subscribers
|
||||
* @param preserveEditorFocus When true, the webview should not steal focus from the editor
|
||||
*/
|
||||
export async function sendShowWebviewEvent(preserveEditorFocus: boolean = false): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(showWebviewSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = ShowWebviewEvent.create({ preserveEditorFocus })
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending show webview event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
showWebviewSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { HookOutputStreamMeta } from "@shared/ExtensionMessage"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import type { HookOutput } from "@shared/proto/cline/hooks"
|
||||
import { MessageStateHandler } from "../task/message-state"
|
||||
import { HookExecutionError } from "./HookError"
|
||||
import { HookFactory } from "./hook-factory"
|
||||
@@ -32,6 +34,20 @@ export interface HookExecutionResult {
|
||||
wasCancelled: boolean
|
||||
}
|
||||
|
||||
function fromHookOutput(output: HookOutput): HookExecutionResult {
|
||||
// HookOutput is protobuf-generated, so fields are defaulted (e.g. ""). Treat empty
|
||||
// strings as “unset” in the hook executor API.
|
||||
const contextModification = output.contextModification?.trim() ? output.contextModification : undefined
|
||||
const errorMessage = output.errorMessage?.trim() ? output.errorMessage : undefined
|
||||
|
||||
return {
|
||||
cancel: output.cancel,
|
||||
contextModification,
|
||||
errorMessage,
|
||||
wasCancelled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a hook with standardized error handling, status tracking, and cleanup.
|
||||
* This consolidates the common pattern used across all hook execution sites.
|
||||
@@ -61,23 +77,29 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
|
||||
const hasHook = await hookFactory.hasHook(hookName)
|
||||
|
||||
if (!hasHook) {
|
||||
return {
|
||||
wasCancelled: false,
|
||||
}
|
||||
return { wasCancelled: false }
|
||||
}
|
||||
|
||||
let hookMessageTs: number | undefined
|
||||
const abortController = new AbortController()
|
||||
|
||||
// Declare hookInfo with empty default - populated inside try block.
|
||||
// If getHookInfo throws, error handlers will use the empty default.
|
||||
let hookInfo: { scriptPaths: string[] } = { scriptPaths: [] }
|
||||
|
||||
try {
|
||||
// Get hook info including script paths
|
||||
hookInfo = await hookFactory.getHookInfo(hookName)
|
||||
|
||||
// Show hook execution indicator and capture timestamp
|
||||
const hookMetadata = {
|
||||
hookName,
|
||||
...(options.toolName && { toolName: options.toolName }),
|
||||
status: "running",
|
||||
scriptPaths: hookInfo.scriptPaths,
|
||||
...(options.pendingToolInfo && { pendingToolInfo: options.pendingToolInfo }),
|
||||
}
|
||||
hookMessageTs = await say("hook", JSON.stringify(hookMetadata))
|
||||
hookMessageTs = await say("hook_status", JSON.stringify(hookMetadata))
|
||||
|
||||
// Reorder messages immediately so hook UI appears above tool UI
|
||||
// This must happen right after creating the hook message, before the hook runs
|
||||
@@ -96,8 +118,23 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
|
||||
}
|
||||
|
||||
// Create streaming callback
|
||||
const streamCallback = async (line: string) => {
|
||||
await say("hook_output", line)
|
||||
const streamCallback = async (line: string, stream: "stdout" | "stderr", meta?: HookOutputStreamMeta) => {
|
||||
// Preserve script identity for multi-hook (global + workspace) scenarios.
|
||||
// Without this, concurrent hooks interleave output and it's hard to tell which
|
||||
// script produced which line (and can look like only one hook is printing).
|
||||
//
|
||||
// NOTE: We keep backward compatibility by encoding metadata into the string.
|
||||
// The CLI prints this as-is in verbose mode.
|
||||
const prefixParts: string[] = []
|
||||
if (meta?.source) prefixParts.push(meta.source)
|
||||
prefixParts.push(stream)
|
||||
// Use a shortened path for readability; full path is still available in hook_status.
|
||||
if (meta?.scriptPath) {
|
||||
const parts = meta.scriptPath.split(/[/\\]/).filter(Boolean)
|
||||
prefixParts.push(parts.slice(-3).join("/"))
|
||||
}
|
||||
const prefix = prefixParts.length ? `[${prefixParts.join(" ")}] ` : ""
|
||||
await say("hook_output_stream", prefix + line)
|
||||
}
|
||||
|
||||
// Create and execute hook
|
||||
@@ -116,6 +153,11 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
|
||||
|
||||
console.log(`[${hookName} Hook]`, result)
|
||||
|
||||
// NoOp hooks return proto defaults; preserve the minimal legacy return shape.
|
||||
if (result.cancel === false && result.contextModification === "" && result.errorMessage === "") {
|
||||
return { wasCancelled: false }
|
||||
}
|
||||
|
||||
// Check if hook wants to cancel
|
||||
if (result.cancel === true) {
|
||||
// Update hook status to cancelled
|
||||
@@ -126,15 +168,11 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
|
||||
status: "cancelled",
|
||||
exitCode: 130,
|
||||
hasJsonResponse: true,
|
||||
scriptPaths: hookInfo.scriptPaths,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
cancel: true,
|
||||
contextModification: result.contextModification,
|
||||
errorMessage: result.errorMessage,
|
||||
wasCancelled: false,
|
||||
}
|
||||
return fromHookOutput(result)
|
||||
}
|
||||
|
||||
// Clear active hook execution after successful completion (only if cancellable)
|
||||
@@ -150,15 +188,11 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
|
||||
status: "completed",
|
||||
exitCode: 0,
|
||||
hasJsonResponse: true,
|
||||
scriptPaths: hookInfo.scriptPaths,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
cancel: result.cancel,
|
||||
contextModification: result.contextModification,
|
||||
errorMessage: result.errorMessage,
|
||||
wasCancelled: false,
|
||||
}
|
||||
return fromHookOutput(result)
|
||||
} catch (hookError) {
|
||||
// Clear active hook execution (only if cancellable)
|
||||
if (isCancellable && clearActiveHookExecution) {
|
||||
@@ -173,6 +207,7 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
|
||||
hookName,
|
||||
status: "cancelled",
|
||||
exitCode: 130,
|
||||
scriptPaths: hookInfo.scriptPaths,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,6 +227,7 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
|
||||
hookName,
|
||||
status: "failed",
|
||||
exitCode: errorInfo?.exitCode ?? 1,
|
||||
scriptPaths: hookInfo.scriptPaths,
|
||||
...(errorInfo && {
|
||||
error: {
|
||||
type: errorInfo.type,
|
||||
@@ -266,7 +302,7 @@ async function reorderHookAndToolMessages(messageStateHandler: MessageStateHandl
|
||||
// Check if there are any hook messages after the tool message
|
||||
let hasHookMessagesAfterTool = false
|
||||
for (let i = lastToolMessageIndex + 1; i < clineMessages.length; i++) {
|
||||
if (clineMessages[i].say === "hook" || clineMessages[i].say === "hook_output") {
|
||||
if (clineMessages[i].say === "hook_status" || clineMessages[i].say === "hook_output_stream") {
|
||||
hasHookMessagesAfterTool = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -215,16 +215,23 @@ class NoOpRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
* @returns A successful hook output (no cancellation)
|
||||
*/
|
||||
override async [exec](_: HookInput): Promise<HookOutput> {
|
||||
return HookOutput.create({
|
||||
cancel: false,
|
||||
})
|
||||
// HookOutput is a protobuf-generated type with non-optional fields.
|
||||
// Protobuf defaults: cancel=false, contextModification="", errorMessage=""
|
||||
return HookOutput.create({ cancel: false })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback type for streaming hook output
|
||||
*/
|
||||
export type HookStreamCallback = (line: string, stream: "stdout" | "stderr") => void
|
||||
export type HookStreamCallback = (
|
||||
line: string,
|
||||
stream: "stdout" | "stderr",
|
||||
meta?: {
|
||||
source: "global" | "workspace"
|
||||
scriptPath: string
|
||||
},
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Executes a hook script as a child process with real-time output streaming.
|
||||
@@ -300,7 +307,12 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
if (this.streamCallback) {
|
||||
const callback = this.streamCallback
|
||||
hookProcess.on("line", (line: string, stream: "stdout" | "stderr") => {
|
||||
callback(line, stream)
|
||||
// NOTE: HookProcess emits a synthetic empty line (""), used as a "start of output" marker.
|
||||
// Preserve it for now so downstream can keep existing behavior.
|
||||
callback(line, stream, {
|
||||
source: this.source,
|
||||
scriptPath: this.scriptPath,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -687,6 +699,21 @@ function isExpectedHookError(error: unknown): boolean {
|
||||
}
|
||||
|
||||
export class HookFactory {
|
||||
/**
|
||||
* Get information about discovered hooks including their script paths
|
||||
* @param hookName The type of hook to query
|
||||
* @returns Object containing array of script paths
|
||||
*/
|
||||
async getHookInfo<Name extends HookName>(
|
||||
hookName: Name,
|
||||
): Promise<{
|
||||
scriptPaths: string[]
|
||||
}> {
|
||||
const { HookDiscoveryCache } = await import("./HookDiscoveryCache")
|
||||
const scripts = await HookDiscoveryCache.getInstance().get(hookName)
|
||||
return { scriptPaths: scripts }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any hook scripts exist for the given hook name
|
||||
* @returns true if at least one hook script exists, false otherwise
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,292 @@
|
||||
import { ParseEntry, parse } from "shell-quote"
|
||||
import { COMMAND_PERMISSIONS_ENV_VAR, CommandPermissionConfig, PermissionValidationResult, ShellOperatorMatch } from "./types"
|
||||
|
||||
const OPERATOR_DESCRIPTIONS: Record<string, string> = {
|
||||
";": "command chaining (semicolon)",
|
||||
"&&": "command chaining (AND)",
|
||||
"||": "command chaining (OR)",
|
||||
"|": "pipe",
|
||||
">": "output redirection",
|
||||
">>": "append redirection",
|
||||
"<": "input redirection",
|
||||
">&": "file descriptor redirection",
|
||||
"<&": "file descriptor duplication",
|
||||
"|&": "pipe with stderr",
|
||||
}
|
||||
|
||||
const LINE_SEPARATOR_REGEX = /[\n\r\u2028\u2029\u0085]/
|
||||
const LINE_SEPARATOR_DESCRIPTIONS: Record<string, ShellOperatorMatch> = {
|
||||
"\n": { operator: "\\n", description: "newline (command separator)" },
|
||||
"\r": { operator: "\\r", description: "carriage return (potential command separator)" },
|
||||
"\u2028": { operator: "U+2028", description: "unicode line separator" },
|
||||
"\u2029": { operator: "U+2029", description: "unicode paragraph separator" },
|
||||
"\u0085": { operator: "U+0085", description: "unicode next line" },
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls command execution permissions based on environment variable configuration.
|
||||
* Uses glob pattern matching to allow/deny specific commands.
|
||||
*
|
||||
* Configuration is read from the CLINE_COMMAND_PERMISSIONS environment variable.
|
||||
* Format: {"allow": ["pattern1", "pattern2"], "deny": ["pattern3"]}
|
||||
*
|
||||
* Rule evaluation:
|
||||
* 1. If shell operators are detected outside quotes → DENIED (security)
|
||||
* 2. If deny rules are defined and command matches a deny pattern → DENIED
|
||||
* 3. If allow rules are defined and command matches an allow pattern → ALLOWED
|
||||
* 4. If allow rules are defined but command doesn't match any → DENIED (deny by default)
|
||||
* 5. If no rules are defined (env var not set) → ALLOWED (backward compatibility)
|
||||
*/
|
||||
export class CommandPermissionController {
|
||||
private config: CommandPermissionConfig | null = null
|
||||
|
||||
constructor() {
|
||||
this.config = this.parseConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the CLINE_COMMAND_PERMISSIONS environment variable
|
||||
* @returns Parsed configuration or null if not set or invalid
|
||||
*/
|
||||
private parseConfig(): CommandPermissionConfig | null {
|
||||
const envValue = process.env[COMMAND_PERMISSIONS_ENV_VAR]
|
||||
if (!envValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(envValue)
|
||||
return {
|
||||
allow: Array.isArray(parsed.allow) ? parsed.allow : undefined,
|
||||
deny: Array.isArray(parsed.deny) ? parsed.deny : undefined,
|
||||
allowOperators: Array.isArray(parsed.allowOperators) ? parsed.allowOperators : undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse ${COMMAND_PERMISSIONS_ENV_VAR}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an operator is in the allowOperators list
|
||||
* @param operator - The operator to check
|
||||
* @returns true if the operator is allowed
|
||||
*/
|
||||
private isOperatorAllowed(operator: string): boolean {
|
||||
return Boolean(this.config?.allowOperators?.includes(operator))
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a command is allowed to execute based on configured permissions
|
||||
* @param command - The command string to validate
|
||||
* @returns PermissionValidationResult indicating if command is allowed and why
|
||||
*/
|
||||
validateCommand(command: string): PermissionValidationResult {
|
||||
// No config = allow everything (backward compatibility)
|
||||
if (!this.config) {
|
||||
return { allowed: true, reason: "no_config" }
|
||||
}
|
||||
|
||||
// Check for shell operators FIRST (security check)
|
||||
const shellOperator = this.detectShellOperator(command)
|
||||
if (shellOperator) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "shell_operator_detected",
|
||||
detectedOperator: shellOperator.operator,
|
||||
}
|
||||
}
|
||||
|
||||
// Check deny rules first (deny takes precedence)
|
||||
if (this.config.deny) {
|
||||
for (const pattern of this.config.deny) {
|
||||
if (this.matchesPattern(command, pattern)) {
|
||||
return { allowed: false, matchedPattern: pattern, reason: "denied" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check allow rules
|
||||
if (this.config.allow && this.config.allow.length > 0) {
|
||||
for (const pattern of this.config.allow) {
|
||||
if (this.matchesPattern(command, pattern)) {
|
||||
return { allowed: true, matchedPattern: pattern, reason: "allowed" }
|
||||
}
|
||||
}
|
||||
// Allow rules defined but no match = deny by default
|
||||
return { allowed: false, reason: "no_match_deny_default" }
|
||||
}
|
||||
|
||||
// No allow rules defined, and no deny matched = allow
|
||||
return { allowed: true, reason: "no_config" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command matches a wildcard pattern.
|
||||
*
|
||||
* Uses simple wildcard matching where `*` matches any characters (including `/` and newlines).
|
||||
* This is different from file glob matching where `*` doesn't cross directory boundaries.
|
||||
* For command permission matching, we want `*` to match any sequence of characters
|
||||
* so that patterns like `gh pr comment *` match `gh pr comment 123 --body-file /tmp/file.txt`
|
||||
* or commands with multiline arguments like `gh pr comment 123 --body "line1\nline2"`.
|
||||
*
|
||||
* Supported patterns:
|
||||
* - `*` matches any sequence of characters (including / and newlines)
|
||||
* - `?` matches exactly one character
|
||||
*
|
||||
* @param command - The command to check
|
||||
* @param pattern - The wildcard pattern to match against
|
||||
* @returns true if command matches the pattern
|
||||
*/
|
||||
private matchesPattern(command: string, pattern: string): boolean {
|
||||
const regex = new RegExp(
|
||||
"^" +
|
||||
pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars
|
||||
.replace(/\*/g, ".*") // * becomes .*
|
||||
.replace(/\?/g, ".") + // ? becomes .
|
||||
"$",
|
||||
"s", // s flag enables dotAll (. matches newlines)
|
||||
)
|
||||
return regex.test(command)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect shell operators using shell-quote parser.
|
||||
* This prevents command chaining/injection attacks like:
|
||||
* gh pr view 123; rm -rf /
|
||||
* gh pr view 123 && malicious_command
|
||||
* gh pr view $(malicious_command)
|
||||
*
|
||||
* Operators inside quotes are allowed (they're literal characters):
|
||||
* echo "hello; world" # OK - semicolon is inside quotes
|
||||
*
|
||||
* @param command - The command string to check
|
||||
* @returns ShellOperatorMatch if an operator is found outside quotes, null otherwise
|
||||
*/
|
||||
private detectShellOperator(command: string): ShellOperatorMatch | null {
|
||||
const dangerousCharMatch = this.detectDangerousCharsOutsideQuotes(command)
|
||||
if (dangerousCharMatch) {
|
||||
return dangerousCharMatch
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the command using shell-quote
|
||||
// shell-quote returns an array where:
|
||||
// - strings are regular arguments
|
||||
// - objects with 'op' key are shell operators
|
||||
// - objects with 'comment' key are comments
|
||||
// - objects with 'pattern' key are glob patterns (we allow these)
|
||||
const parsed = parse(command, (varName: string) => `$${varName}`)
|
||||
|
||||
// Check each parsed element for operators
|
||||
for (const entry of parsed) {
|
||||
const operatorMatch = this.checkParsedEntry(entry)
|
||||
if (operatorMatch) {
|
||||
return operatorMatch
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch {
|
||||
// If parsing fails, be conservative and block the command
|
||||
// This could indicate malformed shell syntax being used for injection
|
||||
return { operator: "parse_error", description: "command parsing failed (potential injection)" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect dangerous characters outside of quoted strings.
|
||||
* This includes newlines, carriage returns, unicode line separators, and backticks.
|
||||
*
|
||||
* For newlines/carriage returns: They are safe inside ANY quotes (single or double)
|
||||
* because they become literal characters in the argument value.
|
||||
*
|
||||
* For backticks: They are only safe inside SINGLE quotes because double quotes
|
||||
* still allow command substitution.
|
||||
*
|
||||
* Examples:
|
||||
* gh pr comment 123 --body "line1\nline2" -> ALLOWED (newline in quotes)
|
||||
* gh pr comment 123\nrm -rf / -> BLOCKED (newline outside quotes)
|
||||
* echo `date` -> BLOCKED (backtick outside quotes)
|
||||
* echo "hello `date`" -> BLOCKED (backtick in double quotes - executes!)
|
||||
* echo 'hello `date`' -> ALLOWED (backtick in single quotes - literal)
|
||||
*
|
||||
* @param command - The command string to check
|
||||
* @returns ShellOperatorMatch if dangerous chars found outside appropriate quotes, null otherwise
|
||||
*/
|
||||
private detectDangerousCharsOutsideQuotes(command: string): ShellOperatorMatch | null {
|
||||
let inSingleQuote = false
|
||||
let inDoubleQuote = false
|
||||
let isEscaped = false
|
||||
|
||||
for (let i = 0; i < command.length; i++) {
|
||||
const char = command[i]
|
||||
|
||||
// If previous char was an unescaped backslash, this char is escaped
|
||||
if (isEscaped) {
|
||||
isEscaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for escape sequence (only outside single quotes)
|
||||
// In single quotes, backslashes are literal
|
||||
if (char === "\\" && !inSingleQuote) {
|
||||
isEscaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle double quotes - we track them to know when single quotes are literal
|
||||
if (char === '"' && !inSingleQuote) {
|
||||
inDoubleQuote = !inDoubleQuote
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle single quotes - only toggle when NOT inside double quotes
|
||||
// Inside double quotes, single quotes are literal characters
|
||||
if (char === "'" && !inDoubleQuote) {
|
||||
inSingleQuote = !inSingleQuote
|
||||
continue
|
||||
}
|
||||
|
||||
const inAnyQuote = inSingleQuote || inDoubleQuote
|
||||
|
||||
// Check for newlines and carriage returns outside ANY quotes
|
||||
// These are command separators when outside quotes
|
||||
if (!inAnyQuote && LINE_SEPARATOR_REGEX.test(char)) {
|
||||
return LINE_SEPARATOR_DESCRIPTIONS[char]
|
||||
}
|
||||
|
||||
// Check for backticks outside SINGLE quotes only
|
||||
// Backticks in double quotes ARE executed as command substitution in bash
|
||||
if (char === "`" && !inSingleQuote) {
|
||||
return { operator: "`", description: "command substitution (backtick)" }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a parsed entry from shell-quote for dangerous operators.
|
||||
*
|
||||
* @param entry - A parsed entry from shell-quote
|
||||
* @returns ShellOperatorMatch if dangerous operator found, null otherwise
|
||||
*/
|
||||
private checkParsedEntry(entry: ParseEntry): ShellOperatorMatch | null {
|
||||
// null entries, string entries, glob patterns, and comments are safe
|
||||
if (!entry || typeof entry === "string" || "pattern" in entry || "comment" in entry) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof entry.op === "string") {
|
||||
if (this.isOperatorAllowed(entry.op)) {
|
||||
return null
|
||||
}
|
||||
const description = OPERATOR_DESCRIPTIONS[entry.op] || `shell operator (${entry.op})`
|
||||
return { operator: entry.op, description }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CommandPermissionController } from "./CommandPermissionController"
|
||||
export type { CommandPermissionConfig, PermissionValidationResult } from "./types"
|
||||
export { COMMAND_PERMISSIONS_ENV_VAR } from "./types"
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Configuration structure for command permissions from environment variable
|
||||
*/
|
||||
export interface CommandPermissionConfig {
|
||||
allow?: string[] // Glob patterns for allowed commands
|
||||
deny?: string[] // Glob patterns for denied commands
|
||||
allowOperators?: string[] // Shell operators to allow (e.g., [">", ">>"] to allow file writing)
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a permission validation check
|
||||
*/
|
||||
export interface PermissionValidationResult {
|
||||
allowed: boolean
|
||||
matchedPattern?: string // The pattern that matched (for error messages)
|
||||
reason: "no_config" | "allowed" | "denied" | "no_match_deny_default" | "shell_operator_detected"
|
||||
detectedOperator?: string // The shell operator that was detected (for error messages)
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment variable name for command permissions
|
||||
*/
|
||||
export const COMMAND_PERMISSIONS_ENV_VAR = "CLINE_COMMAND_PERMISSIONS"
|
||||
|
||||
/**
|
||||
* Shell operators that indicate command chaining, piping, substitution, or redirection.
|
||||
* These are security-sensitive because they can be used to bypass command restrictions.
|
||||
*/
|
||||
export interface ShellOperatorMatch {
|
||||
operator: string
|
||||
description: string
|
||||
}
|
||||
@@ -25,6 +25,9 @@ export const formatResponse = {
|
||||
clineIgnoreError: (path: string) =>
|
||||
`Access to ${path} is blocked by the .clineignore file settings. You must try to continue in the task without using this file, or ask the user to update the .clineignore file.`,
|
||||
|
||||
permissionDeniedError: (reason: string) =>
|
||||
`Command execution blocked by CLINE_COMMAND_PERMISSIONS: ${reason}. You must try a different approach or ask the user to update the permission settings.`,
|
||||
|
||||
noToolsUsed: (usingNativeToolCalls: boolean) =>
|
||||
usingNativeToolCalls
|
||||
? "[ERROR] You did not use a tool in your previous response! Please retry with a tool use."
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ CAPABILITIES
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the replace_in_file tool to implement changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
@@ -618,6 +618,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
@@ -582,6 +582,7 @@ CAPABILITIES
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the replace_in_file tool to implement changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+1
@@ -542,6 +542,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
@@ -598,6 +598,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+2
-2
@@ -265,7 +265,7 @@
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_fetch",
|
||||
"description": "Fetches and analyzes content from a specified URL.",
|
||||
"description": "Fetches and analyzes content from a specified URL. IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
@@ -295,7 +295,7 @@
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"description": "Performs a web search and returns relevant results with titles and URLs.",
|
||||
"description": "Performs a web search and returns relevant results with titles and URLs. IMPORTANT: If an MCP-provided web search tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
|
||||
@@ -9,7 +9,7 @@ const getCapabilitiesTemplateText = (context: SystemPromptContext) => `CAPABILIT
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the replace_in_file tool to implement changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.{{BROWSER_CAPABILITIES}}
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.{{BROWSER_CAPABILITIES}}{{WEB_TOOLS_CAPABILITIES}}
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.`
|
||||
|
||||
export async function getCapabilitiesSection(variant: PromptVariant, context: SystemPromptContext): Promise<string> {
|
||||
@@ -20,10 +20,16 @@ export async function getCapabilitiesSection(variant: PromptVariant, context: Sy
|
||||
? `\n- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.\n\t- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.`
|
||||
: ""
|
||||
|
||||
const webToolsCapabilities =
|
||||
context.providerInfo.providerId === "cline" && context.clineWebToolsEnabled === true
|
||||
? `\n- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.`
|
||||
: ""
|
||||
|
||||
const templateEngine = new TemplateEngine()
|
||||
return templateEngine.resolve(template, context, {
|
||||
BROWSER_SUPPORT: browserSupport,
|
||||
BROWSER_CAPABILITIES: browserCapabilities,
|
||||
WEB_TOOLS_CAPABILITIES: webToolsCapabilities,
|
||||
CWD: context.cwd || process.cwd(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ const NATIVE_NEXT_GEN: ClineToolSpec = {
|
||||
variant: ModelFamily.NATIVE_NEXT_GEN,
|
||||
id: ClineDefaultTool.WEB_FETCH,
|
||||
name: "web_fetch",
|
||||
description: "Fetches and analyzes content from a specified URL.",
|
||||
description:
|
||||
"Fetches and analyzes content from a specified URL. IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.",
|
||||
contextRequirements: (context) => context.providerInfo.providerId === "cline" && context.clineWebToolsEnabled === true,
|
||||
parameters: [
|
||||
{
|
||||
|
||||
@@ -44,7 +44,8 @@ const NATIVE_NEXT_GEN: ClineToolSpec = {
|
||||
variant: ModelFamily.NATIVE_NEXT_GEN,
|
||||
id: ClineDefaultTool.WEB_SEARCH,
|
||||
name: "web_search",
|
||||
description: "Performs a web search and returns relevant results with titles and URLs.",
|
||||
description:
|
||||
"Performs a web search and returns relevant results with titles and URLs. IMPORTANT: If an MCP-provided web search tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.",
|
||||
contextRequirements: (context) => context.providerInfo.providerId === "cline" && context.clineWebToolsEnabled === true,
|
||||
parameters: [
|
||||
{
|
||||
|
||||
@@ -520,6 +520,7 @@ export class StateManager {
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
azureIdentity,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
@@ -597,11 +598,14 @@ export class StateManager {
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
planModeOcaModelId,
|
||||
planModeOcaModelInfo,
|
||||
planModeOcaReasoningEffort,
|
||||
planModeHicapModelId,
|
||||
planModeHicapModelInfo,
|
||||
planModeAihubmixModelId,
|
||||
planModeAihubmixModelInfo,
|
||||
planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo,
|
||||
geminiPlanModeThinkingLevel,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
@@ -635,11 +639,14 @@ export class StateManager {
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
actModeOcaModelId,
|
||||
actModeOcaModelInfo,
|
||||
actModeOcaReasoningEffort,
|
||||
actModeHicapModelId,
|
||||
actModeHicapModelInfo,
|
||||
actModeAihubmixModelId,
|
||||
actModeAihubmixModelInfo,
|
||||
actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo,
|
||||
geminiActModeThinkingLevel,
|
||||
} = apiConfiguration
|
||||
|
||||
@@ -677,11 +684,14 @@ export class StateManager {
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
planModeOcaModelId,
|
||||
planModeOcaModelInfo,
|
||||
planModeOcaReasoningEffort,
|
||||
planModeHicapModelId,
|
||||
planModeHicapModelInfo,
|
||||
planModeAihubmixModelId,
|
||||
planModeAihubmixModelInfo,
|
||||
planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo,
|
||||
geminiPlanModeThinkingLevel,
|
||||
|
||||
// Act mode configuration updates
|
||||
@@ -716,11 +726,14 @@ export class StateManager {
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
actModeOcaModelId,
|
||||
actModeOcaModelInfo,
|
||||
actModeOcaReasoningEffort,
|
||||
actModeHicapModelId,
|
||||
actModeHicapModelInfo,
|
||||
actModeAihubmixModelId,
|
||||
actModeAihubmixModelInfo,
|
||||
actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo,
|
||||
geminiActModeThinkingLevel,
|
||||
|
||||
// Global state updates
|
||||
@@ -744,6 +757,7 @@ export class StateManager {
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
azureIdentity,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
@@ -1172,6 +1186,10 @@ export class StateManager {
|
||||
this.remoteConfigCache["azureApiVersion"] ||
|
||||
this.taskStateCache["azureApiVersion"] ||
|
||||
this.globalStateCache["azureApiVersion"],
|
||||
azureIdentity:
|
||||
this.remoteConfigCache["azureIdentity"] ||
|
||||
this.taskStateCache["azureIdentity"] ||
|
||||
this.globalStateCache["azureIdentity"],
|
||||
openRouterProviderSorting:
|
||||
this.taskStateCache["openRouterProviderSorting"] || this.globalStateCache["openRouterProviderSorting"],
|
||||
liteLlmBaseUrl:
|
||||
@@ -1265,6 +1283,7 @@ export class StateManager {
|
||||
this.globalStateCache["planModeHuaweiCloudMaasModelInfo"],
|
||||
planModeOcaModelId: this.globalStateCache["planModeOcaModelId"],
|
||||
planModeOcaModelInfo: this.globalStateCache["planModeOcaModelInfo"],
|
||||
planModeOcaReasoningEffort: this.globalStateCache["planModeOcaReasoningEffort"],
|
||||
planModeHicapModelId: this.taskStateCache["planModeHicapModelId"] || this.globalStateCache["planModeHicapModelId"],
|
||||
planModeHicapModelInfo:
|
||||
this.taskStateCache["planModeHicapModelInfo"] || this.globalStateCache["planModeHicapModelInfo"],
|
||||
@@ -1274,6 +1293,11 @@ export class StateManager {
|
||||
this.taskStateCache["planModeAihubmixModelInfo"] || this.globalStateCache["planModeAihubmixModelInfo"],
|
||||
planModeNousResearchModelId:
|
||||
this.taskStateCache["planModeNousResearchModelId"] || this.globalStateCache["planModeNousResearchModelId"],
|
||||
planModeVercelAiGatewayModelId:
|
||||
this.taskStateCache["planModeVercelAiGatewayModelId"] || this.globalStateCache["planModeVercelAiGatewayModelId"],
|
||||
planModeVercelAiGatewayModelInfo:
|
||||
this.taskStateCache["planModeVercelAiGatewayModelInfo"] ||
|
||||
this.globalStateCache["planModeVercelAiGatewayModelInfo"],
|
||||
geminiPlanModeThinkingLevel:
|
||||
this.taskStateCache["geminiPlanModeThinkingLevel"] || this.globalStateCache["geminiPlanModeThinkingLevel"],
|
||||
|
||||
@@ -1336,6 +1360,7 @@ export class StateManager {
|
||||
this.globalStateCache["actModeHuaweiCloudMaasModelInfo"],
|
||||
actModeOcaModelId: this.globalStateCache["actModeOcaModelId"],
|
||||
actModeOcaModelInfo: this.globalStateCache["actModeOcaModelInfo"],
|
||||
actModeOcaReasoningEffort: this.globalStateCache["actModeOcaReasoningEffort"],
|
||||
actModeHicapModelId: this.globalStateCache["actModeHicapModelId"],
|
||||
actModeHicapModelInfo: this.globalStateCache["actModeHicapModelInfo"],
|
||||
actModeAihubmixModelId:
|
||||
@@ -1344,6 +1369,11 @@ export class StateManager {
|
||||
this.taskStateCache["actModeAihubmixModelInfo"] || this.globalStateCache["actModeAihubmixModelInfo"],
|
||||
actModeNousResearchModelId:
|
||||
this.taskStateCache["actModeNousResearchModelId"] || this.globalStateCache["actModeNousResearchModelId"],
|
||||
actModeVercelAiGatewayModelId:
|
||||
this.taskStateCache["actModeVercelAiGatewayModelId"] || this.globalStateCache["actModeVercelAiGatewayModelId"],
|
||||
actModeVercelAiGatewayModelInfo:
|
||||
this.taskStateCache["actModeVercelAiGatewayModelInfo"] ||
|
||||
this.globalStateCache["actModeVercelAiGatewayModelInfo"],
|
||||
geminiActModeThinkingLevel:
|
||||
this.taskStateCache["geminiActModeThinkingLevel"] || this.globalStateCache["geminiActModeThinkingLevel"],
|
||||
nousResearchApiKey: this.secretsCache["nousResearchApiKey"],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getTelemetryService } from "@/services/telemetry"
|
||||
import { OpenTelemetryClientProvider } from "@/services/telemetry/providers/opentelemetry/OpenTelemetryClientProvider"
|
||||
import { OpenTelemetryTelemetryProvider } from "@/services/telemetry/providers/opentelemetry/OpenTelemetryTelemetryProvider"
|
||||
import { type TelemetryService } from "@/services/telemetry/TelemetryService"
|
||||
import { OpenTelemetryClientValidConfig, remoteConfigToOtelConfig } from "@/shared/services/config/otel-config"
|
||||
import { isOpenTelemetryConfigValid, remoteConfigToOtelConfig } from "@/shared/services/config/otel-config"
|
||||
import { StateManager } from "../StateManager"
|
||||
|
||||
/**
|
||||
@@ -106,6 +106,9 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
|
||||
if (openAiSettings.azureApiVersion !== undefined) {
|
||||
transformed.azureApiVersion = openAiSettings.azureApiVersion
|
||||
}
|
||||
if (openAiSettings.azureIdentity !== undefined) {
|
||||
transformed.azureIdentity = openAiSettings.azureIdentity
|
||||
}
|
||||
}
|
||||
|
||||
// Map AwsBedrock provider settings
|
||||
@@ -186,8 +189,8 @@ const REMOTE_CONFIG_OTEL_PROVIDER_ID = "OpenTelemetryRemoteConfiguredProvider"
|
||||
async function applyRemoteOTELConfig(transformed: Partial<RemoteConfigFields>, telemetryService: TelemetryService) {
|
||||
try {
|
||||
const otelConfig = remoteConfigToOtelConfig(transformed)
|
||||
if (otelConfig.enabled) {
|
||||
const client = new OpenTelemetryClientProvider(otelConfig as OpenTelemetryClientValidConfig)
|
||||
if (isOpenTelemetryConfigValid(otelConfig)) {
|
||||
const client = new OpenTelemetryClientProvider(otelConfig)
|
||||
|
||||
if (client.meterProvider || client.loggerProvider) {
|
||||
telemetryService.addProvider(
|
||||
|
||||
@@ -204,6 +204,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
const anthropicBaseUrl = context.globalState.get<GlobalStateAndSettings["anthropicBaseUrl"]>("anthropicBaseUrl")
|
||||
const geminiBaseUrl = context.globalState.get<GlobalStateAndSettings["geminiBaseUrl"]>("geminiBaseUrl")
|
||||
const azureApiVersion = context.globalState.get<GlobalStateAndSettings["azureApiVersion"]>("azureApiVersion")
|
||||
const azureIdentity = context.globalState.get<GlobalStateAndSettings["azureIdentity"]>("azureIdentity")
|
||||
const openRouterProviderSorting =
|
||||
context.globalState.get<GlobalStateAndSettings["openRouterProviderSorting"]>("openRouterProviderSorting")
|
||||
const lastShownAnnouncementId =
|
||||
@@ -391,6 +392,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
context.globalState.get<GlobalStateAndSettings["planModeBasetenModelInfo"]>("planModeBasetenModelInfo")
|
||||
const planModeOcaModelId = context.globalState.get("planModeOcaModelId") as string | undefined
|
||||
const planModeOcaModelInfo = context.globalState.get("planModeOcaModelInfo") as OcaModelInfo | undefined
|
||||
const planModeOcaReasoningEffort = context.globalState.get("planModeOcaReasoningEffort") as string | undefined
|
||||
const planModeHicapModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeHicapModelId"]>("planModeHicapModelId")
|
||||
const planModeHicapModelInfo =
|
||||
@@ -401,6 +403,11 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
context.globalState.get<GlobalStateAndSettings["planModeAihubmixModelInfo"]>("planModeAihubmixModelInfo")
|
||||
const planModeNousResearchModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeNousResearchModelId"]>("planModeNousResearchModelId")
|
||||
const planModeVercelAiGatewayModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeVercelAiGatewayModelId"]>("planModeVercelAiGatewayModelId")
|
||||
const planModeVercelAiGatewayModelInfo = context.globalState.get<
|
||||
GlobalStateAndSettings["planModeVercelAiGatewayModelInfo"]
|
||||
>("planModeVercelAiGatewayModelInfo")
|
||||
// Act mode configurations
|
||||
const actModeApiProvider = context.globalState.get<GlobalStateAndSettings["actModeApiProvider"]>("actModeApiProvider")
|
||||
const actModeApiModelId = context.globalState.get<GlobalStateAndSettings["actModeApiModelId"]>("actModeApiModelId")
|
||||
@@ -464,6 +471,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
context.globalState.get<GlobalStateAndSettings["actModeBasetenModelInfo"]>("actModeBasetenModelInfo")
|
||||
const actModeOcaModelId = context.globalState.get("actModeOcaModelId") as string | undefined
|
||||
const actModeOcaModelInfo = context.globalState.get("actModeOcaModelInfo") as OcaModelInfo | undefined
|
||||
const actModeOcaReasoningEffort = context.globalState.get("actModeOcaReasoningEffort") as string | undefined
|
||||
const actModeNousResearchModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeNousResearchModelId"]>("actModeNousResearchModelId")
|
||||
const sapAiCoreUseOrchestrationMode =
|
||||
@@ -475,6 +483,11 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelId"]>("actModeAihubmixModelId")
|
||||
const actModeAihubmixModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelInfo"]>("actModeAihubmixModelInfo")
|
||||
const actModeVercelAiGatewayModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeVercelAiGatewayModelId"]>("actModeVercelAiGatewayModelId")
|
||||
const actModeVercelAiGatewayModelInfo = context.globalState.get<
|
||||
GlobalStateAndSettings["actModeVercelAiGatewayModelInfo"]
|
||||
>("actModeVercelAiGatewayModelInfo")
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
if (planModeApiProvider) {
|
||||
@@ -551,6 +564,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
moonshotApiLine,
|
||||
zaiApiLine,
|
||||
azureApiVersion,
|
||||
azureIdentity,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
@@ -604,11 +618,14 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
planModeBasetenModelInfo,
|
||||
planModeOcaModelId,
|
||||
planModeOcaModelInfo,
|
||||
planModeOcaReasoningEffort,
|
||||
planModeHicapModelId,
|
||||
planModeHicapModelInfo,
|
||||
planModeAihubmixModelId,
|
||||
planModeAihubmixModelInfo,
|
||||
planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo,
|
||||
geminiPlanModeThinkingLevel,
|
||||
// Act mode configurations
|
||||
actModeApiProvider: actModeApiProvider || apiProvider,
|
||||
@@ -642,11 +659,14 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
actModeBasetenModelInfo,
|
||||
actModeOcaModelId,
|
||||
actModeOcaModelInfo,
|
||||
actModeOcaReasoningEffort,
|
||||
actModeHicapModelId,
|
||||
actModeHicapModelInfo,
|
||||
actModeAihubmixModelId,
|
||||
actModeAihubmixModelInfo,
|
||||
actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo,
|
||||
geminiActModeThinkingLevel,
|
||||
|
||||
// Other global fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiHandler } from "@core/api"
|
||||
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
|
||||
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import { CommandPermissionController } from "@core/permissions"
|
||||
import { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import { BrowserSession } from "@services/browser/BrowserSession"
|
||||
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
@@ -77,6 +78,7 @@ export class ToolExecutor {
|
||||
private mcpHub: McpHub,
|
||||
private fileContextTracker: FileContextTracker,
|
||||
private clineIgnoreController: ClineIgnoreController,
|
||||
private commandPermissionController: CommandPermissionController,
|
||||
private contextManager: ContextManager,
|
||||
private stateManager: StateManager,
|
||||
|
||||
@@ -163,6 +165,7 @@ export class ToolExecutor {
|
||||
diffViewProvider: this.diffViewProvider,
|
||||
fileContextTracker: this.fileContextTracker,
|
||||
clineIgnoreController: this.clineIgnoreController,
|
||||
commandPermissionController: this.commandPermissionController,
|
||||
contextManager: this.contextManager,
|
||||
stateManager: this.stateManager,
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialM
|
||||
import { executePreCompactHookWithCleanup, HookCancellationError, HookExecution } from "@core/hooks/precompact-executor"
|
||||
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import { parseMentions } from "@core/mentions"
|
||||
import { CommandPermissionController } from "@core/permissions"
|
||||
import { summarizeTask } from "@core/prompts/contextManagement"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { parseSlashCommands } from "@core/slash-commands"
|
||||
@@ -200,6 +201,7 @@ export class Task {
|
||||
public checkpointManager?: ICheckpointManager
|
||||
private initialCheckpointCommitPromise?: Promise<string | undefined>
|
||||
private clineIgnoreController: ClineIgnoreController
|
||||
private commandPermissionController: CommandPermissionController
|
||||
private toolExecutor: ToolExecutor
|
||||
/**
|
||||
* Whether the task is using native tool calls.
|
||||
@@ -275,6 +277,7 @@ export class Task {
|
||||
this.reinitExistingTaskFromId = reinitExistingTaskFromId
|
||||
this.cancelTask = cancelTask
|
||||
this.clineIgnoreController = new ClineIgnoreController(cwd)
|
||||
this.commandPermissionController = new CommandPermissionController()
|
||||
this.taskLockAcquired = taskLockAcquired
|
||||
// Determine terminal execution mode and create appropriate terminal manager
|
||||
this.terminalExecutionMode = vscodeTerminalExecutionMode || "vscodeTerminal"
|
||||
@@ -536,6 +539,7 @@ export class Task {
|
||||
this.mcpHub,
|
||||
this.fileContextTracker,
|
||||
this.clineIgnoreController,
|
||||
this.commandPermissionController,
|
||||
this.contextManager,
|
||||
this.stateManager,
|
||||
cwd,
|
||||
@@ -712,7 +716,7 @@ export class Task {
|
||||
partial?: boolean,
|
||||
): Promise<number | undefined> {
|
||||
// Allow hook messages even when aborted to enable proper cleanup
|
||||
if (this.taskState.abort && type !== "hook" && type !== "hook_output") {
|
||||
if (this.taskState.abort && type !== "hook_status" && type !== "hook_output_stream") {
|
||||
throw new Error("Cline instance aborted")
|
||||
}
|
||||
|
||||
@@ -1596,7 +1600,7 @@ export class Task {
|
||||
}
|
||||
|
||||
// Notify UI that hook was cancelled
|
||||
await this.say("hook_output", "\nHook execution cancelled by user")
|
||||
await this.say("hook_output_stream", "\nHook execution cancelled by user")
|
||||
|
||||
// Return success - let caller (abortTask) handle next steps
|
||||
// DON'T call abortTask() here to avoid infinite recursion
|
||||
|
||||
@@ -468,7 +468,7 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
changes[path] = {
|
||||
type: PatchActionType.UPDATE,
|
||||
oldContent: originalFiles[path],
|
||||
newContent: this.applyChunks(originalFiles[path]!, action.chunks, path),
|
||||
newContent: this.applyChunks(originalFiles[path]!, action.chunks, path).trimEnd(),
|
||||
movePath: action.movePath,
|
||||
}
|
||||
break
|
||||
|
||||
@@ -115,6 +115,15 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
// If no hint, use primary workspace (cwd)
|
||||
}
|
||||
|
||||
// Check command permission validation (CLINE_COMMAND_PERMISSIONS env var)
|
||||
const permissionResult = config.services.commandPermissionController.validateCommand(actualCommand)
|
||||
if (!permissionResult.allowed) {
|
||||
const matchedPattern = permissionResult.matchedPattern ? ` (matched pattern: ${permissionResult.matchedPattern})` : ""
|
||||
const errorMessage = `Command "${actualCommand}" was denied by CLINE_COMMAND_PERMISSIONS. Reason: ${permissionResult.reason}${matchedPattern}`
|
||||
await config.callbacks.say("command_permission_denied", errorMessage)
|
||||
return formatResponse.toolError(formatResponse.permissionDeniedError(errorMessage))
|
||||
}
|
||||
|
||||
// Check clineignore validation for command
|
||||
const ignoredFileAttemptedToAccess = config.services.clineIgnoreController.validateCommand(actualCommand)
|
||||
if (ignoredFileAttemptedToAccess) {
|
||||
|
||||
@@ -480,6 +480,8 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
return
|
||||
}
|
||||
|
||||
newContent = newContent.trimEnd() // remove any trailing newlines, since it's automatically inserted by the editor
|
||||
|
||||
return { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ApiHandler } from "@core/api"
|
||||
import type { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
|
||||
import type { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import type { CommandPermissionController } from "@core/permissions"
|
||||
import type { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import type { BrowserSession } from "@services/browser/BrowserSession"
|
||||
import type { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
@@ -74,6 +75,7 @@ export interface TaskServices {
|
||||
diffViewProvider: DiffViewProvider
|
||||
fileContextTracker: FileContextTracker
|
||||
clineIgnoreController: ClineIgnoreController
|
||||
commandPermissionController: CommandPermissionController
|
||||
contextManager: ContextManager
|
||||
stateManager: StateManager
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export const TASK_SERVICES_KEYS = [
|
||||
"diffViewProvider",
|
||||
"fileContextTracker",
|
||||
"clineIgnoreController",
|
||||
"commandPermissionController",
|
||||
"contextManager",
|
||||
"stateManager",
|
||||
] as const
|
||||
|
||||
+14
-9
@@ -27,11 +27,11 @@ import { fixWithCline } from "./core/controller/commands/fixWithCline"
|
||||
import { improveWithCline } from "./core/controller/commands/improveWithCline"
|
||||
import { clearOnboardingModelsCache } from "./core/controller/models/getClineOnboardingModels"
|
||||
import { sendAddToInputEvent } from "./core/controller/ui/subscribeToAddToInput"
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { sendShowWebviewEvent } from "./core/controller/ui/subscribeToShowWebview"
|
||||
import { HookDiscoveryCache } from "./core/hooks/HookDiscoveryCache"
|
||||
import { HookProcessRegistry } from "./core/hooks/HookProcessRegistry"
|
||||
import { workspaceResolver } from "./core/workspace"
|
||||
import { focusChatInput, getContextForCommand } from "./hosts/vscode/commandUtils"
|
||||
import { getContextForCommand, showWebview } from "./hosts/vscode/commandUtils"
|
||||
import { abortCommitGeneration, generateCommitMsg } from "./hosts/vscode/commit-message-generator"
|
||||
import {
|
||||
disposeVscodeCommentReviewController,
|
||||
@@ -207,8 +207,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// No terminal content was copied (either nothing selected or some error)
|
||||
return
|
||||
}
|
||||
// Ensure the sidebar view is visible
|
||||
await focusChatInput()
|
||||
// Ensure the sidebar view is visible but preserve editor focus
|
||||
await showWebview(true)
|
||||
|
||||
await sendAddToInputEvent(`Terminal output:\n\`\`\`\n${terminalContents}\n\`\`\``)
|
||||
|
||||
@@ -361,19 +361,24 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
// Register the focusChatInput command handler
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.FocusChatInput, async () => {
|
||||
vscode.commands.registerCommand(commands.FocusChatInput, async (preserveEditorFocus: boolean = false) => {
|
||||
const webview = WebviewProvider.getInstance() as VscodeWebviewProvider
|
||||
|
||||
// Show the webview
|
||||
const webviewView = webview.getWebview()
|
||||
if (webviewView) {
|
||||
webviewView.show()
|
||||
if (preserveEditorFocus) {
|
||||
// Only make webview visible without forcing focus
|
||||
webviewView.show(false)
|
||||
} else {
|
||||
// Show and force focus (default behavior for explicit focus actions)
|
||||
webviewView.show(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Send focus event
|
||||
sendFocusChatInputEvent()
|
||||
// Send show webview event with preserveEditorFocus flag
|
||||
sendShowWebviewEvent(preserveEditorFocus)
|
||||
telemetryService.captureButtonClick("command_focusChatInput", webview.controller?.task?.ulid)
|
||||
}),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user