cursor-reads-xlsx 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 senoff
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # cursor-reads-xlsx
2
+
3
+ Converts `.xlsx` files into rich text dumps that AI coding agents can actually read.
4
+
5
+ AI tools like Cursor, Claude, Copilot, etc. can read text files but **not** `.xlsx` binaries. This CLI bridges the gap — it extracts everything a human would see in Excel and writes it to a plain text file:
6
+
7
+ - **Values** — strings, numbers, dates
8
+ - **Formulas** — the actual formula expression, not just the result
9
+ - **Formatting** — bold, italic, font colors, background fills
10
+ - **Number formats** — percentages, currency, custom patterns
11
+ - **Layout** — column widths, frozen panes, merged cells, alignment
12
+ - **Hyperlinks** — URLs embedded in cells
13
+ - **Comments / notes** — cell annotations
14
+ - **Named ranges** — workbook-defined names and their references
15
+ - **Hidden rows & columns** — flagged so the AI knows data is suppressed
16
+ - **Data validation** — dropdown lists, numeric constraints
17
+ - **Tables** — Excel Table objects with their names and column headers
18
+ - **Images & charts** — existence and position noted (content not rendered)
19
+ - **Auto-filters** — active filter ranges
20
+ - **Print areas** — defined print regions
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install -g cursor-reads-xlsx
26
+ ```
27
+
28
+ Or run directly with npx (no install needed):
29
+
30
+ ```bash
31
+ npx cursor-reads-xlsx budget.xlsx
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ # Dump all sheets
38
+ npx cursor-reads-xlsx data.xlsx
39
+
40
+ # Dump a specific sheet
41
+ npx cursor-reads-xlsx data.xlsx "Sheet1"
42
+
43
+ # List sheet names and dimensions without dumping
44
+ npx cursor-reads-xlsx data.xlsx --list-sheets
45
+
46
+ # Print to stdout instead of writing files
47
+ npx cursor-reads-xlsx data.xlsx --stdout
48
+
49
+ # Limit to first 200 rows per sheet (useful for huge files)
50
+ npx cursor-reads-xlsx data.xlsx --max-rows 200
51
+
52
+ # Combine flags
53
+ npx cursor-reads-xlsx data.xlsx "Sheet1" --stdout --max-rows 50
54
+ ```
55
+
56
+ ### Options
57
+
58
+ | Flag | Description |
59
+ |------|-------------|
60
+ | `--list-sheets` | Print sheet names, row/column counts, and visibility — then exit |
61
+ | `--stdout` | Print output to stdout instead of writing `.txt` files |
62
+ | `--max-rows N` | Cap output at the first N rows per sheet |
63
+ | `-h`, `--help` | Show help message |
64
+
65
+ Output files are written to `.xlsx-read/` in the current working directory.
66
+ Each sheet produces a file named `<filename>--<sheetname>.txt`.
67
+ The path(s) are printed to stdout so your agent knows where to read.
68
+
69
+ ## Output Format
70
+
71
+ ```
72
+ === Sheet: Sales ===
73
+ Frozen: row 1, col 0
74
+ Columns: A(12) B(20) C(15) D(10)
75
+ Auto-filter: A1:D20
76
+ Named ranges:
77
+ Totals: Sales!$D$2:$D$20
78
+ Table: "SalesTable" A1:D20 — columns: Region, Q1, Q2, Total
79
+
80
+ --- Row 1 [bold] ---
81
+ A1: "Region" [bold]
82
+ B1: "Q1" [bold] [align:center]
83
+ C1: "Q2" [bold] [align:center]
84
+ D1: "Total" [bold] [align:center]
85
+ --- Row 2 ---
86
+ A2: "North" [link: https://example.com/north]
87
+ B2: 14500 [numFmt: #,##0]
88
+ C2: 17200 [numFmt: #,##0]
89
+ D2: 31700 [formula: =B2+C2] [numFmt: #,##0] [note: Includes returns]
90
+ --- Row 3 ---
91
+ A3: "South" [fill:FFFFFF00]
92
+ B3: 9800 [numFmt: #,##0] [validation: list [North,South,East,West]]
93
+ C3: 11050 [numFmt: #,##0]
94
+ D3: 20850 [formula: =B3+C3] [numFmt: #,##0]
95
+ --- Row 4 (empty) [hidden] ---
96
+ ```
97
+
98
+ ### Sheet Metadata
99
+
100
+ | Line | Meaning |
101
+ |------|---------|
102
+ | `Frozen: row 1, col 2` | Frozen panes position |
103
+ | `Columns: A(12) B(20)` | Column widths (Excel character units) |
104
+ | `Hidden columns: E, F` | Columns hidden in the spreadsheet |
105
+ | `Merged: A1:B1` | Merged cell ranges |
106
+ | `Auto-filter: A1:D20` | Active auto-filter range |
107
+ | `Print area: A1:D50` | Defined print area |
108
+ | `Named ranges:` | Workbook-defined names referencing this sheet |
109
+ | `Table: "Name" A1:D20` | Excel Table objects with column headers |
110
+ | `Image: A1 to C5` | Embedded image position |
111
+
112
+ ### Cell Tags
113
+
114
+ | Tag | Meaning |
115
+ |-----|---------|
116
+ | `[formula: =SUM(A1:A10)]` | Cell contains this formula |
117
+ | `[numFmt: 0.00%]` | Number format (when not "General") |
118
+ | `[bold]` | Bold font |
119
+ | `[italic]` | Italic font |
120
+ | `[color:FF8B0000]` | Font color (ARGB hex) |
121
+ | `[fill:FFFFFF00]` | Cell background color (ARGB hex) |
122
+ | `[align:center]` | Horizontal alignment (when not default) |
123
+ | `[link: https://...]` | Hyperlink URL |
124
+ | `[note: ...]` | Cell comment or note text |
125
+ | `[validation: list [...]]` | Data validation (dropdown values or constraints) |
126
+ | `[hidden]` | Row is hidden in the spreadsheet |
127
+
128
+ ### `--list-sheets` Output
129
+
130
+ ```
131
+ Sales 250 rows × 12 cols
132
+ Config 15 rows × 4 cols
133
+ Archive 1200 rows × 8 cols [hidden]
134
+ ```
135
+
136
+ ## Cursor Rule Template
137
+
138
+ Copy the included rule template into your project so your AI agent automatically uses this tool when it encounters `.xlsx` files:
139
+
140
+ ```bash
141
+ mkdir -p .cursor/rules
142
+ cp node_modules/cursor-reads-xlsx/cursor-rule-template/read-xlsx.mdc .cursor/rules/
143
+ ```
144
+
145
+ Or if you installed globally / use npx, copy the template from the repo:
146
+
147
+ ```bash
148
+ mkdir -p .cursor/rules
149
+ curl -o .cursor/rules/read-xlsx.mdc https://raw.githubusercontent.com/senoff/cursor-reads-xlsx/main/cursor-rule-template/read-xlsx.mdc
150
+ ```
151
+
152
+ ## Why This Exists
153
+
154
+ Spreadsheets are everywhere in real projects — financial models, data exports, config files. AI coding agents choke on binary formats. This tool makes spreadsheets legible to AI with zero information loss.
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: Reading .xlsx spreadsheet files
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+
7
+ # Reading .xlsx Files
8
+
9
+ The Read tool cannot open `.xlsx` files directly. When you need to inspect a spreadsheet:
10
+
11
+ 1. **Run the converter** from the project root:
12
+ ```bash
13
+ npx cursor-reads-xlsx <path-to-file.xlsx> [sheetName]
14
+ ```
15
+ - If `sheetName` is omitted, all sheets are dumped.
16
+ - Output is written to `.xlsx-read/<filename>--<sheet>.txt` (project root).
17
+ - The script prints the output path(s) to stdout.
18
+
19
+ 2. **Read the output file** with the Read tool. It contains:
20
+ - Sheet metadata (frozen panes, column widths, merged cells, auto-filters, print areas)
21
+ - Named ranges referencing the sheet
22
+ - Table definitions (name, range, columns)
23
+ - Image positions
24
+ - Every row with its cells, showing:
25
+ - **Value** — always present
26
+ - **Formula** — `[formula: =SUM(A1:A10)]` if the cell has one
27
+ - **Number format** — `[numFmt: 0.00%]` if not "General"
28
+ - **Font** — `[bold]`, `[italic]`, `[color:FF8B0000]`
29
+ - **Fill** — `[fill:FFFFFF00]` if background color set
30
+ - **Alignment** — `[align:center]` if non-default
31
+ - **Hyperlink** — `[link: https://...]` if the cell contains a URL
32
+ - **Comment** — `[note: ...]` if the cell has a comment or note
33
+ - **Validation** — `[validation: list [...]]` if the cell has data validation
34
+ - **Hidden** — `[hidden]` on the row header if the row is hidden
35
+ - Empty cells are omitted; empty rows show `(empty)`.
36
+
37
+ 3. **Do not ask the user** before running this. Just run it when you need to see an `.xlsx` file.
38
+
39
+ ## Useful flags
40
+ - `--list-sheets` — list sheet names and dimensions without dumping content
41
+ - `--stdout` — print directly to stdout instead of writing files
42
+ - `--max-rows N` — limit output to first N rows (use for large sheets)
43
+
44
+ ## Important
45
+ - Output goes to `.xlsx-read/` in the current working directory — make sure this directory is in your `.gitignore`.
46
+ - For large files, use `--max-rows` or request a single sheet to keep output manageable.
package/index.js ADDED
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const ExcelJS = require('exceljs');
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Argument parsing
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function parseArgs(argv) {
12
+ const opts = { positional: [], listSheets: false, stdout: false, maxRows: null, help: false };
13
+ let i = 0;
14
+ while (i < argv.length) {
15
+ const arg = argv[i];
16
+ if (arg === '--list-sheets') opts.listSheets = true;
17
+ else if (arg === '--stdout') opts.stdout = true;
18
+ else if (arg === '--max-rows') { opts.maxRows = parseInt(argv[++i], 10); }
19
+ else if (arg === '-h' || arg === '--help') opts.help = true;
20
+ else opts.positional.push(arg);
21
+ i++;
22
+ }
23
+ return opts;
24
+ }
25
+
26
+ function printHelp() {
27
+ console.log(`Usage: npx cursor-reads-xlsx <file.xlsx> [sheetName] [options]
28
+
29
+ Converts .xlsx to rich text that AI coding agents can read.
30
+
31
+ Options:
32
+ --list-sheets List sheet names, dimensions, and visibility then exit
33
+ --stdout Print output to stdout instead of writing files
34
+ --max-rows N Limit output to the first N rows per sheet
35
+ -h, --help Show this help message
36
+
37
+ Examples:
38
+ npx cursor-reads-xlsx data.xlsx
39
+ npx cursor-reads-xlsx data.xlsx "Sheet1"
40
+ npx cursor-reads-xlsx data.xlsx --list-sheets
41
+ npx cursor-reads-xlsx data.xlsx --stdout --max-rows 100`);
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Helpers
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function colLetter(n) {
49
+ let s = '';
50
+ for (; n > 0; n = Math.floor((n - 1) / 26))
51
+ s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
52
+ return s;
53
+ }
54
+
55
+ function describeFill(fill) {
56
+ if (!fill || (fill.type === 'pattern' && fill.pattern === 'none')) return null;
57
+ if (fill.type === 'pattern' && fill.fgColor?.argb) return `fill:${fill.fgColor.argb}`;
58
+ return null;
59
+ }
60
+
61
+ function describeFont(font) {
62
+ const parts = [];
63
+ if (font?.bold) parts.push('bold');
64
+ if (font?.italic) parts.push('italic');
65
+ if (font?.color?.argb) parts.push(`color:${font.color.argb}`);
66
+ return parts;
67
+ }
68
+
69
+ function formatValue(v) {
70
+ if (v == null) return '""';
71
+ if (v instanceof Date) return `"${v.toISOString().slice(0, 10)}"`;
72
+ if (typeof v === 'object' && v.richText) {
73
+ return `"${v.richText.map(r => r.text).join('')}"`;
74
+ }
75
+ if (typeof v === 'object' && v.hyperlink) {
76
+ return `"${v.text || v.hyperlink}"`;
77
+ }
78
+ if (typeof v === 'object' && v.formula) {
79
+ return String(v.result ?? '');
80
+ }
81
+ if (typeof v === 'string') return `"${v}"`;
82
+ return String(v);
83
+ }
84
+
85
+ function describeNote(note) {
86
+ if (!note) return null;
87
+ if (typeof note === 'string') return note;
88
+ if (note.texts) {
89
+ return note.texts.map(t => (typeof t === 'string' ? t : t.text || '')).join('');
90
+ }
91
+ return null;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Named ranges (workbook-level, filtered to a sheet if name provided)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ function getNamedRanges(wb, sheetName) {
99
+ const results = [];
100
+ try {
101
+ const model = wb.definedNames?.model;
102
+ if (!Array.isArray(model)) return results;
103
+ for (const def of model) {
104
+ if (!def.ranges?.length) continue;
105
+ if (sheetName) {
106
+ const relevant = def.ranges.filter(r => r.includes(sheetName + '!'));
107
+ if (relevant.length) results.push({ name: def.name, ranges: relevant });
108
+ } else {
109
+ results.push({ name: def.name, ranges: def.ranges });
110
+ }
111
+ }
112
+ } catch (_) {}
113
+ return results;
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Sheet dump
118
+ // ---------------------------------------------------------------------------
119
+
120
+ function dumpSheet(ws, wb, maxRows) {
121
+ const lines = [];
122
+
123
+ lines.push(`=== Sheet: ${ws.name} ===`);
124
+
125
+ // Frozen panes
126
+ const views = ws.views || [];
127
+ const frozen = views.find(v => v.state === 'frozen');
128
+ if (frozen) {
129
+ lines.push(`Frozen: row ${frozen.ySplit ?? 0}, col ${frozen.xSplit ?? 0}`);
130
+ }
131
+
132
+ // Column widths + hidden columns
133
+ const colWidths = [];
134
+ const hiddenCols = [];
135
+ for (let c = 1; c <= ws.columnCount; c++) {
136
+ const col = ws.getColumn(c);
137
+ const letter = colLetter(c);
138
+ if (col.hidden) hiddenCols.push(letter);
139
+ if (col.width) colWidths.push(`${letter}(${Math.round(col.width)})`);
140
+ }
141
+ if (colWidths.length) lines.push(`Columns: ${colWidths.join(' ')}`);
142
+ if (hiddenCols.length) lines.push(`Hidden columns: ${hiddenCols.join(', ')}`);
143
+
144
+ // Merged cells
145
+ const merges = Object.keys(ws._merges || {});
146
+ if (merges.length) lines.push(`Merged: ${merges.join(', ')}`);
147
+
148
+ // Auto-filter
149
+ if (ws.autoFilter) {
150
+ const af = typeof ws.autoFilter === 'string'
151
+ ? ws.autoFilter
152
+ : (ws.autoFilter.ref || JSON.stringify(ws.autoFilter));
153
+ lines.push(`Auto-filter: ${af}`);
154
+ }
155
+
156
+ // Print area
157
+ try {
158
+ if (ws.pageSetup?.printArea) {
159
+ lines.push(`Print area: ${ws.pageSetup.printArea}`);
160
+ }
161
+ } catch (_) {}
162
+
163
+ // Named ranges relevant to this sheet
164
+ const namedRanges = getNamedRanges(wb, ws.name);
165
+ if (namedRanges.length) {
166
+ lines.push(`Named ranges:`);
167
+ for (const nr of namedRanges) {
168
+ lines.push(` ${nr.name}: ${nr.ranges.join(', ')}`);
169
+ }
170
+ }
171
+
172
+ // Tables
173
+ try {
174
+ const tableMap = ws.tables;
175
+ if (tableMap && typeof tableMap === 'object') {
176
+ const tables = typeof tableMap.forEach === 'function'
177
+ ? (() => { const a = []; tableMap.forEach(t => a.push(t)); return a; })()
178
+ : Object.values(tableMap);
179
+ for (const t of tables) {
180
+ const model = t.table || t.model || t;
181
+ const name = model.name || model.displayName || '(unnamed)';
182
+ const ref = model.ref || model.tableRef || '';
183
+ const cols = (model.columns || []).map(c => c.name).filter(Boolean);
184
+ let desc = `Table: "${name}" ${ref}`;
185
+ if (cols.length) desc += ` — columns: ${cols.join(', ')}`;
186
+ lines.push(desc);
187
+ }
188
+ }
189
+ } catch (_) {}
190
+
191
+ // Images
192
+ try {
193
+ const images = typeof ws.getImages === 'function' ? ws.getImages() : [];
194
+ for (const img of images) {
195
+ if (img.range) {
196
+ const tl = img.range.tl;
197
+ const br = img.range.br;
198
+ if (tl && br) {
199
+ lines.push(`Image: ${colLetter(Math.floor(tl.col) + 1)}${Math.floor(tl.row) + 1} to ${colLetter(Math.floor(br.col) + 1)}${Math.floor(br.row) + 1}`);
200
+ } else if (tl) {
201
+ lines.push(`Image at: ${colLetter(Math.floor(tl.col) + 1)}${Math.floor(tl.row) + 1}`);
202
+ }
203
+ } else {
204
+ lines.push(`Image: (position unknown)`);
205
+ }
206
+ }
207
+ } catch (_) {}
208
+
209
+ lines.push('');
210
+
211
+ // Rows
212
+ const rowLimit = maxRows ? Math.min(ws.rowCount, maxRows) : ws.rowCount;
213
+
214
+ for (let r = 1; r <= rowLimit; r++) {
215
+ const row = ws.getRow(r);
216
+ const cells = [];
217
+ const isHidden = row.hidden;
218
+
219
+ for (let c = 1; c <= ws.columnCount; c++) {
220
+ const cell = row.getCell(c);
221
+ const raw = cell.value;
222
+ if (raw == null || raw === '') continue;
223
+
224
+ const ref = `${colLetter(c)}${r}`;
225
+ const tags = [];
226
+
227
+ // Formula
228
+ if (cell.type === ExcelJS.ValueType.Formula) {
229
+ const formula = typeof raw === 'object' ? raw.formula : null;
230
+ if (formula) tags.push(`formula: =${formula}`);
231
+ }
232
+
233
+ // Number format
234
+ if (cell.numFmt && cell.numFmt !== 'General') {
235
+ tags.push(`numFmt: ${cell.numFmt}`);
236
+ }
237
+
238
+ // Font
239
+ const fontTags = describeFont(cell.font);
240
+ if (fontTags.length) tags.push(...fontTags);
241
+
242
+ // Fill
243
+ const fillDesc = describeFill(cell.fill);
244
+ if (fillDesc) tags.push(fillDesc);
245
+
246
+ // Alignment
247
+ if (cell.alignment?.horizontal && cell.alignment.horizontal !== 'general') {
248
+ tags.push(`align:${cell.alignment.horizontal}`);
249
+ }
250
+
251
+ // Hyperlink
252
+ if (cell.hyperlink) {
253
+ tags.push(`link: ${cell.hyperlink}`);
254
+ } else if (typeof raw === 'object' && raw.hyperlink) {
255
+ tags.push(`link: ${raw.hyperlink}`);
256
+ }
257
+
258
+ // Comment / note
259
+ const noteText = describeNote(cell.note);
260
+ if (noteText) {
261
+ tags.push(`note: ${noteText.replace(/\n/g, ' ').trim()}`);
262
+ }
263
+
264
+ // Data validation
265
+ if (cell.dataValidation) {
266
+ const dv = cell.dataValidation;
267
+ if (dv.type === 'list' && dv.formulae?.length) {
268
+ tags.push(`validation: list [${dv.formulae[0]}]`);
269
+ } else if (dv.type) {
270
+ const parts = [dv.type];
271
+ if (dv.operator) parts.push(dv.operator);
272
+ if (dv.formulae?.length) parts.push(dv.formulae.join(', '));
273
+ tags.push(`validation: ${parts.join(' ')}`);
274
+ }
275
+ }
276
+
277
+ const displayVal = formatValue(raw);
278
+ const tagStr = tags.length ? ` [${tags.join('] [')}]` : '';
279
+ cells.push(` ${ref}: ${displayVal}${tagStr}`);
280
+ }
281
+
282
+ if (cells.length === 0) {
283
+ const hiddenTag = isHidden ? ' [hidden]' : '';
284
+ lines.push(`--- Row ${r} (empty)${hiddenTag} ---`);
285
+ } else {
286
+ const rowBold = row.font?.bold ? ' [bold]' : '';
287
+ const hiddenTag = isHidden ? ' [hidden]' : '';
288
+ lines.push(`--- Row ${r}${rowBold}${hiddenTag} ---`);
289
+ lines.push(...cells);
290
+ }
291
+ }
292
+
293
+ if (maxRows && ws.rowCount > maxRows) {
294
+ lines.push('');
295
+ lines.push(`... ${ws.rowCount - maxRows} more rows (truncated at --max-rows ${maxRows})`);
296
+ }
297
+
298
+ return lines.join('\n');
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // List sheets mode
303
+ // ---------------------------------------------------------------------------
304
+
305
+ function listSheets(wb) {
306
+ const lines = [];
307
+ for (const ws of wb.worksheets) {
308
+ const vis = ws.state === 'hidden' ? ' [hidden]'
309
+ : ws.state === 'veryHidden' ? ' [very hidden]'
310
+ : '';
311
+ lines.push(`${ws.name} ${ws.rowCount} rows × ${ws.columnCount} cols${vis}`);
312
+ }
313
+ return lines.join('\n');
314
+ }
315
+
316
+ // ---------------------------------------------------------------------------
317
+ // Main
318
+ // ---------------------------------------------------------------------------
319
+
320
+ async function main() {
321
+ const opts = parseArgs(process.argv.slice(2));
322
+
323
+ if (opts.help) { printHelp(); process.exit(0); }
324
+ if (opts.positional.length < 1) { printHelp(); process.exit(1); }
325
+
326
+ const xlsxPath = path.resolve(opts.positional[0]);
327
+ const sheetFilter = opts.positional[1] || null;
328
+
329
+ if (!fs.existsSync(xlsxPath)) {
330
+ console.error(`File not found: ${xlsxPath}`);
331
+ process.exit(1);
332
+ }
333
+
334
+ const wb = new ExcelJS.Workbook();
335
+ await wb.xlsx.readFile(xlsxPath);
336
+
337
+ // --list-sheets: print summary and exit
338
+ if (opts.listSheets) {
339
+ console.log(listSheets(wb));
340
+ process.exit(0);
341
+ }
342
+
343
+ const sheets = sheetFilter
344
+ ? [wb.getWorksheet(sheetFilter)].filter(Boolean)
345
+ : wb.worksheets;
346
+
347
+ if (sheets.length === 0) {
348
+ console.error(sheetFilter
349
+ ? `Sheet "${sheetFilter}" not found. Available: ${wb.worksheets.map(s => s.name).join(', ')}`
350
+ : 'No sheets in workbook');
351
+ process.exit(1);
352
+ }
353
+
354
+ const baseName = path.basename(xlsxPath, path.extname(xlsxPath));
355
+
356
+ // --stdout: print to console
357
+ if (opts.stdout) {
358
+ for (const ws of sheets) {
359
+ console.log(dumpSheet(ws, wb, opts.maxRows));
360
+ console.log('');
361
+ }
362
+ process.exit(0);
363
+ }
364
+
365
+ // Default: write to .xlsx-read/ files
366
+ const outDir = path.join(process.cwd(), '.xlsx-read');
367
+ fs.mkdirSync(outDir, { recursive: true });
368
+
369
+ for (const ws of sheets) {
370
+ const content = dumpSheet(ws, wb, opts.maxRows);
371
+ const safeName = ws.name.replace(/[^a-zA-Z0-9_-]/g, '_');
372
+ const outFile = path.join(outDir, `${baseName}--${safeName}.txt`);
373
+ fs.writeFileSync(outFile, content, 'utf8');
374
+ console.log(outFile);
375
+ }
376
+ }
377
+
378
+ main().catch((err) => {
379
+ console.error(err.message);
380
+ process.exit(1);
381
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "cursor-reads-xlsx",
3
+ "version": "1.0.0",
4
+ "description": "Converts .xlsx files into rich text dumps that AI coding agents (Cursor, Claude, etc.) can read — preserving values, formulas, formatting, colors, column widths, and frozen panes.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "cursor-reads-xlsx": "index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "cursor-rule-template",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "xlsx",
17
+ "excel",
18
+ "cursor",
19
+ "ai",
20
+ "cli",
21
+ "spreadsheet",
22
+ "text",
23
+ "converter"
24
+ ],
25
+ "author": "senoff",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "exceljs": "^4.4.0"
29
+ }
30
+ }