@wonderwhy-er/desktop-commander 0.1.26 → 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
@@ -53,7 +53,7 @@ First, ensure you've downloaded and installed the [Claude Desktop app](https://c
53
53
  ### Option 1: Install through npx
54
54
  Just run this in terminal
55
55
  ```
56
- npx @wonderwhy-er/desktop-commander setup
56
+ npx @wonderwhy-er/desktop-commander@latest setup
57
57
  ```
58
58
  Restart Claude if running
59
59
 
@@ -3,6 +3,8 @@ declare class CommandManager {
3
3
  loadBlockedCommands(): Promise<void>;
4
4
  saveBlockedCommands(): Promise<void>;
5
5
  getBaseCommand(command: string): string;
6
+ extractCommands(commandString: string): string[];
7
+ extractBaseCommand(commandStr: string): string | null;
6
8
  validateCommand(command: string): boolean;
7
9
  blockCommand(command: string): Promise<boolean>;
8
10
  unblockCommand(command: string): Promise<boolean>;
@@ -28,6 +28,132 @@ class CommandManager {
28
28
  getBaseCommand(command) {
29
29
  return command.split(' ')[0].toLowerCase().trim();
30
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
+ }
31
157
  validateCommand(command) {
32
158
  const baseCommand = this.getBaseCommand(command);
33
159
  return !this.blockedCommands.has(baseCommand);
package/dist/server.js CHANGED
@@ -186,13 +186,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
186
186
  return executeCommand(parsed);
187
187
  }
188
188
  case "read_output": {
189
- const parsed = ReadOutputArgsSchema.parse(args);
190
189
  capture('server_read_output');
190
+ const parsed = ReadOutputArgsSchema.parse(args);
191
191
  return readOutput(parsed);
192
192
  }
193
193
  case "force_terminate": {
194
- const parsed = ForceTerminateArgsSchema.parse(args);
195
194
  capture('server_force_terminate');
195
+ const parsed = ForceTerminateArgsSchema.parse(args);
196
196
  return forceTerminate(parsed);
197
197
  }
198
198
  case "list_sessions":
@@ -202,11 +202,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
202
202
  capture('server_list_processes');
203
203
  return listProcesses();
204
204
  case "kill_process": {
205
- const parsed = KillProcessArgsSchema.parse(args);
206
205
  capture('server_kill_process');
206
+ const parsed = KillProcessArgsSchema.parse(args);
207
207
  return killProcess(parsed);
208
208
  }
209
209
  case "block_command": {
210
+ capture('server_block_command');
210
211
  const parsed = BlockCommandArgsSchema.parse(args);
211
212
  const blockResult = await commandManager.blockCommand(parsed.command);
212
213
  return {
@@ -214,6 +215,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
214
215
  };
215
216
  }
216
217
  case "unblock_command": {
218
+ capture('server_unblock_command');
217
219
  const parsed = UnblockCommandArgsSchema.parse(args);
218
220
  const unblockResult = await commandManager.unblockCommand(parsed.command);
219
221
  return {
@@ -221,6 +223,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
221
223
  };
222
224
  }
223
225
  case "list_blocked_commands": {
226
+ capture('server_list_blocked_commands');
224
227
  const blockedCommands = await commandManager.listBlockedCommands();
225
228
  return {
226
229
  content: [{ type: "text", text: blockedCommands.join('\n') }],
@@ -228,71 +231,80 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
228
231
  }
229
232
  // Filesystem tools
230
233
  case "edit_block": {
234
+ capture('server_edit_block');
231
235
  const parsed = EditBlockArgsSchema.parse(args);
232
236
  const { filePath, searchReplace } = await parseEditBlock(parsed.blockContent);
233
- await performSearchReplace(filePath, searchReplace);
234
- capture('server_edit_block');
235
- return {
236
- content: [{ type: "text", text: `Successfully applied edit to ${filePath}` }],
237
- };
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
+ }
238
249
  }
239
250
  case "read_file": {
251
+ capture('server_read_file');
240
252
  const parsed = ReadFileArgsSchema.parse(args);
241
253
  const content = await readFile(parsed.path);
242
- capture('server_read_file');
243
254
  return {
244
255
  content: [{ type: "text", text: content }],
245
256
  };
246
257
  }
247
258
  case "read_multiple_files": {
259
+ capture('server_read_multiple_files');
248
260
  const parsed = ReadMultipleFilesArgsSchema.parse(args);
249
261
  const results = await readMultipleFiles(parsed.paths);
250
- capture('server_read_multiple_files');
251
262
  return {
252
263
  content: [{ type: "text", text: results.join("\n---\n") }],
253
264
  };
254
265
  }
255
266
  case "write_file": {
267
+ capture('server_write_file');
256
268
  const parsed = WriteFileArgsSchema.parse(args);
257
269
  await writeFile(parsed.path, parsed.content);
258
- capture('server_write_file');
259
270
  return {
260
271
  content: [{ type: "text", text: `Successfully wrote to ${parsed.path}` }],
261
272
  };
262
273
  }
263
274
  case "create_directory": {
275
+ capture('server_create_directory');
264
276
  const parsed = CreateDirectoryArgsSchema.parse(args);
265
277
  await createDirectory(parsed.path);
266
- capture('server_create_directory');
267
278
  return {
268
279
  content: [{ type: "text", text: `Successfully created directory ${parsed.path}` }],
269
280
  };
270
281
  }
271
282
  case "list_directory": {
283
+ capture('server_list_directory');
272
284
  const parsed = ListDirectoryArgsSchema.parse(args);
273
285
  const entries = await listDirectory(parsed.path);
274
- capture('server_list_directory');
275
286
  return {
276
287
  content: [{ type: "text", text: entries.join('\n') }],
277
288
  };
278
289
  }
279
290
  case "move_file": {
291
+ capture('server_move_file');
280
292
  const parsed = MoveFileArgsSchema.parse(args);
281
293
  await moveFile(parsed.source, parsed.destination);
282
- capture('server_move_file');
283
294
  return {
284
295
  content: [{ type: "text", text: `Successfully moved ${parsed.source} to ${parsed.destination}` }],
285
296
  };
286
297
  }
287
298
  case "search_files": {
299
+ capture('server_search_files');
288
300
  const parsed = SearchFilesArgsSchema.parse(args);
289
301
  const results = await searchFiles(parsed.path, parsed.pattern);
290
- capture('server_search_files');
291
302
  return {
292
303
  content: [{ type: "text", text: results.length > 0 ? results.join('\n') : "No matches found" }],
293
304
  };
294
305
  }
295
306
  case "search_code": {
307
+ capture('server_search_code');
296
308
  const parsed = SearchCodeArgsSchema.parse(args);
297
309
  const results = await searchTextInFiles({
298
310
  rootPath: parsed.path,
@@ -303,7 +315,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
303
315
  includeHidden: parsed.includeHidden,
304
316
  contextLines: parsed.contextLines,
305
317
  });
306
- capture('server_search_code');
307
318
  if (results.length === 0) {
308
319
  return {
309
320
  content: [{ type: "text", text: "No matches found" }],
@@ -324,9 +335,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
324
335
  };
325
336
  }
326
337
  case "get_file_info": {
338
+ capture('server_get_file_info');
327
339
  const parsed = GetFileInfoArgsSchema.parse(args);
328
340
  const info = await getFileInfo(parsed.path);
329
- capture('server_get_file_info');
330
341
  return {
331
342
  content: [{
332
343
  type: "text",
@@ -347,6 +358,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
347
358
  };
348
359
  }
349
360
  default:
361
+ capture('server_unknow_tool', {
362
+ name
363
+ });
350
364
  throw new Error(`Unknown tool: ${name}`);
351
365
  }
352
366
  }
@@ -4,20 +4,30 @@ 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 { PostHog } from 'posthog-node';
8
- import machineId from 'node-machine-id';
9
7
  import { version as nodeVersion } from 'process';
10
8
 
11
- const client = new PostHog(
12
- 'phc_TFQqTkCwtFGxlwkXDY3gSs7uvJJcJu8GurfXd6mV063',
13
- {
14
- host: 'https://eu.i.posthog.com',
15
- flushAt: 1, // send all every time
16
- flushInterval: 0 //send always
17
- }
18
- )
19
- // Get a unique user ID
20
- const uniqueUserId = machineId.machineIdSync();
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
+ }
21
31
 
22
32
  // Function to get npm version
23
33
  async function getNpmVersion() {
@@ -118,14 +128,24 @@ async function getTrackingProperties(additionalProps = {}) {
118
128
  };
119
129
  }
120
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
+
121
147
  // Initial tracking
122
- (async () => {
123
- client.capture({
124
- distinctId: uniqueUserId,
125
- event: 'npx_setup_start',
126
- properties: await getTrackingProperties()
127
- });
128
- })();
148
+ trackEvent('npx_setup_start');
129
149
 
130
150
  // Fix for Windows ESM path resolution
131
151
  const __filename = fileURLToPath(import.meta.url);
@@ -231,11 +251,7 @@ async function restartClaude() {
231
251
 
232
252
  logToFile(`Claude has been restarted.`)
233
253
  } catch (error) {
234
- client.capture({
235
- distinctId: uniqueUserId,
236
- event: 'npx_setup_restart_claude_error',
237
- properties: await getTrackingProperties({ error: error.message })
238
- });
254
+ await trackEvent('npx_setup_restart_claude_error', { error: error.message });
239
255
  logToFile(`Failed to restart Claude: ${error}`, true)
240
256
  }
241
257
  }
@@ -246,11 +262,7 @@ if (!existsSync(claudeConfigPath)) {
246
262
  logToFile('Creating default config file...');
247
263
 
248
264
  // Track new installation
249
- client.capture({
250
- distinctId: uniqueUserId,
251
- event: 'npx_setup_create_default_config',
252
- properties: await getTrackingProperties()
253
- });
265
+ await trackEvent('npx_setup_create_default_config');
254
266
 
255
267
  // Create the directory if it doesn't exist
256
268
  const configDir = dirname(claudeConfigPath);
@@ -322,31 +334,35 @@ export default async function setup() {
322
334
 
323
335
  // Write the updated config back
324
336
  writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), 'utf8');
325
- client.capture({
326
- distinctId: uniqueUserId,
327
- event: 'npx_setup_update_config',
328
- properties: await getTrackingProperties()
329
- });
337
+ await trackEvent('npx_setup_update_config');
330
338
  logToFile('Successfully added MCP server to Claude configuration!');
331
339
  logToFile(`Configuration location: ${claudeConfigPath}`);
332
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');
333
341
 
334
342
  await restartClaude();
335
343
 
336
- client.capture({
337
- distinctId: uniqueUserId,
338
- event: 'npx_setup_complete',
339
- properties: await getTrackingProperties()
340
- });
341
- await client.shutdown()
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
+ }
342
354
  } catch (error) {
343
- client.capture({
344
- distinctId: uniqueUserId,
345
- event: 'npx_setup_final_error',
346
- properties: await getTrackingProperties({ error: error.message })
347
- });
355
+ await trackEvent('npx_setup_final_error', { error: error.message });
348
356
  logToFile(`Error updating Claude configuration: ${error}`, true);
349
- await client.shutdown()
357
+
358
+ // Safe shutdown
359
+ if (client) {
360
+ try {
361
+ await client.shutdown();
362
+ } catch (error) {
363
+ // Ignore shutdown errors
364
+ }
365
+ }
350
366
  process.exit(1);
351
367
  }
352
368
  }
@@ -8,9 +8,22 @@ export async function executeCommand(args) {
8
8
  capture('server_execute_command_failed');
9
9
  throw new Error(`Invalid arguments for execute_command: ${parsed.error}`);
10
10
  }
11
- capture('server_execute_command', {
12
- command: commandManager.getBaseCommand(parsed.data.command)
13
- });
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
+ }
14
27
  if (!commandManager.validateCommand(parsed.data.command)) {
15
28
  throw new Error(`Command not allowed: ${parsed.data.command}`);
16
29
  }
package/dist/utils.js CHANGED
@@ -1,23 +1,41 @@
1
- import { PostHog } from 'posthog-node';
2
- import machineId from 'node-machine-id';
3
1
  import { platform } from 'os';
2
+ // Set default tracking state
4
3
  const isTrackingEnabled = true;
5
- const uniqueUserId = machineId.machineIdSync();
6
- const posthog = isTrackingEnabled ? new PostHog('phc_TFQqTkCwtFGxlwkXDY3gSs7uvJJcJu8GurfXd6mV063', {
7
- host: 'https://eu.i.posthog.com',
8
- flushAt: 3, // send all every time
9
- flushInterval: 5 //send always
10
- }) : null;
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
+ }
11
23
  export const capture = (event, properties) => {
12
24
  if (!posthog || !isTrackingEnabled) {
13
25
  return;
14
26
  }
15
- properties = properties || {};
16
- properties.timestamp = new Date().toISOString();
17
- properties.platform = platform();
18
- posthog.capture({
19
- distinctId: uniqueUserId,
20
- event,
21
- properties
22
- });
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
+ }
23
41
  };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.26";
1
+ export declare const VERSION = "0.1.27";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.1.26';
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.26",
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"