@wonderwhy-er/desktop-commander 0.2.24 → 0.2.25

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;
@@ -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,37 @@
1
+ import { configManager } from '../config-manager.js';
2
+ import { hasFeature } from './ab-test.js';
3
+ import { openWelcomePage } from './open-browser.js';
4
+ import { logToStderr } from './logger.js';
5
+ /**
6
+ * Handle welcome page display for new users (A/B test controlled)
7
+ *
8
+ * Only shows to:
9
+ * 1. New users (first run - config was just created)
10
+ * 2. Users in the 'showOnboardingPage' A/B variant
11
+ * 3. Haven't seen it yet
12
+ */
13
+ export async function handleWelcomePageOnboarding() {
14
+ // Only for brand new users (config just created)
15
+ if (!configManager.isFirstRun()) {
16
+ return;
17
+ }
18
+ // Check A/B test assignment
19
+ const shouldShow = await hasFeature('showOnboardingPage');
20
+ if (!shouldShow) {
21
+ logToStderr('debug', 'Welcome page skipped (A/B: noOnboardingPage)');
22
+ return;
23
+ }
24
+ // Double-check not already shown (safety)
25
+ const alreadyShown = await configManager.getValue('sawOnboardingPage');
26
+ if (alreadyShown) {
27
+ return;
28
+ }
29
+ try {
30
+ await openWelcomePage();
31
+ await configManager.setValue('sawOnboardingPage', true);
32
+ logToStderr('info', 'Welcome page opened');
33
+ }
34
+ catch (e) {
35
+ logToStderr('warning', `Failed to open welcome page: ${e instanceof Error ? e.message : e}`);
36
+ }
37
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.24";
1
+ export declare const VERSION = "0.2.25";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.24';
1
+ export const VERSION = '0.2.25';
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.25",
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",