@arnilo/prism-tools 0.0.0-stage → 0.13.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 (331) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +28 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +545 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env python3
2
+ # MIT License. Part of the Hermes docx skill.
3
+ """List, add, and delete comments in a .docx.
4
+
5
+ Subcommands:
6
+ list JSON per comment: id, author, initials, date, text, anchored_text
7
+ add add a comment anchored to the first occurrence of --target
8
+ delete remove a comment (and its range markers) by --id
9
+
10
+ Examples:
11
+ docx_comments.py list report.docx
12
+ docx_comments.py add report.docx --target "Q3 revenue" \
13
+ --text "Needs a source" --author "Reviewer" -o out.docx
14
+ docx_comments.py delete report.docx --id 0 -o out.docx
15
+
16
+ Uses the native python-docx comments API (>= 1.2) when available; falls
17
+ back to building word/comments.xml and the range markers directly for
18
+ older versions (or when --xml is passed). Listing and deletion always
19
+ work at the XML level so they handle documents from any producer.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import datetime as _dt
25
+ import json
26
+ import sys
27
+ from copy import deepcopy
28
+
29
+ from docx import Document
30
+ from docx.opc.constants import RELATIONSHIP_TYPE as RT
31
+ from lxml import etree
32
+
33
+ from docx_common import iter_part_roots
34
+
35
+ W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
36
+ COMMENTS_CT = ("application/vnd.openxmlformats-officedocument"
37
+ ".wordprocessingml.comments+xml")
38
+
39
+
40
+ def q(tag: str) -> str:
41
+ return f"{{{W}}}{tag}"
42
+
43
+
44
+ # ---------------------------------------------------------------- reading
45
+
46
+ def _comments_root(doc):
47
+ """Return the XML root of the comments part, or None."""
48
+ for rel in doc.part.rels.values():
49
+ if rel.reltype == RT.COMMENTS:
50
+ part = rel.target_part
51
+ el = getattr(part, "_element", None)
52
+ if el is not None:
53
+ return el
54
+ return etree.fromstring(part.blob)
55
+ return None
56
+
57
+
58
+ def _anchored_texts(doc) -> dict:
59
+ """Map comment id -> document text between its range markers."""
60
+ anchored: dict[str, list[str]] = {}
61
+ for root in iter_part_roots(doc):
62
+ active: set[str] = set()
63
+ for el in root.iter():
64
+ if el.tag == q("commentRangeStart"):
65
+ cid = el.get(q("id"))
66
+ active.add(cid)
67
+ anchored.setdefault(cid, [])
68
+ elif el.tag == q("commentRangeEnd"):
69
+ active.discard(el.get(q("id")))
70
+ elif el.tag == q("t") and active:
71
+ for cid in active:
72
+ anchored[cid].append(el.text or "")
73
+ return {cid: "".join(parts) for cid, parts in anchored.items()}
74
+
75
+
76
+ def list_comments(doc) -> list:
77
+ root = _comments_root(doc)
78
+ if root is None:
79
+ return []
80
+ anchored = _anchored_texts(doc)
81
+ out = []
82
+ for c in root.iter(q("comment")):
83
+ cid = c.get(q("id"))
84
+ text = "\n".join(
85
+ "".join(t.text or "" for t in p.iter(q("t")))
86
+ for p in c.iter(q("p")))
87
+ out.append({"id": cid, "author": c.get(q("author")),
88
+ "initials": c.get(q("initials")),
89
+ "date": c.get(q("date")), "text": text,
90
+ "anchored_text": anchored.get(cid, "")})
91
+ return out
92
+
93
+
94
+ # ---------------------------------------------------------------- anchoring
95
+
96
+ def _split_run(para, run_el, offset: int):
97
+ """Split a run element at text offset; return the new right-hand run."""
98
+ text = "".join(t.text or "" for t in run_el.iter(q("t")))
99
+ right = deepcopy(run_el)
100
+ run_el.addnext(right)
101
+ for el, s in ((run_el, text[:offset]), (right, text[offset:])):
102
+ for t in list(el.iter(q("t"))):
103
+ el.remove(t)
104
+ t = etree.SubElement(el, q("t"))
105
+ t.text = s
106
+ t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
107
+ return right
108
+
109
+
110
+ def find_anchor_runs(doc, target: str):
111
+ """Isolate `target`'s first occurrence into whole runs; return them."""
112
+ from docx_common import iter_all_paragraphs
113
+ for para in iter_all_paragraphs(doc):
114
+ full = para.text
115
+ start = full.find(target)
116
+ if start < 0:
117
+ continue
118
+ end = start + len(target)
119
+ pos = 0
120
+ covered = []
121
+ for run_el in para._p.iter(q("r")):
122
+ rtext = "".join(t.text or "" for t in run_el.iter(q("t")))
123
+ r_start, r_end = pos, pos + len(rtext)
124
+ pos = r_end
125
+ if r_end <= start or r_start >= end:
126
+ continue
127
+ if r_start < start: # split off the left part
128
+ run_el = _split_run(para, run_el, start - r_start)
129
+ r_start = start
130
+ if r_end > end: # split off the right part
131
+ _split_run(para, run_el, end - r_start)
132
+ covered.append(run_el)
133
+ return para, covered
134
+ return None, []
135
+
136
+
137
+ # ---------------------------------------------------------------- adding
138
+
139
+ def _next_id(doc) -> int:
140
+ root = _comments_root(doc)
141
+ if root is None:
142
+ return 0
143
+ ids = [int(c.get(q("id"), "0")) for c in root.iter(q("comment"))
144
+ if c.get(q("id"), "").isdigit()]
145
+ return max(ids) + 1 if ids else 0
146
+
147
+
148
+ def add_comment_native(doc, runs, text, author, initials):
149
+ from docx.text.run import Run
150
+ run_objs = [Run(r, None) for r in runs]
151
+ comment = doc.add_comment(run_objs, text=text, author=author,
152
+ initials=initials or "")
153
+ return str(comment.comment_id)
154
+
155
+
156
+ def add_comment_xml(doc, runs, text, author, initials) -> str:
157
+ cid = str(_next_id(doc))
158
+ root = _comments_root(doc)
159
+ if root is None:
160
+ root = etree.fromstring(
161
+ f'<w:comments xmlns:w="{W}"/>'.encode("utf-8"))
162
+ from docx.opc.packuri import PackURI
163
+ from docx.opc.part import Part
164
+ blob = etree.tostring(root, xml_declaration=True,
165
+ encoding="UTF-8", standalone=True)
166
+ part = Part(PackURI("/word/comments.xml"), COMMENTS_CT, blob,
167
+ doc.part.package)
168
+ doc.part.relate_to(part, RT.COMMENTS)
169
+ # keep a live element on the part so edits reach save()
170
+ part._element = root
171
+ part.blob_ = None
172
+
173
+ def _blob(self=part):
174
+ return etree.tostring(self._element, xml_declaration=True,
175
+ encoding="UTF-8", standalone=True)
176
+ part.__class__ = type("CommentsXmlPart", (Part,),
177
+ {"blob": property(lambda self: _blob(self))})
178
+ now = _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
179
+ comment = etree.SubElement(root, q("comment"))
180
+ comment.set(q("id"), cid)
181
+ comment.set(q("author"), author)
182
+ if initials:
183
+ comment.set(q("initials"), initials)
184
+ comment.set(q("date"), now)
185
+ p = etree.SubElement(comment, q("p"))
186
+ r = etree.SubElement(p, q("r"))
187
+ t = etree.SubElement(r, q("t"))
188
+ t.text = text
189
+ # range markers around the anchor runs + reference run after them
190
+ first, last = runs[0], runs[-1]
191
+ start = first.makeelement(q("commentRangeStart"), {q("id"): cid})
192
+ first.addprevious(start)
193
+ end = last.makeelement(q("commentRangeEnd"), {q("id"): cid})
194
+ last.addnext(end)
195
+ ref_run = last.makeelement(q("r"), {})
196
+ ref = etree.SubElement(ref_run, q("commentReference"))
197
+ ref.set(q("id"), cid)
198
+ end.addnext(ref_run)
199
+ return cid
200
+
201
+
202
+ # ---------------------------------------------------------------- deleting
203
+
204
+ def delete_comment(doc, cid: str) -> bool:
205
+ root = _comments_root(doc)
206
+ found = False
207
+ if root is not None:
208
+ for c in list(root.iter(q("comment"))):
209
+ if c.get(q("id")) == cid:
210
+ c.getparent().remove(c)
211
+ found = True
212
+ for part_root in iter_part_roots(doc):
213
+ for tag in ("commentRangeStart", "commentRangeEnd",
214
+ "commentReference"):
215
+ for el in list(part_root.iter(q(tag))):
216
+ if el.get(q("id")) == cid:
217
+ parent = el.getparent()
218
+ # remove the wrapping run for reference marks
219
+ if tag == "commentReference" and parent.tag == q("r"):
220
+ parent.getparent().remove(parent)
221
+ else:
222
+ parent.remove(el)
223
+ found = True
224
+ return found
225
+
226
+
227
+ def main() -> int:
228
+ ap = argparse.ArgumentParser(
229
+ description="List, add, or delete comments in a .docx.")
230
+ sub = ap.add_subparsers(dest="cmd", required=True)
231
+
232
+ p = sub.add_parser("list", help="list comments as JSON")
233
+ p.add_argument("path", help="input .docx")
234
+
235
+ p = sub.add_parser("add", help="add a comment anchored to text")
236
+ p.add_argument("path", help="input .docx")
237
+ p.add_argument("-o", "--output", help="output path (default: in place)")
238
+ p.add_argument("--target", required=True,
239
+ help="anchor: first occurrence of this text")
240
+ p.add_argument("--text", required=True, help="comment body")
241
+ p.add_argument("--author", default="Hermes")
242
+ p.add_argument("--initials", default="")
243
+ p.add_argument("--xml", action="store_true",
244
+ help="force the XML fallback (skip native API)")
245
+
246
+ p = sub.add_parser("delete", help="delete a comment by id")
247
+ p.add_argument("path", help="input .docx")
248
+ p.add_argument("-o", "--output", help="output path (default: in place)")
249
+ p.add_argument("--id", required=True, help="comment id")
250
+
251
+ args = ap.parse_args()
252
+ doc = Document(args.path)
253
+
254
+ if args.cmd == "list":
255
+ print(json.dumps({"ok": True, "comments": list_comments(doc)},
256
+ ensure_ascii=False))
257
+ return 0
258
+
259
+ if args.cmd == "add":
260
+ para, runs = find_anchor_runs(doc, args.target)
261
+ if not runs:
262
+ print(json.dumps({"ok": False,
263
+ "error": f"target not found: {args.target}"}))
264
+ return 1
265
+ native = hasattr(doc, "add_comment") and not args.xml
266
+ if native:
267
+ cid = add_comment_native(doc, runs, args.text, args.author,
268
+ args.initials)
269
+ else:
270
+ cid = add_comment_xml(doc, runs, args.text, args.author,
271
+ args.initials)
272
+ result = {"ok": True, "comment_id": cid,
273
+ "native_api": native, "anchored_to": args.target}
274
+ else: # delete
275
+ if not delete_comment(doc, args.id):
276
+ print(json.dumps({"ok": False,
277
+ "error": f"no comment with id {args.id}"}))
278
+ return 1
279
+ result = {"ok": True, "deleted_id": args.id}
280
+
281
+ out = args.output or args.path
282
+ doc.save(out)
283
+ result["output"] = out
284
+ print(json.dumps(result, ensure_ascii=False))
285
+ return 0
286
+
287
+
288
+ if __name__ == "__main__":
289
+ sys.exit(main())
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ # MIT License. Shared helpers for the docx skill scripts.
3
+ """Shared helpers: paragraph iteration and run-preserving text replacement."""
4
+ from __future__ import annotations
5
+
6
+
7
+ def iter_all_paragraphs(doc, include_headers_footers: bool = True):
8
+ """Yield every paragraph in body, tables (recursively), headers, footers."""
9
+ yield from _iter_container(doc)
10
+ if include_headers_footers:
11
+ for section in doc.sections:
12
+ for part in (
13
+ section.header, section.footer,
14
+ section.first_page_header, section.first_page_footer,
15
+ section.even_page_header, section.even_page_footer,
16
+ ):
17
+ if part is not None:
18
+ yield from _iter_container(part)
19
+
20
+
21
+ def _iter_container(container):
22
+ for para in container.paragraphs:
23
+ yield para
24
+ for table in container.tables:
25
+ yield from _iter_table(table)
26
+
27
+
28
+ def _iter_table(table):
29
+ for row in table.rows:
30
+ for cell in row.cells:
31
+ for para in cell.paragraphs:
32
+ yield para
33
+ for nested in cell.tables:
34
+ yield from _iter_table(nested)
35
+
36
+
37
+ def iter_part_roots(doc):
38
+ """Yield the XML root of the body plus every header/footer part."""
39
+ yield doc.element.body
40
+ seen = set()
41
+ for section in doc.sections:
42
+ for part in (
43
+ section.header, section.footer,
44
+ section.first_page_header, section.first_page_footer,
45
+ section.even_page_header, section.even_page_footer,
46
+ ):
47
+ if part is not None and id(part._element) not in seen:
48
+ seen.add(id(part._element))
49
+ yield part._element
50
+
51
+
52
+ def replace_in_paragraph(para, old: str, new: str) -> int:
53
+ """Replace `old` with `new` in a paragraph, preserving run formatting.
54
+
55
+ Strategy: first replace occurrences fully contained in a single run
56
+ (formatting fully preserved). If the needle spans multiple runs, the
57
+ matched runs are collapsed: the replacement inherits the formatting of
58
+ the run where the match starts. Returns number of replacements made.
59
+ """
60
+ if not old or old not in para.text:
61
+ return 0
62
+ count = 0
63
+ # Pass 1: within-run replacements.
64
+ for run in para.runs:
65
+ if old in run.text:
66
+ count += run.text.count(old)
67
+ run.text = run.text.replace(old, new)
68
+ # Pass 2: cross-run occurrences.
69
+ while old in para.text:
70
+ runs = para.runs
71
+ # Map paragraph text offsets to (run_index, offset_in_run).
72
+ full = "".join(r.text for r in runs)
73
+ start = full.find(old)
74
+ if start < 0:
75
+ break
76
+ end = start + len(old)
77
+ pos = 0
78
+ spans = [] # (run_idx, cut_start, cut_end) portions inside the match
79
+ for i, r in enumerate(runs):
80
+ r_start, r_end = pos, pos + len(r.text)
81
+ if r_end > start and r_start < end:
82
+ spans.append((i, max(start, r_start) - r_start,
83
+ min(end, r_end) - r_start))
84
+ pos = r_end
85
+ first = True
86
+ for i, cs, ce in spans:
87
+ t = runs[i].text
88
+ if first:
89
+ runs[i].text = t[:cs] + new + t[ce:]
90
+ first = False
91
+ else:
92
+ runs[i].text = t[:cs] + t[ce:]
93
+ count += 1
94
+ return count
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ # MIT License. Part of the Hermes docx skill.
3
+ """Create a .docx document from a JSON spec.
4
+
5
+ Usage: docx_create.py spec.json output.docx
6
+ Run with --help for the spec format summary.
7
+
8
+ Spec (JSON object):
9
+ {
10
+ "page": {"width_mm": 210, "height_mm": 297,
11
+ "margins_mm": {"top": 25, "bottom": 25, "left": 20, "right": 20}},
12
+ "header": "text shown in page header",
13
+ "footer": "text shown in page footer",
14
+ "styles": [{"name": "MyStyle", "base": "Normal", "font": "Arial",
15
+ "size_pt": 12, "bold": true, "color": "1F4E79"}],
16
+ "blocks": [
17
+ {"type": "heading", "text": "Title", "level": 1},
18
+ {"type": "paragraph", "style": "MyStyle", "runs": [
19
+ {"text": "plain "}, {"text": "bold", "bold": true},
20
+ {"text": " italic", "italic": true},
21
+ {"text": " under", "underline": true}]},
22
+ {"type": "paragraph", "text": "shortcut: single plain run"},
23
+ {"type": "bullet_list", "items": ["a", "b"]},
24
+ {"type": "numbered_list", "items": ["one", "two"]},
25
+ {"type": "table", "header": ["Col1", "Col2"],
26
+ "rows": [["1", "2"]], "style": "Light Grid Accent 1",
27
+ "header_bold": true},
28
+ {"type": "image", "path": "pic.png", "width_mm": 60},
29
+ {"type": "page_break"},
30
+ {"type": "toc"}
31
+ ]
32
+ }
33
+
34
+ Extras: `"footer_page_numbers": true` at the top level adds a
35
+ "Page X of Y" footer built from PAGE/NUMPAGES fields, and a `toc` block
36
+ inserts a Table of Contents field. Field results are computed by
37
+ Word/LibreOffice when the file is opened, not by python-docx.
38
+ """
39
+ from __future__ import annotations
40
+
41
+ import argparse
42
+ import json
43
+ import sys
44
+
45
+ from docx import Document
46
+ from docx.enum.style import WD_STYLE_TYPE
47
+ from docx.enum.text import WD_BREAK
48
+ from docx.shared import Mm, Pt, RGBColor
49
+
50
+
51
+ def apply_page(doc, page: dict) -> None:
52
+ section = doc.sections[0]
53
+ if "width_mm" in page:
54
+ section.page_width = Mm(page["width_mm"])
55
+ if "height_mm" in page:
56
+ section.page_height = Mm(page["height_mm"])
57
+ m = page.get("margins_mm", {})
58
+ for side in ("top", "bottom", "left", "right"):
59
+ if side in m:
60
+ setattr(section, f"{side}_margin", Mm(m[side]))
61
+
62
+
63
+ def add_styles(doc, styles: list) -> None:
64
+ for s in styles:
65
+ style = doc.styles.add_style(s["name"], WD_STYLE_TYPE.PARAGRAPH)
66
+ if s.get("base"):
67
+ style.base_style = doc.styles[s["base"]]
68
+ font = style.font
69
+ if s.get("font"):
70
+ font.name = s["font"]
71
+ if s.get("size_pt"):
72
+ font.size = Pt(s["size_pt"])
73
+ if s.get("bold") is not None:
74
+ font.bold = s["bold"]
75
+ if s.get("italic") is not None:
76
+ font.italic = s["italic"]
77
+ if s.get("color"):
78
+ font.color.rgb = RGBColor.from_string(s["color"])
79
+
80
+
81
+ def add_runs(para, block: dict) -> None:
82
+ runs = block.get("runs")
83
+ if runs is None:
84
+ runs = [{"text": block.get("text", "")}]
85
+ for r in runs:
86
+ run = para.add_run(r.get("text", ""))
87
+ if r.get("bold"):
88
+ run.bold = True
89
+ if r.get("italic"):
90
+ run.italic = True
91
+ if r.get("underline"):
92
+ run.underline = True
93
+
94
+
95
+ def add_block(doc, block: dict) -> None:
96
+ btype = block["type"]
97
+ if btype == "heading":
98
+ doc.add_heading(block.get("text", ""), level=block.get("level", 1))
99
+ elif btype == "paragraph":
100
+ para = doc.add_paragraph(style=block.get("style"))
101
+ add_runs(para, block)
102
+ elif btype == "bullet_list":
103
+ for item in block.get("items", []):
104
+ doc.add_paragraph(item, style="List Bullet")
105
+ elif btype == "numbered_list":
106
+ for item in block.get("items", []):
107
+ doc.add_paragraph(item, style="List Number")
108
+ elif btype == "table":
109
+ header = block.get("header", [])
110
+ rows = block.get("rows", [])
111
+ ncols = len(header) if header else (len(rows[0]) if rows else 1)
112
+ table = doc.add_table(rows=0, cols=ncols)
113
+ table.style = block.get("style", "Table Grid")
114
+ if header:
115
+ cells = table.add_row().cells
116
+ for i, text in enumerate(header):
117
+ cells[i].text = str(text)
118
+ if block.get("header_bold", True):
119
+ for para in cells[i].paragraphs:
120
+ for run in para.runs:
121
+ run.bold = True
122
+ for row in rows:
123
+ cells = table.add_row().cells
124
+ for i, text in enumerate(row):
125
+ cells[i].text = str(text)
126
+ elif btype == "image":
127
+ width = Mm(block["width_mm"]) if block.get("width_mm") else None
128
+ doc.add_picture(block["path"], width=width)
129
+ elif btype == "page_break":
130
+ doc.add_paragraph().add_run().add_break(WD_BREAK.PAGE)
131
+ elif btype == "toc":
132
+ from docx_edit import _add_field
133
+ para = doc.add_paragraph()
134
+ _add_field(para, r' TOC \o "1-3" \h \z \u ',
135
+ "Table of contents - open in Word/LibreOffice and "
136
+ "update fields to populate.")
137
+ else:
138
+ raise ValueError(f"unknown block type: {btype}")
139
+
140
+
141
+ def main() -> int:
142
+ ap = argparse.ArgumentParser(
143
+ description="Create a .docx from a JSON spec.",
144
+ epilog="See the module docstring (top of this file) for the spec format.")
145
+ ap.add_argument("spec", help="path to JSON spec file")
146
+ ap.add_argument("output", help="path of .docx to write")
147
+ args = ap.parse_args()
148
+
149
+ with open(args.spec, encoding="utf-8") as f:
150
+ spec = json.load(f)
151
+
152
+ doc = Document()
153
+ if spec.get("page"):
154
+ apply_page(doc, spec["page"])
155
+ if spec.get("styles"):
156
+ add_styles(doc, spec["styles"])
157
+ if spec.get("header"):
158
+ doc.sections[0].header.paragraphs[0].text = spec["header"]
159
+ if spec.get("footer"):
160
+ doc.sections[0].footer.paragraphs[0].text = spec["footer"]
161
+ for block in spec.get("blocks", []):
162
+ add_block(doc, block)
163
+ if spec.get("footer_page_numbers"):
164
+ from docx_edit import _add_field
165
+ para = doc.sections[0].footer.paragraphs[0]
166
+ para.add_run("Page ")
167
+ _add_field(para, " PAGE ", "1")
168
+ para.add_run(" of ")
169
+ _add_field(para, " NUMPAGES ", "1")
170
+ doc.save(args.output)
171
+ print(json.dumps({"ok": True, "output": args.output,
172
+ "blocks": len(spec.get("blocks", []))}))
173
+ return 0
174
+
175
+
176
+ if __name__ == "__main__":
177
+ sys.exit(main())