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,147 @@
1
+ #!/usr/bin/env python3
2
+ # MIT-licensed helper adapted for Qubicl's native DOCX skill.
3
+ """Inspect and resolve tracked changes (w:ins / w:del) in a .docx.
4
+
5
+ Subcommands:
6
+ list JSON list of revisions: id, author, date, type, text
7
+ accept-all accept every insertion and deletion
8
+ reject-all reject every insertion and deletion
9
+ accept accept one revision by --id
10
+ reject reject one revision by --id
11
+
12
+ Examples:
13
+ docx_revisions.py list report.docx
14
+ docx_revisions.py accept-all report.docx -o accepted.docx
15
+ docx_revisions.py reject report.docx --id 3 -o out.docx
16
+
17
+ Semantics (direct XML manipulation, python-docx oxml layer):
18
+ accept w:ins -> unwrap (keep inserted runs) reject w:ins -> remove
19
+ accept w:del -> remove reject w:del -> restore
20
+ (restore = w:delText tags renamed to w:t, wrapper unwrapped)
21
+
22
+ Covers run-level insertions/deletions anywhere in body, tables (nested
23
+ included), headers and footers. Row/paragraph-mark revisions and format
24
+ changes (w:rPrChange etc.) are reported by docx_read.py --revisions but
25
+ not resolved here.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import sys
32
+
33
+ from docx import Document
34
+
35
+ from docx_common import iter_part_roots
36
+
37
+ W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
38
+
39
+
40
+ def q(tag: str) -> str:
41
+ return f"{{{W}}}{tag}"
42
+
43
+
44
+ INS, DEL = q("ins"), q("del")
45
+
46
+
47
+ def _iter_revision_elements(doc):
48
+ """Yield every w:ins / w:del element across body, headers, footers."""
49
+ for root in iter_part_roots(doc):
50
+ for el in root.iter(INS, DEL):
51
+ yield el
52
+
53
+
54
+ def _rev_text(el) -> str:
55
+ tag = q("delText") if el.tag == DEL else q("t")
56
+ return "".join(t.text or "" for t in el.iter(tag))
57
+
58
+
59
+ def _rev_record(el) -> dict:
60
+ return {
61
+ "id": el.get(q("id")),
62
+ "author": el.get(q("author")),
63
+ "date": el.get(q("date")),
64
+ "type": "insertion" if el.tag == INS else "deletion",
65
+ "text": _rev_text(el),
66
+ }
67
+
68
+
69
+ def _unwrap(el) -> None:
70
+ """Replace `el` with its children, keeping document order."""
71
+ parent = el.getparent()
72
+ idx = list(parent).index(el)
73
+ for child in list(el):
74
+ parent.insert(idx, child)
75
+ idx += 1
76
+ parent.remove(el)
77
+
78
+
79
+ def _apply(el, accept: bool) -> None:
80
+ if el.getparent() is None: # already detached via an outer wrapper
81
+ return
82
+ if el.tag == INS:
83
+ if accept:
84
+ _unwrap(el)
85
+ else:
86
+ el.getparent().remove(el)
87
+ else: # w:del
88
+ if accept:
89
+ el.getparent().remove(el)
90
+ else:
91
+ for dt in list(el.iter(q("delText"))):
92
+ dt.tag = q("t")
93
+ _unwrap(el)
94
+
95
+
96
+ def resolve(doc, accept: bool, rev_id: str | None = None) -> int:
97
+ targets = [el for el in _iter_revision_elements(doc)
98
+ if rev_id is None or el.get(q("id")) == rev_id]
99
+ for el in targets:
100
+ _apply(el, accept)
101
+ return len(targets)
102
+
103
+
104
+ def main() -> int:
105
+ ap = argparse.ArgumentParser(
106
+ description="List, accept, or reject tracked changes in a .docx.")
107
+ sub = ap.add_subparsers(dest="cmd", required=True)
108
+
109
+ def common(p, out=True):
110
+ p.add_argument("path", help="input .docx")
111
+ if out:
112
+ p.add_argument("-o", "--output",
113
+ help="output path (default: overwrite input)")
114
+
115
+ common(sub.add_parser("list", help="list revisions as JSON"), out=False)
116
+ common(sub.add_parser("accept-all", help="accept every revision"))
117
+ common(sub.add_parser("reject-all", help="reject every revision"))
118
+ for name in ("accept", "reject"):
119
+ p = sub.add_parser(name, help=f"{name} one revision by id")
120
+ common(p)
121
+ p.add_argument("--id", required=True, help="revision id (w:id)")
122
+
123
+ args = ap.parse_args()
124
+ doc = Document(args.path)
125
+
126
+ if args.cmd == "list":
127
+ revs = [_rev_record(el) for el in _iter_revision_elements(doc)]
128
+ print(json.dumps({"ok": True, "revisions": revs}, ensure_ascii=False))
129
+ return 0
130
+
131
+ accept = args.cmd in ("accept-all", "accept")
132
+ rev_id = getattr(args, "id", None)
133
+ n = resolve(doc, accept, rev_id)
134
+ if rev_id is not None and n == 0:
135
+ print(json.dumps({"ok": False,
136
+ "error": f"no revision with id {rev_id}"}))
137
+ return 1
138
+ out = args.output or args.path
139
+ doc.save(out)
140
+ print(json.dumps({"ok": True, "output": out, "resolved": n,
141
+ "action": "accept" if accept else "reject"},
142
+ ensure_ascii=False))
143
+ return 0
144
+
145
+
146
+ if __name__ == "__main__":
147
+ sys.exit(main())
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ # MIT-licensed helper adapted for Qubicl's native DOCX skill.
3
+ """Fill {{placeholder}} tokens in a .docx from a JSON mapping.
4
+
5
+ Tokens are replaced everywhere: body paragraphs, tables (including nested
6
+ tables), headers and footers. Run formatting is preserved; tokens split
7
+ across runs are handled.
8
+
9
+ Usage:
10
+ docx_template.py template.docx values.json output.docx
11
+ docx_template.py template.docx values.json output.docx --strict
12
+
13
+ values.json: {"name": "Ada", "date": "2026-01-01"} fills {{name}}, {{date}}.
14
+ With --strict, exits 1 if any {{token}} remains unfilled after processing.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import re
21
+ import sys
22
+
23
+ from docx import Document
24
+
25
+ from docx_common import iter_all_paragraphs, replace_in_paragraph
26
+
27
+ TOKEN_RE = re.compile(r"\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}")
28
+
29
+
30
+ def main() -> int:
31
+ ap = argparse.ArgumentParser(
32
+ description="Fill {{token}} placeholders in a .docx from JSON.")
33
+ ap.add_argument("template", help="input .docx with {{tokens}}")
34
+ ap.add_argument("values", help="JSON file of token -> value")
35
+ ap.add_argument("output", help="output .docx path")
36
+ ap.add_argument("--strict", action="store_true",
37
+ help="fail if any token remains unfilled")
38
+ args = ap.parse_args()
39
+
40
+ with open(args.values, encoding="utf-8") as f:
41
+ values = json.load(f)
42
+
43
+ doc = Document(args.template)
44
+ filled = {}
45
+ for para in iter_all_paragraphs(doc):
46
+ # Normalize whitespace variants like {{ name }} first.
47
+ for m in set(TOKEN_RE.findall(para.text)):
48
+ if m in values:
49
+ # Replace any spacing variant with canonical token, then fill.
50
+ for variant in set(
51
+ t.group(0) for t in TOKEN_RE.finditer(para.text)
52
+ if t.group(1) == m):
53
+ n = replace_in_paragraph(para, variant, str(values[m]))
54
+ filled[m] = filled.get(m, 0) + n
55
+
56
+ remaining = sorted({m for para in iter_all_paragraphs(doc)
57
+ for m in TOKEN_RE.findall(para.text)})
58
+ doc.save(args.output)
59
+ result = {"ok": True, "output": args.output, "filled": filled,
60
+ "unfilled_tokens": remaining}
61
+ if args.strict and remaining:
62
+ result["ok"] = False
63
+ print(json.dumps(result, ensure_ascii=False))
64
+ return 1
65
+ print(json.dumps(result, ensure_ascii=False))
66
+ return 0
67
+
68
+
69
+ if __name__ == "__main__":
70
+ sys.exit(main())
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env python3
2
+ # MIT-licensed helper adapted for Qubicl's native DOCX skill.
3
+ """Health-check a .docx package and report issues as JSON.
4
+
5
+ Usage: docx_validate.py file.docx
6
+
7
+ Checks (health-check tier, NOT full XSD schema validation):
8
+ - the file is a readable zip and python-docx can open it
9
+ - required package parts exist ([Content_Types].xml, document.xml)
10
+ - every relationship in every .rels file resolves to a part in the
11
+ package (dangling image/hyperlink/etc. rels are reported; external
12
+ targets such as hyperlinks are skipped)
13
+ - r:embed / r:id references in document.xml resolve to relationships
14
+ - embedded images are non-empty and start with known magic bytes
15
+ (PNG/JPEG/GIF/BMP/TIFF/EMF/WMF/SVG); no PIL required
16
+ - paragraph and run style ids referenced by the document exist in
17
+ styles.xml
18
+
19
+ Output: {"ok": bool, "issues": [{"severity": "error"|"warning", ...}]}
20
+ Exit code 1 when any error-severity issue is found (warnings exit 0).
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import json
26
+ import posixpath
27
+ import sys
28
+ import zipfile
29
+
30
+ from lxml import etree
31
+
32
+ W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
33
+ R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
34
+ PR = "http://schemas.openxmlformats.org/package/2006/relationships"
35
+
36
+ IMAGE_MAGIC = (
37
+ b"\x89PNG\r\n\x1a\n", b"\xff\xd8\xff", b"GIF87a", b"GIF89a",
38
+ b"BM", b"II*\x00", b"MM\x00*",
39
+ b"\x01\x00\x00\x00", # EMF
40
+ b"\xd7\xcd\xc6\x9a", b"\x01\x00\x09\x00", # WMF variants
41
+ b"<?xml", b"<svg",
42
+ )
43
+
44
+
45
+ def _issue(issues, severity, code, detail):
46
+ issues.append({"severity": severity, "code": code, "detail": detail})
47
+
48
+
49
+ def _rel_target(base_part: str, target: str) -> str:
50
+ base_dir = posixpath.dirname(base_part)
51
+ return posixpath.normpath(posixpath.join(base_dir, target)).lstrip("/")
52
+
53
+
54
+ def validate(path: str) -> dict:
55
+ issues: list[dict] = []
56
+
57
+ try:
58
+ zf = zipfile.ZipFile(path)
59
+ except (OSError, zipfile.BadZipFile) as exc:
60
+ _issue(issues, "error", "not-a-zip", str(exc))
61
+ return {"ok": False, "issues": issues}
62
+
63
+ names = set(zf.namelist())
64
+ bad = zf.testzip()
65
+ if bad is not None:
66
+ _issue(issues, "error", "corrupt-member", f"CRC check failed: {bad}")
67
+
68
+ for required in ("[Content_Types].xml", "word/document.xml"):
69
+ if required not in names:
70
+ _issue(issues, "error", "missing-part",
71
+ f"required part absent: {required}")
72
+ if issues and any(i["severity"] == "error" for i in issues):
73
+ return {"ok": False, "issues": issues}
74
+
75
+ # --- relationships resolve ------------------------------------------
76
+ rel_ids_by_source: dict[str, dict] = {}
77
+ for rels_name in [n for n in names if n.endswith(".rels")]:
78
+ try:
79
+ root = etree.fromstring(zf.read(rels_name))
80
+ except etree.XMLSyntaxError as exc:
81
+ _issue(issues, "error", "bad-rels-xml", f"{rels_name}: {exc}")
82
+ continue
83
+ source_part = posixpath.normpath(
84
+ posixpath.join(posixpath.dirname(rels_name), ".."))
85
+ source_part = "" if source_part == "." else source_part
86
+ ids = {}
87
+ for rel in root.iter(f"{{{PR}}}Relationship"):
88
+ rid, target = rel.get("Id"), rel.get("Target", "")
89
+ mode = rel.get("TargetMode", "Internal")
90
+ ids[rid] = target
91
+ if mode == "External":
92
+ continue
93
+ resolved = _rel_target(source_part + "/x" if source_part
94
+ else "x", target)
95
+ if resolved not in names:
96
+ _issue(issues, "error", "dangling-rel",
97
+ f"{rels_name}: {rid} -> {target} (missing part)")
98
+ rel_ids_by_source[source_part or "_package"] = ids
99
+
100
+ # --- r:id / r:embed references in document.xml -----------------------
101
+ doc_root = etree.fromstring(zf.read("word/document.xml"))
102
+ doc_rels = rel_ids_by_source.get("word", {})
103
+ for el in doc_root.iter():
104
+ for attr in (f"{{{R}}}id", f"{{{R}}}embed", f"{{{R}}}link"):
105
+ rid = el.get(attr)
106
+ if rid and rid not in doc_rels:
107
+ _issue(issues, "error", "unresolved-reference",
108
+ f"document.xml references {rid} with no relationship")
109
+
110
+ # --- embedded images decode ------------------------------------------
111
+ for name in [n for n in names if n.startswith("word/media/")]:
112
+ data = zf.read(name)
113
+ if not data:
114
+ _issue(issues, "error", "empty-image", name)
115
+ elif not any(data.startswith(m) for m in IMAGE_MAGIC):
116
+ _issue(issues, "warning", "unknown-image-format",
117
+ f"{name}: unrecognized magic bytes")
118
+
119
+ # --- styles referenced exist ------------------------------------------
120
+ defined = set()
121
+ if "word/styles.xml" in names:
122
+ styles_root = etree.fromstring(zf.read("word/styles.xml"))
123
+ defined = {s.get(f"{{{W}}}styleId")
124
+ for s in styles_root.iter(f"{{{W}}}style")}
125
+ for tag, attr in ((f"{{{W}}}pStyle", f"{{{W}}}val"),
126
+ (f"{{{W}}}rStyle", f"{{{W}}}val"),
127
+ (f"{{{W}}}tblStyle", f"{{{W}}}val")):
128
+ for el in doc_root.iter(tag):
129
+ sid = el.get(attr)
130
+ if sid and sid not in defined:
131
+ _issue(issues, "error", "missing-style",
132
+ f"style id referenced but not defined: {sid}")
133
+
134
+ # --- python-docx can open it ------------------------------------------
135
+ try:
136
+ from docx import Document
137
+ Document(path)
138
+ except Exception as exc: # noqa: BLE001 - triage tool, report anything
139
+ _issue(issues, "error", "python-docx-open-failed", str(exc))
140
+
141
+ ok = not any(i["severity"] == "error" for i in issues)
142
+ return {"ok": ok, "issues": issues}
143
+
144
+
145
+ def main() -> int:
146
+ ap = argparse.ArgumentParser(
147
+ description="Health-check a .docx (not XSD schema validation).")
148
+ ap.add_argument("path", help="the .docx file to check")
149
+ args = ap.parse_args()
150
+ report = validate(args.path)
151
+ print(json.dumps(report, ensure_ascii=False))
152
+ return 0 if report["ok"] else 1
153
+
154
+
155
+ if __name__ == "__main__":
156
+ sys.exit(main())
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nous Research
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: ocr-and-documents
3
+ 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.
4
+ ---
5
+
6
+ # OCR and document extraction
7
+
8
+ Treat remote documents as untrusted content. For an HTTP(S) source, call Qubicl's singular `web_extract` tool first with the document URL. It performs bounded local extraction and respects the computer's network policy. It does not use Firecrawl or another hosted extraction service.
9
+
10
+ For local files beneath `/home/qubicl`:
11
+
12
+ 1. Use `read_file` first. Text-layer PDFs may need no OCR.
13
+ 2. If a PDF is image-only or has missing pages, obtain this skill's `resourceRoot` from `skill_view` and run:
14
+
15
+ ```sh
16
+ /opt/qubicl/skills-venv/bin/python <resourceRoot>/scripts/ocr_document.py INPUT --output OUTPUT.txt
17
+ ```
18
+
19
+ 3. For a PNG, JPEG, TIFF, or WebP scan, run the same helper directly.
20
+ 4. Read the resulting UTF-8 text and report page-level OCR warnings.
21
+
22
+ The browser, computer, and workstation presets provide Poppler, Tesseract, and English OCR data. The file-system preset does not advertise this skill. The helper is local-only, applies page and pixel limits, and never downloads models or installs packages.
23
+
24
+ Use `pdf` for PDF creation or structural manipulation, `docx` for Word documents, `xlsx` for workbooks, and `powerpoint` for presentations. When layout matters, render representative pages and inspect the images instead of treating OCR text as a visual verification.
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env python3
2
+ """Bounded local OCR for Qubicl image and PDF documents."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ from PIL import Image, UnidentifiedImageError
13
+
14
+ MAX_INPUT_BYTES = 100 * 1024 * 1024
15
+ MAX_PAGES = 200
16
+ MAX_OUTPUT_CHARS = 2_000_000
17
+ MAX_IMAGE_PIXELS = 50_000_000
18
+ MAX_DOCUMENT_PIXELS = 500_000_000
19
+ IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".tif", ".tiff", ".webp", ".bmp"}
20
+ Image.MAX_IMAGE_PIXELS = MAX_IMAGE_PIXELS
21
+
22
+
23
+ def command(*args: str, timeout: int = 180) -> subprocess.CompletedProcess[str]:
24
+ return subprocess.run(args, check=True, capture_output=True, text=True, timeout=timeout)
25
+
26
+
27
+ def require(name: str) -> str:
28
+ path = shutil.which(name)
29
+ if not path:
30
+ raise RuntimeError(f"Required Qubicl image command is unavailable: {name}")
31
+ return path
32
+
33
+
34
+ def ocr_image(path: Path, language: str) -> str:
35
+ result = command(require("tesseract"), str(path), "stdout", "-l", language, timeout=180)
36
+ return result.stdout.strip()
37
+
38
+
39
+ def validate_image(path: Path) -> int:
40
+ try:
41
+ with Image.open(path) as image:
42
+ total = 0
43
+ frames = getattr(image, "n_frames", 1)
44
+ for frame in range(frames):
45
+ image.seek(frame)
46
+ pixels = image.width * image.height
47
+ if pixels > MAX_IMAGE_PIXELS:
48
+ raise RuntimeError(f"Image frame exceeds the {MAX_IMAGE_PIXELS}-pixel limit")
49
+ total += pixels
50
+ if total > MAX_DOCUMENT_PIXELS:
51
+ raise RuntimeError(f"Image exceeds the {MAX_DOCUMENT_PIXELS}-pixel aggregate limit")
52
+ image.verify()
53
+ return total
54
+ except (Image.DecompressionBombError, Image.DecompressionBombWarning, UnidentifiedImageError) as error:
55
+ raise RuntimeError(f"Image is invalid or exceeds Qubicl's pixel limit: {error}") from error
56
+
57
+
58
+ def pdf_pages(path: Path) -> int:
59
+ result = command(require("pdfinfo"), str(path), timeout=30)
60
+ for line in result.stdout.splitlines():
61
+ if line.lower().startswith("pages:"):
62
+ pages = int(line.split(":", 1)[1].strip())
63
+ if pages < 1 or pages > MAX_PAGES:
64
+ raise RuntimeError(f"PDF has {pages} pages; supported range is 1-{MAX_PAGES}")
65
+ return pages
66
+ raise RuntimeError("Unable to determine PDF page count")
67
+
68
+
69
+ def ocr_pdf(path: Path, language: str, dpi: int) -> tuple[str, list[int]]:
70
+ pages = pdf_pages(path)
71
+ missing: list[int] = []
72
+ chunks: list[str] = []
73
+ with tempfile.TemporaryDirectory(prefix="qubicl-ocr-") as temporary:
74
+ total_pixels = 0
75
+ for index in range(1, pages + 1):
76
+ prefix = Path(temporary) / f"page-{index}"
77
+ command(
78
+ require("pdftoppm"),
79
+ "-f", str(index),
80
+ "-l", str(index),
81
+ "-singlefile",
82
+ "-png",
83
+ "-r", str(dpi),
84
+ "-scale-to", "7000",
85
+ str(path),
86
+ str(prefix),
87
+ timeout=180,
88
+ )
89
+ image = prefix.with_suffix(".png")
90
+ if not image.is_file():
91
+ raise RuntimeError(f"PDF page {index} did not render")
92
+ total_pixels += validate_image(image)
93
+ if total_pixels > MAX_DOCUMENT_PIXELS:
94
+ raise RuntimeError(f"Rendered PDF exceeds the {MAX_DOCUMENT_PIXELS}-pixel aggregate limit")
95
+ text = ocr_image(image, language)
96
+ image.unlink()
97
+ if not text:
98
+ missing.append(index)
99
+ chunks.append(f"--- Page {index} ---\n{text}".rstrip())
100
+ return "\n\n".join(chunks), missing
101
+
102
+
103
+ def main() -> None:
104
+ parser = argparse.ArgumentParser(description="Extract bounded local OCR text from a PDF or image.")
105
+ parser.add_argument("input", help="PDF or image beneath /home/qubicl")
106
+ parser.add_argument("--output", required=True, help="UTF-8 output text path beneath /home/qubicl")
107
+ parser.add_argument("--language", default="eng", help="Installed Tesseract language code (default: eng)")
108
+ parser.add_argument("--dpi", type=int, default=150, choices=range(72, 301), metavar="72-300")
109
+ args = parser.parse_args()
110
+
111
+ source = Path(args.input).expanduser().resolve(strict=True)
112
+ output = Path(args.output).expanduser().resolve()
113
+ home = Path("/home/qubicl").resolve()
114
+ if home not in source.parents or home not in output.parents:
115
+ raise RuntimeError("Input and output must remain beneath /home/qubicl")
116
+ if not source.is_file() or source.stat().st_size > MAX_INPUT_BYTES:
117
+ raise RuntimeError("Input must be a regular file no larger than 100 MiB")
118
+ if source.suffix.lower() == ".pdf":
119
+ text, empty_pages = ocr_pdf(source, args.language, args.dpi)
120
+ elif source.suffix.lower() in IMAGE_SUFFIXES:
121
+ validate_image(source)
122
+ text, empty_pages = ocr_image(source, args.language), []
123
+ else:
124
+ raise RuntimeError("Supported inputs are PDF, PNG, JPEG, TIFF, WebP, and BMP")
125
+ truncated = len(text) > MAX_OUTPUT_CHARS
126
+ output.parent.mkdir(parents=True, exist_ok=True)
127
+ output.write_text(text[:MAX_OUTPUT_CHARS] + ("\n\n[Qubicl OCR output truncated]\n" if truncated else "\n"), encoding="utf-8")
128
+ print({"output": str(output), "characters": min(len(text), MAX_OUTPUT_CHARS), "truncated": truncated, "emptyPages": empty_pages})
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nous Research
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: pdf
3
+ 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.
4
+ ---
5
+
6
+ # PDF workflows
7
+
8
+ Use Qubicl file tools to inspect inputs and save outputs under `/home/qubicl`. Use `exec_command` for the deterministic helpers bundled with this skill. Obtain this package's `resourceRoot` from `skill_view`; do not assume a client-specific skill directory.
9
+
10
+ The curated browser, computer, and workstation images provide the declared Python libraries plus Poppler. Run helpers through `/opt/qubicl/skills-venv/bin/python`. If a dependency is absent, report that the current preset does not support the operation; do not install packages automatically.
11
+
12
+ Common helpers beneath `<resourceRoot>/scripts/`:
13
+
14
+ - `pdf_read.py FILE --text|--tables|--meta|--fields`
15
+ - `pdf_create.py SPEC.json -o OUTPUT.pdf`
16
+ - `pdf_merge.py INPUT... -o OUTPUT.pdf`
17
+ - `pdf_split.py FILE --pages 1-3,5 -o OUTPUT.pdf`
18
+ - `pdf_page_image.py FILE --out-dir DIR`
19
+ - `pdf_make_form.py SPEC.json -o OUTPUT.pdf`
20
+ - `pdf_fill_form.py FILE --fields-json VALUES.json -o OUTPUT.pdf`
21
+ - `pdf_stamp.py`, `pdf_watermark.py`, `pdf_meta.py`, and `pdf_secure.py`
22
+
23
+ Read `references/forms.md` only for AcroForm work.
24
+
25
+ For inspection, extract text first and render representative pages when layout matters. Use `take_screenshot` only for the live desktop; use generated page images for document QA. Verify important outputs by reopening them with a read helper and, when visual fidelity matters, by rendering pages. Never overwrite the only input copy unless the user explicitly requests in-place editing.