@wonderwhy-er/desktop-commander
Advanced tools
Comparing version
@@ -297,2 +297,3 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; | ||
Read new output from a running terminal session. | ||
Set timeout_ms for long running commands. | ||
@@ -299,0 +300,0 @@ ${CMD_PREFIX_DESCRIPTION}`, |
@@ -49,3 +49,2 @@ import { homedir, platform } from 'os'; | ||
} | ||
const getVersion = async () => { | ||
@@ -55,32 +54,24 @@ try { | ||
return process.env.npm_package_version; | ||
} else { | ||
const packageJsonPath = join(__dirname, 'package.json'); | ||
if (existsSync(packageJsonPath)) { | ||
const packageJsonContent = readFileSync(packageJsonPath, 'utf8'); | ||
const packageJson = JSON.parse(packageJsonContent); | ||
if (packageJson.version) { | ||
return packageJson.version; | ||
} | ||
} | ||
// Check if version.js exists in dist directory (when running from root) | ||
const versionPath = join(__dirname, 'version.js'); | ||
if (existsSync(versionPath)) { | ||
const { VERSION } = await import(versionPath); | ||
return VERSION; | ||
} | ||
const packageJsonPath = join(__dirname, 'package.json'); | ||
if (existsSync(packageJsonPath)) { | ||
const packageJsonContent = readFileSync(packageJsonPath, 'utf8'); | ||
const packageJson = JSON.parse(packageJsonContent); | ||
if (packageJson.version) { | ||
return packageJson.version; | ||
} | ||
} | ||
throw new Error('Version not found in environment variable or package.json'); | ||
return 'unknown'; | ||
} catch (error) { | ||
try { | ||
const packageJson = await import('./package.json', { with: { type: 'json' } }); | ||
if (packageJson.default?.version) { | ||
return packageJson.default.version; | ||
} | ||
} catch (importError) { | ||
// Try older syntax as fallback | ||
try { | ||
const packageJson = await import('./package.json', { assert: { type: 'json' } }); | ||
if (packageJson.default?.version) { | ||
return packageJson.default.version; | ||
} | ||
} catch (legacyImportError) { | ||
// Log the error for debugging | ||
logToFile(`Failed to import package.json: ${legacyImportError.message}`, true); | ||
} | ||
} | ||
return 'unknown'; | ||
} | ||
@@ -325,10 +316,10 @@ }; | ||
}; | ||
process.stdout.write(JSON.stringify(jsonOutput) + '\n'); | ||
process.stdout.write(`${message}\n`); | ||
} catch (err) { | ||
// Last resort error handling | ||
process.stderr.write(JSON.stringify({ | ||
process.stderr.write(`${JSON.stringify({ | ||
type: 'error', | ||
timestamp: new Date().toISOString(), | ||
message: `Failed to write to log file: ${err.message}` | ||
}) + '\n'); | ||
})}\n`); | ||
} | ||
@@ -488,11 +479,18 @@ } | ||
updateSetupStep(startStep, 'completed'); | ||
logToFile(`Claude has been restarted.`); | ||
logToFile("\nβ Claude has been restarted automatically!"); | ||
await trackEvent('npx_setup_start_claude_success', { platform }); | ||
} else if (platform === "linux") { | ||
await execAsync(`claude`); | ||
logToFile(`Claude has been restarted.`); | ||
logToFile("\nβ Claude has been restarted automatically!"); | ||
updateSetupStep(startStep, 'completed'); | ||
await trackEvent('npx_setup_start_claude_success', { platform }); | ||
} else { | ||
logToFile('\nTo use the server restart Claude if it\'s currently running\n'); | ||
} | ||
logToFile("\nβ Installation successfully completed! Thank you for using Desktop Commander!\n"); | ||
logToFile('\nThe server is available as "desktop-commander" in Claude\'s MCP server list'); | ||
logToFile("Future updates will install automatically β no need to run this setup again.\n\n"); | ||
logToFile("π¬ Need help or found an issue? Join our community: https://discord.com/invite/kQ27sNnZr7\n\n") | ||
updateSetupStep(restartStep, 'completed'); | ||
@@ -525,2 +523,12 @@ await trackEvent('npx_setup_restart_claude_success', { platform }); | ||
// Print ASCII art for DESKTOP COMMANDER | ||
console.log('\n'); | ||
console.log('βββββββ βββββββββββββββββββ ββββββββββββ βββββββ βββββββ βββββββ βββββββ ββββ ββββββββ ββββ ββββββ ββββ ββββββββββ βββββββββββββββ '); | ||
console.log('βββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ ββββββββββββββββββββββ ββββββββββ ββββββββββββββββββ βββββββββββββββββββββββββββ'); | ||
console.log('βββ βββββββββ βββββββββββββββ βββ βββ βββββββββββ βββ βββ βββββββββββββββββββββββββββββββββββββββ ββββββ βββββββββ ββββββββ'); | ||
console.log('βββ βββββββββ βββββββββββββββ βββ βββ ββββββββββ βββ βββ ββββββββββββββββββββββββββββββββββββββββββββββ βββββββββ ββββββββ'); | ||
console.log('βββββββββββββββββββββββββββ βββ βββ ββββββββββββ ββββββββββββββββββββ βββ ββββββ βββ ββββββ ββββββ βββββββββββββββββββββββββ βββ'); | ||
console.log('βββββββ βββββββββββββββββββ βββ βββ βββββββ βββ βββββββ βββββββ βββ ββββββ ββββββ ββββββ ββββββββββββ βββββββββββ βββ'); | ||
console.log('\n'); | ||
if (debugMode) { | ||
@@ -713,3 +721,3 @@ logToFile('Debug mode enabled. Will configure with Node.js inspector options.'); | ||
const appVersion = await getVersion() | ||
logToFile(`Successfully added Desktop Commander MCP v${appVersion} server to Claude configuration!`); | ||
logToFile(`β Desktop Commander MCP v${appVersion} successfully added to Claudeβs configuration.`); | ||
logToFile(`Configuration location: ${claudeConfigPath}`); | ||
@@ -719,4 +727,2 @@ | ||
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'); | ||
} else { | ||
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'); | ||
} | ||
@@ -736,2 +742,4 @@ | ||
return true; | ||
@@ -738,0 +746,0 @@ } catch (error) { |
@@ -1,2 +0,2 @@ | ||
import { CommandExecutionResult, ActiveSession } from './types.js'; | ||
import { TerminalSession, CommandExecutionResult, ActiveSession } from './types.js'; | ||
interface CompletedSession { | ||
@@ -14,2 +14,8 @@ pid: number; | ||
getNewOutput(pid: number): string | null; | ||
/** | ||
* Get a session by PID | ||
* @param pid Process ID | ||
* @returns The session or undefined if not found | ||
*/ | ||
getSession(pid: number): TerminalSession | undefined; | ||
forceTerminate(pid: number): boolean; | ||
@@ -16,0 +22,0 @@ listActiveSessions(): ActiveSession[]; |
@@ -16,3 +16,3 @@ import { spawn } from 'child_process'; | ||
const config = await configManager.getConfig(); | ||
shellToUse = config.shell || true; | ||
shellToUse = config.defaultShell || true; | ||
} | ||
@@ -107,2 +107,10 @@ catch (error) { | ||
} | ||
/** | ||
* Get a session by PID | ||
* @param pid Process ID | ||
* @returns The session or undefined if not found | ||
*/ | ||
getSession(pid) { | ||
return this.sessions.get(pid); | ||
} | ||
forceTerminate(pid) { | ||
@@ -109,0 +117,0 @@ const session = this.sessions.get(pid); |
@@ -67,11 +67,2 @@ import { readFile, writeFile } from './filesystem.js'; | ||
export async function performSearchReplace(filePath, block, expectedReplacements = 1) { | ||
// Check for empty search string to prevent infinite loops | ||
if (block.search === "") { | ||
return { | ||
content: [{ | ||
type: "text", | ||
text: "Empty search strings are not allowed. Please provide a non-empty string to search for." | ||
}], | ||
}; | ||
} | ||
// Get file extension for telemetry using path module | ||
@@ -88,2 +79,13 @@ const fileExtension = path.extname(filePath).toLowerCase(); | ||
}); | ||
// Check for empty search string to prevent infinite loops | ||
if (block.search === "") { | ||
// Capture file extension in telemetry without capturing the file path | ||
capture('server_edit_block_empty_search', { fileExtension: fileExtension, expectedReplacements }); | ||
return { | ||
content: [{ | ||
type: "text", | ||
text: "Empty search strings are not allowed. Please provide a non-empty string to search for." | ||
}], | ||
}; | ||
} | ||
// Read file as plain string | ||
@@ -93,2 +95,3 @@ const { content } = await readFile(filePath, false, 0, Number.MAX_SAFE_INTEGER); | ||
if (typeof content !== 'string') { | ||
capture('server_edit_block_content_not_string', { fileExtension: fileExtension, expectedReplacements }); | ||
throw new Error('Wrong content for file ' + filePath); | ||
@@ -139,2 +142,3 @@ } | ||
await writeFile(filePath, newContent); | ||
capture('server_edit_block_exact_success', { fileExtension: fileExtension, expectedReplacements, hasWarning: warningMessage !== "" }); | ||
return { | ||
@@ -149,2 +153,3 @@ content: [{ | ||
if (count > 0 && count !== expectedReplacements) { | ||
capture('server_edit_block_unexpected_count', { fileExtension: fileExtension, expectedReplacements, expectedReplacementsCount: count }); | ||
return { | ||
@@ -151,0 +156,0 @@ content: [{ |
@@ -59,9 +59,70 @@ import { terminalManager } from '../terminal-manager.js'; | ||
} | ||
const output = terminalManager.getNewOutput(parsed.data.pid); | ||
const { pid, timeout_ms = 5000 } = parsed.data; | ||
// Check if the process exists | ||
const session = terminalManager.getSession(pid); | ||
if (!session) { | ||
return { | ||
content: [{ type: "text", text: `No session found for PID ${pid}` }], | ||
isError: true, | ||
}; | ||
} | ||
// Wait for output with timeout | ||
let output = ""; | ||
let timeoutReached = false; | ||
try { | ||
// Create a promise that resolves when new output is available or when timeout is reached | ||
const outputPromise = new Promise((resolve) => { | ||
// Check for initial output | ||
const initialOutput = terminalManager.getNewOutput(pid); | ||
if (initialOutput && initialOutput.length > 0) { | ||
resolve(initialOutput); | ||
return; | ||
} | ||
let resolved = false; | ||
let interval = null; | ||
let timeout = null; | ||
const cleanup = () => { | ||
if (interval) { | ||
clearInterval(interval); | ||
interval = null; | ||
} | ||
if (timeout) { | ||
clearTimeout(timeout); | ||
timeout = null; | ||
} | ||
}; | ||
const resolveOnce = (value, isTimeout = false) => { | ||
if (resolved) | ||
return; | ||
resolved = true; | ||
cleanup(); | ||
if (isTimeout) | ||
timeoutReached = true; | ||
resolve(value); | ||
}; | ||
// Setup an interval to poll for output | ||
interval = setInterval(() => { | ||
const newOutput = terminalManager.getNewOutput(pid); | ||
if (newOutput && newOutput.length > 0) { | ||
resolveOnce(newOutput); | ||
} | ||
}, 300); // Check every 300ms | ||
// Set a timeout to stop waiting | ||
timeout = setTimeout(() => { | ||
const finalOutput = terminalManager.getNewOutput(pid) || ""; | ||
resolveOnce(finalOutput, true); | ||
}, timeout_ms); | ||
}); | ||
output = await outputPromise; | ||
} | ||
catch (error) { | ||
return { | ||
content: [{ type: "text", text: `Error reading output: ${error}` }], | ||
isError: true, | ||
}; | ||
} | ||
return { | ||
content: [{ | ||
type: "text", | ||
text: output === null | ||
? `No session found for PID ${parsed.data.pid}` | ||
: output || 'No new output available' | ||
text: output || 'No new output available' + (timeoutReached ? ' (timeout reached)' : '') | ||
}], | ||
@@ -68,0 +129,0 @@ }; |
@@ -392,8 +392,2 @@ import fs from "fs/promises"; | ||
catch (error) { | ||
// Only sanitize for telemetry, not for the returned error | ||
capture('server_search_read_dir_error', { | ||
errorType: error instanceof Error ? error.name : 'Unknown', | ||
error: 'Error reading directory', | ||
isReadDirError: true | ||
}); | ||
return; // Skip this directory on error | ||
@@ -400,0 +394,0 @@ } |
@@ -16,11 +16,11 @@ import { z } from "zod"; | ||
command: z.ZodString; | ||
timeout_ms: z.ZodOptional<z.ZodNumber>; | ||
timeout_ms: z.ZodNumber; | ||
shell: z.ZodOptional<z.ZodString>; | ||
}, "strip", z.ZodTypeAny, { | ||
command: string; | ||
timeout_ms?: number | undefined; | ||
timeout_ms: number; | ||
shell?: string | undefined; | ||
}, { | ||
command: string; | ||
timeout_ms?: number | undefined; | ||
timeout_ms: number; | ||
shell?: string | undefined; | ||
@@ -30,6 +30,9 @@ }>; | ||
pid: z.ZodNumber; | ||
timeout_ms: z.ZodOptional<z.ZodNumber>; | ||
}, "strip", z.ZodTypeAny, { | ||
pid: number; | ||
timeout_ms?: number | undefined; | ||
}, { | ||
pid: number; | ||
timeout_ms?: number | undefined; | ||
}>; | ||
@@ -36,0 +39,0 @@ export declare const ForceTerminateArgsSchema: z.ZodObject<{ |
@@ -14,3 +14,3 @@ import { z } from "zod"; | ||
command: z.string(), | ||
timeout_ms: z.number().optional(), | ||
timeout_ms: z.number(), | ||
shell: z.string().optional(), | ||
@@ -20,2 +20,3 @@ }); | ||
pid: z.number(), | ||
timeout_ms: z.number().optional(), | ||
}); | ||
@@ -22,0 +23,0 @@ export const ForceTerminateArgsSchema = z.object({ |
@@ -1,1 +0,1 @@ | ||
export declare const VERSION = "0.2.1"; | ||
export declare const VERSION = "0.2.2"; |
@@ -1,1 +0,1 @@ | ||
export const VERSION = '0.2.1'; | ||
export const VERSION = '0.2.2'; |
{ | ||
"name": "@wonderwhy-er/desktop-commander", | ||
"version": "0.2.1", | ||
"version": "0.2.2", | ||
"description": "MCP server for terminal operations and file editing", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -69,3 +69,5 @@ # Desktop Commander MCP | ||
### Option 1: Install through npx | ||
> **π Update & Uninstall Information:** Before choosing an installation option, note that **only Options 1 and 3 have automatic updates**. Options 2, 4, and 5 require manual updates. See the sections below for update and uninstall instructions for each option. | ||
### Option 1: Install through npx β **Auto-Updates** | ||
Just run this in terminal: | ||
@@ -82,3 +84,7 @@ ``` | ||
### Option 2: Using bash script installer (macOS) | ||
**β Auto-Updates:** Yes - automatically updates when you restart Claude | ||
**π Manual Update:** Run the setup command again | ||
**ποΈ Uninstall:** Run `npx @wonderwhy-er/desktop-commander@latest setup --uninstall` | ||
### Option 2: Using bash script installer (macOS) β **Auto-Updates** | ||
For macOS users, you can use our automated bash installer which will check your Node.js version, install it if needed, and automatically configure Desktop Commander: | ||
@@ -90,4 +96,8 @@ ``` | ||
### Option 3: Installing via Smithery | ||
**β Auto-Updates:** Yes - requires manual updates | ||
**π Manual Update:** Re-run the bash installer command above | ||
**ποΈ Uninstall:** Remove the MCP server entry from your Claude config file and delete the cloned repository if it exists | ||
### Option 3: Installing via Smithery β **Auto-Updates** | ||
To install Desktop Commander for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@wonderwhy-er/desktop-commander): | ||
@@ -99,3 +109,7 @@ | ||
### Option 4: Add to claude_desktop_config manually | ||
**β Auto-Updates:** Yes - automatically updates when you restart Claude | ||
**π Manual Update:** Re-run the Smithery install command | ||
**ποΈ Uninstall:** `npx -y @smithery/cli uninstall @wonderwhy-er/desktop-commander --client claude` | ||
### Option 4: Add to claude_desktop_config manually β **Manual Updates** | ||
Add this entry to your claude_desktop_config.json: | ||
@@ -122,3 +136,7 @@ | ||
### Option 5: Checkout locally | ||
**β Auto-Updates:** No - uses npx but config might not update automatically | ||
**π Manual Update:** Usually automatic via npx, but if issues occur, update your config file or re-add the entry | ||
**ποΈ Uninstall:** Remove the "desktop-commander" entry from your claude_desktop_config.json file | ||
### Option 5: Checkout locally β **Manual Updates** | ||
1. Clone and build: | ||
@@ -138,8 +156,25 @@ ```bash | ||
### Updating Desktop Commander | ||
**β Auto-Updates:** No - requires manual git updates | ||
**π Manual Update:** `cd DesktopCommanderMCP && git pull && npm run setup` | ||
**ποΈ Uninstall:** Remove the cloned directory and remove MCP server entry from Claude config | ||
When installed through npx (Option 1) or Smithery (Option 3), Desktop Commander will automatically update to the latest version whenever you restart Claude. No manual update process is needed. | ||
## Updating & Uninstalling Desktop Commander | ||
For manual installations, you can update by running the setup command again. | ||
### Automatic Updates (Options 1 & 3 only) | ||
**Options 1 (npx) and 3 (Smithery)** automatically update to the latest version whenever you restart Claude. No manual intervention needed. | ||
### Manual Updates (Options 2, 4 & 5) | ||
- **Option 2 (bash installer):** Re-run the curl command | ||
- **Option 4 (manual config):** Usually automatic via npx, but re-add config entry if issues occur | ||
- **Option 5 (local checkout):** `cd DesktopCommanderMCP && git pull && npm run setup` | ||
### Uninstalling Desktop Commander | ||
- **Option 1:** `npx @wonderwhy-er/desktop-commander@latest setup --uninstall` | ||
- **Option 2:** Remove MCP server entry from Claude config and delete any cloned repositories | ||
- **Option 3:** `npx -y @smithery/cli uninstall @wonderwhy-er/desktop-commander --client claude` | ||
- **Option 4:** Remove the "desktop-commander" entry from your claude_desktop_config.json file | ||
- **Option 5:** Delete the cloned directory and remove MCP server entry from Claude config | ||
After uninstalling, restart Claude Desktop to complete the removal. | ||
## Usage | ||
@@ -307,2 +342,26 @@ | ||
#### Understanding fileWriteLineLimit | ||
The `fileWriteLineLimit` setting controls how many lines can be written in a single `write_file` operation (default: 50 lines). This limit exists for several important reasons: | ||
**Why the limit exists:** | ||
- **AIs are wasteful with tokens**: Instead of doing two small edits in a file, AIs may decide to rewrite the whole thing. We're trying to force AIs to do things in smaller changes as it saves time and tokens | ||
- **Claude UX message limits**: There are limits within one message and hitting "Continue" does not really work. What we're trying here is to make AI work in smaller chunks so when you hit that limit, multiple chunks have succeeded and that work is not lost - it just needs to restart from the last chunk | ||
**Setting the limit:** | ||
```javascript | ||
// You can set it to thousands if you want | ||
set_config_value({ "key": "fileWriteLineLimit", "value": 1000 }) | ||
// Or keep it smaller to force more efficient behavior | ||
set_config_value({ "key": "fileWriteLineLimit", "value": 25 }) | ||
``` | ||
**Maximum value**: You can set it to thousands if you want - there's no technical restriction. | ||
**Best practices**: | ||
- Keep the default (50) to encourage efficient AI behavior and avoid token waste | ||
- The system automatically suggests chunking when limits are exceeded | ||
- Smaller chunks mean less work lost when Claude hits message limits | ||
### Best Practices | ||
@@ -309,0 +368,0 @@ |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
1333758
9.09%159
22.31%9297
0.94%618
10.55%40
2.56%