qubicl-cli 0.1.0-dev.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +46 -21
  2. package/dist/SBOM.spdx.json +222 -0
  3. package/dist/THIRD_PARTY_NOTICES.txt +244 -0
  4. package/dist/assets/chromium-seccomp.json +181 -0
  5. package/dist/assets/computer/BROWSER_SKILLS_THIRD_PARTY_NOTICES.txt +23 -0
  6. package/dist/assets/computer/Dockerfile +191 -49
  7. package/dist/assets/computer/PLAYWRIGHT_THIRD_PARTY_NOTICES.txt +217 -0
  8. package/dist/assets/computer/SKILLS_THIRD_PARTY_NOTICES.txt +29 -0
  9. package/dist/assets/computer/THIRD_PARTY_NOTICES.txt +28 -0
  10. package/dist/assets/computer/WEB_THIRD_PARTY_NOTICES.txt +42 -0
  11. package/dist/assets/computer/browser-skills-requirements.txt +12 -0
  12. package/dist/assets/computer/chromium-wrapper.sh +12 -0
  13. package/dist/assets/computer/control.mjs +112 -52
  14. package/dist/assets/computer/entrypoint.sh +272 -35
  15. package/dist/assets/computer/libreoffice-registrymodifications.xcu +8 -0
  16. package/dist/assets/computer/manifests/browser.json +72 -0
  17. package/dist/assets/computer/manifests/computer.json +76 -0
  18. package/dist/assets/computer/manifests/file-system.json +42 -0
  19. package/dist/assets/computer/manifests/workstation.json +78 -0
  20. package/dist/assets/computer/node_modules/playwright-core/LICENSE +202 -0
  21. package/dist/assets/computer/node_modules/playwright-core/NOTICE +5 -0
  22. package/dist/assets/computer/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
  23. package/dist/assets/computer/node_modules/playwright-core/browsers.json +75 -0
  24. package/dist/assets/computer/node_modules/playwright-core/index.js +17 -0
  25. package/dist/assets/computer/node_modules/playwright-core/index.mjs +28 -0
  26. package/dist/assets/computer/node_modules/playwright-core/lib/bootstrap.js +88 -0
  27. package/dist/assets/computer/node_modules/playwright-core/lib/coreBundle.js +74830 -0
  28. package/dist/assets/computer/node_modules/playwright-core/lib/utilsBundle.js +90764 -0
  29. package/dist/assets/computer/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +2179 -0
  30. package/dist/assets/computer/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  31. package/dist/assets/computer/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  32. package/dist/assets/computer/node_modules/playwright-core/lib/xdg-open +1267 -0
  33. package/dist/assets/computer/node_modules/playwright-core/package.json +34 -0
  34. package/dist/assets/computer/skills/core/docx/LICENSE +21 -0
  35. package/dist/assets/computer/skills/core/docx/SKILL.md +21 -0
  36. package/dist/assets/computer/skills/core/docx/references/revisions-and-comments.md +88 -0
  37. package/dist/assets/computer/skills/core/docx/scripts/docx_comments.py +289 -0
  38. package/dist/assets/computer/skills/core/docx/scripts/docx_common.py +94 -0
  39. package/dist/assets/computer/skills/core/docx/scripts/docx_create.py +177 -0
  40. package/dist/assets/computer/skills/core/docx/scripts/docx_edit.py +250 -0
  41. package/dist/assets/computer/skills/core/docx/scripts/docx_read.py +149 -0
  42. package/dist/assets/computer/skills/core/docx/scripts/docx_revisions.py +147 -0
  43. package/dist/assets/computer/skills/core/docx/scripts/docx_template.py +70 -0
  44. package/dist/assets/computer/skills/core/docx/scripts/docx_validate.py +156 -0
  45. package/dist/assets/computer/skills/core/ocr-and-documents/LICENSE +21 -0
  46. package/dist/assets/computer/skills/core/ocr-and-documents/SKILL.md +24 -0
  47. package/dist/assets/computer/skills/core/ocr-and-documents/scripts/ocr_document.py +132 -0
  48. package/dist/assets/computer/skills/core/pdf/LICENSE +21 -0
  49. package/dist/assets/computer/skills/core/pdf/SKILL.md +25 -0
  50. package/dist/assets/computer/skills/core/pdf/references/forms.md +100 -0
  51. package/dist/assets/computer/skills/core/pdf/scripts/_raster.py +76 -0
  52. package/dist/assets/computer/skills/core/pdf/scripts/pdf_create.py +130 -0
  53. package/dist/assets/computer/skills/core/pdf/scripts/pdf_fill_form.py +97 -0
  54. package/dist/assets/computer/skills/core/pdf/scripts/pdf_form_layout.py +168 -0
  55. package/dist/assets/computer/skills/core/pdf/scripts/pdf_make_form.py +145 -0
  56. package/dist/assets/computer/skills/core/pdf/scripts/pdf_merge.py +50 -0
  57. package/dist/assets/computer/skills/core/pdf/scripts/pdf_meta.py +115 -0
  58. package/dist/assets/computer/skills/core/pdf/scripts/pdf_page_image.py +99 -0
  59. package/dist/assets/computer/skills/core/pdf/scripts/pdf_read.py +153 -0
  60. package/dist/assets/computer/skills/core/pdf/scripts/pdf_secure.py +71 -0
  61. package/dist/assets/computer/skills/core/pdf/scripts/pdf_split.py +84 -0
  62. package/dist/assets/computer/skills/core/pdf/scripts/pdf_stamp.py +143 -0
  63. package/dist/assets/computer/skills/core/pdf/scripts/pdf_watermark.py +51 -0
  64. package/dist/assets/computer/skills/core/plan/LICENSE +21 -0
  65. package/dist/assets/computer/skills/core/plan/SKILL.md +21 -0
  66. package/dist/assets/computer/skills/core/powerpoint/LICENSE +21 -0
  67. package/dist/assets/computer/skills/core/powerpoint/SKILL.md +18 -0
  68. package/dist/assets/computer/skills/core/powerpoint/scripts/pptx_create.py +214 -0
  69. package/dist/assets/computer/skills/core/powerpoint/scripts/pptx_edit.py +436 -0
  70. package/dist/assets/computer/skills/core/powerpoint/scripts/pptx_from_template.py +88 -0
  71. package/dist/assets/computer/skills/core/powerpoint/scripts/pptx_read.py +131 -0
  72. package/dist/assets/computer/skills/core/powerpoint/scripts/pptx_render.py +93 -0
  73. package/dist/assets/computer/skills/core/xlsx/LICENSE +21 -0
  74. package/dist/assets/computer/skills/core/xlsx/SKILL.md +22 -0
  75. package/dist/assets/computer/skills/core/xlsx/references/restructuring.md +71 -0
  76. package/dist/assets/computer/skills/core/xlsx/scripts/csv_to_xlsx.py +104 -0
  77. package/dist/assets/computer/skills/core/xlsx/scripts/xlsx_create.py +259 -0
  78. package/dist/assets/computer/skills/core/xlsx/scripts/xlsx_edit.py +263 -0
  79. package/dist/assets/computer/skills/core/xlsx/scripts/xlsx_read.py +160 -0
  80. package/dist/assets/computer/skills/core/xlsx/scripts/xlsx_recalc.py +110 -0
  81. package/dist/assets/computer/skills/core/xlsx/scripts/xlsx_restructure.py +337 -0
  82. package/dist/assets/computer/skills/core/xlsx/scripts/xlsx_to_csv.py +69 -0
  83. package/dist/assets/computer/skills/core-catalog.json +246 -0
  84. package/dist/assets/computer/skills-requirements.txt +18 -0
  85. package/dist/assets/computer/web-provider.py +592 -0
  86. package/dist/assets/computer/web-requirements.txt +37 -0
  87. package/dist/assets/gateway/Dockerfile +3 -1
  88. package/dist/assets/gateway/gateway.mjs +69 -40
  89. package/dist/assets/image-catalog.json +183 -0
  90. package/dist/qubicl.mjs +297 -167
  91. package/package.json +7 -7
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env python3
2
+ """Reference-aware row/column insert and delete for .xlsx workbooks.
3
+
4
+ Unlike plain openpyxl insert_rows/delete_cols (and xlsx_edit.py's thin
5
+ wrappers), this script also rewrites everything that points at the moved
6
+ cells:
7
+
8
+ * formula references in ALL sheets, including absolute refs ($B$2),
9
+ ranges (B2:B9), and cross-sheet refs ('My Sheet'!A1 / Data!$B$8).
10
+ References into a deleted region become #REF!.
11
+ * merged-cell ranges (shifted; expanded when they span the insertion
12
+ point; removed when fully deleted)
13
+ * autofilter range, freeze panes, data-validation ranges,
14
+ conditional-formatting applied ranges (sqref)
15
+ * native table (ListObject) refs on the edited sheet
16
+ * workbook-scope defined names that point at the edited sheet
17
+ * row heights / column widths
18
+
19
+ It prints a JSON report of every rewrite it made and lists what it
20
+ could NOT shift (chart anchors, images, conditional-format RULE
21
+ formulas). Full rules and limits: references/restructuring.md.
22
+
23
+ One structural operation per invocation:
24
+
25
+ Usage:
26
+ xlsx_restructure.py book.xlsx --sheet Data --insert-rows 3:2
27
+ xlsx_restructure.py book.xlsx --sheet Data --delete-rows 5
28
+ xlsx_restructure.py book.xlsx --sheet Data --insert-cols B:1 --out new.xlsx
29
+ xlsx_restructure.py book.xlsx --sheet Data --delete-cols 4:2
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import json
35
+ import re
36
+ import sys
37
+
38
+ from openpyxl import load_workbook
39
+ from openpyxl.formatting.formatting import ConditionalFormattingList
40
+ from openpyxl.utils import (column_index_from_string, get_column_letter,
41
+ range_boundaries)
42
+
43
+ # A1-style reference, optionally sheet-qualified, optionally a range.
44
+ # Guards: not preceded by a word char/$/. (avoids ABC123 identifiers) and
45
+ # not followed by a word char or "(" (avoids function names like LOG10().
46
+ REF_RE = re.compile(
47
+ r"(?<![\w$.:])"
48
+ r"(?P<sheet>(?:'(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!)?"
49
+ r"(?P<start>\$?[A-Za-z]{1,3}\$?[0-9]{1,7})"
50
+ r"(?::(?P<end>\$?[A-Za-z]{1,3}\$?[0-9]{1,7}))?"
51
+ r"(?![\w(])")
52
+ STRING_RE = re.compile(r'"(?:[^"]|"")*"')
53
+ COORD_RE = re.compile(r"^(\$?)([A-Za-z]{1,3})(\$?)([0-9]+)$")
54
+
55
+
56
+ def shift_point(v, idx, n, delete):
57
+ """New 1-based index for a single row/col, or None if deleted."""
58
+ if delete:
59
+ if v < idx:
60
+ return v
61
+ if v >= idx + n:
62
+ return v - n
63
+ return None
64
+ return v + n if v >= idx else v
65
+
66
+
67
+ def shift_span(a, b, idx, n, delete):
68
+ """New (start, end) for an inclusive span, or None if fully deleted."""
69
+ if delete:
70
+ na = a if a < idx else (a - n if a >= idx + n else idx)
71
+ nb = b if b < idx else (b - n if b >= idx + n else idx - 1)
72
+ return None if na > nb else (na, nb)
73
+ return (a + n if a >= idx else a, b + n if b >= idx else b)
74
+
75
+
76
+ def shift_range(rng, axis, idx, n, delete):
77
+ """Shift an A1 range string (no sheet prefix). None = fully deleted."""
78
+ min_col, min_row, max_col, max_row = range_boundaries(rng)
79
+ if axis == "rows":
80
+ span = shift_span(min_row, max_row, idx, n, delete)
81
+ if span is None:
82
+ return None
83
+ min_row, max_row = span
84
+ else:
85
+ span = shift_span(min_col, max_col, idx, n, delete)
86
+ if span is None:
87
+ return None
88
+ min_col, max_col = span
89
+ start = f"{get_column_letter(min_col)}{min_row}"
90
+ end = f"{get_column_letter(max_col)}{max_row}"
91
+ return start if start == end and ":" not in rng else f"{start}:{end}"
92
+
93
+
94
+ class RefRewriter:
95
+ """Rewrite A1 references in formula-like text for one shift op."""
96
+
97
+ def __init__(self, target_sheet, axis, idx, n, delete):
98
+ self.target = target_sheet.lower()
99
+ self.axis, self.idx, self.n, self.delete = axis, idx, n, delete
100
+
101
+ def _shift_coord(self, coord):
102
+ m = COORD_RE.match(coord)
103
+ col_abs, col, row_abs, row = m.groups()
104
+ ci, ri = column_index_from_string(col.upper()), int(row)
105
+ if self.axis == "rows":
106
+ ri = shift_point(ri, self.idx, self.n, self.delete)
107
+ if ri is None:
108
+ return None
109
+ else:
110
+ ci = shift_point(ci, self.idx, self.n, self.delete)
111
+ if ci is None:
112
+ return None
113
+ return f"{col_abs}{get_column_letter(ci)}{row_abs}{ri}"
114
+
115
+ def _shift_pair(self, start, end):
116
+ """Shift a range preserving $ flags; None = collapsed to #REF!."""
117
+ new_start = self._shift_coord(start)
118
+ new_end = self._shift_coord(end)
119
+ if new_start is None or new_end is None:
120
+ # spans may survive partial deletion: clamp via span math
121
+ s, e = COORD_RE.match(start), COORD_RE.match(end)
122
+ if self.axis == "rows":
123
+ span = shift_span(int(s.group(4)), int(e.group(4)),
124
+ self.idx, self.n, self.delete)
125
+ if span is None:
126
+ return None
127
+ new_start = f"{s.group(1)}{s.group(2)}{s.group(3)}{span[0]}"
128
+ new_end = f"{e.group(1)}{e.group(2)}{e.group(3)}{span[1]}"
129
+ else:
130
+ span = shift_span(column_index_from_string(s.group(2).upper()),
131
+ column_index_from_string(e.group(2).upper()),
132
+ self.idx, self.n, self.delete)
133
+ if span is None:
134
+ return None
135
+ new_start = (f"{s.group(1)}{get_column_letter(span[0])}"
136
+ f"{s.group(3)}{s.group(4)}")
137
+ new_end = (f"{e.group(1)}{get_column_letter(span[1])}"
138
+ f"{e.group(3)}{e.group(4)}")
139
+ return new_start, new_end
140
+
141
+ def _sub(self, match, home_sheet):
142
+ prefix = match.group("sheet") or ""
143
+ if prefix:
144
+ name = prefix[:-1]
145
+ if name.startswith("'"):
146
+ name = name[1:-1].replace("''", "'")
147
+ ref_sheet = name
148
+ else:
149
+ ref_sheet = home_sheet
150
+ if ref_sheet.lower() != self.target:
151
+ return match.group(0)
152
+ start, end = match.group("start"), match.group("end")
153
+ if end is None:
154
+ new = self._shift_coord(start)
155
+ return prefix + ("#REF!" if new is None else new)
156
+ pair = self._shift_pair(start, end)
157
+ return (prefix + "#REF!" if pair is None
158
+ else f"{prefix}{pair[0]}:{pair[1]}")
159
+
160
+ def rewrite(self, text, home_sheet):
161
+ """Rewrite refs outside quoted string literals. Returns new text."""
162
+ out, pos = [], 0
163
+ for lit in STRING_RE.finditer(text):
164
+ out.append(REF_RE.sub(lambda m: self._sub(m, home_sheet),
165
+ text[pos:lit.start()]))
166
+ out.append(lit.group(0))
167
+ pos = lit.end()
168
+ out.append(REF_RE.sub(lambda m: self._sub(m, home_sheet), text[pos:]))
169
+ return "".join(out)
170
+
171
+
172
+ def shift_dimensions(dims, idx, n, delete, is_row):
173
+ """Rebuild a row/column dimensions map with shifted keys."""
174
+ items = list(dims.items())
175
+ saved = {}
176
+ for key, dim in items:
177
+ pos = key if is_row else column_index_from_string(key)
178
+ new = shift_point(pos, idx, n, delete)
179
+ if new is not None and new != pos:
180
+ saved[new if is_row else get_column_letter(new)] = dim
181
+ del dims[key]
182
+ for key, dim in saved.items():
183
+ if is_row:
184
+ dim.index = key
185
+ else:
186
+ dim.index = column_index_from_string(key)
187
+ dims[key] = dim
188
+ return len(saved)
189
+
190
+
191
+ def main(argv=None):
192
+ ap = argparse.ArgumentParser(
193
+ description="Insert/delete rows or columns AND rewrite formula "
194
+ "references, merges, filters, validations, tables, and "
195
+ "defined names to match.",
196
+ epilog="Cannot shift: chart anchors, images, conditional-format "
197
+ "rule formulas. See references/restructuring.md.")
198
+ ap.add_argument("file", help="path to .xlsx file")
199
+ ap.add_argument("--sheet", help="target sheet (default: active)")
200
+ ap.add_argument("--out", help="output path (default: edit in place)")
201
+ op = ap.add_mutually_exclusive_group(required=True)
202
+ op.add_argument("--insert-rows", metavar="IDX[:N]")
203
+ op.add_argument("--delete-rows", metavar="IDX[:N]")
204
+ op.add_argument("--insert-cols", metavar="COL[:N]",
205
+ help="COL is a letter (B) or 1-based number")
206
+ op.add_argument("--delete-cols", metavar="COL[:N]")
207
+ args = ap.parse_args(argv)
208
+
209
+ raw = (args.insert_rows or args.delete_rows
210
+ or args.insert_cols or args.delete_cols)
211
+ idx_s, _, n_s = raw.partition(":")
212
+ n = int(n_s) if n_s else 1
213
+ axis = "rows" if (args.insert_rows or args.delete_rows) else "cols"
214
+ delete = bool(args.delete_rows or args.delete_cols)
215
+ if axis == "cols" and idx_s.isalpha():
216
+ idx = column_index_from_string(idx_s.upper())
217
+ else:
218
+ idx = int(idx_s)
219
+
220
+ wb = load_workbook(args.file)
221
+ ws = wb[args.sheet] if args.sheet else wb.active
222
+ rewriter = RefRewriter(ws.title, axis, idx, n, delete)
223
+ report = {"ok": True, "sheet": ws.title, "axis": axis,
224
+ "op": "delete" if delete else "insert", "index": idx, "count": n,
225
+ "formulas": [], "merges": [], "tables": {}, "defined_names": {},
226
+ "validations": [], "conditional_formats": [],
227
+ "not_shifted": ["chart anchors", "images",
228
+ "conditional-format rule formulas"]}
229
+
230
+ # 1. capture merge ranges (openpyxl does not move them), then unmerge
231
+ old_merges = [str(r) for r in list(ws.merged_cells.ranges)]
232
+ for rng in old_merges:
233
+ ws.unmerge_cells(rng)
234
+
235
+ # 2. structural move of cell values/styles/comments
236
+ getattr(ws, f"{report['op']}_{axis}")(idx, n)
237
+
238
+ # 3. formulas everywhere
239
+ for sheet in wb.worksheets:
240
+ for row in sheet.iter_rows():
241
+ for cell in row:
242
+ if isinstance(cell.value, str) and cell.value.startswith("="):
243
+ new = rewriter.rewrite(cell.value, sheet.title)
244
+ if new != cell.value:
245
+ report["formulas"].append(
246
+ {"sheet": sheet.title, "cell": cell.coordinate,
247
+ "from": cell.value, "to": new})
248
+ cell.value = new
249
+
250
+ # 4. merges back, shifted
251
+ for rng in old_merges:
252
+ new = shift_range(rng, axis, idx, n, delete)
253
+ if new is None:
254
+ report["merges"].append({"from": rng, "to": None})
255
+ else:
256
+ ws.merge_cells(new)
257
+ if new != rng:
258
+ report["merges"].append({"from": rng, "to": new})
259
+
260
+ # 5. autofilter + freeze panes
261
+ if ws.auto_filter.ref:
262
+ new = shift_range(ws.auto_filter.ref, axis, idx, n, delete)
263
+ if new != ws.auto_filter.ref:
264
+ report["autofilter"] = {"from": ws.auto_filter.ref, "to": new}
265
+ ws.auto_filter.ref = new
266
+ if ws.freeze_panes:
267
+ m = COORD_RE.match(ws.freeze_panes)
268
+ ci = column_index_from_string(m.group(2).upper())
269
+ ri = int(m.group(4))
270
+ if axis == "rows":
271
+ ri = shift_point(ri, idx, n, delete) or max(idx, 2)
272
+ else:
273
+ ci = shift_point(ci, idx, n, delete) or max(idx, 2)
274
+ new = f"{get_column_letter(ci)}{ri}"
275
+ if new != ws.freeze_panes:
276
+ report["freeze_panes"] = {"from": ws.freeze_panes, "to": new}
277
+ ws.freeze_panes = new
278
+
279
+ # 6. data validations + conditional formatting applied ranges
280
+ for dv in ws.data_validations.dataValidation:
281
+ old = str(dv.sqref)
282
+ parts = [shift_range(p, axis, idx, n, delete) for p in old.split()]
283
+ parts = [p for p in parts if p]
284
+ if parts and " ".join(parts) != old:
285
+ dv.sqref = " ".join(parts)
286
+ report["validations"].append({"from": old, "to": str(dv.sqref)})
287
+ new_cf = ConditionalFormattingList()
288
+ for cf in ws.conditional_formatting:
289
+ old = str(cf.sqref)
290
+ parts = [shift_range(p, axis, idx, n, delete) for p in old.split()]
291
+ parts = [p for p in parts if p]
292
+ if not parts:
293
+ report["conditional_formats"].append({"from": old, "to": None})
294
+ continue
295
+ new = " ".join(parts)
296
+ for rule in cf.rules:
297
+ new_cf.add(new, rule)
298
+ if new != old:
299
+ report["conditional_formats"].append({"from": old, "to": new})
300
+ ws.conditional_formatting = new_cf
301
+
302
+ # 7. native tables on the edited sheet
303
+ for table in ws.tables.values():
304
+ new = shift_range(table.ref, axis, idx, n, delete)
305
+ if new and new != table.ref:
306
+ report["tables"][table.displayName] = {"from": table.ref,
307
+ "to": new}
308
+ table.ref = new
309
+
310
+ # 8. workbook-scope defined names
311
+ for name, dn in wb.defined_names.items():
312
+ if dn.attr_text and "!" in dn.attr_text:
313
+ new = rewriter.rewrite(dn.attr_text, ws.title)
314
+ if new != dn.attr_text:
315
+ report["defined_names"][name] = {"from": dn.attr_text,
316
+ "to": new}
317
+ dn.attr_text = new
318
+
319
+ # 9. row heights / column widths
320
+ if axis == "rows":
321
+ shift_dimensions(ws.row_dimensions, idx, n, delete, is_row=True)
322
+ else:
323
+ shift_dimensions(ws.column_dimensions, idx, n, delete, is_row=False)
324
+
325
+ out = args.out or args.file
326
+ wb.save(out)
327
+ report["output"] = out
328
+ print(json.dumps(report, ensure_ascii=False, indent=2))
329
+ return 0
330
+
331
+
332
+ if __name__ == "__main__":
333
+ try:
334
+ sys.exit(main())
335
+ except Exception as exc: # noqa: BLE001
336
+ print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
337
+ sys.exit(1)
@@ -0,0 +1,69 @@
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
+ ap = argparse.ArgumentParser(description=".xlsx sheet -> CSV exporter.")
39
+ ap.add_argument("file", help="input .xlsx path")
40
+ ap.add_argument("output", help="output CSV path")
41
+ ap.add_argument("--sheet", help="sheet name (default: active)")
42
+ ap.add_argument("--encoding", default="utf-8",
43
+ help="CSV output encoding (default utf-8)")
44
+ ap.add_argument("--delimiter", default=",")
45
+ ap.add_argument("--data-only", action="store_true",
46
+ help="cached formula results instead of formula strings")
47
+ args = ap.parse_args(argv)
48
+
49
+ wb = load_workbook(args.file, data_only=args.data_only)
50
+ ws = wb[args.sheet] if args.sheet else wb.active
51
+
52
+ with open(args.output, "w", newline="", encoding=args.encoding) as fh:
53
+ writer = csv.writer(fh, delimiter=args.delimiter)
54
+ count = 0
55
+ for row in ws.iter_rows(values_only=True):
56
+ writer.writerow([to_text(v) for v in row])
57
+ count += 1
58
+
59
+ print(json.dumps({"ok": True, "output": args.output, "sheet": ws.title,
60
+ "rows": count}, ensure_ascii=False))
61
+ return 0
62
+
63
+
64
+ if __name__ == "__main__":
65
+ try:
66
+ sys.exit(main())
67
+ except Exception as exc: # noqa: BLE001
68
+ print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
69
+ sys.exit(1)
@@ -0,0 +1,246 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "source": {
4
+ "owner": "Qubicl",
5
+ "license": "MIT",
6
+ "upstream": {
7
+ "repository": "https://github.com/NousResearch/hermes-agent",
8
+ "commit": "7d6db4efb885856078e4d19f804035226df81e0d"
9
+ }
10
+ },
11
+ "review": {
12
+ "reviewedAt": "2026-08-23T00:00:00Z",
13
+ "reviewedBy": "Qubicl maintainers",
14
+ "sourceAudit": "skills/PROVENANCE.md"
15
+ },
16
+ "skills": [
17
+ {
18
+ "id": "qubicl-core/plan",
19
+ "name": "plan",
20
+ "description": "Create a concrete, reviewable implementation or project plan without performing the planned changes. Use for architecture proposals, migration plans, release plans, debugging strategies, and any request to plan before acting.",
21
+ "category": "planning",
22
+ "compatiblePresets": [
23
+ "file-system",
24
+ "browser",
25
+ "computer",
26
+ "workstation"
27
+ ],
28
+ "requiredTools": [
29
+ "skills_list",
30
+ "skill_view",
31
+ "write_file"
32
+ ],
33
+ "requiredCommands": [],
34
+ "license": "MIT",
35
+ "files": [
36
+ "LICENSE",
37
+ "SKILL.md"
38
+ ],
39
+ "sha256": "51952d974e289bb48477a3c2bf52dee60f76f9b01577476979a106b65141ac9c",
40
+ "security": {
41
+ "scannerVersion": "qubicl-content-security-v1",
42
+ "verdict": "no-blocking-findings",
43
+ "findings": []
44
+ }
45
+ },
46
+ {
47
+ "id": "qubicl-core/pdf",
48
+ "name": "pdf",
49
+ "description": "Create, inspect, modify, render, merge, split, fill, watermark, secure, and validate PDF files using Qubicl's pinned local document tools. Use whenever a task substantially involves PDF input or output.",
50
+ "category": "documents",
51
+ "compatiblePresets": [
52
+ "browser",
53
+ "computer",
54
+ "workstation"
55
+ ],
56
+ "requiredTools": [
57
+ "skills_list",
58
+ "skill_view",
59
+ "exec_command",
60
+ "read_file",
61
+ "write_file"
62
+ ],
63
+ "requiredCommands": [
64
+ "python3",
65
+ "pdftoppm"
66
+ ],
67
+ "license": "MIT",
68
+ "files": [
69
+ "LICENSE",
70
+ "SKILL.md",
71
+ "references/forms.md",
72
+ "scripts/_raster.py",
73
+ "scripts/pdf_create.py",
74
+ "scripts/pdf_fill_form.py",
75
+ "scripts/pdf_form_layout.py",
76
+ "scripts/pdf_make_form.py",
77
+ "scripts/pdf_merge.py",
78
+ "scripts/pdf_meta.py",
79
+ "scripts/pdf_page_image.py",
80
+ "scripts/pdf_read.py",
81
+ "scripts/pdf_secure.py",
82
+ "scripts/pdf_split.py",
83
+ "scripts/pdf_stamp.py",
84
+ "scripts/pdf_watermark.py"
85
+ ],
86
+ "sha256": "f6e414154b0ee84a2bd5c3af2ac079a7f21b50aceb5460a461776231eb2e21b8",
87
+ "security": {
88
+ "scannerVersion": "qubicl-content-security-v1",
89
+ "verdict": "no-blocking-findings",
90
+ "findings": []
91
+ }
92
+ },
93
+ {
94
+ "id": "qubicl-core/docx",
95
+ "name": "docx",
96
+ "description": "Create, read, edit, template, validate, and review Word DOCX documents using Qubicl's pinned local document tools. Use for professional Word documents, tracked revisions, comments, structured edits, and DOCX templates.",
97
+ "category": "documents",
98
+ "compatiblePresets": [
99
+ "computer",
100
+ "workstation"
101
+ ],
102
+ "requiredTools": [
103
+ "skills_list",
104
+ "skill_view",
105
+ "exec_command",
106
+ "read_file",
107
+ "write_file"
108
+ ],
109
+ "requiredCommands": [
110
+ "python3"
111
+ ],
112
+ "license": "MIT",
113
+ "files": [
114
+ "LICENSE",
115
+ "SKILL.md",
116
+ "references/revisions-and-comments.md",
117
+ "scripts/docx_comments.py",
118
+ "scripts/docx_common.py",
119
+ "scripts/docx_create.py",
120
+ "scripts/docx_edit.py",
121
+ "scripts/docx_read.py",
122
+ "scripts/docx_revisions.py",
123
+ "scripts/docx_template.py",
124
+ "scripts/docx_validate.py"
125
+ ],
126
+ "sha256": "fbaf54ee274ceb4b632d6e0b6fe52b63cddc8a1a79ba89a5e5e0c90ffbf08b31",
127
+ "security": {
128
+ "scannerVersion": "qubicl-content-security-v1",
129
+ "verdict": "no-blocking-findings",
130
+ "findings": []
131
+ }
132
+ },
133
+ {
134
+ "id": "qubicl-core/xlsx",
135
+ "name": "xlsx",
136
+ "description": "Create, inspect, edit, restructure, convert, and validate XLSX workbooks and CSV files using Qubicl's pinned local spreadsheet tools. Use for formulas, tables, charts, formatting, workbook structure, and CSV interoperability.",
137
+ "category": "documents",
138
+ "compatiblePresets": [
139
+ "computer",
140
+ "workstation"
141
+ ],
142
+ "requiredTools": [
143
+ "skills_list",
144
+ "skill_view",
145
+ "exec_command",
146
+ "read_file",
147
+ "write_file"
148
+ ],
149
+ "requiredCommands": [
150
+ "python3"
151
+ ],
152
+ "license": "MIT",
153
+ "files": [
154
+ "LICENSE",
155
+ "SKILL.md",
156
+ "references/restructuring.md",
157
+ "scripts/csv_to_xlsx.py",
158
+ "scripts/xlsx_create.py",
159
+ "scripts/xlsx_edit.py",
160
+ "scripts/xlsx_read.py",
161
+ "scripts/xlsx_recalc.py",
162
+ "scripts/xlsx_restructure.py",
163
+ "scripts/xlsx_to_csv.py"
164
+ ],
165
+ "sha256": "ca3c1f61964f0389367662ec28074202c1242f02a503d3a91c3b6ce15e71530d",
166
+ "security": {
167
+ "scannerVersion": "qubicl-content-security-v1",
168
+ "verdict": "no-blocking-findings",
169
+ "findings": []
170
+ }
171
+ },
172
+ {
173
+ "id": "qubicl-core/powerpoint",
174
+ "name": "powerpoint",
175
+ "description": "Create, inspect, edit, template, render, and verify PowerPoint PPTX presentations using Qubicl's pinned local presentation tools. Use for slide decks, speaker notes, charts, images, templates, and visual presentation review.",
176
+ "category": "documents",
177
+ "compatiblePresets": [
178
+ "computer",
179
+ "workstation"
180
+ ],
181
+ "requiredTools": [
182
+ "skills_list",
183
+ "skill_view",
184
+ "exec_command",
185
+ "read_file",
186
+ "write_file"
187
+ ],
188
+ "requiredCommands": [
189
+ "python3"
190
+ ],
191
+ "license": "MIT",
192
+ "files": [
193
+ "LICENSE",
194
+ "SKILL.md",
195
+ "scripts/pptx_create.py",
196
+ "scripts/pptx_edit.py",
197
+ "scripts/pptx_from_template.py",
198
+ "scripts/pptx_read.py",
199
+ "scripts/pptx_render.py"
200
+ ],
201
+ "sha256": "e1d6e6a8fd53def0323a13025c2c480e8babeb4ae04336322b392d150c9c1367",
202
+ "security": {
203
+ "scannerVersion": "qubicl-content-security-v1",
204
+ "verdict": "no-blocking-findings",
205
+ "findings": []
206
+ }
207
+ },
208
+ {
209
+ "id": "qubicl-core/ocr-and-documents",
210
+ "name": "ocr-and-documents",
211
+ "description": "Extract text from scanned PDFs and images with Qubicl's local Poppler and Tesseract pipeline, and choose the appropriate Qubicl document or web tool for mixed document sources. Use for OCR, image-only PDFs, scans, and document extraction diagnostics.",
212
+ "category": "documents",
213
+ "compatiblePresets": [
214
+ "browser",
215
+ "computer",
216
+ "workstation"
217
+ ],
218
+ "requiredTools": [
219
+ "skills_list",
220
+ "skill_view",
221
+ "exec_command",
222
+ "read_file",
223
+ "write_file",
224
+ "web_extract"
225
+ ],
226
+ "requiredCommands": [
227
+ "python3",
228
+ "pdfinfo",
229
+ "pdftoppm",
230
+ "tesseract"
231
+ ],
232
+ "license": "MIT",
233
+ "files": [
234
+ "LICENSE",
235
+ "SKILL.md",
236
+ "scripts/ocr_document.py"
237
+ ],
238
+ "sha256": "6bdd0431dca3fef18c3099a9a6d8732d338d47e1149679bd0ad6a7351c1ac37c",
239
+ "security": {
240
+ "scannerVersion": "qubicl-content-security-v1",
241
+ "verdict": "no-blocking-findings",
242
+ "findings": []
243
+ }
244
+ }
245
+ ]
246
+ }
@@ -0,0 +1,18 @@
1
+ # Fully pinned dependency closure for portable document skills in computer and workstation images.
2
+ cffi==2.1.1
3
+ charset-normalizer==3.5.1
4
+ cryptography==50.0.0
5
+ et_xmlfile==2.0.0
6
+ lxml==6.1.2
7
+ openpyxl==3.1.5
8
+ pdfminer.six==20260107
9
+ pdfplumber==0.11.10
10
+ pillow==12.3.0
11
+ pycparser==3.0
12
+ pypdf==6.16.1
13
+ pypdfium2==5.13.0
14
+ python-docx==1.2.0
15
+ python-pptx==1.0.2
16
+ reportlab==5.0.1
17
+ typing_extensions==4.16.0
18
+ xlsxwriter==3.2.9