@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.
@@ -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,9 @@ 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;
68
71
  content?: string;
69
72
  imageData?: string;
70
73
  mimeType?: string;
@@ -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,