import { Type, Static } from "@earendil-works/pi-ai"; import { readConfig, resolveApiKey, getDefaultFetchFormat } from "../config.js"; import { fetchUrls } from "../api.js"; import { formatFetchResults, truncateOutput } from "../format.js"; import { renderCollapsibleMarkdown } from "../render.js"; // --------------------------------------------------------------------------- // Parameters schema // --------------------------------------------------------------------------- const FetchParams = Type.Object({ url: Type.Optional(Type.String({ description: "Single URL to fetch content from", })), urls: Type.Optional(Type.Array(Type.String(), { description: "Multiple URLs to fetch (max 10)", })), format: Type.Optional( Type.Union([ Type.Literal("markdown"), Type.Literal("html"), Type.Literal("json"), ], { description: "Output format", }), ), links: Type.Optional(Type.Boolean({ description: "Extract links from pages", })), imageLinks: Type.Optional(Type.Boolean({ description: "Extract image links from pages", })), maxBytes: Type.Optional(Type.Number({ description: "Maximum output size in bytes (default: 50KB, max: 200KB)", })), }); type FetchParamsType = Static; // --------------------------------------------------------------------------- // Tool definition // --------------------------------------------------------------------------- export const tinyfish_fetch = { name: "tinyfish_fetch", label: "TinyFish Fetch", description: "Fetch and extract clean content from one or more URLs. Renders JavaScript-heavy pages and returns text in markdown, HTML, or JSON format.", promptSnippet: "Extract readable content from known URLs (renders JS, handles SPA, returns clean text/markdown)", promptGuidelines: [ "Use tinyfish_fetch when you already have one or more specific URLs and need their content.", "Prefer tinyfish_fetch over tinyfish_agent_run when you only need to read page content without interaction.", "Use format='markdown' for LLM consumption (default), 'html' for raw markup, 'json' for structured data.", "Set links=true or imageLinks=true when the user needs to discover linked resources.", "Batch up to 10 URLs in a single call when fetching multiple pages.", ], parameters: FetchParams, async execute(_id: string, params: FetchParamsType) { const config = await readConfig(); const apiKey = resolveApiKey(config); if (!apiKey) { return { content: [ { type: "text" as const, text: "TinyFish API key not configured. Run /tinyfish-login to set it up." }, ], details: {}, }; } // Validate URLs const urls: string[] = []; if (params.url) urls.push(params.url); if (params.urls) urls.push(...params.urls); if (urls.length === 0) { return { content: [{ type: "text" as const, text: "Either 'url' or 'urls' parameter is required." }], details: { error: "missing_urls" }, }; } if (urls.length > 10) { return { content: [{ type: "text" as const, text: "Maximum 10 URLs per request." }], details: { error: "too_many_urls" }, }; } const format = params.format ?? getDefaultFetchFormat(config); const response = await fetchUrls(apiKey, { urls, format: format as "markdown" | "html" | "json", links: params.links, imageLinks: params.imageLinks, }); let output = formatFetchResults(response.results ?? []); if (params.maxBytes) { output = truncateOutput(output, params.maxBytes); } const resultCount = response.results?.length ?? 0; return { content: [{ type: "text" as const, text: output }], details: { urlCount: urls.length, resultCount }, }; }, renderResult(result, options, theme) { const count = (result.details?.resultCount as number | undefined) ?? 0; const summary = `📄 Fetched ${count} page${count === 1 ? "" : "s"}`; return renderCollapsibleMarkdown(result, options, theme, summary); }, };