@wonderwhy-er/desktop-commander 0.2.8 → 0.2.9

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.
@@ -0,0 +1,753 @@
1
+ import { homedir, platform } from 'os';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { join } from 'path';
5
+ import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from 'fs';
6
+ import { fileURLToPath } from 'url';
7
+ import { dirname } from 'path';
8
+ import { exec } from "node:child_process";
9
+ import { version as nodeVersion } from 'process';
10
+ import * as https from 'https';
11
+ import { randomUUID } from 'crypto';
12
+
13
+ // Google Analytics configuration
14
+ const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L'; // Replace with your GA4 Measurement ID
15
+ const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A'; // Replace with your GA4 API Secret
16
+ const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
17
+
18
+ // Read clientId and telemetry settings from existing config
19
+ let uniqueUserId = 'unknown';
20
+ let telemetryEnabled = false; // Default to disabled for privacy
21
+
22
+ async function getConfigSettings() {
23
+ try {
24
+ const USER_HOME = homedir();
25
+ const CONFIG_DIR = path.join(USER_HOME, '.claude-server-commander');
26
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
27
+
28
+ if (existsSync(CONFIG_FILE)) {
29
+ const configData = readFileSync(CONFIG_FILE, 'utf8');
30
+ const config = JSON.parse(configData);
31
+
32
+ return {
33
+ clientId: config.clientId || randomUUID(),
34
+ telemetryEnabled: config.telemetryEnabled === true // Explicit check for true
35
+ };
36
+ }
37
+
38
+ // Fallback: generate new ID and default telemetry to false if config doesn't exist
39
+ return {
40
+ clientId: `unknown-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`,
41
+ telemetryEnabled: false
42
+ };
43
+ } catch (error) {
44
+ // Final fallback
45
+ return {
46
+ clientId: `random-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`,
47
+ telemetryEnabled: false
48
+ };
49
+ }
50
+ }
51
+
52
+ // Uninstall tracking
53
+ let uninstallSteps = [];
54
+ let uninstallStartTime = Date.now();
55
+
56
+ // Fix for Windows ESM path resolution
57
+ const __filename = fileURLToPath(import.meta.url);
58
+ const __dirname = dirname(__filename);
59
+
60
+ // Setup logging
61
+ const LOG_FILE = join(__dirname, 'setup.log');
62
+
63
+ function logToFile(message, isError = false) {
64
+ const timestamp = new Date().toISOString();
65
+ const logMessage = `${timestamp} - ${isError ? 'ERROR: ' : ''}${message}\n`;
66
+ try {
67
+ appendFileSync(LOG_FILE, logMessage);
68
+ const jsonOutput = {
69
+ type: isError ? 'error' : 'info',
70
+ timestamp,
71
+ message
72
+ };
73
+ process.stdout.write(`${message}\n`);
74
+ } catch (err) {
75
+ process.stderr.write(`${JSON.stringify({
76
+ type: 'error',
77
+ timestamp: new Date().toISOString(),
78
+ message: `Failed to write to log file: ${err.message}`
79
+ })}\n`);
80
+ }
81
+ }
82
+
83
+ // Function to get npm version
84
+ async function getNpmVersion() {
85
+ try {
86
+ return new Promise((resolve, reject) => {
87
+ exec('npm --version', (error, stdout, stderr) => {
88
+ if (error) {
89
+ resolve('unknown');
90
+ return;
91
+ }
92
+ resolve(stdout.trim());
93
+ });
94
+ });
95
+ } catch (error) {
96
+ return 'unknown';
97
+ }
98
+ }
99
+
100
+ // Get Desktop Commander version
101
+ const getVersion = async () => {
102
+ try {
103
+ if (process.env.npm_package_version) {
104
+ return process.env.npm_package_version;
105
+ }
106
+
107
+ const versionPath = join(__dirname, 'version.js');
108
+ if (existsSync(versionPath)) {
109
+ const { VERSION } = await import(versionPath);
110
+ return VERSION;
111
+ }
112
+
113
+ const packageJsonPath = join(__dirname, 'package.json');
114
+ if (existsSync(packageJsonPath)) {
115
+ const packageJsonContent = readFileSync(packageJsonPath, 'utf8');
116
+ const packageJson = JSON.parse(packageJsonContent);
117
+ if (packageJson.version) {
118
+ return packageJson.version;
119
+ }
120
+ }
121
+
122
+ return 'unknown';
123
+ } catch (error) {
124
+ return 'unknown';
125
+ }
126
+ };
127
+ // Function to detect shell environment
128
+ function detectShell() {
129
+ if (process.platform === 'win32') {
130
+ if (process.env.TERM_PROGRAM === 'vscode') return 'vscode-terminal';
131
+ if (process.env.WT_SESSION) return 'windows-terminal';
132
+ if (process.env.SHELL?.includes('bash')) return 'git-bash';
133
+ if (process.env.TERM?.includes('xterm')) return 'xterm-on-windows';
134
+ if (process.env.ComSpec?.toLowerCase().includes('powershell')) return 'powershell';
135
+ if (process.env.PROMPT) return 'cmd';
136
+
137
+ if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
138
+ return `wsl-${process.env.WSL_DISTRO_NAME || 'unknown'}`;
139
+ }
140
+
141
+ return 'windows-unknown';
142
+ }
143
+
144
+ if (process.env.SHELL) {
145
+ const shellPath = process.env.SHELL.toLowerCase();
146
+ if (shellPath.includes('bash')) return 'bash';
147
+ if (shellPath.includes('zsh')) return 'zsh';
148
+ if (shellPath.includes('fish')) return 'fish';
149
+ if (shellPath.includes('ksh')) return 'ksh';
150
+ if (shellPath.includes('csh')) return 'csh';
151
+ if (shellPath.includes('dash')) return 'dash';
152
+ return `other-unix-${shellPath.split('/').pop()}`;
153
+ }
154
+
155
+ if (process.env.TERM_PROGRAM) {
156
+ return process.env.TERM_PROGRAM.toLowerCase();
157
+ }
158
+
159
+ return 'unknown-shell';
160
+ }
161
+
162
+ // Function to determine execution context
163
+ function getExecutionContext() {
164
+ const isNpx = process.env.npm_lifecycle_event === 'npx' ||
165
+ process.env.npm_execpath?.includes('npx') ||
166
+ process.env._?.includes('npx') ||
167
+ import.meta.url.includes('node_modules');
168
+
169
+ const isGlobal = process.env.npm_config_global === 'true' ||
170
+ process.argv[1]?.includes('node_modules/.bin');
171
+
172
+ const isNpmScript = !!process.env.npm_lifecycle_script;
173
+
174
+ return {
175
+ runMethod: isNpx ? 'npx' : (isGlobal ? 'global' : (isNpmScript ? 'npm_script' : 'direct')),
176
+ isCI: !!process.env.CI || !!process.env.GITHUB_ACTIONS || !!process.env.TRAVIS || !!process.env.CIRCLECI,
177
+ shell: detectShell()
178
+ };
179
+ }
180
+ // Enhanced tracking properties
181
+ let npmVersionCache = null;
182
+
183
+ async function getTrackingProperties(additionalProps = {}) {
184
+ const propertiesStep = addUninstallStep('get_tracking_properties');
185
+ try {
186
+ if (npmVersionCache === null) {
187
+ npmVersionCache = await getNpmVersion();
188
+ }
189
+
190
+ const context = getExecutionContext();
191
+ const version = await getVersion();
192
+
193
+ updateUninstallStep(propertiesStep, 'completed');
194
+ return {
195
+ platform: platform(),
196
+ node_version: nodeVersion,
197
+ npm_version: npmVersionCache,
198
+ execution_context: context.runMethod,
199
+ is_ci: context.isCI,
200
+ shell: context.shell,
201
+ app_version: version,
202
+ engagement_time_msec: "100",
203
+ ...additionalProps
204
+ };
205
+ } catch (error) {
206
+ updateUninstallStep(propertiesStep, 'failed', error);
207
+ return {
208
+ platform: platform(),
209
+ node_version: nodeVersion,
210
+ error: error.message,
211
+ ...additionalProps
212
+ };
213
+ }
214
+ }
215
+ // Enhanced tracking function with retries
216
+ async function trackEvent(eventName, additionalProps = {}) {
217
+ const trackingStep = addUninstallStep(`track_event_${eventName}`);
218
+
219
+ // Check if telemetry is disabled
220
+ if (!telemetryEnabled) {
221
+ updateUninstallStep(trackingStep, 'skipped_telemetry_disabled');
222
+ return true; // Return success since this is expected behavior
223
+ }
224
+
225
+ if (!GA_MEASUREMENT_ID || !GA_API_SECRET) {
226
+ updateUninstallStep(trackingStep, 'skipped', new Error('GA not configured'));
227
+ return;
228
+ }
229
+
230
+ const maxRetries = 2;
231
+ let attempt = 0;
232
+ let lastError = null;
233
+
234
+ while (attempt <= maxRetries) {
235
+ try {
236
+ attempt++;
237
+
238
+ const eventProperties = await getTrackingProperties(additionalProps);
239
+
240
+ const payload = {
241
+ client_id: uniqueUserId,
242
+ non_personalized_ads: false,
243
+ timestamp_micros: Date.now() * 1000,
244
+ events: [{
245
+ name: eventName,
246
+ params: eventProperties
247
+ }]
248
+ };
249
+
250
+ const postData = JSON.stringify(payload);
251
+
252
+ const options = {
253
+ method: 'POST',
254
+ headers: {
255
+ 'Content-Type': 'application/json',
256
+ 'Content-Length': Buffer.byteLength(postData)
257
+ }
258
+ };
259
+
260
+ const result = await new Promise((resolve, reject) => {
261
+ const req = https.request(GA_BASE_URL, options);
262
+
263
+ const timeoutId = setTimeout(() => {
264
+ req.destroy();
265
+ reject(new Error('Request timeout'));
266
+ }, 5000);
267
+
268
+ req.on('error', (error) => {
269
+ clearTimeout(timeoutId);
270
+ reject(error);
271
+ });
272
+
273
+ req.on('response', (res) => {
274
+ clearTimeout(timeoutId);
275
+ let data = '';
276
+
277
+ res.on('data', (chunk) => {
278
+ data += chunk;
279
+ });
280
+
281
+ res.on('error', (error) => {
282
+ reject(error);
283
+ });
284
+
285
+ res.on('end', () => {
286
+ if (res.statusCode >= 200 && res.statusCode < 300) {
287
+ resolve({ success: true, data });
288
+ } else {
289
+ reject(new Error(`HTTP error ${res.statusCode}: ${data}`));
290
+ }
291
+ });
292
+ });
293
+
294
+ req.write(postData);
295
+ req.end();
296
+ });
297
+
298
+ updateUninstallStep(trackingStep, 'completed');
299
+ return result;
300
+
301
+ } catch (error) {
302
+ lastError = error;
303
+ if (attempt <= maxRetries) {
304
+ await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
305
+ }
306
+ }
307
+ }
308
+
309
+ updateUninstallStep(trackingStep, 'failed', lastError);
310
+ return false;
311
+ }
312
+ // Ensure tracking completes before process exits
313
+ async function ensureTrackingCompleted(eventName, additionalProps = {}, timeoutMs = 6000) {
314
+ return new Promise(async (resolve) => {
315
+ const timeoutId = setTimeout(() => {
316
+ resolve(false);
317
+ }, timeoutMs);
318
+
319
+ try {
320
+ await trackEvent(eventName, additionalProps);
321
+ clearTimeout(timeoutId);
322
+ resolve(true);
323
+ } catch (error) {
324
+ clearTimeout(timeoutId);
325
+ resolve(false);
326
+ }
327
+ });
328
+ }
329
+
330
+ // Setup global error handlers (will be initialized after config is loaded)
331
+ let errorHandlersInitialized = false;
332
+
333
+ function initializeErrorHandlers() {
334
+ if (errorHandlersInitialized) return;
335
+
336
+ process.on('uncaughtException', async (error) => {
337
+ if (telemetryEnabled) {
338
+ await trackEvent('uninstall_uncaught_exception', { error: error.message });
339
+ }
340
+ setTimeout(() => {
341
+ process.exit(1);
342
+ }, 1000);
343
+ });
344
+
345
+ process.on('unhandledRejection', async (reason, promise) => {
346
+ if (telemetryEnabled) {
347
+ await trackEvent('uninstall_unhandled_rejection', { error: String(reason) });
348
+ }
349
+ setTimeout(() => {
350
+ process.exit(1);
351
+ }, 1000);
352
+ });
353
+
354
+ errorHandlersInitialized = true;
355
+ }
356
+
357
+ // Track when the process is about to exit
358
+ let isExiting = false;
359
+ process.on('exit', () => {
360
+ if (!isExiting) {
361
+ isExiting = true;
362
+ }
363
+ });
364
+
365
+ // Determine OS and set appropriate config path
366
+ const os = platform();
367
+ const isWindows = os === 'win32';
368
+ let claudeConfigPath;
369
+
370
+ switch (os) {
371
+ case 'win32':
372
+ claudeConfigPath = join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json');
373
+ break;
374
+ case 'darwin':
375
+ claudeConfigPath = join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
376
+ break;
377
+ case 'linux':
378
+ claudeConfigPath = join(homedir(), '.config', 'Claude', 'claude_desktop_config.json');
379
+ break;
380
+ default:
381
+ claudeConfigPath = join(homedir(), '.claude_desktop_config.json');
382
+ }
383
+ // Step tracking functions
384
+ function addUninstallStep(step, status = 'started', error = null) {
385
+ const timestamp = Date.now();
386
+ uninstallSteps.push({
387
+ step,
388
+ status,
389
+ timestamp,
390
+ timeFromStart: timestamp - uninstallStartTime,
391
+ error: error ? error.message || String(error) : null
392
+ });
393
+ return uninstallSteps.length - 1;
394
+ }
395
+
396
+ function updateUninstallStep(index, status, error = null) {
397
+ if (uninstallSteps[index]) {
398
+ const timestamp = Date.now();
399
+ uninstallSteps[index].status = status;
400
+ uninstallSteps[index].completionTime = timestamp;
401
+ uninstallSteps[index].timeFromStart = timestamp - uninstallStartTime;
402
+ if (error) {
403
+ uninstallSteps[index].error = error.message || String(error);
404
+ }
405
+ }
406
+ }
407
+
408
+ async function execAsync(command) {
409
+ const execStep = addUninstallStep(`exec_${command.substring(0, 20)}...`);
410
+ return new Promise((resolve, reject) => {
411
+ const actualCommand = isWindows
412
+ ? `cmd.exe /c ${command}`
413
+ : command;
414
+
415
+ exec(actualCommand, { timeout: 10000 }, (error, stdout, stderr) => {
416
+ if (error) {
417
+ updateUninstallStep(execStep, 'failed', error);
418
+ reject(error);
419
+ return;
420
+ }
421
+ updateUninstallStep(execStep, 'completed');
422
+ resolve({ stdout, stderr });
423
+ });
424
+ });
425
+ }
426
+ // Backup configuration before removal
427
+ async function createConfigBackup(configPath) {
428
+ const backupStep = addUninstallStep('create_config_backup');
429
+ try {
430
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
431
+ const backupPath = `${configPath}.backup.${timestamp}`;
432
+
433
+ if (existsSync(configPath)) {
434
+ const configData = readFileSync(configPath, 'utf8');
435
+ writeFileSync(backupPath, configData, 'utf8');
436
+ updateUninstallStep(backupStep, 'completed');
437
+ logToFile(`Configuration backup created: ${backupPath}`);
438
+ await trackEvent('uninstall_backup_created');
439
+ return backupPath;
440
+ } else {
441
+ updateUninstallStep(backupStep, 'no_config_file');
442
+ return null;
443
+ }
444
+ } catch (error) {
445
+ updateUninstallStep(backupStep, 'failed', error);
446
+ await trackEvent('uninstall_backup_failed', { error: error.message });
447
+ logToFile(`Failed to create backup: ${error.message}`, true);
448
+ return null;
449
+ }
450
+ }
451
+
452
+ // Restore configuration from backup
453
+ async function restoreFromBackup(backupPath) {
454
+ if (!backupPath || !existsSync(backupPath)) {
455
+ return false;
456
+ }
457
+
458
+ try {
459
+ const backupData = readFileSync(backupPath, 'utf8');
460
+ writeFileSync(claudeConfigPath, backupData, 'utf8');
461
+ logToFile(`Configuration restored from backup: ${backupPath}`);
462
+ await trackEvent('uninstall_backup_restored');
463
+ return true;
464
+ } catch (error) {
465
+ logToFile(`Failed to restore from backup: ${error.message}`, true);
466
+ await trackEvent('uninstall_backup_restore_failed', { error: error.message });
467
+ return false;
468
+ }
469
+ }
470
+
471
+ async function restartClaude() {
472
+ const restartStep = addUninstallStep('restart_claude');
473
+ try {
474
+ const platform = process.platform;
475
+ logToFile('Attempting to restart Claude...');
476
+ await trackEvent('uninstall_restart_claude_attempt');
477
+
478
+ // Try to kill Claude process first
479
+ const killStep = addUninstallStep('kill_claude_process');
480
+ try {
481
+ switch (platform) {
482
+ case "win32":
483
+ await execAsync(`taskkill /F /IM "Claude.exe"`);
484
+ break;
485
+ case "darwin":
486
+ await execAsync(`killall "Claude"`);
487
+ break;
488
+ case "linux":
489
+ await execAsync(`pkill -f "claude"`);
490
+ break;
491
+ }
492
+ updateUninstallStep(killStep, 'completed');
493
+ logToFile("Claude process terminated successfully");
494
+ await trackEvent('uninstall_kill_claude_success');
495
+ } catch (killError) {
496
+ updateUninstallStep(killStep, 'no_process_found', killError);
497
+ logToFile("Claude process not found or already terminated");
498
+ await trackEvent('uninstall_kill_claude_not_needed');
499
+ }
500
+
501
+ // Wait a bit to ensure process termination
502
+ await new Promise((resolve) => setTimeout(resolve, 2000));
503
+
504
+ // Try to start Claude
505
+ const startStep = addUninstallStep('start_claude_process');
506
+ try {
507
+ if (platform === "win32") {
508
+ logToFile("Windows: Claude restart skipped - please restart Claude manually");
509
+ updateUninstallStep(startStep, 'skipped');
510
+ await trackEvent('uninstall_start_claude_skipped');
511
+ } else if (platform === "darwin") {
512
+ await execAsync(`open -a "Claude"`);
513
+ updateUninstallStep(startStep, 'completed');
514
+ logToFile("✅ Claude has been restarted automatically!");
515
+ await trackEvent('uninstall_start_claude_success');
516
+ } else if (platform === "linux") {
517
+ await execAsync(`claude`);
518
+ updateUninstallStep(startStep, 'completed');
519
+ logToFile("✅ Claude has been restarted automatically!");
520
+ await trackEvent('uninstall_start_claude_success');
521
+ } else {
522
+ logToFile('To complete uninstallation, restart Claude if it\'s currently running');
523
+ updateUninstallStep(startStep, 'manual_required');
524
+ }
525
+
526
+ updateUninstallStep(restartStep, 'completed');
527
+ await trackEvent('uninstall_restart_claude_success');
528
+ } catch (startError) {
529
+ updateUninstallStep(startStep, 'failed', startError);
530
+ await trackEvent('uninstall_start_claude_error', {
531
+ error: startError.message
532
+ });
533
+ logToFile(`Could not automatically restart Claude: ${startError.message}. Please restart it manually.`);
534
+ }
535
+ } catch (error) {
536
+ updateUninstallStep(restartStep, 'failed', error);
537
+ await trackEvent('uninstall_restart_claude_error', { error: error.message });
538
+ logToFile(`Failed to restart Claude: ${error.message}. Please restart it manually.`, true);
539
+ }
540
+ }
541
+ async function removeDesktopCommanderConfig() {
542
+ const configStep = addUninstallStep('remove_mcp_config');
543
+ let backupPath = null;
544
+
545
+ try {
546
+ // Check if config file exists
547
+ if (!existsSync(claudeConfigPath)) {
548
+ updateUninstallStep(configStep, 'no_config_file');
549
+ logToFile(`Claude config file not found at: ${claudeConfigPath}`);
550
+ logToFile('✅ Desktop Commander was not configured or already removed.');
551
+ await trackEvent('uninstall_config_not_found');
552
+ return true;
553
+ }
554
+
555
+ // Create backup before making changes
556
+ backupPath = await createConfigBackup(claudeConfigPath);
557
+
558
+ // Read existing config
559
+ let config;
560
+ const readStep = addUninstallStep('read_config_file');
561
+ try {
562
+ const configData = readFileSync(claudeConfigPath, 'utf8');
563
+ config = JSON.parse(configData);
564
+ updateUninstallStep(readStep, 'completed');
565
+ } catch (readError) {
566
+ updateUninstallStep(readStep, 'failed', readError);
567
+ await trackEvent('uninstall_config_read_error', { error: readError.message });
568
+ throw new Error(`Failed to read config file: ${readError.message}`);
569
+ }
570
+
571
+ // Check if mcpServers exists
572
+ if (!config.mcpServers) {
573
+ updateUninstallStep(configStep, 'no_mcp_servers');
574
+ logToFile('No MCP servers configured in Claude.');
575
+ logToFile('✅ Desktop Commander was not configured or already removed.');
576
+ await trackEvent('uninstall_no_mcp_servers');
577
+ return true;
578
+ }
579
+ // Track what we're removing
580
+ const serversToRemove = [];
581
+
582
+ if (config.mcpServers["desktop-commander"]) {
583
+ serversToRemove.push("desktop-commander");
584
+ }
585
+
586
+ if (serversToRemove.length === 0) {
587
+ updateUninstallStep(configStep, 'not_found');
588
+ logToFile('Desktop Commander MCP server not found in configuration.');
589
+ logToFile('✅ Desktop Commander was not configured or already removed.');
590
+ await trackEvent('uninstall_server_not_found');
591
+ return true;
592
+ }
593
+
594
+ // Remove the server configurations
595
+ const removeStep = addUninstallStep('remove_server_configs');
596
+ try {
597
+ serversToRemove.forEach(serverName => {
598
+ delete config.mcpServers[serverName];
599
+ logToFile(`Removed "${serverName}" from Claude configuration`);
600
+ });
601
+
602
+ updateUninstallStep(removeStep, 'completed');
603
+ await trackEvent('uninstall_servers_removed');
604
+ } catch (removeError) {
605
+ updateUninstallStep(removeStep, 'failed', removeError);
606
+ await trackEvent('uninstall_servers_remove_error', { error: removeError.message });
607
+ throw new Error(`Failed to remove server configs: ${removeError.message}`);
608
+ } // Write the updated config back
609
+ const writeStep = addUninstallStep('write_updated_config');
610
+ try {
611
+ writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), 'utf8');
612
+ updateUninstallStep(writeStep, 'completed');
613
+ updateUninstallStep(configStep, 'completed');
614
+ logToFile('✅ Desktop Commander successfully removed from Claude configuration');
615
+ logToFile(`Configuration updated at: ${claudeConfigPath}`);
616
+ await trackEvent('uninstall_config_updated');
617
+ } catch (writeError) {
618
+ updateUninstallStep(writeStep, 'failed', writeError);
619
+ await trackEvent('uninstall_config_write_error', { error: writeError.message });
620
+
621
+ // Try to restore from backup
622
+ if (backupPath) {
623
+ logToFile('Attempting to restore configuration from backup...');
624
+ const restored = await restoreFromBackup(backupPath);
625
+ if (restored) {
626
+ throw new Error(`Failed to write updated config, but backup was restored: ${writeError.message}`);
627
+ } else {
628
+ throw new Error(`Failed to write updated config and backup restoration failed: ${writeError.message}`);
629
+ }
630
+ } else {
631
+ throw new Error(`Failed to write updated config: ${writeError.message}`);
632
+ }
633
+ }
634
+
635
+ return true;
636
+ } catch (error) {
637
+ updateUninstallStep(configStep, 'failed', error);
638
+ await trackEvent('uninstall_config_error', { error: error.message });
639
+ logToFile(`Error removing Desktop Commander configuration: ${error.message}`, true);
640
+
641
+ // Try to restore from backup if we have one
642
+ if (backupPath) {
643
+ logToFile('Attempting to restore configuration from backup...');
644
+ await restoreFromBackup(backupPath);
645
+ }
646
+
647
+ return false;
648
+ }
649
+ }
650
+ // Main uninstall function
651
+ export default async function uninstall() {
652
+ // Initialize clientId and telemetry settings from existing config
653
+ const configSettings = await getConfigSettings();
654
+ uniqueUserId = configSettings.clientId;
655
+ telemetryEnabled = configSettings.telemetryEnabled;
656
+
657
+ // Initialize error handlers now that telemetry setting is known
658
+ initializeErrorHandlers();
659
+
660
+ // Log telemetry status for transparency
661
+ if (!telemetryEnabled) {
662
+ logToFile('Telemetry disabled - no analytics will be sent');
663
+ }
664
+
665
+ // Initial tracking (only if telemetry enabled)
666
+ await ensureTrackingCompleted('uninstall_start');
667
+
668
+ const mainStep = addUninstallStep('main_uninstall');
669
+
670
+ try {
671
+ logToFile('Starting Desktop Commander uninstallation...');
672
+
673
+ // Remove the server configuration from Claude
674
+ const configRemoved = await removeDesktopCommanderConfig();
675
+
676
+ if (configRemoved) {
677
+ // Try to restart Claude
678
+ // await restartClaude();
679
+
680
+ updateUninstallStep(mainStep, 'completed');
681
+
682
+ const appVersion = await getVersion();
683
+ logToFile(`\n✅ Desktop Commander has been successfully uninstalled!`);
684
+ logToFile('The MCP server has been removed from Claude\'s configuration.');
685
+
686
+ logToFile('\nIf you want to reinstall later, you can run:');
687
+ logToFile('npx @wonderwhy-er/desktop-commander@latest setup');
688
+
689
+ logToFile('\n🎁 We\'re sorry to see you leaving, we’d love to understand your decision not to use Desktop Commander.')
690
+ logToFile('In return for a brief 30-minute call, we’ll send you a $20 Amazon gift card as a thank-you.');
691
+ logToFile('To get a gift card, please fill out this form:');
692
+ logToFile(' https://tally.so/r/w8lyRo');
693
+
694
+
695
+ logToFile('\nThank you for using Desktop Commander! 👋\n');
696
+
697
+ // Send final tracking event
698
+ await ensureTrackingCompleted('uninstall_complete');
699
+
700
+ return true;
701
+ } else {
702
+ updateUninstallStep(mainStep, 'failed_config_removal');
703
+
704
+ logToFile('\n❌ Uninstallation completed with errors.');
705
+ logToFile('You may need to manually remove Desktop Commander from Claude\'s configuration.');
706
+ logToFile(`Configuration file location: ${claudeConfigPath}\n`);
707
+
708
+ logToFile('\n🎁 We\'re sorry to see you leaving, we\'d love to understand your decision not to use Desktop Commander.')
709
+ logToFile('In return for a brief 30-minute call, we\'ll send you a $20 Amazon gift card as a thank-you.');
710
+ logToFile('To get a gift card, please fill out this form:');
711
+ logToFile(' https://tally.so/r/w8lyRo');
712
+
713
+ await ensureTrackingCompleted('uninstall_partial_failure');
714
+
715
+ return false;
716
+ }
717
+ } catch (error) {
718
+ updateUninstallStep(mainStep, 'fatal_error', error);
719
+
720
+ await ensureTrackingCompleted('uninstall_fatal_error', {
721
+ error: error.message,
722
+ error_stack: error.stack,
723
+ last_successful_step: uninstallSteps.filter(s => s.status === 'completed').pop()?.step || 'none'
724
+ });
725
+
726
+ logToFile(`Fatal error during uninstallation: ${error.message}`, true);
727
+ logToFile('\n❌ Uninstallation failed.');
728
+ logToFile('You may need to manually remove Desktop Commander from Claude\'s configuration.');
729
+ logToFile(`Configuration file location: ${claudeConfigPath}\n`);
730
+
731
+ logToFile('\n🎁 We\'re sorry to see you leaving, we\'d love to understand your decision not to use Desktop Commander.')
732
+ logToFile('In return for a brief 30-minute call, we\'ll send you a $20 Amazon gift card as a thank-you.');
733
+ logToFile('To get a gift card, please fill out this form:');
734
+ logToFile('https://tally.so/r/w8lyRo');
735
+ return false;
736
+ }
737
+ }
738
+
739
+ // Allow direct execution
740
+ if (process.argv.length >= 2 && process.argv[1] === fileURLToPath(import.meta.url)) {
741
+ uninstall().then(success => {
742
+ if (!success) {
743
+ process.exit(1);
744
+ }
745
+ }).catch(async error => {
746
+ await ensureTrackingCompleted('uninstall_execution_error', {
747
+ error: error.message,
748
+ error_stack: error.stack
749
+ });
750
+ logToFile(`Fatal error: ${error}`, true);
751
+ process.exit(1);
752
+ });
753
+ }