fetch.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { z } from "zod";
  2. import { Tool } from "./tool";
  3. import { JSDOM } from "jsdom";
  4. import TurndownService from "turndown";
  5. const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
  6. const DEFAULT_TIMEOUT = 30 * 1000; // 30 seconds
  7. const MAX_TIMEOUT = 120 * 1000; // 2 minutes
  8. const DESCRIPTION = `Fetches content from a URL and returns it in the specified format.
  9. WHEN TO USE THIS TOOL:
  10. - Use when you need to download content from a URL
  11. - Helpful for retrieving documentation, API responses, or web content
  12. - Useful for getting external information to assist with tasks
  13. HOW TO USE:
  14. - Provide the URL to fetch content from
  15. - Specify the desired output format (text, markdown, or html)
  16. - Optionally set a timeout for the request
  17. FEATURES:
  18. - Supports three output formats: text, markdown, and html
  19. - Automatically handles HTTP redirects
  20. - Sets reasonable timeouts to prevent hanging
  21. - Validates input parameters before making requests
  22. LIMITATIONS:
  23. - Maximum response size is 5MB
  24. - Only supports HTTP and HTTPS protocols
  25. - Cannot handle authentication or cookies
  26. - Some websites may block automated requests
  27. TIPS:
  28. - Use text format for plain text content or simple API responses
  29. - Use markdown format for content that should be rendered with formatting
  30. - Use html format when you need the raw HTML structure
  31. - Set appropriate timeouts for potentially slow websites`;
  32. export const Fetch = Tool.define({
  33. name: "fetch",
  34. description: DESCRIPTION,
  35. parameters: z.object({
  36. url: z.string().describe("The URL to fetch content from"),
  37. format: z
  38. .enum(["text", "markdown", "html"])
  39. .describe(
  40. "The format to return the content in (text, markdown, or html)",
  41. ),
  42. timeout: z
  43. .number()
  44. .min(0)
  45. .max(MAX_TIMEOUT / 1000)
  46. .describe("Optional timeout in seconds (max 120)")
  47. .optional(),
  48. }),
  49. async execute(params, opts) {
  50. // Validate URL
  51. if (
  52. !params.url.startsWith("http://") &&
  53. !params.url.startsWith("https://")
  54. ) {
  55. throw new Error("URL must start with http:// or https://");
  56. }
  57. const timeout = Math.min(
  58. (params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
  59. MAX_TIMEOUT,
  60. );
  61. const controller = new AbortController();
  62. const timeoutId = setTimeout(() => controller.abort(), timeout);
  63. if (opts?.abortSignal) {
  64. opts.abortSignal.addEventListener("abort", () => controller.abort());
  65. }
  66. const response = await fetch(params.url, {
  67. signal: controller.signal,
  68. headers: {
  69. "User-Agent": "opencode/1.0",
  70. },
  71. });
  72. clearTimeout(timeoutId);
  73. if (!response.ok) {
  74. throw new Error(`Request failed with status code: ${response.status}`);
  75. }
  76. // Check content length
  77. const contentLength = response.headers.get("content-length");
  78. if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
  79. throw new Error("Response too large (exceeds 5MB limit)");
  80. }
  81. const arrayBuffer = await response.arrayBuffer();
  82. if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {
  83. throw new Error("Response too large (exceeds 5MB limit)");
  84. }
  85. const content = new TextDecoder().decode(arrayBuffer);
  86. const contentType = response.headers.get("content-type") || "";
  87. switch (params.format) {
  88. case "text":
  89. if (contentType.includes("text/html")) {
  90. const text = extractTextFromHTML(content);
  91. return { output: text };
  92. }
  93. return { output: content };
  94. case "markdown":
  95. if (contentType.includes("text/html")) {
  96. const markdown = convertHTMLToMarkdown(content);
  97. return { output: markdown };
  98. }
  99. return { output: "```\n" + content + "\n```" };
  100. case "html":
  101. return { output: content };
  102. default:
  103. return { output: content };
  104. }
  105. },
  106. });
  107. function extractTextFromHTML(html: string): string {
  108. const dom = new JSDOM(html);
  109. const text = dom.window.document.body?.textContent || "";
  110. return text.replace(/\s+/g, " ").trim();
  111. }
  112. function convertHTMLToMarkdown(html: string): string {
  113. const turndownService = new TurndownService();
  114. return turndownService.turndown(html);
  115. }