@wonderwhy-er/desktop-commander 0.2.41 β 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.
- package/README.md +0 -2
- package/dist/handlers/filesystem-handlers.js +22 -3
- package/dist/server.d.ts +2 -1
- package/dist/server.js +49 -13
- package/dist/setup-claude-server.js +56 -50
- package/dist/terminal-manager.js +46 -0
- package/dist/tools/edit.js +7 -1
- package/dist/tools/filesystem.d.ts +5 -0
- package/dist/tools/filesystem.js +43 -0
- package/dist/tools/pdf/markdown.d.ts +13 -0
- package/dist/tools/pdf/markdown.js +93 -29
- package/dist/track-installation.js +57 -38
- package/dist/types.d.ts +3 -0
- package/dist/ui/contracts.d.ts +1 -1
- package/dist/ui/contracts.js +4 -1
- package/dist/ui/file-preview/preview-runtime.js +111 -113
- package/dist/ui/file-preview/src/app.js +19 -17
- package/dist/ui/file-preview/src/host/external-actions.d.ts +0 -11
- package/dist/ui/file-preview/src/host/external-actions.js +0 -39
- package/dist/uninstall-claude-server.js +54 -47
- package/dist/utils/ab-test.d.ts +4 -0
- package/dist/utils/ab-test.js +6 -0
- package/dist/utils/capture.d.ts +10 -2
- package/dist/utils/capture.js +80 -54
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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:
|
|
@@ -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,6 +99,8 @@ 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),
|
|
102
104
|
content: pdfContent
|
|
103
105
|
.filter((item) => item.type === "text")
|
|
104
106
|
.map((item) => item.text)
|
|
@@ -108,8 +110,9 @@ export async function handleReadFile(args) {
|
|
|
108
110
|
}
|
|
109
111
|
// Handle image files
|
|
110
112
|
if (fileResult.metadata?.isImage) {
|
|
111
|
-
//
|
|
112
|
-
//
|
|
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.
|
|
113
116
|
const imageData = typeof fileResult.content === 'string'
|
|
114
117
|
? fileResult.content
|
|
115
118
|
: fileResult.content.toString('base64');
|
|
@@ -119,12 +122,19 @@ export async function handleReadFile(args) {
|
|
|
119
122
|
{
|
|
120
123
|
type: "text",
|
|
121
124
|
text: imageSummary
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: "image",
|
|
128
|
+
data: imageData,
|
|
129
|
+
mimeType: fileResult.mimeType
|
|
122
130
|
}
|
|
123
131
|
],
|
|
124
132
|
structuredContent: {
|
|
125
133
|
fileName: path.basename(resolvedFilePath),
|
|
126
134
|
filePath: resolvedFilePath,
|
|
127
135
|
fileType: 'image',
|
|
136
|
+
sourceTool: 'read_file',
|
|
137
|
+
...await getDefaultEditorMetadata(resolvedFilePath),
|
|
128
138
|
content: imageData,
|
|
129
139
|
imageData,
|
|
130
140
|
mimeType: fileResult.mimeType
|
|
@@ -145,6 +155,8 @@ export async function handleReadFile(args) {
|
|
|
145
155
|
fileName: path.basename(resolvedFilePath),
|
|
146
156
|
filePath: resolvedFilePath,
|
|
147
157
|
fileType,
|
|
158
|
+
sourceTool: 'read_file',
|
|
159
|
+
...await getDefaultEditorMetadata(resolvedFilePath),
|
|
148
160
|
content: textContent,
|
|
149
161
|
},
|
|
150
162
|
};
|
|
@@ -252,6 +264,8 @@ export async function handleWriteFile(args) {
|
|
|
252
264
|
fileName: path.basename(resolvedWritePath),
|
|
253
265
|
filePath: resolvedWritePath,
|
|
254
266
|
fileType: resolvePreviewFileType(resolvedWritePath),
|
|
267
|
+
sourceTool: 'write_file',
|
|
268
|
+
...await getDefaultEditorMetadata(resolvedWritePath),
|
|
255
269
|
},
|
|
256
270
|
};
|
|
257
271
|
}
|
|
@@ -293,6 +307,11 @@ export async function handleListDirectory(args) {
|
|
|
293
307
|
fileName: path.basename(resolvedPath),
|
|
294
308
|
filePath: resolvedPath,
|
|
295
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,
|
|
296
315
|
},
|
|
297
316
|
};
|
|
298
317
|
}
|
package/dist/server.d.ts
CHANGED
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,
|
|
@@ -451,7 +468,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
451
468
|
${PATH_GUIDANCE}
|
|
452
469
|
${CMD_PREFIX_DESCRIPTION}`,
|
|
453
470
|
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema),
|
|
454
|
-
_meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true),
|
|
471
|
+
_meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true, showMcpUiPreviews),
|
|
455
472
|
annotations: {
|
|
456
473
|
title: "List Directory Contents",
|
|
457
474
|
readOnlyHint: true,
|
|
@@ -710,7 +727,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
710
727
|
${PATH_GUIDANCE}
|
|
711
728
|
${CMD_PREFIX_DESCRIPTION}`,
|
|
712
729
|
inputSchema: zodToJsonSchema(EditBlockArgsSchema),
|
|
713
|
-
_meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true),
|
|
730
|
+
_meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true, showMcpUiPreviews),
|
|
714
731
|
annotations: {
|
|
715
732
|
title: "Edit Block",
|
|
716
733
|
readOnlyHint: false,
|
|
@@ -1091,13 +1108,21 @@ import * as handlers from './handlers/index.js';
|
|
|
1091
1108
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1092
1109
|
const { name, arguments: args } = request.params;
|
|
1093
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;
|
|
1094
1116
|
try {
|
|
1095
|
-
//
|
|
1096
|
-
const telemetryData = { tool_name: name };
|
|
1097
|
-
// Extract metadata from _meta field if present
|
|
1117
|
+
// telemetryData declared above; extract metadata from _meta field if present
|
|
1098
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);
|
|
1099
1124
|
if (metadata && typeof metadata === 'object') {
|
|
1100
|
-
// add remote flag if present (convert to string for
|
|
1125
|
+
// add remote flag if present (convert to string for telemetry)
|
|
1101
1126
|
if (metadata.remote) {
|
|
1102
1127
|
telemetryData.remote = String(metadata.remote);
|
|
1103
1128
|
}
|
|
@@ -1124,11 +1149,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1124
1149
|
telemetryData.prompt_id = promptArgs.promptId;
|
|
1125
1150
|
}
|
|
1126
1151
|
}
|
|
1127
|
-
capture_call_tool('server_call_tool', telemetryData);
|
|
1128
1152
|
// Track tool call
|
|
1129
1153
|
trackToolCall(name, args);
|
|
1130
1154
|
// Using a more structured approach with dedicated handlers
|
|
1131
|
-
|
|
1155
|
+
// (result is declared above so the finally block can read execution status)
|
|
1132
1156
|
switch (name) {
|
|
1133
1157
|
// Config tools
|
|
1134
1158
|
case "get_config":
|
|
@@ -1320,6 +1344,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1320
1344
|
}
|
|
1321
1345
|
// Add tool call to history (exclude only get_recent_tool_calls to prevent recursion)
|
|
1322
1346
|
const duration = Date.now() - startTime;
|
|
1347
|
+
isError = !!result.isError;
|
|
1323
1348
|
const EXCLUDED_TOOLS = [
|
|
1324
1349
|
'get_recent_tool_calls',
|
|
1325
1350
|
'track_ui_event'
|
|
@@ -1413,6 +1438,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1413
1438
|
return result;
|
|
1414
1439
|
}
|
|
1415
1440
|
catch (error) {
|
|
1441
|
+
isError = true;
|
|
1416
1442
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1417
1443
|
// Track the failure
|
|
1418
1444
|
await usageTracker.trackFailure(name);
|
|
@@ -1424,6 +1450,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1424
1450
|
isError: true,
|
|
1425
1451
|
};
|
|
1426
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
|
+
}
|
|
1427
1463
|
});
|
|
1428
1464
|
// Add no-op handlers so Visual Studio initialization succeeds
|
|
1429
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
|
-
//
|
|
14
|
-
const
|
|
15
|
-
const
|
|
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
|
|
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
|
|
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
|
|
345
|
-
|
|
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
|
+
}
|
package/dist/terminal-manager.js
CHANGED
|
@@ -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 = '';
|
package/dist/tools/edit.js
CHANGED
|
@@ -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>;
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from "fs/promises";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import fetch from 'cross-fetch';
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
5
7
|
import { capture } from '../utils/capture.js';
|
|
6
8
|
import { withTimeout } from '../utils/withTimeout.js';
|
|
7
9
|
import { configManager } from '../config-manager.js';
|
|
@@ -887,3 +889,44 @@ export async function writePdf(filePath, content, outputPath, options = {}) {
|
|
|
887
889
|
throw new Error('Invalid content type for writePdf. Expected string (markdown) or array of operations.');
|
|
888
890
|
}
|
|
889
891
|
}
|
|
892
|
+
const execFileAsync = promisify(execFile);
|
|
893
|
+
const DEFAULT_EDITOR_NEGATIVE_CACHE_MS = 5 * 60 * 1000;
|
|
894
|
+
const defaultEditorCache = new Map();
|
|
895
|
+
function escapeAppleScriptString(value) {
|
|
896
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
897
|
+
}
|
|
898
|
+
export async function getDefaultEditorMetadata(filePath) {
|
|
899
|
+
if (os.platform() !== 'darwin') {
|
|
900
|
+
return {};
|
|
901
|
+
}
|
|
902
|
+
let cacheKey = '';
|
|
903
|
+
try {
|
|
904
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
905
|
+
cacheKey = extension || path.basename(filePath).toLowerCase();
|
|
906
|
+
const cached = defaultEditorCache.get(cacheKey);
|
|
907
|
+
if (cached) {
|
|
908
|
+
if (!cached.expiresAt || cached.expiresAt > Date.now()) {
|
|
909
|
+
return cached.metadata;
|
|
910
|
+
}
|
|
911
|
+
defaultEditorCache.delete(cacheKey);
|
|
912
|
+
}
|
|
913
|
+
const script = `set appAlias to default application of (info for POSIX file "${escapeAppleScriptString(filePath)}")\nreturn (name of (info for appAlias)) & linefeed & POSIX path of appAlias`;
|
|
914
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script], { timeout: 12000 });
|
|
915
|
+
const lines = stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
916
|
+
const defaultEditorName = lines[lines.length - 2]?.replace(/\.app$/i, '') ?? '';
|
|
917
|
+
const defaultEditorPath = lines[lines.length - 1] ?? '';
|
|
918
|
+
if (defaultEditorName && defaultEditorPath.startsWith('/')) {
|
|
919
|
+
const metadata = { defaultEditorName, defaultEditorPath };
|
|
920
|
+
defaultEditorCache.set(cacheKey, { metadata });
|
|
921
|
+
return metadata;
|
|
922
|
+
}
|
|
923
|
+
defaultEditorCache.set(cacheKey, { metadata: {}, expiresAt: Date.now() + DEFAULT_EDITOR_NEGATIVE_CACHE_MS });
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
if (cacheKey) {
|
|
927
|
+
defaultEditorCache.set(cacheKey, { metadata: {}, expiresAt: Date.now() + DEFAULT_EDITOR_NEGATIVE_CACHE_MS });
|
|
928
|
+
}
|
|
929
|
+
// Generic UI fallback is good enough if detection fails.
|
|
930
|
+
}
|
|
931
|
+
return {};
|
|
932
|
+
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { PageRange } from './lib/pdf2md.js';
|
|
2
2
|
import { PdfParseResult } from './lib/pdf2md.js';
|
|
3
|
+
interface CachedPuppeteerChrome {
|
|
4
|
+
executablePath: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Find Chrome in puppeteer's cache directory
|
|
8
|
+
* Returns the executable path if found, undefined otherwise
|
|
9
|
+
*/
|
|
10
|
+
export declare function findPuppeteerChrome(cacheDir?: string): CachedPuppeteerChrome | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Remove stale Puppeteer Chrome builds while preserving the active build.
|
|
13
|
+
*/
|
|
14
|
+
export declare function pruneOldPuppeteerChromeBuilds(activeExecutablePath: string, cacheDir?: string): Promise<void>;
|
|
3
15
|
/**
|
|
4
16
|
* Preemptively ensure Chrome is available for PDF generation.
|
|
5
17
|
* Call this at server startup to trigger download in background if needed.
|
|
@@ -11,3 +23,4 @@ export declare function ensureChromeAvailable(): void;
|
|
|
11
23
|
*/
|
|
12
24
|
export declare function parsePdfToMarkdown(source: string, pageNumbers?: number[] | PageRange): Promise<PdfParseResult>;
|
|
13
25
|
export declare function parseMarkdownToPdf(markdown: string, options?: any): Promise<Buffer>;
|
|
26
|
+
export {};
|