mirror of
https://github.com/cline/cline.git
synced 2026-09-02 07:42:19 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a07554f191 | |||
| 474c655240 | |||
| b5157a2376 | |||
| b14db72140 | |||
| 0fb4a6c7e9 | |||
| 855db7d8d8 | |||
| bb375b78ca | |||
| 31af254f0a |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add AGENTS.md support
|
||||
+10
-1
@@ -1,23 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
## 3.37.1
|
||||
|
||||
- cf8dd1c: Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
|
||||
- 02abbcf: Add AGENTS.md support
|
||||
- 855db7d: feat(models): Add free minimax/mimax-m2 model to the model picker
|
||||
|
||||
## [3.37.0]
|
||||
|
||||
## Added
|
||||
|
||||
- GPT-5.1 with model-specific prompting: tailored system prompts, tool usage, focus chain, and deep-planning optimizations
|
||||
- Nous Research provider with Hermes 4 model family and custom system prompts
|
||||
- Switched to Aqua Voice's Avalon model in speech to text transcription
|
||||
- Added Linux support for speech to text
|
||||
- Added Linux support for speech to text
|
||||
- Model-family breakouts for deep-planning prompting, laying groundwork for enhanced slash commands
|
||||
- Expanded HTTP proxy support throughout the codebase
|
||||
- Improved focus chain prompting for frontier models (Anthropic, OpenAI, Gemini, xAI)
|
||||
|
||||
## Fixed
|
||||
|
||||
- Duplicate tool results prevention through existence checking
|
||||
- XML entity escaping in model content processor
|
||||
- Commit message generation in command palette
|
||||
- OpenAI Compatible provider temperature parameter type conversion
|
||||
|
||||
## Documentation
|
||||
|
||||
- Added missing proto generation step in CONTRIBUTING.md
|
||||
- New `npm run dev` script for streamlined terminal workflow (fixes #7335)
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Node modules
|
||||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
# Build artifacts
|
||||
dist
|
||||
dist-standalone
|
||||
build
|
||||
*.log
|
||||
|
||||
# Generated code
|
||||
src/generated
|
||||
|
||||
# CLI build artifacts
|
||||
cli/bin
|
||||
cli/dist
|
||||
|
||||
# Webview build artifacts
|
||||
webview-ui/dist
|
||||
webview-ui/build
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Tests
|
||||
tests
|
||||
*.test.js
|
||||
*.spec.js
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
@@ -0,0 +1,49 @@
|
||||
FROM node:22-slim
|
||||
|
||||
# TARGETARCH enables multi-architecture support without emulation warnings:
|
||||
# - Docker automatically sets TARGETARCH to the build platform's architecture
|
||||
# - On arm64 machines (Apple Silicon): TARGETARCH=arm64, uses linux-arm64 binaries
|
||||
# - On amd64 machines (Intel/AMD): TARGETARCH=amd64, uses linux-x64 binaries
|
||||
# The corresponding platform-specific binaries and native modules (better-sqlite3)
|
||||
# are pre-built by scripts/package-standalone.mjs during the build process.
|
||||
ARG TARGETARCH
|
||||
|
||||
# Install only runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git curl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt/cline
|
||||
|
||||
# Copy the entire pre-built distribution
|
||||
COPY dist-standalone/ ./
|
||||
|
||||
# Create symlink for Linux native modules
|
||||
# Map Docker's TARGETARCH (arm64/amd64) to Node's platform naming (x64 for amd64)
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
ln -sf /opt/cline/binaries/linux-x64/node_modules/better-sqlite3 /opt/cline/node_modules/better-sqlite3; \
|
||||
else \
|
||||
ln -sf /opt/cline/binaries/linux-$TARGETARCH/node_modules/better-sqlite3 /opt/cline/node_modules/better-sqlite3; \
|
||||
fi
|
||||
|
||||
# Set up CLI binaries
|
||||
# The Linux binaries are already in /opt/cline/bin/ from dist-standalone
|
||||
# Just need to create symlinks to the platform-specific ones
|
||||
RUN cd /opt/cline/bin && \
|
||||
ln -sf cline-linux-$TARGETARCH cline && \
|
||||
ln -sf cline-host-linux-$TARGETARCH cline-host && \
|
||||
chmod +x cline-linux-$TARGETARCH cline-host-linux-$TARGETARCH cline cline-host
|
||||
|
||||
# Add binaries to PATH
|
||||
ENV PATH="/opt/cline/bin:${PATH}"
|
||||
ENV NODE_ENV=production
|
||||
ENV CLINE_HOME=/root/.cline
|
||||
|
||||
RUN mkdir -p $CLINE_HOME
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["/opt/cline/bin/cline"]
|
||||
CMD ["--help"]
|
||||
+3
-1
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.37.0",
|
||||
"version": "3.37.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -306,6 +306,8 @@
|
||||
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
|
||||
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
|
||||
"build:npm": "scripts/build-npm-package.sh",
|
||||
"build:docker:dev": "node scripts/build-docker-dev.mjs",
|
||||
"docker:shell": "node scripts/docker-shell.mjs",
|
||||
"test:install": "bash scripts/test-install.sh",
|
||||
"dev:cli:watch": "node scripts/dev-cli-watch.mjs",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from "child_process"
|
||||
|
||||
/**
|
||||
* Build Docker image for Cline CLI
|
||||
* This script builds a Docker image using pre-built binaries from dist-standalone/
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Run `npm run compile-standalone` first to build all platform binaries
|
||||
* - Run `npm run compile-cli` first to build CLI binaries
|
||||
*/
|
||||
|
||||
function runCommand(command, description) {
|
||||
console.log(`\n${description}...`)
|
||||
try {
|
||||
execSync(command, { stdio: "inherit" })
|
||||
console.log("✓ Success\n")
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed: ${error.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function getCommandOutput(command) {
|
||||
try {
|
||||
return execSync(command, { encoding: "utf-8" }).trim()
|
||||
} catch (error) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
function buildPrerequisites() {
|
||||
console.log("Building prerequisites...\n")
|
||||
|
||||
// Build standalone (includes cline-core and platform-specific native modules)
|
||||
runCommand("npm run compile-standalone", "Running npm run compile-standalone")
|
||||
|
||||
// Build CLI binaries for all platforms
|
||||
runCommand("npm run compile-cli-all-platforms", "Running npm run compile-cli-all-platforms")
|
||||
|
||||
console.log("✓ All prerequisites built successfully\n")
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log("🐳 Building Cline CLI Docker Image\n")
|
||||
|
||||
// Remove existing container to ensure clean state after rebuild
|
||||
const containerId = getCommandOutput(`docker ps -aq --filter "name=^cline-cli-dev$"`)
|
||||
if (containerId) {
|
||||
console.log("🗑️ Removing existing container to ensure fresh start...")
|
||||
try {
|
||||
execSync(`docker rm -f cline-cli-dev`, { stdio: "inherit" })
|
||||
console.log("✓ Container removed\n")
|
||||
} catch (error) {
|
||||
console.log("Note: Container cleanup failed, continuing anyway\n")
|
||||
}
|
||||
}
|
||||
|
||||
buildPrerequisites()
|
||||
|
||||
// Build Docker image for native platform
|
||||
// Docker will automatically use the correct architecture (arm64 on Apple Silicon, amd64 on Intel)
|
||||
runCommand("docker build -f docker/Dockerfile -t cline-cli:dev .", "Building Docker image")
|
||||
|
||||
console.log("✅ Docker image built successfully!")
|
||||
console.log("\n📋 Next steps:\n")
|
||||
console.log("Interactive shell:")
|
||||
console.log(" npm run docker:shell\n")
|
||||
console.log("This will:")
|
||||
console.log(" • Reuse existing 'cline-cli-dev' container if running")
|
||||
console.log(" • Start stopped container if it exists")
|
||||
console.log(" • Create new persistent container if none exists")
|
||||
console.log(" • Mount current directory at /workspace")
|
||||
console.log(" • Provide all CLI commands (cline auth, cline task, etc.)")
|
||||
console.log("\nContainer persists between sessions. To remove:")
|
||||
console.log(" docker rm -f cline-cli-dev\n")
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from "child_process"
|
||||
import { platform } from "os"
|
||||
|
||||
const CONTAINER_NAME = "cline-cli-dev"
|
||||
|
||||
function runCommand(command) {
|
||||
try {
|
||||
return execSync(command, { encoding: "utf-8" }).trim()
|
||||
} catch (error) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentDirectory() {
|
||||
// Get current working directory in a cross-platform way
|
||||
return process.cwd()
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log("🐳 Cline CLI Docker Shell\n")
|
||||
|
||||
// Check if container exists (running or stopped)
|
||||
const containerId = runCommand(`docker ps -a --filter "name=^${CONTAINER_NAME}$" --format "{{.ID}}"`)
|
||||
|
||||
if (containerId) {
|
||||
// Check if container is running
|
||||
const isRunning = runCommand(`docker ps --filter "id=${containerId}" --format "{{.ID}}"`)
|
||||
|
||||
if (isRunning) {
|
||||
console.log(`📦 Connecting to running container: ${CONTAINER_NAME}\n`)
|
||||
try {
|
||||
execSync(`docker exec -it ${containerId} /bin/bash`, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
// User exited shell normally
|
||||
}
|
||||
} else {
|
||||
console.log(`▶️ Starting stopped container: ${CONTAINER_NAME}\n`)
|
||||
try {
|
||||
execSync(`docker start ${containerId}`, { stdio: "inherit" })
|
||||
execSync(`docker exec -it ${containerId} /bin/bash`, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
// User exited shell normally
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`🚀 Creating new container: ${CONTAINER_NAME}\n`)
|
||||
const cwd = getCurrentDirectory()
|
||||
|
||||
try {
|
||||
// Use different volume mount syntax for Windows vs Unix
|
||||
const isWindows = platform() === "win32"
|
||||
const volumeMount = isWindows ? `${cwd.replace(/\\/g, "/")}:/workspace` : `${cwd}:/workspace`
|
||||
|
||||
execSync(
|
||||
`docker run -it --name ${CONTAINER_NAME} -v "${volumeMount}" -w /workspace --entrypoint /bin/bash cline-cli:dev`,
|
||||
{ stdio: "inherit" },
|
||||
)
|
||||
} catch (error) {
|
||||
// User exited shell normally
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -24,6 +24,7 @@ const TARGET_PLATFORMS = [
|
||||
{ platform: "darwin", arch: "x64", targetDir: "darwin-x64" },
|
||||
{ platform: "darwin", arch: "arm64", targetDir: "darwin-arm64" },
|
||||
{ platform: "linux", arch: "x64", targetDir: "linux-x64" },
|
||||
{ platform: "linux", arch: "arm64", targetDir: "linux-arm64" },
|
||||
]
|
||||
const SUPPORTED_BINARY_MODULES = ["better-sqlite3"]
|
||||
|
||||
|
||||
@@ -194,7 +194,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 (this.getModel().id === "x-ai/grok-code-fast-1") {
|
||||
if (this.getModel().id === "x-ai/grok-code-fast-1" || this.getModel().id === "minimax/minimax-m2") {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Error thrown when a PreToolUse hook requests cancellation.
|
||||
* This signals to the tool handler that execution should be aborted.
|
||||
*/
|
||||
export class PreToolUseHookCancellationError extends Error {
|
||||
constructor(message: string = "PreToolUse hook requested cancellation") {
|
||||
super(message)
|
||||
this.name = "PreToolUseHookCancellationError"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { AssistantMessageContent } from "@core/assistant-message"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import type { HookExecution } from "./types/HookExecution"
|
||||
|
||||
export class TaskState {
|
||||
// Streaming flags
|
||||
@@ -62,12 +63,7 @@ export class TaskState {
|
||||
abandoned = false
|
||||
|
||||
// Hook execution tracking for cancellation
|
||||
activeHookExecution?: {
|
||||
hookName: string
|
||||
toolName?: string
|
||||
messageTs: number
|
||||
abortController: AbortController
|
||||
}
|
||||
activeHookExecution?: HookExecution
|
||||
|
||||
// Auto-context summarization
|
||||
currentlySummarizing: boolean = false
|
||||
|
||||
@@ -174,6 +174,9 @@ export class ToolExecutor {
|
||||
shouldAutoApproveToolWithPath: this.shouldAutoApproveToolWithPath.bind(this),
|
||||
applyLatestBrowserSettings: this.applyLatestBrowserSettings.bind(this),
|
||||
switchToActMode: this.switchToActMode,
|
||||
setActiveHookExecution: this.setActiveHookExecution,
|
||||
clearActiveHookExecution: this.clearActiveHookExecution,
|
||||
getActiveHookExecution: this.getActiveHookExecution,
|
||||
},
|
||||
coordinator: this.coordinator,
|
||||
}
|
||||
@@ -528,14 +531,15 @@ export class ToolExecutor {
|
||||
* Handle complete block execution.
|
||||
*
|
||||
* This is the main execution flow for a tool:
|
||||
* 1. Run PreToolUse hooks (if enabled) - can block execution
|
||||
* 2. Execute the actual tool
|
||||
* 3. Run PostToolUse hooks (if enabled) - cannot block, only observe
|
||||
* 4. Add hook context modifications to the conversation
|
||||
* 5. Update focus chain tracking
|
||||
* 1. Execute the actual tool (tool handlers now run PreToolUse hooks post-approval)
|
||||
* 2. Run PostToolUse hooks (if enabled) - cannot block, only observe
|
||||
* 3. Add hook context modifications to the conversation
|
||||
* 4. Update focus chain tracking
|
||||
*
|
||||
* Note: PreToolUse hooks are now executed by individual tool handlers after approval
|
||||
* and before the actual tool operation. This provides better UX as approval dialogs
|
||||
* appear immediately without hook execution delay.
|
||||
*
|
||||
* Hooks are executed with streaming output to provide real-time feedback.
|
||||
* PreToolUse hooks can prevent tool execution by returning shouldContinue: false.
|
||||
* PostToolUse hooks are for observation/logging only and cannot block.
|
||||
*
|
||||
* @param block The complete tool use block with all parameters
|
||||
@@ -553,96 +557,6 @@ export class ToolExecutor {
|
||||
// Track if we need to cancel after hooks complete
|
||||
let shouldCancelAfterHook = false
|
||||
|
||||
// ============================================================
|
||||
// PHASE 1: Run PreToolUse hook (OUTSIDE try-catch-finally)
|
||||
// This allows early return on cancellation without triggering finally block
|
||||
// ============================================================
|
||||
if (hooksEnabled) {
|
||||
const { executeHook } = await import("../hooks/hook-executor")
|
||||
|
||||
// Build pending tool info for display
|
||||
const pendingToolInfo: any = {
|
||||
tool: block.name,
|
||||
}
|
||||
|
||||
// Add relevant parameters for display based on tool type
|
||||
if (block.params.path) {
|
||||
pendingToolInfo.path = block.params.path
|
||||
}
|
||||
if (block.params.command) {
|
||||
pendingToolInfo.command = block.params.command
|
||||
}
|
||||
if (block.params.content && typeof block.params.content === "string") {
|
||||
pendingToolInfo.content = block.params.content.slice(0, 200)
|
||||
}
|
||||
if (block.params.diff && typeof block.params.diff === "string") {
|
||||
pendingToolInfo.diff = block.params.diff.slice(0, 200)
|
||||
}
|
||||
if (block.params.regex) {
|
||||
pendingToolInfo.regex = block.params.regex
|
||||
}
|
||||
if (block.params.url) {
|
||||
pendingToolInfo.url = block.params.url
|
||||
}
|
||||
// For MCP operations, show tool/resource identifiers
|
||||
if (block.params.tool_name) {
|
||||
pendingToolInfo.mcpTool = block.params.tool_name
|
||||
}
|
||||
if (block.params.server_name) {
|
||||
pendingToolInfo.mcpServer = block.params.server_name
|
||||
}
|
||||
if (block.params.uri) {
|
||||
pendingToolInfo.resourceUri = block.params.uri
|
||||
}
|
||||
|
||||
const preToolResult = await executeHook({
|
||||
hookName: "PreToolUse",
|
||||
hookInput: {
|
||||
preToolUse: {
|
||||
toolName: block.name,
|
||||
parameters: block.params,
|
||||
},
|
||||
},
|
||||
isCancellable: true,
|
||||
say: this.say,
|
||||
setActiveHookExecution: this.setActiveHookExecution,
|
||||
clearActiveHookExecution: this.clearActiveHookExecution,
|
||||
messageStateHandler: this.messageStateHandler,
|
||||
taskId: this.taskId,
|
||||
hooksEnabled,
|
||||
toolName: block.name,
|
||||
pendingToolInfo,
|
||||
})
|
||||
|
||||
// Handle cancellation from hook
|
||||
if (preToolResult.cancel === true) {
|
||||
// Trigger task cancellation (same as clicking cancel button)
|
||||
await config.callbacks.cancelTask()
|
||||
// Early return - never enters try-catch-finally, so PostToolUse won't run
|
||||
return
|
||||
}
|
||||
|
||||
// If task was aborted (e.g., via cancel button during hook), stop execution
|
||||
if (this.taskState.abort) {
|
||||
shouldCancelAfterHook = true
|
||||
}
|
||||
|
||||
// Add context modification to the conversation if provided by the hook
|
||||
if (preToolResult.contextModification) {
|
||||
this.addHookContextToConversation(preToolResult.contextModification, "PreToolUse")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PHASE 2: Execute tool with PostToolUse hook in finally block
|
||||
// This only runs if PreToolUse didn't cancel above
|
||||
// ============================================================
|
||||
|
||||
// Check abort again before tool execution (could have been set by PreToolUse hook)
|
||||
if (this.taskState.abort) {
|
||||
return
|
||||
}
|
||||
|
||||
let executionSuccess = true
|
||||
let toolResult: any = null
|
||||
let toolWasExecuted = false
|
||||
|
||||
@@ -126,6 +126,18 @@ export class AccessMcpResourceHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
await config.callbacks.say("mcp_server_request_started")
|
||||
|
||||
// Execute the MCP resource access
|
||||
|
||||
@@ -269,6 +269,19 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
this.appliedCommit = commit
|
||||
this.config = config
|
||||
|
||||
// Run PreToolUse hook before applying changes
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
await provider.reset()
|
||||
return "The user denied this patch operation."
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Apply the commit
|
||||
const applyResults = await this.applyCommit(commit)
|
||||
|
||||
|
||||
@@ -52,6 +52,18 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
// Run PreToolUse hook before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Show notification if enabled
|
||||
if (config.autoApprovalSettings.enableNotifications) {
|
||||
showSystemNotification({
|
||||
@@ -81,7 +93,7 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
|
||||
}
|
||||
|
||||
// Remove any partial completion_result message that may exist
|
||||
// PreToolUse hook inserts messages after the partial, so we need to search backwards to find it
|
||||
// Search backwards since other messages may have been inserted after the partial
|
||||
const clineMessages = config.messageState.getClineMessages()
|
||||
const partialCompletionIndex = findLastIndex(
|
||||
clineMessages,
|
||||
|
||||
@@ -105,6 +105,18 @@ export class BrowserToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Start loading spinner
|
||||
await config.callbacks.say("browser_action_result", "")
|
||||
|
||||
|
||||
@@ -205,6 +205,18 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Setup timeout notification for long-running auto-approved commands
|
||||
let timeoutId: NodeJS.Timeout | undefined
|
||||
if (didAutoApprove && config.autoApprovalSettings.enableNotifications) {
|
||||
|
||||
@@ -134,6 +134,18 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,13 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
resolutionMethod: (typeof pathResult !== "string" ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
|
||||
}
|
||||
|
||||
// Check clineignore access
|
||||
const accessValidation = this.validator.checkClineIgnorePath(relDirPath!)
|
||||
if (!accessValidation.ok) {
|
||||
await config.callbacks.say("clineignore_error", relDirPath)
|
||||
return formatResponse.toolError(formatResponse.clineIgnoreError(relDirPath!))
|
||||
}
|
||||
|
||||
// Execute the actual list files operation
|
||||
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
|
||||
|
||||
@@ -151,6 +158,18 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,18 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Execute the actual file read operation
|
||||
const supportsImages = config.api.getModel().info.supportsImages ?? false
|
||||
const fileContent = await extractFileContent(absolutePath, supportsImages)
|
||||
|
||||
@@ -357,6 +357,18 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,18 @@ export class UseMcpToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Show MCP request started message
|
||||
await config.callbacks.say("mcp_server_request_started")
|
||||
|
||||
|
||||
@@ -109,6 +109,18 @@ export class WebFetchToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Execute the actual fetch
|
||||
const urlContentFetcher = config.services?.urlContentFetcher as UrlContentFetcher
|
||||
|
||||
|
||||
@@ -270,6 +270,20 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
try {
|
||||
const { ToolHookUtils } = await import("../utils/ToolHookUtils")
|
||||
await ToolHookUtils.runPreToolUseIfEnabled(config, block)
|
||||
} catch (error) {
|
||||
const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
|
||||
if (error instanceof PreToolUseHookCancellationError) {
|
||||
await config.services.diffViewProvider.revertChanges()
|
||||
await config.services.diffViewProvider.reset()
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Mark the file as edited by Cline
|
||||
config.services.fileContextTracker.markFileAsEditedByCline(relPath)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { StateManager } from "../../../storage/StateManager"
|
||||
import type { MessageStateHandler } from "../../message-state"
|
||||
import type { TaskState } from "../../TaskState"
|
||||
import type { AutoApprove } from "../../tools/autoApprove"
|
||||
import type { HookExecution } from "../../types/HookExecution"
|
||||
import type { ToolExecutorCoordinator } from "../ToolExecutorCoordinator"
|
||||
import { TASK_CALLBACKS_KEYS, TASK_CONFIG_KEYS, TASK_SERVICES_KEYS } from "../utils/ToolConstants"
|
||||
|
||||
@@ -116,6 +117,11 @@ export interface TaskCallbacks {
|
||||
applyLatestBrowserSettings: () => Promise<BrowserSession>
|
||||
|
||||
switchToActMode: () => Promise<boolean>
|
||||
|
||||
// Hook execution callbacks
|
||||
setActiveHookExecution: (hookExecution: HookExecution) => Promise<void>
|
||||
clearActiveHookExecution: () => Promise<void>
|
||||
getActiveHookExecution: () => Promise<HookExecution | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,6 +64,9 @@ export const TASK_CALLBACKS_KEYS = [
|
||||
"cancelTask",
|
||||
"updateTaskHistory",
|
||||
"switchToActMode",
|
||||
"setActiveHookExecution",
|
||||
"clearActiveHookExecution",
|
||||
"getActiveHookExecution",
|
||||
] as const
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { PreToolUseHookCancellationError } from "@core/hooks/PreToolUseHookCancellationError"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
|
||||
/**
|
||||
* Utility functions for tool hook execution.
|
||||
*/
|
||||
export class ToolHookUtils {
|
||||
/**
|
||||
* Runs the PreToolUse hook if enabled.
|
||||
*
|
||||
* This should be called by tool handlers after approval succeeds
|
||||
* but before the actual tool execution begins.
|
||||
*
|
||||
* @param config The task configuration
|
||||
* @param block The tool use block being executed
|
||||
* @returns Promise<boolean> - true if execution should continue, false if hook cancelled
|
||||
* @throws PreToolUseHookCancellationError if the hook requests cancellation
|
||||
*/
|
||||
static async runPreToolUseIfEnabled(config: TaskConfig, block: ToolUse): Promise<boolean> {
|
||||
// Check if hooks are enabled via user setting
|
||||
const hooksEnabled = config.services.stateManager.getGlobalSettingsKey("hooksEnabled")
|
||||
|
||||
if (!hooksEnabled) {
|
||||
return true // Hooks disabled, continue execution
|
||||
}
|
||||
|
||||
if (block.name == "attempt_completion") {
|
||||
return true // Skip this hook
|
||||
}
|
||||
|
||||
// Import the hook executor dynamically
|
||||
const { executeHook } = await import("@core/hooks/hook-executor")
|
||||
|
||||
// Build pending tool info for display
|
||||
const pendingToolInfo: any = {
|
||||
tool: block.name,
|
||||
}
|
||||
|
||||
// Add relevant parameters for display based on tool type
|
||||
if (block.params.path) {
|
||||
pendingToolInfo.path = block.params.path
|
||||
}
|
||||
if (block.params.command) {
|
||||
pendingToolInfo.command = block.params.command
|
||||
}
|
||||
if (block.params.content && typeof block.params.content === "string") {
|
||||
pendingToolInfo.content = block.params.content.slice(0, 200)
|
||||
}
|
||||
if (block.params.diff && typeof block.params.diff === "string") {
|
||||
pendingToolInfo.diff = block.params.diff.slice(0, 200)
|
||||
}
|
||||
if (block.params.regex) {
|
||||
pendingToolInfo.regex = block.params.regex
|
||||
}
|
||||
if (block.params.url) {
|
||||
pendingToolInfo.url = block.params.url
|
||||
}
|
||||
// For MCP operations, show tool/resource identifiers
|
||||
if (block.params.tool_name) {
|
||||
pendingToolInfo.mcpTool = block.params.tool_name
|
||||
}
|
||||
if (block.params.server_name) {
|
||||
pendingToolInfo.mcpServer = block.params.server_name
|
||||
}
|
||||
if (block.params.uri) {
|
||||
pendingToolInfo.resourceUri = block.params.uri
|
||||
}
|
||||
|
||||
// Execute the PreToolUse hook
|
||||
const preToolResult = await executeHook({
|
||||
hookName: "PreToolUse",
|
||||
hookInput: {
|
||||
preToolUse: {
|
||||
toolName: block.name,
|
||||
parameters: block.params,
|
||||
},
|
||||
},
|
||||
isCancellable: true,
|
||||
say: config.callbacks.say,
|
||||
setActiveHookExecution: config.callbacks.setActiveHookExecution,
|
||||
clearActiveHookExecution: config.callbacks.clearActiveHookExecution,
|
||||
messageStateHandler: config.messageState,
|
||||
taskId: config.taskId,
|
||||
hooksEnabled,
|
||||
toolName: block.name,
|
||||
pendingToolInfo,
|
||||
})
|
||||
|
||||
// Handle cancellation from hook
|
||||
if (preToolResult.cancel === true) {
|
||||
throw new PreToolUseHookCancellationError(preToolResult.errorMessage || "PreToolUse hook requested cancellation")
|
||||
}
|
||||
|
||||
// If task was aborted (e.g., via cancel button during hook), throw cancellation error
|
||||
if (config.taskState.abort) {
|
||||
throw new PreToolUseHookCancellationError("Task was aborted during PreToolUse hook execution")
|
||||
}
|
||||
|
||||
// Add context modification to the conversation if provided by the hook
|
||||
if (preToolResult.contextModification) {
|
||||
ToolHookUtils.addHookContextToConversation(config, preToolResult.contextModification, "PreToolUse")
|
||||
}
|
||||
|
||||
return true // Hook succeeded, continue execution
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds hook context modification to the conversation if provided.
|
||||
* Parses the context to extract type prefix and formats as XML.
|
||||
*
|
||||
* @param config The task configuration
|
||||
* @param contextModification The context string from the hook output
|
||||
* @param source The hook source name ("PreToolUse" or "PostToolUse")
|
||||
*/
|
||||
private static addHookContextToConversation(
|
||||
config: TaskConfig,
|
||||
contextModification: string | undefined,
|
||||
source: string,
|
||||
): void {
|
||||
if (!contextModification) {
|
||||
return
|
||||
}
|
||||
|
||||
const contextText = contextModification.trim()
|
||||
if (!contextText) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract context type from first line if specified (e.g., "WORKSPACE_RULES: ...")
|
||||
const lines = contextText.split("\n")
|
||||
const firstLine = lines[0]
|
||||
let contextType = "general"
|
||||
let content = contextText
|
||||
|
||||
// Check if first line specifies a type: "TYPE: content"
|
||||
const typeMatchRegex = /^([A-Z_]+):\s*(.*)/
|
||||
const typeMatch = typeMatchRegex.exec(firstLine)
|
||||
if (typeMatch) {
|
||||
contextType = typeMatch[1].toLowerCase()
|
||||
const remainingLines = lines.slice(1).filter((l: string) => l.trim())
|
||||
content = typeMatch[2] ? [typeMatch[2], ...remainingLines].join("\n") : remainingLines.join("\n")
|
||||
}
|
||||
|
||||
const hookContextBlock = {
|
||||
type: "text" as const,
|
||||
text: `<hook_context source="${source}" type="${contextType}">\n${content}\n</hook_context>`,
|
||||
}
|
||||
|
||||
config.taskState.userMessageContent.push(hookContextBlock)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Represents an active hook execution that can be cancelled.
|
||||
* This is tracked in TaskState to allow cancellation via UI or programmatic triggers.
|
||||
*/
|
||||
export interface HookExecution {
|
||||
/** The name of the hook being executed (e.g., "PreToolUse", "PostToolUse") */
|
||||
hookName: string
|
||||
/** The name of the tool that triggered this hook (for PreToolUse/PostToolUse hooks) */
|
||||
toolName?: string
|
||||
/** The timestamp of the message showing hook execution status */
|
||||
messageTs: number
|
||||
/** The abort controller used to cancel the hook execution */
|
||||
abortController: AbortController
|
||||
}
|
||||
@@ -60,6 +60,12 @@ const featuredModels = [
|
||||
description: "Fast open-source model with improved performance in Cline",
|
||||
label: "Trending",
|
||||
},
|
||||
{
|
||||
id: "minimax/minimax-m2",
|
||||
description: "Compact, high-efficiency model optimized for coding and agentic workflows",
|
||||
label: "Free",
|
||||
isFree: true,
|
||||
},
|
||||
{
|
||||
id: "x-ai/grok-code-fast-1",
|
||||
description: "Advanced model with 262K context for complex coding",
|
||||
|
||||
Reference in New Issue
Block a user