import fs from "node:fs/promises"; import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core"; import { detectMime } from "../../media/mime.js"; import { sanitizeToolResultImages } from "../tool-images.js"; // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance. export type AnyAgentTool = AgentTool; export type StringParamOptions = { required?: boolean; trim?: boolean; label?: string; allowEmpty?: boolean; }; export type ActionGate> = ( key: keyof T, defaultValue?: boolean, ) => boolean; export function createActionGate>( actions: T | undefined, ): ActionGate { return (key, defaultValue = true) => { const value = actions?.[key]; if (value === undefined) return defaultValue; return value !== false; }; } export function readStringParam( params: Record, key: string, options: StringParamOptions & { required: true }, ): string; export function readStringParam( params: Record, key: string, options?: StringParamOptions, ): string | undefined; export function readStringParam( params: Record, key: string, options: StringParamOptions = {}, ) { const { required = false, trim = true, label = key, allowEmpty = false } = options; const raw = params[key]; if (typeof raw !== "string") { if (required) throw new Error(`${label} required`); return undefined; } const value = trim ? raw.trim() : raw; if (!value && !allowEmpty) { if (required) throw new Error(`${label} required`); return undefined; } return value; } export function readStringOrNumberParam( params: Record, key: string, options: { required?: boolean; label?: string } = {}, ): string | undefined { const { required = false, label = key } = options; const raw = params[key]; if (typeof raw === "number" && Number.isFinite(raw)) { return String(raw); } if (typeof raw === "string") { const value = raw.trim(); if (value) return value; } if (required) throw new Error(`${label} required`); return undefined; } export function readNumberParam( params: Record, key: string, options: { required?: boolean; label?: string; integer?: boolean } = {}, ): number | undefined { const { required = false, label = key, integer = false } = options; const raw = params[key]; let value: number | undefined; if (typeof raw === "number" && Number.isFinite(raw)) { value = raw; } else if (typeof raw === "string") { const trimmed = raw.trim(); if (trimmed) { const parsed = Number.parseFloat(trimmed); if (Number.isFinite(parsed)) value = parsed; } } if (value === undefined) { if (required) throw new Error(`${label} required`); return undefined; } return integer ? Math.trunc(value) : value; } export function readStringArrayParam( params: Record, key: string, options: StringParamOptions & { required: true }, ): string[]; export function readStringArrayParam( params: Record, key: string, options?: StringParamOptions, ): string[] | undefined; export function readStringArrayParam( params: Record, key: string, options: StringParamOptions = {}, ) { const { required = false, label = key } = options; const raw = params[key]; if (Array.isArray(raw)) { const values = raw .filter((entry) => typeof entry === "string") .map((entry) => entry.trim()) .filter(Boolean); if (values.length === 0) { if (required) throw new Error(`${label} required`); return undefined; } return values; } if (typeof raw === "string") { const value = raw.trim(); if (!value) { if (required) throw new Error(`${label} required`); return undefined; } return [value]; } if (required) throw new Error(`${label} required`); return undefined; } export type ReactionParams = { emoji: string; remove: boolean; isEmpty: boolean; }; export function readReactionParams( params: Record, options: { emojiKey?: string; removeKey?: string; removeErrorMessage: string; }, ): ReactionParams { const emojiKey = options.emojiKey ?? "emoji"; const removeKey = options.removeKey ?? "remove"; const remove = typeof params[removeKey] === "boolean" ? params[removeKey] : false; const emoji = readStringParam(params, emojiKey, { required: true, allowEmpty: true, }); if (remove && !emoji) { throw new Error(options.removeErrorMessage); } return { emoji, remove, isEmpty: !emoji }; } export function jsonResult(payload: unknown): AgentToolResult { return { content: [ { type: "text", text: JSON.stringify(payload, null, 2), }, ], details: payload, }; } export async function imageResult(params: { label: string; path: string; base64: string; mimeType: string; extraText?: string; details?: Record; }): Promise> { const content: AgentToolResult["content"] = [ { type: "text", text: params.extraText ?? `MEDIA:${params.path}`, }, { type: "image", data: params.base64, mimeType: params.mimeType, }, ]; const result: AgentToolResult = { content, details: { path: params.path, ...params.details }, }; return await sanitizeToolResultImages(result, params.label); } export async function imageResultFromFile(params: { label: string; path: string; extraText?: string; details?: Record; }): Promise> { const buf = await fs.readFile(params.path); const mimeType = (await detectMime({ buffer: buf.slice(0, 256) })) ?? "image/png"; return await imageResult({ label: params.label, path: params.path, base64: buf.toString("base64"), mimeType, extraText: params.extraText, details: params.details, }); }