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 +21 -0
- package/README.md +158 -0
- package/cursor-rule-template/read-xlsx.mdc +46 -0
- package/index.js +381 -0
- package/package.json +30 -0
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
|
+
}
|