starmux 0.12.0 → 0.13.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/src/cli/mac-mcp-gateway.js +4 -3
- package/dist/src/cli/mac-mcp-gateway.js.map +1 -1
- package/dist/src/cli/starmux.js +1 -1
- package/dist/src/cli/starmux.js.map +1 -1
- package/dist/src/runtime-paths.js +3 -3
- package/dist/src/runtime-paths.js.map +1 -1
- package/dist/vendor/desktop-commander/LICENSE +21 -0
- package/dist/vendor/desktop-commander/dist/bootstrap.js +22 -0
- package/dist/vendor/desktop-commander/dist/command-manager.js +246 -0
- package/dist/vendor/desktop-commander/dist/config-field-definitions.js +37 -0
- package/dist/vendor/desktop-commander/dist/config-manager.js +634 -0
- package/dist/vendor/desktop-commander/dist/config-recovery.js +81 -0
- package/dist/vendor/desktop-commander/dist/config.js +19 -0
- package/dist/vendor/desktop-commander/dist/custom-stdio.js +358 -0
- package/dist/vendor/desktop-commander/dist/error-handlers.js +15 -0
- package/dist/vendor/desktop-commander/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/vendor/desktop-commander/dist/handlers/filesystem-handlers.js +450 -0
- package/dist/vendor/desktop-commander/dist/handlers/history-handlers.js +67 -0
- package/dist/vendor/desktop-commander/dist/handlers/index.js +7 -0
- package/dist/vendor/desktop-commander/dist/handlers/process-handlers.js +15 -0
- package/dist/vendor/desktop-commander/dist/handlers/search-answers.js +172 -0
- package/dist/vendor/desktop-commander/dist/handlers/search-handlers.js +136 -0
- package/dist/vendor/desktop-commander/dist/handlers/terminal-handlers.js +35 -0
- package/dist/vendor/desktop-commander/dist/index.js +88 -0
- package/dist/vendor/desktop-commander/dist/search-manager.js +1262 -0
- package/dist/vendor/desktop-commander/dist/server.js +1414 -0
- package/dist/vendor/desktop-commander/dist/terminal-manager.js +757 -0
- package/dist/vendor/desktop-commander/dist/tools/config.js +211 -0
- package/dist/vendor/desktop-commander/dist/tools/edit.js +455 -0
- package/dist/vendor/desktop-commander/dist/tools/feedback.js +10 -0
- package/dist/vendor/desktop-commander/dist/tools/filesystem.js +1047 -0
- package/dist/vendor/desktop-commander/dist/tools/fuzzySearch.js +74 -0
- package/dist/vendor/desktop-commander/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/vendor/desktop-commander/dist/tools/improved-process-tools.js +726 -0
- package/dist/vendor/desktop-commander/dist/tools/mime-types.js +28 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/extract-images.js +131 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/index.js +4 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/lib/pdf2md.js +78 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/manipulations.js +112 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/markdown.js +30 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/utils.js +45 -0
- package/dist/vendor/desktop-commander/dist/tools/process.js +90 -0
- package/dist/vendor/desktop-commander/dist/tools/prompts.js +259 -0
- package/dist/vendor/desktop-commander/dist/tools/schemas.js +242 -0
- package/dist/vendor/desktop-commander/dist/tools/usage.js +24 -0
- package/dist/vendor/desktop-commander/dist/types.js +1 -0
- package/dist/vendor/desktop-commander/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/vendor/desktop-commander/dist/ui/config-editor/index.html +13 -0
- package/dist/vendor/desktop-commander/dist/ui/config-editor/styles.css +587 -0
- package/dist/vendor/desktop-commander/dist/ui/contracts.js +57 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/index.html +16 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/preview-runtime.js +471 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/styles.css +1631 -0
- package/dist/vendor/desktop-commander/dist/ui/resources.js +93 -0
- package/dist/vendor/desktop-commander/dist/utils/ab-test.js +122 -0
- package/dist/vendor/desktop-commander/dist/utils/atomic-write.js +80 -0
- package/dist/vendor/desktop-commander/dist/utils/capture.js +40 -0
- package/dist/vendor/desktop-commander/dist/utils/dockerPrompt.js +9 -0
- package/dist/vendor/desktop-commander/dist/utils/exit-process.js +31 -0
- package/dist/vendor/desktop-commander/dist/utils/feature-flags.js +18 -0
- package/dist/vendor/desktop-commander/dist/utils/files/base.js +5 -0
- package/dist/vendor/desktop-commander/dist/utils/files/binary.js +65 -0
- package/dist/vendor/desktop-commander/dist/utils/files/docx.js +694 -0
- package/dist/vendor/desktop-commander/dist/utils/files/excel.js +497 -0
- package/dist/vendor/desktop-commander/dist/utils/files/factory.js +168 -0
- package/dist/vendor/desktop-commander/dist/utils/files/image.js +89 -0
- package/dist/vendor/desktop-commander/dist/utils/files/index.js +14 -0
- package/dist/vendor/desktop-commander/dist/utils/files/pdf.js +124 -0
- package/dist/vendor/desktop-commander/dist/utils/files/text.js +423 -0
- package/dist/vendor/desktop-commander/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/vendor/desktop-commander/dist/utils/internal-facts.js +23 -0
- package/dist/vendor/desktop-commander/dist/utils/lazy-package.js +44 -0
- package/dist/vendor/desktop-commander/dist/utils/lineEndingHandler.js +77 -0
- package/dist/vendor/desktop-commander/dist/utils/logger.js +72 -0
- package/dist/vendor/desktop-commander/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/vendor/desktop-commander/dist/utils/open-browser.js +2 -0
- package/dist/vendor/desktop-commander/dist/utils/process-detection.js +260 -0
- package/dist/vendor/desktop-commander/dist/utils/process-tree.js +223 -0
- package/dist/vendor/desktop-commander/dist/utils/rename.js +74 -0
- package/dist/vendor/desktop-commander/dist/utils/ripgrep-resolver.js +69 -0
- package/dist/vendor/desktop-commander/dist/utils/shell.js +187 -0
- package/dist/vendor/desktop-commander/dist/utils/system-info.js +727 -0
- package/dist/vendor/desktop-commander/dist/utils/terminal-timestamps.js +18 -0
- package/dist/vendor/desktop-commander/dist/utils/toolHistory.js +309 -0
- package/dist/vendor/desktop-commander/dist/utils/trackTools.js +55 -0
- package/dist/vendor/desktop-commander/dist/utils/unsupportedParams.js +66 -0
- package/dist/vendor/desktop-commander/dist/utils/usageTracker.js +225 -0
- package/dist/vendor/desktop-commander/dist/utils/welcome-onboarding.js +2 -0
- package/dist/vendor/desktop-commander/dist/utils/withTimeout.js +85 -0
- package/dist/vendor/desktop-commander/dist/version.js +1 -0
- package/dist/vendor/desktop-commander/package.json +15 -0
- package/package.json +14 -2
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excel file handler using ExcelJS
|
|
3
|
+
* Handles reading, writing, and editing Excel files (.xlsx, .xls, .xlsm)
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
import { LazyPackage } from '../lazy-package.js';
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
// require(), not import(): Node keeps a failed import() failed, so it couldn't load again
|
|
10
|
+
export const exceljsPackage = new LazyPackage('exceljs', 'Excel support', () => require('exceljs'));
|
|
11
|
+
async function newWorkbook() {
|
|
12
|
+
return new (exceljsPackage.load().Workbook)();
|
|
13
|
+
}
|
|
14
|
+
// File size limit: 10MB
|
|
15
|
+
const FILE_SIZE_LIMIT = 10 * 1024 * 1024;
|
|
16
|
+
/**
|
|
17
|
+
* Excel file handler implementation using ExcelJS
|
|
18
|
+
* Supports: .xlsx, .xls, .xlsm files
|
|
19
|
+
*/
|
|
20
|
+
export class ExcelFileHandler {
|
|
21
|
+
canHandle(path) {
|
|
22
|
+
const ext = path.toLowerCase();
|
|
23
|
+
return ext.endsWith('.xlsx') || ext.endsWith('.xls') || ext.endsWith('.xlsm');
|
|
24
|
+
}
|
|
25
|
+
async read(path, options) {
|
|
26
|
+
await this.checkFileSize(path);
|
|
27
|
+
const workbook = await newWorkbook();
|
|
28
|
+
await workbook.xlsx.readFile(path);
|
|
29
|
+
const metadata = await this.extractMetadata(workbook, path);
|
|
30
|
+
const { sheetName, data, totalRows, returnedRows, firstRow } = this.worksheetToArray(workbook, options?.sheet, options?.range, options?.offset, options?.length);
|
|
31
|
+
// Format output with sheet info header, usage hint, and JSON data.
|
|
32
|
+
// No rows while there are some means the offset starts past the end.
|
|
33
|
+
const paginationInfo = totalRows <= returnedRows
|
|
34
|
+
? ''
|
|
35
|
+
: returnedRows > 0
|
|
36
|
+
? `\n[Showing rows ${firstRow}-${firstRow + returnedRows - 1} of ${totalRows} total. Use offset/length to paginate.]`
|
|
37
|
+
: `\n[No rows returned: row ${firstRow} is past the end (${totalRows} rows total). Use offset/length to paginate.]`;
|
|
38
|
+
const sheetHasSpace = /\s/.test(sheetName);
|
|
39
|
+
const exampleSheet = sheetHasSpace ? sheetName : 'Sheet1';
|
|
40
|
+
const content = `[Sheet: '${sheetName}' from ${path}]${paginationInfo}
|
|
41
|
+
[To MODIFY cells: use edit_block with range param, e.g., edit_block(path, {range: "${exampleSheet}!E5", content: [[newValue]]}). read_file accepts the same range form, or pass sheet + range separately.]
|
|
42
|
+
|
|
43
|
+
${JSON.stringify(data)}`;
|
|
44
|
+
return {
|
|
45
|
+
content,
|
|
46
|
+
mimeType: 'application/json',
|
|
47
|
+
metadata: {
|
|
48
|
+
isExcelFile: true,
|
|
49
|
+
sheets: metadata.sheets,
|
|
50
|
+
fileSize: metadata.fileSize,
|
|
51
|
+
isLargeFile: metadata.isLargeFile
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async write(path, content, mode) {
|
|
56
|
+
// Check existing file size if it exists
|
|
57
|
+
try {
|
|
58
|
+
await this.checkFileSize(path);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// File doesn't exist - that's fine for write
|
|
62
|
+
if (error.code !== 'ENOENT' &&
|
|
63
|
+
!(error instanceof Error && error.message.includes('ENOENT'))) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Parse content
|
|
68
|
+
let parsedContent = content;
|
|
69
|
+
if (typeof content === 'string') {
|
|
70
|
+
try {
|
|
71
|
+
parsedContent = JSON.parse(content);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error('Invalid content format. Expected JSON string with 2D array or object with sheet names.');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Handle append mode by finding last row and writing after it
|
|
78
|
+
if (mode === 'append') {
|
|
79
|
+
try {
|
|
80
|
+
const workbook = await newWorkbook();
|
|
81
|
+
await workbook.xlsx.readFile(path);
|
|
82
|
+
if (Array.isArray(parsedContent)) {
|
|
83
|
+
// Append to Sheet1, or to the first sheet of a workbook without one
|
|
84
|
+
let worksheet = workbook.getWorksheet('Sheet1') ?? workbook.worksheets[0];
|
|
85
|
+
if (!worksheet) {
|
|
86
|
+
worksheet = workbook.addWorksheet('Sheet1');
|
|
87
|
+
}
|
|
88
|
+
const startRow = this.dataExtent(worksheet).lastRow + 1;
|
|
89
|
+
this.writeRowsStartingAt(worksheet, startRow, parsedContent);
|
|
90
|
+
}
|
|
91
|
+
else if (typeof parsedContent === 'object' && parsedContent !== null) {
|
|
92
|
+
// Append to each named sheet
|
|
93
|
+
for (const [sheetName, data] of Object.entries(parsedContent)) {
|
|
94
|
+
if (Array.isArray(data)) {
|
|
95
|
+
let worksheet = workbook.getWorksheet(sheetName);
|
|
96
|
+
if (!worksheet) {
|
|
97
|
+
worksheet = workbook.addWorksheet(sheetName);
|
|
98
|
+
}
|
|
99
|
+
const startRow = this.dataExtent(worksheet).lastRow + 1;
|
|
100
|
+
this.writeRowsStartingAt(worksheet, startRow, data);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
await workbook.xlsx.writeFile(path);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// File doesn't exist - fall through to create new file
|
|
109
|
+
if (error.code !== 'ENOENT' &&
|
|
110
|
+
!(error instanceof Error && error.message.includes('ENOENT'))) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Rewrite mode (or append to non-existent file): create new workbook
|
|
116
|
+
const workbook = await newWorkbook();
|
|
117
|
+
if (Array.isArray(parsedContent)) {
|
|
118
|
+
// Single sheet from 2D array
|
|
119
|
+
this.writeDataToSheet(workbook, 'Sheet1', parsedContent);
|
|
120
|
+
}
|
|
121
|
+
else if (typeof parsedContent === 'object' && parsedContent !== null) {
|
|
122
|
+
// Object with sheet names as keys
|
|
123
|
+
for (const [sheetName, data] of Object.entries(parsedContent)) {
|
|
124
|
+
if (Array.isArray(data)) {
|
|
125
|
+
this.writeDataToSheet(workbook, sheetName, data);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
throw new Error('Invalid content format. Expected 2D array or object with sheet names.');
|
|
131
|
+
}
|
|
132
|
+
await workbook.xlsx.writeFile(path);
|
|
133
|
+
}
|
|
134
|
+
async editRange(path, range, content, options) {
|
|
135
|
+
// Verify file exists and check size
|
|
136
|
+
try {
|
|
137
|
+
await this.checkFileSize(path);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error.code === 'ENOENT' ||
|
|
141
|
+
(error instanceof Error && error.message.includes('ENOENT'))) {
|
|
142
|
+
throw new Error(`File not found: ${path}`);
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
// Validate content
|
|
147
|
+
if (!Array.isArray(content)) {
|
|
148
|
+
throw new Error('Content must be a 2D array for range editing');
|
|
149
|
+
}
|
|
150
|
+
// Parse range: "Sheet1!A1:C10" or "Sheet1"
|
|
151
|
+
const [sheetName, cellRange] = this.parseRange(range);
|
|
152
|
+
const workbook = await newWorkbook();
|
|
153
|
+
await workbook.xlsx.readFile(path);
|
|
154
|
+
// Get or create sheet
|
|
155
|
+
let worksheet = workbook.getWorksheet(sheetName);
|
|
156
|
+
if (!worksheet) {
|
|
157
|
+
worksheet = workbook.addWorksheet(sheetName);
|
|
158
|
+
}
|
|
159
|
+
if (cellRange) {
|
|
160
|
+
// Write to specific range
|
|
161
|
+
const { startRow, startCol, endRow, endCol } = this.parseCellRange(cellRange);
|
|
162
|
+
// A FROM:TO range bounds the write: content that doesn't fit is refused
|
|
163
|
+
// before any cell changes, so no cell outside the range is overwritten
|
|
164
|
+
if (endRow !== undefined && endCol !== undefined) {
|
|
165
|
+
const rows = content.length;
|
|
166
|
+
const columns = Math.max(0, ...content.map((row) => Array.isArray(row) ? row.length : 0));
|
|
167
|
+
const rangeRows = endRow - startRow + 1;
|
|
168
|
+
const rangeColumns = endCol - startCol + 1;
|
|
169
|
+
if (rows > rangeRows || columns > rangeColumns) {
|
|
170
|
+
throw new Error(`Content has ${rows} row(s) and ${columns} column(s) but range ${cellRange} holds ` +
|
|
171
|
+
`${rangeRows} row(s) and ${rangeColumns} column(s); nothing was written`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (let r = 0; r < content.length; r++) {
|
|
175
|
+
const rowData = content[r];
|
|
176
|
+
if (!Array.isArray(rowData))
|
|
177
|
+
continue;
|
|
178
|
+
for (let c = 0; c < rowData.length; c++) {
|
|
179
|
+
const cell = worksheet.getCell(startRow + r, startCol + c);
|
|
180
|
+
const value = rowData[c];
|
|
181
|
+
if (typeof value === 'string' && value.startsWith('=')) {
|
|
182
|
+
cell.value = { formula: value.substring(1) };
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
cell.value = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Replace entire sheet content
|
|
192
|
+
// Clear existing data
|
|
193
|
+
worksheet.eachRow((row, rowNumber) => {
|
|
194
|
+
row.eachCell((cell) => {
|
|
195
|
+
cell.value = null;
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
// Write new data
|
|
199
|
+
for (let r = 0; r < content.length; r++) {
|
|
200
|
+
const rowData = content[r];
|
|
201
|
+
if (!Array.isArray(rowData))
|
|
202
|
+
continue;
|
|
203
|
+
const row = worksheet.getRow(r + 1);
|
|
204
|
+
for (let c = 0; c < rowData.length; c++) {
|
|
205
|
+
const value = rowData[c];
|
|
206
|
+
if (typeof value === 'string' && value.startsWith('=')) {
|
|
207
|
+
row.getCell(c + 1).value = { formula: value.substring(1) };
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
row.getCell(c + 1).value = value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
row.commit();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
await workbook.xlsx.writeFile(path);
|
|
217
|
+
return { success: true, editsApplied: 1 };
|
|
218
|
+
}
|
|
219
|
+
async getInfo(path) {
|
|
220
|
+
const stats = await fs.stat(path);
|
|
221
|
+
try {
|
|
222
|
+
const workbook = await newWorkbook();
|
|
223
|
+
await workbook.xlsx.readFile(path);
|
|
224
|
+
const metadata = await this.extractMetadata(workbook, path);
|
|
225
|
+
return {
|
|
226
|
+
size: stats.size,
|
|
227
|
+
created: stats.birthtime,
|
|
228
|
+
modified: stats.mtime,
|
|
229
|
+
accessed: stats.atime,
|
|
230
|
+
isDirectory: stats.isDirectory(),
|
|
231
|
+
isFile: stats.isFile(),
|
|
232
|
+
permissions: stats.mode.toString(8).slice(-3),
|
|
233
|
+
fileType: 'excel',
|
|
234
|
+
metadata: {
|
|
235
|
+
isExcelFile: true,
|
|
236
|
+
sheets: metadata.sheets,
|
|
237
|
+
fileSize: metadata.fileSize,
|
|
238
|
+
isLargeFile: metadata.isLargeFile
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
return {
|
|
244
|
+
size: stats.size,
|
|
245
|
+
created: stats.birthtime,
|
|
246
|
+
modified: stats.mtime,
|
|
247
|
+
accessed: stats.atime,
|
|
248
|
+
isDirectory: stats.isDirectory(),
|
|
249
|
+
isFile: stats.isFile(),
|
|
250
|
+
permissions: stats.mode.toString(8).slice(-3),
|
|
251
|
+
fileType: 'excel',
|
|
252
|
+
metadata: {
|
|
253
|
+
isExcelFile: true,
|
|
254
|
+
fileSize: stats.size,
|
|
255
|
+
error: true,
|
|
256
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ========== Private Helpers ==========
|
|
262
|
+
async checkFileSize(path) {
|
|
263
|
+
const stats = await fs.stat(path);
|
|
264
|
+
if (stats.size > FILE_SIZE_LIMIT) {
|
|
265
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
|
|
266
|
+
throw new Error(`Excel file size (${sizeMB}MB) exceeds 10MB limit. ` +
|
|
267
|
+
`Consider using specialized tools for large file processing.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async extractMetadata(workbook, path) {
|
|
271
|
+
const stats = await fs.stat(path);
|
|
272
|
+
const sheets = workbook.worksheets.map(ws => {
|
|
273
|
+
const { lastRow, lastCol } = this.dataExtent(ws);
|
|
274
|
+
return { name: ws.name, rowCount: lastRow, colCount: lastCol };
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
sheets,
|
|
278
|
+
fileSize: stats.size,
|
|
279
|
+
isLargeFile: stats.size > FILE_SIZE_LIMIT
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The last row and the last column holding a value (0 for an empty sheet):
|
|
284
|
+
* where the sheet's data ends, for reads, sheet info and appends alike.
|
|
285
|
+
* Not actualRowCount/actualColumnCount: those COUNT the used rows and
|
|
286
|
+
* columns, so data in columns A and AH only would end at column B.
|
|
287
|
+
*/
|
|
288
|
+
dataExtent(worksheet) {
|
|
289
|
+
let lastRow = 0;
|
|
290
|
+
let lastCol = 0;
|
|
291
|
+
worksheet.eachRow((row, rowNumber) => {
|
|
292
|
+
lastRow = rowNumber;
|
|
293
|
+
row.eachCell((_cell, colNumber) => {
|
|
294
|
+
lastCol = Math.max(lastCol, colNumber);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
return { lastRow, lastCol };
|
|
298
|
+
}
|
|
299
|
+
worksheetToArray(workbook, sheetRef, range, offset, length) {
|
|
300
|
+
if (workbook.worksheets.length === 0) {
|
|
301
|
+
return { sheetName: '', data: [], totalRows: 0, returnedRows: 0, firstRow: 1 };
|
|
302
|
+
}
|
|
303
|
+
// `sheet` is a name or a 0-based index as a string ("0"). A number that is
|
|
304
|
+
// no sheet's index but a sheet's name ("2024") means that sheet.
|
|
305
|
+
if (typeof sheetRef === 'string' && /^\d+$/.test(sheetRef)
|
|
306
|
+
&& (Number(sheetRef) < workbook.worksheets.length || !workbook.getWorksheet(sheetRef))) {
|
|
307
|
+
sheetRef = Number(sheetRef);
|
|
308
|
+
}
|
|
309
|
+
// Accept range with embedded sheet prefix (parity with edit_block).
|
|
310
|
+
// E.g. range:"Sheet1!A1:B2" or "'My Sheet'!A1" — strip the sheet
|
|
311
|
+
// prefix and, when the caller did not pass an explicit sheet, use it.
|
|
312
|
+
let cellRangeOnly = range;
|
|
313
|
+
if (range && range.includes('!')) {
|
|
314
|
+
const [sheetFromRange, cellsFromRange] = this.parseRange(range);
|
|
315
|
+
cellRangeOnly = cellsFromRange ?? undefined;
|
|
316
|
+
if (sheetRef === undefined && sheetFromRange) {
|
|
317
|
+
sheetRef = sheetFromRange;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Find target worksheet
|
|
321
|
+
let worksheet;
|
|
322
|
+
let sheetName;
|
|
323
|
+
if (sheetRef === undefined) {
|
|
324
|
+
worksheet = workbook.worksheets[0];
|
|
325
|
+
sheetName = worksheet.name;
|
|
326
|
+
}
|
|
327
|
+
else if (typeof sheetRef === 'number') {
|
|
328
|
+
if (sheetRef < 0 || sheetRef >= workbook.worksheets.length) {
|
|
329
|
+
throw new Error(`Sheet index ${sheetRef} out of range (0-${workbook.worksheets.length - 1})`);
|
|
330
|
+
}
|
|
331
|
+
worksheet = workbook.worksheets[sheetRef];
|
|
332
|
+
sheetName = worksheet.name;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
worksheet = workbook.getWorksheet(sheetRef);
|
|
336
|
+
if (!worksheet) {
|
|
337
|
+
const available = workbook.worksheets.map(ws => ws.name).join(', ');
|
|
338
|
+
throw new Error(`Sheet "${sheetRef}" not found. Available sheets: ${available}`);
|
|
339
|
+
}
|
|
340
|
+
sheetName = sheetRef;
|
|
341
|
+
}
|
|
342
|
+
// Determine range to read: from A1 to the last cell of the data
|
|
343
|
+
const { lastRow, lastCol } = this.dataExtent(worksheet);
|
|
344
|
+
let startRow = 1;
|
|
345
|
+
let endRow = lastRow || 1;
|
|
346
|
+
let startCol = 1;
|
|
347
|
+
let endCol = lastCol || 1;
|
|
348
|
+
if (cellRangeOnly) {
|
|
349
|
+
const parsed = this.parseCellRange(cellRangeOnly);
|
|
350
|
+
startRow = parsed.startRow;
|
|
351
|
+
startCol = parsed.startCol;
|
|
352
|
+
if (parsed.endRow)
|
|
353
|
+
endRow = parsed.endRow;
|
|
354
|
+
if (parsed.endCol)
|
|
355
|
+
endCol = parsed.endCol;
|
|
356
|
+
}
|
|
357
|
+
// Calculate total rows before pagination. A single cell reads to the end
|
|
358
|
+
// of the data, so one below the data (A10 on a 5-row sheet) has no rows,
|
|
359
|
+
// and one right of it (Z1 on a sheet ending at column C) has no cells.
|
|
360
|
+
const totalRows = startCol > endCol ? 0 : Math.max(0, endRow - startRow + 1);
|
|
361
|
+
const rangeStartRow = startRow;
|
|
362
|
+
// Apply offset/length pagination (row-based, matching text file behavior)
|
|
363
|
+
if (offset !== undefined) {
|
|
364
|
+
if (offset < 0) {
|
|
365
|
+
// Negative offset: last N rows (like text files)
|
|
366
|
+
// offset: -10 means "last 10 rows"
|
|
367
|
+
const lastNRows = Math.abs(offset);
|
|
368
|
+
startRow = Math.max(startRow, endRow - lastNRows + 1);
|
|
369
|
+
}
|
|
370
|
+
else if (offset > 0) {
|
|
371
|
+
// Positive offset: skip first N rows
|
|
372
|
+
startRow = startRow + offset;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Apply length limit (only for positive offset or no offset)
|
|
376
|
+
if (length !== undefined && length > 0 && (offset === undefined || offset >= 0)) {
|
|
377
|
+
endRow = Math.min(endRow, startRow + length - 1);
|
|
378
|
+
}
|
|
379
|
+
// 1-based position of the first returned row within the totalRows,
|
|
380
|
+
// taken from the pagination actually applied (not the raw offset)
|
|
381
|
+
const firstRow = startRow - rangeStartRow + 1;
|
|
382
|
+
// Ensure valid range
|
|
383
|
+
if (startRow > endRow || startCol > endCol) {
|
|
384
|
+
return { sheetName, data: [], totalRows, returnedRows: 0, firstRow };
|
|
385
|
+
}
|
|
386
|
+
// Build 2D array (preserving types)
|
|
387
|
+
const data = [];
|
|
388
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
389
|
+
const row = worksheet.getRow(r);
|
|
390
|
+
const rowData = [];
|
|
391
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
392
|
+
const cell = row.getCell(c);
|
|
393
|
+
let value = null;
|
|
394
|
+
if (cell.value !== null && cell.value !== undefined) {
|
|
395
|
+
if (typeof cell.value === 'object') {
|
|
396
|
+
// Handle formula results, rich text, etc.
|
|
397
|
+
if ('result' in cell.value) {
|
|
398
|
+
value = cell.value.result ?? null;
|
|
399
|
+
}
|
|
400
|
+
else if ('richText' in cell.value) {
|
|
401
|
+
value = cell.value.richText.map((rt) => rt.text).join('');
|
|
402
|
+
}
|
|
403
|
+
else if ('text' in cell.value) {
|
|
404
|
+
value = cell.value.text;
|
|
405
|
+
}
|
|
406
|
+
else if (cell.value instanceof Date) {
|
|
407
|
+
value = cell.value.toISOString();
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
value = String(cell.value);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
// Preserve native types (string, number, boolean)
|
|
415
|
+
value = cell.value;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
rowData.push(value);
|
|
419
|
+
}
|
|
420
|
+
data.push(rowData);
|
|
421
|
+
}
|
|
422
|
+
return { sheetName, data, totalRows, returnedRows: data.length, firstRow };
|
|
423
|
+
}
|
|
424
|
+
writeDataToSheet(workbook, sheetName, data) {
|
|
425
|
+
// Remove existing sheet if it exists
|
|
426
|
+
const existing = workbook.getWorksheet(sheetName);
|
|
427
|
+
if (existing) {
|
|
428
|
+
workbook.removeWorksheet(existing.id);
|
|
429
|
+
}
|
|
430
|
+
const worksheet = workbook.addWorksheet(sheetName);
|
|
431
|
+
this.writeRowsStartingAt(worksheet, 1, data);
|
|
432
|
+
}
|
|
433
|
+
writeRowsStartingAt(worksheet, startRow, data) {
|
|
434
|
+
for (let r = 0; r < data.length; r++) {
|
|
435
|
+
const rowData = data[r];
|
|
436
|
+
if (!Array.isArray(rowData))
|
|
437
|
+
continue;
|
|
438
|
+
const row = worksheet.getRow(startRow + r);
|
|
439
|
+
for (let c = 0; c < rowData.length; c++) {
|
|
440
|
+
const value = rowData[c];
|
|
441
|
+
if (typeof value === 'string' && value.startsWith('=')) {
|
|
442
|
+
row.getCell(c + 1).value = { formula: value.substring(1) };
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
row.getCell(c + 1).value = value;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
row.commit();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
parseRange(range) {
|
|
452
|
+
if (range.includes('!')) {
|
|
453
|
+
const idx = range.indexOf('!');
|
|
454
|
+
let sheetName = range.slice(0, idx);
|
|
455
|
+
const cellRange = range.slice(idx + 1);
|
|
456
|
+
// Strip Excel-native single quotes around sheet names with spaces:
|
|
457
|
+
// 'My Sheet'!A1 → My Sheet, A1
|
|
458
|
+
if (sheetName.length >= 2 && sheetName.startsWith("'") && sheetName.endsWith("'")) {
|
|
459
|
+
sheetName = sheetName.slice(1, -1).replace(/''/g, "'");
|
|
460
|
+
}
|
|
461
|
+
return [sheetName, cellRange];
|
|
462
|
+
}
|
|
463
|
+
return [range, null];
|
|
464
|
+
}
|
|
465
|
+
parseCellRange(range) {
|
|
466
|
+
// Parse A1 or A1:C10 format
|
|
467
|
+
const match = range.match(/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i);
|
|
468
|
+
if (!match) {
|
|
469
|
+
throw new Error(`Invalid cell range: "${range}". Expected forms: "A1", "A1:C10", or "SheetName!A1:C10" ` +
|
|
470
|
+
`(single-quote sheet names containing spaces: "'My Sheet'!A1:C10").`);
|
|
471
|
+
}
|
|
472
|
+
const startCol = this.columnToNumber(match[1]);
|
|
473
|
+
const startRow = parseInt(match[2], 10);
|
|
474
|
+
if (match[3] && match[4]) {
|
|
475
|
+
// Any two opposite corners name the range, as in Excel: A5:C2,
|
|
476
|
+
// C2:A5 and C5:A2 are all A2:C5
|
|
477
|
+
const endCol = this.columnToNumber(match[3]);
|
|
478
|
+
const endRow = parseInt(match[4], 10);
|
|
479
|
+
return {
|
|
480
|
+
startRow: Math.min(startRow, endRow),
|
|
481
|
+
startCol: Math.min(startCol, endCol),
|
|
482
|
+
endRow: Math.max(startRow, endRow),
|
|
483
|
+
endCol: Math.max(startCol, endCol)
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return { startRow, startCol };
|
|
487
|
+
}
|
|
488
|
+
columnToNumber(col) {
|
|
489
|
+
// Column letters are case-insensitive, as in Excel: "b" is column B (2)
|
|
490
|
+
const letters = col.toUpperCase();
|
|
491
|
+
let result = 0;
|
|
492
|
+
for (let i = 0; i < letters.length; i++) {
|
|
493
|
+
result = result * 26 + letters.charCodeAt(i) - 64;
|
|
494
|
+
}
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory pattern for creating appropriate file handlers
|
|
3
|
+
* Routes file operations to the correct handler based on file type
|
|
4
|
+
*
|
|
5
|
+
* Each handler implements canHandle() which can be sync (extension-based)
|
|
6
|
+
* or async (content-based like BinaryFileHandler using isBinaryFile)
|
|
7
|
+
*/
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { TextFileHandler } from './text.js';
|
|
10
|
+
import { ImageFileHandler } from './image.js';
|
|
11
|
+
import { BinaryFileHandler } from './binary.js';
|
|
12
|
+
import { ExcelFileHandler, exceljsPackage } from './excel.js';
|
|
13
|
+
import { PdfFileHandler } from './pdf.js';
|
|
14
|
+
import { DocxFileHandler, pizzipPackage } from './docx.js';
|
|
15
|
+
import { pdfLibPackage } from '../../tools/pdf/manipulations.js';
|
|
16
|
+
import { pdf2mdPackage } from '../../tools/pdf/lib/pdf2md.js';
|
|
17
|
+
import { unpdfPackage } from '../../tools/pdf/extract-images.js';
|
|
18
|
+
import { mdToPdfPackage } from '../../tools/pdf/markdown.js';
|
|
19
|
+
// Singleton instances of each handler
|
|
20
|
+
let excelHandler = null;
|
|
21
|
+
let imageHandler = null;
|
|
22
|
+
let textHandler = null;
|
|
23
|
+
let binaryHandler = null;
|
|
24
|
+
let pdfHandler = null;
|
|
25
|
+
let docxHandler = null;
|
|
26
|
+
/**
|
|
27
|
+
* Initialize handlers (lazy initialization)
|
|
28
|
+
*/
|
|
29
|
+
function getExcelHandler() {
|
|
30
|
+
if (!excelHandler)
|
|
31
|
+
excelHandler = new ExcelFileHandler();
|
|
32
|
+
return excelHandler;
|
|
33
|
+
}
|
|
34
|
+
function getImageHandler() {
|
|
35
|
+
if (!imageHandler)
|
|
36
|
+
imageHandler = new ImageFileHandler();
|
|
37
|
+
return imageHandler;
|
|
38
|
+
}
|
|
39
|
+
function getTextHandler() {
|
|
40
|
+
if (!textHandler)
|
|
41
|
+
textHandler = new TextFileHandler();
|
|
42
|
+
return textHandler;
|
|
43
|
+
}
|
|
44
|
+
function getBinaryHandler() {
|
|
45
|
+
if (!binaryHandler)
|
|
46
|
+
binaryHandler = new BinaryFileHandler();
|
|
47
|
+
return binaryHandler;
|
|
48
|
+
}
|
|
49
|
+
function getPdfHandler() {
|
|
50
|
+
if (!pdfHandler)
|
|
51
|
+
pdfHandler = new PdfFileHandler();
|
|
52
|
+
return pdfHandler;
|
|
53
|
+
}
|
|
54
|
+
function getDocxHandler() {
|
|
55
|
+
if (!docxHandler)
|
|
56
|
+
docxHandler = new DocxFileHandler();
|
|
57
|
+
return docxHandler;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get the appropriate file handler for a given file path
|
|
61
|
+
*
|
|
62
|
+
* Each handler's canHandle() determines if it can process the file.
|
|
63
|
+
* Extension-based handlers (Excel, Image) return sync boolean.
|
|
64
|
+
* BinaryFileHandler uses async isBinaryFile for content-based detection.
|
|
65
|
+
*
|
|
66
|
+
* Priority order:
|
|
67
|
+
* 1. DOCX files (extension based)
|
|
68
|
+
* 2. PDF files (extension based)
|
|
69
|
+
* 3. Excel files (xlsx, xls, xlsm) - extension based
|
|
70
|
+
* 4. Image files (png, jpg, gif, webp) - extension based
|
|
71
|
+
* 5. Binary files - content-based detection via isBinaryFile
|
|
72
|
+
* 6. Text files (default)
|
|
73
|
+
*
|
|
74
|
+
* @param filePath File path to get handler for
|
|
75
|
+
* @param options svgAsImage: an SVG goes to the image handler, for the file preview widget
|
|
76
|
+
* @returns FileHandler instance that can handle this file
|
|
77
|
+
*/
|
|
78
|
+
export async function getFileHandler(filePath, options) {
|
|
79
|
+
// Check DOCX first (extension-based, sync)
|
|
80
|
+
if (getDocxHandler().canHandle(filePath)) {
|
|
81
|
+
return getDocxHandler();
|
|
82
|
+
}
|
|
83
|
+
// Check PDF (extension-based, sync)
|
|
84
|
+
if (getPdfHandler().canHandle(filePath)) {
|
|
85
|
+
return getPdfHandler();
|
|
86
|
+
}
|
|
87
|
+
// Check Excel (extension-based, sync)
|
|
88
|
+
if (getExcelHandler().canHandle(filePath)) {
|
|
89
|
+
return getExcelHandler();
|
|
90
|
+
}
|
|
91
|
+
// Check Image (extension-based, sync - images are binary but handled specially)
|
|
92
|
+
if (getImageHandler().canHandle(filePath, options)) {
|
|
93
|
+
return getImageHandler();
|
|
94
|
+
}
|
|
95
|
+
// Check Binary (content-based, async via isBinaryFile)
|
|
96
|
+
if (await getBinaryHandler().canHandle(filePath)) {
|
|
97
|
+
return getBinaryHandler();
|
|
98
|
+
}
|
|
99
|
+
// Default to text handler
|
|
100
|
+
return getTextHandler();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if a file path is an Excel file
|
|
104
|
+
* Delegates to ExcelFileHandler.canHandle to avoid duplicating extension logic
|
|
105
|
+
* @param path File path
|
|
106
|
+
* @returns true if file is Excel format
|
|
107
|
+
*/
|
|
108
|
+
export function isExcelFile(path) {
|
|
109
|
+
return getExcelHandler().canHandle(path);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Check if a file path is an image file
|
|
113
|
+
* Delegates to ImageFileHandler.canHandle to avoid duplicating extension logic
|
|
114
|
+
* @param path File path
|
|
115
|
+
* @returns true if file is an image format
|
|
116
|
+
*/
|
|
117
|
+
export function isImageFile(path) {
|
|
118
|
+
return getImageHandler().canHandle(path);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Right after initialize (#777): the packages Excel, DOCX and PDF files need,
|
|
122
|
+
* loaded in the background one at a time, smaller first, so each pause of the
|
|
123
|
+
* main thread is one package.
|
|
124
|
+
*/
|
|
125
|
+
export function preloadFileSupport() {
|
|
126
|
+
void (async () => {
|
|
127
|
+
for (const pkg of [pizzipPackage, pdfLibPackage, pdf2mdPackage, unpdfPackage, exceljsPackage, mdToPdfPackage]) {
|
|
128
|
+
await pkg.preload();
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
}
|
|
132
|
+
/** The packages a file needs for `action`, by the handlers' canHandle() (its name, no disk access) */
|
|
133
|
+
function packagesFor(filePath, action, isPdf, isUrl) {
|
|
134
|
+
if (isPdf) {
|
|
135
|
+
// Editing a PDF can insert markdown pages, which are rendered
|
|
136
|
+
return action === 'read' ? [pdf2mdPackage, unpdfPackage] : action === 'write' ? [mdToPdfPackage] : [pdfLibPackage, mdToPdfPackage];
|
|
137
|
+
}
|
|
138
|
+
// A URL is fetched, not opened by a file handler: only a PDF one is parsed
|
|
139
|
+
if (isUrl)
|
|
140
|
+
return [];
|
|
141
|
+
if (getDocxHandler().canHandle(filePath))
|
|
142
|
+
return [pizzipPackage];
|
|
143
|
+
if (getExcelHandler().canHandle(filePath))
|
|
144
|
+
return [exceljsPackage];
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The answer for a tool call that would `action` `filePath` while a package it
|
|
149
|
+
* needs is still loading, naming the file; undefined once loaded. A package
|
|
150
|
+
* not preloading yet (before initialize, or after a failure) starts here,
|
|
151
|
+
* after the call has answered. `isPdf` for a PDF whatever its name (write_pdf),
|
|
152
|
+
* `isUrl` for a URL read.
|
|
153
|
+
*/
|
|
154
|
+
export function stillLoadingError(filePath, action, { isPdf = getPdfHandler().canHandle(filePath), isUrl = false } = {}) {
|
|
155
|
+
const pending = packagesFor(filePath, action, isPdf, isUrl).find((pkg) => !pkg.loaded);
|
|
156
|
+
if (!pending)
|
|
157
|
+
return undefined;
|
|
158
|
+
const file = path.basename(filePath);
|
|
159
|
+
const { error, needsRestart, support } = pending;
|
|
160
|
+
if (!needsRestart)
|
|
161
|
+
void pending.preload();
|
|
162
|
+
if (!error) {
|
|
163
|
+
return `Can't ${action} ${file} yet: Desktop Commander is still loading its ${support} (it starts right after launch). Try again in a few seconds.`;
|
|
164
|
+
}
|
|
165
|
+
return needsRestart
|
|
166
|
+
? `Can't ${action} ${file}: Desktop Commander couldn't load its ${support} (${error}). Restart Desktop Commander to load it again.`
|
|
167
|
+
: `Can't ${action} ${file}: Desktop Commander couldn't load its ${support} (${error}). It's loading it again; try again in a few seconds.`;
|
|
168
|
+
}
|