@wonderwhy-er/desktop-commander 0.2.40 β†’ 0.2.42

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 (35) hide show
  1. package/README.md +4 -4
  2. package/dist/handlers/filesystem-handlers.js +28 -3
  3. package/dist/server.d.ts +2 -1
  4. package/dist/server.js +50 -13
  5. package/dist/setup-claude-server.js +56 -50
  6. package/dist/terminal-manager.js +46 -0
  7. package/dist/tools/edit.js +7 -1
  8. package/dist/tools/filesystem.d.ts +5 -0
  9. package/dist/tools/filesystem.js +91 -14
  10. package/dist/tools/pdf/markdown.d.ts +13 -0
  11. package/dist/tools/pdf/markdown.js +93 -29
  12. package/dist/track-installation.js +57 -38
  13. package/dist/types.d.ts +4 -0
  14. package/dist/ui/contracts.d.ts +1 -1
  15. package/dist/ui/contracts.js +4 -1
  16. package/dist/ui/file-preview/preview-runtime.js +114 -116
  17. package/dist/ui/file-preview/src/app.js +19 -22
  18. package/dist/ui/file-preview/src/directory-controller.js +9 -2
  19. package/dist/ui/file-preview/src/file-type-handlers.js +20 -9
  20. package/dist/ui/file-preview/src/host/external-actions.d.ts +0 -11
  21. package/dist/ui/file-preview/src/host/external-actions.js +0 -39
  22. package/dist/ui/file-preview/src/payload-utils.js +10 -1
  23. package/dist/uninstall-claude-server.js +54 -47
  24. package/dist/utils/ab-test.d.ts +4 -0
  25. package/dist/utils/ab-test.js +6 -0
  26. package/dist/utils/capture.d.ts +10 -2
  27. package/dist/utils/capture.js +80 -54
  28. package/dist/utils/feature-flags.d.ts +3 -0
  29. package/dist/utils/feature-flags.js +34 -5
  30. package/dist/utils/files/excel.js +26 -5
  31. package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
  32. package/dist/utils/mcp-ui-ab-test.js +62 -0
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +2 -1
package/README.md CHANGED
@@ -17,8 +17,6 @@ Work with code and text, run processes, and automate tasks, going far beyond oth
17
17
  <img width="380" height="200" src="https://glama.ai/mcp/servers/zempur9oh4/badge" alt="Desktop Commander MCP" />
18
18
  </a>
19
19
 
20
- ## πŸ‘‹ We’re hiring β€” come build with us: https://desktopcommander.app/careers/
21
-
22
20
  ## πŸ–₯️ Try the Desktop Commander App (Beta)
23
21
 
24
22
  **Want a better experience?** The Desktop Commander App gives you everything the MCP server does, plus:
@@ -335,9 +333,11 @@ Desktop Commander works with any MCP-compatible client. The standard JSON config
335
333
  Add this to your client's MCP configuration file at the locations below:
336
334
 
337
335
  <details>
338
- <summary><b>Cursor</b></summary>
336
+ <summary><b>Cursor</b></summary><br>
337
+
338
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=desktop-commander&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB3b25kZXJ3aHktZXIvZGVza3RvcC1jb21tYW5kZXJAbGF0ZXN0Il19)
339
339
 
340
- [Install MCP Server in Cursor](https://cursor.directory/mcp/desktop-commander-mcp)
340
+ [View MCP Server in Directory](https://cursor.directory/mcp/desktop-commander-mcp)
341
341
 
342
342
  Or add manually to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in your project folder (project-specific).
343
343
 
@@ -1,4 +1,4 @@
1
- import { readFile, readMultipleFiles, writeFile, createDirectory, listDirectory, moveFile, getFileInfo, writePdf } from '../tools/filesystem.js';
1
+ import { readFile, readMultipleFiles, writeFile, createDirectory, listDirectory, moveFile, getFileInfo, writePdf, getDefaultEditorMetadata } from '../tools/filesystem.js';
2
2
  import { withTimeout } from '../utils/withTimeout.js';
3
3
  import { createErrorResponse } from '../error-handlers.js';
4
4
  import { configManager } from '../config-manager.js';
@@ -99,13 +99,20 @@ export async function handleReadFile(args) {
99
99
  fileName: path.basename(resolvedFilePath),
100
100
  filePath: resolvedFilePath,
101
101
  fileType: 'unsupported',
102
+ sourceTool: 'read_file',
103
+ ...await getDefaultEditorMetadata(resolvedFilePath),
104
+ content: pdfContent
105
+ .filter((item) => item.type === "text")
106
+ .map((item) => item.text)
107
+ .join("\n"),
102
108
  },
103
109
  };
104
110
  }
105
111
  // Handle image files
106
112
  if (fileResult.metadata?.isImage) {
107
- // For image files, keep content payload text-only for broad host compatibility.
108
- // The preview widget reads image bytes from structuredContent.
113
+ // Return the image bytes in the MCP content array so the host model can
114
+ // actually see the image. structuredContent additionally carries the bytes
115
+ // for the preview widget to render.
109
116
  const imageData = typeof fileResult.content === 'string'
110
117
  ? fileResult.content
111
118
  : fileResult.content.toString('base64');
@@ -115,12 +122,20 @@ export async function handleReadFile(args) {
115
122
  {
116
123
  type: "text",
117
124
  text: imageSummary
125
+ },
126
+ {
127
+ type: "image",
128
+ data: imageData,
129
+ mimeType: fileResult.mimeType
118
130
  }
119
131
  ],
120
132
  structuredContent: {
121
133
  fileName: path.basename(resolvedFilePath),
122
134
  filePath: resolvedFilePath,
123
135
  fileType: 'image',
136
+ sourceTool: 'read_file',
137
+ ...await getDefaultEditorMetadata(resolvedFilePath),
138
+ content: imageData,
124
139
  imageData,
125
140
  mimeType: fileResult.mimeType
126
141
  }
@@ -140,6 +155,9 @@ export async function handleReadFile(args) {
140
155
  fileName: path.basename(resolvedFilePath),
141
156
  filePath: resolvedFilePath,
142
157
  fileType,
158
+ sourceTool: 'read_file',
159
+ ...await getDefaultEditorMetadata(resolvedFilePath),
160
+ content: textContent,
143
161
  },
144
162
  };
145
163
  }
@@ -246,6 +264,8 @@ export async function handleWriteFile(args) {
246
264
  fileName: path.basename(resolvedWritePath),
247
265
  filePath: resolvedWritePath,
248
266
  fileType: resolvePreviewFileType(resolvedWritePath),
267
+ sourceTool: 'write_file',
268
+ ...await getDefaultEditorMetadata(resolvedWritePath),
249
269
  },
250
270
  };
251
271
  }
@@ -287,6 +307,11 @@ export async function handleListDirectory(args) {
287
307
  fileName: path.basename(resolvedPath),
288
308
  filePath: resolvedPath,
289
309
  fileType: 'directory',
310
+ sourceTool: 'list_directory',
311
+ // Carry the listing in structuredContent too. Chat reads the text
312
+ // content array, but structuredContent-only consumers (e.g. Cowork)
313
+ // render from here and would otherwise show an empty directory.
314
+ content: resultText,
290
315
  },
291
316
  };
292
317
  }
package/dist/server.d.ts CHANGED
@@ -38,4 +38,5 @@ declare let currentClient: {
38
38
  name: string;
39
39
  version: string;
40
40
  };
41
- export { currentClient };
41
+ declare let currentCallIsRemote: boolean;
42
+ export { currentClient, currentCallIsRemote };
package/dist/server.js CHANGED
@@ -21,8 +21,9 @@ import { handleWelcomePageOnboarding } from './utils/welcome-onboarding.js';
21
21
  import { VERSION } from './version.js';
22
22
  import { capture, capture_call_tool } from "./utils/capture.js";
23
23
  import { logToStderr, logger } from './utils/logger.js';
24
- import { buildUiToolMeta, CONFIG_EDITOR_RESOURCE_URI, FILE_PREVIEW_RESOURCE_URI } from './ui/contracts.js';
24
+ import { buildUiToolMeta, CONFIG_EDITOR_RESOURCE_URI, FILE_PREVIEW_RESOURCE_URI, } from './ui/contracts.js';
25
25
  import { listUiResources, readUiResource } from './ui/resources.js';
26
+ import { shouldShowMcpUiPreviews } from './utils/mcp-ui-ab-test.js';
26
27
  // Store startup messages to send after initialization
27
28
  const deferredMessages = [];
28
29
  function deferLog(level, message) {
@@ -70,6 +71,21 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
70
71
  });
71
72
  // Store current client info (simple variable)
72
73
  let currentClient = { name: 'uninitialized', version: 'uninitialized' };
74
+ // Tracks whether the in-flight tool call originated from a remote device.
75
+ // Mirrors the module-level `currentClient` pattern so that telemetry events
76
+ // emitted deeper inside tool handlers (e.g. server_start_process,
77
+ // server_read_file) can be attributed to the remote path. The CallTool
78
+ // handler sets this on every call (true when _meta.remote is present,
79
+ // false otherwise) so the flag never leaks from a remote call to a
80
+ // subsequent local call.
81
+ let currentCallIsRemote = false;
82
+ /**
83
+ * Set whether the current tool call is from a remote device.
84
+ * Called once per tool call by the CallTool handler.
85
+ */
86
+ function setCurrentCallIsRemote(isRemote) {
87
+ currentCallIsRemote = isRemote;
88
+ }
73
89
  /**
74
90
  * Unified way to update client information
75
91
  */
@@ -131,7 +147,7 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
131
147
  }
132
148
  });
133
149
  // Export current client info for access by other modules
134
- export { currentClient };
150
+ export { currentClient, currentCallIsRemote };
135
151
  deferLog('info', 'Setting up request handlers...');
136
152
  /**
137
153
  * Check if a tool should be included based on current client
@@ -150,6 +166,7 @@ function shouldIncludeTool(toolName) {
150
166
  server.setRequestHandler(ListToolsRequestSchema, async () => {
151
167
  try {
152
168
  // logToStderr('debug', 'Generating tools list...');
169
+ const showMcpUiPreviews = await shouldShowMcpUiPreviews();
153
170
  // Build complete tools array
154
171
  const allTools = [
155
172
  // Configuration tools
@@ -169,7 +186,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
169
186
  - systemInfo (operating system and environment details)
170
187
  ${CMD_PREFIX_DESCRIPTION}`,
171
188
  inputSchema: zodToJsonSchema(GetConfigArgsSchema),
172
- _meta: buildUiToolMeta(CONFIG_EDITOR_RESOURCE_URI, true),
189
+ _meta: buildUiToolMeta(CONFIG_EDITOR_RESOURCE_URI, true, showMcpUiPreviews),
173
190
  annotations: {
174
191
  title: "Get Configuration",
175
192
  readOnlyHint: true,
@@ -262,7 +279,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
262
279
  ${PATH_GUIDANCE}
263
280
  ${CMD_PREFIX_DESCRIPTION}`,
264
281
  inputSchema: zodToJsonSchema(ReadFileArgsSchema),
265
- _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true),
282
+ _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true, showMcpUiPreviews),
266
283
  annotations: {
267
284
  title: "Read File or URL",
268
285
  readOnlyHint: true,
@@ -330,7 +347,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
330
347
  ${PATH_GUIDANCE}
331
348
  ${CMD_PREFIX_DESCRIPTION}`,
332
349
  inputSchema: zodToJsonSchema(WriteFileArgsSchema),
333
- _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true),
350
+ _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true, showMcpUiPreviews),
334
351
  annotations: {
335
352
  title: "Write File",
336
353
  readOnlyHint: false,
@@ -445,12 +462,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
445
462
  [FILE] src/tools/filesystem.ts
446
463
 
447
464
  If a directory cannot be accessed, it will show [DENIED] instead.
465
+ If a path does not exist, it will show [NOT_FOUND] instead.
448
466
  Only works within allowed directories.
449
467
 
450
468
  ${PATH_GUIDANCE}
451
469
  ${CMD_PREFIX_DESCRIPTION}`,
452
470
  inputSchema: zodToJsonSchema(ListDirectoryArgsSchema),
453
- _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true),
471
+ _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true, showMcpUiPreviews),
454
472
  annotations: {
455
473
  title: "List Directory Contents",
456
474
  readOnlyHint: true,
@@ -709,7 +727,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
709
727
  ${PATH_GUIDANCE}
710
728
  ${CMD_PREFIX_DESCRIPTION}`,
711
729
  inputSchema: zodToJsonSchema(EditBlockArgsSchema),
712
- _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true),
730
+ _meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true, showMcpUiPreviews),
713
731
  annotations: {
714
732
  title: "Edit Block",
715
733
  readOnlyHint: false,
@@ -1090,13 +1108,21 @@ import * as handlers from './handlers/index.js';
1090
1108
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1091
1109
  const { name, arguments: args } = request.params;
1092
1110
  const startTime = Date.now();
1111
+ // Hoisted above the try so the finally block can read them when emitting the
1112
+ // server_call_tool completion event (duration + status), even on the crash path.
1113
+ let telemetryData = { tool_name: name };
1114
+ let result;
1115
+ let isError = false;
1093
1116
  try {
1094
- // Prepare telemetry data - add config key for set_config_value
1095
- const telemetryData = { tool_name: name };
1096
- // Extract metadata from _meta field if present
1117
+ // telemetryData declared above; extract metadata from _meta field if present
1097
1118
  const metadata = request.params._meta;
1119
+ // Reset remote attribution for every call so a prior remote call never
1120
+ // leaks its flag onto a subsequent local call. Set to true only when
1121
+ // this call carries the remote marker in _meta.
1122
+ const isRemoteCall = !!(metadata && typeof metadata === 'object' && metadata.remote);
1123
+ setCurrentCallIsRemote(isRemoteCall);
1098
1124
  if (metadata && typeof metadata === 'object') {
1099
- // add remote flag if present (convert to string for GA4)
1125
+ // add remote flag if present (convert to string for telemetry)
1100
1126
  if (metadata.remote) {
1101
1127
  telemetryData.remote = String(metadata.remote);
1102
1128
  }
@@ -1123,11 +1149,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1123
1149
  telemetryData.prompt_id = promptArgs.promptId;
1124
1150
  }
1125
1151
  }
1126
- capture_call_tool('server_call_tool', telemetryData);
1127
1152
  // Track tool call
1128
1153
  trackToolCall(name, args);
1129
1154
  // Using a more structured approach with dedicated handlers
1130
- let result;
1155
+ // (result is declared above so the finally block can read execution status)
1131
1156
  switch (name) {
1132
1157
  // Config tools
1133
1158
  case "get_config":
@@ -1319,6 +1344,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1319
1344
  }
1320
1345
  // Add tool call to history (exclude only get_recent_tool_calls to prevent recursion)
1321
1346
  const duration = Date.now() - startTime;
1347
+ isError = !!result.isError;
1322
1348
  const EXCLUDED_TOOLS = [
1323
1349
  'get_recent_tool_calls',
1324
1350
  'track_ui_event'
@@ -1412,6 +1438,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1412
1438
  return result;
1413
1439
  }
1414
1440
  catch (error) {
1441
+ isError = true;
1415
1442
  const errorMessage = error instanceof Error ? error.message : String(error);
1416
1443
  // Track the failure
1417
1444
  await usageTracker.trackFailure(name);
@@ -1423,6 +1450,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1423
1450
  isError: true,
1424
1451
  };
1425
1452
  }
1453
+ finally {
1454
+ // Single tool-call telemetry event, fired AFTER execution so it can carry
1455
+ // timing. In a finally so it still fires on the hard-crash path (the catch
1456
+ // above). Only missed if a tool never returns or throws (a true hang).
1457
+ capture_call_tool('server_call_tool', {
1458
+ ...telemetryData,
1459
+ duration_ms: Date.now() - startTime,
1460
+ is_error: String(isError),
1461
+ });
1462
+ }
1426
1463
  });
1427
1464
  // Add no-op handlers so Visual Studio initialization succeeds
1428
1465
  server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [] }));
@@ -10,10 +10,9 @@ import { version as nodeVersion } from 'process';
10
10
  import * as https from 'https';
11
11
  import { randomUUID } from 'crypto';
12
12
 
13
- // Google Analytics configuration
14
- const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L'; // Replace with your GA4 Measurement ID
15
- const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A'; // Replace with your GA4 API Secre
16
- const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
13
+ // Telemetry proxy configuration
14
+ const TELEMETRY_PROXY_URL = 'https://telemetry.desktopcommander.app/mp/collect';
15
+ const TELEMETRY_PROXY_FALLBACK_URL = 'https://dc-telemetry-proxy-83847352264.europe-west1.run.app/mp/collect';
17
16
 
18
17
  // Generate a unique anonymous ID using UUID - consistent with privacy policy
19
18
  let uniqueUserId = 'unknown';
@@ -302,11 +301,6 @@ async function enhancedGetTrackingProperties(additionalProps = {}) {
302
301
  async function trackEvent(eventName, additionalProps = {}) {
303
302
  const trackingStep = addSetupStep(`track_event_${eventName}`);
304
303
 
305
- if (!GA_MEASUREMENT_ID || !GA_API_SECRET) {
306
- updateSetupStep(trackingStep, 'skipped', new Error('GA not configured'));
307
- return;
308
- }
309
-
310
304
  // Add retry capability
311
305
  const maxRetries = 2;
312
306
  let attempt = 0;
@@ -319,7 +313,7 @@ async function trackEvent(eventName, additionalProps = {}) {
319
313
  // Get enriched properties
320
314
  const eventProperties = await enhancedGetTrackingProperties(additionalProps);
321
315
 
322
- // Prepare GA4 payload
316
+ // Prepare telemetry payload
323
317
  const payload = {
324
318
  client_id: uniqueUserId,
325
319
  non_personalized_ads: false,
@@ -330,7 +324,7 @@ async function trackEvent(eventName, additionalProps = {}) {
330
324
  }]
331
325
  };
332
326
 
333
- // Send to Google Analytics
327
+ // Send to telemetry proxy
334
328
  const postData = JSON.stringify(payload);
335
329
 
336
330
  const options = {
@@ -341,44 +335,8 @@ async function trackEvent(eventName, additionalProps = {}) {
341
335
  }
342
336
  };
343
337
 
344
- const result = await new Promise((resolve, reject) => {
345
- const req = https.request(GA_BASE_URL, options);
346
-
347
- // Set timeout to prevent blocking
348
- const timeoutId = setTimeout(() => {
349
- req.destroy();
350
- reject(new Error('Request timeout'));
351
- }, 5000); // Increased timeout to 5 seconds
352
-
353
- req.on('error', (error) => {
354
- clearTimeout(timeoutId);
355
- reject(error);
356
- });
357
-
358
- req.on('response', (res) => {
359
- clearTimeout(timeoutId);
360
- let data = '';
361
-
362
- res.on('data', (chunk) => {
363
- data += chunk;
364
- });
365
-
366
- res.on('error', (error) => {
367
- reject(error);
368
- });
369
-
370
- res.on('end', () => {
371
- if (res.statusCode >= 200 && res.statusCode < 300) {
372
- resolve({ success: true, data });
373
- } else {
374
- reject(new Error(`HTTP error ${res.statusCode}: ${data}`));
375
- }
376
- });
377
- });
378
-
379
- req.write(postData);
380
- req.end();
381
- });
338
+ const result = await postTelemetryPayload(postData, options);
339
+ if (!result.success) throw new Error('Telemetry proxy request failed');
382
340
 
383
341
  updateSetupStep(trackingStep, 'completed');
384
342
  return result;
@@ -397,6 +355,54 @@ async function trackEvent(eventName, additionalProps = {}) {
397
355
  return false;
398
356
  }
399
357
 
358
+ async function postTelemetryPayload(postData, options) {
359
+ for (const endpoint of [TELEMETRY_PROXY_URL, TELEMETRY_PROXY_FALLBACK_URL]) {
360
+ const result = await new Promise((resolve) => {
361
+ let settled = false;
362
+ let timeoutId;
363
+ const finish = (result) => {
364
+ if (settled) return;
365
+ settled = true;
366
+ clearTimeout(timeoutId);
367
+ resolve(result);
368
+ };
369
+ const req = https.request(endpoint, options);
370
+
371
+ timeoutId = setTimeout(() => {
372
+ req.destroy();
373
+ finish({ success: false, data: '' });
374
+ }, 5000);
375
+
376
+ req.on('error', () => {
377
+ finish({ success: false, data: '' });
378
+ });
379
+
380
+ req.on('response', (res) => {
381
+ let data = '';
382
+ res.on('data', (chunk) => {
383
+ data += chunk;
384
+ });
385
+ res.on('error', () => {
386
+ finish({ success: false, data: '' });
387
+ });
388
+ res.on('end', () => {
389
+ finish({ success: res.statusCode >= 200 && res.statusCode < 300, data });
390
+ });
391
+ res.on('close', () => {
392
+ finish({ success: false, data: '' });
393
+ });
394
+ });
395
+
396
+ req.write(postData);
397
+ req.end();
398
+ });
399
+
400
+ if (result.success) return result;
401
+ }
402
+
403
+ return { success: false, data: '' };
404
+ }
405
+
400
406
  // Ensure tracking completes before process exits
401
407
  async function ensureTrackingCompleted(eventName, additionalProps = {}, timeoutMs = 6000) {
402
408
  return new Promise(async (resolve) => {
@@ -933,4 +939,4 @@ if (process.argv.length >= 2 && process.argv[1] === fileURLToPath(import.meta.ur
933
939
  process.exit(1);
934
940
  }, 1000);
935
941
  });
936
- }
942
+ }
@@ -4,6 +4,34 @@ import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
4
4
  import { configManager } from './config-manager.js';
5
5
  import { capture } from "./utils/capture.js";
6
6
  import { analyzeProcessState } from './utils/process-detection.js';
7
+ /**
8
+ * Standard Windows PATHEXT value, used to repair a corrupted PATHEXT before
9
+ * spawning child shells.
10
+ *
11
+ * On some Windows Claude Desktop / DXT launches the server process inherits a
12
+ * broken PATHEXT (observed as ".CPL" only). Because we build the child env from
13
+ * { ...process.env }, that broken value would propagate into every spawned
14
+ * shell, stripping ".EXE" and breaking resolution of git / node / python / rg /
15
+ * etc. (and even full-path .exe invocations under PowerShell). See issue #481.
16
+ */
17
+ const STANDARD_PATHEXT = '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC';
18
+ /**
19
+ * Return a healthy PATHEXT for spawned Windows shells.
20
+ * - Unset -> use the standard list.
21
+ * - Missing ".EXE" -> corrupted; merge the standard list with whatever was
22
+ * present (preserves any extra extensions, order-stable).
23
+ * - Otherwise -> leave the inherited value untouched.
24
+ */
25
+ function getRepairedPathExt() {
26
+ const current = process.env.PATHEXT;
27
+ if (!current)
28
+ return STANDARD_PATHEXT;
29
+ const exts = current.split(';').map(e => e.trim().toUpperCase()).filter(Boolean);
30
+ if (!exts.includes('.EXE')) {
31
+ return [...new Set([...STANDARD_PATHEXT.split(';'), ...exts])].join(';');
32
+ }
33
+ return current;
34
+ }
7
35
  /**
8
36
  * Get the appropriate spawn configuration for a given shell
9
37
  * This handles login shell flags for different shell types
@@ -39,6 +67,7 @@ function getShellSpawnArgs(shellPath, command) {
39
67
  return {
40
68
  executable: shellPath,
41
69
  args: ['/c', command],
70
+ windowsVerbatim: true,
42
71
  useShellOption: false
43
72
  };
44
73
  }
@@ -143,6 +172,23 @@ export class TerminalManager {
143
172
  windowsHide: true // Prevent visible console windows on Windows
144
173
  };
145
174
  }
175
+ // Repair PATHEXT on Windows before spawning. On some Windows DXT launches
176
+ // the server process inherits a corrupted PATHEXT (e.g. ".CPL"), which we
177
+ // would otherwise propagate via { ...process.env } and break command
178
+ // resolution (git, node, python, rg, ...) in the spawned shell. See #481.
179
+ if (process.platform === 'win32' && spawnOptions.env) {
180
+ spawnOptions.env.PATHEXT = getRepairedPathExt();
181
+ }
182
+ // On Windows, when we invoke cmd.exe directly and pass the user's command as a
183
+ // single argument, Node/libuv applies MSVCRT-style quoting that escapes embedded
184
+ // double quotes as \" . cmd.exe does not understand that escaping, so any command
185
+ // containing quotes (e.g. a quoted path with spaces like "C:\Program Files\app.exe")
186
+ // is corrupted before the shell ever parses it. Passing arguments verbatim lets
187
+ // cmd handle its own quoting. Scoped to shells that set windowsVerbatim (cmd only)
188
+ // because PowerShell/pwsh have different quote rules and must NOT use verbatim.
189
+ if (process.platform === 'win32' && spawnConfig.windowsVerbatim) {
190
+ spawnOptions.windowsVerbatimArguments = true;
191
+ }
146
192
  // Spawn the process with appropriate arguments
147
193
  const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions);
148
194
  let output = '';
@@ -14,7 +14,7 @@
14
14
  * 2. Make this file a thin dispatch layer that routes to appropriate FileHandler
15
15
  * 3. Unify the editRange() signature to handle both text search/replace and structured edits
16
16
  */
17
- import { writeFile, readFileInternal, validatePath } from './filesystem.js';
17
+ import { getDefaultEditorMetadata, writeFile, readFileInternal, validatePath } from './filesystem.js';
18
18
  import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js';
19
19
  import { capture } from '../utils/capture.js';
20
20
  import { createErrorResponse } from '../error-handlers.js';
@@ -180,6 +180,8 @@ RECOMMENDATION: For large search/replace operations, consider breaking them into
180
180
  fileName: path.basename(resolvedEditPath),
181
181
  filePath: resolvedEditPath,
182
182
  fileType: resolvePreviewFileType(resolvedEditPath),
183
+ sourceTool: 'edit_block',
184
+ ...await getDefaultEditorMetadata(resolvedEditPath),
183
185
  },
184
186
  };
185
187
  }
@@ -368,6 +370,8 @@ export async function handleEditBlock(args) {
368
370
  fileName: path.basename(resolvedRangePath),
369
371
  filePath: resolvedRangePath,
370
372
  fileType: resolvePreviewFileType(resolvedRangePath),
373
+ sourceTool: 'edit_block',
374
+ ...await getDefaultEditorMetadata(resolvedRangePath),
371
375
  },
372
376
  };
373
377
  }
@@ -403,6 +407,8 @@ export async function handleEditBlock(args) {
403
407
  fileName: path.basename(resolvedEditRangePath),
404
408
  filePath: resolvedEditRangePath,
405
409
  fileType: resolvePreviewFileType(resolvedEditRangePath),
410
+ sourceTool: 'edit_block',
411
+ ...await getDefaultEditorMetadata(resolvedEditRangePath),
406
412
  },
407
413
  };
408
414
  }
@@ -71,3 +71,8 @@ export declare function getFileInfo(filePath: string): Promise<Record<string, an
71
71
  * @param options Options for PDF generation or modification. For modification, can include `sourcePdf`.
72
72
  */
73
73
  export declare function writePdf(filePath: string, content: string | PdfOperations[], outputPath?: string, options?: any): Promise<void>;
74
+ type DefaultEditorMetadata = {
75
+ defaultEditorName?: string;
76
+ defaultEditorPath?: string;
77
+ };
78
+ export declare function getDefaultEditorMetadata(filePath: string): Promise<DefaultEditorMetadata>;