sophhub 0.2.1 → 0.2.2

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 (187) hide show
  1. package/package.json +1 -1
  2. package/skills/compact-context/skill.json +20 -0
  3. package/skills/compact-context/src/SKILL.md +133 -0
  4. package/skills/compact-context/src/scripts/check.sh +381 -0
  5. package/skills/compact-context/src/scripts/set-keep-recent.mjs +1337 -0
  6. package/skills/compact-context/src/scripts/setup.sh +96 -0
  7. package/skills/feishu-notes-assistant-universal/skill.json +20 -0
  8. package/skills/feishu-notes-assistant-universal/src/README.md +55 -0
  9. package/skills/feishu-notes-assistant-universal/src/SKILL.md +159 -0
  10. package/skills/feishu-notes-assistant-universal/src/bin/linux-amd64/lark-cli-openclaw +0 -0
  11. package/skills/feishu-notes-assistant-universal/src/bin/linux-arm64/lark-cli-openclaw +0 -0
  12. package/skills/feishu-notes-assistant-universal/src/scripts/_resolve_lark_cli.py +58 -0
  13. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_meeting_minutes.py +462 -0
  14. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud.py +547 -0
  15. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud_test.py +181 -0
  16. package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.py +80 -0
  17. package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.sh +5 -0
  18. package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.py +32 -0
  19. package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.sh +5 -0
  20. package/skills/image-classify/skill.json +5 -5
  21. package/skills/image-classify/src/SKILL.md +60 -67
  22. package/skills/image-classify/src/scripts/face_search.py +400 -15
  23. package/skills/image-classify/src/scripts/send_dm_message.py +332 -0
  24. package/skills/md2pdf-converter/skill.json +20 -0
  25. package/skills/md2pdf-converter/src/SKILL.md +244 -0
  26. package/skills/md2pdf-converter/src/_meta.json +6 -0
  27. package/skills/md2pdf-converter/src/scripts/generate_emoji_mapping.py +74 -0
  28. package/skills/md2pdf-converter/src/scripts/md2pdf-local.sh +291 -0
  29. package/skills/sophnet-bot-client/skill.json +20 -0
  30. package/skills/sophnet-bot-client/src/SKILL.md +255 -0
  31. package/skills/sophnet-bot-client/src/pyproject.toml +13 -0
  32. package/skills/sophnet-bot-client/src/scripts/__init__.py +0 -0
  33. package/skills/sophnet-bot-client/src/scripts/bot_client_proxy.py +165 -0
  34. package/skills/sophnet-bot-client/src/scripts/bot_client_safe.sh +29 -0
  35. package/skills/sophnet-bot-client/src/scripts/bot_client_setup.py +502 -0
  36. package/skills/sophnet-bot-client/src/tests/__init__.py +0 -0
  37. package/skills/sophnet-bot-client/src/tests/test_bot_client_proxy.py +255 -0
  38. package/skills/sophnet-bot-client/src/tests/test_bot_client_setup.py +679 -0
  39. package/skills/sophnet-bot-client/src/uv.lock +8 -0
  40. package/skills/sophnet-docx/skill.json +20 -0
  41. package/skills/sophnet-docx/src/SKILL.md +463 -0
  42. package/skills/sophnet-docx/src/package-lock.json +208 -0
  43. package/skills/sophnet-docx/src/package.json +16 -0
  44. package/skills/sophnet-docx/src/pyproject.toml +11 -0
  45. package/skills/sophnet-docx/src/scripts/__init__.py +1 -0
  46. package/skills/sophnet-docx/src/scripts/accept_changes.py +135 -0
  47. package/skills/sophnet-docx/src/scripts/comment.py +318 -0
  48. package/skills/sophnet-docx/src/scripts/ensure_uv_env.sh +68 -0
  49. package/skills/sophnet-docx/src/scripts/office/helpers/__init__.py +0 -0
  50. package/skills/sophnet-docx/src/scripts/office/helpers/merge_runs.py +199 -0
  51. package/skills/sophnet-docx/src/scripts/office/helpers/simplify_redlines.py +197 -0
  52. package/skills/sophnet-docx/src/scripts/office/pack.py +159 -0
  53. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  54. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  55. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  56. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  57. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  58. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  59. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  60. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  61. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  62. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  63. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  64. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  65. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  66. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  67. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  68. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  69. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  70. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  71. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  72. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  73. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  74. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  75. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  76. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  77. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  78. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  79. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  80. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  81. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  82. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  83. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  84. package/skills/sophnet-docx/src/scripts/office/schemas/mce/mc.xsd +75 -0
  85. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  86. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  87. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  88. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  89. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  90. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  91. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  92. package/skills/sophnet-docx/src/scripts/office/soffice.py +183 -0
  93. package/skills/sophnet-docx/src/scripts/office/unpack.py +132 -0
  94. package/skills/sophnet-docx/src/scripts/office/validate.py +111 -0
  95. package/skills/sophnet-docx/src/scripts/office/validators/__init__.py +15 -0
  96. package/skills/sophnet-docx/src/scripts/office/validators/base.py +847 -0
  97. package/skills/sophnet-docx/src/scripts/office/validators/docx.py +446 -0
  98. package/skills/sophnet-docx/src/scripts/office/validators/pptx.py +275 -0
  99. package/skills/sophnet-docx/src/scripts/office/validators/redlining.py +247 -0
  100. package/skills/sophnet-docx/src/scripts/templates/comments.xml +3 -0
  101. package/skills/sophnet-docx/src/scripts/templates/commentsExtended.xml +3 -0
  102. package/skills/sophnet-docx/src/scripts/templates/commentsExtensible.xml +3 -0
  103. package/skills/sophnet-docx/src/scripts/templates/commentsIds.xml +3 -0
  104. package/skills/sophnet-docx/src/scripts/templates/people.xml +3 -0
  105. package/skills/sophnet-docx/src/scripts/upload_file.sh +96 -0
  106. package/skills/sophnet-docx/src/uv.lock +320 -0
  107. package/skills/sophnet-pdf/skill.json +20 -0
  108. package/skills/sophnet-pdf/src/SKILL.md +413 -0
  109. package/skills/sophnet-pdf/src/forms.md +297 -0
  110. package/skills/sophnet-pdf/src/pyproject.toml +14 -0
  111. package/skills/sophnet-pdf/src/reference.md +612 -0
  112. package/skills/sophnet-pdf/src/scripts/check_bounding_boxes.py +65 -0
  113. package/skills/sophnet-pdf/src/scripts/check_fillable_fields.py +11 -0
  114. package/skills/sophnet-pdf/src/scripts/convert_pdf_to_images.py +33 -0
  115. package/skills/sophnet-pdf/src/scripts/create_validation_image.py +37 -0
  116. package/skills/sophnet-pdf/src/scripts/enhance_tutorial.py +558 -0
  117. package/skills/sophnet-pdf/src/scripts/ensure_uv_env.sh +68 -0
  118. package/skills/sophnet-pdf/src/scripts/extract_form_field_info.py +122 -0
  119. package/skills/sophnet-pdf/src/scripts/extract_form_structure.py +115 -0
  120. package/skills/sophnet-pdf/src/scripts/extract_pdf_content.py +35 -0
  121. package/skills/sophnet-pdf/src/scripts/fill_fillable_fields.py +98 -0
  122. package/skills/sophnet-pdf/src/scripts/fill_pdf_form_with_annotations.py +107 -0
  123. package/skills/sophnet-pdf/src/scripts/upload_file.sh +88 -0
  124. package/skills/sophnet-pdf/src/uv.lock +537 -0
  125. package/skills/sophnet-xlsx/skill.json +20 -0
  126. package/skills/sophnet-xlsx/src/SKILL.md +399 -0
  127. package/skills/sophnet-xlsx/src/pyproject.toml +11 -0
  128. package/skills/sophnet-xlsx/src/scripts/ensure_uv_env.sh +68 -0
  129. package/skills/sophnet-xlsx/src/scripts/office/helpers/__init__.py +0 -0
  130. package/skills/sophnet-xlsx/src/scripts/office/helpers/merge_runs.py +199 -0
  131. package/skills/sophnet-xlsx/src/scripts/office/helpers/simplify_redlines.py +197 -0
  132. package/skills/sophnet-xlsx/src/scripts/office/pack.py +159 -0
  133. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  134. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  135. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  136. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  137. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  138. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  139. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  140. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  141. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  142. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  143. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  144. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  145. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  146. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  147. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  148. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  149. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  150. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  151. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  152. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  153. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  154. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  155. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  156. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  157. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  158. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  159. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  160. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  161. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  162. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  163. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  164. package/skills/sophnet-xlsx/src/scripts/office/schemas/mce/mc.xsd +75 -0
  165. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  166. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  167. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  168. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  169. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  170. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  171. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  172. package/skills/sophnet-xlsx/src/scripts/office/soffice.py +183 -0
  173. package/skills/sophnet-xlsx/src/scripts/office/unpack.py +132 -0
  174. package/skills/sophnet-xlsx/src/scripts/office/validate.py +111 -0
  175. package/skills/sophnet-xlsx/src/scripts/office/validators/__init__.py +15 -0
  176. package/skills/sophnet-xlsx/src/scripts/office/validators/base.py +847 -0
  177. package/skills/sophnet-xlsx/src/scripts/office/validators/docx.py +446 -0
  178. package/skills/sophnet-xlsx/src/scripts/office/validators/pptx.py +275 -0
  179. package/skills/sophnet-xlsx/src/scripts/office/validators/redlining.py +247 -0
  180. package/skills/sophnet-xlsx/src/scripts/recalc.py +184 -0
  181. package/skills/sophnet-xlsx/src/scripts/upload_file.sh +96 -0
  182. package/skills/sophnet-xlsx/src/uv.lock +319 -0
  183. package/skills/wechat-article-publisher/skill.json +20 -0
  184. package/skills/wechat-article-publisher/src/SKILL.md +60 -0
  185. package/skills/wechat-article-publisher/src/config.json +7 -0
  186. package/skills/wechat-article-publisher/src/pyproject.toml +12 -0
  187. package/skills/wechat-article-publisher/src/scripts/publish_wechat.py +825 -0
@@ -0,0 +1,197 @@
1
+ """Simplify tracked changes by merging adjacent w:ins or w:del elements.
2
+
3
+ Merges adjacent <w:ins> elements from the same author into a single element.
4
+ Same for <w:del> elements. This makes heavily-redlined documents easier to
5
+ work with by reducing the number of tracked change wrappers.
6
+
7
+ Rules:
8
+ - Only merges w:ins with w:ins, w:del with w:del (same element type)
9
+ - Only merges if same author (ignores timestamp differences)
10
+ - Only merges if truly adjacent (only whitespace between them)
11
+ """
12
+
13
+ import xml.etree.ElementTree as ET
14
+ import zipfile
15
+ from pathlib import Path
16
+
17
+ import defusedxml.minidom
18
+
19
+ WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
20
+
21
+
22
+ def simplify_redlines(input_dir: str) -> tuple[int, str]:
23
+ doc_xml = Path(input_dir) / "word" / "document.xml"
24
+
25
+ if not doc_xml.exists():
26
+ return 0, f"Error: {doc_xml} not found"
27
+
28
+ try:
29
+ dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
30
+ root = dom.documentElement
31
+
32
+ merge_count = 0
33
+
34
+ containers = _find_elements(root, "p") + _find_elements(root, "tc")
35
+
36
+ for container in containers:
37
+ merge_count += _merge_tracked_changes_in(container, "ins")
38
+ merge_count += _merge_tracked_changes_in(container, "del")
39
+
40
+ doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
41
+ return merge_count, f"Simplified {merge_count} tracked changes"
42
+
43
+ except Exception as e:
44
+ return 0, f"Error: {e}"
45
+
46
+
47
+ def _merge_tracked_changes_in(container, tag: str) -> int:
48
+ merge_count = 0
49
+
50
+ tracked = [
51
+ child
52
+ for child in container.childNodes
53
+ if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag)
54
+ ]
55
+
56
+ if len(tracked) < 2:
57
+ return 0
58
+
59
+ i = 0
60
+ while i < len(tracked) - 1:
61
+ curr = tracked[i]
62
+ next_elem = tracked[i + 1]
63
+
64
+ if _can_merge_tracked(curr, next_elem):
65
+ _merge_tracked_content(curr, next_elem)
66
+ container.removeChild(next_elem)
67
+ tracked.pop(i + 1)
68
+ merge_count += 1
69
+ else:
70
+ i += 1
71
+
72
+ return merge_count
73
+
74
+
75
+ def _is_element(node, tag: str) -> bool:
76
+ name = node.localName or node.tagName
77
+ return name == tag or name.endswith(f":{tag}")
78
+
79
+
80
+ def _get_author(elem) -> str:
81
+ author = elem.getAttribute("w:author")
82
+ if not author:
83
+ for attr in elem.attributes.values():
84
+ if attr.localName == "author" or attr.name.endswith(":author"):
85
+ return attr.value
86
+ return author
87
+
88
+
89
+ def _can_merge_tracked(elem1, elem2) -> bool:
90
+ if _get_author(elem1) != _get_author(elem2):
91
+ return False
92
+
93
+ node = elem1.nextSibling
94
+ while node and node != elem2:
95
+ if node.nodeType == node.ELEMENT_NODE:
96
+ return False
97
+ if node.nodeType == node.TEXT_NODE and node.data.strip():
98
+ return False
99
+ node = node.nextSibling
100
+
101
+ return True
102
+
103
+
104
+ def _merge_tracked_content(target, source):
105
+ while source.firstChild:
106
+ child = source.firstChild
107
+ source.removeChild(child)
108
+ target.appendChild(child)
109
+
110
+
111
+ def _find_elements(root, tag: str) -> list:
112
+ results = []
113
+
114
+ def traverse(node):
115
+ if node.nodeType == node.ELEMENT_NODE:
116
+ name = node.localName or node.tagName
117
+ if name == tag or name.endswith(f":{tag}"):
118
+ results.append(node)
119
+ for child in node.childNodes:
120
+ traverse(child)
121
+
122
+ traverse(root)
123
+ return results
124
+
125
+
126
+ def get_tracked_change_authors(doc_xml_path: Path) -> dict[str, int]:
127
+ if not doc_xml_path.exists():
128
+ return {}
129
+
130
+ try:
131
+ tree = ET.parse(doc_xml_path)
132
+ root = tree.getroot()
133
+ except ET.ParseError:
134
+ return {}
135
+
136
+ namespaces = {"w": WORD_NS}
137
+ author_attr = f"{{{WORD_NS}}}author"
138
+
139
+ authors: dict[str, int] = {}
140
+ for tag in ["ins", "del"]:
141
+ for elem in root.findall(f".//w:{tag}", namespaces):
142
+ author = elem.get(author_attr)
143
+ if author:
144
+ authors[author] = authors.get(author, 0) + 1
145
+
146
+ return authors
147
+
148
+
149
+ def _get_authors_from_docx(docx_path: Path) -> dict[str, int]:
150
+ try:
151
+ with zipfile.ZipFile(docx_path, "r") as zf:
152
+ if "word/document.xml" not in zf.namelist():
153
+ return {}
154
+ with zf.open("word/document.xml") as f:
155
+ tree = ET.parse(f)
156
+ root = tree.getroot()
157
+
158
+ namespaces = {"w": WORD_NS}
159
+ author_attr = f"{{{WORD_NS}}}author"
160
+
161
+ authors: dict[str, int] = {}
162
+ for tag in ["ins", "del"]:
163
+ for elem in root.findall(f".//w:{tag}", namespaces):
164
+ author = elem.get(author_attr)
165
+ if author:
166
+ authors[author] = authors.get(author, 0) + 1
167
+ return authors
168
+ except (zipfile.BadZipFile, ET.ParseError):
169
+ return {}
170
+
171
+
172
+ def infer_author(modified_dir: Path, original_docx: Path, default: str = "Claude") -> str:
173
+ modified_xml = modified_dir / "word" / "document.xml"
174
+ modified_authors = get_tracked_change_authors(modified_xml)
175
+
176
+ if not modified_authors:
177
+ return default
178
+
179
+ original_authors = _get_authors_from_docx(original_docx)
180
+
181
+ new_changes: dict[str, int] = {}
182
+ for author, count in modified_authors.items():
183
+ original_count = original_authors.get(author, 0)
184
+ diff = count - original_count
185
+ if diff > 0:
186
+ new_changes[author] = diff
187
+
188
+ if not new_changes:
189
+ return default
190
+
191
+ if len(new_changes) == 1:
192
+ return next(iter(new_changes))
193
+
194
+ raise ValueError(
195
+ f"Multiple authors added new changes: {new_changes}. "
196
+ "Cannot infer which author to validate."
197
+ )
@@ -0,0 +1,159 @@
1
+ """Pack a directory into a DOCX, PPTX, or XLSX file.
2
+
3
+ Validates with auto-repair, condenses XML formatting, and creates the Office file.
4
+
5
+ Usage:
6
+ python pack.py <input_directory> <output_file> [--original <file>] [--validate true|false]
7
+
8
+ Examples:
9
+ python pack.py unpacked/ output.docx --original input.docx
10
+ python pack.py unpacked/ output.pptx --validate false
11
+ """
12
+
13
+ import argparse
14
+ import sys
15
+ import shutil
16
+ import tempfile
17
+ import zipfile
18
+ from pathlib import Path
19
+
20
+ import defusedxml.minidom
21
+
22
+ from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator
23
+
24
+ def pack(
25
+ input_directory: str,
26
+ output_file: str,
27
+ original_file: str | None = None,
28
+ validate: bool = True,
29
+ infer_author_func=None,
30
+ ) -> tuple[None, str]:
31
+ input_dir = Path(input_directory)
32
+ output_path = Path(output_file)
33
+ suffix = output_path.suffix.lower()
34
+
35
+ if not input_dir.is_dir():
36
+ return None, f"Error: {input_dir} is not a directory"
37
+
38
+ if suffix not in {".docx", ".pptx", ".xlsx"}:
39
+ return None, f"Error: {output_file} must be a .docx, .pptx, or .xlsx file"
40
+
41
+ if validate and original_file:
42
+ original_path = Path(original_file)
43
+ if original_path.exists():
44
+ success, output = _run_validation(
45
+ input_dir, original_path, suffix, infer_author_func
46
+ )
47
+ if output:
48
+ print(output)
49
+ if not success:
50
+ return None, f"Error: Validation failed for {input_dir}"
51
+
52
+ with tempfile.TemporaryDirectory() as temp_dir:
53
+ temp_content_dir = Path(temp_dir) / "content"
54
+ shutil.copytree(input_dir, temp_content_dir)
55
+
56
+ for pattern in ["*.xml", "*.rels"]:
57
+ for xml_file in temp_content_dir.rglob(pattern):
58
+ _condense_xml(xml_file)
59
+
60
+ output_path.parent.mkdir(parents=True, exist_ok=True)
61
+ with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
62
+ for f in temp_content_dir.rglob("*"):
63
+ if f.is_file():
64
+ zf.write(f, f.relative_to(temp_content_dir))
65
+
66
+ return None, f"Successfully packed {input_dir} to {output_file}"
67
+
68
+
69
+ def _run_validation(
70
+ unpacked_dir: Path,
71
+ original_file: Path,
72
+ suffix: str,
73
+ infer_author_func=None,
74
+ ) -> tuple[bool, str | None]:
75
+ output_lines = []
76
+ validators = []
77
+
78
+ if suffix == ".docx":
79
+ author = "Claude"
80
+ if infer_author_func:
81
+ try:
82
+ author = infer_author_func(unpacked_dir, original_file)
83
+ except ValueError as e:
84
+ print(f"Warning: {e} Using default author 'Claude'.", file=sys.stderr)
85
+
86
+ validators = [
87
+ DOCXSchemaValidator(unpacked_dir, original_file),
88
+ RedliningValidator(unpacked_dir, original_file, author=author),
89
+ ]
90
+ elif suffix == ".pptx":
91
+ validators = [PPTXSchemaValidator(unpacked_dir, original_file)]
92
+
93
+ if not validators:
94
+ return True, None
95
+
96
+ total_repairs = sum(v.repair() for v in validators)
97
+ if total_repairs:
98
+ output_lines.append(f"Auto-repaired {total_repairs} issue(s)")
99
+
100
+ success = all(v.validate() for v in validators)
101
+
102
+ if success:
103
+ output_lines.append("All validations PASSED!")
104
+
105
+ return success, "\n".join(output_lines) if output_lines else None
106
+
107
+
108
+ def _condense_xml(xml_file: Path) -> None:
109
+ try:
110
+ with open(xml_file, encoding="utf-8") as f:
111
+ dom = defusedxml.minidom.parse(f)
112
+
113
+ for element in dom.getElementsByTagName("*"):
114
+ if element.tagName.endswith(":t"):
115
+ continue
116
+
117
+ for child in list(element.childNodes):
118
+ if (
119
+ child.nodeType == child.TEXT_NODE
120
+ and child.nodeValue
121
+ and child.nodeValue.strip() == ""
122
+ ) or child.nodeType == child.COMMENT_NODE:
123
+ element.removeChild(child)
124
+
125
+ xml_file.write_bytes(dom.toxml(encoding="UTF-8"))
126
+ except Exception as e:
127
+ print(f"ERROR: Failed to parse {xml_file.name}: {e}", file=sys.stderr)
128
+ raise
129
+
130
+
131
+ if __name__ == "__main__":
132
+ parser = argparse.ArgumentParser(
133
+ description="Pack a directory into a DOCX, PPTX, or XLSX file"
134
+ )
135
+ parser.add_argument("input_directory", help="Unpacked Office document directory")
136
+ parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)")
137
+ parser.add_argument(
138
+ "--original",
139
+ help="Original file for validation comparison",
140
+ )
141
+ parser.add_argument(
142
+ "--validate",
143
+ type=lambda x: x.lower() == "true",
144
+ default=True,
145
+ metavar="true|false",
146
+ help="Run validation with auto-repair (default: true)",
147
+ )
148
+ args = parser.parse_args()
149
+
150
+ _, message = pack(
151
+ args.input_directory,
152
+ args.output_file,
153
+ original_file=args.original,
154
+ validate=args.validate,
155
+ )
156
+ print(message)
157
+
158
+ if "Error" in message:
159
+ sys.exit(1)