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.
Files changed (93) hide show
  1. package/dist/src/cli/mac-mcp-gateway.js +4 -3
  2. package/dist/src/cli/mac-mcp-gateway.js.map +1 -1
  3. package/dist/src/cli/starmux.js +1 -1
  4. package/dist/src/cli/starmux.js.map +1 -1
  5. package/dist/src/runtime-paths.js +3 -3
  6. package/dist/src/runtime-paths.js.map +1 -1
  7. package/dist/vendor/desktop-commander/LICENSE +21 -0
  8. package/dist/vendor/desktop-commander/dist/bootstrap.js +22 -0
  9. package/dist/vendor/desktop-commander/dist/command-manager.js +246 -0
  10. package/dist/vendor/desktop-commander/dist/config-field-definitions.js +37 -0
  11. package/dist/vendor/desktop-commander/dist/config-manager.js +634 -0
  12. package/dist/vendor/desktop-commander/dist/config-recovery.js +81 -0
  13. package/dist/vendor/desktop-commander/dist/config.js +19 -0
  14. package/dist/vendor/desktop-commander/dist/custom-stdio.js +358 -0
  15. package/dist/vendor/desktop-commander/dist/error-handlers.js +15 -0
  16. package/dist/vendor/desktop-commander/dist/handlers/edit-search-handlers.js +6 -0
  17. package/dist/vendor/desktop-commander/dist/handlers/filesystem-handlers.js +450 -0
  18. package/dist/vendor/desktop-commander/dist/handlers/history-handlers.js +67 -0
  19. package/dist/vendor/desktop-commander/dist/handlers/index.js +7 -0
  20. package/dist/vendor/desktop-commander/dist/handlers/process-handlers.js +15 -0
  21. package/dist/vendor/desktop-commander/dist/handlers/search-answers.js +172 -0
  22. package/dist/vendor/desktop-commander/dist/handlers/search-handlers.js +136 -0
  23. package/dist/vendor/desktop-commander/dist/handlers/terminal-handlers.js +35 -0
  24. package/dist/vendor/desktop-commander/dist/index.js +88 -0
  25. package/dist/vendor/desktop-commander/dist/search-manager.js +1262 -0
  26. package/dist/vendor/desktop-commander/dist/server.js +1414 -0
  27. package/dist/vendor/desktop-commander/dist/terminal-manager.js +757 -0
  28. package/dist/vendor/desktop-commander/dist/tools/config.js +211 -0
  29. package/dist/vendor/desktop-commander/dist/tools/edit.js +455 -0
  30. package/dist/vendor/desktop-commander/dist/tools/feedback.js +10 -0
  31. package/dist/vendor/desktop-commander/dist/tools/filesystem.js +1047 -0
  32. package/dist/vendor/desktop-commander/dist/tools/fuzzySearch.js +74 -0
  33. package/dist/vendor/desktop-commander/dist/tools/fuzzySearchCore.js +125 -0
  34. package/dist/vendor/desktop-commander/dist/tools/improved-process-tools.js +726 -0
  35. package/dist/vendor/desktop-commander/dist/tools/mime-types.js +28 -0
  36. package/dist/vendor/desktop-commander/dist/tools/pdf/extract-images.js +131 -0
  37. package/dist/vendor/desktop-commander/dist/tools/pdf/index.js +4 -0
  38. package/dist/vendor/desktop-commander/dist/tools/pdf/lib/pdf2md.js +78 -0
  39. package/dist/vendor/desktop-commander/dist/tools/pdf/manipulations.js +112 -0
  40. package/dist/vendor/desktop-commander/dist/tools/pdf/markdown.js +30 -0
  41. package/dist/vendor/desktop-commander/dist/tools/pdf/utils.js +45 -0
  42. package/dist/vendor/desktop-commander/dist/tools/process.js +90 -0
  43. package/dist/vendor/desktop-commander/dist/tools/prompts.js +259 -0
  44. package/dist/vendor/desktop-commander/dist/tools/schemas.js +242 -0
  45. package/dist/vendor/desktop-commander/dist/tools/usage.js +24 -0
  46. package/dist/vendor/desktop-commander/dist/types.js +1 -0
  47. package/dist/vendor/desktop-commander/dist/ui/config-editor/config-editor-runtime.js +150 -0
  48. package/dist/vendor/desktop-commander/dist/ui/config-editor/index.html +13 -0
  49. package/dist/vendor/desktop-commander/dist/ui/config-editor/styles.css +587 -0
  50. package/dist/vendor/desktop-commander/dist/ui/contracts.js +57 -0
  51. package/dist/vendor/desktop-commander/dist/ui/file-preview/index.html +16 -0
  52. package/dist/vendor/desktop-commander/dist/ui/file-preview/preview-runtime.js +471 -0
  53. package/dist/vendor/desktop-commander/dist/ui/file-preview/shared/preview-file-types.js +59 -0
  54. package/dist/vendor/desktop-commander/dist/ui/file-preview/styles.css +1631 -0
  55. package/dist/vendor/desktop-commander/dist/ui/resources.js +93 -0
  56. package/dist/vendor/desktop-commander/dist/utils/ab-test.js +122 -0
  57. package/dist/vendor/desktop-commander/dist/utils/atomic-write.js +80 -0
  58. package/dist/vendor/desktop-commander/dist/utils/capture.js +40 -0
  59. package/dist/vendor/desktop-commander/dist/utils/dockerPrompt.js +9 -0
  60. package/dist/vendor/desktop-commander/dist/utils/exit-process.js +31 -0
  61. package/dist/vendor/desktop-commander/dist/utils/feature-flags.js +18 -0
  62. package/dist/vendor/desktop-commander/dist/utils/files/base.js +5 -0
  63. package/dist/vendor/desktop-commander/dist/utils/files/binary.js +65 -0
  64. package/dist/vendor/desktop-commander/dist/utils/files/docx.js +694 -0
  65. package/dist/vendor/desktop-commander/dist/utils/files/excel.js +497 -0
  66. package/dist/vendor/desktop-commander/dist/utils/files/factory.js +168 -0
  67. package/dist/vendor/desktop-commander/dist/utils/files/image.js +89 -0
  68. package/dist/vendor/desktop-commander/dist/utils/files/index.js +14 -0
  69. package/dist/vendor/desktop-commander/dist/utils/files/pdf.js +124 -0
  70. package/dist/vendor/desktop-commander/dist/utils/files/text.js +423 -0
  71. package/dist/vendor/desktop-commander/dist/utils/fuzzySearchLogger.js +126 -0
  72. package/dist/vendor/desktop-commander/dist/utils/internal-facts.js +23 -0
  73. package/dist/vendor/desktop-commander/dist/utils/lazy-package.js +44 -0
  74. package/dist/vendor/desktop-commander/dist/utils/lineEndingHandler.js +77 -0
  75. package/dist/vendor/desktop-commander/dist/utils/logger.js +72 -0
  76. package/dist/vendor/desktop-commander/dist/utils/mcp-ui-ab-test.js +62 -0
  77. package/dist/vendor/desktop-commander/dist/utils/open-browser.js +2 -0
  78. package/dist/vendor/desktop-commander/dist/utils/process-detection.js +260 -0
  79. package/dist/vendor/desktop-commander/dist/utils/process-tree.js +223 -0
  80. package/dist/vendor/desktop-commander/dist/utils/rename.js +74 -0
  81. package/dist/vendor/desktop-commander/dist/utils/ripgrep-resolver.js +69 -0
  82. package/dist/vendor/desktop-commander/dist/utils/shell.js +187 -0
  83. package/dist/vendor/desktop-commander/dist/utils/system-info.js +727 -0
  84. package/dist/vendor/desktop-commander/dist/utils/terminal-timestamps.js +18 -0
  85. package/dist/vendor/desktop-commander/dist/utils/toolHistory.js +309 -0
  86. package/dist/vendor/desktop-commander/dist/utils/trackTools.js +55 -0
  87. package/dist/vendor/desktop-commander/dist/utils/unsupportedParams.js +66 -0
  88. package/dist/vendor/desktop-commander/dist/utils/usageTracker.js +225 -0
  89. package/dist/vendor/desktop-commander/dist/utils/welcome-onboarding.js +2 -0
  90. package/dist/vendor/desktop-commander/dist/utils/withTimeout.js +85 -0
  91. package/dist/vendor/desktop-commander/dist/version.js +1 -0
  92. package/dist/vendor/desktop-commander/package.json +15 -0
  93. 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
+ }