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