import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; function textFromHtml(html: string): { title: string; content: string } { const title = html.match(/]*>([^<]+)<\/title>/i)?.[1]?.trim() || "No title"; const content = html .replace(/]*>[\s\S]*?<\/script>/gi, " ") .replace(/]*>[\s\S]*?<\/style>/gi, " ") .replace(/<[^>]+>/g, " ") .replace(/ /g, " ") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/\s+/g, " ") .trim(); return { title, content }; } async function fetchText(url: string, signal?: AbortSignal) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); const activeSignal = signal ?? controller.signal; try { const response = await fetch(url, { signal: activeSignal, headers: { "User-Agent": "Mozilla/5.0 Orquestra/1.0", }, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return textFromHtml(await response.text()); } finally { clearTimeout(timeout); } } export default function orquestraWebFetch(pi: ExtensionAPI) { pi.registerTool({ name: "orquestra_web_fetch", label: "Orquestra Web Fetch", description: "Fetch a web page and return extracted text using a simple non-browser fetch.", parameters: Type.Object({ url: Type.String({ description: "URL to fetch" }), limit: Type.Optional(Type.Integer({ minimum: 500, maximum: 20000, description: "Max characters to return" })), }), async execute(_toolCallId, params, signal) { const { title, content } = await fetchText(params.url, signal); const limit = params.limit ?? 8000; return { content: [{ type: "text", text: `Title: ${title}\n\nContent:\n${content.slice(0, limit)}` }], details: { title, url: params.url, characters: content.length }, }; }, }); }