import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; async function writeTempFile(content: string): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-fetch-")); const filePath = path.join(dir, "response.txt"); await fs.writeFile(filePath, content, "utf8"); return filePath; } export default function (pi: ExtensionAPI) { pi.registerTool({ name: "fetch_url", label: "Fetch URL", description: "Fetch a URL over HTTP(S) and return the response body as text (truncated to 50KB/2000 lines).", parameters: Type.Object({ url: Type.String({ description: "URL to fetch" }), timeoutMs: Type.Optional( Type.Number({ description: "Timeout in milliseconds (default: 10000)" }) ), headers: Type.Optional( Type.Record(Type.String(), Type.String(), { description: "Optional request headers", }) ), }), async execute(_toolCallId, params, signal) { const controller = new AbortController(); const timeout = setTimeout( () => controller.abort(), params.timeoutMs ?? 10000 ); const combinedSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal; try { const response = await fetch(params.url, { headers: params.headers, signal: combinedSignal, }); const body = await response.text(); const truncation = truncateHead(body, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES, }); let content = truncation.content; if (truncation.truncated) { const tempFile = await writeTempFile(body); content += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full output saved to: ${tempFile}]`; } return { content: [{ type: "text", text: content }], details: { status: response.status, statusText: response.statusText, contentType: response.headers.get("content-type"), url: response.url, }, }; } finally { clearTimeout(timeout); } }, }); }