@wonderwhy-er/desktop-commander 0.2.24 → 0.2.26

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.
@@ -20,6 +20,7 @@ declare class ConfigManager {
20
20
  private configPath;
21
21
  private config;
22
22
  private initialized;
23
+ private _isFirstRun;
23
24
  constructor();
24
25
  /**
25
26
  * Initialize configuration - load from disk or create default
@@ -57,6 +58,10 @@ declare class ConfigManager {
57
58
  * Reset configuration to defaults
58
59
  */
59
60
  resetConfig(): Promise<ServerConfig>;
61
+ /**
62
+ * Check if this is the first run (config file was just created)
63
+ */
64
+ isFirstRun(): boolean;
60
65
  }
61
66
  export declare const configManager: ConfigManager;
62
67
  export {};
@@ -12,6 +12,7 @@ class ConfigManager {
12
12
  constructor() {
13
13
  this.config = {};
14
14
  this.initialized = false;
15
+ this._isFirstRun = false; // Track if this is the first run (config was just created)
15
16
  // Get user's home directory
16
17
  // Define config directory and file paths
17
18
  this.configPath = CONFIG_FILE;
@@ -34,10 +35,12 @@ class ConfigManager {
34
35
  // Load existing config
35
36
  const configData = await fs.readFile(this.configPath, 'utf8');
36
37
  this.config = JSON.parse(configData);
38
+ this._isFirstRun = false;
37
39
  }
38
40
  catch (error) {
39
41
  // Config file doesn't exist, create default
40
42
  this.config = this.getDefaultConfig();
43
+ this._isFirstRun = true; // This is a first run!
41
44
  await this.saveConfig();
42
45
  }
43
46
  this.config['version'] = VERSION;
@@ -184,6 +187,12 @@ class ConfigManager {
184
187
  await this.saveConfig();
185
188
  return { ...this.config };
186
189
  }
190
+ /**
191
+ * Check if this is the first run (config file was just created)
192
+ */
193
+ isFirstRun() {
194
+ return this._isFirstRun;
195
+ }
187
196
  }
188
197
  // Export singleton instance
189
198
  export const configManager = new ConfigManager();
package/dist/server.d.ts CHANGED
@@ -7,6 +7,9 @@ export declare const server: Server<{
7
7
  _meta?: {
8
8
  [x: string]: unknown;
9
9
  progressToken?: string | number | undefined;
10
+ "io.modelcontextprotocol/related-task"?: {
11
+ taskId: string;
12
+ } | undefined;
10
13
  } | undefined;
11
14
  } | undefined;
12
15
  }, {
@@ -15,12 +18,20 @@ export declare const server: Server<{
15
18
  [x: string]: unknown;
16
19
  _meta?: {
17
20
  [x: string]: unknown;
21
+ progressToken?: string | number | undefined;
22
+ "io.modelcontextprotocol/related-task"?: {
23
+ taskId: string;
24
+ } | undefined;
18
25
  } | undefined;
19
26
  } | undefined;
20
27
  }, {
21
28
  [x: string]: unknown;
22
29
  _meta?: {
23
30
  [x: string]: unknown;
31
+ progressToken?: string | number | undefined;
32
+ "io.modelcontextprotocol/related-task"?: {
33
+ taskId: string;
34
+ } | undefined;
24
35
  } | undefined;
25
36
  }>;
26
37
  declare let currentClient: {
package/dist/server.js CHANGED
@@ -17,6 +17,7 @@ import { trackToolCall } from './utils/trackTools.js';
17
17
  import { usageTracker } from './utils/usageTracker.js';
18
18
  import { processDockerPrompt } from './utils/dockerPrompt.js';
19
19
  import { toolHistory } from './utils/toolHistory.js';
20
+ import { handleWelcomePageOnboarding } from './utils/welcome-onboarding.js';
20
21
  import { VERSION } from './version.js';
21
22
  import { capture, capture_call_tool } from "./utils/capture.js";
22
23
  import { logToStderr, logger } from './utils/logger.js';
@@ -77,6 +78,10 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
77
78
  }
78
79
  // Defer client connection message until after initialization
79
80
  deferLog('info', `Client connected: ${currentClient.name} v${currentClient.version}`);
81
+ // Welcome page for new claude-ai users (A/B test controlled)
82
+ if (currentClient.name === 'claude-ai' && !global.disableOnboarding) {
83
+ await handleWelcomePageOnboarding();
84
+ }
80
85
  }
81
86
  capture('run_server_mcp_initialized');
82
87
  // Return standard initialization response
@@ -360,6 +365,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
360
365
  ${PATH_GUIDANCE}
361
366
  ${CMD_PREFIX_DESCRIPTION}`,
362
367
  inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema),
368
+ annotations: {
369
+ title: "Create Directory",
370
+ readOnlyHint: false,
371
+ destructiveHint: false,
372
+ },
363
373
  },
364
374
  {
365
375
  name: "list_directory",
@@ -499,6 +509,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
499
509
  ${PATH_GUIDANCE}
500
510
  ${CMD_PREFIX_DESCRIPTION}`,
501
511
  inputSchema: zodToJsonSchema(StartSearchArgsSchema),
512
+ annotations: {
513
+ title: "Start Search",
514
+ readOnlyHint: true,
515
+ },
502
516
  },
503
517
  {
504
518
  name: "get_more_search_results",
@@ -544,6 +558,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
544
558
 
545
559
  ${CMD_PREFIX_DESCRIPTION}`,
546
560
  inputSchema: zodToJsonSchema(StopSearchArgsSchema),
561
+ annotations: {
562
+ title: "Stop Search",
563
+ readOnlyHint: false,
564
+ destructiveHint: false,
565
+ },
547
566
  },
548
567
  {
549
568
  name: "list_searches",
@@ -958,6 +977,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
958
977
 
959
978
  ${CMD_PREFIX_DESCRIPTION}`,
960
979
  inputSchema: zodToJsonSchema(GiveFeedbackArgsSchema),
980
+ annotations: {
981
+ title: "Give Feedback",
982
+ readOnlyHint: false,
983
+ openWorldHint: true,
984
+ },
961
985
  },
962
986
  {
963
987
  name: "get_prompts",
@@ -985,6 +1009,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
985
1009
 
986
1010
  ${CMD_PREFIX_DESCRIPTION}`,
987
1011
  inputSchema: zodToJsonSchema(GetPromptsArgsSchema),
1012
+ annotations: {
1013
+ title: "Get Prompts",
1014
+ readOnlyHint: true,
1015
+ },
988
1016
  }
989
1017
  ];
990
1018
  // Filter tools based on current client
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Check if a feature (variant name) is enabled for current user
3
+ */
4
+ export declare function hasFeature(featureName: string): Promise<boolean>;
5
+ /**
6
+ * Get all A/B test assignments for analytics (reads from config)
7
+ */
8
+ export declare function getABTestAssignments(): Promise<Record<string, string>>;
@@ -0,0 +1,76 @@
1
+ import { configManager } from '../config-manager.js';
2
+ import { featureFlagManager } from './feature-flags.js';
3
+ // Cache for variant assignments (loaded once per session)
4
+ const variantCache = {};
5
+ /**
6
+ * Get experiments config from feature flags
7
+ */
8
+ function getExperiments() {
9
+ return featureFlagManager.get('experiments', {});
10
+ }
11
+ /**
12
+ * Get user's variant for an experiment (cached, deterministic)
13
+ */
14
+ async function getVariant(experimentName) {
15
+ const experiments = getExperiments();
16
+ const experiment = experiments[experimentName];
17
+ if (!experiment?.variants?.length)
18
+ return null;
19
+ // Check cache
20
+ if (variantCache[experimentName]) {
21
+ return variantCache[experimentName];
22
+ }
23
+ // Check persisted assignment
24
+ const configKey = `abTest_${experimentName}`;
25
+ const existing = await configManager.getValue(configKey);
26
+ if (existing && experiment.variants.includes(existing)) {
27
+ variantCache[experimentName] = existing;
28
+ return existing;
29
+ }
30
+ // New assignment based on clientId
31
+ const clientId = await configManager.getValue('clientId') || '';
32
+ const hash = hashCode(clientId + experimentName);
33
+ const variantIndex = hash % experiment.variants.length;
34
+ const variant = experiment.variants[variantIndex];
35
+ await configManager.setValue(configKey, variant);
36
+ variantCache[experimentName] = variant;
37
+ return variant;
38
+ }
39
+ /**
40
+ * Check if a feature (variant name) is enabled for current user
41
+ */
42
+ export async function hasFeature(featureName) {
43
+ const experiments = getExperiments();
44
+ if (!experiments || typeof experiments !== 'object')
45
+ return false;
46
+ for (const [expName, experiment] of Object.entries(experiments)) {
47
+ if (experiment?.variants?.includes(featureName)) {
48
+ const variant = await getVariant(expName);
49
+ return variant === featureName;
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+ /**
55
+ * Get all A/B test assignments for analytics (reads from config)
56
+ */
57
+ export async function getABTestAssignments() {
58
+ const experiments = getExperiments();
59
+ const assignments = {};
60
+ for (const expName of Object.keys(experiments)) {
61
+ const configKey = `abTest_${expName}`;
62
+ const variant = await configManager.getValue(configKey);
63
+ if (variant) {
64
+ assignments[`ab_${expName}`] = variant;
65
+ }
66
+ }
67
+ return assignments;
68
+ }
69
+ function hashCode(str) {
70
+ let hash = 0;
71
+ for (let i = 0; i < str.length; i++) {
72
+ hash = ((hash << 5) - hash) + str.charCodeAt(i);
73
+ hash |= 0;
74
+ }
75
+ return Math.abs(hash);
76
+ }
@@ -86,6 +86,11 @@ export const captureBase = async (captureURL, event, properties) => {
86
86
  client_version: currentClient.version,
87
87
  };
88
88
  }
89
+ // Track if user saw onboarding page
90
+ const sawOnboardingPage = await configManager.getValue('sawOnboardingPage');
91
+ if (sawOnboardingPage !== undefined) {
92
+ clientContext = { ...clientContext, saw_onboarding_page: sawOnboardingPage };
93
+ }
89
94
  // Create a deep copy of properties to avoid modifying the original objects
90
95
  // This ensures we don't alter error objects that are also returned to the AI
91
96
  let sanitizedProperties;
@@ -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
  /**
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Open a URL in the default browser (cross-platform)
3
+ * Uses execFile/spawn with args array to avoid shell injection
4
+ */
5
+ export declare function openBrowser(url: string): Promise<void>;
6
+ /**
7
+ * Open the Desktop Commander welcome page
8
+ */
9
+ export declare function openWelcomePage(): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import { execFile, spawn } from 'child_process';
2
+ import os from 'os';
3
+ import { logToStderr } from './logger.js';
4
+ /**
5
+ * Open a URL in the default browser (cross-platform)
6
+ * Uses execFile/spawn with args array to avoid shell injection
7
+ */
8
+ export async function openBrowser(url) {
9
+ const platform = os.platform();
10
+ return new Promise((resolve, reject) => {
11
+ const callback = (error) => {
12
+ if (error) {
13
+ logToStderr('error', `Failed to open browser: ${error.message}`);
14
+ reject(error);
15
+ }
16
+ else {
17
+ logToStderr('info', `Opened browser to: ${url}`);
18
+ resolve();
19
+ }
20
+ };
21
+ switch (platform) {
22
+ case 'darwin':
23
+ execFile('open', [url], callback);
24
+ break;
25
+ case 'win32':
26
+ // Windows 'start' is a shell builtin, use spawn with shell but pass URL as separate arg
27
+ spawn('cmd', ['/c', 'start', '', url], { shell: false }).on('close', (code) => {
28
+ code === 0 ? resolve() : reject(new Error(`Exit code ${code}`));
29
+ });
30
+ break;
31
+ default:
32
+ execFile('xdg-open', [url], callback);
33
+ break;
34
+ }
35
+ });
36
+ }
37
+ /**
38
+ * Open the Desktop Commander welcome page
39
+ */
40
+ export async function openWelcomePage() {
41
+ const url = 'https://desktopcommander.app/welcome/';
42
+ await openBrowser(url);
43
+ }
@@ -325,6 +325,12 @@ class UsageTracker {
325
325
  * Check if user should see onboarding invitation - SIMPLE VERSION
326
326
  */
327
327
  async shouldShowOnboarding() {
328
+ // Check feature flag first (remote kill switch)
329
+ const { featureFlagManager } = await import('./feature-flags.js');
330
+ const onboardingEnabled = featureFlagManager.get('onboarding_injection', true);
331
+ if (!onboardingEnabled) {
332
+ return false;
333
+ }
328
334
  // Check if onboarding is disabled via command line argument
329
335
  if (global.disableOnboarding) {
330
336
  return false;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Handle welcome page display for new users (A/B test controlled)
3
+ *
4
+ * Only shows to:
5
+ * 1. New users (first run - config was just created)
6
+ * 2. Users in the 'showOnboardingPage' A/B variant
7
+ * 3. Haven't seen it yet
8
+ */
9
+ export declare function handleWelcomePageOnboarding(): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import { configManager } from '../config-manager.js';
2
+ import { hasFeature } from './ab-test.js';
3
+ import { featureFlagManager } from './feature-flags.js';
4
+ import { openWelcomePage } from './open-browser.js';
5
+ import { logToStderr } from './logger.js';
6
+ /**
7
+ * Handle welcome page display for new users (A/B test controlled)
8
+ *
9
+ * Only shows to:
10
+ * 1. New users (first run - config was just created)
11
+ * 2. Users in the 'showOnboardingPage' A/B variant
12
+ * 3. Haven't seen it yet
13
+ */
14
+ export async function handleWelcomePageOnboarding() {
15
+ // Only for brand new users (config just created)
16
+ if (!configManager.isFirstRun()) {
17
+ return;
18
+ }
19
+ // For new users, we need to wait for feature flags to load from network
20
+ // since they won't have a cache file yet. Without this, hasFeature() would
21
+ // return false (no experiments defined) and all new users go to control.
22
+ if (!featureFlagManager.wasLoadedFromCache()) {
23
+ logToStderr('debug', 'Waiting for feature flags to load...');
24
+ await featureFlagManager.waitForFreshFlags();
25
+ }
26
+ // Check A/B test assignment
27
+ const shouldShow = await hasFeature('showOnboardingPage');
28
+ if (!shouldShow) {
29
+ logToStderr('debug', 'Welcome page skipped (A/B: noOnboardingPage)');
30
+ return;
31
+ }
32
+ // Double-check not already shown (safety)
33
+ const alreadyShown = await configManager.getValue('sawOnboardingPage');
34
+ if (alreadyShown) {
35
+ return;
36
+ }
37
+ try {
38
+ await openWelcomePage();
39
+ await configManager.setValue('sawOnboardingPage', true);
40
+ logToStderr('info', 'Welcome page opened');
41
+ }
42
+ catch (e) {
43
+ logToStderr('warning', `Failed to open welcome page: ${e instanceof Error ? e.message : e}`);
44
+ }
45
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.24";
1
+ export declare const VERSION = "0.2.26";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.24';
1
+ export const VERSION = '0.2.26';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "mcpName": "io.github.wonderwhy-er/desktop-commander",
6
6
  "license": "MIT",
@@ -81,11 +81,11 @@
81
81
  "@opendocsg/pdf2md": "^0.2.2",
82
82
  "@vscode/ripgrep": "^1.15.9",
83
83
  "cross-fetch": "^4.1.0",
84
+ "exceljs": "^4.4.0",
84
85
  "fastest-levenshtein": "^1.0.16",
85
86
  "file-type": "^21.1.1",
86
87
  "glob": "^10.3.10",
87
88
  "isbinaryfile": "^5.0.4",
88
- "exceljs": "^4.4.0",
89
89
  "md-to-pdf": "^5.2.5",
90
90
  "pdf-lib": "^1.17.1",
91
91
  "remark": "^15.0.1",