@wonderwhy-er/desktop-commander 0.1.23 → 0.1.26

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 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,15 +65,13 @@ 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
- Add this entry to your claude_desktop_config.json (on Mac, found at ~/Library/Application\ Support/Claude/claude_desktop_config.json):
69
+ Add this entry to your claude_desktop_config.json:
70
+
71
+ - On Mac: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
72
+ - On Windows: `%APPDATA%\Claude\claude_desktop_config.json`
73
+ - On Linux: `~/.config/Claude/claude_desktop_config.json`
74
+
71
75
  ```json
72
76
  {
73
77
  "mcpServers": {
@@ -117,7 +121,7 @@ The server provides these tool categories:
117
121
  - `move_file`: Move/rename files
118
122
  - `search_files`: Pattern-based file search
119
123
  - `get_file_info`: File metadata
120
- - `code_search`: Recursive ripgrep based text and code search
124
+ - `search_code`: Recursive ripgrep based text and code search
121
125
 
122
126
  ### Edit Tools
123
127
  - `edit_block`: Apply surgical text replacements (best for changes <20% of file size)
@@ -127,9 +131,9 @@ Search/Replace Block Format:
127
131
  ```
128
132
  filepath.ext
129
133
  <<<<<<< SEARCH
130
- existing code to replace
134
+ content to find
131
135
  =======
132
- new code to insert
136
+ new content
133
137
  >>>>>>> REPLACE
134
138
  ```
135
139
 
@@ -164,6 +168,7 @@ This project extends the MCP Filesystem Server to enable:
164
168
  Created as part of exploring Claude MCPs: https://youtube.com/live/TlbjFDbl5Us
165
169
 
166
170
  ## DONE
171
+ - **28-03-2025 Fixed "Watching /" JSON error** - Implemented custom stdio transport to handle non-JSON messages and prevent server crashes
167
172
  - **25-03-2025 Better code search** ([merged](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/17)) - Enhanced code exploration with context-aware results
168
173
 
169
174
  ## Work in Progress and TODOs
@@ -195,7 +200,7 @@ Learn more about this project through these resources:
195
200
  This Developer Ditched Windsurf, Cursor Using Claude with MCPs](https://analyticsindiamag.com/ai-features/this-developer-ditched-windsurf-cursor-using-claude-with-mcps/)
196
201
 
197
202
  ### Community
198
- Join our [Discord server](https://discord.gg/7cbccwRp) to get help, share feedback, and connect with other users.
203
+ Join our [Discord server](https://discord.gg/kQ27sNnZr7) to get help, share feedback, and connect with other users.
199
204
 
200
205
  ## Testimonials
201
206
 
@@ -258,6 +263,18 @@ No. This tool works with Claude Desktop's standard Pro subscription ($20/month),
258
263
  ### I'm having trouble installing or using the tool. Where can I get help?
259
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.
260
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
+
261
278
  ## License
262
279
 
263
280
  MIT
@@ -2,6 +2,7 @@ declare class CommandManager {
2
2
  private blockedCommands;
3
3
  loadBlockedCommands(): Promise<void>;
4
4
  saveBlockedCommands(): Promise<void>;
5
+ getBaseCommand(command: string): string;
5
6
  validateCommand(command: string): boolean;
6
7
  blockCommand(command: string): Promise<boolean>;
7
8
  unblockCommand(command: string): Promise<boolean>;
@@ -25,8 +25,11 @@ class CommandManager {
25
25
  // Handle error if needed
26
26
  }
27
27
  }
28
+ getBaseCommand(command) {
29
+ return command.split(' ')[0].toLowerCase().trim();
30
+ }
28
31
  validateCommand(command) {
29
- const baseCommand = command.split(' ')[0].toLowerCase().trim();
32
+ const baseCommand = this.getBaseCommand(command);
30
33
  return !this.blockedCommands.has(baseCommand);
31
34
  }
32
35
  async blockCommand(command) {
@@ -0,0 +1,8 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ /**
3
+ * Extended StdioServerTransport that filters out non-JSON messages.
4
+ * This prevents the "Watching /" error from crashing the server.
5
+ */
6
+ export declare class FilteredStdioServerTransport extends StdioServerTransport {
7
+ constructor();
8
+ }
@@ -0,0 +1,22 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import process from "node:process";
3
+ /**
4
+ * Extended StdioServerTransport that filters out non-JSON messages.
5
+ * This prevents the "Watching /" error from crashing the server.
6
+ */
7
+ export class FilteredStdioServerTransport extends StdioServerTransport {
8
+ constructor() {
9
+ // Create a proxy for stdout that only allows valid JSON to pass through
10
+ const originalStdoutWrite = process.stdout.write;
11
+ process.stdout.write = function (buffer) {
12
+ // Only intercept string output that doesn't look like JSON
13
+ if (typeof buffer === 'string' && !buffer.trim().startsWith('{')) {
14
+ return true; //process.stderr.write(buffer);
15
+ }
16
+ return originalStdoutWrite.apply(process.stdout, arguments);
17
+ };
18
+ super();
19
+ // Log initialization to stderr to avoid polluting the JSON stream
20
+ process.stderr.write(`[desktop-commander] Initialized FilteredStdioServerTransport\n`);
21
+ }
22
+ }
package/dist/index.js CHANGED
@@ -1,20 +1,52 @@
1
1
  #!/usr/bin/env node
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { FilteredStdioServerTransport } from './custom-stdio.js';
3
3
  import { server } from './server.js';
4
4
  import { commandManager } from './command-manager.js';
5
5
  import { join, dirname } from 'path';
6
- import { fileURLToPath } from 'url';
6
+ import { fileURLToPath, pathToFileURL } from 'url';
7
+ import { platform } from 'os';
8
+ import { capture } from './utils.js';
7
9
  const __filename = fileURLToPath(import.meta.url);
8
10
  const __dirname = dirname(__filename);
11
+ const isWindows = platform() === 'win32';
12
+ // Helper function to properly convert file paths to URLs, especially for Windows
13
+ function createFileURL(filePath) {
14
+ if (isWindows) {
15
+ // Ensure path uses forward slashes for URL format
16
+ const normalizedPath = filePath.replace(/\\/g, '/');
17
+ // Ensure path has proper file:// prefix
18
+ if (normalizedPath.startsWith('/')) {
19
+ return new URL(`file://${normalizedPath}`);
20
+ }
21
+ else {
22
+ return new URL(`file:///${normalizedPath}`);
23
+ }
24
+ }
25
+ else {
26
+ // For non-Windows, we can use the built-in function
27
+ return pathToFileURL(filePath);
28
+ }
29
+ }
9
30
  async function runSetup() {
10
- const setupScript = join(__dirname, 'setup-claude-server.js');
11
- const { default: setupModule } = await import(setupScript);
12
- if (typeof setupModule === 'function') {
13
- await setupModule();
31
+ try {
32
+ // Fix for Windows ESM path issue
33
+ const setupScriptPath = join(__dirname, 'setup-claude-server.js');
34
+ const setupScriptUrl = createFileURL(setupScriptPath);
35
+ // Now import using the URL format
36
+ const { default: setupModule } = await import(setupScriptUrl.href);
37
+ if (typeof setupModule === 'function') {
38
+ await setupModule();
39
+ }
40
+ }
41
+ catch (error) {
42
+ console.error('Error running setup:', error);
43
+ process.exit(1);
14
44
  }
15
45
  }
16
46
  async function runServer() {
17
47
  try {
48
+ const transport = new FilteredStdioServerTransport();
49
+ console.log("start");
18
50
  // Check if first argument is "setup"
19
51
  if (process.argv[2] === 'setup') {
20
52
  await runSetup();
@@ -23,14 +55,31 @@ async function runServer() {
23
55
  // Handle uncaught exceptions
24
56
  process.on('uncaughtException', async (error) => {
25
57
  const errorMessage = error instanceof Error ? error.message : String(error);
58
+ // If this is a JSON parsing error, log it to stderr but don't crash
59
+ if (errorMessage.includes('JSON') && errorMessage.includes('Unexpected token')) {
60
+ process.stderr.write(`[desktop-commander] JSON parsing error: ${errorMessage}\n`);
61
+ return; // Don't exit on JSON parsing errors
62
+ }
63
+ capture('run_server_uncaught_exception', {
64
+ error: errorMessage
65
+ });
66
+ process.stderr.write(`[desktop-commander] Uncaught exception: ${errorMessage}\n`);
26
67
  process.exit(1);
27
68
  });
28
69
  // Handle unhandled rejections
29
70
  process.on('unhandledRejection', async (reason) => {
30
71
  const errorMessage = reason instanceof Error ? reason.message : String(reason);
72
+ // If this is a JSON parsing error, log it to stderr but don't crash
73
+ if (errorMessage.includes('JSON') && errorMessage.includes('Unexpected token')) {
74
+ process.stderr.write(`[desktop-commander] JSON parsing rejection: ${errorMessage}\n`);
75
+ return; // Don't exit on JSON parsing errors
76
+ }
77
+ capture('run_server_unhandled_rejection', {
78
+ error: errorMessage
79
+ });
80
+ process.stderr.write(`[desktop-commander] Unhandled rejection: ${errorMessage}\n`);
31
81
  process.exit(1);
32
82
  });
33
- const transport = new StdioServerTransport();
34
83
  // Load blocked commands from config file
35
84
  await commandManager.loadBlockedCommands();
36
85
  await server.connect(transport);
@@ -42,6 +91,9 @@ async function runServer() {
42
91
  timestamp: new Date().toISOString(),
43
92
  message: `Failed to start server: ${errorMessage}`
44
93
  }) + '\n');
94
+ capture('run_server_failed_start_error', {
95
+ error: errorMessage
96
+ });
45
97
  process.exit(1);
46
98
  }
47
99
  }
@@ -52,5 +104,8 @@ runServer().catch(async (error) => {
52
104
  timestamp: new Date().toISOString(),
53
105
  message: `Fatal error running server: ${errorMessage}`
54
106
  }) + '\n');
107
+ capture('run_server_fatal_error', {
108
+ error: errorMessage
109
+ });
55
110
  process.exit(1);
56
111
  });
package/dist/server.d.ts CHANGED
@@ -1,20 +1,24 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  export declare const server: Server<{
3
3
  method: string;
4
- params?: import("zod").objectOutputType<{
5
- _meta: import("zod").ZodOptional<import("zod").ZodObject<{
6
- progressToken: import("zod").ZodOptional<import("zod").ZodUnion<[import("zod").ZodString, import("zod").ZodNumber]>>;
7
- }, "passthrough", import("zod").ZodTypeAny, import("zod").objectOutputType<{
8
- progressToken: import("zod").ZodOptional<import("zod").ZodUnion<[import("zod").ZodString, import("zod").ZodNumber]>>;
9
- }, import("zod").ZodTypeAny, "passthrough">, import("zod").objectInputType<{
10
- progressToken: import("zod").ZodOptional<import("zod").ZodUnion<[import("zod").ZodString, import("zod").ZodNumber]>>;
11
- }, import("zod").ZodTypeAny, "passthrough">>>;
12
- }, import("zod").ZodTypeAny, "passthrough"> | undefined;
4
+ params?: {
5
+ [x: string]: unknown;
6
+ _meta?: {
7
+ [x: string]: unknown;
8
+ progressToken?: string | number | undefined;
9
+ } | undefined;
10
+ } | undefined;
13
11
  }, {
14
12
  method: string;
15
- params?: import("zod").objectOutputType<{
16
- _meta: import("zod").ZodOptional<import("zod").ZodObject<{}, "passthrough", import("zod").ZodTypeAny, import("zod").objectOutputType<{}, import("zod").ZodTypeAny, "passthrough">, import("zod").objectInputType<{}, import("zod").ZodTypeAny, "passthrough">>>;
17
- }, import("zod").ZodTypeAny, "passthrough"> | undefined;
18
- }, import("zod").objectOutputType<{
19
- _meta: import("zod").ZodOptional<import("zod").ZodObject<{}, "passthrough", import("zod").ZodTypeAny, import("zod").objectOutputType<{}, import("zod").ZodTypeAny, "passthrough">, import("zod").objectInputType<{}, import("zod").ZodTypeAny, "passthrough">>>;
20
- }, import("zod").ZodTypeAny, "passthrough">>;
13
+ params?: {
14
+ [x: string]: unknown;
15
+ _meta?: {
16
+ [x: string]: unknown;
17
+ } | undefined;
18
+ } | undefined;
19
+ }, {
20
+ [x: string]: unknown;
21
+ _meta?: {
22
+ [x: string]: unknown;
23
+ } | undefined;
24
+ }>;
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
  ],
@@ -186,18 +187,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
186
187
  }
187
188
  case "read_output": {
188
189
  const parsed = ReadOutputArgsSchema.parse(args);
190
+ capture('server_read_output');
189
191
  return readOutput(parsed);
190
192
  }
191
193
  case "force_terminate": {
192
194
  const parsed = ForceTerminateArgsSchema.parse(args);
195
+ capture('server_force_terminate');
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": {
200
205
  const parsed = KillProcessArgsSchema.parse(args);
206
+ capture('server_kill_process');
201
207
  return killProcess(parsed);
202
208
  }
203
209
  case "block_command": {
@@ -225,6 +231,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
225
231
  const parsed = EditBlockArgsSchema.parse(args);
226
232
  const { filePath, searchReplace } = await parseEditBlock(parsed.blockContent);
227
233
  await performSearchReplace(filePath, searchReplace);
234
+ capture('server_edit_block');
228
235
  return {
229
236
  content: [{ type: "text", text: `Successfully applied edit to ${filePath}` }],
230
237
  };
@@ -232,6 +239,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
232
239
  case "read_file": {
233
240
  const parsed = ReadFileArgsSchema.parse(args);
234
241
  const content = await readFile(parsed.path);
242
+ capture('server_read_file');
235
243
  return {
236
244
  content: [{ type: "text", text: content }],
237
245
  };
@@ -239,6 +247,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
239
247
  case "read_multiple_files": {
240
248
  const parsed = ReadMultipleFilesArgsSchema.parse(args);
241
249
  const results = await readMultipleFiles(parsed.paths);
250
+ capture('server_read_multiple_files');
242
251
  return {
243
252
  content: [{ type: "text", text: results.join("\n---\n") }],
244
253
  };
@@ -246,6 +255,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
246
255
  case "write_file": {
247
256
  const parsed = WriteFileArgsSchema.parse(args);
248
257
  await writeFile(parsed.path, parsed.content);
258
+ capture('server_write_file');
249
259
  return {
250
260
  content: [{ type: "text", text: `Successfully wrote to ${parsed.path}` }],
251
261
  };
@@ -253,6 +263,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
253
263
  case "create_directory": {
254
264
  const parsed = CreateDirectoryArgsSchema.parse(args);
255
265
  await createDirectory(parsed.path);
266
+ capture('server_create_directory');
256
267
  return {
257
268
  content: [{ type: "text", text: `Successfully created directory ${parsed.path}` }],
258
269
  };
@@ -260,6 +271,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
260
271
  case "list_directory": {
261
272
  const parsed = ListDirectoryArgsSchema.parse(args);
262
273
  const entries = await listDirectory(parsed.path);
274
+ capture('server_list_directory');
263
275
  return {
264
276
  content: [{ type: "text", text: entries.join('\n') }],
265
277
  };
@@ -267,6 +279,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
267
279
  case "move_file": {
268
280
  const parsed = MoveFileArgsSchema.parse(args);
269
281
  await moveFile(parsed.source, parsed.destination);
282
+ capture('server_move_file');
270
283
  return {
271
284
  content: [{ type: "text", text: `Successfully moved ${parsed.source} to ${parsed.destination}` }],
272
285
  };
@@ -274,6 +287,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
274
287
  case "search_files": {
275
288
  const parsed = SearchFilesArgsSchema.parse(args);
276
289
  const results = await searchFiles(parsed.path, parsed.pattern);
290
+ capture('server_search_files');
277
291
  return {
278
292
  content: [{ type: "text", text: results.length > 0 ? results.join('\n') : "No matches found" }],
279
293
  };
@@ -289,6 +303,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
289
303
  includeHidden: parsed.includeHidden,
290
304
  contextLines: parsed.contextLines,
291
305
  });
306
+ capture('server_search_code');
292
307
  if (results.length === 0) {
293
308
  return {
294
309
  content: [{ type: "text", text: "No matches found" }],
@@ -311,6 +326,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
311
326
  case "get_file_info": {
312
327
  const parsed = GetFileInfoArgsSchema.parse(args);
313
328
  const info = await getFileInfo(parsed.path);
329
+ capture('server_get_file_info');
314
330
  return {
315
331
  content: [{
316
332
  type: "text",
@@ -322,6 +338,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
322
338
  }
323
339
  case "list_allowed_directories": {
324
340
  const directories = listAllowedDirectories();
341
+ capture('server_list_allowed_directories');
325
342
  return {
326
343
  content: [{
327
344
  type: "text",
@@ -335,6 +352,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
335
352
  }
336
353
  catch (error) {
337
354
  const errorMessage = error instanceof Error ? error.message : String(error);
355
+ capture('server_request_error', {
356
+ error: errorMessage
357
+ });
338
358
  return {
339
359
  content: [{ type: "text", text: `Error: ${errorMessage}` }],
340
360
  isError: true,
@@ -3,15 +3,153 @@ import { join } from 'path';
3
3
  import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { dirname } from 'path';
6
+ import { exec } from "node:child_process";
7
+ import { PostHog } from 'posthog-node';
8
+ import machineId from 'node-machine-id';
9
+ import { version as nodeVersion } from 'process';
6
10
 
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();
21
+
22
+ // Function to get npm version
23
+ async function getNpmVersion() {
24
+ try {
25
+ return new Promise((resolve, reject) => {
26
+ exec('npm --version', (error, stdout, stderr) => {
27
+ if (error) {
28
+ resolve('unknown');
29
+ return;
30
+ }
31
+ resolve(stdout.trim());
32
+ });
33
+ });
34
+ } catch (error) {
35
+ return 'unknown';
36
+ }
37
+ }
38
+
39
+ // Function to detect shell environment
40
+ function detectShell() {
41
+ // Check for Windows shells
42
+ if (process.platform === 'win32') {
43
+ if (process.env.TERM_PROGRAM === 'vscode') return 'vscode-terminal';
44
+ if (process.env.WT_SESSION) return 'windows-terminal';
45
+ if (process.env.SHELL?.includes('bash')) return 'git-bash';
46
+ if (process.env.TERM?.includes('xterm')) return 'xterm-on-windows';
47
+ if (process.env.ComSpec?.toLowerCase().includes('powershell')) return 'powershell';
48
+ if (process.env.PROMPT) return 'cmd';
49
+
50
+ // WSL detection
51
+ if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
52
+ return `wsl-${process.env.WSL_DISTRO_NAME || 'unknown'}`;
53
+ }
54
+
55
+ return 'windows-unknown';
56
+ }
57
+
58
+ // Unix-based shells
59
+ if (process.env.SHELL) {
60
+ const shellPath = process.env.SHELL.toLowerCase();
61
+ if (shellPath.includes('bash')) return 'bash';
62
+ if (shellPath.includes('zsh')) return 'zsh';
63
+ if (shellPath.includes('fish')) return 'fish';
64
+ if (shellPath.includes('ksh')) return 'ksh';
65
+ if (shellPath.includes('csh')) return 'csh';
66
+ if (shellPath.includes('dash')) return 'dash';
67
+ return `other-unix-${shellPath.split('/').pop()}`;
68
+ }
69
+
70
+ // Terminal emulators and IDE terminals
71
+ if (process.env.TERM_PROGRAM) {
72
+ return process.env.TERM_PROGRAM.toLowerCase();
73
+ }
74
+
75
+ return 'unknown-shell';
76
+ }
77
+
78
+ // Function to determine execution context
79
+ function getExecutionContext() {
80
+ // Check if running from npx
81
+ const isNpx = process.env.npm_lifecycle_event === 'npx' ||
82
+ process.env.npm_execpath?.includes('npx') ||
83
+ process.env._?.includes('npx') ||
84
+ import.meta.url.includes('node_modules');
85
+
86
+ // Check if installed globally
87
+ const isGlobal = process.env.npm_config_global === 'true' ||
88
+ process.argv[1]?.includes('node_modules/.bin');
89
+
90
+ // Check if it's run from a script in package.json
91
+ const isNpmScript = !!process.env.npm_lifecycle_script;
92
+
93
+ return {
94
+ runMethod: isNpx ? 'npx' : (isGlobal ? 'global' : (isNpmScript ? 'npm_script' : 'direct')),
95
+ isCI: !!process.env.CI || !!process.env.GITHUB_ACTIONS || !!process.env.TRAVIS || !!process.env.CIRCLECI,
96
+ shell: detectShell()
97
+ };
98
+ }
99
+
100
+ // Helper function to get standard environment properties for tracking
101
+ let npmVersionCache = null;
102
+ async function getTrackingProperties(additionalProps = {}) {
103
+ if (npmVersionCache === null) {
104
+ npmVersionCache = await getNpmVersion();
105
+ }
106
+
107
+ const context = getExecutionContext();
108
+
109
+ return {
110
+ platform: platform(),
111
+ nodeVersion: nodeVersion,
112
+ npmVersion: npmVersionCache,
113
+ executionContext: context.runMethod,
114
+ isCI: context.isCI,
115
+ shell: context.shell,
116
+ timestamp: new Date().toISOString(),
117
+ ...additionalProps
118
+ };
119
+ }
120
+
121
+ // Initial tracking
122
+ (async () => {
123
+ client.capture({
124
+ distinctId: uniqueUserId,
125
+ event: 'npx_setup_start',
126
+ properties: await getTrackingProperties()
127
+ });
128
+ })();
129
+
130
+ // Fix for Windows ESM path resolution
7
131
  const __filename = fileURLToPath(import.meta.url);
8
132
  const __dirname = dirname(__filename);
9
133
 
10
- // Determine OS and set appropriate config path and command
11
- const isWindows = platform() === 'win32';
12
- const claudeConfigPath = isWindows
13
- ? join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json')
14
- : join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
134
+ // Determine OS and set appropriate config path
135
+ const os = platform();
136
+ const isWindows = os === 'win32'; // Define isWindows variable
137
+ let claudeConfigPath;
138
+
139
+ switch (os) {
140
+ case 'win32':
141
+ claudeConfigPath = join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json');
142
+ break;
143
+ case 'darwin':
144
+ claudeConfigPath = join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
145
+ break;
146
+ case 'linux':
147
+ claudeConfigPath = join(homedir(), '.config', 'Claude', 'claude_desktop_config.json');
148
+ break;
149
+ default:
150
+ // Fallback for other platforms
151
+ claudeConfigPath = join(homedir(), '.claude_desktop_config.json');
152
+ }
15
153
 
16
154
  // Setup logging
17
155
  const LOG_FILE = join(__dirname, 'setup.log');
@@ -38,18 +176,89 @@ function logToFile(message, isError = false) {
38
176
  }
39
177
  }
40
178
 
179
+ async function execAsync(command) {
180
+ return new Promise((resolve, reject) => {
181
+ // Use PowerShell on Windows for better Unicode support and consistency
182
+ const actualCommand = isWindows
183
+ ? `cmd.exe /c ${command}`
184
+ : command;
185
+
186
+ exec(actualCommand, (error, stdout, stderr) => {
187
+ if (error) {
188
+ reject(error);
189
+ return;
190
+ }
191
+ resolve({ stdout, stderr });
192
+ });
193
+ });
194
+ }
195
+
196
+ async function restartClaude() {
197
+ try {
198
+ const platform = process.platform
199
+ // ignore errors on windows when claude is not running.
200
+ // just silently kill the process
201
+ try {
202
+ switch (platform) {
203
+ case "win32":
204
+
205
+ await execAsync(
206
+ `taskkill /F /IM "Claude.exe"`,
207
+ )
208
+ break;
209
+ case "darwin":
210
+ await execAsync(
211
+ `killall "Claude"`,
212
+ )
213
+ break;
214
+ case "linux":
215
+ await execAsync(
216
+ `pkill -f "claude"`,
217
+ )
218
+ break;
219
+ }
220
+ } catch {}
221
+ 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
+ }
231
+
232
+ logToFile(`Claude has been restarted.`)
233
+ } catch (error) {
234
+ client.capture({
235
+ distinctId: uniqueUserId,
236
+ event: 'npx_setup_restart_claude_error',
237
+ properties: await getTrackingProperties({ error: error.message })
238
+ });
239
+ logToFile(`Failed to restart Claude: ${error}`, true)
240
+ }
241
+ }
242
+
41
243
  // Check if config file exists and create default if not
42
244
  if (!existsSync(claudeConfigPath)) {
43
245
  logToFile(`Claude config file not found at: ${claudeConfigPath}`);
44
246
  logToFile('Creating default config file...');
45
247
 
248
+ // Track new installation
249
+ client.capture({
250
+ distinctId: uniqueUserId,
251
+ event: 'npx_setup_create_default_config',
252
+ properties: await getTrackingProperties()
253
+ });
254
+
46
255
  // Create the directory if it doesn't exist
47
256
  const configDir = dirname(claudeConfigPath);
48
257
  if (!existsSync(configDir)) {
49
258
  import('fs').then(fs => fs.mkdirSync(configDir, { recursive: true }));
50
259
  }
51
260
 
52
- // Create default config
261
+ // Create default config with shell based on platform
53
262
  const defaultConfig = {
54
263
  "serverConfig": isWindows
55
264
  ? {
@@ -66,49 +275,86 @@ if (!existsSync(claudeConfigPath)) {
66
275
  logToFile('Default config file created. Please update it with your Claude API credentials.');
67
276
  }
68
277
 
69
- try {
70
- // Read existing config
71
- const configData = readFileSync(claudeConfigPath, 'utf8');
72
- const config = JSON.parse(configData);
73
-
74
- // Prepare the new server config based on OS
75
- // Determine if running through npx or locally
76
- const isNpx = import.meta.url.endsWith('dist/setup-claude-server.js');
77
-
78
- const serverConfig = isNpx ? {
79
- "command": "npx",
80
- "args": [
81
- "@wonderwhy-er/desktop-commander"
82
- ]
83
- } : {
84
- "command": "node",
85
- "args": [
86
- join(__dirname, 'dist', 'index.js')
87
- ]
88
- };
278
+ // Main function to export for ESM compatibility
279
+ export default async function setup() {
280
+ try {
281
+ // Read existing config
282
+ const configData = readFileSync(claudeConfigPath, 'utf8');
283
+ const config = JSON.parse(configData);
89
284
 
90
- // Initialize mcpServers if it doesn't exist
91
- if (!config.mcpServers) {
92
- config.mcpServers = {};
93
- }
94
-
95
- // Check if the old "desktopCommander" exists and remove it
96
- if (config.mcpServers.desktopCommander) {
97
- logToFile('Found old "desktopCommander" installation. Removing it...');
98
- delete config.mcpServers.desktopCommander;
285
+ // Prepare the new server config based on OS
286
+ // Determine if running through npx or locally
287
+ const isNpx = import.meta.url.includes('node_modules');
288
+
289
+ // Fix Windows path handling for npx execution
290
+ let serverConfig;
291
+ if (isNpx) {
292
+ serverConfig = {
293
+ "command": isWindows ? "npx.cmd" : "npx",
294
+ "args": [
295
+ "@wonderwhy-er/desktop-commander"
296
+ ]
297
+ };
298
+ } 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
+ };
307
+ }
308
+
309
+ // Initialize mcpServers if it doesn't exist
310
+ if (!config.mcpServers) {
311
+ config.mcpServers = {};
312
+ }
313
+
314
+ // Check if the old "desktopCommander" exists and remove it
315
+ if (config.mcpServers.desktopCommander) {
316
+ logToFile('Found old "desktopCommander" installation. Removing it...');
317
+ delete config.mcpServers.desktopCommander;
318
+ }
319
+
320
+ // Add or update the terminal server config with the proper name "desktop-commander"
321
+ config.mcpServers["desktop-commander"] = serverConfig;
322
+
323
+ // Write the updated config back
324
+ 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
+ });
330
+ logToFile('Successfully added MCP server to Claude configuration!');
331
+ 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');
333
+
334
+ await restartClaude();
335
+
336
+ client.capture({
337
+ distinctId: uniqueUserId,
338
+ event: 'npx_setup_complete',
339
+ properties: await getTrackingProperties()
340
+ });
341
+ await client.shutdown()
342
+ } catch (error) {
343
+ client.capture({
344
+ distinctId: uniqueUserId,
345
+ event: 'npx_setup_final_error',
346
+ properties: await getTrackingProperties({ error: error.message })
347
+ });
348
+ logToFile(`Error updating Claude configuration: ${error}`, true);
349
+ await client.shutdown()
350
+ process.exit(1);
99
351
  }
100
-
101
- // Add or update the terminal server config with the proper name "desktop-commander"
102
- config.mcpServers["desktop-commander"] = serverConfig;
352
+ }
103
353
 
104
- // Write the updated config back
105
- writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), 'utf8');
106
-
107
- logToFile('Successfully added MCP server to Claude configuration!');
108
- logToFile(`Configuration location: ${claudeConfigPath}`);
109
- 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');
110
-
111
- } catch (error) {
112
- logToFile(`Error updating Claude configuration: ${error}`, true);
113
- process.exit(1);
354
+ // Allow direct execution
355
+ if (process.argv.length >= 2 && process.argv[1] === fileURLToPath(import.meta.url)) {
356
+ setup().catch(error => {
357
+ logToFile(`Fatal error: ${error}`, true);
358
+ process.exit(1);
359
+ });
114
360
  }
@@ -1,11 +1,16 @@
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
+ capture('server_execute_command', {
12
+ command: commandManager.getBaseCommand(parsed.data.command)
13
+ });
9
14
  if (!commandManager.validateCommand(parsed.data.command)) {
10
15
  throw new Error(`Command not allowed: ${parsed.data.command}`);
11
16
  }
@@ -0,0 +1 @@
1
+ export declare const capture: (event: string, properties?: any) => void;
package/dist/utils.js ADDED
@@ -0,0 +1,23 @@
1
+ import { PostHog } from 'posthog-node';
2
+ import machineId from 'node-machine-id';
3
+ import { platform } from 'os';
4
+ 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;
11
+ export const capture = (event, properties) => {
12
+ if (!posthog || !isTrackingEnabled) {
13
+ return;
14
+ }
15
+ properties = properties || {};
16
+ properties.timestamp = new Date().toISOString();
17
+ properties.platform = platform();
18
+ posthog.capture({
19
+ distinctId: uniqueUserId,
20
+ event,
21
+ properties
22
+ });
23
+ };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.23";
1
+ export declare const VERSION = "0.1.26";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.1.23';
1
+ export const VERSION = '0.1.26';
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.1.23",
3
+ "version": "0.1.26",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "license": "MIT",
6
6
  "author": "Eduards Ruzga",
7
7
  "homepage": "https://github.com/wonderwhy-er/DesktopCommanderMCP",
8
8
  "bugs": "https://github.com/wonderwhy-er/DesktopCommanderMCP/issues",
9
9
  "type": "module",
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
10
13
  "bin": {
11
14
  "desktop-commander": "dist/index.js",
12
15
  "setup": "dist/setup-claude-server.js"
@@ -56,14 +59,16 @@
56
59
  "file-operations"
57
60
  ],
58
61
  "dependencies": {
59
- "@modelcontextprotocol/sdk": "1.0.1",
62
+ "@modelcontextprotocol/sdk": "^1.8.0",
60
63
  "@vscode/ripgrep": "^1.15.9",
61
64
  "glob": "^10.3.10",
65
+ "node-machine-id": "^1.1.12",
66
+ "posthog-node": "^4.11.1",
62
67
  "zod": "^3.24.1",
63
68
  "zod-to-json-schema": "^3.23.5"
64
69
  },
65
70
  "devDependencies": {
66
- "@types/node": "^20.11.0",
71
+ "@types/node": "^20.17.24",
67
72
  "nodemon": "^3.0.2",
68
73
  "shx": "^0.3.4",
69
74
  "typescript": "^5.3.3"