import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { ImageContent } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import fs from "node:fs/promises"; import path from "node:path"; const MIME_TYPES: Record = { "png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "gif": "image/gif", "webp": "image/webp", "svg": "image/svg+xml", "bmp": "image/bmp", "tif": "image/tiff", "tiff": "image/tiff", }; const SUPPORTED_EXTENSIONS = Object.keys(MIME_TYPES).sort().join(", "); function normalizeInputPath(filePath: string): string { // Match pi built-in path tools: models sometimes include a leading @ in paths. return filePath.startsWith("@") ? filePath.slice(1) : filePath; } /** * An extension that provides a tool to convert local images to base64 strings. * This allows image-capable models to "see" files from the filesystem. */ export default function (pi: ExtensionAPI) { pi.registerTool({ name: "image_to_base64", label: "Image to Base64", description: "Loads an image file from disk and converts it to a base64 data URL. Use this when you need to 'see' an image file provided by the user.", parameters: Type.Object({ filePath: Type.String({ description: "The absolute or relative path to the image file.", }), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { try { const inputPath = normalizeInputPath(params.filePath); const absolutePath = path.resolve(ctx.cwd, inputPath); // Verify file exists and is a file const stats = await fs.stat(absolutePath); if (!stats.isFile()) { return { content: [{ type: "text", text: `Error: '${params.filePath}' is not a file.` }], details: {}, }; } // Read file const buffer = await fs.readFile(absolutePath); // Determine MIME type based on extension. // Tool result image blocks must use pi's top-level { data, mimeType } // ImageContent shape. If mimeType is missing or non-image, provider // adapters serialize an invalid data URL (for example data:undefined). const ext = path.extname(absolutePath).toLowerCase().replace(".", ""); const mimeType = MIME_TYPES[ext]; if (!mimeType) { const extensionLabel = ext ? `.${ext}` : "(none)"; return { content: [{ type: "text", text: `Error: unsupported image extension '${extensionLabel}'. Supported formats: ${SUPPORTED_EXTENSIONS}.` }], details: {}, }; } const base64 = buffer.toString("base64"); const imageContent: ImageContent = { type: "image", data: base64, mimeType, }; return { content: [ { type: "text", text: `Successfully converted '${params.filePath}' to base64.` }, imageContent ], details: { mimeType, fileSize: stats.size, path: absolutePath }, }; } catch (error: any) { return { content: [{ type: "text", text: `Error loading image: ${error.message}` }], details: {}, }; } }, }); }