@wonderwhy-er/desktop-commander 0.2.43 → 0.2.45

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 (41) hide show
  1. package/dist/bootstrap.d.ts +1 -0
  2. package/dist/bootstrap.js +22 -0
  3. package/dist/config-manager.d.ts +30 -1
  4. package/dist/config-manager.js +55 -8
  5. package/dist/handlers/filesystem-handlers.js +86 -53
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/remote-device/desktop-commander-integration.js +8 -2
  9. package/dist/remote-device/remote-channel.d.ts +1 -0
  10. package/dist/remote-device/remote-channel.js +34 -4
  11. package/dist/server.js +63 -22
  12. package/dist/tools/edit.d.ts +1 -1
  13. package/dist/tools/edit.js +30 -23
  14. package/dist/tools/filesystem.d.ts +1 -0
  15. package/dist/tools/filesystem.js +23 -13
  16. package/dist/tools/schemas.d.ts +19 -7
  17. package/dist/tools/schemas.js +20 -5
  18. package/dist/ui/config-editor/config-editor-runtime.js +49 -49
  19. package/dist/ui/config-editor/src/app.js +2 -11
  20. package/dist/ui/file-preview/preview-runtime.js +147 -146
  21. package/dist/ui/file-preview/src/app.js +172 -53
  22. package/dist/ui/file-preview/src/directory-controller.js +1 -1
  23. package/dist/ui/file-preview/src/markdown/controller.js +1 -0
  24. package/dist/ui/file-preview/src/panel-actions.js +2 -2
  25. package/dist/ui/file-preview/src/payload-utils.js +23 -7
  26. package/dist/utils/capture.d.ts +2 -0
  27. package/dist/utils/capture.js +19 -0
  28. package/dist/utils/files/base.d.ts +2 -0
  29. package/dist/utils/files/image.js +1 -1
  30. package/dist/utils/files/text.js +24 -19
  31. package/dist/utils/open-browser.d.ts +1 -1
  32. package/dist/utils/open-browser.js +5 -2
  33. package/dist/utils/usageTracker.d.ts +5 -1
  34. package/dist/utils/usageTracker.js +14 -3
  35. package/dist/utils/welcome-onboarding.d.ts +1 -1
  36. package/dist/utils/welcome-onboarding.js +2 -2
  37. package/dist/utils/withTimeout.d.ts +17 -0
  38. package/dist/utils/withTimeout.js +33 -0
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/package.json +1 -1
@@ -82,7 +82,7 @@ function getCharacterCodeData(expected, actual) {
82
82
  diffLength: fullDiff.length
83
83
  };
84
84
  }
85
- export async function performSearchReplace(filePath, block, expectedReplacements = 1) {
85
+ export async function performSearchReplace(filePath, block, expectedReplacements = 1, origin) {
86
86
  // Get file extension for telemetry using path module
87
87
  const fileExtension = path.extname(filePath).toLowerCase();
88
88
  // Capture file extension and string sizes in telemetry without capturing the file path
@@ -176,13 +176,18 @@ RECOMMENDATION: For large search/replace operations, consider breaking them into
176
176
  type: "text",
177
177
  text: `${statusLine}${previewContent}`
178
178
  }],
179
- structuredContent: {
180
- fileName: path.basename(resolvedEditPath),
181
- filePath: resolvedEditPath,
182
- fileType: resolvePreviewFileType(resolvedEditPath),
183
- sourceTool: 'edit_block',
184
- ...await getDefaultEditorMetadata(resolvedEditPath),
185
- },
179
+ // structuredContent only travels on widget-initiated calls: it is the
180
+ // widget's save-success signal (assertSuccessfulEditBlockResult) and
181
+ // metadata source. LLM-facing responses don't need it — nothing there
182
+ // consumes it, and the widget re-reads by path instead.
183
+ ...(origin === 'ui' ? {
184
+ structuredContent: {
185
+ fileName: path.basename(resolvedEditPath),
186
+ filePath: resolvedEditPath,
187
+ fileType: resolvePreviewFileType(resolvedEditPath),
188
+ ...await getDefaultEditorMetadata(resolvedEditPath),
189
+ },
190
+ } : {}),
186
191
  };
187
192
  }
188
193
  // If exact match found but count doesn't match expected, inform the user
@@ -367,13 +372,14 @@ export async function handleEditBlock(args) {
367
372
  type: "text",
368
373
  text: `Successfully updated range ${parsed.range} in ${parsed.file_path}`
369
374
  }],
370
- structuredContent: {
371
- fileName: path.basename(resolvedRangePath),
372
- filePath: resolvedRangePath,
373
- fileType: resolvePreviewFileType(resolvedRangePath),
374
- sourceTool: 'edit_block',
375
- ...await getDefaultEditorMetadata(resolvedRangePath),
376
- },
375
+ ...(parsed.origin === 'ui' ? {
376
+ structuredContent: {
377
+ fileName: path.basename(resolvedRangePath),
378
+ filePath: resolvedRangePath,
379
+ fileType: resolvePreviewFileType(resolvedRangePath),
380
+ ...await getDefaultEditorMetadata(resolvedRangePath),
381
+ },
382
+ } : {}),
377
383
  };
378
384
  }
379
385
  catch (error) {
@@ -404,13 +410,14 @@ export async function handleEditBlock(args) {
404
410
  type: "text",
405
411
  text: `Successfully applied ${result.editsApplied} edit(s) to ${parsed.file_path}`
406
412
  }],
407
- structuredContent: {
408
- fileName: path.basename(resolvedEditRangePath),
409
- filePath: resolvedEditRangePath,
410
- fileType: resolvePreviewFileType(resolvedEditRangePath),
411
- sourceTool: 'edit_block',
412
- ...await getDefaultEditorMetadata(resolvedEditRangePath),
413
- },
413
+ ...(parsed.origin === 'ui' ? {
414
+ structuredContent: {
415
+ fileName: path.basename(resolvedEditRangePath),
416
+ filePath: resolvedEditRangePath,
417
+ fileType: resolvePreviewFileType(resolvedEditRangePath),
418
+ ...await getDefaultEditorMetadata(resolvedEditRangePath),
419
+ },
420
+ } : {}),
414
421
  };
415
422
  }
416
423
  const errorMsg = result.errors?.map(e => e.error).join('; ') || 'Unknown error';
@@ -424,5 +431,5 @@ export async function handleEditBlock(args) {
424
431
  return performSearchReplace(parsed.file_path, {
425
432
  search: parsed.old_string,
426
433
  replace: parsed.new_string
427
- }, parsed.expected_replacements);
434
+ }, parsed.expected_replacements, parsed.origin);
428
435
  }
@@ -1,5 +1,6 @@
1
1
  import type { ReadOptions, FileResult, PdfPageItem } from '../utils/files/base.js';
2
2
  import { PdfOperations, PdfMetadata } from './pdf/index.js';
3
+ export declare const READ_OPERATION_TIMEOUT_MS: number;
3
4
  /**
4
5
  * Validates a path to ensure it can be accessed or created.
5
6
  * For existing paths, returns the real path (resolving symlinks).
@@ -5,7 +5,7 @@ import fetch from 'cross-fetch';
5
5
  import { execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
  import { capture } from '../utils/capture.js';
8
- import { withTimeout } from '../utils/withTimeout.js';
8
+ import { withTimeout, runWithAbortableTimeout } from '../utils/withTimeout.js';
9
9
  import { configManager } from '../config-manager.js';
10
10
  import { getFileHandler, TextFileHandler } from '../utils/files/index.js';
11
11
  import { isPdfFile } from "./mime-types.js";
@@ -16,6 +16,13 @@ const FILE_OPERATION_TIMEOUTS = {
16
16
  URL_FETCH: 30000, // 30 seconds
17
17
  FILE_READ: 30000, // 30 seconds
18
18
  };
19
+ // Cap file read operations at 3 minutes. The MCP client's hard per-call limit
20
+ // is ~4 minutes; timing out at 3m lets us abort the underlying fs op and return
21
+ // a useful error (e.g. the cloud-storage guidance in buildPermissionError)
22
+ // BEFORE the client gives up with an opaque "No result received after 4
23
+ // minutes". Paired with runWithAbortableTimeout so the read is actually
24
+ // cancelled (fd/thread released), not just abandoned.
25
+ export const READ_OPERATION_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
19
26
  const FILE_SIZE_LIMITS = {
20
27
  LINE_COUNT_LIMIT: 10 * 1024 * 1024, // 10MB for line counting
21
28
  };
@@ -426,8 +433,9 @@ export async function readFileFromDisk(filePath, options) {
426
433
  capture('server_read_file_error', { error: errorMessage, fileExtension: fileExtension });
427
434
  // If we can't stat the file, continue anyway and let the read operation handle errors
428
435
  }
429
- // Use withTimeout to handle potential hangs
430
- const readOperation = async () => {
436
+ // Read under an abortable timeout so a hung/stalled read is cancelled
437
+ // (fd/thread freed) rather than leaked until the OS call returns.
438
+ const readOperation = async (signal) => {
431
439
  // Get appropriate handler for this file type (async - includes binary detection)
432
440
  const handler = await getFileHandler(validPath);
433
441
  // Use handler to read the file
@@ -436,7 +444,8 @@ export async function readFileFromDisk(filePath, options) {
436
444
  length,
437
445
  sheet,
438
446
  range,
439
- includeStatusMessage: true
447
+ includeStatusMessage: true,
448
+ signal
440
449
  });
441
450
  // Return with content as string
442
451
  // For images: content is already base64-encoded string from handler
@@ -458,18 +467,17 @@ export async function readFileFromDisk(filePath, options) {
458
467
  metadata: result.metadata
459
468
  };
460
469
  };
461
- // Execute with timeout
470
+ // Execute with a 3-minute, cancellable timeout
462
471
  let result;
463
472
  try {
464
- result = await withTimeout(readOperation(), FILE_OPERATION_TIMEOUTS.FILE_READ, `Read file operation for ${filePath}`, null);
473
+ result = await runWithAbortableTimeout((signal) => readOperation(signal), READ_OPERATION_TIMEOUT_MS, `Read file operation for ${filePath}`);
465
474
  }
466
475
  catch (error) {
467
476
  const err = error;
468
- // withTimeout rejects with a plain string "__ERROR__: ... timed out after N seconds"
469
- // when defaultValue is null it has no .code property, so check for that too.
470
- const isWithTimeoutString = typeof error === 'string' && error.startsWith('__ERROR__:');
471
- if (isWithTimeoutString || err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'ETIMEDOUT') {
472
- throw buildPermissionError(filePath, isWithTimeoutString ? 'ETIMEDOUT' : err.code);
477
+ // runWithAbortableTimeout rejects with an Error whose .code is 'ETIMEDOUT'
478
+ // on timeout; fs rejects with EPERM/EACCES. Map all to the guidance error.
479
+ if (err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'ETIMEDOUT') {
480
+ throw buildPermissionError(filePath, err.code);
473
481
  }
474
482
  throw error;
475
483
  }
@@ -515,8 +523,10 @@ export async function readFileInternal(filePath, offset = 0, length) {
515
523
  // IMPORTANT: For internal operations (especially edit operations), we must
516
524
  // preserve exact file content including original line endings.
517
525
  // We cannot use readline-based reading as it strips line endings.
518
- // Read entire file content preserving line endings
519
- const content = await fs.readFile(validPath, 'utf8');
526
+ // Read entire file content preserving line endings, under a 3-minute,
527
+ // cancellable timeout so an edit on a stalled/cloud path can't hang forever
528
+ // (previously this read had no timeout at all).
529
+ const content = await runWithAbortableTimeout((signal) => fs.readFile(validPath, { encoding: 'utf8', signal }), READ_OPERATION_TIMEOUT_MS, `Internal read for ${filePath}`);
520
530
  // If we need to apply offset/length, do it while preserving line endings
521
531
  if (offset === 0 && length >= Number.MAX_SAFE_INTEGER) {
522
532
  // Most common case for edit operations: read entire file
@@ -1,5 +1,11 @@
1
1
  import { z } from "zod";
2
- export declare const GetConfigArgsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
2
+ export declare const GetConfigArgsSchema: z.ZodObject<{
3
+ origin: z.ZodOptional<z.ZodEnum<["ui", "llm"]>>;
4
+ }, "strip", z.ZodTypeAny, {
5
+ origin?: "ui" | "llm" | undefined;
6
+ }, {
7
+ origin?: "ui" | "llm" | undefined;
8
+ }>;
3
9
  export declare const SetConfigValueArgsSchema: z.ZodObject<{
4
10
  key: z.ZodString;
5
11
  value: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString, "many">, z.ZodNull]>;
@@ -19,14 +25,17 @@ export declare const StartProcessArgsSchema: z.ZodObject<{
19
25
  timeout_ms: z.ZodNumber;
20
26
  shell: z.ZodOptional<z.ZodString>;
21
27
  verbose_timing: z.ZodOptional<z.ZodBoolean>;
28
+ origin: z.ZodOptional<z.ZodEnum<["ui", "llm"]>>;
22
29
  }, "strip", z.ZodTypeAny, {
23
30
  command: string;
24
31
  timeout_ms: number;
32
+ origin?: "ui" | "llm" | undefined;
25
33
  shell?: string | undefined;
26
34
  verbose_timing?: boolean | undefined;
27
35
  }, {
28
36
  command: string;
29
37
  timeout_ms: number;
38
+ origin?: "ui" | "llm" | undefined;
30
39
  shell?: string | undefined;
31
40
  verbose_timing?: boolean | undefined;
32
41
  }>;
@@ -78,15 +87,15 @@ export declare const ReadFileArgsSchema: z.ZodObject<{
78
87
  path: string;
79
88
  offset: number;
80
89
  isUrl: boolean;
81
- options?: Record<string, any> | undefined;
82
90
  origin?: "ui" | "llm" | undefined;
91
+ options?: Record<string, any> | undefined;
83
92
  sheet?: string | undefined;
84
93
  range?: string | undefined;
85
94
  }, {
86
95
  path: string;
87
96
  length?: number | undefined;
88
- options?: Record<string, any> | undefined;
89
97
  origin?: "ui" | "llm" | undefined;
98
+ options?: Record<string, any> | undefined;
90
99
  offset?: number | undefined;
91
100
  isUrl?: boolean | undefined;
92
101
  sheet?: string | undefined;
@@ -282,16 +291,16 @@ export declare const EditBlockArgsSchema: z.ZodEffects<z.ZodObject<{
282
291
  }, "strip", z.ZodTypeAny, {
283
292
  file_path: string;
284
293
  expected_replacements: number;
285
- options?: Record<string, any> | undefined;
286
294
  origin?: "ui" | "llm" | undefined;
295
+ options?: Record<string, any> | undefined;
287
296
  range?: string | undefined;
288
297
  content?: any;
289
298
  old_string?: string | undefined;
290
299
  new_string?: string | undefined;
291
300
  }, {
292
301
  file_path: string;
293
- options?: Record<string, any> | undefined;
294
302
  origin?: "ui" | "llm" | undefined;
303
+ options?: Record<string, any> | undefined;
295
304
  range?: string | undefined;
296
305
  content?: any;
297
306
  old_string?: string | undefined;
@@ -300,16 +309,16 @@ export declare const EditBlockArgsSchema: z.ZodEffects<z.ZodObject<{
300
309
  }>, {
301
310
  file_path: string;
302
311
  expected_replacements: number;
303
- options?: Record<string, any> | undefined;
304
312
  origin?: "ui" | "llm" | undefined;
313
+ options?: Record<string, any> | undefined;
305
314
  range?: string | undefined;
306
315
  content?: any;
307
316
  old_string?: string | undefined;
308
317
  new_string?: string | undefined;
309
318
  }, {
310
319
  file_path: string;
311
- options?: Record<string, any> | undefined;
312
320
  origin?: "ui" | "llm" | undefined;
321
+ options?: Record<string, any> | undefined;
313
322
  range?: string | undefined;
314
323
  content?: any;
315
324
  old_string?: string | undefined;
@@ -349,6 +358,7 @@ export declare const StartSearchArgsSchema: z.ZodObject<{
349
358
  timeout_ms: z.ZodOptional<z.ZodNumber>;
350
359
  earlyTermination: z.ZodOptional<z.ZodBoolean>;
351
360
  literalSearch: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
361
+ origin: z.ZodOptional<z.ZodEnum<["ui", "llm"]>>;
352
362
  }, "strip", z.ZodTypeAny, {
353
363
  path: string;
354
364
  pattern: string;
@@ -357,6 +367,7 @@ export declare const StartSearchArgsSchema: z.ZodObject<{
357
367
  includeHidden: boolean;
358
368
  contextLines: number;
359
369
  literalSearch: boolean;
370
+ origin?: "ui" | "llm" | undefined;
360
371
  timeout_ms?: number | undefined;
361
372
  filePattern?: string | undefined;
362
373
  maxResults?: number | undefined;
@@ -364,6 +375,7 @@ export declare const StartSearchArgsSchema: z.ZodObject<{
364
375
  }, {
365
376
  path: string;
366
377
  pattern: string;
378
+ origin?: "ui" | "llm" | undefined;
367
379
  timeout_ms?: number | undefined;
368
380
  searchType?: "content" | "files" | undefined;
369
381
  filePattern?: string | undefined;
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
2
  // Config tools schemas
3
- export const GetConfigArgsSchema = z.object({});
3
+ export const GetConfigArgsSchema = z.object({
4
+ // 'ui' marks calls the config-editor widget fires programmatically; they are
5
+ // excluded from tool-call telemetry (see isUiOriginCall in server.ts).
6
+ origin: z.enum(['ui', 'llm']).optional(),
7
+ });
4
8
  export const SetConfigValueArgsSchema = z.object({
5
9
  key: z.string(),
6
10
  value: z.union([
@@ -10,6 +14,7 @@ export const SetConfigValueArgsSchema = z.object({
10
14
  z.array(z.string()),
11
15
  z.null(),
12
16
  ]),
17
+ // 'ui' marks widget-fired calls; excluded from tool-call telemetry.
13
18
  origin: z.enum(['ui', 'llm']).optional(),
14
19
  });
15
20
  // Empty schemas
@@ -20,6 +25,9 @@ export const StartProcessArgsSchema = z.object({
20
25
  timeout_ms: z.number(),
21
26
  shell: z.string().optional(),
22
27
  verbose_timing: z.boolean().optional(),
28
+ // 'ui' marks widget-fired calls (e.g. open-in-folder/editor buttons);
29
+ // excluded from tool-call telemetry (see isUiOriginCall in server.ts).
30
+ origin: z.enum(['ui', 'llm']).optional(),
23
31
  });
24
32
  export const ReadProcessOutputArgsSchema = z.object({
25
33
  pid: z.number(),
@@ -45,7 +53,8 @@ export const ReadFileArgsSchema = z.object({
45
53
  range: z.string().optional(),
46
54
  options: z.record(z.any()).optional(),
47
55
  // Whether the call came from the file-preview UI (refresh/navigation) or the
48
- // LLM. Used only for telemetry attribution; see call_origin in server.ts.
56
+ // LLM. 'ui' calls are excluded from tool-call telemetry; see isUiOriginCall
57
+ // in server.ts.
49
58
  origin: z.enum(['ui', 'llm']).optional(),
50
59
  });
51
60
  export const ReadMultipleFilesArgsSchema = z.object({
@@ -55,7 +64,8 @@ export const WriteFileArgsSchema = z.object({
55
64
  path: z.string(),
56
65
  content: z.string(),
57
66
  mode: z.enum(['rewrite', 'append']).default('rewrite'),
58
- // Telemetry attribution: 'ui' when fired by the file-preview UI, else 'llm'.
67
+ // 'ui' when fired by the file-preview UI, else 'llm'. 'ui' calls are
68
+ // excluded from tool-call telemetry; see isUiOriginCall in server.ts.
59
69
  origin: z.enum(['ui', 'llm']).optional(),
60
70
  });
61
71
  // PDF modification schemas - exported for reuse
@@ -97,7 +107,8 @@ export const CreateDirectoryArgsSchema = z.object({
97
107
  export const ListDirectoryArgsSchema = z.object({
98
108
  path: z.string(),
99
109
  depth: z.number().optional().default(2),
100
- // Telemetry attribution: 'ui' when fired by the file-preview UI, else 'llm'.
110
+ // 'ui' when fired by the file-preview UI, else 'llm'. 'ui' calls are
111
+ // excluded from tool-call telemetry; see isUiOriginCall in server.ts.
101
112
  origin: z.enum(['ui', 'llm']).optional(),
102
113
  });
103
114
  export const MoveFileArgsSchema = z.object({
@@ -122,7 +133,8 @@ export const EditBlockArgsSchema = z.object({
122
133
  range: z.string().optional(),
123
134
  content: z.any().optional(),
124
135
  options: z.record(z.any()).optional(),
125
- // Telemetry attribution: 'ui' when fired by the file-preview UI, else 'llm'.
136
+ // 'ui' when fired by the file-preview UI, else 'llm'. 'ui' calls are
137
+ // excluded from tool-call telemetry; see isUiOriginCall in server.ts.
126
138
  origin: z.enum(['ui', 'llm']).optional(),
127
139
  }).refine(data => {
128
140
  // Helper to check if value is actually provided (not undefined, not empty string)
@@ -162,6 +174,9 @@ export const StartSearchArgsSchema = z.object({
162
174
  timeout_ms: z.number().optional(), // Match process naming convention
163
175
  earlyTermination: z.boolean().optional(), // Stop search early when exact filename match is found (default: true for files, false for content)
164
176
  literalSearch: z.boolean().optional().default(false), // Force literal string matching (-F flag) instead of regex
177
+ // 'ui' marks widget-fired calls (e.g. markdown link-target search);
178
+ // excluded from tool-call telemetry (see isUiOriginCall in server.ts).
179
+ origin: z.enum(['ui', 'llm']).optional(),
165
180
  });
166
181
  export const GetMoreSearchResultsArgsSchema = z.object({
167
182
  sessionId: z.string(),