@hcu-lab.me/mcp-device 1.0.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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/bootstrap.d.ts +1 -0
  4. package/dist/bootstrap.js +22 -0
  5. package/dist/command-manager.d.ts +8 -0
  6. package/dist/command-manager.js +241 -0
  7. package/dist/config-field-definitions.d.ts +41 -0
  8. package/dist/config-field-definitions.js +37 -0
  9. package/dist/config-manager.d.ts +102 -0
  10. package/dist/config-manager.js +291 -0
  11. package/dist/config.d.ts +6 -0
  12. package/dist/config.js +10 -0
  13. package/dist/custom-stdio.d.ts +51 -0
  14. package/dist/custom-stdio.js +358 -0
  15. package/dist/data/official-app-ca.pem +12 -0
  16. package/dist/data/onboarding-prompts.json +70 -0
  17. package/dist/device/device-state.d.ts +13 -0
  18. package/dist/device/device-state.js +100 -0
  19. package/dist/device/device-status.d.ts +81 -0
  20. package/dist/device/device-status.js +156 -0
  21. package/dist/device/device.d.ts +16 -0
  22. package/dist/device/device.js +190 -0
  23. package/dist/device/execution-engine.d.ts +144 -0
  24. package/dist/device/execution-engine.js +195 -0
  25. package/dist/device/gateway-channel.d.ts +48 -0
  26. package/dist/device/gateway-channel.js +384 -0
  27. package/dist/device/gateway-config.d.ts +40 -0
  28. package/dist/device/gateway-config.js +207 -0
  29. package/dist/device/gateway-identity.d.ts +35 -0
  30. package/dist/device/gateway-identity.js +178 -0
  31. package/dist/device/gateway-pairing.d.ts +25 -0
  32. package/dist/device/gateway-pairing.js +105 -0
  33. package/dist/device/gateway-secure-transport.d.ts +25 -0
  34. package/dist/device/gateway-secure-transport.js +117 -0
  35. package/dist/device/gateway-tool-adapter.d.ts +17 -0
  36. package/dist/device/gateway-tool-adapter.js +284 -0
  37. package/dist/device/gateway-url-policy.d.ts +3 -0
  38. package/dist/device/gateway-url-policy.js +43 -0
  39. package/dist/device/linux-service.d.ts +70 -0
  40. package/dist/device/linux-service.js +268 -0
  41. package/dist/device/official-trust.d.ts +7 -0
  42. package/dist/device/official-trust.js +49 -0
  43. package/dist/device/project-inspection.d.ts +1 -0
  44. package/dist/device/project-inspection.js +180 -0
  45. package/dist/device/runtime-owner.d.ts +41 -0
  46. package/dist/device/runtime-owner.js +177 -0
  47. package/dist/device/windows-dpapi.d.ts +11 -0
  48. package/dist/device/windows-dpapi.js +67 -0
  49. package/dist/device/windows-service.d.ts +56 -0
  50. package/dist/device/windows-service.js +238 -0
  51. package/dist/error-handlers.d.ts +7 -0
  52. package/dist/error-handlers.js +15 -0
  53. package/dist/handlers/edit-search-handlers.d.ts +6 -0
  54. package/dist/handlers/edit-search-handlers.js +6 -0
  55. package/dist/handlers/filesystem-handlers.d.ts +38 -0
  56. package/dist/handlers/filesystem-handlers.js +440 -0
  57. package/dist/handlers/history-handlers.d.ts +12 -0
  58. package/dist/handlers/history-handlers.js +67 -0
  59. package/dist/handlers/index.d.ts +6 -0
  60. package/dist/handlers/index.js +7 -0
  61. package/dist/handlers/process-handlers.d.ts +9 -0
  62. package/dist/handlers/process-handlers.js +15 -0
  63. package/dist/handlers/search-handlers.d.ts +17 -0
  64. package/dist/handlers/search-handlers.js +224 -0
  65. package/dist/handlers/terminal-handlers.d.ts +21 -0
  66. package/dist/handlers/terminal-handlers.js +35 -0
  67. package/dist/index.d.ts +2 -0
  68. package/dist/index.js +138 -0
  69. package/dist/mcp-device.d.ts +2 -0
  70. package/dist/mcp-device.js +7 -0
  71. package/dist/npm-scripts/remote.d.ts +3 -0
  72. package/dist/npm-scripts/remote.js +314 -0
  73. package/dist/search-manager.d.ts +147 -0
  74. package/dist/search-manager.js +828 -0
  75. package/dist/server.d.ts +46 -0
  76. package/dist/server.js +1563 -0
  77. package/dist/terminal-manager.d.ts +104 -0
  78. package/dist/terminal-manager.js +635 -0
  79. package/dist/tools/config.d.ts +96 -0
  80. package/dist/tools/config.js +260 -0
  81. package/dist/tools/edit.d.ts +38 -0
  82. package/dist/tools/edit.js +435 -0
  83. package/dist/tools/feedback.d.ts +8 -0
  84. package/dist/tools/feedback.js +132 -0
  85. package/dist/tools/filesystem.d.ts +79 -0
  86. package/dist/tools/filesystem.js +947 -0
  87. package/dist/tools/fuzzySearch.d.ts +12 -0
  88. package/dist/tools/fuzzySearch.js +74 -0
  89. package/dist/tools/fuzzySearchCore.d.ts +52 -0
  90. package/dist/tools/fuzzySearchCore.js +125 -0
  91. package/dist/tools/improved-process-tools.d.ts +24 -0
  92. package/dist/tools/improved-process-tools.js +627 -0
  93. package/dist/tools/mime-types.d.ts +3 -0
  94. package/dist/tools/mime-types.js +28 -0
  95. package/dist/tools/pdf/extract-images.d.ts +34 -0
  96. package/dist/tools/pdf/extract-images.js +132 -0
  97. package/dist/tools/pdf/index.d.ts +6 -0
  98. package/dist/tools/pdf/index.js +3 -0
  99. package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
  100. package/dist/tools/pdf/lib/pdf2md.js +76 -0
  101. package/dist/tools/pdf/manipulations.d.ts +13 -0
  102. package/dist/tools/pdf/manipulations.js +99 -0
  103. package/dist/tools/pdf/markdown.d.ts +26 -0
  104. package/dist/tools/pdf/markdown.js +275 -0
  105. package/dist/tools/pdf/utils.d.ts +12 -0
  106. package/dist/tools/pdf/utils.js +34 -0
  107. package/dist/tools/process.d.ts +3 -0
  108. package/dist/tools/process.js +56 -0
  109. package/dist/tools/prompts.d.ts +28 -0
  110. package/dist/tools/prompts.js +259 -0
  111. package/dist/tools/schemas.d.ts +454 -0
  112. package/dist/tools/schemas.js +242 -0
  113. package/dist/tools/usage.d.ts +5 -0
  114. package/dist/tools/usage.js +24 -0
  115. package/dist/types.d.ts +83 -0
  116. package/dist/types.js +1 -0
  117. package/dist/ui/config-editor/config-editor-runtime.js +150 -0
  118. package/dist/ui/config-editor/index.html +13 -0
  119. package/dist/ui/config-editor/src/app.d.ts +43 -0
  120. package/dist/ui/config-editor/src/app.js +831 -0
  121. package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
  122. package/dist/ui/config-editor/src/array-modal.js +185 -0
  123. package/dist/ui/config-editor/src/main.d.ts +1 -0
  124. package/dist/ui/config-editor/src/main.js +2 -0
  125. package/dist/ui/config-editor/styles.css +587 -0
  126. package/dist/ui/contracts.d.ts +14 -0
  127. package/dist/ui/contracts.js +21 -0
  128. package/dist/ui/file-preview/index.html +16 -0
  129. package/dist/ui/file-preview/preview-runtime.js +469 -0
  130. package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
  131. package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
  132. package/dist/ui/file-preview/src/app.d.ts +4 -0
  133. package/dist/ui/file-preview/src/app.js +680 -0
  134. package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
  135. package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
  136. package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
  137. package/dist/ui/file-preview/src/components/highlighting.js +54 -0
  138. package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
  139. package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
  140. package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
  141. package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
  142. package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
  143. package/dist/ui/file-preview/src/directory-controller.js +240 -0
  144. package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
  145. package/dist/ui/file-preview/src/document-layout.js +109 -0
  146. package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
  147. package/dist/ui/file-preview/src/document-outline.js +97 -0
  148. package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
  149. package/dist/ui/file-preview/src/document-workspace.js +33 -0
  150. package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
  151. package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
  152. package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
  153. package/dist/ui/file-preview/src/host/external-actions.js +55 -0
  154. package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
  155. package/dist/ui/file-preview/src/host/selection-context.js +106 -0
  156. package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
  157. package/dist/ui/file-preview/src/image-preview.js +21 -0
  158. package/dist/ui/file-preview/src/main.d.ts +1 -0
  159. package/dist/ui/file-preview/src/main.js +5 -0
  160. package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
  161. package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
  162. package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
  163. package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
  164. package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
  165. package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
  166. package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
  167. package/dist/ui/file-preview/src/markdown/linking.js +228 -0
  168. package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
  169. package/dist/ui/file-preview/src/markdown/outline.js +16 -0
  170. package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
  171. package/dist/ui/file-preview/src/markdown/parser.js +38 -0
  172. package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
  173. package/dist/ui/file-preview/src/markdown/preview.js +20 -0
  174. package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
  175. package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
  176. package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
  177. package/dist/ui/file-preview/src/markdown/utils.js +15 -0
  178. package/dist/ui/file-preview/src/model.d.ts +35 -0
  179. package/dist/ui/file-preview/src/model.js +1 -0
  180. package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
  181. package/dist/ui/file-preview/src/panel-actions.js +182 -0
  182. package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
  183. package/dist/ui/file-preview/src/path-utils.js +64 -0
  184. package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
  185. package/dist/ui/file-preview/src/payload-utils.js +119 -0
  186. package/dist/ui/file-preview/src/types.d.ts +1 -0
  187. package/dist/ui/file-preview/src/types.js +1 -0
  188. package/dist/ui/file-preview/styles.css +1631 -0
  189. package/dist/ui/resources.d.ts +28 -0
  190. package/dist/ui/resources.js +86 -0
  191. package/dist/ui/shared/compact-row.d.ts +11 -0
  192. package/dist/ui/shared/compact-row.js +18 -0
  193. package/dist/ui/shared/escape-html.d.ts +4 -0
  194. package/dist/ui/shared/escape-html.js +11 -0
  195. package/dist/ui/shared/host-context.d.ts +15 -0
  196. package/dist/ui/shared/host-context.js +51 -0
  197. package/dist/ui/shared/tool-bridge.d.ts +30 -0
  198. package/dist/ui/shared/tool-bridge.js +137 -0
  199. package/dist/ui/shared/tool-shell.d.ts +25 -0
  200. package/dist/ui/shared/tool-shell.js +107 -0
  201. package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
  202. package/dist/ui/shared/ui-event-tracker.js +27 -0
  203. package/dist/ui/shared/widget-state.d.ts +33 -0
  204. package/dist/ui/shared/widget-state.js +158 -0
  205. package/dist/utils/ab-test.d.ts +12 -0
  206. package/dist/utils/ab-test.js +104 -0
  207. package/dist/utils/capture.d.ts +35 -0
  208. package/dist/utils/capture.js +481 -0
  209. package/dist/utils/dockerPrompt.d.ts +17 -0
  210. package/dist/utils/dockerPrompt.js +53 -0
  211. package/dist/utils/feature-flags.d.ts +59 -0
  212. package/dist/utils/feature-flags.js +215 -0
  213. package/dist/utils/files/base.d.ts +173 -0
  214. package/dist/utils/files/base.js +5 -0
  215. package/dist/utils/files/binary.d.ts +21 -0
  216. package/dist/utils/files/binary.js +65 -0
  217. package/dist/utils/files/docx.d.ts +47 -0
  218. package/dist/utils/files/docx.js +679 -0
  219. package/dist/utils/files/excel.d.ts +24 -0
  220. package/dist/utils/files/excel.js +437 -0
  221. package/dist/utils/files/factory.d.ts +41 -0
  222. package/dist/utils/files/factory.js +113 -0
  223. package/dist/utils/files/image.d.ts +21 -0
  224. package/dist/utils/files/image.js +78 -0
  225. package/dist/utils/files/index.d.ts +10 -0
  226. package/dist/utils/files/index.js +13 -0
  227. package/dist/utils/files/pdf.d.ts +32 -0
  228. package/dist/utils/files/pdf.js +142 -0
  229. package/dist/utils/files/text.d.ts +63 -0
  230. package/dist/utils/files/text.js +370 -0
  231. package/dist/utils/fuzzySearchLogger.d.ts +30 -0
  232. package/dist/utils/fuzzySearchLogger.js +126 -0
  233. package/dist/utils/lineEndingHandler.d.ts +21 -0
  234. package/dist/utils/lineEndingHandler.js +77 -0
  235. package/dist/utils/logger.d.ts +32 -0
  236. package/dist/utils/logger.js +72 -0
  237. package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
  238. package/dist/utils/mcp-ui-ab-test.js +62 -0
  239. package/dist/utils/open-browser.d.ts +9 -0
  240. package/dist/utils/open-browser.js +42 -0
  241. package/dist/utils/process-detection.d.ts +23 -0
  242. package/dist/utils/process-detection.js +150 -0
  243. package/dist/utils/ripgrep-resolver.d.ts +9 -0
  244. package/dist/utils/ripgrep-resolver.js +74 -0
  245. package/dist/utils/system-info.d.ts +71 -0
  246. package/dist/utils/system-info.js +712 -0
  247. package/dist/utils/toolHistory.d.ts +86 -0
  248. package/dist/utils/toolHistory.js +257 -0
  249. package/dist/utils/trackTools.d.ts +6 -0
  250. package/dist/utils/trackTools.js +54 -0
  251. package/dist/utils/unsupportedParams.d.ts +24 -0
  252. package/dist/utils/unsupportedParams.js +66 -0
  253. package/dist/utils/usageTracker.d.ts +133 -0
  254. package/dist/utils/usageTracker.js +477 -0
  255. package/dist/utils/welcome-onboarding.d.ts +11 -0
  256. package/dist/utils/welcome-onboarding.js +99 -0
  257. package/dist/utils/withTimeout.d.ts +28 -0
  258. package/dist/utils/withTimeout.js +85 -0
  259. package/dist/version.d.ts +1 -0
  260. package/dist/version.js +1 -0
  261. package/package.json +131 -0
@@ -0,0 +1,679 @@
1
+ /**
2
+ * DOCX File Handler
3
+ *
4
+ * Approach: expose DOCX as filtered/raw XML through existing read_file + edit_block.
5
+ *
6
+ * READ (default): Returns a text-bearing outline — skips shapes, drawings, SVG noise.
7
+ * Shows paragraphs with text, tables with cell content, style info, and image refs.
8
+ * Each element shows its raw XML tag context so Claude can target it for editing.
9
+ *
10
+ * READ (with offset/length): Returns raw pretty-printed XML with line pagination,
11
+ * so Claude can drill into specific sections when the outline isn't enough.
12
+ *
13
+ * EDIT (old_string/new_string): Find/replace on the pretty-printed XML, then
14
+ * compact and repack into valid DOCX. Works exactly like text file editing.
15
+ *
16
+ * Round-trip: DOCX → unzip → pretty-print → [outline or raw] → edit → compact → repack
17
+ */
18
+ import fs from 'fs/promises';
19
+ import PizZip from 'pizzip';
20
+ // ════════════════════════════════════════════════════════════════
21
+ // XML Pretty-Print / Compact
22
+ // ════════════════════════════════════════════════════════════════
23
+ /**
24
+ * Pretty-print XML: split tags onto separate lines with indentation.
25
+ * Preserves text node content exactly. compact→pretty→compact is lossless.
26
+ */
27
+ function prettyPrintXml(xml) {
28
+ const parts = xml.split(/(?<=>)(?=<)/);
29
+ const lines = [];
30
+ let depth = 0;
31
+ for (const part of parts) {
32
+ const trimmed = part.trim();
33
+ if (!trimmed)
34
+ continue;
35
+ const isClosing = trimmed.startsWith('</');
36
+ const isSelfClosing = trimmed.endsWith('/>');
37
+ const isProcessingInstruction = trimmed.startsWith('<?');
38
+ const isInline = !isClosing && !isSelfClosing && trimmed.includes('</');
39
+ if (isClosing)
40
+ depth = Math.max(0, depth - 1);
41
+ lines.push(' '.repeat(depth) + trimmed);
42
+ if (!isClosing && !isSelfClosing && !isInline && !isProcessingInstruction)
43
+ depth++;
44
+ }
45
+ return lines.join('\n');
46
+ }
47
+ /**
48
+ * Compact pretty-printed XML back — strip leading indentation, join lines.
49
+ * Does NOT touch whitespace inside <w:t> text nodes.
50
+ */
51
+ function compactXml(prettyXml) {
52
+ return prettyXml.split('\n').map(l => l.trimStart()).join('');
53
+ }
54
+ function loadDocxZip(buf) {
55
+ const zip = new PizZip(buf);
56
+ const docFile = zip.file('word/document.xml');
57
+ if (!docFile)
58
+ throw new Error('Invalid DOCX: missing word/document.xml');
59
+ const xmlParts = new Map();
60
+ // Collect all XML parts for potential editing
61
+ const zipFiles = zip.files;
62
+ for (const relativePath of Object.keys(zipFiles)) {
63
+ if (relativePath.endsWith('.xml') || relativePath.endsWith('.rels')) {
64
+ try {
65
+ xmlParts.set(relativePath, zipFiles[relativePath].asText());
66
+ }
67
+ catch { /* skip binary entries */ }
68
+ }
69
+ }
70
+ return {
71
+ zip,
72
+ documentXml: docFile.asText(),
73
+ xmlParts,
74
+ };
75
+ }
76
+ /**
77
+ * Extract a text-bearing outline from document.xml.
78
+ *
79
+ * Walks direct children of <w:body> and for each:
80
+ * - w:p (paragraph): extracts text from <w:t> elements, shows style
81
+ * - w:tbl (table): extracts cell text for each row
82
+ * - mc:AlternateContent / shapes / drawings: shows size, skips content
83
+ * - w:sdt: looks inside for text/tables
84
+ *
85
+ * Returns a human-readable outline with enough context for editing.
86
+ */
87
+ function extractOutline(xml) {
88
+ const lines = [];
89
+ // Parse body children using regex — faster and more reliable than DOM for outline
90
+ // Find <w:body>...</w:body>
91
+ const bodyMatch = xml.match(/<w:body[^>]*>([\s\S]*)<\/w:body>/);
92
+ if (!bodyMatch)
93
+ return '[No w:body found in document.xml]';
94
+ const bodyContent = bodyMatch[1];
95
+ // Split into top-level children of w:body
96
+ // We need to find top-level elements, respecting nesting
97
+ const children = splitTopLevelElements(bodyContent);
98
+ let paragraphCount = 0;
99
+ let tableCount = 0;
100
+ let imageCount = 0;
101
+ for (let i = 0; i < children.length; i++) {
102
+ const child = children[i];
103
+ const tagMatch = child.match(/^<(\S+?)[\s>\/]/);
104
+ if (!tagMatch)
105
+ continue;
106
+ const tag = tagMatch[1];
107
+ if (tag === 'w:p') {
108
+ const text = extractAllText(child);
109
+ const textFragments = extractTextFragments(child);
110
+ const style = extractParagraphStyle(child);
111
+ const hasDrawing = child.includes('<w:drawing') || child.includes('<mc:AlternateContent');
112
+ let line = `[${i}] w:p`;
113
+ if (style)
114
+ line += ` style="${style}"`;
115
+ if (text && hasDrawing) {
116
+ line += ` (+ drawing/image)`;
117
+ imageCount++;
118
+ }
119
+ else if (hasDrawing) {
120
+ line += ` [drawing/image, ${(child.length / 1024).toFixed(1)}KB]`;
121
+ imageCount++;
122
+ if (!text) {
123
+ lines.push(line);
124
+ paragraphCount++;
125
+ continue;
126
+ }
127
+ }
128
+ if (textFragments.length > 0) {
129
+ const joined = textFragments.join('');
130
+ if (joined.length > 500) {
131
+ line += `\n ${textFragments.slice(0, 8).join('')}...`;
132
+ }
133
+ else {
134
+ line += `\n ${joined}`;
135
+ }
136
+ }
137
+ else if (!hasDrawing) {
138
+ line += ' (empty)';
139
+ }
140
+ lines.push(line);
141
+ paragraphCount++;
142
+ }
143
+ else if (tag === 'w:tbl') {
144
+ const rows = extractTableRows(child);
145
+ let line = `[${i}] w:tbl (${rows.length} rows)`;
146
+ const style = extractTableStyle(child);
147
+ if (style)
148
+ line += ` style="${style}"`;
149
+ // Show all rows (tables usually contain the real content)
150
+ for (let r = 0; r < rows.length; r++) {
151
+ const cells = rows[r].map(c => {
152
+ if (!c || c.trim() === '')
153
+ return '';
154
+ return c.length > 60 ? c.substring(0, 60) + '…' : c;
155
+ });
156
+ line += `\n row${r}: [${cells.join(' | ')}]`;
157
+ }
158
+ lines.push(line);
159
+ tableCount++;
160
+ }
161
+ else if (tag === 'w:sdt') {
162
+ // Structured document tag — look inside for content
163
+ const sdtFragments = extractTextFragments(child);
164
+ const innerTables = (child.match(/<w:tbl[\s>]/g) || []).length;
165
+ let line = `[${i}] w:sdt`;
166
+ if (innerTables > 0) {
167
+ line += ` (contains ${innerTables} table${innerTables > 1 ? 's' : ''})`;
168
+ tableCount += innerTables;
169
+ }
170
+ if (sdtFragments.length > 0) {
171
+ const joined = sdtFragments.join('');
172
+ if (joined.length > 150) {
173
+ line += `\n ${sdtFragments.slice(0, 3).join('')}...`;
174
+ }
175
+ else {
176
+ line += `\n ${joined}`;
177
+ }
178
+ }
179
+ lines.push(line);
180
+ }
181
+ else if (tag === 'w:sectPr') {
182
+ lines.push(`[${i}] w:sectPr (section properties)`);
183
+ }
184
+ else if (tag === 'mc:AlternateContent') {
185
+ lines.push(`[${i}] mc:AlternateContent [drawing, ${(child.length / 1024).toFixed(1)}KB — skipped]`);
186
+ imageCount++;
187
+ }
188
+ else {
189
+ lines.push(`[${i}] ${tag} (${(child.length / 1024).toFixed(1)}KB)`);
190
+ }
191
+ }
192
+ // Summary header
193
+ const header = `DOCX Outline: ${children.length} body children, ${paragraphCount} paragraphs, ${tableCount} tables, ${imageCount} images\n` +
194
+ `Edit with: edit_block(file, old_string="<w:t>old text</w:t>", new_string="<w:t>new text</w:t>")\n` +
195
+ `Raw XML: use read_file with offset=1 to see pretty-printed XML for advanced edits.\n` +
196
+ '─'.repeat(70);
197
+ return header + '\n' + lines.join('\n');
198
+ }
199
+ // ════════════════════════════════════════════════════════════════
200
+ // XML text extraction helpers (regex-based, no DOM needed)
201
+ // ════════════════════════════════════════════════════════════════
202
+ /** Extract all <w:t>...</w:t> text content from an XML fragment */
203
+ function extractAllText(xml) {
204
+ const texts = [];
205
+ // Match <w:t> or <w:t xml:space="preserve"> but NOT <w:tbl>, <w:tc>, <w:tr>, etc.
206
+ const re = /<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>/g;
207
+ let m;
208
+ while ((m = re.exec(xml)) !== null) {
209
+ if (m[1])
210
+ texts.push(m[1]);
211
+ }
212
+ return texts.join('').trim();
213
+ }
214
+ /** Extract <w:t>...</w:t> elements as XML fragments for use in edit_block */
215
+ function extractTextFragments(xml) {
216
+ const fragments = [];
217
+ const re = /<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>/g;
218
+ let m;
219
+ while ((m = re.exec(xml)) !== null) {
220
+ if (m[1] && m[1].trim())
221
+ fragments.push(m[0]);
222
+ }
223
+ return fragments;
224
+ }
225
+ /** Extract paragraph style id from w:pPr/w:pStyle */
226
+ function extractParagraphStyle(xml) {
227
+ const m = xml.match(/<w:pStyle\s+w:val="([^"]+)"/);
228
+ return m ? m[1] : null;
229
+ }
230
+ /** Extract table style from w:tblPr/w:tblStyle */
231
+ function extractTableStyle(xml) {
232
+ const m = xml.match(/<w:tblStyle\s+w:val="([^"]+)"/);
233
+ return m ? m[1] : null;
234
+ }
235
+ /** Extract table rows as arrays of cell text */
236
+ function extractTableRows(tableXml) {
237
+ const rows = [];
238
+ // Find each <w:tr>...</w:tr> using nesting-aware extraction
239
+ const rowElements = extractNestedElements(tableXml, 'w:tr');
240
+ for (const rowXml of rowElements) {
241
+ const cells = [];
242
+ const cellElements = extractNestedElements(rowXml, 'w:tc');
243
+ for (const cellXml of cellElements) {
244
+ cells.push(extractAllText(cellXml));
245
+ }
246
+ if (cells.length > 0)
247
+ rows.push(cells);
248
+ }
249
+ return rows;
250
+ }
251
+ /**
252
+ * Extract all occurrences of a named element from XML, respecting nesting.
253
+ * Returns array of full element strings including open/close tags.
254
+ */
255
+ function extractNestedElements(xml, tagName) {
256
+ const results = [];
257
+ const openTag = `<${tagName}`;
258
+ const closeTag = `</${tagName}>`;
259
+ let searchFrom = 0;
260
+ while (searchFrom < xml.length) {
261
+ const openPos = xml.indexOf(openTag, searchFrom);
262
+ if (openPos === -1)
263
+ break;
264
+ // Check it's actually a tag start (followed by space, >, or /)
265
+ const charAfter = xml[openPos + openTag.length];
266
+ if (charAfter !== ' ' && charAfter !== '>' && charAfter !== '/') {
267
+ searchFrom = openPos + 1;
268
+ continue;
269
+ }
270
+ // Check for self-closing
271
+ const nextClose = xml.indexOf('>', openPos);
272
+ if (nextClose !== -1 && xml[nextClose - 1] === '/') {
273
+ results.push(xml.substring(openPos, nextClose + 1));
274
+ searchFrom = nextClose + 1;
275
+ continue;
276
+ }
277
+ // Find matching close tag respecting nesting
278
+ let depth = 1;
279
+ let pos = nextClose + 1;
280
+ while (depth > 0 && pos < xml.length) {
281
+ const nextOpen = xml.indexOf(openTag, pos);
282
+ const nextCloseTag = xml.indexOf(closeTag, pos);
283
+ if (nextCloseTag === -1)
284
+ break; // malformed XML
285
+ if (nextOpen !== -1 && nextOpen < nextCloseTag) {
286
+ // Check it's actually an open tag
287
+ const ca = xml[nextOpen + openTag.length];
288
+ if (ca === ' ' || ca === '>' || ca === '/') {
289
+ depth++;
290
+ }
291
+ pos = nextOpen + openTag.length;
292
+ }
293
+ else {
294
+ depth--;
295
+ if (depth === 0) {
296
+ results.push(xml.substring(openPos, nextCloseTag + closeTag.length));
297
+ }
298
+ pos = nextCloseTag + closeTag.length;
299
+ }
300
+ }
301
+ searchFrom = pos;
302
+ }
303
+ return results;
304
+ }
305
+ /**
306
+ * Split XML into top-level elements respecting nesting depth.
307
+ * E.g. for body content, returns each direct child element as a string.
308
+ */
309
+ function splitTopLevelElements(xml) {
310
+ const elements = [];
311
+ let depth = 0;
312
+ let currentStart = -1;
313
+ // Simple state machine: track < > and nesting
314
+ let i = 0;
315
+ while (i < xml.length) {
316
+ if (xml[i] === '<') {
317
+ // Check what kind of tag
318
+ if (xml[i + 1] === '/') {
319
+ // Closing tag
320
+ depth--;
321
+ if (depth === 0) {
322
+ // Find end of this closing tag
323
+ const closeEnd = xml.indexOf('>', i);
324
+ if (closeEnd === -1)
325
+ break; // malformed XML — bail out
326
+ if (currentStart !== -1) {
327
+ elements.push(xml.substring(currentStart, closeEnd + 1).trim());
328
+ currentStart = -1;
329
+ }
330
+ i = closeEnd + 1;
331
+ continue;
332
+ }
333
+ }
334
+ else if (xml[i + 1] === '?' || xml[i + 1] === '!') {
335
+ // Processing instruction or comment — skip
336
+ const end = xml.indexOf('>', i);
337
+ if (end === -1)
338
+ break; // malformed XML — bail out
339
+ i = end + 1;
340
+ continue;
341
+ }
342
+ else {
343
+ // Opening tag
344
+ if (depth === 0)
345
+ currentStart = i;
346
+ // Check for self-closing
347
+ const tagEnd = xml.indexOf('>', i);
348
+ if (tagEnd === -1)
349
+ break; // malformed XML — bail out
350
+ if (xml[tagEnd - 1] === '/') {
351
+ // Self-closing
352
+ if (depth === 0) {
353
+ elements.push(xml.substring(currentStart, tagEnd + 1).trim());
354
+ currentStart = -1;
355
+ i = tagEnd + 1;
356
+ continue;
357
+ }
358
+ }
359
+ else {
360
+ depth++;
361
+ }
362
+ i = tagEnd + 1;
363
+ continue;
364
+ }
365
+ }
366
+ i++;
367
+ }
368
+ return elements.filter(e => e.length > 0);
369
+ }
370
+ /**
371
+ * Extract outline info for headers and footers from the DOCX zip.
372
+ */
373
+ function extractHeaderFooterOutline(zip) {
374
+ const parts = [];
375
+ const zipFiles = zip.files;
376
+ for (const relativePath of Object.keys(zipFiles)) {
377
+ if ((relativePath.startsWith('word/header') || relativePath.startsWith('word/footer'))
378
+ && relativePath.endsWith('.xml')) {
379
+ try {
380
+ const xml = zipFiles[relativePath].asText();
381
+ const text = extractAllText(xml);
382
+ const name = relativePath.replace('word/', '');
383
+ if (text) {
384
+ parts.push(`${name}: "${text.length > 100 ? text.substring(0, 100) + '...' : text}"`);
385
+ }
386
+ else {
387
+ parts.push(`${name}: (no text content)`);
388
+ }
389
+ }
390
+ catch { /* skip */ }
391
+ }
392
+ }
393
+ return parts.length > 0 ? '\n\nHeaders/Footers:\n' + parts.join('\n') : '';
394
+ }
395
+ // ════════════════════════════════════════════════════════════════
396
+ // DOCX creation helpers
397
+ // ════════════════════════════════════════════════════════════════
398
+ function escapeXml(text) {
399
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
400
+ .replace(/"/g, '&quot;').replace(/'/g, '&apos;');
401
+ }
402
+ function createMinimalDocxZip(documentXml) {
403
+ const zip = new PizZip();
404
+ zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
405
+ `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
406
+ `<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
407
+ `<Default Extension="xml" ContentType="application/xml"/>` +
408
+ `<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>` +
409
+ `<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>` +
410
+ `</Types>`);
411
+ zip.file('_rels/.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
412
+ `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
413
+ `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>` +
414
+ `</Relationships>`);
415
+ zip.file('word/_rels/document.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
416
+ `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
417
+ `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>` +
418
+ `</Relationships>`);
419
+ zip.file('word/styles.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
420
+ `<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
421
+ `<w:docDefaults><w:rPrDefault><w:rPr>` +
422
+ `<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/>` +
423
+ `<w:sz w:val="22"/><w:szCs w:val="22"/>` +
424
+ `</w:rPr></w:rPrDefault></w:docDefaults>` +
425
+ `<w:style w:type="paragraph" w:styleId="Normal" w:default="1"><w:name w:val="Normal"/></w:style>` +
426
+ `<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:pPr><w:outlineLvl w:val="0"/></w:pPr><w:rPr><w:b/><w:sz w:val="32"/></w:rPr></w:style>` +
427
+ `<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:pPr><w:outlineLvl w:val="1"/></w:pPr><w:rPr><w:b/><w:sz w:val="28"/></w:rPr></w:style>` +
428
+ `<w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="heading 3"/><w:pPr><w:outlineLvl w:val="2"/></w:pPr><w:rPr><w:b/><w:sz w:val="24"/></w:rPr></w:style>` +
429
+ `</w:styles>`);
430
+ zip.file('word/document.xml', documentXml);
431
+ return zip;
432
+ }
433
+ // ════════════════════════════════════════════════════════════════
434
+ // Count occurrences helper
435
+ // ════════════════════════════════════════════════════════════════
436
+ function countOccurrences(haystack, needle) {
437
+ let count = 0;
438
+ let pos = haystack.indexOf(needle);
439
+ while (pos !== -1) {
440
+ count++;
441
+ pos = haystack.indexOf(needle, pos + 1);
442
+ }
443
+ return count;
444
+ }
445
+ // ════════════════════════════════════════════════════════════════
446
+ // DocxFileHandler — implements FileHandler
447
+ // ════════════════════════════════════════════════════════════════
448
+ export class DocxFileHandler {
449
+ constructor() {
450
+ this.extensions = ['.docx'];
451
+ }
452
+ canHandle(path) {
453
+ return this.extensions.some(e => path.toLowerCase().endsWith(e));
454
+ }
455
+ /**
456
+ * Read DOCX content.
457
+ *
458
+ * Default (offset=0, no explicit length or default length): returns outline
459
+ * With offset/length: returns raw pretty-printed XML with line pagination
460
+ */
461
+ async read(path, options) {
462
+ const buf = await fs.readFile(path);
463
+ const { zip, documentXml } = loadDocxZip(buf);
464
+ const pretty = prettyPrintXml(documentXml);
465
+ const allLines = pretty.split('\n');
466
+ const totalLines = allLines.length;
467
+ const offset = options?.offset ?? 0;
468
+ const length = options?.length;
469
+ // If user explicitly requests non-zero offset, give raw XML with pagination
470
+ const wantsRaw = offset !== 0;
471
+ if (wantsRaw) {
472
+ let startLine;
473
+ let sliceLength;
474
+ if (offset < 0) {
475
+ startLine = Math.max(0, totalLines + offset);
476
+ sliceLength = totalLines - startLine;
477
+ }
478
+ else {
479
+ startLine = offset;
480
+ sliceLength = length ?? totalLines;
481
+ }
482
+ const sliced = allLines.slice(startLine, startLine + sliceLength);
483
+ const remaining = totalLines - (startLine + sliced.length);
484
+ const status = `[DOCX XML: lines ${startLine}-${startLine + sliced.length - 1} of ${totalLines} (${remaining} remaining)]`;
485
+ return {
486
+ content: status + '\n' + sliced.join('\n'),
487
+ mimeType: 'application/xml',
488
+ metadata: { isDocx: true, lineCount: totalLines },
489
+ };
490
+ }
491
+ // Default: return outline
492
+ const outline = extractOutline(documentXml);
493
+ const headerFooterInfo = extractHeaderFooterOutline(zip);
494
+ const rawSizeKB = (documentXml.length / 1024).toFixed(1);
495
+ return {
496
+ content: outline + headerFooterInfo +
497
+ `\n\nRaw XML: ${totalLines} lines, ${rawSizeKB}KB.` +
498
+ `\nFor bulk changes (translation, mass find/replace): use start_process with a Python script using zipfile to edit <w:t> elements.`,
499
+ mimeType: 'text/plain',
500
+ metadata: { isDocx: true, lineCount: totalLines },
501
+ };
502
+ }
503
+ /**
504
+ * Write/create a DOCX file.
505
+ * Content is plain text — each line becomes a paragraph.
506
+ * Lines starting with # become headings (# = Heading1, ## = Heading2, etc.)
507
+ */
508
+ async write(path, content, mode) {
509
+ if (mode === 'append') {
510
+ throw new Error('DOCX append not supported. Use edit_block to modify existing DOCX files.');
511
+ }
512
+ const text = typeof content === 'string' ? content : String(content);
513
+ const lines = text.split('\n');
514
+ // Build paragraph XML from lines
515
+ const paragraphs = [];
516
+ for (const line of lines) {
517
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
518
+ if (headingMatch) {
519
+ const level = headingMatch[1].length;
520
+ const headingText = headingMatch[2];
521
+ paragraphs.push(`<w:p><w:pPr><w:pStyle w:val="Heading${level}"/></w:pPr>` +
522
+ `<w:r><w:t>${escapeXml(headingText)}</w:t></w:r></w:p>`);
523
+ }
524
+ else if (line.trim() === '') {
525
+ paragraphs.push(`<w:p/>`);
526
+ }
527
+ else {
528
+ paragraphs.push(`<w:p><w:r><w:t xml:space="preserve">${escapeXml(line)}</w:t></w:r></w:p>`);
529
+ }
530
+ }
531
+ const docXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
532
+ `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
533
+ `<w:body>${paragraphs.join('')}` +
534
+ `<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr>` +
535
+ `</w:body></w:document>`;
536
+ const zip = createMinimalDocxZip(docXml);
537
+ const buf = zip.generate({ type: 'nodebuffer', compression: 'DEFLATE', compressionOptions: { level: 6 } });
538
+ await fs.writeFile(path, buf);
539
+ }
540
+ /**
541
+ * Edit DOCX via find/replace on pretty-printed XML.
542
+ *
543
+ * Works on the same representation that read() returns when using offset/length,
544
+ * so XML fragments copied from read output work as search strings.
545
+ * After editing, XML is compacted and repacked into the DOCX.
546
+ */
547
+ async editRange(path, _range, content, options) {
548
+ try {
549
+ let oldStr;
550
+ let newStr;
551
+ let expectedReplacements = 1;
552
+ if (typeof content === 'object' && content !== null) {
553
+ oldStr = content.oldStr || content.old_string || content.search || '';
554
+ newStr = content.newStr || content.new_string || content.replace || '';
555
+ expectedReplacements = content.expectedReplacements || content.expected_replacements || 1;
556
+ }
557
+ else {
558
+ return {
559
+ success: false, editsApplied: 0,
560
+ errors: [{ location: 'docx', error: 'DOCX editing requires old_string and new_string' }],
561
+ };
562
+ }
563
+ if (!oldStr) {
564
+ return {
565
+ success: false, editsApplied: 0,
566
+ errors: [{ location: 'docx', error: 'old_string cannot be empty' }],
567
+ };
568
+ }
569
+ // Load and pretty-print
570
+ const buf = await fs.readFile(path);
571
+ const zip = new PizZip(buf);
572
+ const docFile = zip.file('word/document.xml');
573
+ if (!docFile)
574
+ throw new Error('Invalid DOCX: missing word/document.xml');
575
+ const rawXml = docFile.asText();
576
+ const pretty = prettyPrintXml(rawXml);
577
+ // Also check headers/footers for the search string
578
+ let targetPretty = pretty;
579
+ let targetFile = 'word/document.xml';
580
+ let matchCount = countOccurrences(pretty, oldStr);
581
+ // If not found in document.xml, search through headers/footers
582
+ if (matchCount === 0) {
583
+ const xmlFiles = ['word/header1.xml', 'word/header2.xml', 'word/header3.xml',
584
+ 'word/footer1.xml', 'word/footer2.xml', 'word/footer3.xml'];
585
+ for (const xmlPath of xmlFiles) {
586
+ const f = zip.file(xmlPath);
587
+ if (!f)
588
+ continue;
589
+ const partPretty = prettyPrintXml(f.asText());
590
+ const c = countOccurrences(partPretty, oldStr);
591
+ if (c > 0) {
592
+ targetPretty = partPretty;
593
+ targetFile = xmlPath;
594
+ matchCount = c;
595
+ break;
596
+ }
597
+ }
598
+ }
599
+ if (matchCount === 0) {
600
+ return {
601
+ success: false, editsApplied: 0,
602
+ errors: [{ location: targetFile, error: `Search string not found in DOCX` }],
603
+ };
604
+ }
605
+ if (matchCount !== expectedReplacements) {
606
+ return {
607
+ success: false, editsApplied: 0,
608
+ errors: [{
609
+ location: targetFile,
610
+ error: `Expected ${expectedReplacements} occurrence(s) but found ${matchCount}. ` +
611
+ `Set expected_replacements to ${matchCount} to replace all, ` +
612
+ `or add more context to make the search unique.`,
613
+ }],
614
+ };
615
+ }
616
+ // Apply replacement
617
+ let edited = targetPretty;
618
+ if (expectedReplacements === 1) {
619
+ const idx = edited.indexOf(oldStr);
620
+ edited = edited.substring(0, idx) + newStr + edited.substring(idx + oldStr.length);
621
+ }
622
+ else {
623
+ edited = edited.split(oldStr).join(newStr);
624
+ }
625
+ // Compact and repack
626
+ const compacted = compactXml(edited);
627
+ zip.file(targetFile, compacted);
628
+ const outBuf = zip.generate({
629
+ type: 'nodebuffer',
630
+ compression: 'DEFLATE',
631
+ compressionOptions: { level: 6 },
632
+ });
633
+ await fs.writeFile(path, outBuf);
634
+ return { success: true, editsApplied: matchCount };
635
+ }
636
+ catch (error) {
637
+ const errorMessage = error instanceof Error ? error.message : String(error);
638
+ return {
639
+ success: false, editsApplied: 0,
640
+ errors: [{ location: 'docx', error: errorMessage }],
641
+ };
642
+ }
643
+ }
644
+ /**
645
+ * Get DOCX file info
646
+ */
647
+ async getInfo(path) {
648
+ const stats = await fs.stat(path);
649
+ let metadata = { isDocx: true };
650
+ try {
651
+ const buf = await fs.readFile(path);
652
+ const { documentXml } = loadDocxZip(buf);
653
+ const text = extractAllText(documentXml);
654
+ const wordCount = text.split(/\s+/).filter(w => w.length > 0).length;
655
+ const paragraphCount = (documentXml.match(/<w:p[\s>]/g) || []).length;
656
+ const tableCount = (documentXml.match(/<w:tbl[\s>]/g) || []).length;
657
+ const imageCount = (documentXml.match(/<w:drawing[\s>]/g) || []).length;
658
+ metadata = {
659
+ isDocx: true,
660
+ paragraphCount,
661
+ tableCount,
662
+ imageCount,
663
+ wordCount,
664
+ };
665
+ }
666
+ catch { /* return basic info */ }
667
+ return {
668
+ size: stats.size,
669
+ created: stats.birthtime,
670
+ modified: stats.mtime,
671
+ accessed: stats.atime,
672
+ isDirectory: false,
673
+ isFile: true,
674
+ permissions: (stats.mode & 0o777).toString(8),
675
+ fileType: 'docx',
676
+ metadata,
677
+ };
678
+ }
679
+ }