Compare commits

...

8 Commits

Author SHA1 Message Date
celestial-vault a07554f191 rework the docker:shell script to reuse an existing container 2025-11-14 21:39:59 -08:00
celestial-vault 474c655240 update script documentation for next steps after docker build 2025-11-14 11:11:25 -08:00
celestial-vault b5157a2376 code comment 2025-11-14 11:02:53 -08:00
celestial-vault b14db72140 add docker setup for cli development 2025-11-13 21:31:54 -08:00
github-actions[bot] 0fb4a6c7e9 v3.37.1 Release Notes (#7451)
* changeset version bump

* Updating CHANGELOG.md format

* Update changelog for version 3.37.1

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-11-13 17:42:42 -08:00
Saoud Rizwan 855db7d8d8 feat(models): Add free minimax/mimax-m2 model to the model picker (#7453)
* feat(models): Add free minimax/mimax-m2 model to the model picker

* Add free minimax/mimax-m2 model to model picker
2025-11-13 17:41:16 -08:00
CandiedUniverse bb375b78ca fix(hooks): Prevent PreToolUse hook from running before attempt_completion tool (#7450) 2025-11-13 16:14:13 -08:00
CandiedUniverse 31af254f0a fix(hooks): Run PreToolUse only after approval (#7446)
* fix(hooks): Run PreToolUse only after approval; get it working for read_file first

* fix(hooks): Run PreToolUse only after approval; get it working for six more tools now

* fix(hooks): Run PreToolUse only after approval; get it working for the remaining six tools now

* fix(hooks): Implement HookExecution type to avoid using 'any'
2025-11-13 15:41:57 -08:00
30 changed files with 617 additions and 117 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add AGENTS.md support
+10 -1
View File
@@ -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)
+48
View File
@@ -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
+49
View File
@@ -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
View File
@@ -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",
+80
View File
@@ -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()
+66
View File
@@ -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()
+1
View File
@@ -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"]
+1 -1
View File
@@ -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"
}
}
+2 -6
View File
@@ -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
+11 -97
View File
@@ -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)
+6
View File
@@ -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
/**
+152
View File
@@ -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)
}
}
+14
View File
@@ -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",