(args: z.infer<typeof CsvReadArgs>, _ctx: ToolContext)
| 129 | argsSchema = CsvReadArgs; |
| 130 | |
| 131 | async execute(args: z.infer<typeof CsvReadArgs>, _ctx: ToolContext): Promise<ToolResult> { |
| 132 | try { |
| 133 | const text = await fs.readFile(args.path, 'utf-8'); |
| 134 | // Sniff delimiter from first line if not given |
| 135 | const firstNL = text.search(/[\r\n]/); |
| 136 | const headerLine = firstNL === -1 ? text : text.slice(0, firstNL); |
| 137 | const delim = args.delimiter ?? detectDelimiter(headerLine); |
| 138 | |
| 139 | const allRows = parseCSV(text, delim); |
| 140 | if (allRows.length === 0) return { content: `[CSV_EMPTY] ${args.path} has no rows.` }; |
| 141 | |
| 142 | const hasHeader = args.has_header !== false; |
| 143 | const headers = hasHeader ? allRows[0]! : allRows[0]!.map((_, i) => `col${i}`); |
| 144 | const dataRows = hasHeader ? allRows.slice(1) : allRows; |
| 145 | |
| 146 | const startRow = args.start_row ?? 0; |
| 147 | const maxRows = args.max_rows ?? 100; |
| 148 | const slice = dataRows.slice(startRow, startRow + maxRows); |
| 149 | |
| 150 | // Column filter |
| 151 | let cols = headers; |
| 152 | let colIndices = headers.map((_, i) => i); |
| 153 | if (args.columns && args.columns.length > 0) { |
| 154 | const wanted = new Set(args.columns); |
| 155 | colIndices = headers.map((h, i) => wanted.has(h) ? i : -1).filter(i => i !== -1); |
| 156 | cols = colIndices.map(i => headers[i]!); |
| 157 | } |
| 158 | |
| 159 | // Build records |
| 160 | const records = slice.map(r => { |
| 161 | const obj: Record<string, string> = {}; |
| 162 | for (let j = 0; j < cols.length; j++) { |
| 163 | obj[cols[j]!] = r[colIndices[j]!] ?? ''; |
| 164 | } |
| 165 | return obj; |
| 166 | }); |
| 167 | |
| 168 | const lines: string[] = []; |
| 169 | lines.push(`CSV: ${args.path}`); |
| 170 | lines.push(`Delimiter: ${delim === '\t' ? '\\t (tab)' : delim}`); |
| 171 | lines.push(`Total rows: ${dataRows.length}${args.max_rows && dataRows.length > args.max_rows ? ' (showing first ' + maxRows + ')' : ''}`); |
| 172 | lines.push(`Columns (${headers.length}): ${headers.join(', ')}`); |
| 173 | if (args.columns) lines.push(`Filtered to: ${cols.join(', ')}`); |
| 174 | lines.push(''); |
| 175 | lines.push(`Rows ${startRow + 1}-${startRow + slice.length}:`); |
| 176 | lines.push(JSON.stringify(records, null, 2)); |
| 177 | return { |
| 178 | content: lines.join('\n'), |
| 179 | metadata: { totalRows: dataRows.length, returnedRows: slice.length, columns: headers, delimiter: delim }, |
| 180 | }; |
| 181 | } catch (e: any) { |
| 182 | return { content: `[CSV_ERROR] ${e?.message ?? String(e)}`, isError: true }; |
| 183 | } |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | const CsvWriteArgs = z.object({ |
nothing calls this directly
no test coverage detected