@wonderwhy-er/desktop-commander 0.1.25 → 0.1.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.
package/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # Desktop Commander MCP
2
2
 
3
-
4
3
  [![npm downloads](https://img.shields.io/npm/dw/@wonderwhy-er/desktop-commander)](https://www.npmjs.com/package/@wonderwhy-er/desktop-commander)
5
4
  [![smithery badge](https://smithery.ai/badge/@wonderwhy-er/desktop-commander)](https://smithery.ai/server/@wonderwhy-er/desktop-commander)
6
5
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow.svg)](https://www.buymeacoffee.com/wonderwhyer)
@@ -51,7 +50,14 @@ This is server that allows Claude desktop app to execute long-running terminal c
51
50
  ## Installation
52
51
  First, ensure you've downloaded and installed the [Claude Desktop app](https://claude.ai/download) and you have [npm installed](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
53
52
 
54
- ### Option 1: Installing via Smithery
53
+ ### Option 1: Install through npx
54
+ Just run this in terminal
55
+ ```
56
+ npx @wonderwhy-er/desktop-commander@latest setup
57
+ ```
58
+ Restart Claude if running
59
+
60
+ ### Option 2: Installing via Smithery
55
61
 
56
62
  To install Desktop Commander for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@wonderwhy-er/desktop-commander):
57
63
 
@@ -59,13 +65,6 @@ To install Desktop Commander for Claude Desktop automatically via [Smithery](htt
59
65
  npx -y @smithery/cli install @wonderwhy-er/desktop-commander --client claude
60
66
  ```
61
67
 
62
- ### Option 2: Install trough npx
63
- Just run this in terminal
64
- ```
65
- npx @wonderwhy-er/desktop-commander setup
66
- ```
67
- Restart Claude if running
68
-
69
68
  ### Option 3: Add to claude_desktop_config by hand
70
69
  Add this entry to your claude_desktop_config.json:
71
70
 
@@ -122,7 +121,7 @@ The server provides these tool categories:
122
121
  - `move_file`: Move/rename files
123
122
  - `search_files`: Pattern-based file search
124
123
  - `get_file_info`: File metadata
125
- - `code_search`: Recursive ripgrep based text and code search
124
+ - `search_code`: Recursive ripgrep based text and code search
126
125
 
127
126
  ### Edit Tools
128
127
  - `edit_block`: Apply surgical text replacements (best for changes <20% of file size)
@@ -132,9 +131,9 @@ Search/Replace Block Format:
132
131
  ```
133
132
  filepath.ext
134
133
  <<<<<<< SEARCH
135
- existing code to replace
134
+ content to find
136
135
  =======
137
- new code to insert
136
+ new content
138
137
  >>>>>>> REPLACE
139
138
  ```
140
139
 
@@ -264,6 +263,18 @@ No. This tool works with Claude Desktop's standard Pro subscription ($20/month),
264
263
  ### I'm having trouble installing or using the tool. Where can I get help?
265
264
  Join our [Discord server](https://discord.gg/kQ27sNnZr7) for community support, check the [GitHub issues](https://github.com/wonderwhy-er/ClaudeComputerCommander/issues) for known problems, or review the [full FAQ](FAQ.md) for troubleshooting tips. You can also visit our [website FAQ section](https://desktopcommander.app#faq) for a more user-friendly experience. If you encounter a new issue, please consider [opening a GitHub issue](https://github.com/wonderwhy-er/ClaudeComputerCommander/issues/new) with details about your problem.
266
265
 
266
+ ## Data Collection
267
+
268
+ During installation and setup, Desktop Commander collects anonymous usage data to help improve the tool. This includes:
269
+ - Operating system information
270
+ - Node.js and NPM versions
271
+ - Installation method and shell environment
272
+ - Error messages (if any occur during setup)
273
+
274
+ This data is collected using PostHog analytics and is associated with a machine-generated unique ID. No personal information is collected. This helps us understand how the tool is being used and identify common issues.
275
+
276
+ We are currently working on adding a built-in opt-out option for this data collection in an upcoming release. For now, if you wish to opt out, you can block network connections to `eu.i.posthog.com` in your firewall settings.
277
+
267
278
  ## License
268
279
 
269
280
  MIT
@@ -2,6 +2,9 @@ declare class CommandManager {
2
2
  private blockedCommands;
3
3
  loadBlockedCommands(): Promise<void>;
4
4
  saveBlockedCommands(): Promise<void>;
5
+ getBaseCommand(command: string): string;
6
+ extractCommands(commandString: string): string[];
7
+ extractBaseCommand(commandStr: string): string | null;
5
8
  validateCommand(command: string): boolean;
6
9
  blockCommand(command: string): Promise<boolean>;
7
10
  unblockCommand(command: string): Promise<boolean>;
@@ -25,8 +25,137 @@ class CommandManager {
25
25
  // Handle error if needed
26
26
  }
27
27
  }
28
+ getBaseCommand(command) {
29
+ return command.split(' ')[0].toLowerCase().trim();
30
+ }
31
+ extractCommands(commandString) {
32
+ try {
33
+ // Trim any leading/trailing whitespace
34
+ commandString = commandString.trim();
35
+ // Define command separators - these are the operators that can chain commands
36
+ const separators = [';', '&&', '||', '|', '&'];
37
+ // This will store our extracted commands
38
+ const commands = [];
39
+ // Split by common separators while preserving quotes
40
+ let inQuote = false;
41
+ let quoteChar = '';
42
+ let currentCmd = '';
43
+ let escaped = false;
44
+ for (let i = 0; i < commandString.length; i++) {
45
+ const char = commandString[i];
46
+ // Handle escape characters
47
+ if (char === '\\' && !escaped) {
48
+ escaped = true;
49
+ currentCmd += char;
50
+ continue;
51
+ }
52
+ // If this character is escaped, just add it
53
+ if (escaped) {
54
+ escaped = false;
55
+ currentCmd += char;
56
+ continue;
57
+ }
58
+ // Handle quotes (both single and double)
59
+ if ((char === '"' || char === "'") && !inQuote) {
60
+ inQuote = true;
61
+ quoteChar = char;
62
+ currentCmd += char;
63
+ continue;
64
+ }
65
+ else if (char === quoteChar && inQuote) {
66
+ inQuote = false;
67
+ quoteChar = '';
68
+ currentCmd += char;
69
+ continue;
70
+ }
71
+ // If we're inside quotes, just add the character
72
+ if (inQuote) {
73
+ currentCmd += char;
74
+ continue;
75
+ }
76
+ // Handle subshells - if we see an opening parenthesis, we need to find its matching closing parenthesis
77
+ if (char === '(') {
78
+ // Find the matching closing parenthesis
79
+ let openParens = 1;
80
+ let j = i + 1;
81
+ while (j < commandString.length && openParens > 0) {
82
+ if (commandString[j] === '(')
83
+ openParens++;
84
+ if (commandString[j] === ')')
85
+ openParens--;
86
+ j++;
87
+ }
88
+ // Skip to after the closing parenthesis
89
+ if (j <= commandString.length) {
90
+ const subshellContent = commandString.substring(i + 1, j - 1);
91
+ // Recursively extract commands from the subshell
92
+ const subCommands = this.extractCommands(subshellContent);
93
+ commands.push(...subCommands);
94
+ // Move position past the subshell
95
+ i = j - 1;
96
+ continue;
97
+ }
98
+ }
99
+ // Check for separators
100
+ let isSeparator = false;
101
+ for (const separator of separators) {
102
+ if (commandString.startsWith(separator, i)) {
103
+ // We found a separator - extract the command before it
104
+ if (currentCmd.trim()) {
105
+ const baseCommand = this.extractBaseCommand(currentCmd.trim());
106
+ if (baseCommand)
107
+ commands.push(baseCommand);
108
+ }
109
+ // Move past the separator
110
+ i += separator.length - 1;
111
+ currentCmd = '';
112
+ isSeparator = true;
113
+ break;
114
+ }
115
+ }
116
+ if (!isSeparator) {
117
+ currentCmd += char;
118
+ }
119
+ }
120
+ // Don't forget to add the last command
121
+ if (currentCmd.trim()) {
122
+ const baseCommand = this.extractBaseCommand(currentCmd.trim());
123
+ if (baseCommand)
124
+ commands.push(baseCommand);
125
+ }
126
+ // Remove duplicates and return
127
+ return [...new Set(commands)];
128
+ }
129
+ catch (error) {
130
+ // If anything goes wrong, log the error but return the basic command to not break execution
131
+ console.error('Error extracting commands:', error);
132
+ return [this.getBaseCommand(commandString)];
133
+ }
134
+ }
135
+ // This extracts the actual command name from a command string
136
+ extractBaseCommand(commandStr) {
137
+ try {
138
+ // Remove environment variables (patterns like KEY=value)
139
+ const withoutEnvVars = commandStr.replace(/\w+=\S+\s*/g, '').trim();
140
+ // If nothing remains after removing env vars, return null
141
+ if (!withoutEnvVars)
142
+ return null;
143
+ // Get the first token (the command)
144
+ const tokens = withoutEnvVars.split(/\s+/);
145
+ const firstToken = tokens[0];
146
+ // Check if it starts with special characters like (, $ that might indicate it's not a regular command
147
+ if (['(', '$'].includes(firstToken[0])) {
148
+ return null;
149
+ }
150
+ return firstToken.toLowerCase();
151
+ }
152
+ catch (error) {
153
+ console.error('Error extracting base command:', error);
154
+ return null;
155
+ }
156
+ }
28
157
  validateCommand(command) {
29
- const baseCommand = command.split(' ')[0].toLowerCase().trim();
158
+ const baseCommand = this.getBaseCommand(command);
30
159
  return !this.blockedCommands.has(baseCommand);
31
160
  }
32
161
  async blockCommand(command) {
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { commandManager } from './command-manager.js';
5
5
  import { join, dirname } from 'path';
6
6
  import { fileURLToPath, pathToFileURL } from 'url';
7
7
  import { platform } from 'os';
8
+ import { capture } from './utils.js';
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = dirname(__filename);
10
11
  const isWindows = platform() === 'win32';
@@ -59,6 +60,9 @@ async function runServer() {
59
60
  process.stderr.write(`[desktop-commander] JSON parsing error: ${errorMessage}\n`);
60
61
  return; // Don't exit on JSON parsing errors
61
62
  }
63
+ capture('run_server_uncaught_exception', {
64
+ error: errorMessage
65
+ });
62
66
  process.stderr.write(`[desktop-commander] Uncaught exception: ${errorMessage}\n`);
63
67
  process.exit(1);
64
68
  });
@@ -70,6 +74,9 @@ async function runServer() {
70
74
  process.stderr.write(`[desktop-commander] JSON parsing rejection: ${errorMessage}\n`);
71
75
  return; // Don't exit on JSON parsing errors
72
76
  }
77
+ capture('run_server_unhandled_rejection', {
78
+ error: errorMessage
79
+ });
73
80
  process.stderr.write(`[desktop-commander] Unhandled rejection: ${errorMessage}\n`);
74
81
  process.exit(1);
75
82
  });
@@ -84,6 +91,9 @@ async function runServer() {
84
91
  timestamp: new Date().toISOString(),
85
92
  message: `Failed to start server: ${errorMessage}`
86
93
  }) + '\n');
94
+ capture('run_server_failed_start_error', {
95
+ error: errorMessage
96
+ });
87
97
  process.exit(1);
88
98
  }
89
99
  }
@@ -94,5 +104,8 @@ runServer().catch(async (error) => {
94
104
  timestamp: new Date().toISOString(),
95
105
  message: `Fatal error running server: ${errorMessage}`
96
106
  }) + '\n');
107
+ capture('run_server_fatal_error', {
108
+ error: errorMessage
109
+ });
97
110
  process.exit(1);
98
111
  });
package/dist/server.js CHANGED
@@ -9,6 +9,7 @@ import { readFile, readMultipleFiles, writeFile, createDirectory, listDirectory,
9
9
  import { parseEditBlock, performSearchReplace } from './tools/edit.js';
10
10
  import { searchTextInFiles } from './tools/search.js';
11
11
  import { VERSION } from './version.js';
12
+ import { capture } from "./utils.js";
12
13
  export const server = new Server({
13
14
  name: "desktop-commander",
14
15
  version: VERSION,
@@ -96,7 +97,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
96
97
  {
97
98
  name: "read_file",
98
99
  description: "Read the complete contents of a file from the file system. " +
99
- "Handles various text encodings and provides detailed error messages " +
100
+ "Reads UTF-8 text and provides detailed error messages " +
100
101
  "if the file cannot be read. Only works within allowed directories.",
101
102
  inputSchema: zodToJsonSchema(ReadFileArgsSchema),
102
103
  },
@@ -136,7 +137,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
136
137
  },
137
138
  {
138
139
  name: "search_files",
139
- description: "Recursively search for files and directories matching a pattern. " +
140
+ description: "Finds files by name using a case-insensitive substring matching. " +
140
141
  "Searches through all subdirectories from the starting path. " +
141
142
  "Only searches within allowed directories.",
142
143
  inputSchema: zodToJsonSchema(SearchFilesArgsSchema),
@@ -168,8 +169,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
168
169
  {
169
170
  name: "edit_block",
170
171
  description: "Apply surgical text replacements to files. Best for small changes (<20% of file size). " +
171
- "Multiple blocks can be used for separate changes. Will verify changes after application. " +
172
- "Format: filepath, then <<<<<<< SEARCH, content to find, =======, new content, >>>>>>> REPLACE.",
172
+ "Call repeatedly to change multiple blocks. Will verify changes after application. " +
173
+ "Format:\nfilepath\n<<<<<<< SEARCH\ncontent to find\n=======\nnew content\n>>>>>>> REPLACE",
173
174
  inputSchema: zodToJsonSchema(EditBlockArgsSchema),
174
175
  },
175
176
  ],
@@ -185,22 +186,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
185
186
  return executeCommand(parsed);
186
187
  }
187
188
  case "read_output": {
189
+ capture('server_read_output');
188
190
  const parsed = ReadOutputArgsSchema.parse(args);
189
191
  return readOutput(parsed);
190
192
  }
191
193
  case "force_terminate": {
194
+ capture('server_force_terminate');
192
195
  const parsed = ForceTerminateArgsSchema.parse(args);
193
196
  return forceTerminate(parsed);
194
197
  }
195
198
  case "list_sessions":
199
+ capture('server_list_sessions');
196
200
  return listSessions();
197
201
  case "list_processes":
202
+ capture('server_list_processes');
198
203
  return listProcesses();
199
204
  case "kill_process": {
205
+ capture('server_kill_process');
200
206
  const parsed = KillProcessArgsSchema.parse(args);
201
207
  return killProcess(parsed);
202
208
  }
203
209
  case "block_command": {
210
+ capture('server_block_command');
204
211
  const parsed = BlockCommandArgsSchema.parse(args);
205
212
  const blockResult = await commandManager.blockCommand(parsed.command);
206
213
  return {
@@ -208,6 +215,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
208
215
  };
209
216
  }
210
217
  case "unblock_command": {
218
+ capture('server_unblock_command');
211
219
  const parsed = UnblockCommandArgsSchema.parse(args);
212
220
  const unblockResult = await commandManager.unblockCommand(parsed.command);
213
221
  return {
@@ -215,6 +223,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
215
223
  };
216
224
  }
217
225
  case "list_blocked_commands": {
226
+ capture('server_list_blocked_commands');
218
227
  const blockedCommands = await commandManager.listBlockedCommands();
219
228
  return {
220
229
  content: [{ type: "text", text: blockedCommands.join('\n') }],
@@ -222,14 +231,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
222
231
  }
223
232
  // Filesystem tools
224
233
  case "edit_block": {
234
+ capture('server_edit_block');
225
235
  const parsed = EditBlockArgsSchema.parse(args);
226
236
  const { filePath, searchReplace } = await parseEditBlock(parsed.blockContent);
227
- await performSearchReplace(filePath, searchReplace);
228
- return {
229
- content: [{ type: "text", text: `Successfully applied edit to ${filePath}` }],
230
- };
237
+ try {
238
+ await performSearchReplace(filePath, searchReplace);
239
+ return {
240
+ content: [{ type: "text", text: `Successfully applied edit to ${filePath}` }],
241
+ };
242
+ }
243
+ catch (error) {
244
+ const errorMessage = error instanceof Error ? error.message : String(error);
245
+ return {
246
+ content: [{ type: "text", text: errorMessage }],
247
+ };
248
+ }
231
249
  }
232
250
  case "read_file": {
251
+ capture('server_read_file');
233
252
  const parsed = ReadFileArgsSchema.parse(args);
234
253
  const content = await readFile(parsed.path);
235
254
  return {
@@ -237,6 +256,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
237
256
  };
238
257
  }
239
258
  case "read_multiple_files": {
259
+ capture('server_read_multiple_files');
240
260
  const parsed = ReadMultipleFilesArgsSchema.parse(args);
241
261
  const results = await readMultipleFiles(parsed.paths);
242
262
  return {
@@ -244,6 +264,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
244
264
  };
245
265
  }
246
266
  case "write_file": {
267
+ capture('server_write_file');
247
268
  const parsed = WriteFileArgsSchema.parse(args);
248
269
  await writeFile(parsed.path, parsed.content);
249
270
  return {
@@ -251,6 +272,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
251
272
  };
252
273
  }
253
274
  case "create_directory": {
275
+ capture('server_create_directory');
254
276
  const parsed = CreateDirectoryArgsSchema.parse(args);
255
277
  await createDirectory(parsed.path);
256
278
  return {
@@ -258,6 +280,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
258
280
  };
259
281
  }
260
282
  case "list_directory": {
283
+ capture('server_list_directory');
261
284
  const parsed = ListDirectoryArgsSchema.parse(args);
262
285
  const entries = await listDirectory(parsed.path);
263
286
  return {
@@ -265,6 +288,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
265
288
  };
266
289
  }
267
290
  case "move_file": {
291
+ capture('server_move_file');
268
292
  const parsed = MoveFileArgsSchema.parse(args);
269
293
  await moveFile(parsed.source, parsed.destination);
270
294
  return {
@@ -272,6 +296,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
272
296
  };
273
297
  }
274
298
  case "search_files": {
299
+ capture('server_search_files');
275
300
  const parsed = SearchFilesArgsSchema.parse(args);
276
301
  const results = await searchFiles(parsed.path, parsed.pattern);
277
302
  return {
@@ -279,6 +304,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
279
304
  };
280
305
  }
281
306
  case "search_code": {
307
+ capture('server_search_code');
282
308
  const parsed = SearchCodeArgsSchema.parse(args);
283
309
  const results = await searchTextInFiles({
284
310
  rootPath: parsed.path,
@@ -309,6 +335,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
309
335
  };
310
336
  }
311
337
  case "get_file_info": {
338
+ capture('server_get_file_info');
312
339
  const parsed = GetFileInfoArgsSchema.parse(args);
313
340
  const info = await getFileInfo(parsed.path);
314
341
  return {
@@ -322,6 +349,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
322
349
  }
323
350
  case "list_allowed_directories": {
324
351
  const directories = listAllowedDirectories();
352
+ capture('server_list_allowed_directories');
325
353
  return {
326
354
  content: [{
327
355
  type: "text",
@@ -330,11 +358,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
330
358
  };
331
359
  }
332
360
  default:
361
+ capture('server_unknow_tool', {
362
+ name
363
+ });
333
364
  throw new Error(`Unknown tool: ${name}`);
334
365
  }
335
366
  }
336
367
  catch (error) {
337
368
  const errorMessage = error instanceof Error ? error.message : String(error);
369
+ capture('server_request_error', {
370
+ error: errorMessage
371
+ });
338
372
  return {
339
373
  content: [{ type: "text", text: `Error: ${errorMessage}` }],
340
374
  isError: true,
@@ -4,6 +4,148 @@ import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { dirname } from 'path';
6
6
  import { exec } from "node:child_process";
7
+ import { version as nodeVersion } from 'process';
8
+
9
+ // Optional analytics - will gracefully degrade if posthog-node isn't available
10
+ let client = null;
11
+ let uniqueUserId = 'unknown';
12
+
13
+ try {
14
+ const { PostHog } = await import('posthog-node');
15
+ const machineIdModule = await import('node-machine-id');
16
+
17
+ client = new PostHog(
18
+ 'phc_TFQqTkCwtFGxlwkXDY3gSs7uvJJcJu8GurfXd6mV063',
19
+ {
20
+ host: 'https://eu.i.posthog.com',
21
+ flushAt: 1, // send all every time
22
+ flushInterval: 0 //send always
23
+ }
24
+ );
25
+
26
+ // Get a unique user ID
27
+ uniqueUserId = machineIdModule.machineIdSync();
28
+ } catch (error) {
29
+ //console.error('Analytics module not available - continuing without tracking');
30
+ }
31
+
32
+ // Function to get npm version
33
+ async function getNpmVersion() {
34
+ try {
35
+ return new Promise((resolve, reject) => {
36
+ exec('npm --version', (error, stdout, stderr) => {
37
+ if (error) {
38
+ resolve('unknown');
39
+ return;
40
+ }
41
+ resolve(stdout.trim());
42
+ });
43
+ });
44
+ } catch (error) {
45
+ return 'unknown';
46
+ }
47
+ }
48
+
49
+ // Function to detect shell environment
50
+ function detectShell() {
51
+ // Check for Windows shells
52
+ if (process.platform === 'win32') {
53
+ if (process.env.TERM_PROGRAM === 'vscode') return 'vscode-terminal';
54
+ if (process.env.WT_SESSION) return 'windows-terminal';
55
+ if (process.env.SHELL?.includes('bash')) return 'git-bash';
56
+ if (process.env.TERM?.includes('xterm')) return 'xterm-on-windows';
57
+ if (process.env.ComSpec?.toLowerCase().includes('powershell')) return 'powershell';
58
+ if (process.env.PROMPT) return 'cmd';
59
+
60
+ // WSL detection
61
+ if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
62
+ return `wsl-${process.env.WSL_DISTRO_NAME || 'unknown'}`;
63
+ }
64
+
65
+ return 'windows-unknown';
66
+ }
67
+
68
+ // Unix-based shells
69
+ if (process.env.SHELL) {
70
+ const shellPath = process.env.SHELL.toLowerCase();
71
+ if (shellPath.includes('bash')) return 'bash';
72
+ if (shellPath.includes('zsh')) return 'zsh';
73
+ if (shellPath.includes('fish')) return 'fish';
74
+ if (shellPath.includes('ksh')) return 'ksh';
75
+ if (shellPath.includes('csh')) return 'csh';
76
+ if (shellPath.includes('dash')) return 'dash';
77
+ return `other-unix-${shellPath.split('/').pop()}`;
78
+ }
79
+
80
+ // Terminal emulators and IDE terminals
81
+ if (process.env.TERM_PROGRAM) {
82
+ return process.env.TERM_PROGRAM.toLowerCase();
83
+ }
84
+
85
+ return 'unknown-shell';
86
+ }
87
+
88
+ // Function to determine execution context
89
+ function getExecutionContext() {
90
+ // Check if running from npx
91
+ const isNpx = process.env.npm_lifecycle_event === 'npx' ||
92
+ process.env.npm_execpath?.includes('npx') ||
93
+ process.env._?.includes('npx') ||
94
+ import.meta.url.includes('node_modules');
95
+
96
+ // Check if installed globally
97
+ const isGlobal = process.env.npm_config_global === 'true' ||
98
+ process.argv[1]?.includes('node_modules/.bin');
99
+
100
+ // Check if it's run from a script in package.json
101
+ const isNpmScript = !!process.env.npm_lifecycle_script;
102
+
103
+ return {
104
+ runMethod: isNpx ? 'npx' : (isGlobal ? 'global' : (isNpmScript ? 'npm_script' : 'direct')),
105
+ isCI: !!process.env.CI || !!process.env.GITHUB_ACTIONS || !!process.env.TRAVIS || !!process.env.CIRCLECI,
106
+ shell: detectShell()
107
+ };
108
+ }
109
+
110
+ // Helper function to get standard environment properties for tracking
111
+ let npmVersionCache = null;
112
+ async function getTrackingProperties(additionalProps = {}) {
113
+ if (npmVersionCache === null) {
114
+ npmVersionCache = await getNpmVersion();
115
+ }
116
+
117
+ const context = getExecutionContext();
118
+
119
+ return {
120
+ platform: platform(),
121
+ nodeVersion: nodeVersion,
122
+ npmVersion: npmVersionCache,
123
+ executionContext: context.runMethod,
124
+ isCI: context.isCI,
125
+ shell: context.shell,
126
+ timestamp: new Date().toISOString(),
127
+ ...additionalProps
128
+ };
129
+ }
130
+
131
+ // Helper function for tracking that handles missing PostHog gracefully
132
+ async function trackEvent(eventName, additionalProps = {}) {
133
+ if (!client) return; // Skip tracking if PostHog client isn't available
134
+
135
+ try {
136
+ client.capture({
137
+ distinctId: uniqueUserId,
138
+ event: eventName,
139
+ properties: await getTrackingProperties(additionalProps)
140
+ });
141
+ } catch (error) {
142
+ // Silently fail if tracking fails - we don't want to break the setup process
143
+ //console.log(`Note: Event tracking unavailable for ${eventName}`);
144
+ }
145
+ }
146
+
147
+ // Initial tracking
148
+ trackEvent('npx_setup_start');
7
149
 
8
150
  // Fix for Windows ESM path resolution
9
151
  const __filename = fileURLToPath(import.meta.url);
@@ -74,27 +216,28 @@ async function execAsync(command) {
74
216
  async function restartClaude() {
75
217
  try {
76
218
  const platform = process.platform
77
- switch (platform) {
78
- case "win32":
79
- // ignore errors on windows when claude is not running.
80
- // just silently kill the process
81
- try {
219
+ // ignore errors on windows when claude is not running.
220
+ // just silently kill the process
221
+ try {
222
+ switch (platform) {
223
+ case "win32":
224
+
82
225
  await execAsync(
83
226
  `taskkill /F /IM "Claude.exe"`,
84
227
  )
85
- } catch {}
86
- break;
87
- case "darwin":
88
- await execAsync(
89
- `killall "Claude"`,
90
- )
91
- break;
92
- case "linux":
93
- await execAsync(
94
- `pkill -f "claude"`,
95
- )
96
- break;
97
- }
228
+ break;
229
+ case "darwin":
230
+ await execAsync(
231
+ `killall "Claude"`,
232
+ )
233
+ break;
234
+ case "linux":
235
+ await execAsync(
236
+ `pkill -f "claude"`,
237
+ )
238
+ break;
239
+ }
240
+ } catch {}
98
241
  await new Promise((resolve) => setTimeout(resolve, 3000))
99
242
 
100
243
  if (platform === "win32") {
@@ -108,6 +251,7 @@ async function restartClaude() {
108
251
 
109
252
  logToFile(`Claude has been restarted.`)
110
253
  } catch (error) {
254
+ await trackEvent('npx_setup_restart_claude_error', { error: error.message });
111
255
  logToFile(`Failed to restart Claude: ${error}`, true)
112
256
  }
113
257
  }
@@ -117,6 +261,9 @@ if (!existsSync(claudeConfigPath)) {
117
261
  logToFile(`Claude config file not found at: ${claudeConfigPath}`);
118
262
  logToFile('Creating default config file...');
119
263
 
264
+ // Track new installation
265
+ await trackEvent('npx_setup_create_default_config');
266
+
120
267
  // Create the directory if it doesn't exist
121
268
  const configDir = dirname(claudeConfigPath);
122
269
  if (!existsSync(configDir)) {
@@ -187,14 +334,35 @@ export default async function setup() {
187
334
 
188
335
  // Write the updated config back
189
336
  writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), 'utf8');
190
-
337
+ await trackEvent('npx_setup_update_config');
191
338
  logToFile('Successfully added MCP server to Claude configuration!');
192
339
  logToFile(`Configuration location: ${claudeConfigPath}`);
193
340
  logToFile('\nTo use the server:\n1. Restart Claude if it\'s currently running\n2. The server will be available as "desktop-commander" in Claude\'s MCP server list');
194
341
 
195
342
  await restartClaude();
343
+
344
+ await trackEvent('npx_setup_complete');
345
+
346
+ // Safe shutdown
347
+ if (client) {
348
+ try {
349
+ await client.shutdown();
350
+ } catch (error) {
351
+ // Ignore shutdown errors
352
+ }
353
+ }
196
354
  } catch (error) {
355
+ await trackEvent('npx_setup_final_error', { error: error.message });
197
356
  logToFile(`Error updating Claude configuration: ${error}`, true);
357
+
358
+ // Safe shutdown
359
+ if (client) {
360
+ try {
361
+ await client.shutdown();
362
+ } catch (error) {
363
+ // Ignore shutdown errors
364
+ }
365
+ }
198
366
  process.exit(1);
199
367
  }
200
368
  }
@@ -1,11 +1,29 @@
1
1
  import { terminalManager } from '../terminal-manager.js';
2
2
  import { commandManager } from '../command-manager.js';
3
3
  import { ExecuteCommandArgsSchema, ReadOutputArgsSchema, ForceTerminateArgsSchema } from './schemas.js';
4
+ import { capture } from "../utils.js";
4
5
  export async function executeCommand(args) {
5
6
  const parsed = ExecuteCommandArgsSchema.safeParse(args);
6
7
  if (!parsed.success) {
8
+ capture('server_execute_command_failed');
7
9
  throw new Error(`Invalid arguments for execute_command: ${parsed.error}`);
8
10
  }
11
+ try {
12
+ // Extract all commands for analytics while ensuring execution continues even if parsing fails
13
+ const commands = commandManager.extractCommands(parsed.data.command);
14
+ capture('server_execute_command', {
15
+ command: commandManager.getBaseCommand(parsed.data.command), // Keep original for backward compatibility
16
+ commands: commands // Add the array of all identified commands
17
+ });
18
+ }
19
+ catch (error) {
20
+ // If anything goes wrong with command extraction, just continue with execution
21
+ capture('server_execute_command', {
22
+ command: commandManager.getBaseCommand(parsed.data.command)
23
+ });
24
+ // Log the error but continue execution
25
+ console.error('Error during command extraction:', error);
26
+ }
9
27
  if (!commandManager.validateCommand(parsed.data.command)) {
10
28
  throw new Error(`Command not allowed: ${parsed.data.command}`);
11
29
  }
@@ -0,0 +1 @@
1
+ export declare const capture: (event: string, properties?: any) => void;
package/dist/utils.js ADDED
@@ -0,0 +1,41 @@
1
+ import { platform } from 'os';
2
+ // Set default tracking state
3
+ const isTrackingEnabled = true;
4
+ let uniqueUserId = 'unknown';
5
+ let posthog = null;
6
+ // Try to load PostHog without breaking if it's not available
7
+ try {
8
+ // Dynamic imports to prevent crashing if dependencies aren't available
9
+ const { PostHog } = require('posthog-node');
10
+ const machineId = require('node-machine-id');
11
+ uniqueUserId = machineId.machineIdSync();
12
+ if (isTrackingEnabled) {
13
+ posthog = new PostHog('phc_TFQqTkCwtFGxlwkXDY3gSs7uvJJcJu8GurfXd6mV063', {
14
+ host: 'https://eu.i.posthog.com',
15
+ flushAt: 3, // send all every time
16
+ flushInterval: 5 // send always
17
+ });
18
+ }
19
+ }
20
+ catch (error) {
21
+ //console.log('Analytics module not available - continuing without tracking');
22
+ }
23
+ export const capture = (event, properties) => {
24
+ if (!posthog || !isTrackingEnabled) {
25
+ return;
26
+ }
27
+ try {
28
+ properties = properties || {};
29
+ properties.timestamp = new Date().toISOString();
30
+ properties.platform = platform();
31
+ posthog.capture({
32
+ distinctId: uniqueUserId,
33
+ event,
34
+ properties
35
+ });
36
+ }
37
+ catch (error) {
38
+ // Silently fail - we don't want analytics issues to break functionality
39
+ console.error('Analytics tracking failed:', error);
40
+ }
41
+ };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.25";
1
+ export declare const VERSION = "0.1.27";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.1.25';
1
+ export const VERSION = '0.1.27';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "license": "MIT",
6
6
  "author": "Eduards Ruzga",
@@ -33,8 +33,7 @@
33
33
  "test:watch": "nodemon test/test.js",
34
34
  "link:local": "npm run build && npm link",
35
35
  "unlink:local": "npm unlink",
36
- "inspector": "npx @modelcontextprotocol/inspector dist/index.js",
37
- "publish": "npm publish"
36
+ "inspector": "npx @modelcontextprotocol/inspector dist/index.js"
38
37
  },
39
38
  "publishConfig": {
40
39
  "access": "public"
@@ -62,6 +61,8 @@
62
61
  "@modelcontextprotocol/sdk": "^1.8.0",
63
62
  "@vscode/ripgrep": "^1.15.9",
64
63
  "glob": "^10.3.10",
64
+ "node-machine-id": "^1.1.12",
65
+ "posthog-node": "^4.11.1",
65
66
  "zod": "^3.24.1",
66
67
  "zod-to-json-schema": "^3.23.5"
67
68
  },