@wonderwhy-er/desktop-commander 0.2.25 → 0.2.27

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.
@@ -62,6 +62,10 @@ declare class ConfigManager {
62
62
  * Check if this is the first run (config file was just created)
63
63
  */
64
64
  isFirstRun(): boolean;
65
+ /**
66
+ * Get or create a persistent client ID for analytics and A/B tests
67
+ */
68
+ getOrCreateClientId(): Promise<string>;
65
69
  }
66
70
  export declare const configManager: ConfigManager;
67
71
  export {};
@@ -193,6 +193,18 @@ class ConfigManager {
193
193
  isFirstRun() {
194
194
  return this._isFirstRun;
195
195
  }
196
+ /**
197
+ * Get or create a persistent client ID for analytics and A/B tests
198
+ */
199
+ async getOrCreateClientId() {
200
+ let clientId = await this.getValue('clientId');
201
+ if (!clientId) {
202
+ const { randomUUID } = await import('crypto');
203
+ clientId = randomUUID();
204
+ await this.setValue('clientId', clientId);
205
+ }
206
+ return clientId;
207
+ }
196
208
  }
197
209
  // Export singleton instance
198
210
  export const configManager = new ConfigManager();
@@ -28,7 +28,7 @@ async function getVariant(experimentName) {
28
28
  return existing;
29
29
  }
30
30
  // New assignment based on clientId
31
- const clientId = await configManager.getValue('clientId') || '';
31
+ const clientId = await configManager.getOrCreateClientId();
32
32
  const hash = hashCode(clientId + experimentName);
33
33
  const variantIndex = hash % experiment.variants.length;
34
34
  const variant = experiment.variants[variantIndex];
@@ -1,5 +1,4 @@
1
1
  import { platform } from 'os';
2
- import { randomUUID } from 'crypto';
3
2
  import * as https from 'https';
4
3
  import { configManager } from '../config-manager.js';
5
4
  import { currentClient } from '../server.js';
@@ -13,23 +12,6 @@ catch {
13
12
  }
14
13
  // Will be initialized when needed
15
14
  let uniqueUserId = 'unknown';
16
- // Function to get or create a persistent UUID
17
- async function getOrCreateUUID() {
18
- try {
19
- // Try to get the UUID from the config
20
- let clientId = await configManager.getValue('clientId');
21
- // If it doesn't exist, create a new one and save it
22
- if (!clientId) {
23
- clientId = randomUUID();
24
- await configManager.setValue('clientId', clientId);
25
- }
26
- return clientId;
27
- }
28
- catch (error) {
29
- // Fallback to a random UUID if config operations fail
30
- return randomUUID();
31
- }
32
- }
33
15
  /**
34
16
  * Sanitizes error objects to remove potentially sensitive information like file paths
35
17
  * @param error Error object or string to sanitize
@@ -76,7 +58,7 @@ export const captureBase = async (captureURL, event, properties) => {
76
58
  }
77
59
  // Get or create the client ID if not already initialized
78
60
  if (uniqueUserId === 'unknown') {
79
- uniqueUserId = await getOrCreateUUID();
61
+ uniqueUserId = await configManager.getOrCreateClientId();
80
62
  }
81
63
  // Get current client information for all events
82
64
  let clientContext = {};
@@ -5,6 +5,9 @@ declare class FeatureFlagManager {
5
5
  private cacheMaxAge;
6
6
  private flagUrl;
7
7
  private refreshInterval;
8
+ private freshFetchPromise;
9
+ private resolveFreshFetch;
10
+ private loadedFromCache;
8
11
  constructor();
9
12
  /**
10
13
  * Initialize - load from cache and start background refresh
@@ -22,6 +25,16 @@ declare class FeatureFlagManager {
22
25
  * Manually refresh flags immediately (for testing)
23
26
  */
24
27
  refresh(): Promise<boolean>;
28
+ /**
29
+ * Check if flags were loaded from cache (vs fresh fetch)
30
+ */
31
+ 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
+ waitForFreshFlags(): Promise<void>;
25
38
  /**
26
39
  * Load flags from local cache
27
40
  */
@@ -9,11 +9,19 @@ class FeatureFlagManager {
9
9
  this.lastFetch = 0;
10
10
  this.cacheMaxAge = 30 * 60 * 1000;
11
11
  this.refreshInterval = null;
12
+ // Track fresh fetch status for A/B tests that need network flags
13
+ this.freshFetchPromise = null;
14
+ this.resolveFreshFetch = null;
15
+ this.loadedFromCache = false;
12
16
  const configDir = path.dirname(CONFIG_FILE);
13
17
  this.cachePath = path.join(configDir, 'feature-flags.json');
14
18
  // Use production flags
15
19
  this.flagUrl = process.env.DC_FLAG_URL ||
16
20
  'https://desktopcommander.app/flags/v1/production.json';
21
+ // Set up promise for waiting on fresh fetch
22
+ this.freshFetchPromise = new Promise((resolve) => {
23
+ this.resolveFreshFetch = resolve;
24
+ });
17
25
  }
18
26
  /**
19
27
  * Initialize - load from cache and start background refresh
@@ -23,8 +31,17 @@ class FeatureFlagManager {
23
31
  // Load from cache immediately (non-blocking)
24
32
  await this.loadFromCache();
25
33
  // Fetch in background (don't block startup)
26
- this.fetchFlags().catch(err => {
34
+ this.fetchFlags().then(() => {
35
+ // Signal that fresh flags are now available
36
+ if (this.resolveFreshFetch) {
37
+ this.resolveFreshFetch();
38
+ }
39
+ }).catch(err => {
27
40
  logger.debug('Initial flag fetch failed:', err.message);
41
+ // Still resolve the promise so waiters don't hang forever
42
+ if (this.resolveFreshFetch) {
43
+ this.resolveFreshFetch();
44
+ }
28
45
  });
29
46
  // Start periodic refresh every 5 minutes
30
47
  this.refreshInterval = setInterval(() => {
@@ -66,6 +83,22 @@ class FeatureFlagManager {
66
83
  return false;
67
84
  }
68
85
  }
86
+ /**
87
+ * Check if flags were loaded from cache (vs fresh fetch)
88
+ */
89
+ wasLoadedFromCache() {
90
+ return this.loadedFromCache;
91
+ }
92
+ /**
93
+ * Wait for fresh flags to be fetched from network.
94
+ * Use this when you need to ensure flags are loaded before making decisions
95
+ * (e.g., A/B test assignments for new users who don't have a cache yet)
96
+ */
97
+ async waitForFreshFlags() {
98
+ if (this.freshFetchPromise) {
99
+ await this.freshFetchPromise;
100
+ }
101
+ }
69
102
  /**
70
103
  * Load flags from local cache
71
104
  */
@@ -73,6 +106,7 @@ class FeatureFlagManager {
73
106
  try {
74
107
  if (!existsSync(this.cachePath)) {
75
108
  logger.debug('No feature flag cache found');
109
+ this.loadedFromCache = false;
76
110
  return;
77
111
  }
78
112
  const data = await fs.readFile(this.cachePath, 'utf8');
@@ -80,11 +114,13 @@ class FeatureFlagManager {
80
114
  if (config.flags) {
81
115
  this.flags = config.flags;
82
116
  this.lastFetch = Date.now();
117
+ this.loadedFromCache = true;
83
118
  logger.debug(`Loaded ${Object.keys(this.flags).length} feature flags from cache`);
84
119
  }
85
120
  }
86
121
  catch (error) {
87
122
  logger.warning('Failed to load feature flags from cache:', error);
123
+ this.loadedFromCache = false;
88
124
  }
89
125
  }
90
126
  /**
@@ -1,7 +1,9 @@
1
1
  import { configManager } from '../config-manager.js';
2
2
  import { hasFeature } from './ab-test.js';
3
+ import { featureFlagManager } from './feature-flags.js';
3
4
  import { openWelcomePage } from './open-browser.js';
4
5
  import { logToStderr } from './logger.js';
6
+ import { capture } from './capture.js';
5
7
  /**
6
8
  * Handle welcome page display for new users (A/B test controlled)
7
9
  *
@@ -15,8 +17,22 @@ export async function handleWelcomePageOnboarding() {
15
17
  if (!configManager.isFirstRun()) {
16
18
  return;
17
19
  }
20
+ // Track that we have a first-run user attempting onboarding
21
+ const loadedFromCache = featureFlagManager.wasLoadedFromCache();
22
+ // For new users, we need to wait for feature flags to load from network
23
+ // since they won't have a cache file yet. Without this, hasFeature() would
24
+ // return false (no experiments defined) and all new users go to control.
25
+ if (!loadedFromCache) {
26
+ logToStderr('debug', 'Waiting for feature flags to load...');
27
+ await featureFlagManager.waitForFreshFlags();
28
+ }
18
29
  // Check A/B test assignment
19
30
  const shouldShow = await hasFeature('showOnboardingPage');
31
+ // Track the A/B decision
32
+ capture('server_welcome_page_ab_decision', {
33
+ variant: shouldShow ? 'treatment' : 'control',
34
+ loaded_from_cache: loadedFromCache
35
+ });
20
36
  if (!shouldShow) {
21
37
  logToStderr('debug', 'Welcome page skipped (A/B: noOnboardingPage)');
22
38
  return;
@@ -29,9 +45,11 @@ export async function handleWelcomePageOnboarding() {
29
45
  try {
30
46
  await openWelcomePage();
31
47
  await configManager.setValue('sawOnboardingPage', true);
48
+ capture('server_welcome_page_opened', { success: true });
32
49
  logToStderr('info', 'Welcome page opened');
33
50
  }
34
51
  catch (e) {
52
+ capture('server_welcome_page_opened', { success: false, error: e instanceof Error ? e.message : String(e) });
35
53
  logToStderr('warning', `Failed to open welcome page: ${e instanceof Error ? e.message : e}`);
36
54
  }
37
55
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.25";
1
+ export declare const VERSION = "0.2.27";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.25';
1
+ export const VERSION = '0.2.27';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "mcpName": "io.github.wonderwhy-er/desktop-commander",
6
6
  "license": "MIT",