@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.
Files changed (45) hide show
  1. package/README.md +7 -71
  2. package/dist/bootstrap.js +1 -1
  3. package/dist/config-manager.js +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/custom-stdio.js +1 -2
  6. package/dist/data/onboarding-prompts.json +22 -11
  7. package/dist/device/device-state.d.ts +0 -5
  8. package/dist/device/device-state.js +0 -84
  9. package/dist/device/device.d.ts +1 -1
  10. package/dist/device/device.js +8 -7
  11. package/dist/device/execution-engine.d.ts +10 -1
  12. package/dist/device/execution-engine.js +81 -128
  13. package/dist/device/gateway-channel.d.ts +6 -0
  14. package/dist/device/gateway-channel.js +16 -1
  15. package/dist/device/gateway-tool-adapter.d.ts +3 -3
  16. package/dist/device/gateway-tool-adapter.js +12 -11
  17. package/dist/device/linux-service.d.ts +4 -0
  18. package/dist/device/linux-service.js +10 -3
  19. package/dist/device/runtime-owner.js +2 -2
  20. package/dist/device/self-update.js +3 -1
  21. package/dist/device/windows-service.d.ts +3 -4
  22. package/dist/device/windows-service.js +5 -44
  23. package/dist/npm-scripts/remote.js +0 -2
  24. package/dist/server.js +9 -99
  25. package/dist/terminal-manager.js +1 -1
  26. package/dist/tools/config.d.ts +1 -1
  27. package/dist/tools/filesystem.js +3 -3
  28. package/dist/tools/pdf/markdown.js +1 -1
  29. package/dist/tools/schemas.d.ts +2 -3
  30. package/dist/tools/schemas.js +0 -10
  31. package/dist/ui/config-editor/index.html +1 -1
  32. package/dist/ui/file-preview/index.html +1 -1
  33. package/dist/ui/file-preview/styles.css +2 -2
  34. package/dist/utils/capture.d.ts +5 -28
  35. package/dist/utils/capture.js +11 -455
  36. package/dist/utils/feature-flags.d.ts +0 -47
  37. package/dist/utils/feature-flags.js +5 -199
  38. package/dist/utils/files/docx.d.ts +2 -2
  39. package/dist/utils/files/docx.js +2 -2
  40. package/dist/utils/usageTracker.js +0 -24
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +5 -6
  44. package/dist/tools/feedback.d.ts +0 -8
  45. package/dist/tools/feedback.js +0 -132
@@ -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.DESKTOP_COMMANDER_DISABLE_TELEMETRY;
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 = undefined;
17
+ let errorCode;
58
18
  if (error instanceof Error) {
59
- // Extract just the error name and message without stack trace
60
- errorMessage = error.name + ': ' + error.message;
61
- // Extract error code if available (common in Node.js errors)
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
- // Remove any file paths using regex
73
- // This pattern matches common path formats including Windows and Unix-style paths
74
- errorMessage = errorMessage.replace(/(?:\/|\\)[\w\d_.-\/\\]+/g, '[PATH]');
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
- * Send an event to telemetry
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;