@kortix/sandbox 0.4.1

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 (246) hide show
  1. package/config/customize.sh +143 -0
  2. package/config/kortix-env-setup.sh +25 -0
  3. package/kortix-master/package.json +22 -0
  4. package/kortix-master/src/config.ts +22 -0
  5. package/kortix-master/src/index.ts +44 -0
  6. package/kortix-master/src/routes/env.ts +65 -0
  7. package/kortix-master/src/routes/proxy.ts +108 -0
  8. package/kortix-master/src/routes/update.ts +185 -0
  9. package/kortix-master/src/services/proxy.ts +43 -0
  10. package/kortix-master/src/services/secret-store.ts +156 -0
  11. package/kortix-master/tsconfig.json +14 -0
  12. package/opencode/agents/kortix-browser.md +142 -0
  13. package/opencode/agents/kortix-build.md +62 -0
  14. package/opencode/agents/kortix-explore.md +66 -0
  15. package/opencode/agents/kortix-image-gen.md +33 -0
  16. package/opencode/agents/kortix-main.md +450 -0
  17. package/opencode/agents/kortix-plan.md +100 -0
  18. package/opencode/agents/kortix-research.md +84 -0
  19. package/opencode/agents/kortix-sheets.md +61 -0
  20. package/opencode/agents/kortix-slides.md +64 -0
  21. package/opencode/agents/kortix-web-dev.md +572 -0
  22. package/opencode/commands/email.md +36 -0
  23. package/opencode/commands/init.md +43 -0
  24. package/opencode/commands/journal.md +44 -0
  25. package/opencode/commands/memory-init.md +81 -0
  26. package/opencode/commands/memory-search.md +50 -0
  27. package/opencode/commands/memory-status.md +56 -0
  28. package/opencode/commands/research.md +36 -0
  29. package/opencode/commands/search.md +38 -0
  30. package/opencode/commands/slides.md +32 -0
  31. package/opencode/commands/spreadsheet.md +30 -0
  32. package/opencode/memory.json +37 -0
  33. package/opencode/ocx.jsonc +10 -0
  34. package/opencode/opencode.jsonc +103 -0
  35. package/opencode/package.json +25 -0
  36. package/opencode/patches/apply.sh +19 -0
  37. package/opencode/patches/opencode-pty-spawn.txt +49 -0
  38. package/opencode/plugin/background-agents.ts.disabled +483 -0
  39. package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
  40. package/opencode/plugin/kdco-primitives/index.ts +26 -0
  41. package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
  42. package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
  43. package/opencode/plugin/kdco-primitives/shell.ts +138 -0
  44. package/opencode/plugin/kdco-primitives/temp.ts +36 -0
  45. package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
  46. package/opencode/plugin/kdco-primitives/types.ts +13 -0
  47. package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
  48. package/opencode/plugin/memory.ts +306 -0
  49. package/opencode/plugin/worktree/state.ts +412 -0
  50. package/opencode/plugin/worktree/terminal.ts +1002 -0
  51. package/opencode/plugin/worktree.ts +861 -0
  52. package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
  53. package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
  54. package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
  55. package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
  56. package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
  57. package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
  58. package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
  59. package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
  60. package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
  61. package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
  62. package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
  63. package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
  64. package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
  65. package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
  66. package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
  67. package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
  68. package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
  69. package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
  70. package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
  71. package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
  72. package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
  73. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
  74. package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
  75. package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
  76. package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
  77. package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
  78. package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
  79. package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
  80. package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
  81. package/opencode/skills/KORTIX-email/SKILL.md +145 -0
  82. package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
  83. package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
  84. package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
  85. package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
  86. package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
  87. package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
  88. package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
  89. package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
  90. package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
  91. package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
  92. package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
  93. package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
  94. package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
  95. package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
  96. package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
  97. package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
  98. package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
  99. package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
  100. package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
  101. package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
  102. package/opencode/skills/KORTIX-pdf/forms.md +36 -0
  103. package/opencode/skills/KORTIX-pdf/reference.md +105 -0
  104. package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
  105. package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
  106. package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
  107. package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
  108. package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
  109. package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
  110. package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
  111. package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  112. package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
  113. package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
  114. package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
  115. package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
  116. package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
  117. package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
  118. package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
  119. package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
  120. package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
  121. package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
  122. package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
  123. package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
  124. package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
  125. package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
  126. package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
  127. package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
  128. package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
  129. package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
  130. package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
  131. package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
  132. package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
  133. package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
  134. package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
  135. package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
  136. package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
  137. package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
  138. package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
  139. package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
  140. package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
  141. package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
  142. package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
  143. package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
  144. package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
  145. package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
  146. package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
  147. package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
  148. package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
  149. package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
  150. package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
  151. package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
  152. package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
  153. package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
  154. package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
  155. package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
  156. package/opencode/skills/KORTIX-session-search/Untitled +1 -0
  157. package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
  158. package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
  159. package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
  160. package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
  161. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
  162. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
  163. package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  164. package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
  165. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  166. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  167. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  168. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  169. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  170. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  171. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  172. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  173. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  174. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  175. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  176. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  177. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  178. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  179. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  180. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  181. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  182. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  183. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  184. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  185. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  186. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  187. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  188. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  189. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  190. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  191. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  192. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  193. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  194. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  195. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  196. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  197. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  198. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  199. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  200. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  201. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  202. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  203. package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  204. package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
  205. package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
  206. package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
  207. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
  208. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
  209. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
  210. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
  211. package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
  212. package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
  213. package/opencode/tools/image-gen.ts +342 -0
  214. package/opencode/tools/image-search.ts +190 -0
  215. package/opencode/tools/memory-get.ts +168 -0
  216. package/opencode/tools/memory-search.ts +247 -0
  217. package/opencode/tools/presentation-gen.ts +723 -0
  218. package/opencode/tools/scrape-webpage.ts +115 -0
  219. package/opencode/tools/scripts/.python-version +1 -0
  220. package/opencode/tools/scripts/convert_pdf.py +184 -0
  221. package/opencode/tools/scripts/convert_pptx.py +562 -0
  222. package/opencode/tools/scripts/pyproject.toml +11 -0
  223. package/opencode/tools/scripts/uv.lock +287 -0
  224. package/opencode/tools/scripts/validate_slide.py +74 -0
  225. package/opencode/tools/show-user.ts +217 -0
  226. package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
  227. package/opencode/tools/tests/image-gen.test.ts +215 -0
  228. package/opencode/tools/tests/image-search.test.ts +125 -0
  229. package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
  230. package/opencode/tools/tests/presentation-gen.test.ts +389 -0
  231. package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
  232. package/opencode/tools/tests/show-user.test.ts +241 -0
  233. package/opencode/tools/tests/video-gen.test.ts +110 -0
  234. package/opencode/tools/tests/web-search.test.ts +106 -0
  235. package/opencode/tools/video-gen.ts +200 -0
  236. package/opencode/tools/web-search.ts +153 -0
  237. package/opencode/tsconfig.json +29 -0
  238. package/package.json +36 -0
  239. package/patch-agent-browser.js +100 -0
  240. package/postinstall.sh +88 -0
  241. package/services/KORTIX-presentation-viewer/run +37 -0
  242. package/services/agent-browser-viewer/run +48 -0
  243. package/services/kortix-master/run +16 -0
  244. package/services/lss-sync/run +22 -0
  245. package/services/opencode-serve/run +25 -0
  246. package/services/opencode-web/run +21 -0
@@ -0,0 +1,104 @@
1
+ """Accept all tracked changes in a DOCX file using LibreOffice.
2
+
3
+ Requires LibreOffice (soffice) to be installed.
4
+ """
5
+
6
+ import argparse
7
+ import logging
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+ from office.soffice import get_soffice_env
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ LIBREOFFICE_PROFILE = "/tmp/libreoffice_docx_profile"
17
+ MACRO_DIR = f"{LIBREOFFICE_PROFILE}/user/basic/Standard"
18
+
19
+ ACCEPT_CHANGES_MACRO = """<?xml version="1.0" encoding="UTF-8"?>
20
+ <!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
21
+ <script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
22
+ Sub AcceptAllTrackedChanges()
23
+ Dim document As Object
24
+ Dim dispatcher As Object
25
+
26
+ document = ThisComponent.CurrentController.Frame
27
+ dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
28
+
29
+ dispatcher.executeDispatch(document, ".uno:AcceptAllTrackedChanges", "", 0, Array())
30
+ ThisComponent.store()
31
+ ThisComponent.close(True)
32
+ End Sub
33
+ </script:module>"""
34
+
35
+
36
+ def accept_changes(input_file: str, output_file: str) -> tuple[None, str]:
37
+ input_path = Path(input_file)
38
+ output_path = Path(output_file)
39
+
40
+ if not input_path.exists():
41
+ return None, f"Error: Input file not found: {input_file}"
42
+ if not input_path.suffix.lower() == ".docx":
43
+ return None, f"Error: Input file is not a DOCX file: {input_file}"
44
+
45
+ try:
46
+ output_path.parent.mkdir(parents=True, exist_ok=True)
47
+ shutil.copy2(input_path, output_path)
48
+ except Exception as e:
49
+ return None, f"Error: Failed to copy input file: {e}"
50
+
51
+ if not _setup_libreoffice_macro():
52
+ return None, "Error: Failed to setup LibreOffice macro"
53
+
54
+ cmd = [
55
+ "soffice", "--headless",
56
+ f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
57
+ "--norestore",
58
+ "vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application",
59
+ str(output_path.absolute()),
60
+ ]
61
+
62
+ try:
63
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, check=False, env=get_soffice_env())
64
+ except subprocess.TimeoutExpired:
65
+ return None, f"Successfully accepted all tracked changes: {input_file} -> {output_file}"
66
+
67
+ if result.returncode != 0:
68
+ return None, f"Error: LibreOffice failed: {result.stderr}"
69
+
70
+ return None, f"Successfully accepted all tracked changes: {input_file} -> {output_file}"
71
+
72
+
73
+ def _setup_libreoffice_macro() -> bool:
74
+ macro_dir = Path(MACRO_DIR)
75
+ macro_file = macro_dir / "Module1.xba"
76
+
77
+ if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text():
78
+ return True
79
+
80
+ if not macro_dir.exists():
81
+ subprocess.run(
82
+ ["soffice", "--headless", f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}", "--terminate_after_init"],
83
+ capture_output=True, timeout=10, check=False, env=get_soffice_env(),
84
+ )
85
+ macro_dir.mkdir(parents=True, exist_ok=True)
86
+
87
+ try:
88
+ macro_file.write_text(ACCEPT_CHANGES_MACRO)
89
+ return True
90
+ except Exception as e:
91
+ logger.warning(f"Failed to setup LibreOffice macro: {e}")
92
+ return False
93
+
94
+
95
+ if __name__ == "__main__":
96
+ parser = argparse.ArgumentParser(description="Accept all tracked changes in a DOCX file")
97
+ parser.add_argument("input_file", help="Input DOCX file with tracked changes")
98
+ parser.add_argument("output_file", help="Output DOCX file (clean, no tracked changes)")
99
+ args = parser.parse_args()
100
+
101
+ _, message = accept_changes(args.input_file, args.output_file)
102
+ print(message)
103
+ if "Error" in message:
104
+ raise SystemExit(1)
@@ -0,0 +1,244 @@
1
+ """Add comments to DOCX documents.
2
+
3
+ Usage:
4
+ python comment.py unpacked/ 0 "Comment text"
5
+ python comment.py unpacked/ 1 "Reply text" --parent 0
6
+
7
+ Text should be pre-escaped XML (e.g., &amp; for &, &#x2019; for smart quotes).
8
+
9
+ After running, add markers to document.xml:
10
+ <w:commentRangeStart w:id="0"/>
11
+ ... commented content ...
12
+ <w:commentRangeEnd w:id="0"/>
13
+ <w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
14
+ """
15
+
16
+ import argparse
17
+ import random
18
+ import shutil
19
+ import sys
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+
23
+ import defusedxml.minidom
24
+
25
+ TEMPLATE_DIR = Path(__file__).parent / "templates"
26
+ NS = {
27
+ "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
28
+ "w14": "http://schemas.microsoft.com/office/word/2010/wordml",
29
+ "w15": "http://schemas.microsoft.com/office/word/2012/wordml",
30
+ "w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid",
31
+ "w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex",
32
+ }
33
+
34
+ COMMENT_XML = """\
35
+ <w:comment w:id="{id}" w:author="{author}" w:date="{date}" w:initials="{initials}">
36
+ <w:p w14:paraId="{para_id}" w14:textId="77777777">
37
+ <w:r>
38
+ <w:rPr><w:rStyle w:val="CommentReference"/></w:rPr>
39
+ <w:annotationRef/>
40
+ </w:r>
41
+ <w:r>
42
+ <w:rPr>
43
+ <w:color w:val="000000"/>
44
+ <w:sz w:val="20"/>
45
+ <w:szCs w:val="20"/>
46
+ </w:rPr>
47
+ <w:t>{text}</w:t>
48
+ </w:r>
49
+ </w:p>
50
+ </w:comment>"""
51
+
52
+ SMART_QUOTE_ENTITIES = {
53
+ "\u201c": "&#x201C;",
54
+ "\u201d": "&#x201D;",
55
+ "\u2018": "&#x2018;",
56
+ "\u2019": "&#x2019;",
57
+ }
58
+
59
+
60
+ def _generate_hex_id() -> str:
61
+ return f"{random.randint(0, 0x7FFFFFFE):08X}"
62
+
63
+
64
+ def _encode_smart_quotes(text: str) -> str:
65
+ for char, entity in SMART_QUOTE_ENTITIES.items():
66
+ text = text.replace(char, entity)
67
+ return text
68
+
69
+
70
+ def _append_xml(xml_path: Path, root_tag: str, content: str) -> None:
71
+ dom = defusedxml.minidom.parseString(xml_path.read_text(encoding="utf-8"))
72
+ root = dom.getElementsByTagName(root_tag)[0]
73
+ ns_attrs = " ".join(f'xmlns:{k}="{v}"' for k, v in NS.items())
74
+ wrapper_dom = defusedxml.minidom.parseString(f"<root {ns_attrs}>{content}</root>")
75
+ for child in wrapper_dom.documentElement.childNodes:
76
+ if child.nodeType == child.ELEMENT_NODE:
77
+ root.appendChild(dom.importNode(child, True))
78
+ output = _encode_smart_quotes(dom.toxml(encoding="UTF-8").decode("utf-8"))
79
+ xml_path.write_text(output, encoding="utf-8")
80
+
81
+
82
+ def _find_para_id(comments_path: Path, comment_id: int) -> str | None:
83
+ dom = defusedxml.minidom.parseString(comments_path.read_text(encoding="utf-8"))
84
+ for c in dom.getElementsByTagName("w:comment"):
85
+ if c.getAttribute("w:id") == str(comment_id):
86
+ for p in c.getElementsByTagName("w:p"):
87
+ if pid := p.getAttribute("w14:paraId"):
88
+ return pid
89
+ return None
90
+
91
+
92
+ def _get_next_rid(rels_path: Path) -> int:
93
+ dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
94
+ max_rid = 0
95
+ for rel in dom.getElementsByTagName("Relationship"):
96
+ rid = rel.getAttribute("Id")
97
+ if rid and rid.startswith("rId"):
98
+ try:
99
+ max_rid = max(max_rid, int(rid[3:]))
100
+ except ValueError:
101
+ pass
102
+ return max_rid + 1
103
+
104
+
105
+ def _has_relationship(rels_path: Path, target: str) -> bool:
106
+ dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
107
+ for rel in dom.getElementsByTagName("Relationship"):
108
+ if rel.getAttribute("Target") == target:
109
+ return True
110
+ return False
111
+
112
+
113
+ def _has_content_type(ct_path: Path, part_name: str) -> bool:
114
+ dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8"))
115
+ for override in dom.getElementsByTagName("Override"):
116
+ if override.getAttribute("PartName") == part_name:
117
+ return True
118
+ return False
119
+
120
+
121
+ def _ensure_comment_relationships(unpacked_dir: Path) -> None:
122
+ rels_path = unpacked_dir / "word" / "_rels" / "document.xml.rels"
123
+ if not rels_path.exists():
124
+ return
125
+ if _has_relationship(rels_path, "comments.xml"):
126
+ return
127
+
128
+ dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
129
+ root = dom.documentElement
130
+ next_rid = _get_next_rid(rels_path)
131
+
132
+ rels = [
133
+ ("http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", "comments.xml"),
134
+ ("http://schemas.microsoft.com/office/2011/relationships/commentsExtended", "commentsExtended.xml"),
135
+ ("http://schemas.microsoft.com/office/2016/09/relationships/commentsIds", "commentsIds.xml"),
136
+ ("http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible", "commentsExtensible.xml"),
137
+ ]
138
+
139
+ for rel_type, target in rels:
140
+ rel = dom.createElement("Relationship")
141
+ rel.setAttribute("Id", f"rId{next_rid}")
142
+ rel.setAttribute("Type", rel_type)
143
+ rel.setAttribute("Target", target)
144
+ root.appendChild(rel)
145
+ next_rid += 1
146
+
147
+ rels_path.write_bytes(dom.toxml(encoding="UTF-8"))
148
+
149
+
150
+ def _ensure_comment_content_types(unpacked_dir: Path) -> None:
151
+ ct_path = unpacked_dir / "[Content_Types].xml"
152
+ if not ct_path.exists():
153
+ return
154
+ if _has_content_type(ct_path, "/word/comments.xml"):
155
+ return
156
+
157
+ dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8"))
158
+ root = dom.documentElement
159
+
160
+ overrides = [
161
+ ("/word/comments.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"),
162
+ ("/word/commentsExtended.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml"),
163
+ ("/word/commentsIds.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml"),
164
+ ("/word/commentsExtensible.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml"),
165
+ ]
166
+
167
+ for part_name, content_type in overrides:
168
+ override = dom.createElement("Override")
169
+ override.setAttribute("PartName", part_name)
170
+ override.setAttribute("ContentType", content_type)
171
+ root.appendChild(override)
172
+
173
+ ct_path.write_bytes(dom.toxml(encoding="UTF-8"))
174
+
175
+
176
+ def add_comment(
177
+ unpacked_dir: str,
178
+ comment_id: int,
179
+ text: str,
180
+ author: str = "Kortix Agent",
181
+ initials: str = "K",
182
+ parent_id: int | None = None,
183
+ ) -> tuple[str, str]:
184
+ word = Path(unpacked_dir) / "word"
185
+ if not word.exists():
186
+ return "", f"Error: {word} not found"
187
+
188
+ para_id, durable_id = _generate_hex_id(), _generate_hex_id()
189
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
190
+
191
+ comments = word / "comments.xml"
192
+ first_comment = not comments.exists()
193
+ if first_comment:
194
+ shutil.copy(TEMPLATE_DIR / "comments.xml", comments)
195
+ _ensure_comment_relationships(Path(unpacked_dir))
196
+ _ensure_comment_content_types(Path(unpacked_dir))
197
+ _append_xml(comments, "w:comments", COMMENT_XML.format(
198
+ id=comment_id, author=author, date=ts, initials=initials, para_id=para_id, text=text,
199
+ ))
200
+
201
+ ext = word / "commentsExtended.xml"
202
+ if not ext.exists():
203
+ shutil.copy(TEMPLATE_DIR / "commentsExtended.xml", ext)
204
+ if parent_id is not None:
205
+ parent_para = _find_para_id(comments, parent_id)
206
+ if not parent_para:
207
+ return "", f"Error: Parent comment {parent_id} not found"
208
+ _append_xml(ext, "w15:commentsEx", f'<w15:commentEx w15:paraId="{para_id}" w15:paraIdParent="{parent_para}" w15:done="0"/>')
209
+ else:
210
+ _append_xml(ext, "w15:commentsEx", f'<w15:commentEx w15:paraId="{para_id}" w15:done="0"/>')
211
+
212
+ ids = word / "commentsIds.xml"
213
+ if not ids.exists():
214
+ shutil.copy(TEMPLATE_DIR / "commentsIds.xml", ids)
215
+ _append_xml(ids, "w16cid:commentsIds", f'<w16cid:commentId w16cid:paraId="{para_id}" w16cid:durableId="{durable_id}"/>')
216
+
217
+ extensible = word / "commentsExtensible.xml"
218
+ if not extensible.exists():
219
+ shutil.copy(TEMPLATE_DIR / "commentsExtensible.xml", extensible)
220
+ _append_xml(extensible, "w16cex:commentsExtensible", f'<w16cex:commentExtensible w16cex:durableId="{durable_id}" w16cex:dateUtc="{ts}"/>')
221
+
222
+ action = "reply" if parent_id is not None else "comment"
223
+ return para_id, f"Added {action} {comment_id} (para_id={para_id})"
224
+
225
+
226
+ if __name__ == "__main__":
227
+ p = argparse.ArgumentParser(description="Add comments to DOCX documents")
228
+ p.add_argument("unpacked_dir", help="Unpacked DOCX directory")
229
+ p.add_argument("comment_id", type=int, help="Comment ID (must be unique)")
230
+ p.add_argument("text", help="Comment text")
231
+ p.add_argument("--author", default="Kortix Agent", help="Author name")
232
+ p.add_argument("--initials", default="K", help="Author initials")
233
+ p.add_argument("--parent", type=int, help="Parent comment ID (for replies)")
234
+ args = p.parse_args()
235
+
236
+ para_id, msg = add_comment(args.unpacked_dir, args.comment_id, args.text, args.author, args.initials, args.parent)
237
+ print(msg)
238
+ if "Error" in msg:
239
+ sys.exit(1)
240
+ cid = args.comment_id
241
+ if args.parent is not None:
242
+ print(f"\nNest markers inside parent {args.parent}'s markers:\n <w:commentRangeStart w:id=\"{args.parent}\"/><w:commentRangeStart w:id=\"{cid}\"/>\n <w:r>...</w:r>\n <w:commentRangeEnd w:id=\"{cid}\"/><w:commentRangeEnd w:id=\"{args.parent}\"/>")
243
+ else:
244
+ print(f"\nAdd to document.xml:\n <w:commentRangeStart w:id=\"{cid}\"/>\n <w:r>...</w:r>\n <w:commentRangeEnd w:id=\"{cid}\"/>\n <w:r><w:rPr><w:rStyle w:val=\"CommentReference\"/></w:rPr><w:commentReference w:id=\"{cid}\"/></w:r>")
@@ -0,0 +1,199 @@
1
+ """Merge adjacent runs with identical formatting in DOCX.
2
+
3
+ Merges adjacent <w:r> elements that have identical <w:rPr> properties.
4
+ Works on runs in paragraphs and inside tracked changes (<w:ins>, <w:del>).
5
+
6
+ Also:
7
+ - Removes rsid attributes from runs (revision metadata that doesn't affect rendering)
8
+ - Removes proofErr elements (spell/grammar markers that block merging)
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+ import defusedxml.minidom
14
+
15
+
16
+ def merge_runs(input_dir: str) -> tuple[int, str]:
17
+ doc_xml = Path(input_dir) / "word" / "document.xml"
18
+
19
+ if not doc_xml.exists():
20
+ return 0, f"Error: {doc_xml} not found"
21
+
22
+ try:
23
+ dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
24
+ root = dom.documentElement
25
+
26
+ _remove_elements(root, "proofErr")
27
+ _strip_run_rsid_attrs(root)
28
+
29
+ containers = {run.parentNode for run in _find_elements(root, "r")}
30
+
31
+ merge_count = 0
32
+ for container in containers:
33
+ merge_count += _merge_runs_in(container)
34
+
35
+ doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
36
+ return merge_count, f"Merged {merge_count} runs"
37
+
38
+ except Exception as e:
39
+ return 0, f"Error: {e}"
40
+
41
+
42
+
43
+
44
+ def _find_elements(root, tag: str) -> list:
45
+ results = []
46
+
47
+ def traverse(node):
48
+ if node.nodeType == node.ELEMENT_NODE:
49
+ name = node.localName or node.tagName
50
+ if name == tag or name.endswith(f":{tag}"):
51
+ results.append(node)
52
+ for child in node.childNodes:
53
+ traverse(child)
54
+
55
+ traverse(root)
56
+ return results
57
+
58
+
59
+ def _get_child(parent, tag: str):
60
+ for child in parent.childNodes:
61
+ if child.nodeType == child.ELEMENT_NODE:
62
+ name = child.localName or child.tagName
63
+ if name == tag or name.endswith(f":{tag}"):
64
+ return child
65
+ return None
66
+
67
+
68
+ def _get_children(parent, tag: str) -> list:
69
+ results = []
70
+ for child in parent.childNodes:
71
+ if child.nodeType == child.ELEMENT_NODE:
72
+ name = child.localName or child.tagName
73
+ if name == tag or name.endswith(f":{tag}"):
74
+ results.append(child)
75
+ return results
76
+
77
+
78
+ def _is_adjacent(elem1, elem2) -> bool:
79
+ node = elem1.nextSibling
80
+ while node:
81
+ if node == elem2:
82
+ return True
83
+ if node.nodeType == node.ELEMENT_NODE:
84
+ return False
85
+ if node.nodeType == node.TEXT_NODE and node.data.strip():
86
+ return False
87
+ node = node.nextSibling
88
+ return False
89
+
90
+
91
+
92
+
93
+ def _remove_elements(root, tag: str):
94
+ for elem in _find_elements(root, tag):
95
+ if elem.parentNode:
96
+ elem.parentNode.removeChild(elem)
97
+
98
+
99
+ def _strip_run_rsid_attrs(root):
100
+ for run in _find_elements(root, "r"):
101
+ for attr in list(run.attributes.values()):
102
+ if "rsid" in attr.name.lower():
103
+ run.removeAttribute(attr.name)
104
+
105
+
106
+
107
+
108
+ def _merge_runs_in(container) -> int:
109
+ merge_count = 0
110
+ run = _first_child_run(container)
111
+
112
+ while run:
113
+ while True:
114
+ next_elem = _next_element_sibling(run)
115
+ if next_elem and _is_run(next_elem) and _can_merge(run, next_elem):
116
+ _merge_run_content(run, next_elem)
117
+ container.removeChild(next_elem)
118
+ merge_count += 1
119
+ else:
120
+ break
121
+
122
+ _consolidate_text(run)
123
+ run = _next_sibling_run(run)
124
+
125
+ return merge_count
126
+
127
+
128
+ def _first_child_run(container):
129
+ for child in container.childNodes:
130
+ if child.nodeType == child.ELEMENT_NODE and _is_run(child):
131
+ return child
132
+ return None
133
+
134
+
135
+ def _next_element_sibling(node):
136
+ sibling = node.nextSibling
137
+ while sibling:
138
+ if sibling.nodeType == sibling.ELEMENT_NODE:
139
+ return sibling
140
+ sibling = sibling.nextSibling
141
+ return None
142
+
143
+
144
+ def _next_sibling_run(node):
145
+ sibling = node.nextSibling
146
+ while sibling:
147
+ if sibling.nodeType == sibling.ELEMENT_NODE:
148
+ if _is_run(sibling):
149
+ return sibling
150
+ sibling = sibling.nextSibling
151
+ return None
152
+
153
+
154
+ def _is_run(node) -> bool:
155
+ name = node.localName or node.tagName
156
+ return name == "r" or name.endswith(":r")
157
+
158
+
159
+ def _can_merge(run1, run2) -> bool:
160
+ rpr1 = _get_child(run1, "rPr")
161
+ rpr2 = _get_child(run2, "rPr")
162
+
163
+ if (rpr1 is None) != (rpr2 is None):
164
+ return False
165
+ if rpr1 is None:
166
+ return True
167
+ return rpr1.toxml() == rpr2.toxml()
168
+
169
+
170
+ def _merge_run_content(target, source):
171
+ for child in list(source.childNodes):
172
+ if child.nodeType == child.ELEMENT_NODE:
173
+ name = child.localName or child.tagName
174
+ if name != "rPr" and not name.endswith(":rPr"):
175
+ target.appendChild(child)
176
+
177
+
178
+ def _consolidate_text(run):
179
+ t_elements = _get_children(run, "t")
180
+
181
+ for i in range(len(t_elements) - 1, 0, -1):
182
+ curr, prev = t_elements[i], t_elements[i - 1]
183
+
184
+ if _is_adjacent(prev, curr):
185
+ prev_text = prev.firstChild.data if prev.firstChild else ""
186
+ curr_text = curr.firstChild.data if curr.firstChild else ""
187
+ merged = prev_text + curr_text
188
+
189
+ if prev.firstChild:
190
+ prev.firstChild.data = merged
191
+ else:
192
+ prev.appendChild(run.ownerDocument.createTextNode(merged))
193
+
194
+ if merged.startswith(" ") or merged.endswith(" "):
195
+ prev.setAttribute("xml:space", "preserve")
196
+ elif prev.hasAttribute("xml:space"):
197
+ prev.removeAttribute("xml:space")
198
+
199
+ run.removeChild(curr)