@xpr-agents/openclaw 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +31 -5
  2. package/openclaw.plugin.json +15 -1
  3. package/package.json +7 -4
  4. package/skills/code-sandbox/SKILL.md +30 -0
  5. package/skills/code-sandbox/skill.json +13 -0
  6. package/skills/code-sandbox/src/index.ts +212 -0
  7. package/skills/creative/SKILL.md +32 -0
  8. package/skills/creative/skill.json +13 -0
  9. package/skills/creative/src/index.ts +679 -0
  10. package/skills/defi/SKILL.md +123 -0
  11. package/skills/defi/dist/index.js +1 -0
  12. package/skills/defi/skill.json +44 -0
  13. package/skills/defi/src/index.ts +1788 -0
  14. package/skills/defi/test-read.mjs +281 -0
  15. package/skills/governance/SKILL.md +69 -0
  16. package/skills/governance/dist/index.js +632 -0
  17. package/skills/governance/skill.json +21 -0
  18. package/skills/governance/src/index.ts +656 -0
  19. package/skills/governance/test-read.mjs +176 -0
  20. package/skills/lending/SKILL.md +63 -0
  21. package/skills/lending/dist/index.js +1039 -0
  22. package/skills/lending/skill.json +29 -0
  23. package/skills/lending/src/index.ts +1105 -0
  24. package/skills/lending/test-read.mjs +156 -0
  25. package/skills/nft/SKILL.md +95 -0
  26. package/skills/nft/dist/index.js +4 -10
  27. package/skills/nft/skill.json +37 -0
  28. package/skills/nft/src/index.ts +1539 -0
  29. package/skills/shellbook/SKILL.md +59 -0
  30. package/skills/shellbook/skill.json +29 -0
  31. package/skills/shellbook/src/index.ts +391 -0
  32. package/skills/shellbook/tsconfig.json +14 -0
  33. package/skills/smart-contracts/SKILL.md +128 -0
  34. package/skills/smart-contracts/skill.json +25 -0
  35. package/skills/smart-contracts/src/index.ts +1327 -0
  36. package/skills/smart-contracts/tsconfig.json +14 -0
  37. package/skills/structured-data/SKILL.md +36 -0
  38. package/skills/structured-data/dist/index.js +501 -0
  39. package/skills/structured-data/skill.json +13 -0
  40. package/skills/structured-data/src/index.ts +597 -0
  41. package/skills/tax/SKILL.md +109 -0
  42. package/skills/tax/dist/index.js +216 -32
  43. package/skills/tax/skill.json +20 -0
  44. package/skills/tax/src/index.ts +1985 -0
  45. package/skills/web-scraping/SKILL.md +29 -0
  46. package/skills/web-scraping/dist/index.js +311 -0
  47. package/skills/web-scraping/skill.json +13 -0
  48. package/skills/web-scraping/src/index.ts +371 -0
  49. package/skills/xmd/SKILL.md +52 -0
  50. package/skills/xmd/dist/index.js +596 -0
  51. package/skills/xmd/skill.json +22 -0
  52. package/skills/xmd/src/index.ts +635 -0
  53. package/skills/xmd/test-read.mjs +178 -0
@@ -0,0 +1,597 @@
1
+ /**
2
+ * Structured Data Skill — CSV parsing, JSON-to-CSV conversion, and SVG chart generation
3
+ *
4
+ * Zero external dependencies — pure TypeScript implementations.
5
+ */
6
+
7
+ interface ToolDef {
8
+ name: string;
9
+ description: string;
10
+ parameters: { type: 'object'; required?: string[]; properties: Record<string, unknown> };
11
+ handler: (params: any) => Promise<unknown>;
12
+ }
13
+
14
+ interface SkillApi {
15
+ registerTool(tool: ToolDef): void;
16
+ getConfig(): Record<string, unknown>;
17
+ }
18
+
19
+ // ── CSV Parser ──────────────────────────────────
20
+
21
+ function detectDelimiter(text: string): string {
22
+ const firstLine = text.split('\n')[0] || '';
23
+ const counts: Record<string, number> = { ',': 0, '\t': 0, ';': 0, '|': 0 };
24
+ // Count delimiters outside quotes
25
+ let inQuote = false;
26
+ for (const ch of firstLine) {
27
+ if (ch === '"') { inQuote = !inQuote; continue; }
28
+ if (!inQuote && ch in counts) counts[ch]++;
29
+ }
30
+ // Pick the delimiter with the highest count
31
+ let best = ',';
32
+ let bestCount = 0;
33
+ for (const [delim, count] of Object.entries(counts)) {
34
+ if (count > bestCount) { best = delim; bestCount = count; }
35
+ }
36
+ return best;
37
+ }
38
+
39
+ function parseCSVLine(line: string, delimiter: string): string[] {
40
+ const fields: string[] = [];
41
+ let current = '';
42
+ let inQuote = false;
43
+ let i = 0;
44
+
45
+ while (i < line.length) {
46
+ const ch = line[i];
47
+
48
+ if (inQuote) {
49
+ if (ch === '"') {
50
+ // Check for escaped quote ("")
51
+ if (i + 1 < line.length && line[i + 1] === '"') {
52
+ current += '"';
53
+ i += 2;
54
+ } else {
55
+ inQuote = false;
56
+ i++;
57
+ }
58
+ } else {
59
+ current += ch;
60
+ i++;
61
+ }
62
+ } else {
63
+ if (ch === '"') {
64
+ inQuote = true;
65
+ i++;
66
+ } else if (ch === delimiter) {
67
+ fields.push(current.trim());
68
+ current = '';
69
+ i++;
70
+ } else {
71
+ current += ch;
72
+ i++;
73
+ }
74
+ }
75
+ }
76
+ fields.push(current.trim());
77
+ return fields;
78
+ }
79
+
80
+ function parseCSVText(
81
+ csv: string,
82
+ options: { delimiter?: string; headers?: boolean; limit?: number },
83
+ ): { data: Record<string, string>[]; columns: string[] } {
84
+ const delimiter = options.delimiter || detectDelimiter(csv);
85
+ const useHeaders = options.headers !== false;
86
+
87
+ // Split into lines, handling quoted newlines
88
+ const lines: string[] = [];
89
+ let current = '';
90
+ let inQuote = false;
91
+ for (const ch of csv) {
92
+ if (ch === '"') inQuote = !inQuote;
93
+ if (ch === '\n' && !inQuote) {
94
+ if (current.trim()) lines.push(current);
95
+ current = '';
96
+ } else if (ch === '\r') {
97
+ // Skip carriage returns
98
+ } else {
99
+ current += ch;
100
+ }
101
+ }
102
+ if (current.trim()) lines.push(current);
103
+
104
+ if (lines.length === 0) return { data: [], columns: [] };
105
+
106
+ let columns: string[];
107
+ let startRow: number;
108
+
109
+ if (useHeaders) {
110
+ columns = parseCSVLine(lines[0], delimiter);
111
+ startRow = 1;
112
+ } else {
113
+ const firstRow = parseCSVLine(lines[0], delimiter);
114
+ columns = firstRow.map((_, i) => `col_${i}`);
115
+ startRow = 0;
116
+ }
117
+
118
+ const maxRows = options.limit || Infinity;
119
+ const data: Record<string, string>[] = [];
120
+
121
+ for (let i = startRow; i < lines.length && data.length < maxRows; i++) {
122
+ const fields = parseCSVLine(lines[i], delimiter);
123
+ const row: Record<string, string> = {};
124
+ for (let j = 0; j < columns.length; j++) {
125
+ row[columns[j]] = fields[j] || '';
126
+ }
127
+ data.push(row);
128
+ }
129
+
130
+ return { data, columns };
131
+ }
132
+
133
+ // ── JSON to CSV ─────────────────────────────────
134
+
135
+ function escapeCSVField(value: string, delimiter: string): string {
136
+ if (
137
+ value.includes(delimiter) ||
138
+ value.includes('"') ||
139
+ value.includes('\n') ||
140
+ value.includes('\r')
141
+ ) {
142
+ return '"' + value.replace(/"/g, '""') + '"';
143
+ }
144
+ return value;
145
+ }
146
+
147
+ function jsonToCSVText(
148
+ data: Record<string, unknown>[],
149
+ options: { columns?: string[]; delimiter?: string },
150
+ ): string {
151
+ if (data.length === 0) return '';
152
+
153
+ const delimiter = options.delimiter || ',';
154
+
155
+ // Determine columns
156
+ let columns: string[];
157
+ if (options.columns && options.columns.length > 0) {
158
+ columns = options.columns;
159
+ } else {
160
+ // Collect all keys from all rows
161
+ const keySet = new Set<string>();
162
+ for (const row of data) {
163
+ for (const key of Object.keys(row)) keySet.add(key);
164
+ }
165
+ columns = [...keySet];
166
+ }
167
+
168
+ const lines: string[] = [];
169
+
170
+ // Header row
171
+ lines.push(columns.map(c => escapeCSVField(c, delimiter)).join(delimiter));
172
+
173
+ // Data rows
174
+ for (const row of data) {
175
+ const fields = columns.map(col => {
176
+ const val = row[col];
177
+ if (val === null || val === undefined) return '';
178
+ if (typeof val === 'object') return escapeCSVField(JSON.stringify(val), delimiter);
179
+ return escapeCSVField(String(val), delimiter);
180
+ });
181
+ lines.push(fields.join(delimiter));
182
+ }
183
+
184
+ return lines.join('\n');
185
+ }
186
+
187
+ // ── SVG Chart Generator ─────────────────────────
188
+
189
+ const DEFAULT_COLORS = ['#4285F4', '#EA4335', '#FBBC04', '#34A853', '#FF6D01', '#46BDC6', '#7B1FA2', '#C2185B'];
190
+
191
+ function escapeXml(text: string): string {
192
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
193
+ }
194
+
195
+ interface ChartData {
196
+ labels: string[];
197
+ values?: number[];
198
+ series?: Array<{ name: string; values: number[] }>;
199
+ }
200
+
201
+ function generateBarChart(
202
+ data: ChartData,
203
+ title: string,
204
+ width: number,
205
+ height: number,
206
+ colors: string[],
207
+ ): string {
208
+ const margin = { top: title ? 50 : 30, right: 20, bottom: 60, left: 60 };
209
+ const chartW = width - margin.left - margin.right;
210
+ const chartH = height - margin.top - margin.bottom;
211
+
212
+ const series = data.series || [{ name: 'Value', values: data.values || [] }];
213
+ const allValues = series.flatMap(s => s.values);
214
+ const maxVal = Math.max(...allValues, 0) || 1;
215
+ const minVal = Math.min(0, ...allValues);
216
+ const range = maxVal - minVal || 1;
217
+
218
+ const groupCount = data.labels.length;
219
+ const seriesCount = series.length;
220
+ const groupWidth = chartW / groupCount;
221
+ const barWidth = Math.min((groupWidth * 0.8) / seriesCount, 50);
222
+ const groupPad = (groupWidth - barWidth * seriesCount) / 2;
223
+
224
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" style="font-family:system-ui,sans-serif">`;
225
+
226
+ // Background
227
+ svg += `<rect width="${width}" height="${height}" fill="white"/>`;
228
+
229
+ // Title
230
+ if (title) {
231
+ svg += `<text x="${width / 2}" y="25" text-anchor="middle" font-size="16" font-weight="bold" fill="#333">${escapeXml(title)}</text>`;
232
+ }
233
+
234
+ // Y-axis gridlines and labels
235
+ const ySteps = 5;
236
+ for (let i = 0; i <= ySteps; i++) {
237
+ const val = minVal + (range * i) / ySteps;
238
+ const y = margin.top + chartH - (chartH * (val - minVal)) / range;
239
+ svg += `<line x1="${margin.left}" y1="${y}" x2="${margin.left + chartW}" y2="${y}" stroke="#eee" stroke-width="1"/>`;
240
+ const label = Math.abs(val) >= 1000 ? `${(val / 1000).toFixed(1)}k` : val % 1 === 0 ? String(val) : val.toFixed(1);
241
+ svg += `<text x="${margin.left - 8}" y="${y + 4}" text-anchor="end" font-size="11" fill="#666">${label}</text>`;
242
+ }
243
+
244
+ // Bars
245
+ const zeroY = margin.top + chartH - (chartH * (0 - minVal)) / range;
246
+ for (let g = 0; g < groupCount; g++) {
247
+ for (let s = 0; s < seriesCount; s++) {
248
+ const val = series[s].values[g] || 0;
249
+ const barH = (chartH * Math.abs(val)) / range;
250
+ const x = margin.left + g * groupWidth + groupPad + s * barWidth;
251
+ const y = val >= 0 ? zeroY - barH : zeroY;
252
+ const color = colors[s % colors.length];
253
+ svg += `<rect x="${x}" y="${y}" width="${barWidth}" height="${barH}" fill="${color}" rx="2"/>`;
254
+ }
255
+ // X-axis label
256
+ const labelX = margin.left + g * groupWidth + groupWidth / 2;
257
+ const label = data.labels[g].length > 12 ? data.labels[g].slice(0, 11) + '\u2026' : data.labels[g];
258
+ svg += `<text x="${labelX}" y="${margin.top + chartH + 18}" text-anchor="middle" font-size="11" fill="#666">${escapeXml(label)}</text>`;
259
+ }
260
+
261
+ // Axes
262
+ svg += `<line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + chartH}" stroke="#ccc" stroke-width="1"/>`;
263
+ svg += `<line x1="${margin.left}" y1="${zeroY}" x2="${margin.left + chartW}" y2="${zeroY}" stroke="#ccc" stroke-width="1"/>`;
264
+
265
+ // Legend for multi-series
266
+ if (seriesCount > 1) {
267
+ const legendY = height - 15;
268
+ let legendX = margin.left;
269
+ for (let s = 0; s < seriesCount; s++) {
270
+ const color = colors[s % colors.length];
271
+ svg += `<rect x="${legendX}" y="${legendY - 8}" width="12" height="12" fill="${color}" rx="2"/>`;
272
+ svg += `<text x="${legendX + 16}" y="${legendY + 2}" font-size="11" fill="#666">${escapeXml(series[s].name)}</text>`;
273
+ legendX += 16 + series[s].name.length * 7 + 16;
274
+ }
275
+ }
276
+
277
+ svg += '</svg>';
278
+ return svg;
279
+ }
280
+
281
+ function generateLineChart(
282
+ data: ChartData,
283
+ title: string,
284
+ width: number,
285
+ height: number,
286
+ colors: string[],
287
+ ): string {
288
+ const margin = { top: title ? 50 : 30, right: 20, bottom: 60, left: 60 };
289
+ const chartW = width - margin.left - margin.right;
290
+ const chartH = height - margin.top - margin.bottom;
291
+
292
+ const series = data.series || [{ name: 'Value', values: data.values || [] }];
293
+ const allValues = series.flatMap(s => s.values);
294
+ const maxVal = Math.max(...allValues, 0) || 1;
295
+ const minVal = Math.min(...allValues, 0);
296
+ const range = maxVal - minVal || 1;
297
+ // Add 10% padding
298
+ const paddedMin = minVal - range * 0.05;
299
+ const paddedRange = range * 1.1;
300
+
301
+ const pointCount = data.labels.length;
302
+
303
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" style="font-family:system-ui,sans-serif">`;
304
+ svg += `<rect width="${width}" height="${height}" fill="white"/>`;
305
+
306
+ if (title) {
307
+ svg += `<text x="${width / 2}" y="25" text-anchor="middle" font-size="16" font-weight="bold" fill="#333">${escapeXml(title)}</text>`;
308
+ }
309
+
310
+ // Y-axis gridlines
311
+ const ySteps = 5;
312
+ for (let i = 0; i <= ySteps; i++) {
313
+ const val = paddedMin + (paddedRange * i) / ySteps;
314
+ const y = margin.top + chartH - (chartH * i) / ySteps;
315
+ svg += `<line x1="${margin.left}" y1="${y}" x2="${margin.left + chartW}" y2="${y}" stroke="#eee" stroke-width="1"/>`;
316
+ const label = Math.abs(val) >= 1000 ? `${(val / 1000).toFixed(1)}k` : val % 1 === 0 ? String(Math.round(val)) : val.toFixed(1);
317
+ svg += `<text x="${margin.left - 8}" y="${y + 4}" text-anchor="end" font-size="11" fill="#666">${label}</text>`;
318
+ }
319
+
320
+ // Lines and points
321
+ for (let s = 0; s < series.length; s++) {
322
+ const color = colors[s % colors.length];
323
+ const points: Array<{ x: number; y: number }> = [];
324
+
325
+ for (let i = 0; i < pointCount; i++) {
326
+ const val = series[s].values[i] || 0;
327
+ const x = margin.left + (i / Math.max(pointCount - 1, 1)) * chartW;
328
+ const y = margin.top + chartH - (chartH * (val - paddedMin)) / paddedRange;
329
+ points.push({ x, y });
330
+ }
331
+
332
+ // Line path
333
+ if (points.length > 0) {
334
+ const pathD = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
335
+ svg += `<path d="${pathD}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linejoin="round"/>`;
336
+ }
337
+
338
+ // Points
339
+ for (const p of points) {
340
+ svg += `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3.5" fill="${color}" stroke="white" stroke-width="1.5"/>`;
341
+ }
342
+ }
343
+
344
+ // X-axis labels
345
+ for (let i = 0; i < pointCount; i++) {
346
+ const x = margin.left + (i / Math.max(pointCount - 1, 1)) * chartW;
347
+ const label = data.labels[i].length > 12 ? data.labels[i].slice(0, 11) + '\u2026' : data.labels[i];
348
+ svg += `<text x="${x}" y="${margin.top + chartH + 18}" text-anchor="middle" font-size="11" fill="#666">${escapeXml(label)}</text>`;
349
+ }
350
+
351
+ // Axes
352
+ svg += `<line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + chartH}" stroke="#ccc" stroke-width="1"/>`;
353
+ svg += `<line x1="${margin.left}" y1="${margin.top + chartH}" x2="${margin.left + chartW}" y2="${margin.top + chartH}" stroke="#ccc" stroke-width="1"/>`;
354
+
355
+ // Legend
356
+ if (series.length > 1) {
357
+ const legendY = height - 15;
358
+ let legendX = margin.left;
359
+ for (let s = 0; s < series.length; s++) {
360
+ const color = colors[s % colors.length];
361
+ svg += `<rect x="${legendX}" y="${legendY - 8}" width="12" height="12" fill="${color}" rx="2"/>`;
362
+ svg += `<text x="${legendX + 16}" y="${legendY + 2}" font-size="11" fill="#666">${escapeXml(series[s].name)}</text>`;
363
+ legendX += 16 + series[s].name.length * 7 + 16;
364
+ }
365
+ }
366
+
367
+ svg += '</svg>';
368
+ return svg;
369
+ }
370
+
371
+ function generatePieChart(
372
+ data: ChartData,
373
+ title: string,
374
+ width: number,
375
+ height: number,
376
+ colors: string[],
377
+ ): string {
378
+ const values = data.values || [];
379
+ const total = values.reduce((sum, v) => sum + Math.abs(v), 0) || 1;
380
+
381
+ const cx = width / 2;
382
+ const cy = (height - (title ? 30 : 0)) / 2 + (title ? 40 : 0);
383
+ const radius = Math.min(width, height - (title ? 60 : 20)) / 2 - 40;
384
+
385
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" style="font-family:system-ui,sans-serif">`;
386
+ svg += `<rect width="${width}" height="${height}" fill="white"/>`;
387
+
388
+ if (title) {
389
+ svg += `<text x="${width / 2}" y="25" text-anchor="middle" font-size="16" font-weight="bold" fill="#333">${escapeXml(title)}</text>`;
390
+ }
391
+
392
+ let startAngle = -Math.PI / 2;
393
+ for (let i = 0; i < values.length; i++) {
394
+ const sliceAngle = (Math.abs(values[i]) / total) * 2 * Math.PI;
395
+ const endAngle = startAngle + sliceAngle;
396
+ const color = colors[i % colors.length];
397
+
398
+ const x1 = cx + radius * Math.cos(startAngle);
399
+ const y1 = cy + radius * Math.sin(startAngle);
400
+ const x2 = cx + radius * Math.cos(endAngle);
401
+ const y2 = cy + radius * Math.sin(endAngle);
402
+ const largeArc = sliceAngle > Math.PI ? 1 : 0;
403
+
404
+ if (values.length === 1) {
405
+ // Full circle
406
+ svg += `<circle cx="${cx}" cy="${cy}" r="${radius}" fill="${color}"/>`;
407
+ } else {
408
+ svg += `<path d="M${cx},${cy} L${x1.toFixed(2)},${y1.toFixed(2)} A${radius},${radius} 0 ${largeArc},1 ${x2.toFixed(2)},${y2.toFixed(2)} Z" fill="${color}" stroke="white" stroke-width="2"/>`;
409
+ }
410
+
411
+ // Percentage label on slice
412
+ const midAngle = startAngle + sliceAngle / 2;
413
+ const pct = ((Math.abs(values[i]) / total) * 100).toFixed(1);
414
+ if (parseFloat(pct) >= 3) {
415
+ const labelR = radius * 0.65;
416
+ const lx = cx + labelR * Math.cos(midAngle);
417
+ const ly = cy + labelR * Math.sin(midAngle);
418
+ svg += `<text x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" text-anchor="middle" dominant-baseline="central" font-size="12" font-weight="bold" fill="white">${pct}%</text>`;
419
+ }
420
+
421
+ startAngle = endAngle;
422
+ }
423
+
424
+ // Legend
425
+ const legendStartY = height - 20 - Math.ceil(data.labels.length / 3) * 18;
426
+ const colWidth = width / 3;
427
+ for (let i = 0; i < data.labels.length; i++) {
428
+ const col = i % 3;
429
+ const row = Math.floor(i / 3);
430
+ const lx = 20 + col * colWidth;
431
+ const ly = legendStartY + row * 18;
432
+ const color = colors[i % colors.length];
433
+ const label = data.labels[i].length > 16 ? data.labels[i].slice(0, 15) + '\u2026' : data.labels[i];
434
+ svg += `<rect x="${lx}" y="${ly}" width="10" height="10" fill="${color}" rx="2"/>`;
435
+ svg += `<text x="${lx + 14}" y="${ly + 9}" font-size="11" fill="#666">${escapeXml(label)}</text>`;
436
+ }
437
+
438
+ svg += '</svg>';
439
+ return svg;
440
+ }
441
+
442
+ // ── Skill entry point ───────────────────────────
443
+
444
+ export default function structuredDataSkill(api: SkillApi): void {
445
+ // ── parse_csv ──
446
+ api.registerTool({
447
+ name: 'parse_csv',
448
+ description: [
449
+ 'Parse CSV text into a JSON array of objects.',
450
+ 'Auto-detects delimiter (comma, tab, semicolon, pipe).',
451
+ 'Handles quoted fields with embedded commas and newlines.',
452
+ 'Returns data, columns, row_count, and preview (first 5 rows).',
453
+ ].join(' '),
454
+ parameters: {
455
+ type: 'object',
456
+ required: ['csv'],
457
+ properties: {
458
+ csv: { type: 'string', description: 'CSV text to parse.' },
459
+ delimiter: { type: 'string', description: 'Delimiter character. Auto-detected if not specified.' },
460
+ headers: { type: 'boolean', description: 'Whether first row is headers (default true).' },
461
+ limit: { type: 'number', description: 'Maximum number of data rows to parse.' },
462
+ },
463
+ },
464
+ handler: async ({ csv, delimiter, headers, limit }: {
465
+ csv: string; delimiter?: string; headers?: boolean; limit?: number;
466
+ }) => {
467
+ if (!csv || typeof csv !== 'string') {
468
+ return { error: 'csv parameter is required and must be a string' };
469
+ }
470
+
471
+ try {
472
+ const result = parseCSVText(csv, { delimiter, headers, limit });
473
+ return {
474
+ data: result.data,
475
+ columns: result.columns,
476
+ row_count: result.data.length,
477
+ preview: result.data.slice(0, 5),
478
+ };
479
+ } catch (err: any) {
480
+ return { error: `CSV parse error: ${err.message}` };
481
+ }
482
+ },
483
+ });
484
+
485
+ // ── json_to_csv ──
486
+ api.registerTool({
487
+ name: 'json_to_csv',
488
+ description: [
489
+ 'Convert a JSON array of objects to CSV text.',
490
+ 'Auto-quotes fields containing delimiters, newlines, or quotes.',
491
+ 'Handles nested values via JSON.stringify.',
492
+ ].join(' '),
493
+ parameters: {
494
+ type: 'object',
495
+ required: ['data'],
496
+ properties: {
497
+ data: { type: 'array', description: 'Array of objects to convert to CSV.' },
498
+ columns: { type: 'array', description: 'Optional array of column names to include/reorder.' },
499
+ delimiter: { type: 'string', description: 'Delimiter character (default ",").' },
500
+ },
501
+ },
502
+ handler: async ({ data, columns, delimiter }: {
503
+ data: Record<string, unknown>[]; columns?: string[]; delimiter?: string;
504
+ }) => {
505
+ if (!Array.isArray(data)) {
506
+ return { error: 'data parameter must be an array of objects' };
507
+ }
508
+ if (data.length === 0) {
509
+ return { csv: '', row_count: 0, column_count: 0 };
510
+ }
511
+
512
+ try {
513
+ const csvText = jsonToCSVText(data, { columns, delimiter });
514
+ const usedColumns = columns && columns.length > 0
515
+ ? columns
516
+ : [...new Set(data.flatMap(row => Object.keys(row)))];
517
+
518
+ return {
519
+ csv: csvText,
520
+ row_count: data.length,
521
+ column_count: usedColumns.length,
522
+ };
523
+ } catch (err: any) {
524
+ return { error: `CSV conversion error: ${err.message}` };
525
+ }
526
+ },
527
+ });
528
+
529
+ // ── generate_chart ──
530
+ api.registerTool({
531
+ name: 'generate_chart',
532
+ description: [
533
+ 'Generate an SVG chart from data. Supports bar, line, and pie charts.',
534
+ 'Single series: { labels: ["A","B"], values: [10,20] }.',
535
+ 'Multi-series: { labels: ["Q1","Q2"], series: [{ name: "2024", values: [10,20] }] }.',
536
+ 'Returns svg (raw) and data_uri (base64 for markdown embedding).',
537
+ ].join(' '),
538
+ parameters: {
539
+ type: 'object',
540
+ required: ['type', 'data'],
541
+ properties: {
542
+ type: { type: 'string', description: 'Chart type: "bar", "line", or "pie".' },
543
+ data: { description: 'Chart data: { labels, values } or { labels, series: [{ name, values }] }.' },
544
+ title: { type: 'string', description: 'Optional chart title.' },
545
+ width: { type: 'number', description: 'Chart width in pixels (default 600).' },
546
+ height: { type: 'number', description: 'Chart height in pixels (default 400).' },
547
+ colors: { type: 'array', description: 'Optional array of hex color strings.' },
548
+ },
549
+ },
550
+ handler: async ({ type, data, title, width, height, colors }: {
551
+ type: string;
552
+ data: ChartData;
553
+ title?: string;
554
+ width?: number;
555
+ height?: number;
556
+ colors?: string[];
557
+ }) => {
558
+ if (!type || !['bar', 'line', 'pie'].includes(type)) {
559
+ return { error: 'type must be "bar", "line", or "pie"' };
560
+ }
561
+ if (!data || !Array.isArray(data.labels)) {
562
+ return { error: 'data must include a labels array' };
563
+ }
564
+ if (!data.values && !data.series) {
565
+ return { error: 'data must include values (single series) or series (multi-series)' };
566
+ }
567
+
568
+ const w = Math.min(Math.max(width || 600, 200), 2000);
569
+ const h = Math.min(Math.max(height || 400, 150), 2000);
570
+ const palette = colors || DEFAULT_COLORS;
571
+ const chartTitle = title || '';
572
+
573
+ let svg: string;
574
+ try {
575
+ switch (type) {
576
+ case 'bar':
577
+ svg = generateBarChart(data, chartTitle, w, h, palette);
578
+ break;
579
+ case 'line':
580
+ svg = generateLineChart(data, chartTitle, w, h, palette);
581
+ break;
582
+ case 'pie':
583
+ svg = generatePieChart(data, chartTitle, w, h, palette);
584
+ break;
585
+ default:
586
+ return { error: `Unknown chart type: ${type}` };
587
+ }
588
+ } catch (err: any) {
589
+ return { error: `Chart generation error: ${err.message}` };
590
+ }
591
+
592
+ const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`;
593
+
594
+ return { svg, data_uri: dataUri };
595
+ },
596
+ });
597
+ }