@yeyuan98/opencode-bioresearcher-plugin 1.6.8-alpha.2 → 1.7.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.
- package/dist/skills/bioresearcher-tests/SKILL.md +46 -4
- package/dist/skills/bioresearcher-tests/test_cases/table_tests.md +347 -9
- package/dist/skills/bioresearcher-tests/test_runner.py +36 -1
- package/dist/tools/table/index.d.ts +35 -7
- package/dist/tools/table/tools.d.ts +36 -8
- package/dist/tools/table/tools.js +269 -117
- package/dist/tools/table/utils.d.ts +7 -1
- package/dist/tools/table/utils.js +56 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as fs from 'fs';
|
|
2
3
|
import * as XLSX from 'xlsx';
|
|
3
|
-
import
|
|
4
|
-
import { processCellValue, formatError, resolvePath, getSheet } from './utils';
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { processCellValue, formatError, resolvePath, getSheet, checkFileSize, writeAtomic, iterativeMin, iterativeMax, DEFAULT_MAX_ROW_CAP, DEFAULT_MAX_RANGE_CELLS, } from './utils';
|
|
6
|
+
const CELL_ADDRESS_REGEX = /^[A-Za-z]+\d+$/;
|
|
7
|
+
const FORCE_DESC = "Override file size / row count limits. Use when you need to work with large files and accept the performance implications.";
|
|
5
8
|
export const tableGetSheetPreview = tool({
|
|
6
9
|
description: "Get first 6 rows of a worksheet to preview data structure",
|
|
7
10
|
args: {
|
|
8
11
|
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
9
|
-
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)")
|
|
12
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
13
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
10
14
|
},
|
|
11
15
|
execute: async (args, context) => {
|
|
12
16
|
await context.ask({ permission: "tableGetSheetPreview", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
13
17
|
try {
|
|
14
18
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
19
|
+
checkFileSize(resolvedPath, args.force);
|
|
15
20
|
const workbook = XLSX.readFile(resolvedPath);
|
|
16
21
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
17
|
-
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
|
22
|
+
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 6 });
|
|
18
23
|
const previewRows = data.slice(0, 6);
|
|
24
|
+
const ref = worksheet['!ref'] ? XLSX.utils.decode_range(worksheet['!ref']) : null;
|
|
25
|
+
const totalRows = ref ? ref.e.r - ref.s.r + 1 : data.length;
|
|
19
26
|
return JSON.stringify({
|
|
20
27
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
21
|
-
total_rows:
|
|
22
|
-
preview: previewRows
|
|
28
|
+
total_rows: totalRows,
|
|
29
|
+
preview: previewRows,
|
|
23
30
|
}, null, 2);
|
|
24
31
|
}
|
|
25
32
|
catch (error) {
|
|
@@ -30,12 +37,14 @@ export const tableGetSheetPreview = tool({
|
|
|
30
37
|
export const tableListSheets = tool({
|
|
31
38
|
description: "List all worksheet names in a table file",
|
|
32
39
|
args: {
|
|
33
|
-
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)")
|
|
40
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
41
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
34
42
|
},
|
|
35
43
|
execute: async (args, context) => {
|
|
36
44
|
await context.ask({ permission: "tableListSheets", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
37
45
|
try {
|
|
38
46
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
47
|
+
checkFileSize(resolvedPath, args.force);
|
|
39
48
|
const workbook = XLSX.readFile(resolvedPath);
|
|
40
49
|
return JSON.stringify({ sheets: workbook.SheetNames }, null, 2);
|
|
41
50
|
}
|
|
@@ -48,20 +57,27 @@ export const tableGetHeaders = tool({
|
|
|
48
57
|
description: "Get column headers (first row) from a worksheet",
|
|
49
58
|
args: {
|
|
50
59
|
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
51
|
-
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)")
|
|
60
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
61
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
52
62
|
},
|
|
53
63
|
execute: async (args, context) => {
|
|
54
64
|
await context.ask({ permission: "tableGetHeaders", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
55
65
|
try {
|
|
56
66
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
67
|
+
checkFileSize(resolvedPath, args.force);
|
|
57
68
|
const workbook = XLSX.readFile(resolvedPath);
|
|
58
69
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
59
|
-
const
|
|
60
|
-
const headers =
|
|
70
|
+
const ref = worksheet['!ref'] ? XLSX.utils.decode_range(worksheet['!ref']) : { s: { r: 0, c: 0 }, e: { r: 0, c: 0 } };
|
|
71
|
+
const headers = [];
|
|
72
|
+
for (let C = ref.s.c; C <= ref.e.c; C++) {
|
|
73
|
+
const addr = XLSX.utils.encode_cell({ r: ref.s.r, c: C });
|
|
74
|
+
const cell = worksheet[addr];
|
|
75
|
+
headers.push(processCellValue(cell));
|
|
76
|
+
}
|
|
61
77
|
return JSON.stringify({
|
|
62
78
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
63
79
|
headers: headers,
|
|
64
|
-
column_count: headers.length
|
|
80
|
+
column_count: headers.length,
|
|
65
81
|
}, null, 2);
|
|
66
82
|
}
|
|
67
83
|
catch (error) {
|
|
@@ -74,12 +90,17 @@ export const tableGetCell = tool({
|
|
|
74
90
|
args: {
|
|
75
91
|
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
76
92
|
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
77
|
-
cell_address: z.string().describe("Cell address (e.g., 'A1', 'B5')")
|
|
93
|
+
cell_address: z.string().describe("Cell address (e.g., 'A1', 'B5')"),
|
|
94
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
78
95
|
},
|
|
79
96
|
execute: async (args, context) => {
|
|
80
97
|
await context.ask({ permission: "tableGetCell", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
81
98
|
try {
|
|
99
|
+
if (!CELL_ADDRESS_REGEX.test(args.cell_address)) {
|
|
100
|
+
throw new Error(`Invalid cell address "${args.cell_address}". Expected format like "A1", "B5", "AA123".`);
|
|
101
|
+
}
|
|
82
102
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
103
|
+
checkFileSize(resolvedPath, args.force);
|
|
83
104
|
const workbook = XLSX.readFile(resolvedPath);
|
|
84
105
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
85
106
|
const cell = worksheet[args.cell_address];
|
|
@@ -88,7 +109,7 @@ export const tableGetCell = tool({
|
|
|
88
109
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
89
110
|
cell_address: args.cell_address,
|
|
90
111
|
value: value,
|
|
91
|
-
type: cell?.t || 'unknown'
|
|
112
|
+
type: cell?.t || 'unknown',
|
|
92
113
|
}, null, 2);
|
|
93
114
|
}
|
|
94
115
|
catch (error) {
|
|
@@ -97,49 +118,90 @@ export const tableGetCell = tool({
|
|
|
97
118
|
}
|
|
98
119
|
});
|
|
99
120
|
export const tableFilterRows = tool({
|
|
100
|
-
description: "Filter rows
|
|
121
|
+
description: "Filter rows by a column value using an operator. Preferred over tableSearch when targeting a specific column. Supports: exact match (=), comparison (>, <, >=, <=), and text matching (contains, starts_with, ends_with). Returns full matching rows. All text matching is case-insensitive.",
|
|
101
122
|
args: {
|
|
102
123
|
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
103
124
|
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
104
125
|
column: z.string().describe("Column name to filter on"),
|
|
105
|
-
operator: z.enum(['=', '!=', '>', '<', '>=', '<=', 'contains']).describe("Comparison operator"),
|
|
106
|
-
value: z.
|
|
107
|
-
max_results: z.number().default(100).describe("Maximum number of results to return")
|
|
126
|
+
operator: z.enum(['=', '!=', '>', '<', '>=', '<=', 'contains', 'starts_with', 'ends_with']).describe("Comparison operator"),
|
|
127
|
+
value: z.union([z.string(), z.number(), z.boolean()]).describe("Value to compare against"),
|
|
128
|
+
max_results: z.number().default(100).describe("Maximum number of results to return"),
|
|
129
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
108
130
|
},
|
|
109
131
|
execute: async (args, context) => {
|
|
110
132
|
await context.ask({ permission: "tableFilterRows", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
111
133
|
try {
|
|
112
134
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
135
|
+
checkFileSize(resolvedPath, args.force);
|
|
113
136
|
const workbook = XLSX.readFile(resolvedPath);
|
|
114
137
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
138
|
+
const effectiveMax = args.force ? args.max_results : Math.min(args.max_results, DEFAULT_MAX_ROW_CAP);
|
|
139
|
+
const filteredRows = [];
|
|
140
|
+
let totalMatchesFound = 0;
|
|
115
141
|
const data = XLSX.utils.sheet_to_json(worksheet);
|
|
116
|
-
|
|
142
|
+
for (let i = 0; i < data.length; i++) {
|
|
143
|
+
const row = data[i];
|
|
117
144
|
let cellValue = row[args.column];
|
|
118
145
|
let comparisonValue = args.value;
|
|
119
|
-
// Type coercion for numeric comparisons
|
|
120
146
|
if (typeof cellValue === 'string' && cellValue.trim() !== '' && !isNaN(Number(cellValue))) {
|
|
121
147
|
cellValue = Number(cellValue);
|
|
122
148
|
}
|
|
123
149
|
if (typeof comparisonValue === 'string' && comparisonValue.trim() !== '' && !isNaN(Number(comparisonValue))) {
|
|
124
150
|
comparisonValue = Number(comparisonValue);
|
|
125
151
|
}
|
|
152
|
+
let matches = false;
|
|
126
153
|
switch (args.operator) {
|
|
127
|
-
case '=':
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
case '
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
case '
|
|
134
|
-
|
|
154
|
+
case '=':
|
|
155
|
+
matches = cellValue === comparisonValue;
|
|
156
|
+
break;
|
|
157
|
+
case '!=':
|
|
158
|
+
matches = cellValue !== comparisonValue;
|
|
159
|
+
break;
|
|
160
|
+
case '>':
|
|
161
|
+
matches = cellValue > comparisonValue;
|
|
162
|
+
break;
|
|
163
|
+
case '<':
|
|
164
|
+
matches = cellValue < comparisonValue;
|
|
165
|
+
break;
|
|
166
|
+
case '>=':
|
|
167
|
+
matches = cellValue >= comparisonValue;
|
|
168
|
+
break;
|
|
169
|
+
case '<=':
|
|
170
|
+
matches = cellValue <= comparisonValue;
|
|
171
|
+
break;
|
|
172
|
+
case 'contains':
|
|
173
|
+
matches = String(cellValue).toLowerCase().includes(String(comparisonValue).toLowerCase());
|
|
174
|
+
break;
|
|
175
|
+
case 'starts_with':
|
|
176
|
+
matches = String(cellValue).toLowerCase().startsWith(String(comparisonValue).toLowerCase());
|
|
177
|
+
break;
|
|
178
|
+
case 'ends_with':
|
|
179
|
+
matches = String(cellValue).toLowerCase().endsWith(String(comparisonValue).toLowerCase());
|
|
180
|
+
break;
|
|
135
181
|
}
|
|
136
|
-
|
|
137
|
-
|
|
182
|
+
if (matches) {
|
|
183
|
+
totalMatchesFound++;
|
|
184
|
+
if (filteredRows.length < effectiveMax) {
|
|
185
|
+
filteredRows.push(row);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!args.force && i >= DEFAULT_MAX_ROW_CAP - 1) {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const truncated = !args.force && data.length > DEFAULT_MAX_ROW_CAP;
|
|
193
|
+
const result = {
|
|
138
194
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
139
195
|
filter: { column: args.column, operator: args.operator, value: args.value },
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
196
|
+
total_matches_found: totalMatchesFound,
|
|
197
|
+
returned_count: filteredRows.length,
|
|
198
|
+
matched_rows: filteredRows,
|
|
199
|
+
};
|
|
200
|
+
if (truncated) {
|
|
201
|
+
result.truncated = true;
|
|
202
|
+
result.message = `Processed ${DEFAULT_MAX_ROW_CAP} of ${data.length} rows. Set force: true to process all rows.`;
|
|
203
|
+
}
|
|
204
|
+
return JSON.stringify(result, null, 2);
|
|
143
205
|
}
|
|
144
206
|
catch (error) {
|
|
145
207
|
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "filter_rows", error });
|
|
@@ -147,43 +209,58 @@ export const tableFilterRows = tool({
|
|
|
147
209
|
}
|
|
148
210
|
});
|
|
149
211
|
export const tableSearch = tool({
|
|
150
|
-
description: "Search for a term across all cells in a worksheet",
|
|
212
|
+
description: "Search for a term across all cells in a worksheet using case-insensitive substring matching. Returns individual cell matches (address, value, row index) — not full rows. Use when you don't know which column is relevant. For column-specific filtering, use tableFilterRows.",
|
|
151
213
|
args: {
|
|
152
214
|
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
153
215
|
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
154
216
|
search_term: z.string().describe("Term to search for"),
|
|
155
|
-
max_results: z.number().default(50).describe("Maximum number of results to return")
|
|
217
|
+
max_results: z.number().default(50).describe("Maximum number of results to return"),
|
|
218
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
156
219
|
},
|
|
157
220
|
execute: async (args, context) => {
|
|
158
221
|
await context.ask({ permission: "tableSearch", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
159
222
|
try {
|
|
160
223
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
224
|
+
checkFileSize(resolvedPath, args.force);
|
|
161
225
|
const workbook = XLSX.readFile(resolvedPath);
|
|
162
226
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
163
227
|
const results = [];
|
|
164
228
|
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1');
|
|
229
|
+
const effectiveMax = args.force ? args.max_results : Math.min(args.max_results, DEFAULT_MAX_ROW_CAP);
|
|
230
|
+
let rowsScanned = 0;
|
|
231
|
+
let totalMatchesFound = 0;
|
|
165
232
|
for (let R = range.s.r; R <= range.e.r; ++R) {
|
|
233
|
+
rowsScanned++;
|
|
234
|
+
if (!args.force && rowsScanned > DEFAULT_MAX_ROW_CAP)
|
|
235
|
+
break;
|
|
166
236
|
for (let C = range.s.c; C <= range.e.c; ++C) {
|
|
167
|
-
if (results.length >= args.max_results)
|
|
168
|
-
break;
|
|
169
237
|
const address = XLSX.utils.encode_cell({ r: R, c: C });
|
|
170
238
|
const cell = worksheet[address];
|
|
171
239
|
if (cell && cell.v !== undefined) {
|
|
172
240
|
const cellValue = String(processCellValue(cell)).toLowerCase();
|
|
173
241
|
if (cellValue.includes(args.search_term.toLowerCase())) {
|
|
174
|
-
|
|
242
|
+
totalMatchesFound++;
|
|
243
|
+
if (results.length < effectiveMax) {
|
|
244
|
+
results.push({ address, value: processCellValue(cell), type: cell.t || 'unknown', row_index: R });
|
|
245
|
+
}
|
|
175
246
|
}
|
|
176
247
|
}
|
|
177
248
|
}
|
|
178
|
-
if (results.length >= args.max_results)
|
|
179
|
-
break;
|
|
180
249
|
}
|
|
181
|
-
|
|
250
|
+
const truncated = !args.force && rowsScanned >= DEFAULT_MAX_ROW_CAP && range.e.r >= DEFAULT_MAX_ROW_CAP;
|
|
251
|
+
const result = {
|
|
182
252
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
183
253
|
search_term: args.search_term,
|
|
184
|
-
|
|
185
|
-
results
|
|
186
|
-
|
|
254
|
+
total_matches_found: totalMatchesFound,
|
|
255
|
+
returned_count: results.length,
|
|
256
|
+
rows_scanned: rowsScanned,
|
|
257
|
+
results,
|
|
258
|
+
};
|
|
259
|
+
if (truncated) {
|
|
260
|
+
result.truncated = true;
|
|
261
|
+
result.message = `Scanned ${DEFAULT_MAX_ROW_CAP} rows. Set force: true to scan all rows.`;
|
|
262
|
+
}
|
|
263
|
+
return JSON.stringify(result, null, 2);
|
|
187
264
|
}
|
|
188
265
|
catch (error) {
|
|
189
266
|
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "search", error });
|
|
@@ -195,32 +272,55 @@ export const tableGetRange = tool({
|
|
|
195
272
|
args: {
|
|
196
273
|
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
197
274
|
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
198
|
-
range: z.string().describe("Cell range (e.g., 'A1:C10', 'A1:B20')")
|
|
275
|
+
range: z.string().describe("Cell range (e.g., 'A1:C10', 'A1:B20')"),
|
|
276
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
199
277
|
},
|
|
200
278
|
execute: async (args, context) => {
|
|
201
279
|
await context.ask({ permission: "tableGetRange", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
202
280
|
try {
|
|
203
281
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
282
|
+
checkFileSize(resolvedPath, args.force);
|
|
204
283
|
const workbook = XLSX.readFile(resolvedPath);
|
|
205
284
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
206
285
|
const range = XLSX.utils.decode_range(args.range);
|
|
286
|
+
const sheetName = args.sheet_name || workbook.SheetNames[0];
|
|
287
|
+
const sheetRef = worksheet['!ref'] ? XLSX.utils.decode_range(worksheet['!ref']) : null;
|
|
288
|
+
if (!sheetRef) {
|
|
289
|
+
return JSON.stringify({ sheet_name: sheetName, range: args.range, rows: 0, columns: 0, data: [], out_of_bounds: true }, null, 2);
|
|
290
|
+
}
|
|
291
|
+
const clamped = {
|
|
292
|
+
s: { r: Math.max(range.s.r, sheetRef.s.r), c: Math.max(range.s.c, sheetRef.s.c) },
|
|
293
|
+
e: { r: Math.min(range.e.r, sheetRef.e.r), c: Math.min(range.e.c, sheetRef.e.c) },
|
|
294
|
+
};
|
|
295
|
+
if (clamped.s.r > clamped.e.r || clamped.s.c > clamped.e.c) {
|
|
296
|
+
return JSON.stringify({ sheet_name: sheetName, range: args.range, rows: 0, columns: 0, data: [], out_of_bounds: true }, null, 2);
|
|
297
|
+
}
|
|
298
|
+
const totalCells = (clamped.e.r - clamped.s.r + 1) * (clamped.e.c - clamped.s.c + 1);
|
|
299
|
+
if (!args.force && totalCells > DEFAULT_MAX_RANGE_CELLS) {
|
|
300
|
+
throw new Error(`Range "${args.range}" exceeds ${DEFAULT_MAX_RANGE_CELLS} cell limit (${totalCells} cells). ` +
|
|
301
|
+
`Set force: true to override.`);
|
|
302
|
+
}
|
|
207
303
|
const data = [];
|
|
208
|
-
for (let R =
|
|
304
|
+
for (let R = clamped.s.r; R <= clamped.e.r; ++R) {
|
|
209
305
|
const row = [];
|
|
210
|
-
for (let C =
|
|
306
|
+
for (let C = clamped.s.c; C <= clamped.e.c; ++C) {
|
|
211
307
|
const address = XLSX.utils.encode_cell({ r: R, c: C });
|
|
212
308
|
const cell = worksheet[address];
|
|
213
309
|
row.push(processCellValue(cell));
|
|
214
310
|
}
|
|
215
311
|
data.push(row);
|
|
216
312
|
}
|
|
217
|
-
|
|
218
|
-
sheet_name:
|
|
313
|
+
const result = {
|
|
314
|
+
sheet_name: sheetName,
|
|
219
315
|
range: args.range,
|
|
220
316
|
rows: data.length,
|
|
221
317
|
columns: data[0]?.length || 0,
|
|
222
|
-
data
|
|
223
|
-
}
|
|
318
|
+
data,
|
|
319
|
+
};
|
|
320
|
+
if (clamped.s.r !== range.s.r || clamped.s.c !== range.s.c || clamped.e.r !== range.e.r || clamped.e.c !== range.e.c) {
|
|
321
|
+
result.clamped_to = XLSX.utils.encode_range(clamped);
|
|
322
|
+
}
|
|
323
|
+
return JSON.stringify(result, null, 2);
|
|
224
324
|
}
|
|
225
325
|
catch (error) {
|
|
226
326
|
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "get_range", error });
|
|
@@ -232,34 +332,44 @@ export const tableSummarize = tool({
|
|
|
232
332
|
args: {
|
|
233
333
|
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
234
334
|
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
235
|
-
columns: z.array(z.string()).optional().describe("Specific columns to summarize (empty = all numeric columns)")
|
|
335
|
+
columns: z.array(z.string()).optional().describe("Specific columns to summarize (empty = all numeric columns)"),
|
|
336
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
236
337
|
},
|
|
237
338
|
execute: async (args, context) => {
|
|
238
339
|
await context.ask({ permission: "tableSummarize", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
239
340
|
try {
|
|
240
341
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
342
|
+
checkFileSize(resolvedPath, args.force);
|
|
241
343
|
const workbook = XLSX.readFile(resolvedPath);
|
|
242
344
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
243
345
|
const data = XLSX.utils.sheet_to_json(worksheet);
|
|
244
|
-
const
|
|
346
|
+
const effectiveData = args.force ? data : data.slice(0, DEFAULT_MAX_ROW_CAP);
|
|
347
|
+
const columnsToAnalyze = args.columns && args.columns.length > 0 ? args.columns : Object.keys(effectiveData[0] || {});
|
|
245
348
|
const summaries = {};
|
|
246
349
|
columnsToAnalyze.forEach(col => {
|
|
247
|
-
const values =
|
|
350
|
+
const values = effectiveData.map(row => row[col]).filter((v) => typeof v === 'number' && !isNaN(v));
|
|
248
351
|
if (values.length > 0) {
|
|
249
352
|
const sum = values.reduce((a, b) => a + b, 0);
|
|
250
353
|
const avg = sum / values.length;
|
|
251
|
-
const min =
|
|
252
|
-
const max =
|
|
354
|
+
const min = iterativeMin(values);
|
|
355
|
+
const max = iterativeMax(values);
|
|
253
356
|
const variance = values.reduce((acc, val) => acc + Math.pow(val - avg, 2), 0) / values.length;
|
|
254
357
|
const stdDev = Math.sqrt(variance);
|
|
255
358
|
summaries[col] = { sum, avg, min, max, std_dev: stdDev };
|
|
256
359
|
}
|
|
257
360
|
});
|
|
258
|
-
|
|
361
|
+
const truncated = !args.force && data.length > DEFAULT_MAX_ROW_CAP;
|
|
362
|
+
const result = {
|
|
259
363
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
260
|
-
row_count:
|
|
261
|
-
summaries
|
|
262
|
-
}
|
|
364
|
+
row_count: effectiveData.length,
|
|
365
|
+
summaries,
|
|
366
|
+
};
|
|
367
|
+
if (truncated) {
|
|
368
|
+
result.total_rows = data.length;
|
|
369
|
+
result.truncated = true;
|
|
370
|
+
result.message = `Summarized ${DEFAULT_MAX_ROW_CAP} of ${data.length} rows. Set force: true to process all rows.`;
|
|
371
|
+
}
|
|
372
|
+
return JSON.stringify(result, null, 2);
|
|
263
373
|
}
|
|
264
374
|
catch (error) {
|
|
265
375
|
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "summarize", error });
|
|
@@ -273,17 +383,20 @@ export const tableGroupBy = tool({
|
|
|
273
383
|
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
274
384
|
group_column: z.string().describe("Column to group by"),
|
|
275
385
|
agg_column: z.string().describe("Column to aggregate"),
|
|
276
|
-
agg_type: z.enum(['sum', 'count', 'avg', 'min', 'max']).default('sum').describe("Aggregation type")
|
|
386
|
+
agg_type: z.enum(['sum', 'count', 'avg', 'min', 'max']).default('sum').describe("Aggregation type"),
|
|
387
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
277
388
|
},
|
|
278
389
|
execute: async (args, context) => {
|
|
279
390
|
await context.ask({ permission: "tableGroupBy", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
280
391
|
try {
|
|
281
392
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
393
|
+
checkFileSize(resolvedPath, args.force);
|
|
282
394
|
const workbook = XLSX.readFile(resolvedPath);
|
|
283
395
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
284
396
|
const data = XLSX.utils.sheet_to_json(worksheet);
|
|
397
|
+
const effectiveData = args.force ? data : data.slice(0, DEFAULT_MAX_ROW_CAP);
|
|
285
398
|
const grouped = {};
|
|
286
|
-
|
|
399
|
+
effectiveData.forEach(row => {
|
|
287
400
|
const groupKey = String(row[args.group_column] || '');
|
|
288
401
|
const aggValue = row[args.agg_column];
|
|
289
402
|
if (!grouped[groupKey])
|
|
@@ -298,6 +411,10 @@ export const tableGroupBy = tool({
|
|
|
298
411
|
const result = {};
|
|
299
412
|
Object.keys(grouped).forEach(groupKey => {
|
|
300
413
|
const values = grouped[groupKey];
|
|
414
|
+
if (values.length === 0) {
|
|
415
|
+
result[groupKey] = 0;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
301
418
|
switch (args.agg_type) {
|
|
302
419
|
case 'sum':
|
|
303
420
|
result[groupKey] = values.reduce((a, b) => a + b, 0);
|
|
@@ -308,21 +425,33 @@ export const tableGroupBy = tool({
|
|
|
308
425
|
case 'avg':
|
|
309
426
|
result[groupKey] = values.reduce((a, b) => a + b, 0) / values.length;
|
|
310
427
|
break;
|
|
311
|
-
case 'min':
|
|
312
|
-
|
|
428
|
+
case 'min': {
|
|
429
|
+
const m = iterativeMin(values);
|
|
430
|
+
result[groupKey] = m !== undefined ? m : 0;
|
|
313
431
|
break;
|
|
314
|
-
|
|
315
|
-
|
|
432
|
+
}
|
|
433
|
+
case 'max': {
|
|
434
|
+
const m = iterativeMax(values);
|
|
435
|
+
result[groupKey] = m !== undefined ? m : 0;
|
|
316
436
|
break;
|
|
437
|
+
}
|
|
317
438
|
}
|
|
318
439
|
});
|
|
319
|
-
|
|
440
|
+
const truncated = !args.force && data.length > DEFAULT_MAX_ROW_CAP;
|
|
441
|
+
const response = {
|
|
320
442
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
321
443
|
group_column: args.group_column,
|
|
322
444
|
agg_column: args.agg_column,
|
|
323
445
|
agg_type: args.agg_type,
|
|
324
|
-
groups: result
|
|
325
|
-
}
|
|
446
|
+
groups: result,
|
|
447
|
+
};
|
|
448
|
+
if (truncated) {
|
|
449
|
+
response.total_rows = data.length;
|
|
450
|
+
response.processed_rows = effectiveData.length;
|
|
451
|
+
response.truncated = true;
|
|
452
|
+
response.message = `Processed ${DEFAULT_MAX_ROW_CAP} of ${data.length} rows. Set force: true to process all rows.`;
|
|
453
|
+
}
|
|
454
|
+
return JSON.stringify(response, null, 2);
|
|
326
455
|
}
|
|
327
456
|
catch (error) {
|
|
328
457
|
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "group_by", error });
|
|
@@ -337,33 +466,46 @@ export const tablePivotSummary = tool({
|
|
|
337
466
|
row_field: z.string().describe("Field for row grouping"),
|
|
338
467
|
col_field: z.string().describe("Field for column grouping"),
|
|
339
468
|
value_field: z.string().describe("Field to aggregate"),
|
|
340
|
-
agg: z.enum(['sum', 'count', 'avg', 'min', 'max']).default('sum').describe("Aggregation type")
|
|
469
|
+
agg: z.enum(['sum', 'count', 'avg', 'min', 'max']).default('sum').describe("Aggregation type"),
|
|
470
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
341
471
|
},
|
|
342
472
|
execute: async (args, context) => {
|
|
343
473
|
await context.ask({ permission: "tablePivotSummary", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
344
474
|
try {
|
|
345
475
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
476
|
+
checkFileSize(resolvedPath, args.force);
|
|
346
477
|
const workbook = XLSX.readFile(resolvedPath);
|
|
347
478
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
348
479
|
const data = XLSX.utils.sheet_to_json(worksheet);
|
|
480
|
+
const effectiveData = args.force ? data : data.slice(0, DEFAULT_MAX_ROW_CAP);
|
|
349
481
|
const pivot = {};
|
|
482
|
+
const counts = {};
|
|
350
483
|
const allCols = new Set();
|
|
351
|
-
|
|
484
|
+
effectiveData.forEach(row => {
|
|
352
485
|
const rowKey = String(row[args.row_field] || '');
|
|
353
486
|
const colKey = String(row[args.col_field] || '');
|
|
354
487
|
allCols.add(colKey);
|
|
355
488
|
if (!pivot[rowKey])
|
|
356
489
|
pivot[rowKey] = {};
|
|
490
|
+
if (!counts[rowKey])
|
|
491
|
+
counts[rowKey] = {};
|
|
357
492
|
const value = row[args.value_field];
|
|
358
|
-
|
|
493
|
+
const isNumeric = typeof value === 'number' && !isNaN(value);
|
|
494
|
+
if (isNumeric) {
|
|
359
495
|
if (!pivot[rowKey][colKey]) {
|
|
360
496
|
if (args.agg === 'min') {
|
|
361
497
|
pivot[rowKey][colKey] = Infinity;
|
|
362
498
|
}
|
|
499
|
+
else if (args.agg === 'max') {
|
|
500
|
+
pivot[rowKey][colKey] = -Infinity;
|
|
501
|
+
}
|
|
363
502
|
else {
|
|
364
503
|
pivot[rowKey][colKey] = 0;
|
|
365
504
|
}
|
|
366
505
|
}
|
|
506
|
+
if (!counts[rowKey][colKey])
|
|
507
|
+
counts[rowKey][colKey] = 0;
|
|
508
|
+
counts[rowKey][colKey]++;
|
|
367
509
|
switch (args.agg) {
|
|
368
510
|
case 'sum':
|
|
369
511
|
case 'avg':
|
|
@@ -382,31 +524,32 @@ export const tablePivotSummary = tool({
|
|
|
382
524
|
}
|
|
383
525
|
});
|
|
384
526
|
if (args.agg === 'avg') {
|
|
385
|
-
const counts = {};
|
|
386
|
-
data.forEach(row => {
|
|
387
|
-
const rowKey = String(row[args.row_field] || '');
|
|
388
|
-
const colKey = String(row[args.col_field] || '');
|
|
389
|
-
if (!counts[rowKey])
|
|
390
|
-
counts[rowKey] = {};
|
|
391
|
-
if (!counts[rowKey][colKey])
|
|
392
|
-
counts[rowKey][colKey] = 0;
|
|
393
|
-
counts[rowKey][colKey]++;
|
|
394
|
-
});
|
|
395
527
|
Object.keys(pivot).forEach(rowKey => {
|
|
396
528
|
Object.keys(pivot[rowKey]).forEach(colKey => {
|
|
397
|
-
|
|
529
|
+
const count = counts[rowKey][colKey];
|
|
530
|
+
if (count > 0) {
|
|
531
|
+
pivot[rowKey][colKey] /= count;
|
|
532
|
+
}
|
|
398
533
|
});
|
|
399
534
|
});
|
|
400
535
|
}
|
|
401
|
-
|
|
536
|
+
const truncated = !args.force && data.length > DEFAULT_MAX_ROW_CAP;
|
|
537
|
+
const response = {
|
|
402
538
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
403
539
|
row_field: args.row_field,
|
|
404
540
|
col_field: args.col_field,
|
|
405
541
|
value_field: args.value_field,
|
|
406
542
|
agg_type: args.agg,
|
|
407
543
|
columns: Array.from(allCols).sort(),
|
|
408
|
-
pivot
|
|
409
|
-
}
|
|
544
|
+
pivot,
|
|
545
|
+
};
|
|
546
|
+
if (truncated) {
|
|
547
|
+
response.total_rows = data.length;
|
|
548
|
+
response.processed_rows = effectiveData.length;
|
|
549
|
+
response.truncated = true;
|
|
550
|
+
response.message = `Processed ${DEFAULT_MAX_ROW_CAP} of ${data.length} rows. Set force: true to process all rows.`;
|
|
551
|
+
}
|
|
552
|
+
return JSON.stringify(response, null, 2);
|
|
410
553
|
}
|
|
411
554
|
catch (error) {
|
|
412
555
|
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "pivot_summary", error });
|
|
@@ -418,35 +561,34 @@ export const tableAppendRows = tool({
|
|
|
418
561
|
args: {
|
|
419
562
|
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
420
563
|
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
421
|
-
rows: z.array(z.union([z.array(z.any()), z.record(z.string(), z.any())])).describe("Rows to append (array of arrays or array of objects)")
|
|
564
|
+
rows: z.array(z.union([z.array(z.any()), z.record(z.string(), z.any())])).describe("Rows to append (array of arrays or array of objects)"),
|
|
565
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
422
566
|
},
|
|
423
567
|
execute: async (args, context) => {
|
|
424
568
|
await context.ask({ permission: "tableAppendRows", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
425
569
|
try {
|
|
426
570
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
571
|
+
checkFileSize(resolvedPath, args.force);
|
|
427
572
|
const workbook = XLSX.readFile(resolvedPath);
|
|
428
573
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
429
574
|
if (args.rows.length > 0 && Array.isArray(args.rows[0])) {
|
|
430
575
|
XLSX.utils.sheet_add_aoa(worksheet, args.rows, { origin: -1 });
|
|
431
576
|
}
|
|
432
577
|
else {
|
|
433
|
-
|
|
434
|
-
const existingData = XLSX.utils.sheet_to_json(worksheet);
|
|
435
|
-
const hasExistingData = existingData.length > 0;
|
|
436
|
-
// Only add header if sheet is empty
|
|
578
|
+
const hasExistingData = !!worksheet['!ref'];
|
|
437
579
|
const header = hasExistingData ? undefined : Object.keys(args.rows[0] || {});
|
|
438
580
|
XLSX.utils.sheet_add_json(worksheet, args.rows, {
|
|
439
581
|
origin: -1,
|
|
440
|
-
header
|
|
582
|
+
header,
|
|
441
583
|
});
|
|
442
584
|
}
|
|
443
|
-
|
|
585
|
+
writeAtomic(workbook, resolvedPath);
|
|
444
586
|
return JSON.stringify({
|
|
445
587
|
success: true,
|
|
446
588
|
file_path: args.file_path,
|
|
447
589
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
448
590
|
rows_appended: args.rows.length,
|
|
449
|
-
message: `Successfully appended ${args.rows.length} rows
|
|
591
|
+
message: `Successfully appended ${args.rows.length} rows`,
|
|
450
592
|
}, null, 2);
|
|
451
593
|
}
|
|
452
594
|
catch (error) {
|
|
@@ -455,49 +597,55 @@ export const tableAppendRows = tool({
|
|
|
455
597
|
}
|
|
456
598
|
});
|
|
457
599
|
export const tableUpdateCell = tool({
|
|
458
|
-
description: "Update a single cell value in a table file",
|
|
600
|
+
description: "Update a single cell value in a table file. Set value to null to clear a cell.",
|
|
459
601
|
args: {
|
|
460
602
|
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
461
603
|
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
462
604
|
cell_address: z.string().describe("Cell address (e.g., 'A1', 'B5')"),
|
|
463
|
-
value: z.
|
|
605
|
+
value: z.union([z.string(), z.number(), z.boolean(), z.null()]).describe("Value to set (null clears the cell)"),
|
|
606
|
+
force: z.boolean().default(false).describe(FORCE_DESC),
|
|
464
607
|
},
|
|
465
608
|
execute: async (args, context) => {
|
|
466
609
|
await context.ask({ permission: "tableUpdateCell", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
467
610
|
try {
|
|
611
|
+
if (!CELL_ADDRESS_REGEX.test(args.cell_address)) {
|
|
612
|
+
throw new Error(`Invalid cell address "${args.cell_address}". Expected format like "A1", "B5", "AA123".`);
|
|
613
|
+
}
|
|
468
614
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
615
|
+
checkFileSize(resolvedPath, args.force);
|
|
469
616
|
const workbook = XLSX.readFile(resolvedPath);
|
|
470
617
|
const worksheet = getSheet(workbook, args.sheet_name);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
else
|
|
475
|
-
cellType
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
618
|
+
if (args.value === null) {
|
|
619
|
+
delete worksheet[args.cell_address];
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
let cellType;
|
|
623
|
+
if (typeof args.value === 'number')
|
|
624
|
+
cellType = 'n';
|
|
625
|
+
else if (typeof args.value === 'boolean')
|
|
626
|
+
cellType = 'b';
|
|
627
|
+
else
|
|
628
|
+
cellType = 's';
|
|
629
|
+
worksheet[args.cell_address] = { v: args.value, t: cellType };
|
|
630
|
+
}
|
|
479
631
|
const newCellAddr = XLSX.utils.decode_cell(args.cell_address);
|
|
480
632
|
const currentRef = worksheet['!ref'];
|
|
481
633
|
if (currentRef) {
|
|
482
634
|
const currentRange = XLSX.utils.decode_range(currentRef);
|
|
483
|
-
// Only expand range if new cell is significantly beyond current data
|
|
484
|
-
// Avoid creating sparse sheets by checking if expansion is reasonable
|
|
485
635
|
const rowDifference = newCellAddr.r - currentRange.e.r;
|
|
486
636
|
const colDifference = newCellAddr.c - currentRange.e.c;
|
|
487
637
|
if (newCellAddr.r > currentRange.e.r || newCellAddr.c > currentRange.e.c) {
|
|
488
|
-
// Limit expansion to avoid creating sparse sheets
|
|
489
|
-
// Only expand if the new cell is reasonably close to existing data
|
|
490
638
|
const maxReasonableRow = Math.max(currentRange.e.r + 1000, newCellAddr.r);
|
|
491
639
|
const maxReasonableCol = Math.max(currentRange.e.c + 26, newCellAddr.c);
|
|
492
640
|
const updatedRange = {
|
|
493
641
|
s: {
|
|
494
642
|
r: Math.min(currentRange.s.r, newCellAddr.r),
|
|
495
|
-
c: Math.min(currentRange.s.c, newCellAddr.c)
|
|
643
|
+
c: Math.min(currentRange.s.c, newCellAddr.c),
|
|
496
644
|
},
|
|
497
645
|
e: {
|
|
498
646
|
r: Math.min(Math.max(currentRange.e.r, newCellAddr.r), maxReasonableRow),
|
|
499
|
-
c: Math.min(Math.max(currentRange.e.c, newCellAddr.c), maxReasonableCol)
|
|
500
|
-
}
|
|
647
|
+
c: Math.min(Math.max(currentRange.e.c, newCellAddr.c), maxReasonableCol),
|
|
648
|
+
},
|
|
501
649
|
};
|
|
502
650
|
worksheet['!ref'] = XLSX.utils.encode_range(updatedRange);
|
|
503
651
|
}
|
|
@@ -505,14 +653,14 @@ export const tableUpdateCell = tool({
|
|
|
505
653
|
else {
|
|
506
654
|
worksheet['!ref'] = XLSX.utils.encode_range({ s: newCellAddr, e: newCellAddr });
|
|
507
655
|
}
|
|
508
|
-
|
|
656
|
+
writeAtomic(workbook, resolvedPath);
|
|
509
657
|
return JSON.stringify({
|
|
510
658
|
success: true,
|
|
511
659
|
file_path: args.file_path,
|
|
512
660
|
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
513
661
|
cell_address: args.cell_address,
|
|
514
662
|
new_value: args.value,
|
|
515
|
-
message: `Successfully updated cell ${args.cell_address}
|
|
663
|
+
message: `Successfully updated cell ${args.cell_address}`,
|
|
516
664
|
}, null, 2);
|
|
517
665
|
}
|
|
518
666
|
catch (error) {
|
|
@@ -525,18 +673,22 @@ export const tableCreateFile = tool({
|
|
|
525
673
|
args: {
|
|
526
674
|
file_path: z.string().describe("Path for new table file (supports .xlsx, .ods, .csv formats)"),
|
|
527
675
|
sheet_name: z.string().default("Sheet1").describe("Worksheet name"),
|
|
528
|
-
data: z.any().describe("Data to write (array of arrays
|
|
676
|
+
data: z.union([z.array(z.record(z.string(), z.any())), z.array(z.array(z.any())), z.string()]).describe("Data to write (array of arrays, array of objects, or JSON string)"),
|
|
677
|
+
overwrite: z.boolean().default(false).describe("Overwrite existing file. Set to true if the file already exists."),
|
|
529
678
|
},
|
|
530
679
|
execute: async (args, context) => {
|
|
531
680
|
await context.ask({ permission: "tableCreateFile", patterns: [args.file_path], always: ["*"], metadata: {} });
|
|
532
681
|
try {
|
|
533
682
|
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
683
|
+
if (fs.existsSync(resolvedPath) && !args.overwrite) {
|
|
684
|
+
throw new Error(`File already exists. Set overwrite: true to replace it.`);
|
|
685
|
+
}
|
|
534
686
|
const workbook = XLSX.utils.book_new();
|
|
535
687
|
let worksheet;
|
|
536
688
|
let parsedData = args.data;
|
|
537
|
-
if (typeof
|
|
689
|
+
if (typeof parsedData === 'string') {
|
|
538
690
|
try {
|
|
539
|
-
parsedData = JSON.parse(
|
|
691
|
+
parsedData = JSON.parse(parsedData);
|
|
540
692
|
}
|
|
541
693
|
catch (e) {
|
|
542
694
|
throw new Error(`Failed to parse data string as JSON: ${e}`);
|
|
@@ -555,13 +707,13 @@ export const tableCreateFile = tool({
|
|
|
555
707
|
worksheet = XLSX.utils.json_to_sheet(parsedData);
|
|
556
708
|
}
|
|
557
709
|
XLSX.utils.book_append_sheet(workbook, worksheet, args.sheet_name);
|
|
558
|
-
|
|
710
|
+
writeAtomic(workbook, resolvedPath);
|
|
559
711
|
return JSON.stringify({
|
|
560
712
|
success: true,
|
|
561
713
|
file_path: args.file_path,
|
|
562
714
|
sheet_name: args.sheet_name,
|
|
563
715
|
rows_created: parsedData.length,
|
|
564
|
-
message: `Successfully created Excel file with ${parsedData.length} rows
|
|
716
|
+
message: `Successfully created Excel file with ${parsedData.length} rows`,
|
|
565
717
|
}, null, 2);
|
|
566
718
|
}
|
|
567
719
|
catch (error) {
|