diff options
| author | Allen Hutchison <[email protected]> | 2025-04-18 13:20:39 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-04-18 13:20:39 -0700 |
| commit | 3ed61f1ff25a50b94cb6a6235c67eb3c9d4737e7 (patch) | |
| tree | 63b768850ca73a60af62da1ec5b491e4ab0bfd71 /packages/cli/src | |
| parent | 56d4a35d05fbb581fd9efad8c8646e4b9bc42cd1 (diff) | |
Web fetch tool (#31)
* Adding a web fetch tool.
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/gemini.ts | 3 | ||||
| -rw-r--r-- | packages/cli/src/tools/web-fetch.tool.ts | 174 |
2 files changed, 177 insertions, 0 deletions
diff --git a/packages/cli/src/gemini.ts b/packages/cli/src/gemini.ts index 8762d39b..f6de65c3 100644 --- a/packages/cli/src/gemini.ts +++ b/packages/cli/src/gemini.ts @@ -9,6 +9,7 @@ import { GlobTool } from './tools/glob.tool.js'; import { EditTool } from './tools/edit.tool.js'; import { TerminalTool } from './tools/terminal.tool.js'; import { WriteFileTool } from './tools/write-file.tool.js'; +import { WebFetchTool } from './tools/web-fetch.tool.js'; import { globalConfig } from './config/config.js'; async function main() { @@ -77,6 +78,7 @@ function registerTools(targetDir: string) { const editTool = new EditTool(targetDir); const terminalTool = new TerminalTool(targetDir); const writeFileTool = new WriteFileTool(targetDir); + const webFetchTool = new WebFetchTool(); toolRegistry.registerTool(lsTool); toolRegistry.registerTool(readFileTool); @@ -85,4 +87,5 @@ function registerTools(targetDir: string) { toolRegistry.registerTool(editTool); toolRegistry.registerTool(terminalTool); toolRegistry.registerTool(writeFileTool); + toolRegistry.registerTool(webFetchTool); } diff --git a/packages/cli/src/tools/web-fetch.tool.ts b/packages/cli/src/tools/web-fetch.tool.ts new file mode 100644 index 00000000..74467605 --- /dev/null +++ b/packages/cli/src/tools/web-fetch.tool.ts @@ -0,0 +1,174 @@ +import { SchemaValidator } from '../utils/schemaValidator.js'; +import { BaseTool, ToolResult } from './tools.js'; +import { ToolCallConfirmationDetails } from '../ui/types.js'; // Added for shouldConfirmExecute + +/** + * Parameters for the WebFetch tool + */ +export interface WebFetchToolParams { + /** + * The URL to fetch content from. + */ + url: string; +} + +/** + * Standardized result from the WebFetch tool + */ +export interface WebFetchToolResult extends ToolResult {} + +/** + * Implementation of the WebFetch tool that reads content from a URL. + */ +export class WebFetchTool extends BaseTool< + WebFetchToolParams, + WebFetchToolResult +> { + static readonly Name: string = 'web_fetch'; + + /** + * Creates a new instance of the WebFetchTool + */ + constructor() { + super( + WebFetchTool.Name, + 'WebFetch', + 'Fetches text content from a given URL. Handles potential network errors and non-success HTTP status codes.', + { + properties: { + url: { + description: + "The URL to fetch. Must be an absolute URL (e.g., 'https://example.com/file.txt').", + type: 'string', + }, + }, + required: ['url'], + type: 'object', + }, + ); + // No rootDirectory needed for web fetching + } + + /** + * Validates the parameters for the WebFetch tool + * @param params Parameters to validate + * @returns An error message string if invalid, null otherwise + */ + invalidParams(params: WebFetchToolParams): string | null { + // 1. Validate against the basic schema first + if ( + this.schema.parameters && + !SchemaValidator.validate( + this.schema.parameters as Record<string, unknown>, + params, + ) + ) { + return 'Parameters failed schema validation.'; + } + + // 2. Validate the URL format and protocol + try { + const parsedUrl = new URL(params.url); + // Ensure it's an HTTP or HTTPS URL + if (!['http:', 'https:'].includes(parsedUrl.protocol)) { + return `Invalid URL protocol: "${parsedUrl.protocol}". Only 'http:' and 'https:' are supported.`; + } + } catch (error) { + // The URL constructor throws if the format is invalid + return `Invalid URL format: "${params.url}". Please provide a valid absolute URL (e.g., 'https://example.com').`; + } + + // If all checks pass, the parameters are valid + return null; + } + + /** + * Gets a description of the web fetch operation. + * @param params Parameters for the web fetch. + * @returns A string describing the operation. + */ + getDescription(params: WebFetchToolParams): string { + // Shorten long URLs for display + const displayUrl = + params.url.length > 80 + ? params.url.substring(0, 77) + '...' + : params.url; + return `Fetching content from ${displayUrl}`; + } + + /** + * Determines if the tool should prompt for confirmation before execution. + * Web fetches are generally safe, so default to false. + * @param params Parameters for the tool execution + * @returns Whether execute should be confirmed. + */ + async shouldConfirmExecute( + params: WebFetchToolParams, + ): Promise<ToolCallConfirmationDetails | false> { + // Could add logic here to confirm based on domain, etc. if needed + return Promise.resolve(false); + } + + /** + * Fetches content from the specified URL. + * @param params Parameters for the web fetch operation. + * @returns Result with the fetched content or an error message. + */ + async execute(params: WebFetchToolParams): Promise<WebFetchToolResult> { + const validationError = this.invalidParams(params); + if (validationError) { + return { + llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`, + returnDisplay: `**Error:** Invalid parameters. ${validationError}`, + }; + } + + const url = params.url; + + try { + const response = await fetch(url, { + headers: { + 'User-Agent': 'GeminiCode-CLI/1.0', + }, + signal: AbortSignal.timeout(15000) // 15 seconds timeout + }); + + if (!response.ok) { + // fetch doesn't throw on bad HTTP status codes (4xx, 5xx) + const errorText = `Failed to fetch data from ${url}. Status: ${response.status} ${response.statusText}`; + return { + llmContent: `Error: ${errorText}`, + returnDisplay: `**Error:** ${errorText}`, + }; + } + + // Assuming the response is text. Add checks for content-type if needed. + const data = await response.text(); + let llmContent = ''; + // Truncate very large responses for the LLM context + const MAX_LLM_CONTENT_LENGTH = 100000; + if (data) { + llmContent = `Fetched data from ${url}:\n\n${ + data.length > MAX_LLM_CONTENT_LENGTH + ? data.substring(0, MAX_LLM_CONTENT_LENGTH) + + '\n... [Content truncated]' + : data + }`; + } else { + llmContent = `No data fetched from ${url}. Status: ${response.status}`; + } + return { + llmContent, + returnDisplay: `Fetched content from ${url}`, // Simple display message + }; + } catch (error: any) { + // This catches network errors (DNS resolution, connection refused, etc.) + // and errors from the URL constructor if somehow bypassed validation (unlikely) + const errorMessage = `Failed to fetch data from ${url}. Error: ${error instanceof Error ? error.message : String(error)}`; + return { + llmContent: `Error: ${errorMessage}`, + returnDisplay: `**Error:** ${errorMessage}`, + }; + } + } +} |
