@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
@@ -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';
@@ -220,6 +222,38 @@ export async function validatePath(requestedPath) {
220
222
  });
221
223
  throw new Error(`Failed to resolve symlink for path: ${absoluteOriginal}. Error: ${err.message}`);
222
224
  }
225
+ // SECURITY FIX: When the full path doesn't exist (e.g., writing a new file),
226
+ // resolve the parent directory to detect symlinks in the path chain.
227
+ // Without this, an attacker could create a symlink inside an allowed directory
228
+ // pointing to a restricted location, then write to a non-existent file through
229
+ // that symlink — bypassing the directory restriction check.
230
+ try {
231
+ const parentDir = path.dirname(absoluteOriginal);
232
+ const resolvedParent = await fs.realpath(parentDir, { encoding: 'utf8' });
233
+ const basename = path.basename(absoluteOriginal);
234
+ resolvedRealPath = path.join(resolvedParent, basename);
235
+ }
236
+ catch {
237
+ // Parent also doesn't exist — walk up the tree to find
238
+ // the deepest existing ancestor and resolve it
239
+ let current = absoluteOriginal;
240
+ let remaining = [];
241
+ while (true) {
242
+ const parent = path.dirname(current);
243
+ if (parent === current)
244
+ break; // reached filesystem root
245
+ remaining.unshift(path.basename(current));
246
+ current = parent;
247
+ try {
248
+ const resolvedAncestor = await fs.realpath(current, { encoding: 'utf8' });
249
+ resolvedRealPath = path.join(resolvedAncestor, ...remaining);
250
+ break;
251
+ }
252
+ catch {
253
+ // keep walking up
254
+ }
255
+ }
256
+ }
223
257
  }
224
258
  const pathForNextCheck = resolvedRealPath ?? absoluteOriginal;
225
259
  // Check if path is allowed
@@ -230,25 +264,25 @@ export async function validatePath(requestedPath) {
230
264
  });
231
265
  throw new Error(`Path not allowed: ${requestedPath}. Must be within one of these directories: ${(await getAllowedDirs()).join(', ')}`);
232
266
  }
267
+ // SECURITY: Always return the resolved path (with symlinks resolved) so that
268
+ // all subsequent file operations (read, write, mkdir, etc.) operate on the
269
+ // canonical target, not on a symlink that could point outside allowed directories.
270
+ // pathForNextCheck already holds resolvedRealPath ?? absoluteOriginal from above.
233
271
  // Check if path exists
234
272
  try {
235
273
  // fs.stat() will automatically follow symlinks, so we get existence info
236
- const stats = await fs.stat(absoluteOriginal);
237
- // If path exists, resolve any symlinks
238
- if (resolvedRealPath) {
239
- return resolvedRealPath;
240
- }
241
- return absoluteOriginal;
274
+ await fs.stat(pathForNextCheck);
275
+ return pathForNextCheck;
242
276
  }
243
277
  catch (error) {
244
278
  // Path doesn't exist - validate parent directories
245
- if (await validateParentDirectories(absoluteOriginal)) {
246
- // Return the path if a valid parent exists
279
+ if (await validateParentDirectories(pathForNextCheck)) {
280
+ // Return the resolved path if a valid parent exists
247
281
  // This will be used for folder creation and many other file operations
248
- return absoluteOriginal;
282
+ return pathForNextCheck;
249
283
  }
250
- // If no valid parent found, return the absolute path anyway
251
- return absoluteOriginal;
284
+ // If no valid parent found, return the resolved path anyway
285
+ return pathForNextCheck;
252
286
  }
253
287
  };
254
288
  // Execute with timeout
@@ -573,9 +607,11 @@ export async function listDirectory(dirPath, depth = 2) {
573
607
  catch (error) {
574
608
  const err = error;
575
609
  const displayPath = relativePath || path.basename(currentPath);
576
- // Keep [DENIED] prefix so UI parser regex still matches.
577
- // Append a hint for permission/timeout errors so user gets context.
578
- if (err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'ETIMEDOUT') {
610
+ // Distinguish "not found" from "permission denied" so AI and UI get accurate info.
611
+ if (err.code === 'ENOENT') {
612
+ results.push(`[NOT_FOUND] ${displayPath} path does not exist`);
613
+ }
614
+ else if (err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'ETIMEDOUT') {
579
615
  results.push(`[DENIED] ${displayPath} — not accessible (permission denied, cloud-only file, or Full Disk Access not granted)`);
580
616
  }
581
617
  else {
@@ -853,3 +889,44 @@ export async function writePdf(filePath, content, outputPath, options = {}) {
853
889
  throw new Error('Invalid content type for writePdf. Expected string (markdown) or array of operations.');
854
890
  }
855
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 {};
@@ -1,48 +1,76 @@
1
1
  import fs from 'fs/promises';
2
- import { existsSync } from 'fs';
3
- import { homedir } from 'os';
4
- import { join } from 'path';
2
+ import { existsSync, readdirSync } from 'fs';
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
5
4
  import { mdToPdf } from 'md-to-pdf';
6
5
  import { pdf2md } from './lib/pdf2md.js';
6
+ import { CONFIG_FILE } from '../../config.js';
7
7
  const isUrl = (source) => source.startsWith('http://') || source.startsWith('https://');
8
8
  // Cached Chrome path to avoid repeated lookups
9
9
  let cachedChromePath = null; // null = not checked yet
10
10
  let chromeCheckPromise = null;
11
11
  /**
12
- * Get the puppeteer cache directory
12
+ * Get Desktop Commander's private Puppeteer cache directory.
13
13
  */
14
14
  function getPuppeteerCacheDir() {
15
- return join(homedir(), '.cache', 'puppeteer');
15
+ return join(dirname(CONFIG_FILE), 'puppeteer-cache');
16
+ }
17
+ /**
18
+ * Get the cache path where Puppeteer stores Chrome for Testing builds.
19
+ */
20
+ function getPuppeteerChromeDir(cacheDir = getPuppeteerCacheDir()) {
21
+ return join(cacheDir, 'chrome');
22
+ }
23
+ /**
24
+ * Find the platform-specific executable within a cached Chrome build directory.
25
+ */
26
+ function getChromeExecutablePath(chromeDir, version) {
27
+ const chromePath = process.platform === 'win32'
28
+ ? join(chromeDir, version, 'chrome-win64', 'chrome.exe')
29
+ : process.platform === 'darwin'
30
+ ? join(chromeDir, version, 'chrome-mac-x64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing')
31
+ : join(chromeDir, version, 'chrome-linux64', 'chrome');
32
+ if (existsSync(chromePath)) {
33
+ return chromePath;
34
+ }
35
+ // Also check for arm64 mac
36
+ if (process.platform === 'darwin') {
37
+ const armPath = join(chromeDir, version, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing');
38
+ if (existsSync(armPath)) {
39
+ return armPath;
40
+ }
41
+ }
42
+ return undefined;
43
+ }
44
+ /**
45
+ * Resolve the cached Chrome build directory that owns an executable path.
46
+ */
47
+ function getCachedChromeBuildDir(chromeDir, executablePath) {
48
+ const relativePath = relative(chromeDir, executablePath);
49
+ if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
50
+ return undefined;
51
+ }
52
+ const [buildDir] = relativePath.split(sep);
53
+ return buildDir ? join(chromeDir, buildDir) : undefined;
16
54
  }
17
55
  /**
18
56
  * Find Chrome in puppeteer's cache directory
19
57
  * Returns the executable path if found, undefined otherwise
20
58
  */
21
- function findPuppeteerChrome() {
22
- const cacheDir = getPuppeteerCacheDir();
23
- const chromeDir = join(cacheDir, 'chrome');
59
+ export function findPuppeteerChrome(cacheDir = getPuppeteerCacheDir()) {
60
+ const chromeDir = getPuppeteerChromeDir(cacheDir);
24
61
  if (!existsSync(chromeDir)) {
25
62
  return undefined;
26
63
  }
27
64
  try {
28
65
  // Look for chrome directories (e.g., win64-143.0.7499.169)
29
- const { readdirSync } = require('fs');
30
- const versions = readdirSync(chromeDir);
66
+ const versions = readdirSync(chromeDir, { withFileTypes: true })
67
+ .filter(entry => entry.isDirectory())
68
+ .map(entry => entry.name)
69
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
31
70
  for (const version of versions) {
32
- const chromePath = process.platform === 'win32'
33
- ? join(chromeDir, version, 'chrome-win64', 'chrome.exe')
34
- : process.platform === 'darwin'
35
- ? join(chromeDir, version, 'chrome-mac-x64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing')
36
- : join(chromeDir, version, 'chrome-linux64', 'chrome');
37
- if (existsSync(chromePath)) {
38
- return chromePath;
39
- }
40
- // Also check for arm64 mac
41
- if (process.platform === 'darwin') {
42
- const armPath = join(chromeDir, version, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing');
43
- if (existsSync(armPath)) {
44
- return armPath;
45
- }
71
+ const executablePath = getChromeExecutablePath(chromeDir, version);
72
+ if (executablePath) {
73
+ return { executablePath };
46
74
  }
47
75
  }
48
76
  }
@@ -51,6 +79,37 @@ function findPuppeteerChrome() {
51
79
  }
52
80
  return undefined;
53
81
  }
82
+ /**
83
+ * Remove stale Puppeteer Chrome builds while preserving the active build.
84
+ */
85
+ export async function pruneOldPuppeteerChromeBuilds(activeExecutablePath, cacheDir = getPuppeteerCacheDir()) {
86
+ const chromeDir = getPuppeteerChromeDir(cacheDir);
87
+ const activeBuildDir = getCachedChromeBuildDir(chromeDir, activeExecutablePath);
88
+ if (!activeBuildDir) {
89
+ return;
90
+ }
91
+ let entries;
92
+ try {
93
+ entries = await fs.readdir(chromeDir, { withFileTypes: true });
94
+ }
95
+ catch {
96
+ return;
97
+ }
98
+ await Promise.all(entries
99
+ .filter(entry => entry.isDirectory())
100
+ .map(async (entry) => {
101
+ const buildDir = join(chromeDir, entry.name);
102
+ if (resolve(buildDir) === resolve(activeBuildDir)) {
103
+ return;
104
+ }
105
+ try {
106
+ await fs.rm(buildDir, { recursive: true, force: true });
107
+ }
108
+ catch (error) {
109
+ console.error(`Failed to remove old Chrome cache build at ${buildDir}:`, error);
110
+ }
111
+ }));
112
+ }
54
113
  /**
55
114
  * Find system-installed Chrome/Chromium browser
56
115
  * Returns the executable path if found, undefined otherwise
@@ -91,6 +150,7 @@ async function installChrome() {
91
150
  const platform = detectBrowserPlatform();
92
151
  const buildId = await resolveBuildId(Browser.CHROME, platform, 'stable');
93
152
  console.error('Downloading Chrome for PDF generation (this may take a few minutes)...');
153
+ await fs.mkdir(cacheDir, { recursive: true });
94
154
  const installedBrowser = await install({
95
155
  browser: Browser.CHROME,
96
156
  buildId,
@@ -101,7 +161,9 @@ async function installChrome() {
101
161
  },
102
162
  });
103
163
  console.error('\nChrome download complete.');
104
- return installedBrowser.executablePath;
164
+ return {
165
+ executablePath: installedBrowser.executablePath,
166
+ };
105
167
  }
106
168
  /**
107
169
  * Find or install Chrome for PDF generation
@@ -122,8 +184,9 @@ async function getChromePath() {
122
184
  // 1. Check puppeteer cache first (exact compatible version)
123
185
  const cachedChrome = findPuppeteerChrome();
124
186
  if (cachedChrome) {
125
- cachedChromePath = cachedChrome;
126
- return cachedChrome;
187
+ await pruneOldPuppeteerChromeBuilds(cachedChrome.executablePath);
188
+ cachedChromePath = cachedChrome.executablePath;
189
+ return cachedChrome.executablePath;
127
190
  }
128
191
  // 2. Check system Chrome
129
192
  const systemChrome = findSystemChrome();
@@ -134,8 +197,9 @@ async function getChromePath() {
134
197
  // 3. Install Chrome as last resort
135
198
  try {
136
199
  const installedChrome = await installChrome();
137
- cachedChromePath = installedChrome;
138
- return installedChrome;
200
+ await pruneOldPuppeteerChromeBuilds(installedChrome.executablePath);
201
+ cachedChromePath = installedChrome.executablePath;
202
+ return installedChrome.executablePath;
139
203
  }
140
204
  catch (error) {
141
205
  console.error('Failed to install Chrome:', error);
@@ -73,10 +73,9 @@ async function getClientId() {
73
73
  }
74
74
  }
75
75
 
76
- // Google Analytics configuration (same as setup script)
77
- const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L';
78
- const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A';
79
- const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
76
+ // Telemetry proxy configuration (same as setup script)
77
+ const TELEMETRY_PROXY_URL = 'https://telemetry.desktopcommander.app/mp/collect';
78
+ const TELEMETRY_PROXY_FALLBACK_URL = 'https://dc-telemetry-proxy-83847352264.europe-west1.run.app/mp/collect';
80
79
 
81
80
  /**
82
81
  * Detect installation source from environment and process context
@@ -267,18 +266,13 @@ async function detectInstallationSource() {
267
266
  }
268
267
 
269
268
  /**
270
- * Send installation tracking to analytics
269
+ * Send installation tracking to telemetry proxy
271
270
  */
272
271
  async function trackInstallation(installationData) {
273
- if (!GA_MEASUREMENT_ID || !GA_API_SECRET) {
274
- debug('Analytics not configured, skipping tracking');
275
- return;
276
- }
277
-
278
272
  try {
279
273
  const uniqueUserId = await getClientId();
280
274
  log("user id", uniqueUserId)
281
- // Prepare GA4 payload
275
+ // Prepare telemetry payload
282
276
  const payload = {
283
277
  client_id: uniqueUserId,
284
278
  non_personalized_ads: false,
@@ -308,33 +302,11 @@ async function trackInstallation(installationData) {
308
302
  }
309
303
  };
310
304
 
311
- await new Promise((resolve, reject) => {
312
- const req = https.request(GA_BASE_URL, options);
313
-
314
- const timeoutId = setTimeout(() => {
315
- req.destroy();
316
- reject(new Error('Request timeout'));
317
- }, 5000);
318
-
319
- req.on('error', (error) => {
320
- clearTimeout(timeoutId);
321
- debug(`Analytics error: ${error.message}`);
322
- resolve(); // Don't fail installation on analytics error
323
- });
324
-
325
- req.on('response', (res) => {
326
- clearTimeout(timeoutId);
327
- // Consume the response data to complete the request
328
- res.on('data', () => {}); // Ignore response data
329
- res.on('end', () => {
330
- log(`Installation tracked: ${installationData.source}`);
331
- resolve();
332
- });
333
- });
334
-
335
- req.write(postData);
336
- req.end();
337
- });
305
+ const sent = await postTelemetryPayload(TELEMETRY_PROXY_URL, postData, options);
306
+ if (!sent) {
307
+ await postTelemetryPayload(TELEMETRY_PROXY_FALLBACK_URL, postData, options);
308
+ }
309
+ log(`Installation tracked: ${installationData.source}`);
338
310
 
339
311
  } catch (error) {
340
312
  debug(`Failed to track installation: ${error.message}`);
@@ -342,6 +314,53 @@ async function trackInstallation(installationData) {
342
314
  }
343
315
  }
344
316
 
317
+ // TODO(dedup): postTelemetryPayload is copy-pasted across setup/uninstall/track
318
+ // scripts + a variant in src/utils/capture.ts. Consolidate into one shared module.
319
+ // TODO(timeout): per-endpoint timeout here is 5s, but ensureTrackingCompleted caps
320
+ // the whole flow at 6s — so if the primary times out, the fallback gets ~1s and
321
+ // almost never completes. Lower per-endpoint timeout (capture.ts uses 3s) or raise
322
+ // the overall budget so the fallback is actually usable.
323
+ async function postTelemetryPayload(endpoint, postData, options) {
324
+ return await new Promise((resolve) => {
325
+ let settled = false;
326
+ let timeoutId;
327
+ const finish = (result) => {
328
+ if (settled) return;
329
+ settled = true;
330
+ clearTimeout(timeoutId);
331
+ resolve(result);
332
+ };
333
+ const req = https.request(endpoint, options);
334
+
335
+ timeoutId = setTimeout(() => {
336
+ req.destroy();
337
+ finish(false);
338
+ }, 5000);
339
+
340
+ req.on('error', (error) => {
341
+ debug(`Telemetry error: ${error.message}`);
342
+ finish(false);
343
+ });
344
+
345
+ req.on('response', (res) => {
346
+ res.on('data', () => {});
347
+ res.on('error', (error) => {
348
+ debug(`Telemetry response error: ${error.message}`);
349
+ finish(false);
350
+ });
351
+ res.on('end', () => {
352
+ finish(res.statusCode >= 200 && res.statusCode < 300);
353
+ });
354
+ res.on('close', () => {
355
+ finish(false);
356
+ });
357
+ });
358
+
359
+ req.write(postData);
360
+ req.end();
361
+ });
362
+ }
363
+
345
364
  /**
346
365
  * Main execution
347
366
  */
package/dist/types.d.ts CHANGED
@@ -65,6 +65,10 @@ export interface FilePreviewStructuredContent {
65
65
  fileName: string;
66
66
  filePath: string;
67
67
  fileType: PreviewFileType;
68
+ sourceTool?: 'read_file' | 'write_file' | 'edit_block';
69
+ defaultEditorName?: string;
70
+ defaultEditorPath?: string;
71
+ content?: string;
68
72
  imageData?: string;
69
73
  mimeType?: string;
70
74
  }
@@ -11,4 +11,4 @@ export interface UiToolMeta extends Record<string, unknown> {
11
11
  };
12
12
  'openai/widgetAccessible'?: boolean;
13
13
  }
14
- export declare function buildUiToolMeta(resourceUri: string, widgetAccessible?: boolean): UiToolMeta;
14
+ export declare function buildUiToolMeta(resourceUri: string, widgetAccessible?: boolean, showMcpUiPreviews?: boolean): UiToolMeta | undefined;
@@ -3,7 +3,10 @@
3
3
  */
4
4
  export const FILE_PREVIEW_RESOURCE_URI = 'ui://desktop-commander/file-preview';
5
5
  export const CONFIG_EDITOR_RESOURCE_URI = 'ui://desktop-commander/config-editor';
6
- export function buildUiToolMeta(resourceUri, widgetAccessible = false) {
6
+ export function buildUiToolMeta(resourceUri, widgetAccessible = false, showMcpUiPreviews = true) {
7
+ if (!showMcpUiPreviews) {
8
+ return undefined;
9
+ }
7
10
  const meta = {
8
11
  'ui/resourceUri': resourceUri,
9
12
  'openai/outputTemplate': resourceUri,