@hcu-lab.me/mcp-device 1.0.6 → 1.0.8
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 +7 -71
- package/dist/bootstrap.js +1 -1
- package/dist/config-manager.js +1 -1
- package/dist/config.js +1 -1
- package/dist/custom-stdio.js +1 -2
- package/dist/data/onboarding-prompts.json +22 -11
- package/dist/device/device-state.d.ts +0 -5
- package/dist/device/device-state.js +0 -84
- package/dist/device/device.d.ts +1 -1
- package/dist/device/device.js +8 -7
- package/dist/device/execution-engine.d.ts +10 -1
- package/dist/device/execution-engine.js +81 -128
- package/dist/device/gateway-channel.d.ts +6 -0
- package/dist/device/gateway-channel.js +16 -1
- package/dist/device/gateway-tool-adapter.d.ts +3 -3
- package/dist/device/gateway-tool-adapter.js +12 -11
- package/dist/device/linux-service.d.ts +4 -0
- package/dist/device/linux-service.js +10 -3
- package/dist/device/runtime-owner.js +2 -2
- package/dist/device/self-update.js +3 -1
- package/dist/device/windows-service.d.ts +3 -4
- package/dist/device/windows-service.js +5 -44
- package/dist/npm-scripts/remote.js +0 -2
- package/dist/server.js +9 -99
- package/dist/terminal-manager.js +1 -1
- package/dist/tools/config.d.ts +1 -1
- package/dist/tools/filesystem.js +3 -3
- package/dist/tools/pdf/markdown.js +1 -1
- package/dist/tools/schemas.d.ts +2 -3
- package/dist/tools/schemas.js +0 -10
- package/dist/ui/config-editor/index.html +1 -1
- package/dist/ui/file-preview/index.html +1 -1
- package/dist/ui/file-preview/styles.css +2 -2
- package/dist/utils/capture.d.ts +5 -28
- package/dist/utils/capture.js +11 -455
- package/dist/utils/feature-flags.d.ts +0 -47
- package/dist/utils/feature-flags.js +5 -199
- package/dist/utils/files/docx.d.ts +2 -2
- package/dist/utils/files/docx.js +2 -2
- package/dist/utils/usageTracker.js +0 -24
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -6
- package/dist/tools/feedback.d.ts +0 -8
- package/dist/tools/feedback.js +0 -132
package/dist/utils/capture.js
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
import { platform } from 'os';
|
|
2
|
-
import * as https from 'https';
|
|
3
1
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
4
|
-
import { configManager, isTelemetryDisabledValue } from '../config-manager.js';
|
|
5
|
-
import { currentClient, currentCallIsRemote, currentRemoteClient } from '../server.js';
|
|
6
|
-
// Execution context for tool calls fired programmatically by the widget UIs
|
|
7
|
-
// (file preview, config editor), marked by args.origin === 'ui'. While code
|
|
8
|
-
// runs inside this context, capture() drops every event, so UI refresh churn
|
|
9
|
-
// (pull-by-path reads, in-preview saves, folder expansion, link search, ...)
|
|
10
|
-
// produces zero telemetry. AsyncLocalStorage (rather than a module-level flag)
|
|
11
|
-
// keeps attribution correct when a widget call interleaves with an agent call.
|
|
12
2
|
const uiOriginCallContext = new AsyncLocalStorage();
|
|
13
3
|
export function runInUiOriginCallContext(fn) {
|
|
14
4
|
return uiOriginCallContext.run(true, fn);
|
|
@@ -16,52 +6,19 @@ export function runInUiOriginCallContext(fn) {
|
|
|
16
6
|
export function isInsideUiOriginCall() {
|
|
17
7
|
return uiOriginCallContext.getStore() === true;
|
|
18
8
|
}
|
|
19
|
-
let VERSION = 'unknown';
|
|
20
|
-
try {
|
|
21
|
-
const versionModule = await import('../version.js');
|
|
22
|
-
VERSION = versionModule.VERSION;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
// Continue without version info if not available
|
|
26
|
-
}
|
|
27
|
-
// Will be initialized when needed
|
|
28
|
-
let uniqueUserId = 'unknown';
|
|
29
|
-
// --- Telemetry Proxy (direct BigQuery ingestion) ---
|
|
30
|
-
// TODO: Move proxy endpoints, auth header setup, request retry/fallback, and
|
|
31
|
-
// transport code into a dedicated telemetry utility once this migration lands.
|
|
32
|
-
// TODO(security): bearer token was removed, so this endpoint is now unauthenticated.
|
|
33
|
-
// Confirm the proxy enforces rate limiting / payload validation server-side,
|
|
34
|
-
// otherwise anyone can POST arbitrary events straight into BigQuery ingestion.
|
|
35
|
-
const TELEMETRY_PROXY_URL = 'https://telemetry.desktopcommander.app/mp/collect';
|
|
36
|
-
const TELEMETRY_PROXY_FALLBACK_URL = 'https://dc-telemetry-proxy-83847352264.europe-west1.run.app/mp/collect';
|
|
37
|
-
/**
|
|
38
|
-
* Hard kill-switch for telemetry via environment variable.
|
|
39
|
-
*
|
|
40
|
-
* Independent of the persisted `telemetryEnabled` config so that tests, CI and
|
|
41
|
-
* one-off runs can suppress all analytics without mutating the user's config.
|
|
42
|
-
* Set DESKTOP_COMMANDER_DISABLE_TELEMETRY to 1/true/yes/on to disable.
|
|
43
|
-
*/
|
|
44
9
|
export function isTelemetryDisabledByEnv() {
|
|
45
|
-
const raw = process.env.
|
|
10
|
+
const raw = process.env.MCP_DEVICE_DISABLE_TELEMETRY;
|
|
46
11
|
if (!raw)
|
|
47
12
|
return false;
|
|
48
13
|
return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase());
|
|
49
14
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Sanitizes error objects to remove potentially sensitive information like file paths
|
|
52
|
-
* @param error Error object or string to sanitize
|
|
53
|
-
* @returns An object with sanitized message and optional error code
|
|
54
|
-
*/
|
|
55
15
|
export function sanitizeError(error) {
|
|
56
16
|
let errorMessage = '';
|
|
57
|
-
let errorCode
|
|
17
|
+
let errorCode;
|
|
58
18
|
if (error instanceof Error) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if ('code' in error) {
|
|
63
|
-
errorCode = error.code;
|
|
64
|
-
}
|
|
19
|
+
errorMessage = `${error.name}: ${error.message}`;
|
|
20
|
+
if ('code' in error)
|
|
21
|
+
errorCode = String(error.code);
|
|
65
22
|
}
|
|
66
23
|
else if (typeof error === 'string') {
|
|
67
24
|
errorMessage = error;
|
|
@@ -69,413 +26,12 @@ export function sanitizeError(error) {
|
|
|
69
26
|
else {
|
|
70
27
|
errorMessage = 'Unknown error';
|
|
71
28
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
errorMessage = errorMessage.replace(/[A-Za-z]:\\[\w\d_.-\/\\]+/g, '[PATH]');
|
|
76
|
-
return {
|
|
77
|
-
message: errorMessage,
|
|
78
|
-
code: errorCode
|
|
79
|
-
};
|
|
29
|
+
errorMessage = errorMessage.replace(/(?:\/|\\)[\w\d_.\-\/\\]+/g, '[PATH]');
|
|
30
|
+
errorMessage = errorMessage.replace(/[A-Za-z]:\\[\w\d_.\-\/\\]+/g, '[PATH]');
|
|
31
|
+
return { message: errorMessage, code: errorCode };
|
|
80
32
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* @param event Event name
|
|
84
|
-
* @param properties Optional event properties
|
|
85
|
-
*/
|
|
86
|
-
// TODO(cleanup): captureBase is now dead code — no caller remains after the GA
|
|
87
|
-
// removal (only referenced in a comment). It still carries the full GA4-flavored
|
|
88
|
-
// send path. Remove it, or repurpose it as the shared proxy transport.
|
|
89
|
-
export const captureBase = async (captureURL, event, properties) => {
|
|
90
|
-
try {
|
|
91
|
-
// Env kill-switch takes precedence over config (tests/CI).
|
|
92
|
-
if (isTelemetryDisabledByEnv()) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
// Check if telemetry is enabled in config (defaults to true if not set)
|
|
96
|
-
const telemetryEnabled = await configManager.getValue('telemetryEnabled');
|
|
97
|
-
// If telemetry is explicitly disabled or GA credentials are missing, don't send
|
|
98
|
-
if (isTelemetryDisabledValue(telemetryEnabled) || !captureURL) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
// Get or create the client ID if not already initialized
|
|
102
|
-
if (uniqueUserId === 'unknown') {
|
|
103
|
-
uniqueUserId = await configManager.getOrCreateClientId();
|
|
104
|
-
}
|
|
105
|
-
// Get current client information for all events
|
|
106
|
-
// For remote calls, attribute to the originating remote client (carried on
|
|
107
|
-
// the tool call) instead of the device's local currentClient.
|
|
108
|
-
const effectiveClient = currentCallIsRemote && currentRemoteClient ? currentRemoteClient : currentClient;
|
|
109
|
-
let clientContext = {};
|
|
110
|
-
if (effectiveClient) {
|
|
111
|
-
clientContext = {
|
|
112
|
-
client_name: effectiveClient.name,
|
|
113
|
-
client_version: effectiveClient.version,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
// Track if user saw onboarding page
|
|
117
|
-
const sawOnboardingPage = await configManager.getValue('sawOnboardingPage');
|
|
118
|
-
if (sawOnboardingPage !== undefined) {
|
|
119
|
-
clientContext = { ...clientContext, saw_onboarding_page: sawOnboardingPage };
|
|
120
|
-
}
|
|
121
|
-
// Create a deep copy of properties to avoid modifying the original objects
|
|
122
|
-
// This ensures we don't alter error objects that are also returned to the AI
|
|
123
|
-
let sanitizedProperties;
|
|
124
|
-
try {
|
|
125
|
-
sanitizedProperties = properties ? JSON.parse(JSON.stringify(properties)) : {};
|
|
126
|
-
}
|
|
127
|
-
catch (e) {
|
|
128
|
-
sanitizedProperties = {};
|
|
129
|
-
}
|
|
130
|
-
// Sanitize error objects if present
|
|
131
|
-
if (sanitizedProperties.error) {
|
|
132
|
-
// Handle different types of error objects
|
|
133
|
-
if (typeof sanitizedProperties.error === 'object' && sanitizedProperties.error !== null) {
|
|
134
|
-
const sanitized = sanitizeError(sanitizedProperties.error);
|
|
135
|
-
sanitizedProperties.error = sanitized.message;
|
|
136
|
-
if (sanitized.code) {
|
|
137
|
-
sanitizedProperties.errorCode = sanitized.code;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
else if (typeof sanitizedProperties.error === 'string') {
|
|
141
|
-
sanitizedProperties.error = sanitizeError(sanitizedProperties.error).message;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// Remove any properties that might contain paths
|
|
145
|
-
const sensitiveKeys = ['path', 'filePath', 'directory', 'file_path', 'sourcePath', 'destinationPath', 'fullPath', 'rootPath'];
|
|
146
|
-
for (const key of Object.keys(sanitizedProperties)) {
|
|
147
|
-
const lowerKey = key.toLowerCase();
|
|
148
|
-
if (sensitiveKeys.some(sensitiveKey => lowerKey.includes(sensitiveKey)) &&
|
|
149
|
-
lowerKey !== 'fileextension') { // keep fileExtension as it's safe
|
|
150
|
-
delete sanitizedProperties[key];
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// Is MCP installed with DXT
|
|
154
|
-
let isDXT = 'false';
|
|
155
|
-
if (process.env.MCP_DXT) {
|
|
156
|
-
isDXT = 'true';
|
|
157
|
-
}
|
|
158
|
-
// Is MCP running in a container - use robust detection
|
|
159
|
-
const { getSystemInfo } = await import('./system-info.js');
|
|
160
|
-
const systemInfo = getSystemInfo();
|
|
161
|
-
const isContainer = systemInfo.docker.isContainer ? 'true' : 'false';
|
|
162
|
-
const containerType = systemInfo.docker.containerType || 'none';
|
|
163
|
-
const orchestrator = systemInfo.docker.orchestrator || 'none';
|
|
164
|
-
// Add container metadata (with privacy considerations)
|
|
165
|
-
let containerName = 'none';
|
|
166
|
-
let containerImage = 'none';
|
|
167
|
-
if (systemInfo.docker.isContainer && systemInfo.docker.containerEnvironment) {
|
|
168
|
-
const env = systemInfo.docker.containerEnvironment;
|
|
169
|
-
// Container name - sanitize to remove potentially sensitive info
|
|
170
|
-
if (env.containerName) {
|
|
171
|
-
// Keep only alphanumeric chars, dashes, and underscores
|
|
172
|
-
// Remove random IDs and UUIDs for privacy
|
|
173
|
-
containerName = env.containerName
|
|
174
|
-
.replace(/[0-9a-f]{8,}/gi, 'ID') // Replace long hex strings with 'ID'
|
|
175
|
-
.replace(/[0-9]{8,}/g, 'ID') // Replace long numeric IDs with 'ID'
|
|
176
|
-
.substring(0, 50); // Limit length
|
|
177
|
-
}
|
|
178
|
-
// Docker image - sanitize registry info for privacy
|
|
179
|
-
if (env.dockerImage) {
|
|
180
|
-
// Remove registry URLs and keep just image:tag format
|
|
181
|
-
containerImage = env.dockerImage
|
|
182
|
-
.replace(/^[^/]+\/[^/]+\//, '') // Remove registry.com/namespace/ prefix
|
|
183
|
-
.replace(/^[^/]+\//, '') // Remove simple registry.com/ prefix
|
|
184
|
-
.replace(/@sha256:.*$/, '') // Remove digest hashes
|
|
185
|
-
.substring(0, 100); // Limit length
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// Detect if we're running through Smithery at runtime
|
|
189
|
-
let runtimeSource = 'unknown';
|
|
190
|
-
const processArgs = process.argv.join(' ');
|
|
191
|
-
try {
|
|
192
|
-
if (processArgs.includes('@smithery/cli') || processArgs.includes('smithery')) {
|
|
193
|
-
runtimeSource = 'smithery-runtime';
|
|
194
|
-
}
|
|
195
|
-
else if (processArgs.includes('npx')) {
|
|
196
|
-
runtimeSource = 'npx-runtime';
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
runtimeSource = 'direct-runtime';
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
// Ignore detection errors
|
|
204
|
-
}
|
|
205
|
-
// Prepare standard properties
|
|
206
|
-
const baseProperties = {
|
|
207
|
-
timestamp: new Date().toISOString(),
|
|
208
|
-
platform: platform(),
|
|
209
|
-
isContainer,
|
|
210
|
-
containerType,
|
|
211
|
-
orchestrator,
|
|
212
|
-
containerName,
|
|
213
|
-
containerImage,
|
|
214
|
-
runtimeSource,
|
|
215
|
-
isDXT,
|
|
216
|
-
app_version: VERSION,
|
|
217
|
-
engagement_time_msec: "100"
|
|
218
|
-
};
|
|
219
|
-
// Combine with sanitized properties and client context
|
|
220
|
-
const eventProperties = {
|
|
221
|
-
...baseProperties,
|
|
222
|
-
...clientContext,
|
|
223
|
-
// Attribute events to the remote path when the in-flight tool call
|
|
224
|
-
// came from a remote device. Placed before sanitizedProperties so an
|
|
225
|
-
// explicit `remote` passed by the caller (e.g. captureRemote) wins.
|
|
226
|
-
...(currentCallIsRemote ? { remote: String(true) } : {}),
|
|
227
|
-
...sanitizedProperties
|
|
228
|
-
};
|
|
229
|
-
// Prepare telemetry payload
|
|
230
|
-
const payload = {
|
|
231
|
-
client_id: uniqueUserId,
|
|
232
|
-
non_personalized_ads: false,
|
|
233
|
-
timestamp_micros: Date.now() * 1000,
|
|
234
|
-
events: [{
|
|
235
|
-
name: event,
|
|
236
|
-
params: eventProperties
|
|
237
|
-
}]
|
|
238
|
-
};
|
|
239
|
-
// Send data to telemetry endpoint
|
|
240
|
-
const postData = JSON.stringify(payload);
|
|
241
|
-
const options = {
|
|
242
|
-
method: 'POST',
|
|
243
|
-
headers: {
|
|
244
|
-
'Content-Type': 'application/json',
|
|
245
|
-
'Content-Length': Buffer.byteLength(postData)
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
const req = https.request(captureURL, options, (res) => {
|
|
249
|
-
// Response handling (optional)
|
|
250
|
-
let data = '';
|
|
251
|
-
res.on('data', (chunk) => {
|
|
252
|
-
data += chunk;
|
|
253
|
-
});
|
|
254
|
-
res.on('end', () => {
|
|
255
|
-
const success = res.statusCode === 200 || res.statusCode === 204;
|
|
256
|
-
if (!success) {
|
|
257
|
-
// Optional debug logging
|
|
258
|
-
// console.debug(`Telemetry tracking error: ${res.statusCode} ${data}`);
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
req.on('error', (error) => {
|
|
263
|
-
// Silently fail - we don't want analytics issues to break functionality
|
|
264
|
-
});
|
|
265
|
-
// Set timeout to prevent blocking the app
|
|
266
|
-
req.setTimeout(3000, () => {
|
|
267
|
-
req.destroy();
|
|
268
|
-
});
|
|
269
|
-
// Send data
|
|
270
|
-
req.write(postData);
|
|
271
|
-
req.end();
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
// Silently fail - we don't want analytics issues to break functionality
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
/**
|
|
278
|
-
* Build the standard event properties used by the telemetry proxy.
|
|
279
|
-
* Extracted from captureBase so both paths get identical data.
|
|
280
|
-
*/
|
|
281
|
-
const buildEventProperties = async (properties) => {
|
|
282
|
-
if (uniqueUserId === 'unknown') {
|
|
283
|
-
uniqueUserId = await configManager.getOrCreateClientId();
|
|
284
|
-
}
|
|
285
|
-
// For remote calls, attribute to the originating remote client (carried on
|
|
286
|
-
// the tool call) instead of the device's local currentClient.
|
|
287
|
-
const effectiveClient = currentCallIsRemote && currentRemoteClient ? currentRemoteClient : currentClient;
|
|
288
|
-
let clientContext = {};
|
|
289
|
-
if (effectiveClient) {
|
|
290
|
-
clientContext = {
|
|
291
|
-
client_name: effectiveClient.name,
|
|
292
|
-
client_version: effectiveClient.version,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
const sawOnboardingPage = await configManager.getValue('sawOnboardingPage');
|
|
296
|
-
if (sawOnboardingPage !== undefined) {
|
|
297
|
-
clientContext.saw_onboarding_page = sawOnboardingPage;
|
|
298
|
-
}
|
|
299
|
-
let sanitizedProperties;
|
|
300
|
-
try {
|
|
301
|
-
sanitizedProperties = properties ? JSON.parse(JSON.stringify(properties)) : {};
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
sanitizedProperties = {};
|
|
305
|
-
}
|
|
306
|
-
if (sanitizedProperties.error) {
|
|
307
|
-
if (typeof sanitizedProperties.error === 'object' && sanitizedProperties.error !== null) {
|
|
308
|
-
const sanitized = sanitizeError(sanitizedProperties.error);
|
|
309
|
-
sanitizedProperties.error = sanitized.message;
|
|
310
|
-
if (sanitized.code)
|
|
311
|
-
sanitizedProperties.errorCode = sanitized.code;
|
|
312
|
-
}
|
|
313
|
-
else if (typeof sanitizedProperties.error === 'string') {
|
|
314
|
-
sanitizedProperties.error = sanitizeError(sanitizedProperties.error).message;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
const sensitiveKeys = ['path', 'filePath', 'directory', 'file_path', 'sourcePath', 'destinationPath', 'fullPath', 'rootPath'];
|
|
318
|
-
for (const key of Object.keys(sanitizedProperties)) {
|
|
319
|
-
const lowerKey = key.toLowerCase();
|
|
320
|
-
if (sensitiveKeys.some(sk => lowerKey.includes(sk)) && lowerKey !== 'fileextension') {
|
|
321
|
-
delete sanitizedProperties[key];
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
let isDXT = 'false';
|
|
325
|
-
if (process.env.MCP_DXT)
|
|
326
|
-
isDXT = 'true';
|
|
327
|
-
const { getSystemInfo } = await import('./system-info.js');
|
|
328
|
-
const systemInfo = getSystemInfo();
|
|
329
|
-
const isContainer = systemInfo.docker.isContainer ? 'true' : 'false';
|
|
330
|
-
const containerType = systemInfo.docker.containerType || 'none';
|
|
331
|
-
const orchestrator = systemInfo.docker.orchestrator || 'none';
|
|
332
|
-
let containerName = 'none';
|
|
333
|
-
let containerImage = 'none';
|
|
334
|
-
if (systemInfo.docker.isContainer && systemInfo.docker.containerEnvironment) {
|
|
335
|
-
const env = systemInfo.docker.containerEnvironment;
|
|
336
|
-
if (env.containerName) {
|
|
337
|
-
containerName = env.containerName
|
|
338
|
-
.replace(/[0-9a-f]{8,}/gi, 'ID')
|
|
339
|
-
.replace(/[0-9]{8,}/g, 'ID')
|
|
340
|
-
.substring(0, 50);
|
|
341
|
-
}
|
|
342
|
-
if (env.dockerImage) {
|
|
343
|
-
containerImage = env.dockerImage
|
|
344
|
-
.replace(/^[^/]+\/[^/]+\//, '')
|
|
345
|
-
.replace(/^[^/]+\//, '')
|
|
346
|
-
.replace(/@sha256:.*$/, '')
|
|
347
|
-
.substring(0, 100);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
let runtimeSource = 'unknown';
|
|
351
|
-
try {
|
|
352
|
-
const processArgs = process.argv.join(' ');
|
|
353
|
-
if (processArgs.includes('@smithery/cli') || processArgs.includes('smithery')) {
|
|
354
|
-
runtimeSource = 'smithery-runtime';
|
|
355
|
-
}
|
|
356
|
-
else if (processArgs.includes('npx')) {
|
|
357
|
-
runtimeSource = 'npx-runtime';
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
runtimeSource = 'direct-runtime';
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
catch { }
|
|
364
|
-
return {
|
|
365
|
-
timestamp: new Date().toISOString(),
|
|
366
|
-
platform: platform(),
|
|
367
|
-
isContainer,
|
|
368
|
-
containerType,
|
|
369
|
-
orchestrator,
|
|
370
|
-
containerName,
|
|
371
|
-
containerImage,
|
|
372
|
-
runtimeSource,
|
|
373
|
-
isDXT,
|
|
374
|
-
app_version: VERSION,
|
|
375
|
-
engagement_time_msec: "100",
|
|
376
|
-
...clientContext,
|
|
377
|
-
// Attribute events to the remote path when the in-flight tool call
|
|
378
|
-
// came from a remote device. Placed before sanitizedProperties so an
|
|
379
|
-
// explicit `remote` passed by the caller (e.g. captureRemote) wins.
|
|
380
|
-
...(currentCallIsRemote ? { remote: String(true) } : {}),
|
|
381
|
-
...sanitizedProperties,
|
|
382
|
-
};
|
|
383
|
-
};
|
|
384
|
-
/**
|
|
385
|
-
* Send event to the telemetry proxy (direct BigQuery ingestion).
|
|
386
|
-
* Uses the custom domain first and retries the generated Cloud Run URL on failure.
|
|
387
|
-
*/
|
|
388
|
-
const sendToTelemetryProxy = async (event, eventProperties) => {
|
|
389
|
-
try {
|
|
390
|
-
if (isTelemetryDisabledByEnv())
|
|
391
|
-
return;
|
|
392
|
-
const telemetryEnabled = await configManager.getValue('telemetryEnabled');
|
|
393
|
-
if (isTelemetryDisabledValue(telemetryEnabled))
|
|
394
|
-
return;
|
|
395
|
-
const payload = JSON.stringify({
|
|
396
|
-
client_id: uniqueUserId,
|
|
397
|
-
timestamp_micros: Date.now() * 1000,
|
|
398
|
-
events: [{
|
|
399
|
-
name: event,
|
|
400
|
-
params: eventProperties
|
|
401
|
-
}]
|
|
402
|
-
});
|
|
403
|
-
const sent = await postTelemetryPayload(TELEMETRY_PROXY_URL, payload);
|
|
404
|
-
if (!sent) {
|
|
405
|
-
await postTelemetryPayload(TELEMETRY_PROXY_FALLBACK_URL, payload);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
catch {
|
|
409
|
-
// Silent fail — telemetry should never break functionality
|
|
410
|
-
}
|
|
411
|
-
};
|
|
412
|
-
const postTelemetryPayload = async (endpoint, payload) => {
|
|
413
|
-
return await new Promise((resolve) => {
|
|
414
|
-
const url = new URL(endpoint);
|
|
415
|
-
const options = {
|
|
416
|
-
hostname: url.hostname,
|
|
417
|
-
port: 443,
|
|
418
|
-
path: url.pathname,
|
|
419
|
-
method: 'POST',
|
|
420
|
-
headers: {
|
|
421
|
-
'Content-Type': 'application/json',
|
|
422
|
-
'Content-Length': Buffer.byteLength(payload)
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
const req = https.request(options, (res) => {
|
|
426
|
-
res.resume();
|
|
427
|
-
res.on('end', () => resolve(res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 300));
|
|
428
|
-
});
|
|
429
|
-
req.on('error', () => resolve(false));
|
|
430
|
-
req.setTimeout(3000, () => {
|
|
431
|
-
req.destroy();
|
|
432
|
-
resolve(false);
|
|
433
|
-
});
|
|
434
|
-
req.write(payload);
|
|
435
|
-
req.end();
|
|
436
|
-
});
|
|
437
|
-
};
|
|
438
|
-
// TODO(behavior): capture() is now fire-and-forget — every `await capture(...)`
|
|
439
|
-
// call site resolves before the network send completes. Fine for the long-running
|
|
440
|
-
// MCP server, but events fired right before process exit (e.g. opt-out, feedback)
|
|
441
|
-
// can be silently dropped. If we need delivery guarantees on short-lived paths,
|
|
442
|
-
// expose an awaitable variant or flush-before-exit hook.
|
|
443
|
-
export const capture = async (event, properties) => {
|
|
444
|
-
// Tool calls fired programmatically by the widget UIs must produce zero
|
|
445
|
-
// telemetry — drop every event raised while serving one.
|
|
446
|
-
if (isInsideUiOriginCall() || isTelemetryDisabledByEnv()) {
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
void (async () => {
|
|
450
|
-
try {
|
|
451
|
-
const eventProperties = await buildEventProperties(properties);
|
|
452
|
-
await sendToTelemetryProxy(event, eventProperties);
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
// Silent fail — telemetry should never break functionality
|
|
456
|
-
}
|
|
457
|
-
})();
|
|
458
|
-
};
|
|
33
|
+
export const captureBase = async (_captureURL, _event, _properties) => { };
|
|
34
|
+
export const capture = async (_event, _properties) => { };
|
|
459
35
|
export const capture_call_tool = capture;
|
|
460
36
|
export const capture_ui_event = capture;
|
|
461
|
-
|
|
462
|
-
* Wrapper for capture() that automatically adds remote flag for device telemetry
|
|
463
|
-
* Also adds additional privacy filtering to remove sensitive identity information
|
|
464
|
-
* @param event Event name
|
|
465
|
-
* @param properties Optional event properties
|
|
466
|
-
*/
|
|
467
|
-
export const captureRemote = async (event, properties) => {
|
|
468
|
-
// Create a copy of properties to avoid mutating the original
|
|
469
|
-
const sanitizedProps = properties ? { ...properties } : {};
|
|
470
|
-
// Remove sensitive identity keys specific to remote devices
|
|
471
|
-
const sensitiveIdentityKeys = ['deviceId', 'userId', 'email', 'user_id', 'device_id', 'user_email'];
|
|
472
|
-
for (const key of sensitiveIdentityKeys) {
|
|
473
|
-
if (key in sanitizedProps) {
|
|
474
|
-
delete sanitizedProps[key];
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return await capture(event, {
|
|
478
|
-
...sanitizedProps,
|
|
479
|
-
remote: String(true)
|
|
480
|
-
});
|
|
481
|
-
};
|
|
37
|
+
export const captureRemote = async (_event, _properties) => { };
|
|
@@ -1,58 +1,11 @@
|
|
|
1
1
|
declare class FeatureFlagManager {
|
|
2
2
|
private flags;
|
|
3
|
-
private lastFetch;
|
|
4
|
-
private cachePath;
|
|
5
|
-
private cacheMaxAge;
|
|
6
|
-
private flagUrl;
|
|
7
|
-
private refreshInterval;
|
|
8
|
-
private freshFetchPromise;
|
|
9
|
-
private resolveFreshFetch;
|
|
10
|
-
private loadedFromCache;
|
|
11
|
-
constructor();
|
|
12
|
-
/**
|
|
13
|
-
* Initialize - load from cache and start background refresh
|
|
14
|
-
*/
|
|
15
3
|
initialize(): Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* Get a flag value
|
|
18
|
-
*/
|
|
19
4
|
get(flagName: string, defaultValue?: any): any;
|
|
20
|
-
/**
|
|
21
|
-
* Get all flags for debugging
|
|
22
|
-
*/
|
|
23
5
|
getAll(): Record<string, any>;
|
|
24
|
-
/**
|
|
25
|
-
* Manually refresh flags immediately (for testing)
|
|
26
|
-
*/
|
|
27
6
|
refresh(): Promise<boolean>;
|
|
28
|
-
/**
|
|
29
|
-
* Check if flags were loaded from cache (vs fresh fetch)
|
|
30
|
-
*/
|
|
31
7
|
wasLoadedFromCache(): boolean;
|
|
32
|
-
/**
|
|
33
|
-
* Wait for fresh flags to be fetched from network.
|
|
34
|
-
* Use this when you need to ensure flags are loaded before making decisions
|
|
35
|
-
* (e.g., A/B test assignments for new users who don't have a cache yet)
|
|
36
|
-
*
|
|
37
|
-
* Has a hard timeout to prevent blocking MCP startup if the fetch hangs.
|
|
38
|
-
* See: https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/465
|
|
39
|
-
*/
|
|
40
8
|
waitForFreshFlags(): Promise<void>;
|
|
41
|
-
/**
|
|
42
|
-
* Load flags from local cache
|
|
43
|
-
*/
|
|
44
|
-
private loadFromCache;
|
|
45
|
-
/**
|
|
46
|
-
* Fetch flags from remote URL
|
|
47
|
-
*/
|
|
48
|
-
private fetchFlags;
|
|
49
|
-
/**
|
|
50
|
-
* Save flags to local cache
|
|
51
|
-
*/
|
|
52
|
-
private saveToCache;
|
|
53
|
-
/**
|
|
54
|
-
* Cleanup on shutdown
|
|
55
|
-
*/
|
|
56
9
|
destroy(): void;
|
|
57
10
|
}
|
|
58
11
|
export declare const featureFlagManager: FeatureFlagManager;
|