@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.
- package/README.md +4 -4
- package/dist/handlers/filesystem-handlers.js +28 -3
- package/dist/server.d.ts +2 -1
- package/dist/server.js +50 -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 +91 -14
- 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 +4 -0
- package/dist/ui/contracts.d.ts +1 -1
- package/dist/ui/contracts.js +4 -1
- package/dist/ui/file-preview/preview-runtime.js +114 -116
- package/dist/ui/file-preview/src/app.js +19 -22
- package/dist/ui/file-preview/src/directory-controller.js +9 -2
- package/dist/ui/file-preview/src/file-type-handlers.js +20 -9
- 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/ui/file-preview/src/payload-utils.js +10 -1
- 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/feature-flags.d.ts +3 -0
- package/dist/utils/feature-flags.js +34 -5
- package/dist/utils/files/excel.js +26 -5
- 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/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';
|
|
@@ -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
|
-
|
|
237
|
-
|
|
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(
|
|
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
|
|
282
|
+
return pathForNextCheck;
|
|
249
283
|
}
|
|
250
|
-
// If no valid parent found, return the
|
|
251
|
-
return
|
|
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
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
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 {
|
|
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
|
|
12
|
+
* Get Desktop Commander's private Puppeteer cache directory.
|
|
13
13
|
*/
|
|
14
14
|
function getPuppeteerCacheDir() {
|
|
15
|
-
return join(
|
|
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
|
|
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
|
|
30
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
//
|
|
77
|
-
const
|
|
78
|
-
const
|
|
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
|
|
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
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
}
|
package/dist/ui/contracts.d.ts
CHANGED
|
@@ -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;
|
package/dist/ui/contracts.js
CHANGED
|
@@ -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,
|