UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

728 lines (711 loc) • 29.8 kB
import os from 'os'; import fs from 'fs'; import path from 'path'; import { execFileSync } from 'child_process'; import { getDefaultShell, resolveShellPath } from './shell.js'; /** * Detect container environment and type */ function detectContainerEnvironment() { // Method 1: Check environment variables first (most reliable when set) // Docker-specific if (process.env.MCP_CLIENT_DOCKER === 'true') { return { isContainer: true, containerType: 'docker', orchestrator: null }; } // Kubernetes detection if (process.env.KUBERNETES_SERVICE_HOST || process.env.KUBERNETES_PORT) { return { isContainer: true, containerType: 'kubernetes', orchestrator: 'kubernetes' }; } // Podman detection if (process.env.PODMAN_CONTAINER || process.env.CONTAINER_HOST?.includes('podman')) { return { isContainer: true, containerType: 'podman', orchestrator: null }; } // Method 2: Check for container indicator files if (fs.existsSync('/.dockerenv')) { return { isContainer: true, containerType: 'docker', orchestrator: null }; } // Method 3: Check /proc/1/cgroup for container indicators (Linux only) if (os.platform() === 'linux') { try { const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8'); // Docker detection if (cgroup.includes('docker')) { return { isContainer: true, containerType: 'docker', orchestrator: null }; } // Kubernetes detection (pods run in containerd/cri-o) if (cgroup.includes('kubepods') || cgroup.includes('pod')) { return { isContainer: true, containerType: 'kubernetes', orchestrator: 'kubernetes' }; } // Podman detection if (cgroup.includes('podman') || cgroup.includes('libpod')) { return { isContainer: true, containerType: 'podman', orchestrator: null }; } // LXC detection if (cgroup.includes('lxc')) { return { isContainer: true, containerType: 'lxc', orchestrator: null }; } // systemd-nspawn detection if (cgroup.includes('machine.slice')) { return { isContainer: true, containerType: 'systemd-nspawn', orchestrator: null }; } // Generic containerd detection if (cgroup.includes('containerd')) { return { isContainer: true, containerType: 'other', orchestrator: null }; } } catch (error) { // /proc/1/cgroup might not exist } } // Method 4: Check /proc/1/environ for container indicators if (os.platform() === 'linux') { try { const environ = fs.readFileSync('/proc/1/environ', 'utf8'); if (environ.includes('container=')) { // systemd-nspawn sets container=systemd-nspawn if (environ.includes('container=systemd-nspawn')) { return { isContainer: true, containerType: 'systemd-nspawn', orchestrator: null }; } // LXC sets container=lxc if (environ.includes('container=lxc')) { return { isContainer: true, containerType: 'lxc', orchestrator: null }; } // Generic container detection return { isContainer: true, containerType: 'other', orchestrator: null }; } } catch (error) { // /proc/1/environ might not exist or be accessible } } // Method 5: Check hostname for Kubernetes patterns try { const hostname = os.hostname(); // Kubernetes pods often have hostnames like: podname-deploymentid-randomid if (hostname && (hostname.includes('-') && hostname.split('-').length >= 3)) { // Additional check for Kubernetes service account if (fs.existsSync('/var/run/secrets/kubernetes.io')) { return { isContainer: true, containerType: 'kubernetes', orchestrator: 'kubernetes' }; } } } catch (error) { // Hostname check failed } // Method 6: Check for orchestrator-specific indicators // Docker Compose detection if (process.env.COMPOSE_PROJECT_NAME || process.env.COMPOSE_SERVICE) { return { isContainer: true, containerType: 'docker', orchestrator: 'docker-compose' }; } // Docker Swarm detection if (process.env.DOCKER_SWARM_MODE || process.env.DOCKER_NODE_ID) { return { isContainer: true, containerType: 'docker', orchestrator: 'docker-swarm' }; } return { isContainer: false, containerType: null, orchestrator: null }; } /** * Discover container mount points */ function discoverContainerMounts(isContainer) { const mounts = []; if (!isContainer) { return mounts; } // Method 1: Parse /proc/mounts (Linux only) if (os.platform() === 'linux') { try { const mountsContent = fs.readFileSync('/proc/mounts', 'utf8'); const mountLines = mountsContent.split('\n'); // System filesystem types that are never user mounts const systemFsTypes = new Set([ 'overlay', 'tmpfs', 'proc', 'sysfs', 'devpts', 'cgroup', 'cgroup2', 'mqueue', 'debugfs', 'securityfs', 'pstore', 'configfs', 'fusectl', 'hugetlbfs', 'autofs', 'devtmpfs', 'bpf', 'tracefs', 'shm' ]); // Filesystem types that indicate host mounts const hostMountFsTypes = new Set(['fakeowner', '9p', 'virtiofs', 'fuse.sshfs']); for (const line of mountLines) { const parts = line.split(' '); if (parts.length >= 4) { const device = parts[0]; const mountPoint = parts[1]; const fsType = parts[2]; const options = parts[3].split(','); // Skip system mount points const isSystemMountPoint = mountPoint === '/' || mountPoint.startsWith('/dev') || mountPoint.startsWith('/sys') || mountPoint.startsWith('/proc') || mountPoint.startsWith('/run') || mountPoint.startsWith('/sbin') || mountPoint === '/etc/resolv.conf' || mountPoint === '/etc/hostname' || mountPoint === '/etc/hosts'; if (isSystemMountPoint) { continue; } // Detect user mounts by: // 1. Known host-mount filesystem types (fakeowner, 9p, virtiofs) // 2. Device from /run/host_mark/ (docker-mcp-gateway pattern) // 3. Non-system filesystem type with user-like mount point const isHostMountFs = hostMountFsTypes.has(fsType); const isHostMarkDevice = device.startsWith('/run/host_mark/'); const isNonSystemFs = !systemFsTypes.has(fsType); const isUserLikePath = mountPoint.startsWith('/mnt/') || mountPoint.startsWith('/workspace') || mountPoint.startsWith('/data/') || mountPoint.startsWith('/home/') || mountPoint.startsWith('/Users/') || mountPoint.startsWith('/app/') || mountPoint.startsWith('/project/') || mountPoint.startsWith('/src/') || mountPoint.startsWith('/code/'); if (isHostMountFs || isHostMarkDevice || (isNonSystemFs && isUserLikePath)) { const isReadOnly = options.includes('ro'); mounts.push({ hostPath: device, containerPath: mountPoint, type: 'bind', readOnly: isReadOnly, verifiedMount: true, description: `Mounted directory: ${path.basename(mountPoint)}` }); } } } } catch (error) { // /proc/mounts might not be available } } // Method 2: Check /mnt directory contents try { if (fs.existsSync('/mnt')) { const contents = fs.readdirSync('/mnt'); for (const item of contents) { const itemPath = `/mnt/${item}`; try { const stats = fs.statSync(itemPath); if (stats.isDirectory()) { // Check if we already have this mount const exists = mounts.some(m => m.containerPath === itemPath); if (!exists) { mounts.push({ hostPath: `<host>/${item}`, containerPath: itemPath, type: 'bind', readOnly: false, verifiedMount: false, description: `Candidate folder under /mnt: ${item}` }); } } } catch (itemError) { // Skip items we can't stat } } } } catch (error) { // /mnt directory doesn't exist or not accessible } // Method 3: Check /home directory for user-mounted folders (Desktop Commander Docker installer pattern) try { if (fs.existsSync('/home')) { const contents = fs.readdirSync('/home'); for (const item of contents) { // Skip the root user directory and common system directories if (item === 'root' || item === 'node' || item === 'bin' || item === 'sbin' || item === 'usr' || item === 'lib' || item === 'lib64' || item === 'var' || item === 'tmp' || item === 'opt' || item === 'sys' || item === 'proc') { continue; } const itemPath = `/home/${item}`; try { const stats = fs.statSync(itemPath); if (stats.isDirectory()) { // Check if we already have this mount const exists = mounts.some(m => m.containerPath === itemPath); if (!exists) { mounts.push({ hostPath: `<host>/${item}`, containerPath: itemPath, type: 'bind', readOnly: false, verifiedMount: false, description: `Candidate folder under /home: ${item}` }); } } } catch (itemError) { // Skip items we can't stat } } } } catch (error) { // /home directory doesn't exist or not accessible } return mounts; } /** * Get container environment information */ function getContainerEnvironment(containerType) { const env = {}; // Try to get container name from hostname (often set to container ID/name) try { const hostname = os.hostname(); if (hostname && hostname !== 'localhost') { env.containerName = hostname; } } catch (error) { // Hostname not available } // Docker-specific environment if (containerType === 'docker') { // Try multiple sources for Docker image name if (process.env.DOCKER_IMAGE) { env.dockerImage = process.env.DOCKER_IMAGE; } else if (process.env.IMAGE_NAME) { env.dockerImage = process.env.IMAGE_NAME; } else if (process.env.CONTAINER_IMAGE) { env.dockerImage = process.env.CONTAINER_IMAGE; } // Try to read from Docker labels if available (less common but possible) try { if (fs.existsSync('/proc/self/cgroup')) { const cgroup = fs.readFileSync('/proc/self/cgroup', 'utf8'); // Extract container ID from cgroup path const containerIdMatch = cgroup.match(/docker\/([a-f0-9]{64})/); if (containerIdMatch && !env.containerName) { // Use short container ID as fallback name env.containerName = containerIdMatch[1].substring(0, 12); } } } catch (error) { // Ignore errors reading cgroup } } // Kubernetes-specific environment if (containerType === 'kubernetes') { if (process.env.KUBERNETES_NAMESPACE || process.env.POD_NAMESPACE) { env.kubernetesNamespace = process.env.KUBERNETES_NAMESPACE || process.env.POD_NAMESPACE; } if (process.env.POD_NAME || process.env.HOSTNAME) { env.kubernetesPod = process.env.POD_NAME || process.env.HOSTNAME; } if (process.env.NODE_NAME || process.env.KUBERNETES_NODE_NAME) { env.kubernetesNode = process.env.NODE_NAME || process.env.KUBERNETES_NODE_NAME; } // Try to get container image from common Kubernetes environment variables if (process.env.CONTAINER_IMAGE) { env.dockerImage = process.env.CONTAINER_IMAGE; } else if (process.env.IMAGE_NAME) { env.dockerImage = process.env.IMAGE_NAME; } // Try to read Kubernetes service account info try { if (fs.existsSync('/var/run/secrets/kubernetes.io/serviceaccount/namespace')) { const namespace = fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/namespace', 'utf8').trim(); if (namespace && !env.kubernetesNamespace) { env.kubernetesNamespace = namespace; } } } catch (error) { // Service account info not available } } // Podman-specific environment if (containerType === 'podman') { // Podman uses similar environment variables to Docker if (process.env.CONTAINER_IMAGE || process.env.PODMAN_IMAGE) { env.dockerImage = process.env.CONTAINER_IMAGE || process.env.PODMAN_IMAGE; } } // LXC-specific environment if (containerType === 'lxc') { // LXC containers might have different naming conventions if (process.env.LXC_NAME) { env.containerName = process.env.LXC_NAME; } } // Try to detect host platform if (process.env.HOST_PLATFORM) { env.hostPlatform = process.env.HOST_PLATFORM; } return Object.keys(env).length > 0 ? env : undefined; } /** * Detect Node.js installation and version from current process */ function detectNodeInfo() { try { // Get Node.js version from current process const version = process.version.replace('v', ''); // Remove 'v' prefix // Get Node.js executable path from current process const path = process.execPath; // Get npm version from environment if available const npmVersion = process.env.npm_version; return { version, path, ...(npmVersion && { npmVersion }) }; } catch (error) { return undefined; } } /** * Detect Python installation and version and put on systeminfo.pythonInfo */ function detectPythonInfo() { // Try python commands in order of preference const pythonCommands = process.platform === 'win32' ? ['python', 'python3', 'py'] // Windows: 'python' is common, 'py' launcher : ['python3', 'python']; // Unix: prefer python3 for (const cmd of pythonCommands) { // The python on PATH, never one in the working folder (which Windows // searches first for a bare name); not on PATH means not available const program = resolveShellPath(cmd); if (!program) continue; try { const version = execFileSync(program, ['--version'], { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim(); // Verify it's Python 3.x if (version.includes('Python 3')) { return { available: true, command: cmd, version: version.replace('Python ', '') }; } } catch { // Command not found or failed, try next } } return { available: false, command: '' }; } /** * Get comprehensive system information for tool prompts */ export function getSystemInfo() { const platform = os.platform(); const isWindows = platform === 'win32'; const isMacOS = platform === 'darwin'; const isLinux = platform === 'linux'; // Container detection const containerDetection = detectContainerEnvironment(); const mountPoints = containerDetection.isContainer ? discoverContainerMounts(containerDetection.isContainer) : []; let platformName; const defaultShell = getDefaultShell(); let pathSeparator; let examplePaths; if (isWindows) { platformName = 'Windows'; pathSeparator = '\\'; examplePaths = { home: 'C:\\Users\\username', temp: 'C:\\Temp', absolute: 'C:\\path\\to\\file.txt' }; } else if (isMacOS) { platformName = 'macOS'; pathSeparator = '/'; examplePaths = { home: '/Users/username', temp: '/tmp', absolute: '/path/to/file.txt' }; } else if (isLinux) { platformName = 'Linux'; pathSeparator = '/'; examplePaths = { home: '/home/username', temp: '/tmp', absolute: '/path/to/file.txt' }; } else { // Fallback for other Unix-like systems platformName = 'Unix'; pathSeparator = '/'; examplePaths = { home: '/home/username', temp: '/tmp', absolute: '/path/to/file.txt' }; } // Adjust platform name for containers if (containerDetection.isContainer) { let containerLabel = ''; if (containerDetection.containerType === 'kubernetes') { containerLabel = 'Kubernetes'; if (containerDetection.orchestrator === 'kubernetes') { containerLabel += ' Pod'; } } else if (containerDetection.containerType === 'docker') { containerLabel = 'Docker'; if (containerDetection.orchestrator === 'docker-compose') { containerLabel += ' Compose'; } else if (containerDetection.orchestrator === 'docker-swarm') { containerLabel += ' Swarm'; } } else if (containerDetection.containerType === 'podman') { containerLabel = 'Podman'; } else if (containerDetection.containerType === 'lxc') { containerLabel = 'LXC'; } else if (containerDetection.containerType === 'systemd-nspawn') { containerLabel = 'systemd-nspawn'; } else { containerLabel = 'Container'; } platformName = `${platformName} (${containerLabel})`; // Add accessible paths from mounts if (mountPoints.length > 0) { examplePaths.accessible = mountPoints.map(mount => mount.containerPath); } } // Detect Node.js installation from current process const nodeInfo = detectNodeInfo(); // Detect Python installation const pythonInfo = detectPythonInfo(); // Get process information const processInfo = { pid: process.pid, arch: process.arch, platform: process.platform, versions: process.versions }; return { platform, platformName, defaultShell, pathSeparator, isWindows, isMacOS, isLinux, docker: { // New container detection fields isContainer: containerDetection.isContainer, containerType: containerDetection.containerType, orchestrator: containerDetection.orchestrator, // Backward compatibility - keep old field isDocker: containerDetection.isContainer && containerDetection.containerType === 'docker', mountPoints, containerEnvironment: getContainerEnvironment(containerDetection.containerType) }, isDXT: !!process.env.MCP_DXT, nodeInfo, pythonInfo, processInfo, examplePaths }; } /** * Generate OS-specific guidance for tool prompts */ export function getOSSpecificGuidance(systemInfo) { const { platformName, defaultShell, isWindows, docker } = systemInfo; let guidance = `Running on ${platformName}. Default shell: ${defaultShell}.`; // Container-specific guidance if (docker.isContainer) { const containerTypeLabel = docker.containerType === 'kubernetes' ? 'KUBERNETES POD' : docker.containerType === 'docker' ? 'DOCKER CONTAINER' : docker.containerType === 'podman' ? 'PODMAN CONTAINER' : docker.containerType === 'lxc' ? 'LXC CONTAINER' : docker.containerType === 'systemd-nspawn' ? 'SYSTEMD-NSPAWN CONTAINER' : 'CONTAINER'; guidance += ` 🐳 ${containerTypeLabel} ENVIRONMENT DETECTED:`; if (docker.containerType === 'kubernetes') { guidance += ` This Desktop Commander instance is running inside a Kubernetes pod.`; // Add Kubernetes-specific info if (docker.containerEnvironment?.kubernetesNamespace) { guidance += ` Namespace: ${docker.containerEnvironment.kubernetesNamespace}`; } if (docker.containerEnvironment?.kubernetesPod) { guidance += ` Pod: ${docker.containerEnvironment.kubernetesPod}`; } if (docker.containerEnvironment?.kubernetesNode) { guidance += ` Node: ${docker.containerEnvironment.kubernetesNode}`; } } else if (docker.containerType === 'docker') { guidance += ` This Desktop Commander instance is running inside a Docker container.`; if (docker.orchestrator === 'docker-compose') { guidance += ` (Docker Compose)`; } else if (docker.orchestrator === 'docker-swarm') { guidance += ` (Docker Swarm)`; } } else { guidance += ` This Desktop Commander instance is running inside a ${docker.containerType || 'container'} environment.`; } if (docker.mountPoints.length > 0) { guidance += ` AVAILABLE MOUNT LOCATIONS:`; for (const mount of docker.mountPoints) { const access = mount.readOnly ? '(read-only)' : '(read-write)'; const verification = mount.verifiedMount ? '[verified mount]' : '[unverified candidate]'; guidance += ` - ${mount.containerPath} ${access} ${verification} - ${mount.description}`; } guidance += ` Entries marked [verified mount] are suitable for persistent file work. Entries marked [unverified candidate] are inferred from common container paths and may not persist. Files outside verified mounts may be lost when the container stops. PATH TRANSLATION IN DOCKER: Host paths may require translation to their corresponding container paths: Windows: "C:\\projects\\data\\file.txt" → "/home/projects/data/file.txt" Linux/Mac: "/Users/john/projects/data/file.txt" → "/home/projects/data/file.txt" Typical translation removes the drive letter or user prefix, keeps the full folder structure, and maps it under /home/. NOTE: Desktop Commander Docker installer mounts host folders to /home/[folder-name].`; } else { guidance += ` ⚠️ WARNING: No mounted directories detected. Files created outside mounted volumes will be lost when the container stops. Persistent file work requires a mounted directory, available through the Docker installer or a -v mount. Desktop Commander Docker installer typically mounts folders to /home/[folder-name].`; } if (docker.containerEnvironment?.containerName) { guidance += ` Container: ${docker.containerEnvironment.containerName}`; } } if (isWindows) { guidance += ` WINDOWS-SPECIFIC TROUBLESHOOTING: - If Node.js/Python commands fail with "not recognized" errors: * Alternative shells include "cmd" and "powershell.exe" * PowerShell may have execution policy restrictions for some tools * CMD typically has better compatibility with development tools * defaultShell can be changed with set_config_value if needed - Windows services and processes use different commands (Get-Process vs ps) - Package managers: choco, winget, scoop instead of apt/brew - Environment variables: $env:VAR instead of $VAR - File permissions work differently than Unix systems`; } else if (systemInfo.isMacOS) { guidance += ` MACOS-SPECIFIC NOTES: - Package manager: brew (Homebrew) is commonly used - Python 3 might be 'python3' command, not 'python' - Some GNU tools have different names (e.g., gsed instead of sed) - System Integrity Protection (SIP) may block certain operations - The 'open' command opens files and applications from the terminal - mdfind (Spotlight) provides fast exact filename searches`; } else { guidance += ` LINUX-SPECIFIC NOTES: - Package managers vary by distro: apt, yum, dnf, pacman, zypper - Python 3 might be 'python3' command, not 'python' - Standard Unix shell tools available (grep, awk, sed, etc.) - File permissions and ownership important for many operations - Systemd services common on modern distributions`; } return guidance; } /** * Get common development tool guidance based on OS */ export function getDevelopmentToolGuidance(systemInfo) { const { isWindows, isMacOS, isLinux, platformName, nodeInfo, processInfo } = systemInfo; // Add detected Node.js info to guidance const nodeGuidance = nodeInfo ? `Node.js: v${nodeInfo.version} (${nodeInfo.path})${nodeInfo.npmVersion ? ` | npm: v${nodeInfo.npmVersion}` : ''}` : 'Node.js: Not detected'; // Add process environment info const envInfo = ` Current Process Environment: - Node: v${processInfo.versions.node} - V8: v${processInfo.versions.v8} - Architecture: ${processInfo.arch} - Platform: ${processInfo.platform} - Process ID: ${processInfo.pid}`; if (isWindows) { return ` COMMON WINDOWS DEVELOPMENT TOOLS: - ${nodeGuidance} - Python: May be 'python' or 'py' command, check both - Git: Git Bash provides Unix-like environment - WSL: Windows Subsystem for Linux available for Unix tools - Visual Studio tools: cl, msbuild for C++ compilation ${envInfo}`; } else if (isMacOS) { return ` COMMON MACOS DEVELOPMENT TOOLS: - Xcode Command Line Tools: Required for many development tools - Homebrew: Primary package manager for development tools - ${nodeGuidance} - Python: Usually python3, check if python points to Python 2 - Ruby: System Ruby available, rbenv/rvm for version management ${envInfo}`; } else { return ` COMMON LINUX DEVELOPMENT TOOLS: - Package managers: Install tools via distribution package manager - Python: Usually python3, python may point to Python 2 - ${nodeGuidance} - Build tools: gcc, make typically available or easily installed - Container tools: docker, podman common for development ${envInfo}`; } } /** * Get path guidance (simplified since paths are normalized) */ export function getPathGuidance(systemInfo) { let guidance = `Absolute paths are the most reliable because they do not depend on the current working directory. Paths are automatically normalized regardless of slash direction.`; if (systemInfo.docker.isContainer && systemInfo.docker.mountPoints.length > 0) { const containerLabel = systemInfo.docker.containerType === 'kubernetes' ? 'KUBERNETES' : systemInfo.docker.containerType === 'docker' ? 'DOCKER' : systemInfo.docker.containerType === 'podman' ? 'PODMAN' : 'CONTAINER'; const verifiedMounts = systemInfo.docker.mountPoints.filter(m => m.verifiedMount).map(m => m.containerPath); const unverifiedCandidates = systemInfo.docker.mountPoints.filter(m => !m.verifiedMount).map(m => m.containerPath); if (verifiedMounts.length > 0) { guidance += ` 🐳 ${containerLabel}: Verified mounts for persistent file operations: ${verifiedMounts.join(', ')}. Files outside verified mounts may not persist after the container stops.`; } if (unverifiedCandidates.length > 0) { guidance += ` 🐳 ${containerLabel}: Unverified mount candidates: ${unverifiedCandidates.join(', ')}. These paths are inferred from common container directories and may not persist.`; } } return guidance; }