@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,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true,
11
+ "declaration": false
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: structured-data
3
+ description: CSV parsing, JSON-to-CSV conversion, and SVG chart generation
4
+ ---
5
+
6
+ ## Structured Data
7
+
8
+ You have tools for working with structured data and creating visualizations:
9
+
10
+ **CSV handling:**
11
+ - `parse_csv` — parse CSV text into a JSON array of objects
12
+ - Auto-detects delimiter (comma, tab, semicolon, pipe)
13
+ - Handles quoted fields with embedded commas and newlines
14
+ - Returns `data` (full array), `columns`, `row_count`, and `preview` (first 5 rows)
15
+ - Use `limit` parameter for large datasets to get just the first N rows
16
+
17
+ - `json_to_csv` — convert a JSON array of objects to CSV text
18
+ - Auto-quotes fields containing delimiters, newlines, or quotes
19
+ - Use `columns` parameter to select/reorder specific columns
20
+ - Nested objects are serialized via JSON.stringify
21
+
22
+ **Charts:**
23
+ - `generate_chart` — generate an SVG chart from data
24
+ - Chart types: `bar`, `line`, `pie`
25
+ - Single series: `{ labels: ["A", "B"], values: [10, 20] }`
26
+ - Multi-series: `{ labels: ["Q1", "Q2"], series: [{ name: "2024", values: [10, 20] }, { name: "2025", values: [15, 25] }] }`
27
+ - Returns `svg` (raw SVG) and `data_uri` (base64 for embedding in markdown)
28
+ - Embed in markdown: `![Chart](data:image/svg+xml;base64,...)`
29
+
30
+ **Best practices:**
31
+ - Use `parse_csv` to convert CSV data into JSON for processing
32
+ - Use `json_to_csv` to convert results back to CSV for delivery
33
+ - Use `generate_chart` to create visualizations for reports
34
+ - Combine with `execute_js` (code-sandbox skill) for complex data transformations
35
+ - Combine with `store_deliverable` to save charts and processed data as job evidence
36
+ - Embed charts in PDF deliverables via the `data_uri` output
@@ -0,0 +1,501 @@
1
+ "use strict";
2
+ /**
3
+ * Structured Data Skill — CSV parsing, JSON-to-CSV conversion, and SVG chart generation
4
+ *
5
+ * Zero external dependencies — pure TypeScript implementations.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.default = structuredDataSkill;
9
+ // ── CSV Parser ──────────────────────────────────
10
+ function detectDelimiter(text) {
11
+ const firstLine = text.split('\n')[0] || '';
12
+ const counts = { ',': 0, '\t': 0, ';': 0, '|': 0 };
13
+ // Count delimiters outside quotes
14
+ let inQuote = false;
15
+ for (const ch of firstLine) {
16
+ if (ch === '"') {
17
+ inQuote = !inQuote;
18
+ continue;
19
+ }
20
+ if (!inQuote && ch in counts)
21
+ counts[ch]++;
22
+ }
23
+ // Pick the delimiter with the highest count
24
+ let best = ',';
25
+ let bestCount = 0;
26
+ for (const [delim, count] of Object.entries(counts)) {
27
+ if (count > bestCount) {
28
+ best = delim;
29
+ bestCount = count;
30
+ }
31
+ }
32
+ return best;
33
+ }
34
+ function parseCSVLine(line, delimiter) {
35
+ const fields = [];
36
+ let current = '';
37
+ let inQuote = false;
38
+ let i = 0;
39
+ while (i < line.length) {
40
+ const ch = line[i];
41
+ if (inQuote) {
42
+ if (ch === '"') {
43
+ // Check for escaped quote ("")
44
+ if (i + 1 < line.length && line[i + 1] === '"') {
45
+ current += '"';
46
+ i += 2;
47
+ }
48
+ else {
49
+ inQuote = false;
50
+ i++;
51
+ }
52
+ }
53
+ else {
54
+ current += ch;
55
+ i++;
56
+ }
57
+ }
58
+ else {
59
+ if (ch === '"') {
60
+ inQuote = true;
61
+ i++;
62
+ }
63
+ else if (ch === delimiter) {
64
+ fields.push(current.trim());
65
+ current = '';
66
+ i++;
67
+ }
68
+ else {
69
+ current += ch;
70
+ i++;
71
+ }
72
+ }
73
+ }
74
+ fields.push(current.trim());
75
+ return fields;
76
+ }
77
+ function parseCSVText(csv, options) {
78
+ const delimiter = options.delimiter || detectDelimiter(csv);
79
+ const useHeaders = options.headers !== false;
80
+ // Split into lines, handling quoted newlines
81
+ const lines = [];
82
+ let current = '';
83
+ let inQuote = false;
84
+ for (const ch of csv) {
85
+ if (ch === '"')
86
+ inQuote = !inQuote;
87
+ if (ch === '\n' && !inQuote) {
88
+ if (current.trim())
89
+ lines.push(current);
90
+ current = '';
91
+ }
92
+ else if (ch === '\r') {
93
+ // Skip carriage returns
94
+ }
95
+ else {
96
+ current += ch;
97
+ }
98
+ }
99
+ if (current.trim())
100
+ lines.push(current);
101
+ if (lines.length === 0)
102
+ return { data: [], columns: [] };
103
+ let columns;
104
+ let startRow;
105
+ if (useHeaders) {
106
+ columns = parseCSVLine(lines[0], delimiter);
107
+ startRow = 1;
108
+ }
109
+ else {
110
+ const firstRow = parseCSVLine(lines[0], delimiter);
111
+ columns = firstRow.map((_, i) => `col_${i}`);
112
+ startRow = 0;
113
+ }
114
+ const maxRows = options.limit || Infinity;
115
+ const data = [];
116
+ for (let i = startRow; i < lines.length && data.length < maxRows; i++) {
117
+ const fields = parseCSVLine(lines[i], delimiter);
118
+ const row = {};
119
+ for (let j = 0; j < columns.length; j++) {
120
+ row[columns[j]] = fields[j] || '';
121
+ }
122
+ data.push(row);
123
+ }
124
+ return { data, columns };
125
+ }
126
+ // ── JSON to CSV ─────────────────────────────────
127
+ function escapeCSVField(value, delimiter) {
128
+ if (value.includes(delimiter) ||
129
+ value.includes('"') ||
130
+ value.includes('\n') ||
131
+ value.includes('\r')) {
132
+ return '"' + value.replace(/"/g, '""') + '"';
133
+ }
134
+ return value;
135
+ }
136
+ function jsonToCSVText(data, options) {
137
+ if (data.length === 0)
138
+ return '';
139
+ const delimiter = options.delimiter || ',';
140
+ // Determine columns
141
+ let columns;
142
+ if (options.columns && options.columns.length > 0) {
143
+ columns = options.columns;
144
+ }
145
+ else {
146
+ // Collect all keys from all rows
147
+ const keySet = new Set();
148
+ for (const row of data) {
149
+ for (const key of Object.keys(row))
150
+ keySet.add(key);
151
+ }
152
+ columns = [...keySet];
153
+ }
154
+ const lines = [];
155
+ // Header row
156
+ lines.push(columns.map(c => escapeCSVField(c, delimiter)).join(delimiter));
157
+ // Data rows
158
+ for (const row of data) {
159
+ const fields = columns.map(col => {
160
+ const val = row[col];
161
+ if (val === null || val === undefined)
162
+ return '';
163
+ if (typeof val === 'object')
164
+ return escapeCSVField(JSON.stringify(val), delimiter);
165
+ return escapeCSVField(String(val), delimiter);
166
+ });
167
+ lines.push(fields.join(delimiter));
168
+ }
169
+ return lines.join('\n');
170
+ }
171
+ // ── SVG Chart Generator ─────────────────────────
172
+ const DEFAULT_COLORS = ['#4285F4', '#EA4335', '#FBBC04', '#34A853', '#FF6D01', '#46BDC6', '#7B1FA2', '#C2185B'];
173
+ function escapeXml(text) {
174
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
175
+ }
176
+ function generateBarChart(data, title, width, height, colors) {
177
+ const margin = { top: title ? 50 : 30, right: 20, bottom: 60, left: 60 };
178
+ const chartW = width - margin.left - margin.right;
179
+ const chartH = height - margin.top - margin.bottom;
180
+ const series = data.series || [{ name: 'Value', values: data.values || [] }];
181
+ const allValues = series.flatMap(s => s.values);
182
+ const maxVal = Math.max(...allValues, 0) || 1;
183
+ const minVal = Math.min(0, ...allValues);
184
+ const range = maxVal - minVal || 1;
185
+ const groupCount = data.labels.length;
186
+ const seriesCount = series.length;
187
+ const groupWidth = chartW / groupCount;
188
+ const barWidth = Math.min((groupWidth * 0.8) / seriesCount, 50);
189
+ const groupPad = (groupWidth - barWidth * seriesCount) / 2;
190
+ 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">`;
191
+ // Background
192
+ svg += `<rect width="${width}" height="${height}" fill="white"/>`;
193
+ // Title
194
+ if (title) {
195
+ svg += `<text x="${width / 2}" y="25" text-anchor="middle" font-size="16" font-weight="bold" fill="#333">${escapeXml(title)}</text>`;
196
+ }
197
+ // Y-axis gridlines and labels
198
+ const ySteps = 5;
199
+ for (let i = 0; i <= ySteps; i++) {
200
+ const val = minVal + (range * i) / ySteps;
201
+ const y = margin.top + chartH - (chartH * (val - minVal)) / range;
202
+ svg += `<line x1="${margin.left}" y1="${y}" x2="${margin.left + chartW}" y2="${y}" stroke="#eee" stroke-width="1"/>`;
203
+ const label = Math.abs(val) >= 1000 ? `${(val / 1000).toFixed(1)}k` : val % 1 === 0 ? String(val) : val.toFixed(1);
204
+ svg += `<text x="${margin.left - 8}" y="${y + 4}" text-anchor="end" font-size="11" fill="#666">${label}</text>`;
205
+ }
206
+ // Bars
207
+ const zeroY = margin.top + chartH - (chartH * (0 - minVal)) / range;
208
+ for (let g = 0; g < groupCount; g++) {
209
+ for (let s = 0; s < seriesCount; s++) {
210
+ const val = series[s].values[g] || 0;
211
+ const barH = (chartH * Math.abs(val)) / range;
212
+ const x = margin.left + g * groupWidth + groupPad + s * barWidth;
213
+ const y = val >= 0 ? zeroY - barH : zeroY;
214
+ const color = colors[s % colors.length];
215
+ svg += `<rect x="${x}" y="${y}" width="${barWidth}" height="${barH}" fill="${color}" rx="2"/>`;
216
+ }
217
+ // X-axis label
218
+ const labelX = margin.left + g * groupWidth + groupWidth / 2;
219
+ const label = data.labels[g].length > 12 ? data.labels[g].slice(0, 11) + '\u2026' : data.labels[g];
220
+ svg += `<text x="${labelX}" y="${margin.top + chartH + 18}" text-anchor="middle" font-size="11" fill="#666">${escapeXml(label)}</text>`;
221
+ }
222
+ // Axes
223
+ svg += `<line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + chartH}" stroke="#ccc" stroke-width="1"/>`;
224
+ svg += `<line x1="${margin.left}" y1="${zeroY}" x2="${margin.left + chartW}" y2="${zeroY}" stroke="#ccc" stroke-width="1"/>`;
225
+ // Legend for multi-series
226
+ if (seriesCount > 1) {
227
+ const legendY = height - 15;
228
+ let legendX = margin.left;
229
+ for (let s = 0; s < seriesCount; s++) {
230
+ const color = colors[s % colors.length];
231
+ svg += `<rect x="${legendX}" y="${legendY - 8}" width="12" height="12" fill="${color}" rx="2"/>`;
232
+ svg += `<text x="${legendX + 16}" y="${legendY + 2}" font-size="11" fill="#666">${escapeXml(series[s].name)}</text>`;
233
+ legendX += 16 + series[s].name.length * 7 + 16;
234
+ }
235
+ }
236
+ svg += '</svg>';
237
+ return svg;
238
+ }
239
+ function generateLineChart(data, title, width, height, colors) {
240
+ const margin = { top: title ? 50 : 30, right: 20, bottom: 60, left: 60 };
241
+ const chartW = width - margin.left - margin.right;
242
+ const chartH = height - margin.top - margin.bottom;
243
+ const series = data.series || [{ name: 'Value', values: data.values || [] }];
244
+ const allValues = series.flatMap(s => s.values);
245
+ const maxVal = Math.max(...allValues, 0) || 1;
246
+ const minVal = Math.min(...allValues, 0);
247
+ const range = maxVal - minVal || 1;
248
+ // Add 10% padding
249
+ const paddedMin = minVal - range * 0.05;
250
+ const paddedRange = range * 1.1;
251
+ const pointCount = data.labels.length;
252
+ 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">`;
253
+ svg += `<rect width="${width}" height="${height}" fill="white"/>`;
254
+ if (title) {
255
+ svg += `<text x="${width / 2}" y="25" text-anchor="middle" font-size="16" font-weight="bold" fill="#333">${escapeXml(title)}</text>`;
256
+ }
257
+ // Y-axis gridlines
258
+ const ySteps = 5;
259
+ for (let i = 0; i <= ySteps; i++) {
260
+ const val = paddedMin + (paddedRange * i) / ySteps;
261
+ const y = margin.top + chartH - (chartH * i) / ySteps;
262
+ svg += `<line x1="${margin.left}" y1="${y}" x2="${margin.left + chartW}" y2="${y}" stroke="#eee" stroke-width="1"/>`;
263
+ const label = Math.abs(val) >= 1000 ? `${(val / 1000).toFixed(1)}k` : val % 1 === 0 ? String(Math.round(val)) : val.toFixed(1);
264
+ svg += `<text x="${margin.left - 8}" y="${y + 4}" text-anchor="end" font-size="11" fill="#666">${label}</text>`;
265
+ }
266
+ // Lines and points
267
+ for (let s = 0; s < series.length; s++) {
268
+ const color = colors[s % colors.length];
269
+ const points = [];
270
+ for (let i = 0; i < pointCount; i++) {
271
+ const val = series[s].values[i] || 0;
272
+ const x = margin.left + (i / Math.max(pointCount - 1, 1)) * chartW;
273
+ const y = margin.top + chartH - (chartH * (val - paddedMin)) / paddedRange;
274
+ points.push({ x, y });
275
+ }
276
+ // Line path
277
+ if (points.length > 0) {
278
+ const pathD = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
279
+ svg += `<path d="${pathD}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linejoin="round"/>`;
280
+ }
281
+ // Points
282
+ for (const p of points) {
283
+ svg += `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3.5" fill="${color}" stroke="white" stroke-width="1.5"/>`;
284
+ }
285
+ }
286
+ // X-axis labels
287
+ for (let i = 0; i < pointCount; i++) {
288
+ const x = margin.left + (i / Math.max(pointCount - 1, 1)) * chartW;
289
+ const label = data.labels[i].length > 12 ? data.labels[i].slice(0, 11) + '\u2026' : data.labels[i];
290
+ svg += `<text x="${x}" y="${margin.top + chartH + 18}" text-anchor="middle" font-size="11" fill="#666">${escapeXml(label)}</text>`;
291
+ }
292
+ // Axes
293
+ svg += `<line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + chartH}" stroke="#ccc" stroke-width="1"/>`;
294
+ svg += `<line x1="${margin.left}" y1="${margin.top + chartH}" x2="${margin.left + chartW}" y2="${margin.top + chartH}" stroke="#ccc" stroke-width="1"/>`;
295
+ // Legend
296
+ if (series.length > 1) {
297
+ const legendY = height - 15;
298
+ let legendX = margin.left;
299
+ for (let s = 0; s < series.length; s++) {
300
+ const color = colors[s % colors.length];
301
+ svg += `<rect x="${legendX}" y="${legendY - 8}" width="12" height="12" fill="${color}" rx="2"/>`;
302
+ svg += `<text x="${legendX + 16}" y="${legendY + 2}" font-size="11" fill="#666">${escapeXml(series[s].name)}</text>`;
303
+ legendX += 16 + series[s].name.length * 7 + 16;
304
+ }
305
+ }
306
+ svg += '</svg>';
307
+ return svg;
308
+ }
309
+ function generatePieChart(data, title, width, height, colors) {
310
+ const values = data.values || [];
311
+ const total = values.reduce((sum, v) => sum + Math.abs(v), 0) || 1;
312
+ const cx = width / 2;
313
+ const cy = (height - (title ? 30 : 0)) / 2 + (title ? 40 : 0);
314
+ const radius = Math.min(width, height - (title ? 60 : 20)) / 2 - 40;
315
+ 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">`;
316
+ svg += `<rect width="${width}" height="${height}" fill="white"/>`;
317
+ if (title) {
318
+ svg += `<text x="${width / 2}" y="25" text-anchor="middle" font-size="16" font-weight="bold" fill="#333">${escapeXml(title)}</text>`;
319
+ }
320
+ let startAngle = -Math.PI / 2;
321
+ for (let i = 0; i < values.length; i++) {
322
+ const sliceAngle = (Math.abs(values[i]) / total) * 2 * Math.PI;
323
+ const endAngle = startAngle + sliceAngle;
324
+ const color = colors[i % colors.length];
325
+ const x1 = cx + radius * Math.cos(startAngle);
326
+ const y1 = cy + radius * Math.sin(startAngle);
327
+ const x2 = cx + radius * Math.cos(endAngle);
328
+ const y2 = cy + radius * Math.sin(endAngle);
329
+ const largeArc = sliceAngle > Math.PI ? 1 : 0;
330
+ if (values.length === 1) {
331
+ // Full circle
332
+ svg += `<circle cx="${cx}" cy="${cy}" r="${radius}" fill="${color}"/>`;
333
+ }
334
+ else {
335
+ 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"/>`;
336
+ }
337
+ // Percentage label on slice
338
+ const midAngle = startAngle + sliceAngle / 2;
339
+ const pct = ((Math.abs(values[i]) / total) * 100).toFixed(1);
340
+ if (parseFloat(pct) >= 3) {
341
+ const labelR = radius * 0.65;
342
+ const lx = cx + labelR * Math.cos(midAngle);
343
+ const ly = cy + labelR * Math.sin(midAngle);
344
+ 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>`;
345
+ }
346
+ startAngle = endAngle;
347
+ }
348
+ // Legend
349
+ const legendStartY = height - 20 - Math.ceil(data.labels.length / 3) * 18;
350
+ const colWidth = width / 3;
351
+ for (let i = 0; i < data.labels.length; i++) {
352
+ const col = i % 3;
353
+ const row = Math.floor(i / 3);
354
+ const lx = 20 + col * colWidth;
355
+ const ly = legendStartY + row * 18;
356
+ const color = colors[i % colors.length];
357
+ const label = data.labels[i].length > 16 ? data.labels[i].slice(0, 15) + '\u2026' : data.labels[i];
358
+ svg += `<rect x="${lx}" y="${ly}" width="10" height="10" fill="${color}" rx="2"/>`;
359
+ svg += `<text x="${lx + 14}" y="${ly + 9}" font-size="11" fill="#666">${escapeXml(label)}</text>`;
360
+ }
361
+ svg += '</svg>';
362
+ return svg;
363
+ }
364
+ // ── Skill entry point ───────────────────────────
365
+ function structuredDataSkill(api) {
366
+ // ── parse_csv ──
367
+ api.registerTool({
368
+ name: 'parse_csv',
369
+ description: [
370
+ 'Parse CSV text into a JSON array of objects.',
371
+ 'Auto-detects delimiter (comma, tab, semicolon, pipe).',
372
+ 'Handles quoted fields with embedded commas and newlines.',
373
+ 'Returns data, columns, row_count, and preview (first 5 rows).',
374
+ ].join(' '),
375
+ parameters: {
376
+ type: 'object',
377
+ required: ['csv'],
378
+ properties: {
379
+ csv: { type: 'string', description: 'CSV text to parse.' },
380
+ delimiter: { type: 'string', description: 'Delimiter character. Auto-detected if not specified.' },
381
+ headers: { type: 'boolean', description: 'Whether first row is headers (default true).' },
382
+ limit: { type: 'number', description: 'Maximum number of data rows to parse.' },
383
+ },
384
+ },
385
+ handler: async ({ csv, delimiter, headers, limit }) => {
386
+ if (!csv || typeof csv !== 'string') {
387
+ return { error: 'csv parameter is required and must be a string' };
388
+ }
389
+ try {
390
+ const result = parseCSVText(csv, { delimiter, headers, limit });
391
+ return {
392
+ data: result.data,
393
+ columns: result.columns,
394
+ row_count: result.data.length,
395
+ preview: result.data.slice(0, 5),
396
+ };
397
+ }
398
+ catch (err) {
399
+ return { error: `CSV parse error: ${err.message}` };
400
+ }
401
+ },
402
+ });
403
+ // ── json_to_csv ──
404
+ api.registerTool({
405
+ name: 'json_to_csv',
406
+ description: [
407
+ 'Convert a JSON array of objects to CSV text.',
408
+ 'Auto-quotes fields containing delimiters, newlines, or quotes.',
409
+ 'Handles nested values via JSON.stringify.',
410
+ ].join(' '),
411
+ parameters: {
412
+ type: 'object',
413
+ required: ['data'],
414
+ properties: {
415
+ data: { type: 'array', description: 'Array of objects to convert to CSV.' },
416
+ columns: { type: 'array', description: 'Optional array of column names to include/reorder.' },
417
+ delimiter: { type: 'string', description: 'Delimiter character (default ",").' },
418
+ },
419
+ },
420
+ handler: async ({ data, columns, delimiter }) => {
421
+ if (!Array.isArray(data)) {
422
+ return { error: 'data parameter must be an array of objects' };
423
+ }
424
+ if (data.length === 0) {
425
+ return { csv: '', row_count: 0, column_count: 0 };
426
+ }
427
+ try {
428
+ const csvText = jsonToCSVText(data, { columns, delimiter });
429
+ const usedColumns = columns && columns.length > 0
430
+ ? columns
431
+ : [...new Set(data.flatMap(row => Object.keys(row)))];
432
+ return {
433
+ csv: csvText,
434
+ row_count: data.length,
435
+ column_count: usedColumns.length,
436
+ };
437
+ }
438
+ catch (err) {
439
+ return { error: `CSV conversion error: ${err.message}` };
440
+ }
441
+ },
442
+ });
443
+ // ── generate_chart ──
444
+ api.registerTool({
445
+ name: 'generate_chart',
446
+ description: [
447
+ 'Generate an SVG chart from data. Supports bar, line, and pie charts.',
448
+ 'Single series: { labels: ["A","B"], values: [10,20] }.',
449
+ 'Multi-series: { labels: ["Q1","Q2"], series: [{ name: "2024", values: [10,20] }] }.',
450
+ 'Returns svg (raw) and data_uri (base64 for markdown embedding).',
451
+ ].join(' '),
452
+ parameters: {
453
+ type: 'object',
454
+ required: ['type', 'data'],
455
+ properties: {
456
+ type: { type: 'string', description: 'Chart type: "bar", "line", or "pie".' },
457
+ data: { description: 'Chart data: { labels, values } or { labels, series: [{ name, values }] }.' },
458
+ title: { type: 'string', description: 'Optional chart title.' },
459
+ width: { type: 'number', description: 'Chart width in pixels (default 600).' },
460
+ height: { type: 'number', description: 'Chart height in pixels (default 400).' },
461
+ colors: { type: 'array', description: 'Optional array of hex color strings.' },
462
+ },
463
+ },
464
+ handler: async ({ type, data, title, width, height, colors }) => {
465
+ if (!type || !['bar', 'line', 'pie'].includes(type)) {
466
+ return { error: 'type must be "bar", "line", or "pie"' };
467
+ }
468
+ if (!data || !Array.isArray(data.labels)) {
469
+ return { error: 'data must include a labels array' };
470
+ }
471
+ if (!data.values && !data.series) {
472
+ return { error: 'data must include values (single series) or series (multi-series)' };
473
+ }
474
+ const w = Math.min(Math.max(width || 600, 200), 2000);
475
+ const h = Math.min(Math.max(height || 400, 150), 2000);
476
+ const palette = colors || DEFAULT_COLORS;
477
+ const chartTitle = title || '';
478
+ let svg;
479
+ try {
480
+ switch (type) {
481
+ case 'bar':
482
+ svg = generateBarChart(data, chartTitle, w, h, palette);
483
+ break;
484
+ case 'line':
485
+ svg = generateLineChart(data, chartTitle, w, h, palette);
486
+ break;
487
+ case 'pie':
488
+ svg = generatePieChart(data, chartTitle, w, h, palette);
489
+ break;
490
+ default:
491
+ return { error: `Unknown chart type: ${type}` };
492
+ }
493
+ }
494
+ catch (err) {
495
+ return { error: `Chart generation error: ${err.message}` };
496
+ }
497
+ const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`;
498
+ return { svg, data_uri: dataUri };
499
+ },
500
+ });
501
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "structured-data",
3
+ "version": "1.0.0",
4
+ "description": "Parse CSV, convert JSON to CSV, and generate SVG charts from data",
5
+ "author": "xpr-agents",
6
+ "category": "data",
7
+ "tags": ["csv", "json", "chart", "data", "visualization"],
8
+ "capabilities": ["csv-processing", "chart-generation"],
9
+ "tools": ["parse_csv", "json_to_csv", "generate_chart"],
10
+ "requires": {
11
+ "env": []
12
+ }
13
+ }