@cteno/sdk-linux-x64 0.0.0-stage → 2.10.13
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.daemon +31 -0
- package/README.md +4 -2
- package/agents/flow-analyzer/AGENT.md +71 -0
- package/bin/cteno-host-cli +4 -0
- package/bin/cteno-wiki +0 -0
- package/bin/rg +0 -0
- package/bin/secret-exec +0 -0
- package/manifest.json +12 -0
- package/package.json +33 -4
- package/plugins/agent-secrets/.cteno-plugin/plugin.json +133 -0
- package/plugins/agent-secrets/bin/linux-x64/fnox +4 -0
- package/plugins/agent-secrets/bin/linux-x64/secret-exec +0 -0
- package/plugins/agent-secrets/skills/secret/SKILL.md +201 -0
- package/plugins/cteno.browser/.cteno-plugin/plugin.json +65 -0
- package/plugins/cteno.browser/skills/browser/SKILL.md +28 -0
- package/plugins/cteno.inbox/.cteno-plugin/plugin.json +128 -0
- package/plugins/cteno.inbox/bin/all/cteno-connector-credential +31 -0
- package/plugins/cteno.inbox/bin/all/cteno-telegram-connector +33 -0
- package/plugins/cteno.inbox/bin/linux-x64/cteno-inbox +0 -0
- package/plugins/cteno.inbox/skills/cteno-inbox/SKILL.md +374 -0
- package/plugins/cteno.inbox/tools/inbox_focus/TOOL.md +44 -0
- package/plugins/cteno.memory/.cteno-plugin/plugin.json +176 -0
- package/plugins/cteno.memory/agents/llm-wiki/AGENT.md +96 -0
- package/plugins/cteno.memory/bin/linux-x64/cteno-wiki +0 -0
- package/plugins/cteno.memory/skills/codebase-wiki/SKILL.md +52 -0
- package/plugins/cteno.memory/skills/cteno.memory.native/SKILL.md +110 -0
- package/plugins/cteno.memory/tools/memory/TOOL.md +492 -0
- package/plugins/cteno.memory/ui/index.html +822 -0
- package/plugins/cteno.models/.cteno-plugin/plugin.json +191 -0
- package/plugins/cteno.websearch/.cteno-plugin/plugin.json +45 -0
- package/plugins/jev/.cteno-plugin/plugin.json +305 -0
- package/plugins/jev/skills/jev-browser/SKILL.md +66 -0
- package/plugins/jev/skills/jev-browser/jev_browser.py +335 -0
- package/skills/agent-create/SKILL.md +86 -0
- package/skills/cteno-host-cli/SKILL.md +248 -0
- package/skills/docx/LICENSE +23 -0
- package/skills/docx/NOTICE.md +8 -0
- package/skills/docx/SKILL.md +69 -0
- package/skills/docx/references/cjk_typography.md +357 -0
- package/skills/docx/references/design_principles.md +819 -0
- package/skills/docx/references/revisions-and-comments.md +88 -0
- package/skills/docx/references/track_changes_guide.md +200 -0
- package/skills/docx/references/typography_guide.md +294 -0
- package/skills/docx/scripts/cjk_fonts.py +244 -0
- package/skills/docx/scripts/docx_comments.py +289 -0
- package/skills/docx/scripts/docx_common.py +92 -0
- package/skills/docx/scripts/docx_create.py +259 -0
- package/skills/docx/scripts/docx_edit.py +250 -0
- package/skills/docx/scripts/docx_read.py +149 -0
- package/skills/docx/scripts/docx_revisions.py +146 -0
- package/skills/docx/scripts/docx_template.py +70 -0
- package/skills/docx/scripts/docx_validate.py +156 -0
- package/skills/docx/scripts/render.py +250 -0
- package/skills/docx/tests/test_docx_skill.py +525 -0
- package/skills/home-devices/SKILL.md +121 -0
- package/skills/mcp/SKILL.md +72 -0
- package/skills/mcp/scripts/mcp_setup.py +439 -0
- package/skills/pdf/LICENSE +23 -0
- package/skills/pdf/NOTICE.md +8 -0
- package/skills/pdf/SKILL.md +63 -0
- package/skills/pdf/references/design-tokens.md +381 -0
- package/skills/pdf/references/forms.md +99 -0
- package/skills/pdf/references/nano-pdf-editing.md +40 -0
- package/skills/pdf/references/ocr-extraction.md +165 -0
- package/skills/pdf/scripts/_raster.py +76 -0
- package/skills/pdf/scripts/extract_marker.py +88 -0
- package/skills/pdf/scripts/extract_pymupdf.py +101 -0
- package/skills/pdf/scripts/pdf_create.py +191 -0
- package/skills/pdf/scripts/pdf_fill_form.py +97 -0
- package/skills/pdf/scripts/pdf_form_layout.py +168 -0
- package/skills/pdf/scripts/pdf_make_form.py +145 -0
- package/skills/pdf/scripts/pdf_merge.py +50 -0
- package/skills/pdf/scripts/pdf_meta.py +115 -0
- package/skills/pdf/scripts/pdf_page_image.py +99 -0
- package/skills/pdf/scripts/pdf_read.py +153 -0
- package/skills/pdf/scripts/pdf_secure.py +71 -0
- package/skills/pdf/scripts/pdf_split.py +84 -0
- package/skills/pdf/scripts/pdf_stamp.py +143 -0
- package/skills/pdf/scripts/pdf_watermark.py +51 -0
- package/skills/pdf/scripts/render.py +250 -0
- package/skills/pdf/tests/test_pdf_skill.py +415 -0
- package/skills/pptx/LICENSE +23 -0
- package/skills/pptx/NOTICE.md +8 -0
- package/skills/pptx/SKILL.md +62 -0
- package/skills/pptx/references/design-system.md +392 -0
- package/skills/pptx/references/pptxgenjs.md +420 -0
- package/skills/pptx/references/slide-types.md +413 -0
- package/skills/pptx/scripts/cjk_fonts.py +244 -0
- package/skills/pptx/scripts/pptx_create.py +240 -0
- package/skills/pptx/scripts/pptx_edit.py +436 -0
- package/skills/pptx/scripts/pptx_from_template.py +88 -0
- package/skills/pptx/scripts/pptx_read.py +131 -0
- package/skills/pptx/scripts/pptx_render.py +96 -0
- package/skills/pptx/scripts/render.py +250 -0
- package/skills/pptx/tests/test_powerpoint_skill.py +477 -0
- package/skills/skill-create/SKILL.md +114 -0
- package/skills/video-generation/SKILL.md +27 -0
- package/skills/xlsx/LICENSE +23 -0
- package/skills/xlsx/NOTICE.md +8 -0
- package/skills/xlsx/SKILL.md +63 -0
- package/skills/xlsx/references/format.md +71 -0
- package/skills/xlsx/references/restructuring.md +71 -0
- package/skills/xlsx/scripts/csv_to_xlsx.py +110 -0
- package/skills/xlsx/scripts/formula_check.py +428 -0
- package/skills/xlsx/scripts/render.py +250 -0
- package/skills/xlsx/scripts/xlsx_create.py +265 -0
- package/skills/xlsx/scripts/xlsx_edit.py +269 -0
- package/skills/xlsx/scripts/xlsx_read.py +166 -0
- package/skills/xlsx/scripts/xlsx_recalc.py +119 -0
- package/skills/xlsx/scripts/xlsx_restructure.py +343 -0
- package/skills/xlsx/scripts/xlsx_to_csv.py +75 -0
- package/skills/xlsx/tests/test_xlsx_skill.py +544 -0
- package/tools/create_goal/TOOL.md +36 -0
- package/tools/image_generation/TOOL.md +81 -0
- package/tools/skill/TOOL.md +117 -0
- package/tools/update_profile_notes/TOOL.md +39 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create an .xlsx workbook from a JSON spec.
|
|
3
|
+
|
|
4
|
+
Spec (JSON object):
|
|
5
|
+
{
|
|
6
|
+
"full_calc_on_load": true, # force recalculation on open (optional)
|
|
7
|
+
"defined_names": {"Rates": "'Data'!$B$2:$B$4"}, # workbook scope
|
|
8
|
+
"sheets": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Data",
|
|
11
|
+
"rows": [["Header", 1, true], ...], # scalars or cell objects (see below)
|
|
12
|
+
"cells": {"A1": {"value": 5, "format": "0.00%"}}, # sparse overrides
|
|
13
|
+
"column_widths": {"A": 22, "B": 12},
|
|
14
|
+
"row_heights": {"1": 24},
|
|
15
|
+
"merges": ["A1:C1"],
|
|
16
|
+
"freeze_panes": "A2",
|
|
17
|
+
"autofilter": "A1:C10",
|
|
18
|
+
"conditional_formats": [
|
|
19
|
+
{"range": "B2:B9", "type": "cell_is", "operator": "greaterThan",
|
|
20
|
+
"formula": ["100"], "fill": "FFC7CE"},
|
|
21
|
+
{"range": "C2:C9", "type": "color_scale"}
|
|
22
|
+
],
|
|
23
|
+
"charts": [
|
|
24
|
+
{"type": "bar", "title": "Sales", "anchor": "F2",
|
|
25
|
+
"data": "B1:B5", "categories": "A2:A5"}
|
|
26
|
+
],
|
|
27
|
+
"validations": [
|
|
28
|
+
{"range": "D2:D9", "type": "list", "formula1": "\"Yes,No,Maybe\""}
|
|
29
|
+
],
|
|
30
|
+
"tables": [
|
|
31
|
+
{"name": "Sales", "range": "A1:C4",
|
|
32
|
+
"style": "TableStyleMedium9"} # native Excel table
|
|
33
|
+
],
|
|
34
|
+
"protection": {"password": "your-password", # NOT security --
|
|
35
|
+
"unlock": ["B2:B9"]} # see SKILL.md Pitfalls
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Cell object keys (all optional except value/formula):
|
|
41
|
+
value scalar; JSON true/false -> bool, numbers stay numeric
|
|
42
|
+
type "date" or "datetime" -> value parsed from ISO string
|
|
43
|
+
formula e.g. "=SUM(A2:A9)" (leading '=' optional)
|
|
44
|
+
hyperlink URL; value becomes the display text
|
|
45
|
+
note cell note text (or {"text": ..., "author": ...})
|
|
46
|
+
format Excel number format, e.g. "$#,##0.00", "0.0%", "yyyy-mm-dd"
|
|
47
|
+
bold, italic booleans
|
|
48
|
+
font_size points
|
|
49
|
+
font_color hex RGB like "FF0000"
|
|
50
|
+
fill solid fill hex RGB like "DDEBF7"
|
|
51
|
+
border "thin" | "medium" | "thick" (all four sides)
|
|
52
|
+
align "left" | "center" | "right"
|
|
53
|
+
valign "top" | "center" | "bottom"
|
|
54
|
+
wrap boolean (wrap text)
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
xlsx_create.py spec.json out.xlsx
|
|
58
|
+
xlsx_create.py - out.xlsx (spec on stdin)
|
|
59
|
+
|
|
60
|
+
Prints a JSON summary to stdout; exits non-zero on failure.
|
|
61
|
+
"""
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
import argparse
|
|
65
|
+
import json
|
|
66
|
+
import sys
|
|
67
|
+
from datetime import date, datetime
|
|
68
|
+
|
|
69
|
+
from openpyxl import Workbook
|
|
70
|
+
from openpyxl.chart import BarChart, LineChart, PieChart, Reference
|
|
71
|
+
from openpyxl.comments import Comment
|
|
72
|
+
from openpyxl.formatting.rule import CellIsRule, ColorScaleRule
|
|
73
|
+
from openpyxl.styles import (Alignment, Border, Font, PatternFill,
|
|
74
|
+
Protection, Side)
|
|
75
|
+
from openpyxl.utils import column_index_from_string, range_boundaries
|
|
76
|
+
from openpyxl.workbook.defined_name import DefinedName
|
|
77
|
+
from openpyxl.worksheet.datavalidation import DataValidation
|
|
78
|
+
from openpyxl.worksheet.table import Table, TableStyleInfo
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_typed(value, type_hint=None):
|
|
82
|
+
if type_hint == "date" and isinstance(value, str):
|
|
83
|
+
return date.fromisoformat(value)
|
|
84
|
+
if type_hint == "datetime" and isinstance(value, str):
|
|
85
|
+
return datetime.fromisoformat(value)
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def apply_cell(ws, coord, spec):
|
|
90
|
+
cell = ws[coord]
|
|
91
|
+
if isinstance(spec, dict):
|
|
92
|
+
if "formula" in spec:
|
|
93
|
+
f = spec["formula"]
|
|
94
|
+
cell.value = f if f.startswith("=") else "=" + f
|
|
95
|
+
elif "value" in spec:
|
|
96
|
+
cell.value = parse_typed(spec["value"], spec.get("type"))
|
|
97
|
+
if "hyperlink" in spec:
|
|
98
|
+
cell.hyperlink = spec["hyperlink"]
|
|
99
|
+
if cell.value is None:
|
|
100
|
+
cell.value = spec["hyperlink"]
|
|
101
|
+
cell.style = "Hyperlink"
|
|
102
|
+
if "note" in spec:
|
|
103
|
+
note = spec["note"]
|
|
104
|
+
if isinstance(note, dict):
|
|
105
|
+
cell.comment = Comment(note.get("text", ""),
|
|
106
|
+
note.get("author", "xlsx-skill"))
|
|
107
|
+
else:
|
|
108
|
+
cell.comment = Comment(str(note), "xlsx-skill")
|
|
109
|
+
if "format" in spec:
|
|
110
|
+
cell.number_format = spec["format"]
|
|
111
|
+
font_kw = {}
|
|
112
|
+
if spec.get("bold"):
|
|
113
|
+
font_kw["bold"] = True
|
|
114
|
+
if spec.get("italic"):
|
|
115
|
+
font_kw["italic"] = True
|
|
116
|
+
if "font_size" in spec:
|
|
117
|
+
font_kw["size"] = spec["font_size"]
|
|
118
|
+
if "font_color" in spec:
|
|
119
|
+
font_kw["color"] = spec["font_color"]
|
|
120
|
+
if font_kw:
|
|
121
|
+
cell.font = Font(**font_kw)
|
|
122
|
+
if "fill" in spec:
|
|
123
|
+
cell.fill = PatternFill("solid", fgColor=spec["fill"])
|
|
124
|
+
if "border" in spec:
|
|
125
|
+
side = Side(style=spec["border"])
|
|
126
|
+
cell.border = Border(left=side, right=side, top=side, bottom=side)
|
|
127
|
+
align_kw = {}
|
|
128
|
+
if "align" in spec:
|
|
129
|
+
align_kw["horizontal"] = spec["align"]
|
|
130
|
+
if "valign" in spec:
|
|
131
|
+
align_kw["vertical"] = spec["valign"]
|
|
132
|
+
if spec.get("wrap"):
|
|
133
|
+
align_kw["wrap_text"] = True
|
|
134
|
+
if align_kw:
|
|
135
|
+
cell.alignment = Alignment(**align_kw)
|
|
136
|
+
else:
|
|
137
|
+
cell.value = spec
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def ref_from_range(ws, rng):
|
|
141
|
+
min_col, min_row, max_col, max_row = range_boundaries(rng)
|
|
142
|
+
return Reference(ws, min_col=min_col, min_row=min_row,
|
|
143
|
+
max_col=max_col, max_row=max_row)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def add_chart(ws, spec):
|
|
147
|
+
kind = spec.get("type", "bar")
|
|
148
|
+
chart = {"bar": BarChart, "line": LineChart, "pie": PieChart}[kind]()
|
|
149
|
+
if "title" in spec:
|
|
150
|
+
chart.title = spec["title"]
|
|
151
|
+
data = ref_from_range(ws, spec["data"])
|
|
152
|
+
chart.add_data(data, titles_from_data=spec.get("titles_from_data", True))
|
|
153
|
+
if "categories" in spec:
|
|
154
|
+
chart.set_categories(ref_from_range(ws, spec["categories"]))
|
|
155
|
+
ws.add_chart(chart, spec.get("anchor", "H2"))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def add_conditional(ws, spec):
|
|
159
|
+
rng = spec["range"]
|
|
160
|
+
kind = spec.get("type", "cell_is")
|
|
161
|
+
if kind == "color_scale":
|
|
162
|
+
rule = ColorScaleRule(
|
|
163
|
+
start_type="min", start_color=spec.get("start_color", "FFF8696B"),
|
|
164
|
+
end_type="max", end_color=spec.get("end_color", "FF63BE7B"))
|
|
165
|
+
else:
|
|
166
|
+
fill = PatternFill("solid", fgColor=spec.get("fill", "FFC7CE"))
|
|
167
|
+
rule = CellIsRule(operator=spec.get("operator", "greaterThan"),
|
|
168
|
+
formula=spec.get("formula", ["0"]), fill=fill)
|
|
169
|
+
ws.conditional_formatting.add(rng, rule)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build_sheet(ws, spec):
|
|
173
|
+
for row in spec.get("rows", []):
|
|
174
|
+
values, styled = [], []
|
|
175
|
+
for item in row:
|
|
176
|
+
if isinstance(item, dict):
|
|
177
|
+
values.append(None)
|
|
178
|
+
styled.append(item)
|
|
179
|
+
else:
|
|
180
|
+
values.append(item)
|
|
181
|
+
styled.append(None)
|
|
182
|
+
ws.append(values)
|
|
183
|
+
r = ws.max_row
|
|
184
|
+
for idx, item in enumerate(styled, start=1):
|
|
185
|
+
if item is not None:
|
|
186
|
+
apply_cell(ws, ws.cell(row=r, column=idx).coordinate, item)
|
|
187
|
+
for coord, cell_spec in spec.get("cells", {}).items():
|
|
188
|
+
apply_cell(ws, coord, cell_spec)
|
|
189
|
+
for col, width in spec.get("column_widths", {}).items():
|
|
190
|
+
ws.column_dimensions[col].width = width
|
|
191
|
+
for row, height in spec.get("row_heights", {}).items():
|
|
192
|
+
ws.row_dimensions[int(row)].height = height
|
|
193
|
+
for rng in spec.get("merges", []):
|
|
194
|
+
ws.merge_cells(rng)
|
|
195
|
+
if spec.get("freeze_panes"):
|
|
196
|
+
ws.freeze_panes = spec["freeze_panes"]
|
|
197
|
+
if spec.get("autofilter"):
|
|
198
|
+
ws.auto_filter.ref = spec["autofilter"]
|
|
199
|
+
for cf in spec.get("conditional_formats", []):
|
|
200
|
+
add_conditional(ws, cf)
|
|
201
|
+
for ch in spec.get("charts", []):
|
|
202
|
+
add_chart(ws, ch)
|
|
203
|
+
for dv_spec in spec.get("validations", []):
|
|
204
|
+
dv = DataValidation(type=dv_spec.get("type", "list"),
|
|
205
|
+
formula1=dv_spec["formula1"],
|
|
206
|
+
allow_blank=dv_spec.get("allow_blank", True))
|
|
207
|
+
dv.add(dv_spec["range"])
|
|
208
|
+
ws.add_data_validation(dv)
|
|
209
|
+
for t_spec in spec.get("tables", []):
|
|
210
|
+
table = Table(displayName=t_spec["name"], ref=t_spec["range"])
|
|
211
|
+
table.tableStyleInfo = TableStyleInfo(
|
|
212
|
+
name=t_spec.get("style", "TableStyleMedium9"),
|
|
213
|
+
showRowStripes=t_spec.get("row_stripes", True),
|
|
214
|
+
showColumnStripes=t_spec.get("column_stripes", False))
|
|
215
|
+
ws.add_table(table)
|
|
216
|
+
prot = spec.get("protection")
|
|
217
|
+
if prot:
|
|
218
|
+
for rng in prot.get("unlock", []):
|
|
219
|
+
for row in ws[rng]:
|
|
220
|
+
for cell in row:
|
|
221
|
+
cell.protection = Protection(locked=False)
|
|
222
|
+
if prot.get("password"):
|
|
223
|
+
ws.protection.password = prot["password"]
|
|
224
|
+
ws.protection.sheet = True
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def main(argv=None):
|
|
228
|
+
# Text I/O is UTF-8 regardless of the locale (GBK on Chinese Windows).
|
|
229
|
+
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
230
|
+
try:
|
|
231
|
+
stream.reconfigure(encoding="utf-8")
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
ap = argparse.ArgumentParser(description="Create .xlsx from a JSON spec.")
|
|
235
|
+
ap.add_argument("spec", help="path to JSON spec, or '-' for stdin")
|
|
236
|
+
ap.add_argument("output", help="output .xlsx path")
|
|
237
|
+
args = ap.parse_args(argv)
|
|
238
|
+
|
|
239
|
+
if args.spec == "-":
|
|
240
|
+
spec = json.load(sys.stdin)
|
|
241
|
+
else:
|
|
242
|
+
with open(args.spec, encoding="utf-8") as fh:
|
|
243
|
+
spec = json.load(fh)
|
|
244
|
+
|
|
245
|
+
wb = Workbook()
|
|
246
|
+
wb.remove(wb.active)
|
|
247
|
+
for sheet_spec in spec.get("sheets", []):
|
|
248
|
+
ws = wb.create_sheet(sheet_spec.get("name", "Sheet1"))
|
|
249
|
+
build_sheet(ws, sheet_spec)
|
|
250
|
+
for name, ref in spec.get("defined_names", {}).items():
|
|
251
|
+
wb.defined_names[name] = DefinedName(name, attr_text=ref)
|
|
252
|
+
if spec.get("full_calc_on_load"):
|
|
253
|
+
wb.calculation.fullCalcOnLoad = True
|
|
254
|
+
wb.save(args.output)
|
|
255
|
+
print(json.dumps({"ok": True, "output": args.output,
|
|
256
|
+
"sheets": wb.sheetnames}, ensure_ascii=False))
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
try:
|
|
262
|
+
sys.exit(main())
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
|
|
265
|
+
sys.exit(1)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Edit an existing .xlsx workbook in place (or to --out).
|
|
3
|
+
|
|
4
|
+
Operations (repeatable where noted, applied in the order listed below):
|
|
5
|
+
--rename-sheet OLD:NEW rename a sheet
|
|
6
|
+
--copy-sheet SRC:NEW duplicate a sheet under a new name
|
|
7
|
+
--insert-rows IDX[:N] insert N rows before row IDX (default N=1)
|
|
8
|
+
--delete-rows IDX[:N] delete N rows starting at row IDX
|
|
9
|
+
--insert-cols IDX[:N] insert N columns before column IDX (number)
|
|
10
|
+
--delete-cols IDX[:N] delete N columns starting at column IDX
|
|
11
|
+
--set CELL=VALUE repeatable; type-inferred (int, float, bool,
|
|
12
|
+
ISO date, else string). '=...' sets a formula.
|
|
13
|
+
--append ROWJSON repeatable; JSON array appended as a row
|
|
14
|
+
--add-table NAME:RANGE[:STYLE] create a native Excel table (ListObject)
|
|
15
|
+
--table-append NAME=ROWJSON append a row inside a table, auto-extending
|
|
16
|
+
the table's range (repeatable)
|
|
17
|
+
--list-tables print tables on the target sheet and exit
|
|
18
|
+
--define-name NAME=REF workbook-scope defined name, e.g.
|
|
19
|
+
"Rates='Data'!$B$2:$B$9" (repeatable)
|
|
20
|
+
--delete-name NAME remove a defined name (repeatable)
|
|
21
|
+
--hyperlink CELL=URL[|TEXT] set a hyperlink (optional display text)
|
|
22
|
+
--note CELL=TEXT[|AUTHOR] set a cell note/comment (repeatable)
|
|
23
|
+
--clear-note CELL remove a cell note (repeatable)
|
|
24
|
+
--protect [PASSWORD] enable sheet protection; combine with
|
|
25
|
+
--unlock RANGE to leave ranges editable.
|
|
26
|
+
NOT security: trivially strippable (see
|
|
27
|
+
SKILL.md Pitfalls).
|
|
28
|
+
--recalc set fullCalcOnLoad so Excel/LibreOffice
|
|
29
|
+
recomputes all formulas on next open
|
|
30
|
+
|
|
31
|
+
WARNING: openpyxl does NOT shift merged-cell ranges, chart anchors, or
|
|
32
|
+
formula references when rows/columns are inserted or deleted. Verify any
|
|
33
|
+
sheet containing merges or formulas after structural edits — or use
|
|
34
|
+
xlsx_restructure.py, which rewrites references for you.
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
xlsx_edit.py book.xlsx --sheet Data --set B2=42 --set C2=2026-01-01 \
|
|
38
|
+
--set "D2==SUM(B2:C2)" --recalc
|
|
39
|
+
xlsx_edit.py book.xlsx --sheet Data --append '["Widget", 9.99, true]'
|
|
40
|
+
xlsx_edit.py book.xlsx --copy-sheet Data:Backup --rename-sheet Data:Main
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import json
|
|
46
|
+
import sys
|
|
47
|
+
from datetime import date, datetime
|
|
48
|
+
|
|
49
|
+
from openpyxl import load_workbook
|
|
50
|
+
from openpyxl.comments import Comment
|
|
51
|
+
from openpyxl.styles import Protection
|
|
52
|
+
from openpyxl.utils import get_column_letter, range_boundaries
|
|
53
|
+
from openpyxl.workbook.defined_name import DefinedName
|
|
54
|
+
from openpyxl.worksheet.table import Table, TableStyleInfo
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def infer(text):
|
|
58
|
+
if text.startswith("="):
|
|
59
|
+
return text # formula
|
|
60
|
+
low = text.lower()
|
|
61
|
+
if low in ("true", "false"):
|
|
62
|
+
return low == "true"
|
|
63
|
+
for caster in (int, float):
|
|
64
|
+
try:
|
|
65
|
+
return caster(text)
|
|
66
|
+
except ValueError:
|
|
67
|
+
pass
|
|
68
|
+
for parser in (date.fromisoformat, datetime.fromisoformat):
|
|
69
|
+
try:
|
|
70
|
+
return parser(text)
|
|
71
|
+
except ValueError:
|
|
72
|
+
pass
|
|
73
|
+
return text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_idx(arg):
|
|
77
|
+
if ":" in arg:
|
|
78
|
+
idx, n = arg.split(":", 1)
|
|
79
|
+
return int(idx), int(n)
|
|
80
|
+
return int(arg), 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def add_table(ws, spec):
|
|
84
|
+
parts = spec.split(":")
|
|
85
|
+
if len(parts) < 3:
|
|
86
|
+
raise ValueError("--add-table needs NAME:RANGE like Sales:A1:C9")
|
|
87
|
+
name = parts[0]
|
|
88
|
+
rng = ":".join(parts[1:3])
|
|
89
|
+
style = parts[3] if len(parts) > 3 else "TableStyleMedium9"
|
|
90
|
+
table = Table(displayName=name, ref=rng)
|
|
91
|
+
table.tableStyleInfo = TableStyleInfo(name=style, showRowStripes=True)
|
|
92
|
+
ws.add_table(table)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def table_append(ws, name, row_values):
|
|
96
|
+
table = ws.tables[name]
|
|
97
|
+
min_col, min_row, max_col, max_row = range_boundaries(table.ref)
|
|
98
|
+
new_row = max_row + 1
|
|
99
|
+
for offset, value in enumerate(row_values):
|
|
100
|
+
ws.cell(row=new_row, column=min_col + offset, value=value)
|
|
101
|
+
table.ref = (f"{get_column_letter(min_col)}{min_row}:"
|
|
102
|
+
f"{get_column_letter(max_col)}{new_row}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main(argv=None):
|
|
106
|
+
# Text I/O is UTF-8 regardless of the locale (GBK on Chinese Windows).
|
|
107
|
+
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
108
|
+
try:
|
|
109
|
+
stream.reconfigure(encoding="utf-8")
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
ap = argparse.ArgumentParser(
|
|
113
|
+
description="Edit an existing .xlsx workbook.",
|
|
114
|
+
epilog="Plain insert/delete does not shift merges/formula refs — "
|
|
115
|
+
"use xlsx_restructure.py for reference-aware moves.")
|
|
116
|
+
ap.add_argument("file", help="path to .xlsx file")
|
|
117
|
+
ap.add_argument("--sheet", help="target sheet (default: active)")
|
|
118
|
+
ap.add_argument("--out", help="output path (default: edit in place)")
|
|
119
|
+
ap.add_argument("--rename-sheet", action="append", default=[],
|
|
120
|
+
metavar="OLD:NEW")
|
|
121
|
+
ap.add_argument("--copy-sheet", action="append", default=[],
|
|
122
|
+
metavar="SRC:NEW")
|
|
123
|
+
ap.add_argument("--insert-rows", action="append", default=[],
|
|
124
|
+
metavar="IDX[:N]")
|
|
125
|
+
ap.add_argument("--delete-rows", action="append", default=[],
|
|
126
|
+
metavar="IDX[:N]")
|
|
127
|
+
ap.add_argument("--insert-cols", action="append", default=[],
|
|
128
|
+
metavar="IDX[:N]")
|
|
129
|
+
ap.add_argument("--delete-cols", action="append", default=[],
|
|
130
|
+
metavar="IDX[:N]")
|
|
131
|
+
ap.add_argument("--set", action="append", default=[], metavar="CELL=VALUE")
|
|
132
|
+
ap.add_argument("--append", action="append", default=[], metavar="ROWJSON")
|
|
133
|
+
ap.add_argument("--add-table", action="append", default=[],
|
|
134
|
+
metavar="NAME:RANGE[:STYLE]")
|
|
135
|
+
ap.add_argument("--table-append", action="append", default=[],
|
|
136
|
+
metavar="NAME=ROWJSON")
|
|
137
|
+
ap.add_argument("--list-tables", action="store_true",
|
|
138
|
+
help="print tables on the target sheet and exit")
|
|
139
|
+
ap.add_argument("--define-name", action="append", default=[],
|
|
140
|
+
metavar="NAME=REF")
|
|
141
|
+
ap.add_argument("--delete-name", action="append", default=[],
|
|
142
|
+
metavar="NAME")
|
|
143
|
+
ap.add_argument("--hyperlink", action="append", default=[],
|
|
144
|
+
metavar="CELL=URL[|TEXT]")
|
|
145
|
+
ap.add_argument("--note", action="append", default=[],
|
|
146
|
+
metavar="CELL=TEXT[|AUTHOR]")
|
|
147
|
+
ap.add_argument("--clear-note", action="append", default=[],
|
|
148
|
+
metavar="CELL")
|
|
149
|
+
ap.add_argument("--protect", nargs="?", const="", metavar="PASSWORD",
|
|
150
|
+
help="protect the target sheet (integrity signal only, "
|
|
151
|
+
"NOT security)")
|
|
152
|
+
ap.add_argument("--unlock", action="append", default=[], metavar="RANGE",
|
|
153
|
+
help="cell range left editable under --protect")
|
|
154
|
+
ap.add_argument("--recalc", action="store_true",
|
|
155
|
+
help="force full recalculation when the file is opened")
|
|
156
|
+
args = ap.parse_args(argv)
|
|
157
|
+
|
|
158
|
+
wb = load_workbook(args.file)
|
|
159
|
+
changes = []
|
|
160
|
+
|
|
161
|
+
for pair in args.rename_sheet:
|
|
162
|
+
old, new = pair.split(":", 1)
|
|
163
|
+
wb[old].title = new
|
|
164
|
+
changes.append(f"rename {old}->{new}")
|
|
165
|
+
for pair in args.copy_sheet:
|
|
166
|
+
src, new = pair.split(":", 1)
|
|
167
|
+
copy = wb.copy_worksheet(wb[src])
|
|
168
|
+
copy.title = new
|
|
169
|
+
changes.append(f"copy {src}->{new}")
|
|
170
|
+
|
|
171
|
+
ws = wb[args.sheet] if args.sheet else wb.active
|
|
172
|
+
|
|
173
|
+
if args.list_tables:
|
|
174
|
+
print(json.dumps({"ok": True, "sheet": ws.title,
|
|
175
|
+
"tables": {t.displayName: {
|
|
176
|
+
"ref": t.ref,
|
|
177
|
+
"style": t.tableStyleInfo.name
|
|
178
|
+
if t.tableStyleInfo else None}
|
|
179
|
+
for t in ws.tables.values()}},
|
|
180
|
+
ensure_ascii=False))
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
for arg in args.insert_rows:
|
|
184
|
+
idx, n = parse_idx(arg)
|
|
185
|
+
ws.insert_rows(idx, n)
|
|
186
|
+
changes.append(f"insert_rows {idx}x{n}")
|
|
187
|
+
for arg in args.delete_rows:
|
|
188
|
+
idx, n = parse_idx(arg)
|
|
189
|
+
ws.delete_rows(idx, n)
|
|
190
|
+
changes.append(f"delete_rows {idx}x{n}")
|
|
191
|
+
for arg in args.insert_cols:
|
|
192
|
+
idx, n = parse_idx(arg)
|
|
193
|
+
ws.insert_cols(idx, n)
|
|
194
|
+
changes.append(f"insert_cols {idx}x{n}")
|
|
195
|
+
for arg in args.delete_cols:
|
|
196
|
+
idx, n = parse_idx(arg)
|
|
197
|
+
ws.delete_cols(idx, n)
|
|
198
|
+
changes.append(f"delete_cols {idx}x{n}")
|
|
199
|
+
|
|
200
|
+
for assignment in args.set:
|
|
201
|
+
coord, raw = assignment.split("=", 1)
|
|
202
|
+
ws[coord] = infer(raw)
|
|
203
|
+
changes.append(f"set {coord}")
|
|
204
|
+
for row_json in args.append:
|
|
205
|
+
ws.append(json.loads(row_json))
|
|
206
|
+
changes.append(f"append row {ws.max_row}")
|
|
207
|
+
|
|
208
|
+
for spec in args.add_table:
|
|
209
|
+
add_table(ws, spec)
|
|
210
|
+
changes.append(f"add_table {spec.split(':')[0]}")
|
|
211
|
+
for spec in args.table_append:
|
|
212
|
+
name, row_json = spec.split("=", 1)
|
|
213
|
+
table_append(ws, name, json.loads(row_json))
|
|
214
|
+
changes.append(f"table_append {name} -> {ws.tables[name].ref}")
|
|
215
|
+
|
|
216
|
+
for spec in args.define_name:
|
|
217
|
+
name, ref = spec.split("=", 1)
|
|
218
|
+
wb.defined_names[name] = DefinedName(name, attr_text=ref)
|
|
219
|
+
changes.append(f"define_name {name}")
|
|
220
|
+
for name in args.delete_name:
|
|
221
|
+
del wb.defined_names[name]
|
|
222
|
+
changes.append(f"delete_name {name}")
|
|
223
|
+
|
|
224
|
+
for spec in args.hyperlink:
|
|
225
|
+
coord, rest = spec.split("=", 1)
|
|
226
|
+
url, _, text = rest.partition("|")
|
|
227
|
+
cell = ws[coord]
|
|
228
|
+
cell.hyperlink = url
|
|
229
|
+
cell.value = text or (cell.value if cell.value is not None else url)
|
|
230
|
+
cell.style = "Hyperlink"
|
|
231
|
+
changes.append(f"hyperlink {coord}")
|
|
232
|
+
for spec in args.note:
|
|
233
|
+
coord, rest = spec.split("=", 1)
|
|
234
|
+
text, _, author = rest.partition("|")
|
|
235
|
+
ws[coord].comment = Comment(text, author or "xlsx-skill")
|
|
236
|
+
changes.append(f"note {coord}")
|
|
237
|
+
for coord in args.clear_note:
|
|
238
|
+
ws[coord].comment = None
|
|
239
|
+
changes.append(f"clear_note {coord}")
|
|
240
|
+
|
|
241
|
+
if args.protect is not None:
|
|
242
|
+
for rng in args.unlock:
|
|
243
|
+
for row in ws[rng]:
|
|
244
|
+
for cell in row:
|
|
245
|
+
cell.protection = Protection(locked=False)
|
|
246
|
+
if args.protect:
|
|
247
|
+
ws.protection.password = args.protect
|
|
248
|
+
ws.protection.sheet = True
|
|
249
|
+
changes.append(f"protect {ws.title}"
|
|
250
|
+
+ (f" (unlocked {len(args.unlock)} ranges)"
|
|
251
|
+
if args.unlock else ""))
|
|
252
|
+
|
|
253
|
+
if args.recalc:
|
|
254
|
+
wb.calculation.fullCalcOnLoad = True
|
|
255
|
+
changes.append("fullCalcOnLoad")
|
|
256
|
+
|
|
257
|
+
out = args.out or args.file
|
|
258
|
+
wb.save(out)
|
|
259
|
+
print(json.dumps({"ok": True, "output": out, "sheet": ws.title,
|
|
260
|
+
"changes": changes}, ensure_ascii=False))
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
try:
|
|
266
|
+
sys.exit(main())
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
|
|
269
|
+
sys.exit(1)
|