@wonderwhy-er/desktop-commander 0.1.26 → 0.1.28

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,4 +1,5 @@
1
1
  # Desktop Commander MCP
2
+ ### Search, update, manage files and run terminal commands with AI
2
3
 
3
4
  [![npm downloads](https://img.shields.io/npm/dw/@wonderwhy-er/desktop-commander)](https://www.npmjs.com/package/@wonderwhy-er/desktop-commander)
4
5
  [![smithery badge](https://smithery.ai/badge/@wonderwhy-er/desktop-commander)](https://smithery.ai/server/@wonderwhy-er/desktop-commander)
@@ -8,8 +9,8 @@
8
9
 
9
10
  Short version. Two key things. Terminal commands and diff based file editing.
10
11
 
11
- ![Desktop Commander MCP](https://raw.githubusercontent.com/wonderwhy-er/ClaudeComputerCommander/main/logo.png)
12
12
 
13
+ ![Desktop Commander MCP](https://raw.githubusercontent.com/wonderwhy-er/ClaudeComputerCommander/main/header.png)
13
14
  <a href="https://glama.ai/mcp/servers/zempur9oh4">
14
15
  <img width="380" height="200" src="https://glama.ai/mcp/servers/zempur9oh4/badge" alt="Claude Desktop Commander MCP server" />
15
16
  </a>
@@ -53,7 +54,12 @@ First, ensure you've downloaded and installed the [Claude Desktop app](https://c
53
54
  ### Option 1: Install through npx
54
55
  Just run this in terminal
55
56
  ```
56
- npx @wonderwhy-er/desktop-commander setup
57
+ npx @wonderwhy-er/desktop-commander@latest setup
58
+ ```
59
+
60
+ For debugging mode (allows Node.js inspector connection):
61
+ ```
62
+ npx @wonderwhy-er/desktop-commander@latest setup --debug
57
63
  ```
58
64
  Restart Claude if running
59
65
 
@@ -156,6 +162,40 @@ For commands that may take a while:
156
162
  3. Use `read_output` with PID to get new output
157
163
  4. Use `force_terminate` to stop if needed
158
164
 
165
+ ## Debugging
166
+
167
+ If you need to debug the server, you can install it in debug mode:
168
+
169
+ ```bash
170
+ # Using npx
171
+ npx @wonderwhy-er/desktop-commander@latest setup --debug
172
+
173
+ # Or if installed locally
174
+ npm run setup:debug
175
+ ```
176
+
177
+ This will:
178
+ 1. Configure Claude to use a separate "desktop-commander" server
179
+ 2. Enable Node.js inspector protocol with `--inspect-brk=9229` flag
180
+ 3. Pause execution at the start until a debugger connects
181
+ 4. Enable additional debugging environment variables
182
+
183
+ To connect a debugger:
184
+ - In Chrome, visit `chrome://inspect` and look for the Node.js instance
185
+ - In VS Code, use the "Attach to Node Process" debug configuration
186
+ - Other IDEs/tools may have similar "attach" options for Node.js debugging
187
+
188
+ Important debugging notes:
189
+ - The server will pause on startup until a debugger connects (due to the `--inspect-brk` flag)
190
+ - If you don't see activity during debugging, ensure you're connected to the correct Node.js process
191
+ - Multiple Node processes may be running; connect to the one on port 9229
192
+ - The debug server is identified as "desktop-commander-debug" in Claude's MCP server list
193
+
194
+ Troubleshooting:
195
+ - If Claude times out while trying to use the debug server, your debugger might not be properly connected
196
+ - When properly connected, the process will continue execution after hitting the first breakpoint
197
+ - You can add additional breakpoints in your IDE once connected
198
+
159
199
  ## Model Context Protocol Integration
160
200
 
161
201
  This project extends the MCP Filesystem Server to enable:
@@ -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/index.js CHANGED
@@ -80,6 +80,7 @@ async function runServer() {
80
80
  process.stderr.write(`[desktop-commander] Unhandled rejection: ${errorMessage}\n`);
81
81
  process.exit(1);
82
82
  });
83
+ capture('run_server_start');
83
84
  // Load blocked commands from config file
84
85
  await commandManager.loadBlockedCommands();
85
86
  await server.connect(transport);
package/dist/server.js CHANGED
@@ -179,20 +179,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
179
179
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
180
180
  try {
181
181
  const { name, arguments: args } = request.params;
182
+ capture('server_call_tool');
182
183
  switch (name) {
183
184
  // Terminal tools
184
185
  case "execute_command": {
186
+ capture('server_execute_command');
185
187
  const parsed = ExecuteCommandArgsSchema.parse(args);
186
188
  return executeCommand(parsed);
187
189
  }
188
190
  case "read_output": {
189
- const parsed = ReadOutputArgsSchema.parse(args);
190
191
  capture('server_read_output');
192
+ const parsed = ReadOutputArgsSchema.parse(args);
191
193
  return readOutput(parsed);
192
194
  }
193
195
  case "force_terminate": {
194
- const parsed = ForceTerminateArgsSchema.parse(args);
195
196
  capture('server_force_terminate');
197
+ const parsed = ForceTerminateArgsSchema.parse(args);
196
198
  return forceTerminate(parsed);
197
199
  }
198
200
  case "list_sessions":
@@ -202,11 +204,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
202
204
  capture('server_list_processes');
203
205
  return listProcesses();
204
206
  case "kill_process": {
205
- const parsed = KillProcessArgsSchema.parse(args);
206
207
  capture('server_kill_process');
208
+ const parsed = KillProcessArgsSchema.parse(args);
207
209
  return killProcess(parsed);
208
210
  }
209
211
  case "block_command": {
212
+ capture('server_block_command');
210
213
  const parsed = BlockCommandArgsSchema.parse(args);
211
214
  const blockResult = await commandManager.blockCommand(parsed.command);
212
215
  return {
@@ -214,6 +217,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
214
217
  };
215
218
  }
216
219
  case "unblock_command": {
220
+ capture('server_unblock_command');
217
221
  const parsed = UnblockCommandArgsSchema.parse(args);
218
222
  const unblockResult = await commandManager.unblockCommand(parsed.command);
219
223
  return {
@@ -221,6 +225,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
221
225
  };
222
226
  }
223
227
  case "list_blocked_commands": {
228
+ capture('server_list_blocked_commands');
224
229
  const blockedCommands = await commandManager.listBlockedCommands();
225
230
  return {
226
231
  content: [{ type: "text", text: blockedCommands.join('\n') }],
@@ -228,85 +233,158 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
228
233
  }
229
234
  // Filesystem tools
230
235
  case "edit_block": {
231
- const parsed = EditBlockArgsSchema.parse(args);
232
- const { filePath, searchReplace } = await parseEditBlock(parsed.blockContent);
233
- await performSearchReplace(filePath, searchReplace);
234
236
  capture('server_edit_block');
235
- return {
236
- content: [{ type: "text", text: `Successfully applied edit to ${filePath}` }],
237
- };
237
+ try {
238
+ const parsed = EditBlockArgsSchema.parse(args);
239
+ const { filePath, searchReplace } = await parseEditBlock(parsed.blockContent);
240
+ await performSearchReplace(filePath, searchReplace);
241
+ return {
242
+ content: [{ type: "text", text: `Successfully applied edit to ${filePath}` }],
243
+ };
244
+ }
245
+ catch (error) {
246
+ const errorMessage = error instanceof Error ? error.message : String(error);
247
+ return {
248
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
249
+ };
250
+ }
238
251
  }
239
252
  case "read_file": {
240
- const parsed = ReadFileArgsSchema.parse(args);
241
- const content = await readFile(parsed.path);
242
253
  capture('server_read_file');
243
- return {
244
- content: [{ type: "text", text: content }],
245
- };
254
+ try {
255
+ const parsed = ReadFileArgsSchema.parse(args);
256
+ const content = await readFile(parsed.path);
257
+ return {
258
+ content: [{ type: "text", text: content }],
259
+ };
260
+ }
261
+ catch (error) {
262
+ const errorMessage = error instanceof Error ? error.message : String(error);
263
+ return {
264
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
265
+ };
266
+ }
246
267
  }
247
268
  case "read_multiple_files": {
248
- const parsed = ReadMultipleFilesArgsSchema.parse(args);
249
- const results = await readMultipleFiles(parsed.paths);
250
269
  capture('server_read_multiple_files');
251
- return {
252
- content: [{ type: "text", text: results.join("\n---\n") }],
253
- };
270
+ try {
271
+ const parsed = ReadMultipleFilesArgsSchema.parse(args);
272
+ const results = await readMultipleFiles(parsed.paths);
273
+ return {
274
+ content: [{ type: "text", text: results.join("\n---\n") }],
275
+ };
276
+ }
277
+ catch (error) {
278
+ const errorMessage = error instanceof Error ? error.message : String(error);
279
+ return {
280
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
281
+ };
282
+ }
254
283
  }
255
284
  case "write_file": {
256
- const parsed = WriteFileArgsSchema.parse(args);
257
- await writeFile(parsed.path, parsed.content);
258
285
  capture('server_write_file');
259
- return {
260
- content: [{ type: "text", text: `Successfully wrote to ${parsed.path}` }],
261
- };
286
+ try {
287
+ const parsed = WriteFileArgsSchema.parse(args);
288
+ await writeFile(parsed.path, parsed.content);
289
+ return {
290
+ content: [{ type: "text", text: `Successfully wrote to ${parsed.path}` }],
291
+ };
292
+ }
293
+ catch (error) {
294
+ const errorMessage = error instanceof Error ? error.message : String(error);
295
+ return {
296
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
297
+ };
298
+ }
262
299
  }
263
300
  case "create_directory": {
264
- const parsed = CreateDirectoryArgsSchema.parse(args);
265
- await createDirectory(parsed.path);
266
301
  capture('server_create_directory');
267
- return {
268
- content: [{ type: "text", text: `Successfully created directory ${parsed.path}` }],
269
- };
302
+ try {
303
+ const parsed = CreateDirectoryArgsSchema.parse(args);
304
+ await createDirectory(parsed.path);
305
+ return {
306
+ content: [{ type: "text", text: `Successfully created directory ${parsed.path}` }],
307
+ };
308
+ }
309
+ catch (error) {
310
+ const errorMessage = error instanceof Error ? error.message : String(error);
311
+ return {
312
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
313
+ };
314
+ }
270
315
  }
271
316
  case "list_directory": {
272
- const parsed = ListDirectoryArgsSchema.parse(args);
273
- const entries = await listDirectory(parsed.path);
274
317
  capture('server_list_directory');
275
- return {
276
- content: [{ type: "text", text: entries.join('\n') }],
277
- };
318
+ try {
319
+ const parsed = ListDirectoryArgsSchema.parse(args);
320
+ const entries = await listDirectory(parsed.path);
321
+ return {
322
+ content: [{ type: "text", text: entries.join('\n') }],
323
+ };
324
+ }
325
+ catch (error) {
326
+ const errorMessage = error instanceof Error ? error.message : String(error);
327
+ return {
328
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
329
+ };
330
+ }
278
331
  }
279
332
  case "move_file": {
280
- const parsed = MoveFileArgsSchema.parse(args);
281
- await moveFile(parsed.source, parsed.destination);
282
333
  capture('server_move_file');
283
- return {
284
- content: [{ type: "text", text: `Successfully moved ${parsed.source} to ${parsed.destination}` }],
285
- };
334
+ try {
335
+ const parsed = MoveFileArgsSchema.parse(args);
336
+ await moveFile(parsed.source, parsed.destination);
337
+ return {
338
+ content: [{ type: "text", text: `Successfully moved ${parsed.source} to ${parsed.destination}` }],
339
+ };
340
+ }
341
+ catch (error) {
342
+ const errorMessage = error instanceof Error ? error.message : String(error);
343
+ return {
344
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
345
+ };
346
+ }
286
347
  }
287
348
  case "search_files": {
288
- const parsed = SearchFilesArgsSchema.parse(args);
289
- const results = await searchFiles(parsed.path, parsed.pattern);
290
349
  capture('server_search_files');
291
- return {
292
- content: [{ type: "text", text: results.length > 0 ? results.join('\n') : "No matches found" }],
293
- };
350
+ try {
351
+ const parsed = SearchFilesArgsSchema.parse(args);
352
+ const results = await searchFiles(parsed.path, parsed.pattern);
353
+ return {
354
+ content: [{ type: "text", text: results.length > 0 ? results.join('\n') : "No matches found" }],
355
+ };
356
+ }
357
+ catch (error) {
358
+ const errorMessage = error instanceof Error ? error.message : String(error);
359
+ return {
360
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
361
+ };
362
+ }
294
363
  }
295
364
  case "search_code": {
296
- const parsed = SearchCodeArgsSchema.parse(args);
297
- const results = await searchTextInFiles({
298
- rootPath: parsed.path,
299
- pattern: parsed.pattern,
300
- filePattern: parsed.filePattern,
301
- ignoreCase: parsed.ignoreCase,
302
- maxResults: parsed.maxResults,
303
- includeHidden: parsed.includeHidden,
304
- contextLines: parsed.contextLines,
305
- });
306
365
  capture('server_search_code');
307
- if (results.length === 0) {
366
+ let results = [];
367
+ try {
368
+ const parsed = SearchCodeArgsSchema.parse(args);
369
+ results = await searchTextInFiles({
370
+ rootPath: parsed.path,
371
+ pattern: parsed.pattern,
372
+ filePattern: parsed.filePattern,
373
+ ignoreCase: parsed.ignoreCase,
374
+ maxResults: parsed.maxResults,
375
+ includeHidden: parsed.includeHidden,
376
+ contextLines: parsed.contextLines,
377
+ });
378
+ if (results.length === 0) {
379
+ return {
380
+ content: [{ type: "text", text: "No matches found" }],
381
+ };
382
+ }
383
+ }
384
+ catch (error) {
385
+ const errorMessage = error instanceof Error ? error.message : String(error);
308
386
  return {
309
- content: [{ type: "text", text: "No matches found" }],
387
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
310
388
  };
311
389
  }
312
390
  // Format the results in a VS Code-like format
@@ -324,21 +402,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
324
402
  };
325
403
  }
326
404
  case "get_file_info": {
327
- const parsed = GetFileInfoArgsSchema.parse(args);
328
- const info = await getFileInfo(parsed.path);
329
405
  capture('server_get_file_info');
330
- return {
331
- content: [{
332
- type: "text",
333
- text: Object.entries(info)
334
- .map(([key, value]) => `${key}: ${value}`)
335
- .join('\n')
336
- }],
337
- };
406
+ try {
407
+ const parsed = GetFileInfoArgsSchema.parse(args);
408
+ const info = await getFileInfo(parsed.path);
409
+ return {
410
+ content: [{
411
+ type: "text",
412
+ text: Object.entries(info)
413
+ .map(([key, value]) => `${key}: ${value}`)
414
+ .join('\n')
415
+ }],
416
+ };
417
+ }
418
+ catch (error) {
419
+ const errorMessage = error instanceof Error ? error.message : String(error);
420
+ return {
421
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
422
+ };
423
+ }
338
424
  }
339
425
  case "list_allowed_directories": {
340
- const directories = listAllowedDirectories();
341
426
  capture('server_list_allowed_directories');
427
+ const directories = listAllowedDirectories();
342
428
  return {
343
429
  content: [{
344
430
  type: "text",
@@ -347,6 +433,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
347
433
  };
348
434
  }
349
435
  default:
436
+ capture('server_unknow_tool', {
437
+ name
438
+ });
350
439
  throw new Error(`Unknown tool: ${name}`);
351
440
  }
352
441
  }
@@ -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);
@@ -219,23 +239,20 @@ async function restartClaude() {
219
239
  }
220
240
  } catch {}
221
241
  await new Promise((resolve) => setTimeout(resolve, 3000))
222
-
223
- if (platform === "win32") {
224
- // it will never start claude
225
- // await execAsync(`start "" "Claude.exe"`)
226
- } else if (platform === "darwin") {
227
- await execAsync(`open -a "Claude"`)
228
- } else if (platform === "linux") {
229
- await execAsync(`claude`)
230
- }
242
+ try {
243
+ if (platform === "win32") {
244
+ // it will never start claude
245
+ // await execAsync(`start "" "Claude.exe"`)
246
+ } else if (platform === "darwin") {
247
+ await execAsync(`open -a "Claude"`)
248
+ } else if (platform === "linux") {
249
+ await execAsync(`claude`)
250
+ }
251
+ } catch{}
231
252
 
232
253
  logToFile(`Claude has been restarted.`)
233
254
  } catch (error) {
234
- client.capture({
235
- distinctId: uniqueUserId,
236
- event: 'npx_setup_restart_claude_error',
237
- properties: await getTrackingProperties({ error: error.message })
238
- });
255
+ await trackEvent('npx_setup_restart_claude_error', { error: error.message });
239
256
  logToFile(`Failed to restart Claude: ${error}`, true)
240
257
  }
241
258
  }
@@ -246,11 +263,7 @@ if (!existsSync(claudeConfigPath)) {
246
263
  logToFile('Creating default config file...');
247
264
 
248
265
  // Track new installation
249
- client.capture({
250
- distinctId: uniqueUserId,
251
- event: 'npx_setup_create_default_config',
252
- properties: await getTrackingProperties()
253
- });
266
+ await trackEvent('npx_setup_create_default_config');
254
267
 
255
268
  // Create the directory if it doesn't exist
256
269
  const configDir = dirname(claudeConfigPath);
@@ -275,8 +288,17 @@ if (!existsSync(claudeConfigPath)) {
275
288
  logToFile('Default config file created. Please update it with your Claude API credentials.');
276
289
  }
277
290
 
291
+ // Function to check for debug mode argument
292
+ function isDebugMode() {
293
+ return process.argv.includes('--debug');
294
+ }
295
+
278
296
  // Main function to export for ESM compatibility
279
297
  export default async function setup() {
298
+ const debugMode = isDebugMode();
299
+ if (debugMode) {
300
+ logToFile('Debug mode enabled. Will configure with Node.js inspector options.');
301
+ }
280
302
  try {
281
303
  // Read existing config
282
304
  const configData = readFileSync(claudeConfigPath, 'utf8');
@@ -288,22 +310,67 @@ export default async function setup() {
288
310
 
289
311
  // Fix Windows path handling for npx execution
290
312
  let serverConfig;
291
- if (isNpx) {
292
- serverConfig = {
293
- "command": isWindows ? "npx.cmd" : "npx",
294
- "args": [
295
- "@wonderwhy-er/desktop-commander"
296
- ]
297
- };
313
+
314
+ if (debugMode) {
315
+ // Use Node.js with inspector flag for debugging
316
+ if (isNpx) {
317
+ // Debug with npx
318
+ logToFile('Setting up debug configuration with npx. The process will pause on start until a debugger connects.');
319
+ // Add environment variables to help with debugging
320
+ const debugEnv = {
321
+ "NODE_OPTIONS": "--trace-warnings --trace-exit",
322
+ "DEBUG": "*"
323
+ };
324
+
325
+ serverConfig = {
326
+ "command": isWindows ? "node.exe" : "node",
327
+ "args": [
328
+ "--inspect-brk=9229",
329
+ isWindows ?
330
+ join(process.env.APPDATA || '', "npm", "npx.cmd").replace(/\\/g, '\\\\') :
331
+ "$(which npx)",
332
+ "@wonderwhy-er/desktop-commander"
333
+ ],
334
+ "env": debugEnv
335
+ };
336
+ } else {
337
+ // Debug with local installation path
338
+ const indexPath = join(__dirname, 'dist', 'index.js');
339
+ logToFile('Setting up debug configuration with local path. The process will pause on start until a debugger connects.');
340
+ // Add environment variables to help with debugging
341
+ const debugEnv = {
342
+ "NODE_OPTIONS": "--trace-warnings --trace-exit",
343
+ "DEBUG": "*"
344
+ };
345
+
346
+ serverConfig = {
347
+ "command": isWindows ? "node.exe" : "node",
348
+ "args": [
349
+ "--inspect-brk=9229",
350
+ indexPath.replace(/\\/g, '\\\\') // Double escape backslashes for JSON
351
+ ],
352
+ "env": debugEnv
353
+ };
354
+ }
298
355
  } else {
299
- // For local installation, use absolute path to handle Windows properly
300
- const indexPath = join(__dirname, 'dist', 'index.js');
301
- serverConfig = {
302
- "command": "node",
303
- "args": [
304
- indexPath.replace(/\\/g, '\\\\') // Double escape backslashes for JSON
305
- ]
306
- };
356
+ // Standard configuration without debug
357
+ if (isNpx) {
358
+ serverConfig = {
359
+ "command": isWindows ? "npx.cmd" : "npx",
360
+ "args": [
361
+ "@wonderwhy-er/desktop-commander"
362
+ ]
363
+ };
364
+ } else {
365
+ // For local installation, use absolute path to handle Windows properly
366
+ const indexPath = join(__dirname, 'dist', 'index.js');
367
+ serverConfig = {
368
+ "command": "node",
369
+ "args": [
370
+ indexPath.replace(/\\/g, '\\\\') // Double escape backslashes for JSON
371
+ ]
372
+ };
373
+ }
307
374
  }
308
375
 
309
376
  // Initialize mcpServers if it doesn't exist
@@ -322,31 +389,40 @@ export default async function setup() {
322
389
 
323
390
  // Write the updated config back
324
391
  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
- });
392
+ await trackEvent('npx_setup_update_config');
330
393
  logToFile('Successfully added MCP server to Claude configuration!');
331
394
  logToFile(`Configuration location: ${claudeConfigPath}`);
332
- 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');
395
+
396
+ if (debugMode) {
397
+ logToFile('\nTo use the debug server:\n1. Restart Claude if it\'s currently running\n2. The server will be available as "desktop-commander-debug" in Claude\'s MCP server list\n3. Connect your debugger to port 9229');
398
+ } else {
399
+ 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');
400
+ }
333
401
 
334
402
  await restartClaude();
335
403
 
336
- client.capture({
337
- distinctId: uniqueUserId,
338
- event: 'npx_setup_complete',
339
- properties: await getTrackingProperties()
340
- });
341
- await client.shutdown()
404
+ await trackEvent('npx_setup_complete');
405
+
406
+ // Safe shutdown
407
+ if (client) {
408
+ try {
409
+ await client.shutdown();
410
+ } catch (error) {
411
+ // Ignore shutdown errors
412
+ }
413
+ }
342
414
  } catch (error) {
343
- client.capture({
344
- distinctId: uniqueUserId,
345
- event: 'npx_setup_final_error',
346
- properties: await getTrackingProperties({ error: error.message })
347
- });
415
+ await trackEvent('npx_setup_final_error', { error: error.message });
348
416
  logToFile(`Error updating Claude configuration: ${error}`, true);
349
- await client.shutdown()
417
+
418
+ // Safe shutdown
419
+ if (client) {
420
+ try {
421
+ await client.shutdown();
422
+ } catch (error) {
423
+ // Ignore shutdown errors
424
+ }
425
+ }
350
426
  process.exit(1);
351
427
  }
352
428
  }
@@ -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
  }
@@ -27,17 +27,21 @@ function expandHome(filepath) {
27
27
  export async function validatePath(requestedPath) {
28
28
  // Temporarily allow all paths by just returning the resolved path
29
29
  // TODO: Implement configurable path validation
30
+ // Expand home directory if present
30
31
  const expandedPath = expandHome(requestedPath);
32
+ // Convert to absolute path
31
33
  const absolute = path.isAbsolute(expandedPath)
32
34
  ? path.resolve(expandedPath)
33
35
  : path.resolve(process.cwd(), expandedPath);
34
- // Try to resolve real path for symlinks, but don't enforce restrictions
36
+ // Check if path exists
35
37
  try {
38
+ const stats = await fs.stat(absolute);
39
+ // If path exists, resolve any symlinks
36
40
  return await fs.realpath(absolute);
37
41
  }
38
42
  catch (error) {
39
- // If can't resolve (e.g., file doesn't exist yet), return absolute path
40
- return absolute;
43
+ // Path doesn't exist, throw an error
44
+ throw new Error(`Path does not exist: ${absolute}`);
41
45
  }
42
46
  /* Original implementation commented out for future reference
43
47
  const expandedPath = expandHome(requestedPath);
@@ -135,6 +139,7 @@ export async function searchFiles(rootPath, pattern) {
135
139
  }
136
140
  }
137
141
  }
142
+ // if path not exist, it will throw an error
138
143
  const validPath = await validatePath(rootPath);
139
144
  await search(validPath);
140
145
  return results;
package/dist/utils.js CHANGED
@@ -1,23 +1,49 @@
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
+ import('posthog-node').then((posthogModule) => {
10
+ const PostHog = posthogModule.PostHog;
11
+ import('node-machine-id').then((machineIdModule) => {
12
+ // Access the default export from the module
13
+ uniqueUserId = machineIdModule.default.machineIdSync();
14
+ if (isTrackingEnabled) {
15
+ posthog = new PostHog('phc_TFQqTkCwtFGxlwkXDY3gSs7uvJJcJu8GurfXd6mV063', {
16
+ host: 'https://eu.i.posthog.com',
17
+ flushAt: 3, // send all every time
18
+ flushInterval: 5 // send always
19
+ });
20
+ }
21
+ }).catch(() => {
22
+ // Silently fail - we don't want analytics issues to break functionality
23
+ });
24
+ }).catch(() => {
25
+ // Silently fail - we don't want analytics issues to break functionality
26
+ });
27
+ }
28
+ catch (error) {
29
+ //console.log('Analytics module not available - continuing without tracking');
30
+ }
11
31
  export const capture = (event, properties) => {
12
32
  if (!posthog || !isTrackingEnabled) {
13
33
  return;
14
34
  }
15
- properties = properties || {};
16
- properties.timestamp = new Date().toISOString();
17
- properties.platform = platform();
18
- posthog.capture({
19
- distinctId: uniqueUserId,
20
- event,
21
- properties
22
- });
35
+ try {
36
+ properties = properties || {};
37
+ properties.timestamp = new Date().toISOString();
38
+ properties.platform = platform();
39
+ posthog.capture({
40
+ distinctId: uniqueUserId,
41
+ event,
42
+ properties
43
+ });
44
+ }
45
+ catch (error) {
46
+ // Silently fail - we don't want analytics issues to break functionality
47
+ console.error('Analytics tracking failed:', error);
48
+ }
23
49
  };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.26";
1
+ export declare const VERSION = "0.1.28";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.1.26';
1
+ export const VERSION = '0.1.28';
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.28",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "license": "MIT",
6
6
  "author": "Eduards Ruzga",
@@ -27,14 +27,15 @@
27
27
  "build": "tsc && shx cp setup-claude-server.js dist/ && shx chmod +x dist/*.js",
28
28
  "watch": "tsc --watch",
29
29
  "start": "node dist/index.js",
30
+ "start:debug": "node --inspect-brk=9229 dist/index.js",
30
31
  "setup": "npm install && npm run build && node setup-claude-server.js",
32
+ "setup:debug": "npm install && npm run build && node setup-claude-server.js --debug",
31
33
  "prepare": "npm run build",
32
34
  "test": "node test/test.js",
33
35
  "test:watch": "nodemon test/test.js",
34
36
  "link:local": "npm run build && npm link",
35
37
  "unlink:local": "npm unlink",
36
- "inspector": "npx @modelcontextprotocol/inspector dist/index.js",
37
- "publish": "npm publish"
38
+ "inspector": "npx @modelcontextprotocol/inspector dist/index.js"
38
39
  },
39
40
  "publishConfig": {
40
41
  "access": "public"