@wonderwhy-er/desktop-commander 0.2.41 → 0.2.43

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 (45) hide show
  1. package/README.md +0 -2
  2. package/dist/handlers/filesystem-handlers.js +24 -4
  3. package/dist/remote-device/remote-channel.d.ts +14 -0
  4. package/dist/remote-device/remote-channel.js +107 -28
  5. package/dist/server.d.ts +6 -1
  6. package/dist/server.js +112 -25
  7. package/dist/setup-claude-server.js +56 -50
  8. package/dist/terminal-manager.d.ts +18 -0
  9. package/dist/terminal-manager.js +130 -18
  10. package/dist/tools/edit.js +11 -4
  11. package/dist/tools/filesystem.d.ts +5 -0
  12. package/dist/tools/filesystem.js +43 -0
  13. package/dist/tools/fuzzySearch.d.ts +10 -20
  14. package/dist/tools/fuzzySearch.js +66 -105
  15. package/dist/tools/fuzzySearchCore.d.ts +52 -0
  16. package/dist/tools/fuzzySearchCore.js +125 -0
  17. package/dist/tools/improved-process-tools.js +8 -1
  18. package/dist/tools/pdf/markdown.d.ts +13 -0
  19. package/dist/tools/pdf/markdown.js +93 -29
  20. package/dist/tools/schemas.d.ts +20 -0
  21. package/dist/tools/schemas.js +45 -2
  22. package/dist/track-installation.js +57 -38
  23. package/dist/types.d.ts +6 -1
  24. package/dist/ui/contracts.d.ts +1 -1
  25. package/dist/ui/contracts.js +4 -1
  26. package/dist/ui/file-preview/preview-runtime.js +111 -113
  27. package/dist/ui/file-preview/src/app.js +19 -17
  28. package/dist/ui/file-preview/src/directory-controller.js +3 -3
  29. package/dist/ui/file-preview/src/file-type-handlers.js +2 -2
  30. package/dist/ui/file-preview/src/host/external-actions.d.ts +0 -11
  31. package/dist/ui/file-preview/src/host/external-actions.js +0 -39
  32. package/dist/ui/file-preview/src/markdown/controller.js +3 -1
  33. package/dist/ui/file-preview/src/panel-actions.js +2 -2
  34. package/dist/uninstall-claude-server.js +54 -47
  35. package/dist/utils/ab-test.d.ts +4 -0
  36. package/dist/utils/ab-test.js +6 -0
  37. package/dist/utils/capture.d.ts +10 -2
  38. package/dist/utils/capture.js +92 -60
  39. package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
  40. package/dist/utils/mcp-ui-ab-test.js +62 -0
  41. package/dist/utils/unsupportedParams.d.ts +24 -0
  42. package/dist/utils/unsupportedParams.js +66 -0
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +6 -1
@@ -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
+ }
@@ -5,7 +5,18 @@ interface CompletedSession {
5
5
  exitCode: number | null;
6
6
  startTime: Date;
7
7
  endTime: Date;
8
+ evictedLines: number;
9
+ evictedChars: number;
8
10
  }
11
+ /**
12
+ * Output buffering caps. Without a cap, a process emitting enough output makes
13
+ * string concatenation throw "RangeError: Invalid string length" at V8's max
14
+ * string size (~536M chars) inside a stdout 'data' handler — an uncaught
15
+ * exception that kills the whole server (index.ts exits on uncaughtException).
16
+ * The cap also bounds the join() cost in snapshot reads and the periodic
17
+ * process-state scan, both of which are O(total output).
18
+ */
19
+ export declare const MAX_BUFFERED_OUTPUT_CHARS: number;
9
20
  export interface PaginatedOutputResult {
10
21
  lines: string[];
11
22
  totalLines: number;
@@ -15,6 +26,7 @@ export interface PaginatedOutputResult {
15
26
  isComplete: boolean;
16
27
  exitCode?: number | null;
17
28
  runtimeMs?: number;
29
+ evictedLines?: number;
18
30
  }
19
31
  export declare class TerminalManager {
20
32
  private sessions;
@@ -72,6 +84,12 @@ export declare class TerminalManager {
72
84
  totalChars: number;
73
85
  lineCount: number;
74
86
  }): string | null;
87
+ /**
88
+ * New output since a snapshot, in absolute (since process start) offsets.
89
+ * If eviction dropped part of the unseen output, returns what the buffer
90
+ * still holds — the oldest unseen chars are lost to the cap.
91
+ */
92
+ private static outputSinceSnapshot;
75
93
  /**
76
94
  * Get a session by PID
77
95
  * @param pid Process ID
@@ -4,6 +4,45 @@ 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
+ }
35
+ /**
36
+ * Output buffering caps. Without a cap, a process emitting enough output makes
37
+ * string concatenation throw "RangeError: Invalid string length" at V8's max
38
+ * string size (~536M chars) inside a stdout 'data' handler — an uncaught
39
+ * exception that kills the whole server (index.ts exits on uncaughtException).
40
+ * The cap also bounds the join() cost in snapshot reads and the periodic
41
+ * process-state scan, both of which are O(total output).
42
+ */
43
+ export const MAX_BUFFERED_OUTPUT_CHARS = 50 * 1024 * 1024; // per session; oldest lines evicted first
44
+ const MAX_LINE_CHARS = 1024 * 1024; // force-split longer lines so eviction can work
45
+ const MAX_WAIT_OUTPUT_CHARS = 2 * 1024 * 1024; // start_process wait buffer (prompt/state detection)
7
46
  /**
8
47
  * Get the appropriate spawn configuration for a given shell
9
48
  * This handles login shell flags for different shell types
@@ -39,6 +78,7 @@ function getShellSpawnArgs(shellPath, command) {
39
78
  return {
40
79
  executable: shellPath,
41
80
  args: ['/c', command],
81
+ windowsVerbatim: true,
42
82
  useShellOption: false
43
83
  };
44
84
  }
@@ -143,6 +183,23 @@ export class TerminalManager {
143
183
  windowsHide: true // Prevent visible console windows on Windows
144
184
  };
145
185
  }
186
+ // Repair PATHEXT on Windows before spawning. On some Windows DXT launches
187
+ // the server process inherits a corrupted PATHEXT (e.g. ".CPL"), which we
188
+ // would otherwise propagate via { ...process.env } and break command
189
+ // resolution (git, node, python, rg, ...) in the spawned shell. See #481.
190
+ if (process.platform === 'win32' && spawnOptions.env) {
191
+ spawnOptions.env.PATHEXT = getRepairedPathExt();
192
+ }
193
+ // On Windows, when we invoke cmd.exe directly and pass the user's command as a
194
+ // single argument, Node/libuv applies MSVCRT-style quoting that escapes embedded
195
+ // double quotes as \" . cmd.exe does not understand that escaping, so any command
196
+ // containing quotes (e.g. a quoted path with spaces like "C:\Program Files\app.exe")
197
+ // is corrupted before the shell ever parses it. Passing arguments verbatim lets
198
+ // cmd handle its own quoting. Scoped to shells that set windowsVerbatim (cmd only)
199
+ // because PowerShell/pwsh have different quote rules and must NOT use verbatim.
200
+ if (process.platform === 'win32' && spawnConfig.windowsVerbatim) {
201
+ spawnOptions.windowsVerbatimArguments = true;
202
+ }
146
203
  // Spawn the process with appropriate arguments
147
204
  const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions);
148
205
  let output = '';
@@ -161,7 +218,10 @@ export class TerminalManager {
161
218
  outputLines: [], // Line-based buffer
162
219
  lastReadIndex: 0, // Track where "new" output starts
163
220
  isBlocked: false,
164
- startTime: new Date()
221
+ startTime: new Date(),
222
+ bufferedChars: 0,
223
+ evictedLines: 0,
224
+ evictedChars: 0
165
225
  };
166
226
  this.sessions.set(childProcess.pid, session);
167
227
  // Timing telemetry
@@ -203,7 +263,14 @@ export class TerminalManager {
203
263
  if (!firstOutputTime)
204
264
  firstOutputTime = now;
205
265
  lastOutputTime = now;
206
- output += text;
266
+ // `output` only feeds the wait-phase result and prompt/state detection,
267
+ // so stop growing it once resolved and keep only a bounded tail.
268
+ if (!resolved) {
269
+ output += text;
270
+ if (output.length > MAX_WAIT_OUTPUT_CHARS) {
271
+ output = output.slice(-Math.floor(MAX_WAIT_OUTPUT_CHARS / 2));
272
+ }
273
+ }
207
274
  // Append to line-based buffer
208
275
  this.appendToLineBuffer(session, text);
209
276
  // Record output event if collecting timing
@@ -236,7 +303,12 @@ export class TerminalManager {
236
303
  if (!firstOutputTime)
237
304
  firstOutputTime = now;
238
305
  lastOutputTime = now;
239
- output += text;
306
+ if (!resolved) {
307
+ output += text;
308
+ if (output.length > MAX_WAIT_OUTPUT_CHARS) {
309
+ output = output.slice(-Math.floor(MAX_WAIT_OUTPUT_CHARS / 2));
310
+ }
311
+ }
240
312
  // Append to line-based buffer
241
313
  this.appendToLineBuffer(session, text);
242
314
  // Record output event if collecting timing
@@ -283,7 +355,9 @@ export class TerminalManager {
283
355
  outputLines: [...session.outputLines], // Copy line buffer
284
356
  exitCode: code,
285
357
  startTime: session.startTime,
286
- endTime: new Date()
358
+ endTime: new Date(),
359
+ evictedLines: session.evictedLines,
360
+ evictedChars: session.evictedChars
287
361
  });
288
362
  // Keep only last 100 completed sessions
289
363
  if (this.completedSessions.size > 100) {
@@ -327,6 +401,32 @@ export class TerminalManager {
327
401
  session.outputLines.push(line);
328
402
  }
329
403
  }
404
+ // Appended text contributes exactly its length to the joined buffer
405
+ // (its newlines become the join separators).
406
+ session.bufferedChars += text.length;
407
+ // A process printing without newlines grows a single line forever, which
408
+ // eviction can't bound — force-split so no line exceeds MAX_LINE_CHARS.
409
+ // Each inserted break adds one separator to the joined length.
410
+ let lastIndex = session.outputLines.length - 1;
411
+ while (session.outputLines[lastIndex].length > MAX_LINE_CHARS) {
412
+ const overlong = session.outputLines[lastIndex];
413
+ session.outputLines[lastIndex] = overlong.slice(0, MAX_LINE_CHARS);
414
+ session.outputLines.push(overlong.slice(MAX_LINE_CHARS));
415
+ session.bufferedChars += 1;
416
+ lastIndex++;
417
+ }
418
+ // Enforce the per-session cap by evicting the oldest lines. Keeps the
419
+ // buffer far below V8's max string length so concatenation and join()
420
+ // can never throw "Invalid string length" and kill the server.
421
+ while (session.bufferedChars > MAX_BUFFERED_OUTPUT_CHARS && session.outputLines.length > 1) {
422
+ const dropped = session.outputLines.shift();
423
+ const droppedJoinedChars = dropped.length + 1; // +1 for its join separator
424
+ session.bufferedChars -= droppedJoinedChars;
425
+ session.evictedChars += droppedJoinedChars;
426
+ session.evictedLines++;
427
+ if (session.lastReadIndex > 0)
428
+ session.lastReadIndex--;
429
+ }
330
430
  }
331
431
  /**
332
432
  * Read process output with pagination (like file reading)
@@ -339,15 +439,19 @@ export class TerminalManager {
339
439
  // First check active sessions
340
440
  const session = this.sessions.get(pid);
341
441
  if (session) {
342
- return this.readFromLineBuffer(session.outputLines, offset, length, session.lastReadIndex, (newIndex) => { session.lastReadIndex = newIndex; }, false, undefined);
442
+ const result = this.readFromLineBuffer(session.outputLines, offset, length, session.lastReadIndex, (newIndex) => { session.lastReadIndex = newIndex; }, false, undefined);
443
+ result.evictedLines = session.evictedLines;
444
+ return result;
343
445
  }
344
446
  // Then check completed sessions
345
447
  const completedSession = this.completedSessions.get(pid);
346
448
  if (completedSession) {
347
449
  const runtimeMs = completedSession.endTime.getTime() - completedSession.startTime.getTime();
348
- return this.readFromLineBuffer(completedSession.outputLines, offset, length, 0, // Completed sessions don't track read position
450
+ const result = this.readFromLineBuffer(completedSession.outputLines, offset, length, 0, // Completed sessions don't track read position
349
451
  () => { }, // No-op for completed sessions
350
452
  true, completedSession.exitCode, runtimeMs);
453
+ result.evictedLines = completedSession.evictedLines;
454
+ return result;
351
455
  }
352
456
  return null;
353
457
  }
@@ -445,8 +549,11 @@ export class TerminalManager {
445
549
  if (session) {
446
550
  const fullOutput = session.outputLines.join('\n');
447
551
  return {
448
- totalChars: fullOutput.length,
449
- lineCount: session.outputLines.length
552
+ // Absolute since process start (includes evicted output), so the
553
+ // offset stays valid even if the cap evicts lines between
554
+ // snapshot and read.
555
+ totalChars: session.evictedChars + fullOutput.length,
556
+ lineCount: session.evictedLines + session.outputLines.length
450
557
  };
451
558
  }
452
559
  return null;
@@ -460,23 +567,28 @@ export class TerminalManager {
460
567
  // Check active session first
461
568
  const session = this.sessions.get(pid);
462
569
  if (session) {
463
- const fullOutput = session.outputLines.join('\n');
464
- if (fullOutput.length <= snapshot.totalChars) {
465
- return ''; // No new output
466
- }
467
- return fullOutput.substring(snapshot.totalChars);
570
+ return TerminalManager.outputSinceSnapshot(session.outputLines, session.evictedChars, snapshot.totalChars);
468
571
  }
469
572
  // Fallback to completed sessions - process may have finished between snapshot and poll
470
573
  const completedSession = this.completedSessions.get(pid);
471
574
  if (completedSession) {
472
- const fullOutput = completedSession.outputLines.join('\n');
473
- if (fullOutput.length <= snapshot.totalChars) {
474
- return ''; // No new output
475
- }
476
- return fullOutput.substring(snapshot.totalChars);
575
+ return TerminalManager.outputSinceSnapshot(completedSession.outputLines, completedSession.evictedChars, snapshot.totalChars);
477
576
  }
478
577
  return null;
479
578
  }
579
+ /**
580
+ * New output since a snapshot, in absolute (since process start) offsets.
581
+ * If eviction dropped part of the unseen output, returns what the buffer
582
+ * still holds — the oldest unseen chars are lost to the cap.
583
+ */
584
+ static outputSinceSnapshot(outputLines, evictedChars, snapshotTotalChars) {
585
+ const fullOutput = outputLines.join('\n');
586
+ const newChars = evictedChars + fullOutput.length - snapshotTotalChars;
587
+ if (newChars <= 0) {
588
+ return ''; // No new output
589
+ }
590
+ return fullOutput.substring(Math.max(0, fullOutput.length - newChars));
591
+ }
480
592
  /**
481
593
  * Get a session by PID
482
594
  * @param pid Process ID
@@ -14,8 +14,8 @@
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';
18
- import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js';
17
+ import { getDefaultEditorMetadata, writeFile, readFileInternal, validatePath } from './filesystem.js';
18
+ import { runFuzzySearchInWorker, getSimilarityRatio } from './fuzzySearch.js';
19
19
  import { capture } from '../utils/capture.js';
20
20
  import { createErrorResponse } from '../error-handlers.js';
21
21
  import { EditBlockArgsSchema } from "./schemas.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
  }
@@ -200,8 +202,9 @@ RECOMMENDATION: For large search/replace operations, consider breaking them into
200
202
  if (count === 0) {
201
203
  // Track fuzzy search time
202
204
  const startTime = performance.now();
203
- // Perform fuzzy search
204
- const fuzzyResult = recursiveFuzzyIndexOf(content, block.search);
205
+ // Perform fuzzy search in a worker thread so the main event loop stays
206
+ // responsive to pings and parallel tool calls during the scan
207
+ const fuzzyResult = await runFuzzySearchInWorker(content, block.search);
205
208
  const similarity = getSimilarityRatio(block.search, fuzzyResult.value);
206
209
  // Calculate execution time in milliseconds
207
210
  const executionTime = performance.now() - startTime;
@@ -368,6 +371,8 @@ export async function handleEditBlock(args) {
368
371
  fileName: path.basename(resolvedRangePath),
369
372
  filePath: resolvedRangePath,
370
373
  fileType: resolvePreviewFileType(resolvedRangePath),
374
+ sourceTool: 'edit_block',
375
+ ...await getDefaultEditorMetadata(resolvedRangePath),
371
376
  },
372
377
  };
373
378
  }
@@ -403,6 +408,8 @@ export async function handleEditBlock(args) {
403
408
  fileName: path.basename(resolvedEditRangePath),
404
409
  filePath: resolvedEditRangePath,
405
410
  fileType: resolvePreviewFileType(resolvedEditRangePath),
411
+ sourceTool: 'edit_block',
412
+ ...await getDefaultEditorMetadata(resolvedEditRangePath),
406
413
  },
407
414
  };
408
415
  }
@@ -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>;
@@ -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,22 +1,12 @@
1
+ import type { FuzzyMatch } from './fuzzySearchCore.js';
2
+ export { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearchCore.js';
3
+ /** Abort fuzzy search in the worker after this many ms to avoid unbounded CPU burn. */
4
+ export declare const FUZZY_SEARCH_TIMEOUT_MS = 30000;
1
5
  /**
2
- * Recursively finds the closest match to a query string within text using fuzzy matching
3
- * @param text The text to search within
4
- * @param query The query string to find
5
- * @param start Start index in the text (default: 0)
6
- * @param end End index in the text (default: text.length)
7
- * @param parentDistance Best distance found so far (default: Infinity)
8
- * @returns Object with start and end indices, matched value, and Levenshtein distance
6
+ * Runs the fuzzy search in a Worker thread so the main MCP event loop stays
7
+ * responsive to pings and other tool calls during heavy scans. Rejects if the
8
+ * scan exceeds timeoutMs, terminating the worker so it doesn't linger in the
9
+ * background. Search metrics come back with the result and are captured here,
10
+ * on the main thread, where the client identity is initialized.
9
11
  */
10
- export declare function recursiveFuzzyIndexOf(text: string, query: string, start?: number, end?: number | null, parentDistance?: number, depth?: number): {
11
- start: number;
12
- end: number;
13
- value: string;
14
- distance: number;
15
- };
16
- /**
17
- * Calculates the similarity ratio between two strings
18
- * @param a First string
19
- * @param b Second string
20
- * @returns Similarity ratio (0-1)
21
- */
22
- export declare function getSimilarityRatio(a: string, b: string): number;
12
+ export declare function runFuzzySearchInWorker(text: string, query: string, timeoutMs?: number): Promise<FuzzyMatch>;