@wonderwhy-er/desktop-commander 0.2.35 → 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.
Files changed (132) hide show
  1. package/README.md +3 -0
  2. package/dist/handlers/filesystem-handlers.js +58 -11
  3. package/dist/handlers/history-handlers.d.ts +7 -0
  4. package/dist/handlers/history-handlers.js +33 -1
  5. package/dist/remote-device/remote-channel.d.ts +8 -3
  6. package/dist/remote-device/remote-channel.js +68 -21
  7. package/dist/search-manager.d.ts +13 -0
  8. package/dist/search-manager.js +146 -0
  9. package/dist/server.js +56 -4
  10. package/dist/test-docx.d.ts +1 -0
  11. package/dist/tools/docx/builders/image.d.ts +14 -0
  12. package/dist/tools/docx/builders/image.js +84 -0
  13. package/dist/tools/docx/builders/index.d.ts +9 -3
  14. package/dist/tools/docx/builders/index.js +9 -3
  15. package/dist/tools/docx/builders/paragraph.d.ts +12 -0
  16. package/dist/tools/docx/builders/paragraph.js +29 -0
  17. package/dist/tools/docx/builders/table.d.ts +10 -0
  18. package/dist/tools/docx/builders/table.js +138 -0
  19. package/dist/tools/docx/builders/utils.d.ts +5 -0
  20. package/dist/tools/docx/builders/utils.js +18 -0
  21. package/dist/tools/docx/constants.d.ts +28 -32
  22. package/dist/tools/docx/constants.js +56 -52
  23. package/dist/tools/docx/create.d.ts +21 -0
  24. package/dist/tools/docx/create.js +386 -0
  25. package/dist/tools/docx/dom.d.ts +139 -0
  26. package/dist/tools/docx/dom.js +448 -0
  27. package/dist/tools/docx/index.d.ts +8 -12
  28. package/dist/tools/docx/index.js +8 -14
  29. package/dist/tools/docx/modify.d.ts +28 -0
  30. package/dist/tools/docx/modify.js +271 -0
  31. package/dist/tools/docx/ops/delete-paragraph-at-body-index.d.ts +11 -0
  32. package/dist/tools/docx/ops/delete-paragraph-at-body-index.js +23 -0
  33. package/dist/tools/docx/ops/header-replace-text-exact.d.ts +13 -0
  34. package/dist/tools/docx/ops/header-replace-text-exact.js +55 -0
  35. package/dist/tools/docx/ops/index.d.ts +17 -0
  36. package/dist/tools/docx/ops/index.js +70 -0
  37. package/dist/tools/docx/ops/insert-image-after-text.d.ts +24 -0
  38. package/dist/tools/docx/ops/insert-image-after-text.js +128 -0
  39. package/dist/tools/docx/ops/insert-paragraph-after-text.d.ts +12 -0
  40. package/dist/tools/docx/ops/insert-paragraph-after-text.js +74 -0
  41. package/dist/tools/docx/ops/insert-table-after-text.d.ts +19 -0
  42. package/dist/tools/docx/ops/insert-table-after-text.js +57 -0
  43. package/dist/tools/docx/ops/replace-hyperlink-url.d.ts +12 -0
  44. package/dist/tools/docx/ops/replace-hyperlink-url.js +37 -0
  45. package/dist/tools/docx/ops/replace-paragraph-at-body-index.d.ts +9 -0
  46. package/dist/tools/docx/ops/replace-paragraph-at-body-index.js +25 -0
  47. package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +21 -0
  48. package/dist/tools/docx/ops/replace-paragraph-text-exact.js +36 -0
  49. package/dist/tools/docx/ops/replace-table-cell-text.d.ts +25 -0
  50. package/dist/tools/docx/ops/replace-table-cell-text.js +85 -0
  51. package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +9 -0
  52. package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +24 -0
  53. package/dist/tools/docx/ops/set-color-for-style.d.ts +13 -0
  54. package/dist/tools/docx/ops/set-color-for-style.js +31 -0
  55. package/dist/tools/docx/ops/set-paragraph-style-at-body-index.d.ts +8 -0
  56. package/dist/tools/docx/ops/set-paragraph-style-at-body-index.js +57 -0
  57. package/dist/tools/docx/ops/table-set-cell-text.d.ts +9 -0
  58. package/dist/tools/docx/ops/table-set-cell-text.js +40 -0
  59. package/dist/tools/docx/read.d.ts +27 -0
  60. package/dist/tools/docx/read.js +308 -0
  61. package/dist/tools/docx/relationships.d.ts +22 -0
  62. package/dist/tools/docx/relationships.js +76 -0
  63. package/dist/tools/docx/types.d.ts +202 -103
  64. package/dist/tools/docx/types.js +2 -5
  65. package/dist/tools/docx/validate.d.ts +33 -0
  66. package/dist/tools/docx/validate.js +49 -0
  67. package/dist/tools/docx/write.d.ts +17 -0
  68. package/dist/tools/docx/write.js +88 -0
  69. package/dist/tools/docx/xml-view-test.d.ts +1 -0
  70. package/dist/tools/docx/xml-view-test.js +63 -0
  71. package/dist/tools/docx/xml-view.d.ts +56 -0
  72. package/dist/tools/docx/xml-view.js +169 -0
  73. package/dist/tools/docx/zip.d.ts +21 -0
  74. package/dist/tools/docx/zip.js +35 -0
  75. package/dist/tools/edit.js +57 -27
  76. package/dist/tools/schemas.d.ts +13 -0
  77. package/dist/tools/schemas.js +6 -1
  78. package/dist/types.d.ts +10 -0
  79. package/dist/ui/contracts.d.ts +14 -0
  80. package/dist/ui/contracts.js +18 -0
  81. package/dist/ui/file-preview/index.html +16 -0
  82. package/dist/ui/file-preview/preview-runtime.js +13983 -0
  83. package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
  84. package/dist/ui/file-preview/shared/preview-file-types.js +57 -0
  85. package/dist/ui/file-preview/src/app.d.ts +4 -0
  86. package/dist/ui/file-preview/src/app.js +800 -0
  87. package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
  88. package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
  89. package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
  90. package/dist/ui/file-preview/src/components/highlighting.js +54 -0
  91. package/dist/ui/file-preview/src/components/html-renderer.d.ts +9 -0
  92. package/dist/ui/file-preview/src/components/html-renderer.js +63 -0
  93. package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
  94. package/dist/ui/file-preview/src/components/markdown-renderer.js +21 -0
  95. package/dist/ui/file-preview/src/components/toolbar.d.ts +6 -0
  96. package/dist/ui/file-preview/src/components/toolbar.js +75 -0
  97. package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
  98. package/dist/ui/file-preview/src/image-preview.js +21 -0
  99. package/dist/ui/file-preview/src/main.d.ts +1 -0
  100. package/dist/ui/file-preview/src/main.js +5 -0
  101. package/dist/ui/file-preview/src/types.d.ts +1 -0
  102. package/dist/ui/file-preview/src/types.js +1 -0
  103. package/dist/ui/file-preview/styles.css +764 -0
  104. package/dist/ui/resources.d.ts +21 -0
  105. package/dist/ui/resources.js +72 -0
  106. package/dist/ui/shared/escape-html.d.ts +4 -0
  107. package/dist/ui/shared/escape-html.js +11 -0
  108. package/dist/ui/shared/host-lifecycle.d.ts +16 -0
  109. package/dist/ui/shared/host-lifecycle.js +35 -0
  110. package/dist/ui/shared/rpc-client.d.ts +14 -0
  111. package/dist/ui/shared/rpc-client.js +72 -0
  112. package/dist/ui/shared/theme-adaptation.d.ts +10 -0
  113. package/dist/ui/shared/theme-adaptation.js +118 -0
  114. package/dist/ui/shared/tool-header.d.ts +9 -0
  115. package/dist/ui/shared/tool-header.js +25 -0
  116. package/dist/ui/shared/tool-shell.d.ts +16 -0
  117. package/dist/ui/shared/tool-shell.js +65 -0
  118. package/dist/ui/shared/widget-state.d.ts +28 -0
  119. package/dist/ui/shared/widget-state.js +60 -0
  120. package/dist/utils/capture.d.ts +1 -0
  121. package/dist/utils/capture.js +176 -8
  122. package/dist/utils/files/base.d.ts +3 -1
  123. package/dist/utils/files/docx.d.ts +28 -22
  124. package/dist/utils/files/docx.js +630 -196
  125. package/dist/utils/files/factory.d.ts +6 -5
  126. package/dist/utils/files/factory.js +18 -6
  127. package/dist/utils/files/text.js +9 -1
  128. package/dist/utils/system-info.js +1 -1
  129. package/dist/utils/usageTracker.js +5 -0
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/package.json +6 -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
  [![npm downloads](https://img.shields.io/npm/dw/@wonderwhy-er/desktop-commander)](https://www.npmjs.com/package/@wonderwhy-er/desktop-commander)
5
+ [![AgentAudit Verified](https://agentaudit.dev/api/badge/desktop-commander)](https://agentaudit.dev/skills/desktop-commander)
5
6
  [![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/wonderwhy-er/DesktopCommanderMCP)](https://archestra.ai/mcp-catalog/wonderwhy-er__desktopcommandermcp)
6
7
  [![smithery badge](https://smithery.ai/badge/@wonderwhy-er/desktop-commander)](https://smithery.ai/server/@wonderwhy-er/desktop-commander)
7
8
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow.svg)](https://www.buymeacoffee.com/wonderwhyer)
@@ -16,6 +17,8 @@ Work with code and text, run processes, and automate tasks, going far beyond oth
16
17
  <img width="380" height="200" src="https://glama.ai/mcp/servers/zempur9oh4/badge" alt="Desktop Commander MCP" />
17
18
  </a>
18
19
 
20
+ ## 👋 We’re hiring — come build with us: https://desktopcommander.app/careers/
21
+
19
22
  ## Table of Contents
20
23
  - [Features](#features)
21
24
  - [How to install](#how-to-install)
@@ -3,11 +3,34 @@ import { withTimeout } from '../utils/withTimeout.js';
3
3
  import { createErrorResponse } from '../error-handlers.js';
4
4
  import { configManager } from '../config-manager.js';
5
5
  import { ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, GetFileInfoArgsSchema, WritePdfArgsSchema } from '../tools/schemas.js';
6
+ import path from 'path';
7
+ import os from 'os';
8
+ import { buildUiToolMeta, FILE_PREVIEW_RESOURCE_URI } from '../ui/contracts.js';
9
+ import { resolvePreviewFileType } from '../ui/file-preview/shared/preview-file-types.js';
10
+ /**
11
+ * Expand home directory (~) in a file path
12
+ */
13
+ function expandHome(filePath) {
14
+ if (filePath === '~' || filePath.startsWith('~/') || filePath.startsWith(`~${path.sep}`)) {
15
+ return path.join(os.homedir(), filePath.slice(1));
16
+ }
17
+ return filePath;
18
+ }
19
+ /**
20
+ * Resolve a file path to an absolute path for use in structured content.
21
+ * This ensures "Open in folder" always has a valid absolute path.
22
+ */
23
+ function resolveAbsolutePath(filePath) {
24
+ const expanded = expandHome(filePath);
25
+ return path.isAbsolute(expanded)
26
+ ? path.resolve(expanded)
27
+ : path.resolve(process.cwd(), expanded);
28
+ }
6
29
  /**
7
30
  * Helper function to check if path contains an error
8
31
  */
9
- function isErrorPath(path) {
10
- return path.startsWith('__ERROR__:');
32
+ function isErrorPath(filePath) {
33
+ return filePath.startsWith('__ERROR__:');
11
34
  }
12
35
  /**
13
36
  * Extract error message from error path
@@ -44,6 +67,10 @@ export async function handleReadFile(args) {
44
67
  sheet: sheetParam,
45
68
  range: parsed.range
46
69
  };
70
+ // Resolve to absolute path for local files (not URLs) so "Open in folder" works
71
+ const resolvedFilePath = parsed.isUrl
72
+ ? parsed.path
73
+ : resolveAbsolutePath(parsed.path);
47
74
  const fileResult = await readFile(parsed.path, options);
48
75
  // Handle PDF files
49
76
  if (fileResult.metadata?.isPdf) {
@@ -68,28 +95,40 @@ export async function handleReadFile(args) {
68
95
  text: `PDF file: ${parsed.path}${author}${title} (${meta?.totalPages} pages) \n`
69
96
  },
70
97
  ...pdfContent
71
- ]
98
+ ],
99
+ structuredContent: {
100
+ fileName: path.basename(resolvedFilePath),
101
+ filePath: resolvedFilePath,
102
+ fileType: 'unsupported',
103
+ content: ''
104
+ },
105
+ _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true)
72
106
  };
73
107
  }
74
108
  // Handle image files
75
109
  if (fileResult.metadata?.isImage) {
76
- // For image files, return as an image content type
77
- // Content should already be base64-encoded string from handler
110
+ // For image files, keep content payload text-only for broad host compatibility.
111
+ // The preview widget reads image bytes from structuredContent.
78
112
  const imageData = typeof fileResult.content === 'string'
79
113
  ? fileResult.content
80
114
  : fileResult.content.toString('base64');
115
+ const imageSummary = `Image file: ${parsed.path} (${fileResult.mimeType})\n`;
81
116
  return {
82
117
  content: [
83
118
  {
84
119
  type: "text",
85
- text: `Image file: ${parsed.path} (${fileResult.mimeType})\n`
86
- },
87
- {
88
- type: "image",
89
- data: imageData,
90
- mimeType: fileResult.mimeType
120
+ text: imageSummary
91
121
  }
92
122
  ],
123
+ structuredContent: {
124
+ fileName: path.basename(resolvedFilePath),
125
+ filePath: resolvedFilePath,
126
+ fileType: 'image',
127
+ content: imageSummary,
128
+ imageData,
129
+ mimeType: fileResult.mimeType
130
+ },
131
+ _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true)
93
132
  };
94
133
  }
95
134
  else {
@@ -97,8 +136,16 @@ export async function handleReadFile(args) {
97
136
  const textContent = typeof fileResult.content === 'string'
98
137
  ? fileResult.content
99
138
  : fileResult.content.toString('utf8');
139
+ const previewFileType = resolvePreviewFileType(resolvedFilePath);
100
140
  return {
101
141
  content: [{ type: "text", text: textContent }],
142
+ structuredContent: {
143
+ fileName: path.basename(resolvedFilePath),
144
+ filePath: resolvedFilePath,
145
+ fileType: previewFileType,
146
+ content: textContent
147
+ },
148
+ _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true)
102
149
  };
103
150
  }
104
151
  };
@@ -1,5 +1,12 @@
1
1
  import { ServerResult } from '../types.js';
2
+ type TrackUiEventParams = Record<string, string | number | boolean | null>;
3
+ export declare function buildTrackUiEventCapturePayload(event: string, component: string, params: TrackUiEventParams): Record<string, string | number | boolean | null>;
2
4
  /**
3
5
  * Handle get_recent_tool_calls command
4
6
  */
5
7
  export declare function handleGetRecentToolCalls(args: unknown): Promise<ServerResult>;
8
+ /**
9
+ * Handle track_ui_event command
10
+ */
11
+ export declare function handleTrackUiEvent(args: unknown): Promise<ServerResult>;
12
+ export {};
@@ -1,5 +1,13 @@
1
1
  import { toolHistory } from '../utils/toolHistory.js';
2
- import { GetRecentToolCallsArgsSchema } from '../tools/schemas.js';
2
+ import { GetRecentToolCallsArgsSchema, TrackUiEventArgsSchema } from '../tools/schemas.js';
3
+ import { capture_ui_event } from '../utils/capture.js';
4
+ export function buildTrackUiEventCapturePayload(event, component, params) {
5
+ return {
6
+ ...params,
7
+ component,
8
+ event
9
+ };
10
+ }
3
11
  /**
4
12
  * Handle get_recent_tool_calls command
5
13
  */
@@ -33,3 +41,27 @@ export async function handleGetRecentToolCalls(args) {
33
41
  };
34
42
  }
35
43
  }
44
+ /**
45
+ * Handle track_ui_event command
46
+ */
47
+ export async function handleTrackUiEvent(args) {
48
+ try {
49
+ const parsed = TrackUiEventArgsSchema.parse(args);
50
+ await capture_ui_event('mcp_ui_event', buildTrackUiEventCapturePayload(parsed.event, parsed.component, parsed.params));
51
+ return {
52
+ content: [{
53
+ type: "text",
54
+ text: `Tracked UI event: ${parsed.event}`
55
+ }]
56
+ };
57
+ }
58
+ catch (error) {
59
+ return {
60
+ content: [{
61
+ type: "text",
62
+ text: `Error tracking UI event: ${error instanceof Error ? error.message : String(error)}`
63
+ }],
64
+ isError: true
65
+ };
66
+ }
67
+ }
@@ -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<import("@supabase/postgrest-js").PostgrestSingleResponse<null>>;
40
- createDevice(deviceData: DeviceData): Promise<import("@supabase/postgrest-js").PostgrestSingleResponse<any>>;
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.debug('[DEBUG] Failed to get user:', userError.message);
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?.email);
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
- return await this.client
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
- return await this.client
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
- await this.createChannel();
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
- await this.client
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) {
@@ -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
  */
@@ -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
  */