import type { FastifyInstance } from "fastify"; import Papa from "papaparse"; import { z } from "zod"; import { InputValidationError } from "../../modality/contract.js"; import { createToolRoute } from "../tool-factory.js"; const settingsSchema = z.object({}); export function registerMergeCsvs(app: FastifyInstance) { createToolRoute(app, { toolId: "merge-csvs", maxInputs: 20, minInputs: 2, settingsSchema, process: async () => { throw new Error("merge-csvs is v2-only"); }, processV2: async (ctx) => { if (ctx.inputs.length < 2) { throw new InputValidationError("Merging needs at least two CSV files"); } // Parse the first file to establish column order and delimiter const firstText = ctx.inputs[0].buffer.toString("utf8"); const firstResult = Papa.parse>(firstText, { header: true, skipEmptyLines: true, }); if (firstResult.errors.length > 0) { throw new InputValidationError(`CSV parse failed: ${firstResult.errors[0].message}`); } const fields = firstResult.meta.fields ?? []; const fieldSet = new Set(fields); const detectedDelimiter = firstResult.meta.delimiter || ","; const allRows: Record[] = [...firstResult.data]; // Parse remaining files and validate headers match for (let i = 1; i < ctx.inputs.length; i++) { const input = ctx.inputs[i]; const text = input.buffer.toString("utf8"); const result = Papa.parse>(text, { header: true, skipEmptyLines: true, }); if (result.errors.length > 0) { throw new InputValidationError( `CSV parse failed in ${input.filename}: ${result.errors[0].message}`, ); } const otherFields = new Set(result.meta.fields ?? []); if (otherFields.size !== fieldSet.size || ![...otherFields].every((f) => fieldSet.has(f))) { throw new InputValidationError(`${input.filename} has different columns`); } allRows.push(...result.data); } // Unparse preserving the first file's field order and delimiter const merged = Papa.unparse(allRows, { columns: fields, delimiter: detectedDelimiter, }); return { buffer: Buffer.from(merged, "utf8"), filename: "merged.csv", contentType: "text/csv", }; }, }); }