@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.
Files changed (116) hide show
  1. package/LICENSE.daemon +31 -0
  2. package/README.md +4 -2
  3. package/agents/flow-analyzer/AGENT.md +71 -0
  4. package/bin/cteno-host-cli +4 -0
  5. package/bin/cteno-wiki +0 -0
  6. package/bin/rg +0 -0
  7. package/bin/secret-exec +0 -0
  8. package/manifest.json +12 -0
  9. package/package.json +33 -4
  10. package/plugins/agent-secrets/.cteno-plugin/plugin.json +133 -0
  11. package/plugins/agent-secrets/bin/linux-x64/fnox +4 -0
  12. package/plugins/agent-secrets/bin/linux-x64/secret-exec +0 -0
  13. package/plugins/agent-secrets/skills/secret/SKILL.md +201 -0
  14. package/plugins/cteno.browser/.cteno-plugin/plugin.json +65 -0
  15. package/plugins/cteno.browser/skills/browser/SKILL.md +28 -0
  16. package/plugins/cteno.inbox/.cteno-plugin/plugin.json +128 -0
  17. package/plugins/cteno.inbox/bin/all/cteno-connector-credential +31 -0
  18. package/plugins/cteno.inbox/bin/all/cteno-telegram-connector +33 -0
  19. package/plugins/cteno.inbox/bin/linux-x64/cteno-inbox +0 -0
  20. package/plugins/cteno.inbox/skills/cteno-inbox/SKILL.md +374 -0
  21. package/plugins/cteno.inbox/tools/inbox_focus/TOOL.md +44 -0
  22. package/plugins/cteno.memory/.cteno-plugin/plugin.json +176 -0
  23. package/plugins/cteno.memory/agents/llm-wiki/AGENT.md +96 -0
  24. package/plugins/cteno.memory/bin/linux-x64/cteno-wiki +0 -0
  25. package/plugins/cteno.memory/skills/codebase-wiki/SKILL.md +52 -0
  26. package/plugins/cteno.memory/skills/cteno.memory.native/SKILL.md +110 -0
  27. package/plugins/cteno.memory/tools/memory/TOOL.md +492 -0
  28. package/plugins/cteno.memory/ui/index.html +822 -0
  29. package/plugins/cteno.models/.cteno-plugin/plugin.json +191 -0
  30. package/plugins/cteno.websearch/.cteno-plugin/plugin.json +45 -0
  31. package/plugins/jev/.cteno-plugin/plugin.json +305 -0
  32. package/plugins/jev/skills/jev-browser/SKILL.md +66 -0
  33. package/plugins/jev/skills/jev-browser/jev_browser.py +335 -0
  34. package/skills/agent-create/SKILL.md +86 -0
  35. package/skills/cteno-host-cli/SKILL.md +248 -0
  36. package/skills/docx/LICENSE +23 -0
  37. package/skills/docx/NOTICE.md +8 -0
  38. package/skills/docx/SKILL.md +69 -0
  39. package/skills/docx/references/cjk_typography.md +357 -0
  40. package/skills/docx/references/design_principles.md +819 -0
  41. package/skills/docx/references/revisions-and-comments.md +88 -0
  42. package/skills/docx/references/track_changes_guide.md +200 -0
  43. package/skills/docx/references/typography_guide.md +294 -0
  44. package/skills/docx/scripts/cjk_fonts.py +244 -0
  45. package/skills/docx/scripts/docx_comments.py +289 -0
  46. package/skills/docx/scripts/docx_common.py +92 -0
  47. package/skills/docx/scripts/docx_create.py +259 -0
  48. package/skills/docx/scripts/docx_edit.py +250 -0
  49. package/skills/docx/scripts/docx_read.py +149 -0
  50. package/skills/docx/scripts/docx_revisions.py +146 -0
  51. package/skills/docx/scripts/docx_template.py +70 -0
  52. package/skills/docx/scripts/docx_validate.py +156 -0
  53. package/skills/docx/scripts/render.py +250 -0
  54. package/skills/docx/tests/test_docx_skill.py +525 -0
  55. package/skills/home-devices/SKILL.md +121 -0
  56. package/skills/mcp/SKILL.md +72 -0
  57. package/skills/mcp/scripts/mcp_setup.py +439 -0
  58. package/skills/pdf/LICENSE +23 -0
  59. package/skills/pdf/NOTICE.md +8 -0
  60. package/skills/pdf/SKILL.md +63 -0
  61. package/skills/pdf/references/design-tokens.md +381 -0
  62. package/skills/pdf/references/forms.md +99 -0
  63. package/skills/pdf/references/nano-pdf-editing.md +40 -0
  64. package/skills/pdf/references/ocr-extraction.md +165 -0
  65. package/skills/pdf/scripts/_raster.py +76 -0
  66. package/skills/pdf/scripts/extract_marker.py +88 -0
  67. package/skills/pdf/scripts/extract_pymupdf.py +101 -0
  68. package/skills/pdf/scripts/pdf_create.py +191 -0
  69. package/skills/pdf/scripts/pdf_fill_form.py +97 -0
  70. package/skills/pdf/scripts/pdf_form_layout.py +168 -0
  71. package/skills/pdf/scripts/pdf_make_form.py +145 -0
  72. package/skills/pdf/scripts/pdf_merge.py +50 -0
  73. package/skills/pdf/scripts/pdf_meta.py +115 -0
  74. package/skills/pdf/scripts/pdf_page_image.py +99 -0
  75. package/skills/pdf/scripts/pdf_read.py +153 -0
  76. package/skills/pdf/scripts/pdf_secure.py +71 -0
  77. package/skills/pdf/scripts/pdf_split.py +84 -0
  78. package/skills/pdf/scripts/pdf_stamp.py +143 -0
  79. package/skills/pdf/scripts/pdf_watermark.py +51 -0
  80. package/skills/pdf/scripts/render.py +250 -0
  81. package/skills/pdf/tests/test_pdf_skill.py +415 -0
  82. package/skills/pptx/LICENSE +23 -0
  83. package/skills/pptx/NOTICE.md +8 -0
  84. package/skills/pptx/SKILL.md +62 -0
  85. package/skills/pptx/references/design-system.md +392 -0
  86. package/skills/pptx/references/pptxgenjs.md +420 -0
  87. package/skills/pptx/references/slide-types.md +413 -0
  88. package/skills/pptx/scripts/cjk_fonts.py +244 -0
  89. package/skills/pptx/scripts/pptx_create.py +240 -0
  90. package/skills/pptx/scripts/pptx_edit.py +436 -0
  91. package/skills/pptx/scripts/pptx_from_template.py +88 -0
  92. package/skills/pptx/scripts/pptx_read.py +131 -0
  93. package/skills/pptx/scripts/pptx_render.py +96 -0
  94. package/skills/pptx/scripts/render.py +250 -0
  95. package/skills/pptx/tests/test_powerpoint_skill.py +477 -0
  96. package/skills/skill-create/SKILL.md +114 -0
  97. package/skills/video-generation/SKILL.md +27 -0
  98. package/skills/xlsx/LICENSE +23 -0
  99. package/skills/xlsx/NOTICE.md +8 -0
  100. package/skills/xlsx/SKILL.md +63 -0
  101. package/skills/xlsx/references/format.md +71 -0
  102. package/skills/xlsx/references/restructuring.md +71 -0
  103. package/skills/xlsx/scripts/csv_to_xlsx.py +110 -0
  104. package/skills/xlsx/scripts/formula_check.py +428 -0
  105. package/skills/xlsx/scripts/render.py +250 -0
  106. package/skills/xlsx/scripts/xlsx_create.py +265 -0
  107. package/skills/xlsx/scripts/xlsx_edit.py +269 -0
  108. package/skills/xlsx/scripts/xlsx_read.py +166 -0
  109. package/skills/xlsx/scripts/xlsx_recalc.py +119 -0
  110. package/skills/xlsx/scripts/xlsx_restructure.py +343 -0
  111. package/skills/xlsx/scripts/xlsx_to_csv.py +75 -0
  112. package/skills/xlsx/tests/test_xlsx_skill.py +544 -0
  113. package/tools/create_goal/TOOL.md +36 -0
  114. package/tools/image_generation/TOOL.md +81 -0
  115. package/tools/skill/TOOL.md +117 -0
  116. package/tools/update_profile_notes/TOOL.md +39 -0
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """Export one sheet of an .xlsx workbook to CSV.
3
+
4
+ Dates/datetimes are written in ISO format; None becomes an empty field.
5
+ With --data-only, formula cells yield their cached results (present only
6
+ if the file was last saved by Excel/LibreOffice; openpyxl never computes).
7
+
8
+ Usage:
9
+ xlsx_to_csv.py book.xlsx out.csv
10
+ xlsx_to_csv.py book.xlsx out.csv --sheet Data --encoding utf-8-sig
11
+ xlsx_to_csv.py book.xlsx out.csv --delimiter ';' --data-only
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import csv
17
+ import json
18
+ import sys
19
+ from datetime import date, datetime, time
20
+
21
+ from openpyxl import load_workbook
22
+
23
+
24
+ def to_text(value):
25
+ if value is None:
26
+ return ""
27
+ if isinstance(value, datetime):
28
+ # Excel stores pure dates as midnight datetimes; emit a bare date.
29
+ if value.time() == time(0, 0):
30
+ return value.date().isoformat()
31
+ return value.isoformat()
32
+ if isinstance(value, (date, time)):
33
+ return value.isoformat()
34
+ return value
35
+
36
+
37
+ def main(argv=None):
38
+ # Text I/O is UTF-8 regardless of the locale (GBK on Chinese Windows).
39
+ for stream in (sys.stdin, sys.stdout, sys.stderr):
40
+ try:
41
+ stream.reconfigure(encoding="utf-8")
42
+ except Exception:
43
+ pass
44
+ ap = argparse.ArgumentParser(description=".xlsx sheet -> CSV exporter.")
45
+ ap.add_argument("file", help="input .xlsx path")
46
+ ap.add_argument("output", help="output CSV path")
47
+ ap.add_argument("--sheet", help="sheet name (default: active)")
48
+ ap.add_argument("--encoding", default="utf-8",
49
+ help="CSV output encoding (default utf-8)")
50
+ ap.add_argument("--delimiter", default=",")
51
+ ap.add_argument("--data-only", action="store_true",
52
+ help="cached formula results instead of formula strings")
53
+ args = ap.parse_args(argv)
54
+
55
+ wb = load_workbook(args.file, data_only=args.data_only)
56
+ ws = wb[args.sheet] if args.sheet else wb.active
57
+
58
+ with open(args.output, "w", newline="", encoding=args.encoding) as fh:
59
+ writer = csv.writer(fh, delimiter=args.delimiter)
60
+ count = 0
61
+ for row in ws.iter_rows(values_only=True):
62
+ writer.writerow([to_text(v) for v in row])
63
+ count += 1
64
+
65
+ print(json.dumps({"ok": True, "output": args.output, "sheet": ws.title,
66
+ "rows": count}, ensure_ascii=False))
67
+ return 0
68
+
69
+
70
+ if __name__ == "__main__":
71
+ try:
72
+ sys.exit(main())
73
+ except Exception as exc:
74
+ print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
75
+ sys.exit(1)
@@ -0,0 +1,544 @@
1
+ """End-to-end tests for the xlsx skill helper scripts.
2
+
3
+ Runs each script as a subprocess under LC_ALL=C to prove all text I/O
4
+ uses explicit UTF-8 rather than locale defaults. No network access.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import csv
9
+ import json
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from datetime import date
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+ from openpyxl import load_workbook
19
+
20
+ SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
21
+
22
+
23
+ def run(script, *args, expect_ok=True):
24
+ env = dict(os.environ, LC_ALL="C", LANG="C")
25
+ env.pop("PYTHONIOENCODING", None)
26
+ proc = subprocess.run(
27
+ [sys.executable, str(SCRIPTS / script), *map(str, args)],
28
+ capture_output=True, text=True, env=env, encoding="utf-8", check=False)
29
+ if expect_ok:
30
+ assert proc.returncode == 0, f"{script} failed: {proc.stderr}"
31
+ return proc
32
+
33
+
34
+ SPEC = {
35
+ "full_calc_on_load": True,
36
+ "sheets": [
37
+ {
38
+ "name": "Data",
39
+ "rows": [
40
+ [
41
+ {"value": "Region", "bold": True, "fill": "DDEBF7",
42
+ "border": "thin", "align": "center", "valign": "center"},
43
+ {"value": "Sales", "bold": True, "fill": "DDEBF7"},
44
+ {"value": "Growth", "bold": True},
45
+ {"value": "Audited", "bold": True},
46
+ {"value": "Closed", "bold": True},
47
+ {"value": "Status", "bold": True},
48
+ ],
49
+ ["North", 1500.5, {"value": 0.125, "format": "0.0%"}, True,
50
+ {"value": "2026-01-31", "type": "date",
51
+ "format": "yyyy-mm-dd"}, "Yes"],
52
+ ["South", 900, {"value": -0.03, "format": "0.0%"}, False,
53
+ {"value": "2026-02-28", "type": "date",
54
+ "format": "yyyy-mm-dd"}, "No"],
55
+ ["East", 2100, {"value": 0.4, "format": "0.0%"}, True,
56
+ {"value": "2026-03-31", "type": "date",
57
+ "format": "yyyy-mm-dd"}, "Yes"],
58
+ ],
59
+ "cells": {
60
+ "A6": {"value": "Total", "bold": True, "italic": True,
61
+ "font_size": 12, "font_color": "1F4E78"},
62
+ "B6": {"formula": "SUM(B2:B4)", "format": "$#,##0.00"},
63
+ },
64
+ "column_widths": {"A": 18, "B": 14},
65
+ "row_heights": {"1": 24},
66
+ "merges": ["A8:C8"],
67
+ "freeze_panes": "A2",
68
+ "autofilter": "A1:F4",
69
+ "conditional_formats": [
70
+ {"range": "B2:B4", "type": "cell_is",
71
+ "operator": "greaterThan", "formula": ["1000"],
72
+ "fill": "C6EFCE"},
73
+ {"range": "C2:C4", "type": "color_scale"},
74
+ ],
75
+ "charts": [
76
+ {"type": "bar", "title": "Sales by region", "anchor": "H2",
77
+ "data": "B1:B4", "categories": "A2:A4"},
78
+ {"type": "line", "title": "Growth", "anchor": "H18",
79
+ "data": "C1:C4", "categories": "A2:A4"},
80
+ {"type": "pie", "title": "Share", "anchor": "P2",
81
+ "data": "B2:B4", "categories": "A2:A4",
82
+ "titles_from_data": False},
83
+ ],
84
+ "validations": [
85
+ {"range": "F2:F10", "type": "list",
86
+ "formula1": '"Yes,No,Maybe"'},
87
+ ],
88
+ },
89
+ {"name": "Notes", "rows": [["Zürich", "Фамилия", "12,5%"]]},
90
+ ],
91
+ }
92
+
93
+
94
+ @pytest.fixture
95
+ def workbook(tmp_path):
96
+ spec_path = tmp_path / "spec.json"
97
+ spec_path.write_text(json.dumps(SPEC), encoding="utf-8")
98
+ out = tmp_path / "report.xlsx"
99
+ proc = run("xlsx_create.py", spec_path, out)
100
+ summary = json.loads(proc.stdout)
101
+ assert summary["ok"] and summary["sheets"] == ["Data", "Notes"]
102
+ return out
103
+
104
+
105
+ def test_create_features_roundtrip(workbook):
106
+ wb = load_workbook(workbook)
107
+ ws = wb["Data"]
108
+ # typed values
109
+ assert ws["B2"].value == 1500.5
110
+ assert ws["D2"].value is True
111
+ e2 = ws["E2"].value
112
+ assert (e2.date() if hasattr(e2, "date") else e2) == date(2026, 1, 31)
113
+ # formula + number formats
114
+ assert ws["B6"].value == "=SUM(B2:B4)"
115
+ assert ws["B6"].number_format == "$#,##0.00"
116
+ assert ws["C2"].number_format == "0.0%"
117
+ assert ws["E2"].number_format == "yyyy-mm-dd"
118
+ # styling
119
+ assert ws["A1"].font.bold is True
120
+ assert ws["A1"].fill.fgColor.rgb.endswith("DDEBF7")
121
+ assert ws["A1"].border.left.style == "thin"
122
+ assert ws["A1"].alignment.horizontal == "center"
123
+ assert ws["A6"].font.italic is True and ws["A6"].font.size == 12
124
+ # dimensions
125
+ assert ws.column_dimensions["A"].width == 18
126
+ assert ws.row_dimensions[1].height == 24
127
+ # merges / freeze / autofilter
128
+ assert "A8:C8" in [str(r) for r in ws.merged_cells.ranges]
129
+ assert ws.freeze_panes == "A2"
130
+ assert ws.auto_filter.ref == "A1:F4"
131
+ # conditional formatting, charts, validation
132
+ assert len(list(ws.conditional_formatting)) == 2
133
+ assert len(ws._charts) == 3
134
+ types = {type(c).__name__ for c in ws._charts}
135
+ assert types == {"BarChart", "LineChart", "PieChart"}
136
+ assert len(ws.data_validations.dataValidation) == 1
137
+ # recalc flag
138
+ assert wb.calculation.fullCalcOnLoad is True
139
+
140
+
141
+ def test_read_sheets_json_formulas(workbook, tmp_path):
142
+ inv = json.loads(run("xlsx_read.py", workbook, "--sheets").stdout)
143
+ names = [s["name"] for s in inv["sheets"]]
144
+ assert names == ["Data", "Notes"]
145
+ data_info = inv["sheets"][0]
146
+ assert data_info["charts"] == 3
147
+ assert "A8:C8" in data_info["merged"]
148
+ assert data_info["freeze_panes"] == "A2"
149
+
150
+ dump = json.loads(
151
+ run("xlsx_read.py", workbook, "--json", "--sheet", "Data").stdout)
152
+ assert dump["rows"][1][0] == "North"
153
+ assert dump["rows"][1][4] == "2026-01-31T00:00:00"
154
+
155
+ notes = json.loads(
156
+ run("xlsx_read.py", workbook, "--json", "--sheet", "Notes").stdout)
157
+ assert notes["rows"][0] == ["Zürich", "Фамилия", "12,5%"]
158
+
159
+ formulas = json.loads(run("xlsx_read.py", workbook, "--formulas").stdout)
160
+ entry = next(f for f in formulas["formulas"] if f["cell"] == "B6")
161
+ assert entry["formula"] == "=SUM(B2:B4)"
162
+ # openpyxl never computes: cached value absent on a fresh file
163
+ assert entry["cached"] is None
164
+
165
+ csv_out = tmp_path / "data.csv"
166
+ run("xlsx_read.py", workbook, "--csv", "--sheet", "Notes",
167
+ "--out", csv_out)
168
+ text = csv_out.read_text(encoding="utf-8")
169
+ assert "Zürich" in text and "Фамилия" in text
170
+
171
+
172
+ def test_csv_roundtrip_nonascii(tmp_path):
173
+ src = tmp_path / "src.csv"
174
+ with open(src, "w", newline="", encoding="utf-8") as fh:
175
+ w = csv.writer(fh)
176
+ w.writerow(["City", "Share", "Surname", "Active", "When"])
177
+ w.writerow(["Zürich", "12,5%", "Фамилия", "true", "2026-05-01"])
178
+ w.writerow(["Oslo", "7", "Ås", "false", "2026-06-01"])
179
+ xlsx = tmp_path / "conv.xlsx"
180
+ run("csv_to_xlsx.py", src, xlsx, "--sheet-name", "Import")
181
+
182
+ wb = load_workbook(xlsx)
183
+ ws = wb["Import"]
184
+ assert ws["A2"].value == "Zürich"
185
+ assert ws["B2"].value == "12,5%" # decimal comma stays a string
186
+ assert ws["C2"].value == "Фамилия"
187
+ assert ws["D2"].value is True # bool inferred
188
+ assert ws["E2"].value.date() == date(2026, 5, 1) # date inferred
189
+ assert ws["B3"].value == 7 # int inferred
190
+ assert ws["A1"].font.bold is True # styled header
191
+ assert ws.freeze_panes == "A2"
192
+
193
+ back = tmp_path / "back.csv"
194
+ run("xlsx_to_csv.py", xlsx, back, "--sheet", "Import")
195
+ with open(back, newline="", encoding="utf-8") as fh:
196
+ rows = list(csv.reader(fh))
197
+ assert rows[1][0] == "Zürich"
198
+ assert rows[1][2] == "Фамилия"
199
+ assert rows[1][3] == "True"
200
+ assert rows[1][4] == "2026-05-01"
201
+
202
+ # encoding override
203
+ latin = tmp_path / "latin.csv"
204
+ run("xlsx_to_csv.py", xlsx, latin, "--sheet", "Import",
205
+ "--encoding", "utf-8-sig")
206
+ assert latin.read_bytes().startswith(b"\xef\xbb\xbf")
207
+
208
+
209
+ def test_edit_existing(workbook, tmp_path):
210
+ edited = tmp_path / "edited.xlsx"
211
+ proc = run("xlsx_edit.py", workbook, "--sheet", "Notes",
212
+ "--out", edited,
213
+ "--copy-sheet", "Notes:Backup",
214
+ "--rename-sheet", "Data:Main",
215
+ "--set", "B1=Änderung",
216
+ "--set", "C1=99.5",
217
+ "--set", "D1=2026-12-24",
218
+ "--set", "E1==SUM(C1:C1)",
219
+ "--append", '["appended", 1, false]',
220
+ "--insert-rows", "1:1",
221
+ "--recalc")
222
+ result = json.loads(proc.stdout)
223
+ assert result["ok"]
224
+
225
+ wb = load_workbook(edited)
226
+ assert set(wb.sheetnames) == {"Main", "Notes", "Backup"}
227
+ ws = wb["Notes"]
228
+ # insert-rows ran before --set per documented order, so row 1 is blank
229
+ # and original data moved to row 2... check documented ordering:
230
+ # structural ops run before --set, so B1 etc. were written after insert.
231
+ assert ws["B1"].value == "Änderung"
232
+ assert ws["C1"].value == 99.5
233
+ assert ws["D1"].value.date() == date(2026, 12, 24)
234
+ assert ws["E1"].value == "=SUM(C1:C1)"
235
+ assert wb.calculation.fullCalcOnLoad is True
236
+ # appended row present
237
+ found = [r for r in ws.iter_rows(values_only=True)
238
+ if r and r[0] == "appended"]
239
+ assert found and found[0][1] == 1 and found[0][2] is False
240
+ # copy preserved data
241
+ assert wb["Backup"]["A1"].value == "Zürich"
242
+
243
+
244
+ def test_help_and_errors():
245
+ for script in ["xlsx_create.py", "xlsx_read.py", "xlsx_edit.py",
246
+ "csv_to_xlsx.py", "xlsx_to_csv.py",
247
+ "xlsx_restructure.py", "xlsx_recalc.py"]:
248
+ proc = run(script, "--help")
249
+ assert "usage" in proc.stdout.lower()
250
+ bad = run("xlsx_read.py", "/nonexistent.xlsx", "--sheets",
251
+ expect_ok=False)
252
+ assert bad.returncode != 0
253
+ assert json.loads(bad.stderr)["ok"] is False
254
+
255
+
256
+ # ---------------------------------------------------------------------------
257
+ # Reference-aware restructuring (xlsx_restructure.py)
258
+ # ---------------------------------------------------------------------------
259
+
260
+ RESTRUCTURE_SPEC = {
261
+ "defined_names": {"SalesRange": "'Data'!$B$2:$B$4"},
262
+ "sheets": [
263
+ {
264
+ "name": "Data",
265
+ "rows": [
266
+ ["Region", "Sales", "Weight"],
267
+ ["North", 100, 0.5],
268
+ ["South", 200, 0.3],
269
+ ["East", 300, 0.2],
270
+ [None, None, None],
271
+ ["Total", None, None],
272
+ ],
273
+ "cells": {
274
+ "B6": {"formula": "SUM(B2:B4)"},
275
+ "C6": {"formula": "$B$2*C2"},
276
+ "D6": {"formula": "LOG10(B4)"},
277
+ "E6": {"formula": "SUM(B:B)"},
278
+ "F6": {"formula": '"row B2: "&B2'},
279
+ },
280
+ "merges": ["E2:E4", "A7:B7"],
281
+ "freeze_panes": "A2",
282
+ "autofilter": "A1:C4",
283
+ "conditional_formats": [
284
+ {"range": "B2:B4", "type": "cell_is",
285
+ "operator": "greaterThan", "formula": ["150"],
286
+ "fill": "C6EFCE"},
287
+ ],
288
+ "validations": [
289
+ {"range": "C2:C4", "type": "list",
290
+ "formula1": '"0.2,0.3,0.5"'},
291
+ ],
292
+ "tables": [
293
+ {"name": "SalesTbl", "range": "A1:C4"},
294
+ ],
295
+ },
296
+ {
297
+ "name": "Summary",
298
+ "rows": [["Grand total"]],
299
+ "cells": {
300
+ "B1": {"formula": "SUM(Data!B2:B4)"},
301
+ "B2": {"formula": "'Data'!$B$3"},
302
+ "B3": {"formula": "SUM(A1:A1)"},
303
+ },
304
+ },
305
+ ],
306
+ }
307
+
308
+
309
+ @pytest.fixture
310
+ def restructure_book(tmp_path):
311
+ spec_path = tmp_path / "rspec.json"
312
+ spec_path.write_text(json.dumps(RESTRUCTURE_SPEC), encoding="utf-8")
313
+ out = tmp_path / "restructure.xlsx"
314
+ run("xlsx_create.py", spec_path, out)
315
+ return out
316
+
317
+
318
+ def test_restructure_insert_rows_shifts_everything(restructure_book):
319
+ # merge A6:C6 gets pushed down; A1:A1 merge is before the insert point
320
+ proc = run("xlsx_restructure.py", restructure_book,
321
+ "--sheet", "Data", "--insert-rows", "3:2")
322
+ report = json.loads(proc.stdout)
323
+ assert report["ok"] and report["op"] == "insert"
324
+
325
+ wb = load_workbook(restructure_book)
326
+ data, summary = wb["Data"], wb["Summary"]
327
+ # values physically moved
328
+ assert data["A2"].value == "North"
329
+ assert data["A5"].value == "South" # was row 3
330
+ assert data["A8"].value == "Total" # was row 6
331
+ # same-sheet formulas rewritten (range expanded across insert point)
332
+ assert data["B8"].value == "=SUM(B2:B6)"
333
+ # absolute ref before insert point unchanged; relative arm shifted
334
+ assert data["C8"].value == "=$B$2*C2"
335
+ # function names, whole-column refs, string literals untouched
336
+ assert data["D8"].value == "=LOG10(B6)"
337
+ assert data["E8"].value == "=SUM(B:B)"
338
+ assert data["F8"].value == '="row B2: "&B2'
339
+ # cross-sheet formulas on the OTHER sheet rewritten
340
+ assert summary["B1"].value == "=SUM(Data!B2:B6)"
341
+ assert summary["B2"].value == "='Data'!$B$5"
342
+ # Summary-local refs not confused with Data refs
343
+ assert summary["B3"].value == "=SUM(A1:A1)"
344
+ # merges: E2:E4 spans the insert point -> expanded; A7:B7 -> shifted
345
+ merged = [str(r) for r in data.merged_cells.ranges]
346
+ assert "E2:E6" in merged and "A9:B9" in merged
347
+ # autofilter expanded, freeze panes intact
348
+ assert data.auto_filter.ref == "A1:C6"
349
+ assert data.freeze_panes == "A2"
350
+ # validation + conditional format ranges shifted
351
+ dv = data.data_validations.dataValidation[0]
352
+ assert str(dv.sqref) == "C2:C6"
353
+ cf = next(iter(data.conditional_formatting))
354
+ assert str(cf.sqref) == "B2:B6"
355
+ # native table expanded
356
+ assert data.tables["SalesTbl"].ref == "A1:C6"
357
+ # defined name rewritten
358
+ assert wb.defined_names["SalesRange"].attr_text == "'Data'!$B$2:$B$6"
359
+ # report is honest about limits
360
+ assert "chart anchors" in report["not_shifted"]
361
+ assert any(f["cell"] == "B1" and f["sheet"] == "Summary"
362
+ for f in report["formulas"])
363
+
364
+
365
+ def test_restructure_delete_rows_and_ref_errors(restructure_book):
366
+ run("xlsx_restructure.py", restructure_book,
367
+ "--sheet", "Data", "--delete-rows", "3")
368
+ wb = load_workbook(restructure_book)
369
+ data, summary = wb["Data"], wb["Summary"]
370
+ assert data["A3"].value == "East" # South deleted
371
+ assert data["B5"].value == "=SUM(B2:B3)" # range clamped
372
+ # single-cell ref into the deleted row becomes #REF!
373
+ assert summary["B2"].value == "='Data'!#REF!"
374
+ assert summary["B1"].value == "=SUM(Data!B2:B3)"
375
+ assert data.tables["SalesTbl"].ref == "A1:C3"
376
+
377
+
378
+ def test_restructure_insert_cols(restructure_book):
379
+ proc = run("xlsx_restructure.py", restructure_book,
380
+ "--sheet", "Data", "--insert-cols", "B:1")
381
+ report = json.loads(proc.stdout)
382
+ assert report["axis"] == "cols" and report["index"] == 2
383
+ wb = load_workbook(restructure_book)
384
+ data, summary = wb["Data"], wb["Summary"]
385
+ assert data["C2"].value == 100 # Sales moved B->C
386
+ assert data["C6"].value == "=SUM(C2:C4)"
387
+ assert data["D6"].value == "=$C$2*D2"
388
+ assert summary["B1"].value == "=SUM(Data!C2:C4)"
389
+ assert wb.defined_names["SalesRange"].attr_text == "'Data'!$C$2:$C$4"
390
+ merged = [str(r) for r in data.merged_cells.ranges]
391
+ assert "F2:F4" in merged # merge shifted right
392
+ assert "A7:C7" in merged # merge expanded across col B
393
+
394
+
395
+ # ---------------------------------------------------------------------------
396
+ # Tables, defined names, hyperlinks, notes, protection (edit + read paths)
397
+ # ---------------------------------------------------------------------------
398
+
399
+ def test_tables_create_append_list(tmp_path):
400
+ spec = {"sheets": [{"name": "T",
401
+ "rows": [["Item", "Qty"], ["a", 1], ["b", 2]],
402
+ "tables": [{"name": "Stock", "range": "A1:B3",
403
+ "style": "TableStyleLight1"}]}]}
404
+ spec_path = tmp_path / "tspec.json"
405
+ spec_path.write_text(json.dumps(spec), encoding="utf-8")
406
+ book = tmp_path / "tables.xlsx"
407
+ run("xlsx_create.py", spec_path, book)
408
+
409
+ wb = load_workbook(book)
410
+ tbl = wb["T"].tables["Stock"]
411
+ assert tbl.ref == "A1:B3"
412
+ assert tbl.tableStyleInfo.name == "TableStyleLight1"
413
+
414
+ # --add-table + --table-append auto-extends the range
415
+ run("xlsx_edit.py", book, "--sheet", "T",
416
+ "--add-table", "Extra:D1:E2",
417
+ "--table-append", 'Stock=["c", 3]')
418
+ wb = load_workbook(book)
419
+ ws = wb["T"]
420
+ assert ws.tables["Stock"].ref == "A1:B4"
421
+ assert ws["A4"].value == "c" and ws["B4"].value == 3
422
+ assert ws.tables["Extra"].ref == "D1:E2"
423
+
424
+ listing = json.loads(
425
+ run("xlsx_edit.py", book, "--sheet", "T", "--list-tables").stdout)
426
+ assert listing["tables"]["Stock"]["ref"] == "A1:B4"
427
+ assert set(listing["tables"]) == {"Stock", "Extra"}
428
+ # tables also appear in the read inventory
429
+ inv = json.loads(run("xlsx_read.py", book, "--sheets").stdout)
430
+ assert inv["sheets"][0]["tables"]["Stock"] == "A1:B4"
431
+
432
+
433
+ def test_names_hyperlinks_notes(tmp_path):
434
+ spec = {
435
+ "defined_names": {"Rate": "'D'!$B$1"},
436
+ "sheets": [{"name": "D", "cells": {
437
+ "A1": {"value": "docs",
438
+ "hyperlink": "https://example.com/docs"},
439
+ "B1": {"value": 0.07, "note": "quarterly rate"},
440
+ "C1": {"value": 1, "note": {"text": "check", "author": "QA"}},
441
+ }}],
442
+ }
443
+ spec_path = tmp_path / "nspec.json"
444
+ spec_path.write_text(json.dumps(spec), encoding="utf-8")
445
+ book = tmp_path / "names.xlsx"
446
+ run("xlsx_create.py", spec_path, book)
447
+
448
+ wb = load_workbook(book)
449
+ ws = wb["D"]
450
+ assert ws["A1"].hyperlink.target == "https://example.com/docs"
451
+ assert ws["B1"].comment.text == "quarterly rate"
452
+ assert ws["C1"].comment.author == "QA"
453
+ assert wb.defined_names["Rate"].attr_text == "'D'!$B$1"
454
+
455
+ # edit path: add/delete names, hyperlink, note, clear note
456
+ run("xlsx_edit.py", book, "--sheet", "D",
457
+ "--define-name", "Extra='D'!$C$1",
458
+ "--delete-name", "Rate",
459
+ "--hyperlink", "D1=https://example.com/more|More",
460
+ "--note", "D1=see more|Reviewer",
461
+ "--clear-note", "B1")
462
+ wb = load_workbook(book)
463
+ ws = wb["D"]
464
+ assert "Rate" not in wb.defined_names
465
+ assert wb.defined_names["Extra"].attr_text == "'D'!$C$1"
466
+ assert ws["D1"].hyperlink.target == "https://example.com/more"
467
+ assert ws["D1"].value == "More"
468
+ assert ws["D1"].comment.author == "Reviewer"
469
+ assert ws["B1"].comment is None
470
+
471
+ # read path: --notes and --names JSON output
472
+ notes = json.loads(run("xlsx_read.py", book, "--notes").stdout)["notes"]
473
+ coords = {(n["cell"], n["author"]) for n in notes}
474
+ assert ("D1", "Reviewer") in coords and ("C1", "QA") in coords
475
+ names = json.loads(run("xlsx_read.py", book, "--names").stdout)
476
+ assert names["defined_names"] == {"Extra": "'D'!$C$1"}
477
+
478
+
479
+ def test_sheet_protection(tmp_path):
480
+ spec = {"sheets": [{"name": "P", "rows": [["locked", "open"]],
481
+ "protection": {"password": "your-password",
482
+ "unlock": ["B1:B1"]}}]}
483
+ spec_path = tmp_path / "pspec.json"
484
+ spec_path.write_text(json.dumps(spec), encoding="utf-8")
485
+ book = tmp_path / "prot.xlsx"
486
+ run("xlsx_create.py", spec_path, book)
487
+
488
+ wb = load_workbook(book)
489
+ ws = wb["P"]
490
+ assert ws.protection.sheet is True
491
+ assert ws.protection.password # hash stored
492
+ assert ws["B1"].protection.locked is False
493
+ assert ws["A1"].protection.locked is not False
494
+ inv = json.loads(run("xlsx_read.py", book, "--sheets").stdout)
495
+ assert inv["sheets"][0]["protected"] is True
496
+
497
+ # edit path on a fresh unprotected sheet
498
+ plain = tmp_path / "plain.xlsx"
499
+ spec_path.write_text(json.dumps(
500
+ {"sheets": [{"name": "P", "rows": [["a", "b"]]}]}), encoding="utf-8")
501
+ run("xlsx_create.py", spec_path, plain)
502
+ run("xlsx_edit.py", plain, "--sheet", "P",
503
+ "--protect", "your-password", "--unlock", "B1:B1")
504
+ ws = load_workbook(plain)["P"]
505
+ assert ws.protection.sheet is True and ws["B1"].protection.locked is False
506
+
507
+
508
+ # ---------------------------------------------------------------------------
509
+ # Headless recalculation (xlsx_recalc.py) — branches on soffice presence
510
+ # ---------------------------------------------------------------------------
511
+
512
+ def test_recalc_reports_json_both_ways(tmp_path):
513
+ spec = {"sheets": [{"name": "R", "rows": [[2], [3]],
514
+ "cells": {"A3": {"formula": "SUM(A1:A2)"}}}]}
515
+ spec_path = tmp_path / "cspec.json"
516
+ spec_path.write_text(json.dumps(spec), encoding="utf-8")
517
+ book = tmp_path / "calc.xlsx"
518
+ run("xlsx_create.py", spec_path, book)
519
+
520
+ # absent-soffice branch is always testable by hiding PATH
521
+ env = dict(os.environ, LC_ALL="C", LANG="C", PATH=str(tmp_path), SOFFICE="")
522
+ proc = subprocess.run(
523
+ [sys.executable, str(SCRIPTS / "xlsx_recalc.py"), str(book)],
524
+ capture_output=True, text=True, env=env, encoding="utf-8", check=False)
525
+ assert proc.returncode == 0
526
+ absent = json.loads(proc.stdout)
527
+ assert absent["recalculated"] is False and "soffice" in absent["reason"]
528
+ assert "guidance" in absent
529
+
530
+ sys.path.insert(0, str(SCRIPTS))
531
+ from render import find_soffice
532
+ if not find_soffice():
533
+ pytest.skip("LibreOffice not installed; absent branch covered above")
534
+
535
+ out = tmp_path / "calced.xlsx"
536
+ proc = run("xlsx_recalc.py", book, "--out", out, "--timeout", "300")
537
+ result = json.loads(proc.stdout)
538
+ assert result["recalculated"] is True
539
+ assert result["formula_cells"] == 1
540
+ assert result["with_cached_values"] == 1
541
+ # cached value now visible to --formulas
542
+ formulas = json.loads(run("xlsx_read.py", out, "--formulas").stdout)
543
+ entry = formulas["formulas"][0]
544
+ assert entry["formula"] == "=SUM(A1:A2)" and entry["cached"] == 5
@@ -0,0 +1,36 @@
1
+ ---
2
+ id: "create_goal"
3
+ name: "Create Goal"
4
+ description: "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks. Set token_budget only when an explicit positive token budget is requested; omit it or pass null for an unlimited/no-budget goal. Fails if a goal exists; use update_goal only for status."
5
+ category: "system"
6
+ version: "1.0.0"
7
+ supports_background: false
8
+ always_load: true
9
+ input_schema:
10
+ type: object
11
+ properties:
12
+ objective:
13
+ type: string
14
+ description: "Required. The concrete objective to start pursuing. This starts a new active goal only when no goal is currently defined; if a goal already exists, this tool fails."
15
+ token_budget:
16
+ type:
17
+ - integer
18
+ - "null"
19
+ description: "Optional positive token budget for the new active goal. Set only when the user explicitly requested a budget; null or omission means unlimited/no token budget."
20
+ required:
21
+ - objective
22
+ is_read_only: false
23
+ is_concurrency_safe: false
24
+ ---
25
+
26
+ # Create Goal
27
+
28
+ Start a persisted goal only when the user explicitly asks for goal mode.
29
+
30
+ ## Budget contract
31
+
32
+ - Omit `token_budget`, or pass `null`, for an unlimited goal with no token budget.
33
+ - Pass a positive integer only when the user explicitly requested that budget.
34
+ - Never invent a default token budget.
35
+
36
+ The runtime implementation and schema are authoritative. A Rust consistency test compares this declaration with the runtime registration so the documentation cannot silently drift.