@wonderwhy-er/desktop-commander 0.2.36 → 0.2.37
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.
- package/README.md +1 -0
- package/dist/remote-device/remote-channel.d.ts +8 -3
- package/dist/remote-device/remote-channel.js +68 -21
- package/dist/search-manager.d.ts +13 -0
- package/dist/search-manager.js +146 -0
- package/dist/server.js +26 -0
- package/dist/test-docx.d.ts +1 -0
- package/dist/tools/docx/builders/table.d.ts +2 -0
- package/dist/tools/docx/builders/table.js +60 -16
- package/dist/tools/docx/dom.d.ts +74 -1
- package/dist/tools/docx/dom.js +221 -1
- package/dist/tools/docx/index.d.ts +2 -2
- package/dist/tools/docx/ops/index.js +3 -0
- package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +15 -3
- package/dist/tools/docx/ops/replace-paragraph-text-exact.js +25 -10
- package/dist/tools/docx/ops/replace-table-cell-text.d.ts +25 -0
- package/dist/tools/docx/ops/replace-table-cell-text.js +85 -0
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +2 -1
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +9 -8
- package/dist/tools/docx/ops/set-color-for-style.d.ts +4 -0
- package/dist/tools/docx/ops/set-color-for-style.js +11 -7
- package/dist/tools/docx/ops/table-set-cell-text.js +8 -40
- package/dist/tools/docx/read.d.ts +2 -2
- package/dist/tools/docx/read.js +137 -17
- package/dist/tools/docx/types.d.ts +32 -3
- package/dist/tools/docx/xml-view-test.d.ts +1 -0
- package/dist/tools/docx/xml-view-test.js +63 -0
- package/dist/tools/docx/xml-view.d.ts +56 -0
- package/dist/tools/docx/xml-view.js +169 -0
- package/dist/tools/edit.js +57 -27
- package/dist/tools/schemas.js +1 -1
- package/dist/ui/file-preview/preview-runtime.js +7 -1
- package/dist/utils/capture.js +171 -9
- package/dist/utils/files/base.d.ts +3 -1
- package/dist/utils/files/docx.d.ts +28 -15
- package/dist/utils/files/docx.js +622 -88
- package/dist/utils/files/factory.d.ts +6 -5
- package/dist/utils/files/factory.js +18 -6
- package/dist/utils/system-info.js +1 -1
- package/dist/utils/usageTracker.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
### Search, update, manage files and run terminal commands with AI
|
|
3
3
|
|
|
4
4
|
[](https://www.npmjs.com/package/@wonderwhy-er/desktop-commander)
|
|
5
|
+
[](https://agentaudit.dev/skills/desktop-commander)
|
|
5
6
|
[](https://archestra.ai/mcp-catalog/wonderwhy-er__desktopcommandermcp)
|
|
6
7
|
[](https://smithery.ai/server/@wonderwhy-er/desktop-commander)
|
|
7
8
|
[](https://www.buymeacoffee.com/wonderwhyer)
|
|
@@ -36,10 +36,15 @@ export declare class RemoteChannel {
|
|
|
36
36
|
id: any;
|
|
37
37
|
device_name: any;
|
|
38
38
|
} | null>;
|
|
39
|
-
updateDevice(deviceId: string, updates: any): Promise<
|
|
40
|
-
|
|
39
|
+
updateDevice(deviceId: string, updates: any): Promise<{
|
|
40
|
+
data: any[] | null;
|
|
41
|
+
error: import("@supabase/postgrest-js").PostgrestError | null;
|
|
42
|
+
}>;
|
|
43
|
+
createDevice(deviceData: DeviceData): Promise<{
|
|
44
|
+
data: any;
|
|
45
|
+
error: null;
|
|
46
|
+
}>;
|
|
41
47
|
registerDevice(capabilities: any, currentDeviceId: string | undefined, deviceName: string, onToolCall: (payload: any) => void): Promise<void>;
|
|
42
|
-
subscribe(deviceId: string, onToolCall: (payload: any) => void): Promise<void>;
|
|
43
48
|
/**
|
|
44
49
|
* Create and subscribe to the channel.
|
|
45
50
|
* This is used for both initial subscription and recreation after socket reconnects.
|
|
@@ -28,14 +28,26 @@ export class RemoteChannel {
|
|
|
28
28
|
access_token: session.access_token,
|
|
29
29
|
refresh_token: session.refresh_token || ''
|
|
30
30
|
});
|
|
31
|
+
if (error) {
|
|
32
|
+
console.error('[DEBUG] Failed to set session:', error.message);
|
|
33
|
+
await captureRemote('remote_channel_set_session_error', { error });
|
|
34
|
+
return { error };
|
|
35
|
+
}
|
|
31
36
|
// Get user info
|
|
32
37
|
const { data: { user }, error: userError } = await this.client.auth.getUser();
|
|
33
38
|
if (userError) {
|
|
34
|
-
console.
|
|
39
|
+
console.error('[DEBUG] Failed to get user:', userError.message);
|
|
40
|
+
await captureRemote('remote_channel_get_user_error', { error: userError });
|
|
35
41
|
throw userError;
|
|
36
42
|
}
|
|
43
|
+
if (!user) {
|
|
44
|
+
const noUserError = new Error('No user returned after setSession');
|
|
45
|
+
console.error('[DEBUG] No user returned:', noUserError.message);
|
|
46
|
+
await captureRemote('remote_channel_get_user_empty', {});
|
|
47
|
+
throw noUserError;
|
|
48
|
+
}
|
|
37
49
|
this._user = user;
|
|
38
|
-
console.debug('[DEBUG] Session set successfully, user:', user
|
|
50
|
+
console.debug('[DEBUG] Session set successfully, user:', user.email);
|
|
39
51
|
return { error };
|
|
40
52
|
}
|
|
41
53
|
async getSession() {
|
|
@@ -52,26 +64,45 @@ export class RemoteChannel {
|
|
|
52
64
|
.eq('id', deviceId)
|
|
53
65
|
.eq('user_id', this.user?.id)
|
|
54
66
|
.maybeSingle();
|
|
55
|
-
if (error)
|
|
67
|
+
if (error) {
|
|
68
|
+
console.error('[DEBUG] Failed to find device:', error.message);
|
|
69
|
+
await captureRemote('remote_channel_find_device_error', { error });
|
|
56
70
|
throw error;
|
|
71
|
+
}
|
|
57
72
|
return data;
|
|
58
73
|
}
|
|
59
74
|
async updateDevice(deviceId, updates) {
|
|
60
75
|
if (!this.client)
|
|
61
76
|
throw new Error('Client not initialized');
|
|
62
|
-
|
|
77
|
+
const { data, error } = await this.client
|
|
63
78
|
.from('mcp_devices')
|
|
64
79
|
.update(updates)
|
|
65
|
-
.eq('id', deviceId)
|
|
80
|
+
.eq('id', deviceId)
|
|
81
|
+
.select();
|
|
82
|
+
if (error) {
|
|
83
|
+
console.error('[DEBUG] Failed to update device:', error.message);
|
|
84
|
+
await captureRemote('remote_channel_update_device_error', { error });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.debug('[DEBUG] Device updated successfully');
|
|
88
|
+
}
|
|
89
|
+
return { data, error };
|
|
66
90
|
}
|
|
67
91
|
async createDevice(deviceData) {
|
|
68
92
|
if (!this.client)
|
|
69
93
|
throw new Error('Client not initialized');
|
|
70
|
-
|
|
94
|
+
const { data, error } = await this.client
|
|
71
95
|
.from('mcp_devices')
|
|
72
96
|
.insert(deviceData)
|
|
73
97
|
.select()
|
|
74
98
|
.single();
|
|
99
|
+
if (error) {
|
|
100
|
+
console.error('[DEBUG] Failed to create device:', error.message);
|
|
101
|
+
await captureRemote('remote_channel_create_device_error', { error });
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
console.debug('[DEBUG] Device created successfully');
|
|
105
|
+
return { data, error };
|
|
75
106
|
}
|
|
76
107
|
async registerDevice(capabilities, currentDeviceId, deviceName, onToolCall) {
|
|
77
108
|
console.debug('[DEBUG] RemoteChannel.registerDevice() called, deviceId:', currentDeviceId);
|
|
@@ -95,7 +126,10 @@ export class RemoteChannel {
|
|
|
95
126
|
console.debug(`⏳ Subscribing to tool call channel...`);
|
|
96
127
|
// Create and subscribe to the channel
|
|
97
128
|
console.debug('[DEBUG] Calling createChannel()');
|
|
98
|
-
|
|
129
|
+
// ! Ignore silently in Inialization to reconnect after
|
|
130
|
+
await this.createChannel().catch((error) => {
|
|
131
|
+
console.debug('[DEBUG] Failed to create channel, will retry after socket reconnect', error);
|
|
132
|
+
});
|
|
99
133
|
}
|
|
100
134
|
else {
|
|
101
135
|
console.error(` - ❌ Device not found: ${currentDeviceId}`);
|
|
@@ -103,16 +137,6 @@ export class RemoteChannel {
|
|
|
103
137
|
throw new Error(`Device not found: ${currentDeviceId}`);
|
|
104
138
|
}
|
|
105
139
|
}
|
|
106
|
-
async subscribe(deviceId, onToolCall) {
|
|
107
|
-
if (!this.client)
|
|
108
|
-
throw new Error('Client not initialized');
|
|
109
|
-
// Store parameters for channel recreation
|
|
110
|
-
this.deviceId = deviceId;
|
|
111
|
-
this.onToolCall = onToolCall;
|
|
112
|
-
console.debug(`⏳ Subscribing to tool call channel...`);
|
|
113
|
-
// Create and subscribe to the channel
|
|
114
|
-
await this.createChannel();
|
|
115
|
-
}
|
|
116
140
|
/**
|
|
117
141
|
* Create and subscribe to the channel.
|
|
118
142
|
* This is used for both initial subscription and recreation after socket reconnects.
|
|
@@ -156,7 +180,7 @@ export class RemoteChannel {
|
|
|
156
180
|
reject(err || new Error('Failed to initialize tool call channel subscription'));
|
|
157
181
|
}
|
|
158
182
|
else if (status === 'TIMED_OUT') {
|
|
159
|
-
console.error('⏱️ Channel subscription timed out');
|
|
183
|
+
console.error('⏱️ Channel subscription timed out, Reconnecting...');
|
|
160
184
|
this.setOnlineStatus(this.deviceId, 'offline');
|
|
161
185
|
captureRemote('remote_channel_subscription_timeout', {}).catch(() => { });
|
|
162
186
|
reject(new Error('Tool call channel subscription timed out'));
|
|
@@ -213,10 +237,17 @@ export class RemoteChannel {
|
|
|
213
237
|
async markCallExecuting(callId) {
|
|
214
238
|
if (!this.client)
|
|
215
239
|
throw new Error('Client not initialized');
|
|
216
|
-
await this.client
|
|
240
|
+
const { error } = await this.client
|
|
217
241
|
.from('mcp_remote_calls')
|
|
218
242
|
.update({ status: 'executing' })
|
|
219
243
|
.eq('id', callId);
|
|
244
|
+
if (error) {
|
|
245
|
+
console.error('[DEBUG] Failed to mark call executing:', error.message);
|
|
246
|
+
await captureRemote('remote_channel_mark_call_executing_error', { error });
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
console.debug('[DEBUG] Call marked executing:', callId);
|
|
250
|
+
}
|
|
220
251
|
}
|
|
221
252
|
async updateCallResult(callId, status, result = null, errorMessage = null) {
|
|
222
253
|
if (!this.client)
|
|
@@ -229,19 +260,31 @@ export class RemoteChannel {
|
|
|
229
260
|
updateData.result = result;
|
|
230
261
|
if (errorMessage !== null)
|
|
231
262
|
updateData.error_message = errorMessage;
|
|
232
|
-
|
|
263
|
+
console.debug('[DEBUG] Updating call result:', updateData);
|
|
264
|
+
const { data, error } = await this.client
|
|
233
265
|
.from('mcp_remote_calls')
|
|
234
266
|
.update(updateData)
|
|
235
267
|
.eq('id', callId);
|
|
268
|
+
if (error) {
|
|
269
|
+
console.error('[DEBUG] Failed to update call result:', error.message);
|
|
270
|
+
await captureRemote('remote_channel_update_call_result_error', { error });
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.debug('[DEBUG] Call result updated successfully:', data);
|
|
274
|
+
}
|
|
236
275
|
}
|
|
237
276
|
async updateHeartbeat(deviceId) {
|
|
238
277
|
if (!this.client)
|
|
239
278
|
return;
|
|
240
279
|
try {
|
|
241
|
-
await this.client
|
|
280
|
+
const { error } = await this.client
|
|
242
281
|
.from('mcp_devices')
|
|
243
282
|
.update({ last_seen: new Date().toISOString() })
|
|
244
283
|
.eq('id', deviceId);
|
|
284
|
+
if (error) {
|
|
285
|
+
console.error('[DEBUG] Heartbeat update failed:', error.message);
|
|
286
|
+
await captureRemote('remote_channel_heartbeat_error', { error });
|
|
287
|
+
}
|
|
245
288
|
// console.log(`🔌 Heartbeat sent for device: ${deviceId}`);
|
|
246
289
|
}
|
|
247
290
|
catch (error) {
|
|
@@ -283,12 +326,16 @@ export class RemoteChannel {
|
|
|
283
326
|
.update({ status: status, last_seen: new Date().toISOString() })
|
|
284
327
|
.eq('id', deviceId);
|
|
285
328
|
if (error) {
|
|
329
|
+
console.error(`[DEBUG] Failed to set status ${status}:`, error.message);
|
|
286
330
|
if (status == "online") {
|
|
287
331
|
console.error('Failed to update device status:', error.message);
|
|
288
332
|
}
|
|
289
333
|
await captureRemote('remote_channel_status_update_error', { error, status });
|
|
290
334
|
return;
|
|
291
335
|
}
|
|
336
|
+
else {
|
|
337
|
+
console.debug(`[DEBUG] Device status set to ${status}`);
|
|
338
|
+
}
|
|
292
339
|
// console.log(status === 'online' ? `🔌 Device marked as ${status}` : `❌ Device marked as ${status}`);
|
|
293
340
|
}
|
|
294
341
|
async setOffline(deviceId) {
|
package/dist/search-manager.d.ts
CHANGED
|
@@ -97,6 +97,19 @@ export interface SearchSessionOptions {
|
|
|
97
97
|
* Find all Excel files in a directory recursively
|
|
98
98
|
*/
|
|
99
99
|
private findExcelFiles;
|
|
100
|
+
/**
|
|
101
|
+
* Determine if DOCX search should be included based on context
|
|
102
|
+
*/
|
|
103
|
+
private shouldIncludeDocxSearch;
|
|
104
|
+
/**
|
|
105
|
+
* Search DOCX files for content matches
|
|
106
|
+
* Extracts <w:t> text from document.xml and searches it
|
|
107
|
+
*/
|
|
108
|
+
private searchDocxFiles;
|
|
109
|
+
/**
|
|
110
|
+
* Find all DOCX files in a directory recursively
|
|
111
|
+
*/
|
|
112
|
+
private findDocxFiles;
|
|
100
113
|
/**
|
|
101
114
|
* Extract context around a match for display (show surrounding text)
|
|
102
115
|
*/
|
package/dist/search-manager.js
CHANGED
|
@@ -5,6 +5,7 @@ import { validatePath } from './tools/filesystem.js';
|
|
|
5
5
|
import { capture } from './utils/capture.js';
|
|
6
6
|
import { getRipgrepPath } from './utils/ripgrep-resolver.js';
|
|
7
7
|
import { isExcelFile } from './utils/files/index.js';
|
|
8
|
+
import PizZip from 'pizzip';
|
|
8
9
|
/**
|
|
9
10
|
* Search Session Manager - handles ripgrep processes like terminal sessions
|
|
10
11
|
* Supports both file search and content search with progressive results
|
|
@@ -105,6 +106,19 @@ import { isExcelFile } from './utils/files/index.js';
|
|
|
105
106
|
capture('excel_search_error', { error: err instanceof Error ? err.message : String(err) });
|
|
106
107
|
});
|
|
107
108
|
}
|
|
109
|
+
// For content searches, also search DOCX files
|
|
110
|
+
const shouldSearchDocx = options.searchType === 'content' &&
|
|
111
|
+
this.shouldIncludeDocxSearch(options.filePattern, validPath);
|
|
112
|
+
if (shouldSearchDocx) {
|
|
113
|
+
this.searchDocxFiles(validPath, options.pattern, options.ignoreCase !== false, options.maxResults, options.filePattern).then(docxResults => {
|
|
114
|
+
for (const result of docxResults) {
|
|
115
|
+
session.results.push(result);
|
|
116
|
+
session.totalMatches++;
|
|
117
|
+
}
|
|
118
|
+
}).catch((err) => {
|
|
119
|
+
capture('docx_search_error', { error: err instanceof Error ? err.message : String(err) });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
108
122
|
// Wait for first chunk of data or early completion instead of fixed delay
|
|
109
123
|
// Excel search runs in background and results are merged via readSearchResults
|
|
110
124
|
const firstChunk = new Promise(resolve => {
|
|
@@ -349,6 +363,138 @@ import { isExcelFile } from './utils/files/index.js';
|
|
|
349
363
|
}
|
|
350
364
|
return excelFiles;
|
|
351
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Determine if DOCX search should be included based on context
|
|
368
|
+
*/
|
|
369
|
+
shouldIncludeDocxSearch(filePattern, rootPath) {
|
|
370
|
+
const docxExtensions = ['.docx'];
|
|
371
|
+
if (rootPath) {
|
|
372
|
+
const lowerPath = rootPath.toLowerCase();
|
|
373
|
+
if (docxExtensions.some(ext => lowerPath.endsWith(ext))) {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (filePattern) {
|
|
378
|
+
const lowerPattern = filePattern.toLowerCase();
|
|
379
|
+
if (docxExtensions.some(ext => lowerPattern.includes(`*${ext}`) || lowerPattern.endsWith(ext))) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Search DOCX files for content matches
|
|
387
|
+
* Extracts <w:t> text from document.xml and searches it
|
|
388
|
+
*/
|
|
389
|
+
async searchDocxFiles(rootPath, pattern, ignoreCase, maxResults, filePattern) {
|
|
390
|
+
const results = [];
|
|
391
|
+
const flags = ignoreCase ? 'i' : '';
|
|
392
|
+
let regex;
|
|
393
|
+
try {
|
|
394
|
+
regex = new RegExp(pattern, flags);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
398
|
+
regex = new RegExp(escaped, flags);
|
|
399
|
+
}
|
|
400
|
+
let docxFiles = await this.findDocxFiles(rootPath);
|
|
401
|
+
if (filePattern) {
|
|
402
|
+
const patterns = filePattern.split('|').map(p => p.trim()).filter(Boolean);
|
|
403
|
+
docxFiles = docxFiles.filter(filePath => {
|
|
404
|
+
const fileName = path.basename(filePath);
|
|
405
|
+
return patterns.some(pat => {
|
|
406
|
+
if (pat.includes('*')) {
|
|
407
|
+
const regexPat = pat.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
|
408
|
+
return new RegExp(`^${regexPat}$`, 'i').test(fileName);
|
|
409
|
+
}
|
|
410
|
+
return fileName.toLowerCase() === pat.toLowerCase();
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
for (const filePath of docxFiles) {
|
|
415
|
+
if (maxResults && results.length >= maxResults)
|
|
416
|
+
break;
|
|
417
|
+
try {
|
|
418
|
+
const buf = await fs.readFile(filePath);
|
|
419
|
+
const zip = new PizZip(buf);
|
|
420
|
+
// Search all XML parts that can contain text
|
|
421
|
+
const xmlParts = ['word/document.xml', 'word/header1.xml', 'word/header2.xml',
|
|
422
|
+
'word/header3.xml', 'word/footer1.xml', 'word/footer2.xml', 'word/footer3.xml'];
|
|
423
|
+
for (const xmlPath of xmlParts) {
|
|
424
|
+
if (maxResults && results.length >= maxResults)
|
|
425
|
+
break;
|
|
426
|
+
const file = zip.file(xmlPath);
|
|
427
|
+
if (!file)
|
|
428
|
+
continue;
|
|
429
|
+
const xml = file.asText();
|
|
430
|
+
// Extract all <w:t> text with position tracking
|
|
431
|
+
const wtRe = /<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>/g;
|
|
432
|
+
let m;
|
|
433
|
+
let lineNum = 0;
|
|
434
|
+
while ((m = wtRe.exec(xml)) !== null) {
|
|
435
|
+
if (maxResults && results.length >= maxResults)
|
|
436
|
+
break;
|
|
437
|
+
const text = m[1];
|
|
438
|
+
if (!text || !text.trim())
|
|
439
|
+
continue;
|
|
440
|
+
lineNum++;
|
|
441
|
+
if (regex.test(text)) {
|
|
442
|
+
const match = text.match(regex);
|
|
443
|
+
const matchContext = match
|
|
444
|
+
? this.getMatchContext(text, match.index || 0, match[0].length)
|
|
445
|
+
: text.substring(0, 150);
|
|
446
|
+
const partName = xmlPath === 'word/document.xml' ? '' : `:${xmlPath.replace('word/', '')}`;
|
|
447
|
+
results.push({
|
|
448
|
+
file: `${filePath}${partName}`,
|
|
449
|
+
line: lineNum,
|
|
450
|
+
match: matchContext,
|
|
451
|
+
type: 'content'
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return results;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Find all DOCX files in a directory recursively
|
|
465
|
+
*/
|
|
466
|
+
async findDocxFiles(rootPath) {
|
|
467
|
+
const docxFiles = [];
|
|
468
|
+
const isDocx = (name) => name.toLowerCase().endsWith('.docx');
|
|
469
|
+
async function walk(dir) {
|
|
470
|
+
try {
|
|
471
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
472
|
+
for (const entry of entries) {
|
|
473
|
+
const fullPath = path.join(dir, entry.name);
|
|
474
|
+
if (entry.isDirectory()) {
|
|
475
|
+
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
476
|
+
await walk(fullPath);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else if (entry.isFile() && isDocx(entry.name)) {
|
|
480
|
+
docxFiles.push(fullPath);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch { /* skip */ }
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const stats = await fs.stat(rootPath);
|
|
488
|
+
if (stats.isFile() && isDocx(rootPath)) {
|
|
489
|
+
return [rootPath];
|
|
490
|
+
}
|
|
491
|
+
else if (stats.isDirectory()) {
|
|
492
|
+
await walk(rootPath);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch { /* skip */ }
|
|
496
|
+
return docxFiles;
|
|
497
|
+
}
|
|
352
498
|
/**
|
|
353
499
|
* Extract context around a match for display (show surrounding text)
|
|
354
500
|
*/
|
package/dist/server.js
CHANGED
|
@@ -242,6 +242,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
242
242
|
- PDF: Extracts text content as markdown with page structure
|
|
243
243
|
* offset/length work as page pagination (0-based)
|
|
244
244
|
* Includes embedded images when available
|
|
245
|
+
- DOCX (.docx): Two modes depending on parameters:
|
|
246
|
+
* DEFAULT (no offset/length): Returns a text-bearing outline — shows paragraphs with text,
|
|
247
|
+
tables with cell content, styles, image refs. Skips shapes/drawings/SVG noise.
|
|
248
|
+
Each element shows its body index [0], [1], etc.
|
|
249
|
+
* WITH offset/length: Returns raw pretty-printed XML with line pagination.
|
|
250
|
+
Use this to drill into specific sections or see the actual XML for editing.
|
|
251
|
+
* EDITING WORKFLOW: 1) read_file to get outline, 2) read_file with offset/length
|
|
252
|
+
to see raw XML around what you want to edit, 3) edit_block with old_string/new_string
|
|
253
|
+
using XML fragments copied from the read output.
|
|
254
|
+
* IMPORTANT: offset MUST be non-zero to get raw XML (use offset=1 to start from line 1).
|
|
255
|
+
offset=0 always returns the outline regardless of length.
|
|
256
|
+
* For BULK changes (translation, mass replacements): use start_process with Python
|
|
257
|
+
zipfile module to find/replace all <w:t> elements at once.
|
|
245
258
|
|
|
246
259
|
${PATH_GUIDANCE}
|
|
247
260
|
${CMD_PREFIX_DESCRIPTION}`,
|
|
@@ -279,6 +292,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
279
292
|
Write or append to file contents.
|
|
280
293
|
|
|
281
294
|
IMPORTANT: DO NOT use this tool to create PDF files. Use 'write_pdf' for all PDF creation tasks.
|
|
295
|
+
DO NOT use this tool to edit DOCX files. Use 'edit_block' with old_string/new_string instead.
|
|
296
|
+
To CREATE a new DOCX, use write_file with .docx extension — text content with markdown headings (#, ##, ###) is converted to styled DOCX paragraphs.
|
|
282
297
|
|
|
283
298
|
CHUNKING IS STANDARD PRACTICE: Always write files in chunks of 25-30 lines maximum.
|
|
284
299
|
This is the normal, recommended way to write files - not an emergency measure.
|
|
@@ -657,6 +672,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
657
672
|
- new_string: Replacement text
|
|
658
673
|
- expected_replacements: Optional number of replacements (default: 1)
|
|
659
674
|
|
|
675
|
+
DOCX FILES (.docx) - XML Find/Replace mode:
|
|
676
|
+
Takes same parameters as text files (old_string, new_string, expected_replacements).
|
|
677
|
+
Operates on the pretty-printed XML inside the DOCX — the same XML you see from
|
|
678
|
+
read_file with offset/length. Copy XML fragments from read output as old_string.
|
|
679
|
+
After editing, the XML is repacked into a valid DOCX.
|
|
680
|
+
Also searches headers/footers if not found in document body.
|
|
681
|
+
Examples:
|
|
682
|
+
- Replace text: old_string="<w:t>Old Text</w:t>" new_string="<w:t>New Text</w:t>"
|
|
683
|
+
- Change style: old_string='<w:pStyle w:val="Normal"/>' new_string='<w:pStyle w:val="Heading1"/>'
|
|
684
|
+
- Add content: include surrounding XML context in old_string, add new elements in new_string
|
|
685
|
+
|
|
660
686
|
By default, replaces only ONE occurrence of the search text.
|
|
661
687
|
To replace multiple occurrences, provide expected_replacements with
|
|
662
688
|
the exact number of matches expected.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Table builder — creates w:tbl elements with headers, rows, and styling.
|
|
3
|
+
* Supports multiple paragraphs per cell with different styles.
|
|
3
4
|
*/
|
|
4
5
|
import type { DocxContentTable, InsertTableOp } from '../types.js';
|
|
5
6
|
/**
|
|
6
7
|
* Build a table element from content structure or operation.
|
|
8
|
+
* Supports cells with multiple paragraphs, each with its own style.
|
|
7
9
|
*/
|
|
8
10
|
export declare function buildTable(doc: Document, spec: DocxContentTable | InsertTableOp): Element;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Table builder — creates w:tbl elements with headers, rows, and styling.
|
|
3
|
+
* Supports multiple paragraphs per cell with different styles.
|
|
3
4
|
*/
|
|
5
|
+
import { buildParagraph } from './paragraph.js';
|
|
4
6
|
/**
|
|
5
7
|
* Build a table element from content structure or operation.
|
|
8
|
+
* Supports cells with multiple paragraphs, each with its own style.
|
|
6
9
|
*/
|
|
7
10
|
export function buildTable(doc, spec) {
|
|
8
11
|
const tbl = doc.createElement('w:tbl');
|
|
@@ -29,12 +32,13 @@ export function buildTable(doc, spec) {
|
|
|
29
32
|
}
|
|
30
33
|
tblPr.appendChild(tblBorders);
|
|
31
34
|
tbl.appendChild(tblPr);
|
|
32
|
-
//
|
|
35
|
+
// Determine column count
|
|
33
36
|
const colCount = spec.headers
|
|
34
37
|
? spec.headers.length
|
|
35
38
|
: spec.rows.length > 0
|
|
36
39
|
? spec.rows[0].length
|
|
37
40
|
: 0;
|
|
41
|
+
// Table grid
|
|
38
42
|
if (colCount > 0) {
|
|
39
43
|
const tblGrid = doc.createElement('w:tblGrid');
|
|
40
44
|
for (let c = 0; c < colCount; c++) {
|
|
@@ -45,9 +49,15 @@ export function buildTable(doc, spec) {
|
|
|
45
49
|
}
|
|
46
50
|
tbl.appendChild(tblGrid);
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Build a cell from content.
|
|
54
|
+
* Content can be:
|
|
55
|
+
* - A string: creates one paragraph with that text
|
|
56
|
+
* - An array of DocxContentParagraph: creates multiple paragraphs, each with its own style
|
|
57
|
+
*/
|
|
58
|
+
const buildCell = (content, isHeader, widthTwips) => {
|
|
50
59
|
const tc = doc.createElement('w:tc');
|
|
60
|
+
// Cell properties (width)
|
|
51
61
|
if (widthTwips) {
|
|
52
62
|
const tcPr = doc.createElement('w:tcPr');
|
|
53
63
|
const tcW = doc.createElement('w:tcW');
|
|
@@ -56,20 +66,54 @@ export function buildTable(doc, spec) {
|
|
|
56
66
|
tcPr.appendChild(tcW);
|
|
57
67
|
tc.appendChild(tcPr);
|
|
58
68
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
// Handle content: string or array of paragraphs
|
|
70
|
+
if (typeof content === 'string') {
|
|
71
|
+
// Simple case: single paragraph
|
|
72
|
+
const p = doc.createElement('w:p');
|
|
73
|
+
const r = doc.createElement('w:r');
|
|
74
|
+
// Header cells get bold
|
|
75
|
+
if (isHeader) {
|
|
76
|
+
const rPr = doc.createElement('w:rPr');
|
|
77
|
+
const b = doc.createElement('w:b');
|
|
78
|
+
rPr.appendChild(b);
|
|
79
|
+
r.appendChild(rPr);
|
|
80
|
+
}
|
|
81
|
+
const t = doc.createElement('w:t');
|
|
82
|
+
t.setAttribute('xml:space', 'preserve');
|
|
83
|
+
t.textContent = content;
|
|
84
|
+
r.appendChild(t);
|
|
85
|
+
p.appendChild(r);
|
|
86
|
+
tc.appendChild(p);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Complex case: multiple paragraphs with different styles
|
|
90
|
+
for (const paraSpec of content) {
|
|
91
|
+
const p = buildParagraph(doc, paraSpec);
|
|
92
|
+
// If header and first paragraph, ensure bold on runs
|
|
93
|
+
if (isHeader) {
|
|
94
|
+
const runs = p.getElementsByTagName('w:r');
|
|
95
|
+
for (let i = 0; i < runs.length; i++) {
|
|
96
|
+
const run = runs.item(i);
|
|
97
|
+
let rPr = run.getElementsByTagName('w:rPr').item(0);
|
|
98
|
+
if (!rPr) {
|
|
99
|
+
rPr = doc.createElement('w:rPr');
|
|
100
|
+
if (run.firstChild) {
|
|
101
|
+
run.insertBefore(rPr, run.firstChild);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
run.appendChild(rPr);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Add bold if not already present
|
|
108
|
+
if (!rPr.getElementsByTagName('w:b').length) {
|
|
109
|
+
const b = doc.createElement('w:b');
|
|
110
|
+
rPr.appendChild(b);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
tc.appendChild(p);
|
|
115
|
+
}
|
|
66
116
|
}
|
|
67
|
-
const t = doc.createElement('w:t');
|
|
68
|
-
t.setAttribute('xml:space', 'preserve');
|
|
69
|
-
t.textContent = text;
|
|
70
|
-
r.appendChild(t);
|
|
71
|
-
p.appendChild(r);
|
|
72
|
-
tc.appendChild(p);
|
|
73
117
|
return tc;
|
|
74
118
|
};
|
|
75
119
|
// Header row
|
package/dist/tools/docx/dom.d.ts
CHANGED
|
@@ -23,6 +23,17 @@ export declare function getBody(doc: Document): Element;
|
|
|
23
23
|
* Includes w:p, w:tbl, w:sdt, w:sectPr, etc.
|
|
24
24
|
*/
|
|
25
25
|
export declare function getBodyChildren(body: Element): Element[];
|
|
26
|
+
/**
|
|
27
|
+
* Return ALL top‑level tables that are logically in the body, including those
|
|
28
|
+
* wrapped in structured document tags (w:sdt / w:sdtContent).
|
|
29
|
+
*
|
|
30
|
+
* Previous logic only saw tables that were direct children of <w:body>. That
|
|
31
|
+
* meant tables inside SDTs were invisible to table operations and readDocxOutline.
|
|
32
|
+
* This helper walks the body tree and collects any <w:tbl> that appears as a
|
|
33
|
+
* *first‑class* block (we do not recurse into tables themselves, so nested
|
|
34
|
+
* tables are not double‑counted).
|
|
35
|
+
*/
|
|
36
|
+
export declare function getAllBodyTables(body: Element): Element[];
|
|
26
37
|
/**
|
|
27
38
|
* Build a compact signature string from the body children array.
|
|
28
39
|
* Maps each node's qualified name to a short local name:
|
|
@@ -34,13 +45,75 @@ export declare function bodySignature(children: Element[]): string;
|
|
|
34
45
|
export declare function getParagraphText(p: Element): string;
|
|
35
46
|
/** Read the style id from w:pPr/w:pStyle/@w:val, or null if absent. */
|
|
36
47
|
export declare function getParagraphStyle(p: Element): string | null;
|
|
48
|
+
/**
|
|
49
|
+
* Extract all text content from a table cell (w:tc).
|
|
50
|
+
* Returns the concatenated text from all paragraphs in the cell.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getCellText(tc: Element): string;
|
|
53
|
+
/**
|
|
54
|
+
* Extract all rows from a table (w:tbl).
|
|
55
|
+
* Returns an array of rows, where each row is an array of cell text strings.
|
|
56
|
+
* First row is treated as header if it exists.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getTableContent(tbl: Element): {
|
|
59
|
+
headers?: string[];
|
|
60
|
+
rows: string[][];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Get table style from w:tblPr/w:tblStyle/@w:val, or null if absent.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getTableStyle(tbl: Element): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* Extract image reference from a w:drawing element.
|
|
68
|
+
* Returns the relationship ID (rId) and media file path if found.
|
|
69
|
+
*/
|
|
70
|
+
export declare function getImageReference(drawing: Element): {
|
|
71
|
+
rId: string | null;
|
|
72
|
+
mediaPath: string | null;
|
|
73
|
+
};
|
|
37
74
|
/**
|
|
38
75
|
* Replace the text of a paragraph with minimal DOM changes.
|
|
39
76
|
* Sets the FIRST w:t to `text`, clears every subsequent w:t.
|
|
40
77
|
* Sets xml:space="preserve" so leading/trailing spaces survive.
|
|
41
|
-
* Does NOT recreate runs or remove paragraph properties.
|
|
78
|
+
* Does NOT remove/recreate runs or remove paragraph properties.
|
|
79
|
+
*
|
|
80
|
+
* WARNING: This function does NOT preserve multiple runs with different styles.
|
|
81
|
+
* Use setParagraphTextPreservingStyles() for cells with multiple styled runs.
|
|
42
82
|
*/
|
|
43
83
|
export declare function setParagraphTextMinimal(p: Element, text: string): void;
|
|
84
|
+
/**
|
|
85
|
+
* Replace paragraph text while preserving all run styles.
|
|
86
|
+
*
|
|
87
|
+
* This function preserves the structure of all runs (w:r) and their
|
|
88
|
+
* properties (w:rPr), distributing the new text across existing runs.
|
|
89
|
+
*
|
|
90
|
+
* Strategy:
|
|
91
|
+
* 1. Collect all runs with their properties
|
|
92
|
+
* 2. Distribute new text across runs (preserving run count and styles)
|
|
93
|
+
* 3. If new text is longer, extend the last run
|
|
94
|
+
* 4. If new text is shorter, clear excess runs but keep their structure
|
|
95
|
+
*/
|
|
96
|
+
export declare function setParagraphTextPreservingStyles(p: Element, text: string): void;
|
|
97
|
+
/**
|
|
98
|
+
* Replace cell text while preserving ALL paragraphs and their styles.
|
|
99
|
+
*
|
|
100
|
+
* This function works at the cell level:
|
|
101
|
+
* - Preserves ALL paragraphs in the cell (doesn't remove any)
|
|
102
|
+
* - Updates text in the first paragraph while preserving its styles
|
|
103
|
+
* - Keeps all other paragraphs intact with their original text and styles
|
|
104
|
+
*
|
|
105
|
+
* This ensures that cells with multiple paragraphs (each with different
|
|
106
|
+
* styles, font sizes, etc.) maintain their structure after text replacement.
|
|
107
|
+
*
|
|
108
|
+
* Example: If a cell has:
|
|
109
|
+
* - Paragraph 1: "LAWN AND LANDSCAPE" (Heading1 style, large font, red color)
|
|
110
|
+
* - Paragraph 2: "Take your weekends back..." (Normal style, smaller font, black color)
|
|
111
|
+
*
|
|
112
|
+
* Replacing with "EARTH AND MOUNTAIN" will:
|
|
113
|
+
* - Update paragraph 1 to "EARTH AND MOUNTAIN" (preserving Heading1 style, large font, red color)
|
|
114
|
+
* - Keep paragraph 2 completely intact with its original text and style
|
|
115
|
+
*/
|
|
116
|
+
export declare function setCellTextPreservingStyles(tc: Element, text: string): void;
|
|
44
117
|
/**
|
|
45
118
|
* Ensure a <w:r> element has w:rPr/w:color[@w:val=hex].
|
|
46
119
|
* Creates w:rPr and w:color if they don't exist.
|