Compare commits

...

1 Commits

Author SHA1 Message Date
0xtoshii 53fdd5c7de adding new tool handling and tool spec for webfetch 2025-11-16 13:22:28 -08:00
5 changed files with 87 additions and 87 deletions
+1
View File
@@ -24,6 +24,7 @@ export const toolParamNames = [
"url",
"coordinate",
"text",
"prompt",
"server_name",
"tool_name",
"arguments",
@@ -3,18 +3,20 @@ import { ClineDefaultTool } from "@/shared/tools"
import type { ClineToolSpec } from "../spec"
import { TASK_PROGRESS_PARAMETER } from "../types"
const nextGen: ClineToolSpec = {
variant: ModelFamily.NEXT_GEN,
const GENERIC: ClineToolSpec = {
variant: ModelFamily.GENERIC,
id: ClineDefaultTool.WEB_FETCH,
name: "web_fetch",
description: `Fetches content from a specified URL and processes into markdown
- Takes a URL as input
- Fetches the URL content, converts HTML to markdown
description: `Fetches content from a specified URL and analyzes it using your prompt
- Takes a URL and analysis prompt as input
- Fetches the URL content and processes based on your prompt
- Use this tool when you need to retrieve and analyze web content
- IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.
- The URL must be a fully-formed valid URL
- The prompt must be at least 2 characters
- HTTP URLs will be automatically upgraded to HTTPS
- This tool is read-only and does not modify any files`,
contextRequirements: (context) => context.providerInfo.providerId === "cline" && context.clineWebToolsEnabled === true,
parameters: [
{
name: "url",
@@ -22,6 +24,12 @@ const nextGen: ClineToolSpec = {
instruction: "The URL to fetch content from",
usage: "https://example.com/docs",
},
{
name: "prompt",
required: true,
instruction: "The prompt to use for analyzing the webpage content",
usage: "Summarize the main points and key takeaways",
},
TASK_PROGRESS_PARAMETER,
],
}
@@ -30,13 +38,19 @@ const NATIVE_NEXT_GEN: ClineToolSpec = {
variant: ModelFamily.NATIVE_NEXT_GEN,
id: ClineDefaultTool.WEB_FETCH,
name: "web_fetch",
description: "Fetches content from a specified URL. Only used for gathering needed information relevant to the task.",
description: "Fetches and analyzes content from a specified URL.",
contextRequirements: (context) => context.providerInfo.providerId === "cline" && context.clineWebToolsEnabled === true,
parameters: [
{
name: "url",
required: true,
instruction: "The URL to fetch content from",
},
{
name: "prompt",
required: true,
instruction: "Prompt for analyzing the webpage content",
},
TASK_PROGRESS_PARAMETER,
],
}
@@ -46,4 +60,4 @@ const NATIVE_GPT_5: ClineToolSpec = {
variant: ModelFamily.NATIVE_GPT_5,
}
export const web_fetch_variants = [nextGen, NATIVE_GPT_5, NATIVE_NEXT_GEN]
export const web_fetch_variants = [GENERIC, NATIVE_GPT_5, NATIVE_NEXT_GEN]
-3
View File
@@ -53,7 +53,6 @@ export class AutoApprove {
return [true, true]
case ClineDefaultTool.BROWSER:
case ClineDefaultTool.WEB_FETCH:
case ClineDefaultTool.MCP_ACCESS:
case ClineDefaultTool.MCP_USE:
return true
@@ -79,8 +78,6 @@ export class AutoApprove {
]
case ClineDefaultTool.BROWSER:
return autoApprovalSettings.actions.useBrowser
case ClineDefaultTool.WEB_FETCH:
return autoApprovalSettings.actions.useBrowser
case ClineDefaultTool.MCP_ACCESS:
case ClineDefaultTool.MCP_USE:
return autoApprovalSettings.actions.useMcp
@@ -1,15 +1,18 @@
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
import { ClineAsk, ClineSayTool } from "@shared/ExtensionMessage"
import { ClineDefaultTool } from "@shared/tools"
import axios from "axios"
import { ClineEnv } from "@/config"
import { AuthService } from "@/services/auth/AuthService"
import { buildClineExtraHeaders } from "@/services/EnvUtils"
import { telemetryService } from "@/services/telemetry"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
import { getAxiosSettings } from "@/shared/net"
import { ToolUse } from "../../../assistant-message"
import { formatResponse } from "../../../prompts/responses"
import { ToolResponse } from "../.."
import { showNotificationForApproval } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
export class WebFetchToolHandler implements IFullyManagedTool {
readonly name = ClineDefaultTool.WEB_FETCH
@@ -20,17 +23,17 @@ export class WebFetchToolHandler implements IFullyManagedTool {
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
const url = block.params.url || ""
const prompt = block.params.prompt || ""
const sharedMessageProps: ClineSayTool = {
tool: "webFetch",
path: uiHelpers.removeClosingTag(block, "url", url),
content: `Fetching URL: ${uiHelpers.removeClosingTag(block, "url", url)}`,
content: `Fetching URL: ${uiHelpers.removeClosingTag(block, "url", url)}\nPrompt: ${uiHelpers.removeClosingTag(block, "prompt", prompt)}`,
operationIsLocatedInWorkspace: false, // web_fetch is always external
} satisfies ClineSayTool
const partialMessage = JSON.stringify(sharedMessageProps)
// For partial blocks, we'll let the ToolExecutor handle auto-approval logic
// Just stream the UI update for now
await uiHelpers.removeLastPartialMessageIfExistsWithType("say", "tool")
await uiHelpers.ask("tool" as ClineAsk, partialMessage, block.partial).catch(() => {})
}
@@ -38,76 +41,54 @@ export class WebFetchToolHandler implements IFullyManagedTool {
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
try {
const url: string | undefined = block.params.url
const prompt: string | undefined = block.params.prompt
// Extract provider information for telemetry
// Extract provider information for validation and telemetry
const apiConfig = config.services.stateManager.getApiConfiguration()
const currentMode = config.services.stateManager.getGlobalSettingsKey("mode")
const provider = (currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider) as string
// Validate required parameter
// Ensure feature is enabled
const clineWebToolsEnabled = config.services.stateManager.getGlobalSettingsKey("clineWebToolsEnabled")
if (provider !== "cline" || !clineWebToolsEnabled) {
return formatResponse.toolError("Cline web tools are currently disabled.")
}
if (!url) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError(this.name, "url")
}
if (!prompt) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError(this.name, "prompt")
}
config.taskState.consecutiveMistakeCount = 0
// Create message for approval
const sharedMessageProps: ClineSayTool = {
tool: "webFetch",
path: url,
content: `Fetching URL: ${url}`,
content: `Fetching URL: ${url}\nPrompt: ${prompt}`,
operationIsLocatedInWorkspace: false,
}
const completeMessage = JSON.stringify(sharedMessageProps)
if (config.callbacks.shouldAutoApproveTool(this.name)) {
// Auto-approve flow
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
telemetryService.captureToolUsage(
config.ulid,
"web_fetch",
config.api.getModel().id,
provider,
true,
true,
undefined,
block.isNativeToolCall,
)
} else {
// Manual approval flow
showNotificationForApproval(
`Cline wants to fetch content from ${url}`,
config.autoApprovalSettings.enableNotifications,
)
await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "tool")
// Web tools are toggleable, so not checking approvals
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("tool", completeMessage, config)
if (!didApprove) {
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
false,
undefined,
block.isNativeToolCall,
)
return formatResponse.toolDenied()
} else {
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
true,
undefined,
block.isNativeToolCall,
)
}
}
telemetryService.captureToolUsage(
config.ulid,
"web_fetch",
config.api.getModel().id,
provider,
true, // autoApproved
true, // didUserApprove
undefined,
block.isNativeToolCall,
)
// Run PreToolUse hook after approval but before execution
try {
@@ -122,24 +103,35 @@ export class WebFetchToolHandler implements IFullyManagedTool {
}
// Execute the actual fetch
const urlContentFetcher = config.services?.urlContentFetcher as UrlContentFetcher
const baseUrl = ClineEnv.config().apiBaseUrl
const authToken = await AuthService.getInstance().getAuthToken()
await urlContentFetcher.launchBrowser()
try {
// Fetch Markdown content
const markdownContent = await urlContentFetcher.urlToMarkdown(url)
// TODO: Implement secondary AI call to process markdownContent with prompt
// For now, returning markdown directly.
// This will be a significant sub-task.
// Placeholder for processed summary:
const processedSummary = `Fetched Markdown for ${url}:\n\n${markdownContent}`
return formatResponse.toolResult(processedSummary)
} finally {
// Ensure browser is closed even on error
await urlContentFetcher.closeBrowser()
if (!authToken) {
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
}
const response = await axios.post(
`${baseUrl}/api/v1/search/webfetch`,
{
Url: url,
Prompt: prompt,
},
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
...(await buildClineExtraHeaders()),
},
timeout: 15000,
...getAxiosSettings(),
},
)
// Parse response
// Axios will throw on non-200 status, so no need to check fetchStatus
const result = response.data.data.result
return formatResponse.toolResult(result)
} catch (error) {
return `Error fetching web content: ${(error as Error).message}`
}
+1 -5
View File
@@ -830,11 +830,7 @@ export const ChatRowContent = memo(
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
{tool.operationIsLocatedInWorkspace === false &&
toolIcon("sign-out", "yellow", -90, "This URL is external")}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? "Cline wants to fetch content from this URL:"
: "Cline fetched content from this URL:"}
</span>
<span style={{ fontWeight: "bold" }}>Cline is fetching content from this URL:</span>
</div>
<div
onClick={() => {