opencode-bioresearcher-plugin 1.0.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/LICENSE +403 -0
- package/README.md +55 -0
- package/dist/agents/bioresearcher/index.d.ts +8 -0
- package/dist/agents/bioresearcher/index.js +18 -0
- package/dist/agents/bioresearcher/prompt.d.ts +8 -0
- package/dist/agents/bioresearcher/prompt.js +32 -0
- package/dist/agents/bioresearcher_worker/index.d.ts +8 -0
- package/dist/agents/bioresearcher_worker/index.js +18 -0
- package/dist/agents/bioresearcher_worker/prompt.d.ts +8 -0
- package/dist/agents/bioresearcher_worker/prompt.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/table-tools/index.d.ts +203 -0
- package/dist/table-tools/index.js +16 -0
- package/dist/table-tools/tools.d.ts +203 -0
- package/dist/table-tools/tools.js +558 -0
- package/dist/table-tools/utils.d.ts +11 -0
- package/dist/table-tools/utils.js +43 -0
- package/package.json +41 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import * as XLSX from 'xlsx';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { processCellValue, formatError, resolvePath, getSheet } from './utils';
|
|
5
|
+
export const tableGetSheetPreview = tool({
|
|
6
|
+
description: "Get first 6 rows of a worksheet to preview data structure",
|
|
7
|
+
args: {
|
|
8
|
+
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)")
|
|
10
|
+
},
|
|
11
|
+
execute: async (args, context) => {
|
|
12
|
+
try {
|
|
13
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
14
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
15
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
16
|
+
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
|
17
|
+
const previewRows = data.slice(0, 6);
|
|
18
|
+
return JSON.stringify({
|
|
19
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
20
|
+
total_rows: data.length,
|
|
21
|
+
preview: previewRows
|
|
22
|
+
}, null, 2);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "get_sheet_preview", error });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
export const tableListSheets = tool({
|
|
30
|
+
description: "List all worksheet names in a table file",
|
|
31
|
+
args: {
|
|
32
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)")
|
|
33
|
+
},
|
|
34
|
+
execute: async (args, context) => {
|
|
35
|
+
try {
|
|
36
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
37
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
38
|
+
return JSON.stringify({ sheets: workbook.SheetNames }, null, 2);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return formatError({ file_path: args.file_path, operation: "list_sheets", error });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
export const tableGetHeaders = tool({
|
|
46
|
+
description: "Get column headers (first row) from a worksheet",
|
|
47
|
+
args: {
|
|
48
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
49
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)")
|
|
50
|
+
},
|
|
51
|
+
execute: async (args, context) => {
|
|
52
|
+
try {
|
|
53
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
54
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
55
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
56
|
+
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
|
57
|
+
const headers = data[0] || [];
|
|
58
|
+
return JSON.stringify({
|
|
59
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
60
|
+
headers: headers,
|
|
61
|
+
column_count: headers.length
|
|
62
|
+
}, null, 2);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "get_headers", error });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
export const tableGetCell = tool({
|
|
70
|
+
description: "Get value of a single cell by address (e.g., 'A1', 'B5')",
|
|
71
|
+
args: {
|
|
72
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
73
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
74
|
+
cell_address: z.string().describe("Cell address (e.g., 'A1', 'B5')")
|
|
75
|
+
},
|
|
76
|
+
execute: async (args, context) => {
|
|
77
|
+
try {
|
|
78
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
79
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
80
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
81
|
+
const cell = worksheet[args.cell_address];
|
|
82
|
+
const value = processCellValue(cell);
|
|
83
|
+
return JSON.stringify({
|
|
84
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
85
|
+
cell_address: args.cell_address,
|
|
86
|
+
value: value,
|
|
87
|
+
type: cell?.t || 'unknown'
|
|
88
|
+
}, null, 2);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "get_cell", error });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
export const tableFilterRows = tool({
|
|
96
|
+
description: "Filter rows based on a condition (e.g., {column: 'Age', operator: '>', value: 25})",
|
|
97
|
+
args: {
|
|
98
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
99
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
100
|
+
column: z.string().describe("Column name to filter on"),
|
|
101
|
+
operator: z.enum(['=', '!=', '>', '<', '>=', '<=', 'contains']).describe("Comparison operator"),
|
|
102
|
+
value: z.any().describe("Value to compare against"),
|
|
103
|
+
max_results: z.number().default(100).describe("Maximum number of results to return")
|
|
104
|
+
},
|
|
105
|
+
execute: async (args, context) => {
|
|
106
|
+
try {
|
|
107
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
108
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
109
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
110
|
+
const data = XLSX.utils.sheet_to_json(worksheet);
|
|
111
|
+
const filteredRows = data.filter(row => {
|
|
112
|
+
let cellValue = row[args.column];
|
|
113
|
+
let comparisonValue = args.value;
|
|
114
|
+
// Type coercion for numeric comparisons
|
|
115
|
+
if (typeof cellValue === 'string' && cellValue.trim() !== '' && !isNaN(Number(cellValue))) {
|
|
116
|
+
cellValue = Number(cellValue);
|
|
117
|
+
}
|
|
118
|
+
if (typeof comparisonValue === 'string' && comparisonValue.trim() !== '' && !isNaN(Number(comparisonValue))) {
|
|
119
|
+
comparisonValue = Number(comparisonValue);
|
|
120
|
+
}
|
|
121
|
+
switch (args.operator) {
|
|
122
|
+
case '=': return cellValue === comparisonValue;
|
|
123
|
+
case '!=': return cellValue !== comparisonValue;
|
|
124
|
+
case '>': return cellValue > comparisonValue;
|
|
125
|
+
case '<': return cellValue < comparisonValue;
|
|
126
|
+
case '>=': return cellValue >= comparisonValue;
|
|
127
|
+
case '<=': return cellValue <= comparisonValue;
|
|
128
|
+
case 'contains': return String(cellValue).includes(String(comparisonValue));
|
|
129
|
+
default: return false;
|
|
130
|
+
}
|
|
131
|
+
}).slice(0, args.max_results);
|
|
132
|
+
return JSON.stringify({
|
|
133
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
134
|
+
filter: { column: args.column, operator: args.operator, value: args.value },
|
|
135
|
+
total_matches: filteredRows.length,
|
|
136
|
+
matched_rows: filteredRows
|
|
137
|
+
}, null, 2);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "filter_rows", error });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
export const tableSearch = tool({
|
|
145
|
+
description: "Search for a term across all cells in a worksheet",
|
|
146
|
+
args: {
|
|
147
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
148
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
149
|
+
search_term: z.string().describe("Term to search for"),
|
|
150
|
+
max_results: z.number().default(50).describe("Maximum number of results to return")
|
|
151
|
+
},
|
|
152
|
+
execute: async (args, context) => {
|
|
153
|
+
try {
|
|
154
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
155
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
156
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
157
|
+
const results = [];
|
|
158
|
+
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1');
|
|
159
|
+
for (let R = range.s.r; R <= range.e.r; ++R) {
|
|
160
|
+
for (let C = range.s.c; C <= range.e.c; ++C) {
|
|
161
|
+
if (results.length >= args.max_results)
|
|
162
|
+
break;
|
|
163
|
+
const address = XLSX.utils.encode_cell({ r: R, c: C });
|
|
164
|
+
const cell = worksheet[address];
|
|
165
|
+
if (cell && cell.v !== undefined) {
|
|
166
|
+
const cellValue = String(processCellValue(cell)).toLowerCase();
|
|
167
|
+
if (cellValue.includes(args.search_term.toLowerCase())) {
|
|
168
|
+
results.push({ address, value: processCellValue(cell), type: cell.t || 'unknown' });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (results.length >= args.max_results)
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
return JSON.stringify({
|
|
176
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
177
|
+
search_term: args.search_term,
|
|
178
|
+
total_found: results.length,
|
|
179
|
+
results
|
|
180
|
+
}, null, 2);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "search", error });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
export const tableGetRange = tool({
|
|
188
|
+
description: "Get data from a specific range (e.g., 'A1:C10')",
|
|
189
|
+
args: {
|
|
190
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
191
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
192
|
+
range: z.string().describe("Cell range (e.g., 'A1:C10', 'A1:B20')")
|
|
193
|
+
},
|
|
194
|
+
execute: async (args, context) => {
|
|
195
|
+
try {
|
|
196
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
197
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
198
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
199
|
+
const range = XLSX.utils.decode_range(args.range);
|
|
200
|
+
const data = [];
|
|
201
|
+
for (let R = range.s.r; R <= range.e.r; ++R) {
|
|
202
|
+
const row = [];
|
|
203
|
+
for (let C = range.s.c; C <= range.e.c; ++C) {
|
|
204
|
+
const address = XLSX.utils.encode_cell({ r: R, c: C });
|
|
205
|
+
const cell = worksheet[address];
|
|
206
|
+
row.push(processCellValue(cell));
|
|
207
|
+
}
|
|
208
|
+
data.push(row);
|
|
209
|
+
}
|
|
210
|
+
return JSON.stringify({
|
|
211
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
212
|
+
range: args.range,
|
|
213
|
+
rows: data.length,
|
|
214
|
+
columns: data[0]?.length || 0,
|
|
215
|
+
data
|
|
216
|
+
}, null, 2);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "get_range", error });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
export const tableSummarize = tool({
|
|
224
|
+
description: "Get statistical summary (sum, avg, min, max, std_dev) for numeric columns",
|
|
225
|
+
args: {
|
|
226
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
227
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
228
|
+
columns: z.array(z.string()).optional().describe("Specific columns to summarize (empty = all numeric columns)")
|
|
229
|
+
},
|
|
230
|
+
execute: async (args, context) => {
|
|
231
|
+
try {
|
|
232
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
233
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
234
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
235
|
+
const data = XLSX.utils.sheet_to_json(worksheet);
|
|
236
|
+
const columnsToAnalyze = args.columns && args.columns.length > 0 ? args.columns : Object.keys(data[0] || {});
|
|
237
|
+
const summaries = {};
|
|
238
|
+
columnsToAnalyze.forEach(col => {
|
|
239
|
+
const values = data.map(row => row[col]).filter(v => typeof v === 'number' && !isNaN(v));
|
|
240
|
+
if (values.length > 0) {
|
|
241
|
+
const sum = values.reduce((a, b) => a + b, 0);
|
|
242
|
+
const avg = sum / values.length;
|
|
243
|
+
const min = Math.min(...values);
|
|
244
|
+
const max = Math.max(...values);
|
|
245
|
+
const variance = values.reduce((acc, val) => acc + Math.pow(val - avg, 2), 0) / values.length;
|
|
246
|
+
const stdDev = Math.sqrt(variance);
|
|
247
|
+
summaries[col] = { sum, avg, min, max, std_dev: stdDev };
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return JSON.stringify({
|
|
251
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
252
|
+
row_count: data.length,
|
|
253
|
+
summaries
|
|
254
|
+
}, null, 2);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "summarize", error });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
export const tableGroupBy = tool({
|
|
262
|
+
description: "Group data by a column and calculate aggregation",
|
|
263
|
+
args: {
|
|
264
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
265
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
266
|
+
group_column: z.string().describe("Column to group by"),
|
|
267
|
+
agg_column: z.string().describe("Column to aggregate"),
|
|
268
|
+
agg_type: z.enum(['sum', 'count', 'avg', 'min', 'max']).default('sum').describe("Aggregation type")
|
|
269
|
+
},
|
|
270
|
+
execute: async (args, context) => {
|
|
271
|
+
try {
|
|
272
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
273
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
274
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
275
|
+
const data = XLSX.utils.sheet_to_json(worksheet);
|
|
276
|
+
const grouped = {};
|
|
277
|
+
data.forEach(row => {
|
|
278
|
+
const groupKey = String(row[args.group_column] || '');
|
|
279
|
+
const aggValue = row[args.agg_column];
|
|
280
|
+
if (!grouped[groupKey])
|
|
281
|
+
grouped[groupKey] = [];
|
|
282
|
+
if (args.agg_type === 'count') {
|
|
283
|
+
grouped[groupKey].push(1);
|
|
284
|
+
}
|
|
285
|
+
else if (typeof aggValue === 'number' && !isNaN(aggValue)) {
|
|
286
|
+
grouped[groupKey].push(aggValue);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
const result = {};
|
|
290
|
+
Object.keys(grouped).forEach(groupKey => {
|
|
291
|
+
const values = grouped[groupKey];
|
|
292
|
+
switch (args.agg_type) {
|
|
293
|
+
case 'sum':
|
|
294
|
+
result[groupKey] = values.reduce((a, b) => a + b, 0);
|
|
295
|
+
break;
|
|
296
|
+
case 'count':
|
|
297
|
+
result[groupKey] = values.length;
|
|
298
|
+
break;
|
|
299
|
+
case 'avg':
|
|
300
|
+
result[groupKey] = values.reduce((a, b) => a + b, 0) / values.length;
|
|
301
|
+
break;
|
|
302
|
+
case 'min':
|
|
303
|
+
result[groupKey] = Math.min(...values);
|
|
304
|
+
break;
|
|
305
|
+
case 'max':
|
|
306
|
+
result[groupKey] = Math.max(...values);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
return JSON.stringify({
|
|
311
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
312
|
+
group_column: args.group_column,
|
|
313
|
+
agg_column: args.agg_column,
|
|
314
|
+
agg_type: args.agg_type,
|
|
315
|
+
groups: result
|
|
316
|
+
}, null, 2);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "group_by", error });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
export const tablePivotSummary = tool({
|
|
324
|
+
description: "Create pivot table summary (row groups vs column groups)",
|
|
325
|
+
args: {
|
|
326
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
327
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
328
|
+
row_field: z.string().describe("Field for row grouping"),
|
|
329
|
+
col_field: z.string().describe("Field for column grouping"),
|
|
330
|
+
value_field: z.string().describe("Field to aggregate"),
|
|
331
|
+
agg: z.enum(['sum', 'count', 'avg', 'min', 'max']).default('sum').describe("Aggregation type")
|
|
332
|
+
},
|
|
333
|
+
execute: async (args, context) => {
|
|
334
|
+
try {
|
|
335
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
336
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
337
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
338
|
+
const data = XLSX.utils.sheet_to_json(worksheet);
|
|
339
|
+
const pivot = {};
|
|
340
|
+
const allCols = new Set();
|
|
341
|
+
data.forEach(row => {
|
|
342
|
+
const rowKey = String(row[args.row_field] || '');
|
|
343
|
+
const colKey = String(row[args.col_field] || '');
|
|
344
|
+
allCols.add(colKey);
|
|
345
|
+
if (!pivot[rowKey])
|
|
346
|
+
pivot[rowKey] = {};
|
|
347
|
+
const value = row[args.value_field];
|
|
348
|
+
if (typeof value === 'number' && !isNaN(value)) {
|
|
349
|
+
if (!pivot[rowKey][colKey]) {
|
|
350
|
+
if (args.agg === 'min') {
|
|
351
|
+
pivot[rowKey][colKey] = Infinity;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
pivot[rowKey][colKey] = 0;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
switch (args.agg) {
|
|
358
|
+
case 'sum':
|
|
359
|
+
case 'avg':
|
|
360
|
+
pivot[rowKey][colKey] += value;
|
|
361
|
+
break;
|
|
362
|
+
case 'min':
|
|
363
|
+
pivot[rowKey][colKey] = Math.min(pivot[rowKey][colKey], value);
|
|
364
|
+
break;
|
|
365
|
+
case 'max':
|
|
366
|
+
pivot[rowKey][colKey] = Math.max(pivot[rowKey][colKey], value);
|
|
367
|
+
break;
|
|
368
|
+
case 'count':
|
|
369
|
+
pivot[rowKey][colKey]++;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
if (args.agg === 'avg') {
|
|
375
|
+
const counts = {};
|
|
376
|
+
data.forEach(row => {
|
|
377
|
+
const rowKey = String(row[args.row_field] || '');
|
|
378
|
+
const colKey = String(row[args.col_field] || '');
|
|
379
|
+
if (!counts[rowKey])
|
|
380
|
+
counts[rowKey] = {};
|
|
381
|
+
if (!counts[rowKey][colKey])
|
|
382
|
+
counts[rowKey][colKey] = 0;
|
|
383
|
+
counts[rowKey][colKey]++;
|
|
384
|
+
});
|
|
385
|
+
Object.keys(pivot).forEach(rowKey => {
|
|
386
|
+
Object.keys(pivot[rowKey]).forEach(colKey => {
|
|
387
|
+
pivot[rowKey][colKey] /= counts[rowKey][colKey];
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return JSON.stringify({
|
|
392
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
393
|
+
row_field: args.row_field,
|
|
394
|
+
col_field: args.col_field,
|
|
395
|
+
value_field: args.value_field,
|
|
396
|
+
agg_type: args.agg,
|
|
397
|
+
columns: Array.from(allCols).sort(),
|
|
398
|
+
pivot
|
|
399
|
+
}, null, 2);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "pivot_summary", error });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
export const tableAppendRows = tool({
|
|
407
|
+
description: "Append rows to an existing table file",
|
|
408
|
+
args: {
|
|
409
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
410
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
411
|
+
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)")
|
|
412
|
+
},
|
|
413
|
+
execute: async (args, context) => {
|
|
414
|
+
try {
|
|
415
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
416
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
417
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
418
|
+
if (args.rows.length > 0 && Array.isArray(args.rows[0])) {
|
|
419
|
+
XLSX.utils.sheet_add_aoa(worksheet, args.rows, { origin: -1 });
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
// Check if sheet already has data (to avoid duplicate headers for object format)
|
|
423
|
+
const existingData = XLSX.utils.sheet_to_json(worksheet);
|
|
424
|
+
const hasExistingData = existingData.length > 0;
|
|
425
|
+
// Only add header if sheet is empty
|
|
426
|
+
const header = hasExistingData ? undefined : Object.keys(args.rows[0] || {});
|
|
427
|
+
XLSX.utils.sheet_add_json(worksheet, args.rows, {
|
|
428
|
+
origin: -1,
|
|
429
|
+
header
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
XLSX.writeFile(workbook, resolvedPath);
|
|
433
|
+
return JSON.stringify({
|
|
434
|
+
success: true,
|
|
435
|
+
file_path: args.file_path,
|
|
436
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
437
|
+
rows_appended: args.rows.length,
|
|
438
|
+
message: `Successfully appended ${args.rows.length} rows`
|
|
439
|
+
}, null, 2);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "append_rows", error });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
export const tableUpdateCell = tool({
|
|
447
|
+
description: "Update a single cell value in a table file",
|
|
448
|
+
args: {
|
|
449
|
+
file_path: z.string().describe("Path to table file (supports .xlsx, .ods, .csv formats)"),
|
|
450
|
+
sheet_name: z.string().optional().describe("Worksheet name (optional, uses first sheet by default)"),
|
|
451
|
+
cell_address: z.string().describe("Cell address (e.g., 'A1', 'B5')"),
|
|
452
|
+
value: z.any().describe("Value to set")
|
|
453
|
+
},
|
|
454
|
+
execute: async (args, context) => {
|
|
455
|
+
try {
|
|
456
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
457
|
+
const workbook = XLSX.readFile(resolvedPath);
|
|
458
|
+
const worksheet = getSheet(workbook, args.sheet_name);
|
|
459
|
+
let cellType;
|
|
460
|
+
if (typeof args.value === 'number')
|
|
461
|
+
cellType = 'n';
|
|
462
|
+
else if (typeof args.value === 'boolean')
|
|
463
|
+
cellType = 'b';
|
|
464
|
+
else
|
|
465
|
+
cellType = 's';
|
|
466
|
+
worksheet[args.cell_address] = { v: args.value, t: cellType };
|
|
467
|
+
const newCellAddr = XLSX.utils.decode_cell(args.cell_address);
|
|
468
|
+
const currentRef = worksheet['!ref'];
|
|
469
|
+
if (currentRef) {
|
|
470
|
+
const currentRange = XLSX.utils.decode_range(currentRef);
|
|
471
|
+
// Only expand range if new cell is significantly beyond current data
|
|
472
|
+
// Avoid creating sparse sheets by checking if expansion is reasonable
|
|
473
|
+
const rowDifference = newCellAddr.r - currentRange.e.r;
|
|
474
|
+
const colDifference = newCellAddr.c - currentRange.e.c;
|
|
475
|
+
if (newCellAddr.r > currentRange.e.r || newCellAddr.c > currentRange.e.c) {
|
|
476
|
+
// Limit expansion to avoid creating sparse sheets
|
|
477
|
+
// Only expand if the new cell is reasonably close to existing data
|
|
478
|
+
const maxReasonableRow = Math.max(currentRange.e.r + 1000, newCellAddr.r);
|
|
479
|
+
const maxReasonableCol = Math.max(currentRange.e.c + 26, newCellAddr.c);
|
|
480
|
+
const updatedRange = {
|
|
481
|
+
s: {
|
|
482
|
+
r: Math.min(currentRange.s.r, newCellAddr.r),
|
|
483
|
+
c: Math.min(currentRange.s.c, newCellAddr.c)
|
|
484
|
+
},
|
|
485
|
+
e: {
|
|
486
|
+
r: Math.min(Math.max(currentRange.e.r, newCellAddr.r), maxReasonableRow),
|
|
487
|
+
c: Math.min(Math.max(currentRange.e.c, newCellAddr.c), maxReasonableCol)
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
worksheet['!ref'] = XLSX.utils.encode_range(updatedRange);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
worksheet['!ref'] = XLSX.utils.encode_range({ s: newCellAddr, e: newCellAddr });
|
|
495
|
+
}
|
|
496
|
+
XLSX.writeFile(workbook, resolvedPath);
|
|
497
|
+
return JSON.stringify({
|
|
498
|
+
success: true,
|
|
499
|
+
file_path: args.file_path,
|
|
500
|
+
sheet_name: args.sheet_name || workbook.SheetNames[0],
|
|
501
|
+
cell_address: args.cell_address,
|
|
502
|
+
new_value: args.value,
|
|
503
|
+
message: `Successfully updated cell ${args.cell_address}`
|
|
504
|
+
}, null, 2);
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "update_cell", error });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
export const tableCreateFile = tool({
|
|
512
|
+
description: "Create a new table file from data",
|
|
513
|
+
args: {
|
|
514
|
+
file_path: z.string().describe("Path for new table file (supports .xlsx, .ods, .csv formats)"),
|
|
515
|
+
sheet_name: z.string().default("Sheet1").describe("Worksheet name"),
|
|
516
|
+
data: z.any().describe("Data to write (array of arrays or array of objects)")
|
|
517
|
+
},
|
|
518
|
+
execute: async (args, context) => {
|
|
519
|
+
try {
|
|
520
|
+
const resolvedPath = resolvePath(args.file_path, context.directory);
|
|
521
|
+
const workbook = XLSX.utils.book_new();
|
|
522
|
+
let worksheet;
|
|
523
|
+
let parsedData = args.data;
|
|
524
|
+
if (typeof args.data === 'string') {
|
|
525
|
+
try {
|
|
526
|
+
parsedData = JSON.parse(args.data);
|
|
527
|
+
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
throw new Error(`Failed to parse data string as JSON: ${e}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (!Array.isArray(parsedData)) {
|
|
533
|
+
throw new Error(`Data must be an array, received: ${typeof parsedData}`);
|
|
534
|
+
}
|
|
535
|
+
if (parsedData.length === 0) {
|
|
536
|
+
worksheet = XLSX.utils.aoa_to_sheet([]);
|
|
537
|
+
}
|
|
538
|
+
else if (Array.isArray(parsedData[0])) {
|
|
539
|
+
worksheet = XLSX.utils.aoa_to_sheet(parsedData);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
worksheet = XLSX.utils.json_to_sheet(parsedData);
|
|
543
|
+
}
|
|
544
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, args.sheet_name);
|
|
545
|
+
XLSX.writeFile(workbook, resolvedPath);
|
|
546
|
+
return JSON.stringify({
|
|
547
|
+
success: true,
|
|
548
|
+
file_path: args.file_path,
|
|
549
|
+
sheet_name: args.sheet_name,
|
|
550
|
+
rows_created: parsedData.length,
|
|
551
|
+
message: `Successfully created Excel file with ${parsedData.length} rows`
|
|
552
|
+
}, null, 2);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
return formatError({ file_path: args.file_path, sheet_name: args.sheet_name, operation: "create_file", error });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as XLSX from 'xlsx';
|
|
2
|
+
declare function processCellValue(cell: XLSX.CellObject | undefined): any;
|
|
3
|
+
declare function formatError(context: {
|
|
4
|
+
file_path: string;
|
|
5
|
+
sheet_name?: string;
|
|
6
|
+
operation: string;
|
|
7
|
+
error: any;
|
|
8
|
+
}): string;
|
|
9
|
+
declare function resolvePath(filePath: string, workingDir: string): string;
|
|
10
|
+
declare function getSheet(workbook: XLSX.WorkBook, sheetName?: string): XLSX.WorkSheet;
|
|
11
|
+
export { processCellValue, formatError, resolvePath, getSheet };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
function convertExcelDate(excelDate) {
|
|
3
|
+
const daysSinceEpoch = excelDate - 25569;
|
|
4
|
+
const timestamp = daysSinceEpoch * 86400 * 1000;
|
|
5
|
+
const date = new Date(timestamp);
|
|
6
|
+
return date.toISOString().split('T')[0];
|
|
7
|
+
}
|
|
8
|
+
function processCellValue(cell) {
|
|
9
|
+
if (!cell)
|
|
10
|
+
return "";
|
|
11
|
+
switch (cell.t) {
|
|
12
|
+
case 'd': return convertExcelDate(cell.v);
|
|
13
|
+
case 'n': return cell.v;
|
|
14
|
+
case 'b': return cell.v;
|
|
15
|
+
case 's': return cell.v;
|
|
16
|
+
case 'e': return cell.w || cell.v;
|
|
17
|
+
case 'z': return "";
|
|
18
|
+
default: return cell.w || cell.v || "";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function formatError(context) {
|
|
22
|
+
return `Error: ${context.error.message || 'Unknown error'}
|
|
23
|
+
Details:
|
|
24
|
+
- File: ${context.file_path}
|
|
25
|
+
${context.sheet_name ? `- Sheet: ${context.sheet_name}` : ''}
|
|
26
|
+
- Operation: ${context.operation}`;
|
|
27
|
+
}
|
|
28
|
+
function resolvePath(filePath, workingDir) {
|
|
29
|
+
if (path.isAbsolute(filePath)) {
|
|
30
|
+
return filePath;
|
|
31
|
+
}
|
|
32
|
+
return path.resolve(workingDir, filePath);
|
|
33
|
+
}
|
|
34
|
+
function getSheet(workbook, sheetName) {
|
|
35
|
+
if (!sheetName) {
|
|
36
|
+
return workbook.Sheets[workbook.SheetNames[0]];
|
|
37
|
+
}
|
|
38
|
+
if (!workbook.Sheets[sheetName]) {
|
|
39
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
|
40
|
+
}
|
|
41
|
+
return workbook.Sheets[sheetName];
|
|
42
|
+
}
|
|
43
|
+
export { processCellValue, formatError, resolvePath, getSheet };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-bioresearcher-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenCode plugin that adds a bioresearcher agent",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"opencode",
|
|
20
|
+
"plugin",
|
|
21
|
+
"bioresearch",
|
|
22
|
+
"biomcp",
|
|
23
|
+
"agent"
|
|
24
|
+
],
|
|
25
|
+
"author": "Ye Yuan",
|
|
26
|
+
"license": "CC-BY-NC-ND-4.0",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist/",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@opencode-ai/plugin": "^1.0.0",
|
|
34
|
+
"xlsx": "^0.18.5",
|
|
35
|
+
"zod": "^4.1.8"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.0.0",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
}
|
|
41
|
+
}
|