@wonderwhy-er/desktop-commander 0.2.36 → 0.2.38

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 (94) hide show
  1. package/README.md +240 -100
  2. package/dist/command-manager.js +6 -3
  3. package/dist/config-field-definitions.d.ts +41 -0
  4. package/dist/config-field-definitions.js +37 -0
  5. package/dist/config-manager.d.ts +2 -0
  6. package/dist/config-manager.js +22 -2
  7. package/dist/handlers/filesystem-handlers.js +6 -11
  8. package/dist/handlers/macos-control-handlers.d.ts +16 -0
  9. package/dist/handlers/macos-control-handlers.js +81 -0
  10. package/dist/lib.d.ts +10 -0
  11. package/dist/lib.js +10 -0
  12. package/dist/remote-device/remote-channel.d.ts +8 -3
  13. package/dist/remote-device/remote-channel.js +68 -21
  14. package/dist/search-manager.d.ts +13 -0
  15. package/dist/search-manager.js +146 -0
  16. package/dist/server.js +29 -1
  17. package/dist/test-docx.d.ts +1 -0
  18. package/dist/tools/config.d.ts +71 -0
  19. package/dist/tools/config.js +117 -2
  20. package/dist/tools/docx/builders/table.d.ts +2 -0
  21. package/dist/tools/docx/builders/table.js +60 -16
  22. package/dist/tools/docx/dom.d.ts +74 -1
  23. package/dist/tools/docx/dom.js +221 -1
  24. package/dist/tools/docx/index.d.ts +2 -2
  25. package/dist/tools/docx/ops/index.js +3 -0
  26. package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +15 -3
  27. package/dist/tools/docx/ops/replace-paragraph-text-exact.js +25 -10
  28. package/dist/tools/docx/ops/replace-table-cell-text.d.ts +25 -0
  29. package/dist/tools/docx/ops/replace-table-cell-text.js +85 -0
  30. package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +2 -1
  31. package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +9 -8
  32. package/dist/tools/docx/ops/set-color-for-style.d.ts +4 -0
  33. package/dist/tools/docx/ops/set-color-for-style.js +11 -7
  34. package/dist/tools/docx/ops/table-set-cell-text.js +8 -40
  35. package/dist/tools/docx/read.d.ts +2 -2
  36. package/dist/tools/docx/read.js +137 -17
  37. package/dist/tools/docx/types.d.ts +32 -3
  38. package/dist/tools/docx/xml-view-test.d.ts +1 -0
  39. package/dist/tools/docx/xml-view-test.js +63 -0
  40. package/dist/tools/docx/xml-view.d.ts +56 -0
  41. package/dist/tools/docx/xml-view.js +169 -0
  42. package/dist/tools/edit.js +57 -27
  43. package/dist/tools/macos-control/ax-adapter.d.ts +55 -0
  44. package/dist/tools/macos-control/ax-adapter.js +438 -0
  45. package/dist/tools/macos-control/cdp-adapter.d.ts +23 -0
  46. package/dist/tools/macos-control/cdp-adapter.js +402 -0
  47. package/dist/tools/macos-control/orchestrator.d.ts +77 -0
  48. package/dist/tools/macos-control/orchestrator.js +136 -0
  49. package/dist/tools/macos-control/role-aliases.d.ts +5 -0
  50. package/dist/tools/macos-control/role-aliases.js +34 -0
  51. package/dist/tools/macos-control/types.d.ts +129 -0
  52. package/dist/tools/macos-control/types.js +1 -0
  53. package/dist/tools/schemas.d.ts +3 -0
  54. package/dist/tools/schemas.js +2 -1
  55. package/dist/types.d.ts +0 -1
  56. package/dist/ui/config-editor/config-editor-runtime.js +14181 -0
  57. package/dist/ui/config-editor/index.html +13 -0
  58. package/dist/ui/config-editor/src/app.d.ts +43 -0
  59. package/dist/ui/config-editor/src/app.js +840 -0
  60. package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
  61. package/dist/ui/config-editor/src/array-modal.js +185 -0
  62. package/dist/ui/config-editor/src/main.d.ts +1 -0
  63. package/dist/ui/config-editor/src/main.js +2 -0
  64. package/dist/ui/config-editor/styles.css +586 -0
  65. package/dist/ui/file-preview/preview-runtime.js +13337 -752
  66. package/dist/ui/file-preview/shared/preview-file-types.js +3 -1
  67. package/dist/ui/file-preview/src/app.d.ts +5 -1
  68. package/dist/ui/file-preview/src/app.js +114 -200
  69. package/dist/ui/file-preview/src/components/html-renderer.d.ts +1 -5
  70. package/dist/ui/file-preview/src/components/html-renderer.js +11 -27
  71. package/dist/ui/file-preview/styles.css +117 -83
  72. package/dist/ui/resources.d.ts +7 -0
  73. package/dist/ui/resources.js +16 -2
  74. package/dist/ui/shared/compact-row.d.ts +11 -0
  75. package/dist/ui/shared/compact-row.js +18 -0
  76. package/dist/ui/shared/host-context.d.ts +15 -0
  77. package/dist/ui/shared/host-context.js +51 -0
  78. package/dist/ui/shared/tool-bridge.d.ts +30 -0
  79. package/dist/ui/shared/tool-bridge.js +137 -0
  80. package/dist/ui/shared/tool-shell.d.ts +9 -0
  81. package/dist/ui/shared/tool-shell.js +46 -4
  82. package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
  83. package/dist/ui/shared/ui-event-tracker.js +27 -0
  84. package/dist/utils/capture.js +173 -11
  85. package/dist/utils/files/base.d.ts +3 -1
  86. package/dist/utils/files/docx.d.ts +28 -15
  87. package/dist/utils/files/docx.js +622 -88
  88. package/dist/utils/files/factory.d.ts +6 -5
  89. package/dist/utils/files/factory.js +18 -6
  90. package/dist/utils/system-info.js +1 -1
  91. package/dist/utils/usageTracker.js +5 -0
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.js +1 -1
  94. package/package.json +8 -3
@@ -1,135 +1,669 @@
1
1
  /**
2
2
  * DOCX File Handler
3
- * Implements FileHandler interface for DOCX documents
4
- * Handles reading, writing, and modifying DOCX files while preserving formatting
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
5
17
  */
6
18
  import fs from 'fs/promises';
7
- import { readDocx, getDocxMetadata, modifyDocxContent } from '../../tools/docx/index.js';
19
+ import PizZip from 'pizzip';
20
+ // ════════════════════════════════════════════════════════════════
21
+ // XML Pretty-Print / Compact
22
+ // ════════════════════════════════════════════════════════════════
8
23
  /**
9
- * File handler for DOCX documents
10
- * Extracts text and metadata, supports paragraph-based pagination
24
+ * Pretty-print XML: split tags onto separate lines with indentation.
25
+ * Preserves text node content exactly. compact→pretty→compact is lossless.
11
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
+ // ════════════════════════════════════════════════════════════════
12
448
  export class DocxFileHandler {
13
449
  constructor() {
14
450
  this.extensions = ['.docx'];
15
451
  }
16
- /**
17
- * Check if this handler can handle the given file
18
- */
19
452
  canHandle(path) {
20
- const ext = path.toLowerCase();
21
- return this.extensions.some(e => ext.endsWith(e));
453
+ return this.extensions.some(e => path.toLowerCase().endsWith(e));
22
454
  }
23
455
  /**
24
- * Read DOCX content - returns body XML for LLM modification
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
25
460
  */
26
461
  async read(path, options) {
27
- const { offset = 0, length } = options ?? {};
28
- try {
29
- const result = await readDocx(path, {
30
- offset,
31
- length
32
- });
33
- // Return body XML as content - LLMs can modify this and write it back
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)]`;
34
485
  return {
35
- content: result.bodyXml,
486
+ content: status + '\n' + sliced.join('\n'),
36
487
  mimeType: 'application/xml',
37
- metadata: {
38
- isDocx: true,
39
- author: result.metadata.author,
40
- title: result.metadata.title,
41
- subject: result.metadata.subject,
42
- creator: result.metadata.creator,
43
- paragraphCount: result.metadata.paragraphCount,
44
- wordCount: result.metadata.wordCount,
45
- paragraphs: result.paragraphs,
46
- // Include extracted text for reference
47
- extractedText: result.text
48
- }
49
- };
50
- }
51
- catch (error) {
52
- const errorMessage = error instanceof Error ? error.message : String(error);
53
- return {
54
- content: `Error reading DOCX: ${errorMessage}`,
55
- mimeType: 'text/plain',
56
- metadata: {
57
- error: true,
58
- errorMessage
59
- }
488
+ metadata: { isDocx: true, lineCount: totalLines },
60
489
  };
61
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
+ };
62
502
  }
63
503
  /**
64
- * Write DOCX - NOT SUPPORTED via write_file
65
- * Use write_docx tool instead to preserve styles
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.)
66
507
  */
67
508
  async write(path, content, mode) {
68
- throw new Error('DOCX files cannot be written using write_file tool. ' +
69
- 'Use write_docx tool instead to create or modify DOCX files while preserving styles and formatting.');
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);
70
539
  }
71
540
  /**
72
- * Edit DOCX by applying modifications
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.
73
546
  */
74
- async editRange(path, range, content, options) {
547
+ async editRange(path, _range, content, options) {
75
548
  try {
76
- // Parse content as modifications
77
- let modifications = [];
78
- if (Array.isArray(content)) {
79
- modifications = content;
80
- }
81
- else if (typeof content === 'string') {
82
- // Try to parse as JSON
83
- try {
84
- modifications = JSON.parse(content);
85
- }
86
- catch {
87
- // If not JSON, treat as single replace operation
88
- modifications = [{
89
- type: 'replace',
90
- findText: range,
91
- replaceText: content
92
- }];
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
+ }
93
597
  }
94
598
  }
95
- const outputPath = options?.outputPath || path;
96
- await modifyDocxContent(path, outputPath, modifications);
97
- return {
98
- success: true,
99
- editsApplied: modifications.length
100
- };
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 };
101
635
  }
102
636
  catch (error) {
103
637
  const errorMessage = error instanceof Error ? error.message : String(error);
104
638
  return {
105
- success: false,
106
- editsApplied: 0,
107
- errors: [{ location: range, error: errorMessage }]
639
+ success: false, editsApplied: 0,
640
+ errors: [{ location: 'docx', error: errorMessage }],
108
641
  };
109
642
  }
110
643
  }
111
644
  /**
112
- * Get DOCX file information
645
+ * Get DOCX file info
113
646
  */
114
647
  async getInfo(path) {
115
648
  const stats = await fs.stat(path);
116
- // Get DOCX metadata
117
649
  let metadata = { isDocx: true };
118
650
  try {
119
- const docxMetadata = await getDocxMetadata(path);
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;
120
658
  metadata = {
121
659
  isDocx: true,
122
- title: docxMetadata.title,
123
- author: docxMetadata.author,
124
- subject: docxMetadata.subject,
125
- creator: docxMetadata.creator,
126
- paragraphCount: docxMetadata.paragraphCount,
127
- wordCount: docxMetadata.wordCount
660
+ paragraphCount,
661
+ tableCount,
662
+ imageCount,
663
+ wordCount,
128
664
  };
129
665
  }
130
- catch {
131
- // If we can't parse, just return basic info
132
- }
666
+ catch { /* return basic info */ }
133
667
  return {
134
668
  size: stats.size,
135
669
  created: stats.birthtime,
@@ -138,8 +672,8 @@ export class DocxFileHandler {
138
672
  isDirectory: false,
139
673
  isFile: true,
140
674
  permissions: (stats.mode & 0o777).toString(8),
141
- fileType: 'binary',
142
- metadata
675
+ fileType: 'docx',
676
+ metadata,
143
677
  };
144
678
  }
145
679
  }