@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.
- package/dist/config-manager.d.ts +5 -0
- package/dist/config-manager.js +9 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +28 -0
- package/dist/utils/ab-test.d.ts +8 -0
- package/dist/utils/ab-test.js +76 -0
- package/dist/utils/capture.js +5 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +43 -0
- package/dist/utils/usageTracker.js +6 -0
- package/dist/utils/welcome-onboarding.d.ts +9 -0
- package/dist/utils/welcome-onboarding.js +37 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/config-manager.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/config-manager.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/utils/capture.js
CHANGED
|
@@ -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.
|
|
1
|
+
export declare const VERSION = "0.2.25";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
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.
|
|
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",
|