@wonderwhy-er/desktop-commander 0.2.9 → 0.2.11

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.
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Installation Source Tracking Script
5
+ * Runs during npm install to detect how Desktop Commander was installed
6
+ *
7
+ * Debug logging can be enabled with:
8
+ * - DEBUG=desktop-commander npm install
9
+ * - DEBUG=* npm install
10
+ * - NODE_ENV=development npm install
11
+ * - DC_DEBUG=true npm install
12
+ */
13
+
14
+ import { randomUUID } from 'crypto';
15
+ import * as https from 'https';
16
+ import { platform } from 'os';
17
+ import path from 'path';
18
+ import { fileURLToPath } from 'url';
19
+
20
+ // Get current file directory
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+
24
+ // Debug logging utility - configurable via environment variables
25
+ const DEBUG_ENABLED = process.env.DEBUG === 'desktop-commander' ||
26
+ process.env.DEBUG === '*' ||
27
+ process.env.NODE_ENV === 'development' ||
28
+ process.env.DC_DEBUG === 'true';
29
+
30
+ const debug = (...args) => {
31
+ if (DEBUG_ENABLED) {
32
+ console.log('[Desktop Commander Debug]', ...args);
33
+ }
34
+ };
35
+
36
+ const log = (...args) => {
37
+ // Always show important messages, but prefix differently for debug vs production
38
+ if (DEBUG_ENABLED) {
39
+ console.log('[Desktop Commander]', ...args);
40
+ }
41
+ };
42
+
43
+ /**
44
+ * Get the client ID from the Desktop Commander config file, or generate a new one
45
+ */
46
+ async function getClientId() {
47
+ try {
48
+ const { homedir } = await import('os');
49
+ const { join } = await import('path');
50
+ const fs = await import('fs');
51
+
52
+ const USER_HOME = homedir();
53
+ const CONFIG_DIR = join(USER_HOME, '.claude-server-commander');
54
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
55
+
56
+ // Try to read existing config
57
+ if (fs.existsSync(CONFIG_FILE)) {
58
+ const configData = fs.readFileSync(CONFIG_FILE, 'utf8');
59
+ const config = JSON.parse(configData);
60
+ if (config.clientId) {
61
+ debug(`Using existing clientId from config: ${config.clientId.substring(0, 8)}...`);
62
+ return config.clientId;
63
+ }
64
+ }
65
+
66
+ debug('No existing clientId found, generating new one');
67
+ // Fallback to random UUID if config doesn't exist or lacks clientId
68
+ return randomUUID();
69
+ } catch (error) {
70
+ debug(`Error reading config file: ${error.message}, using random UUID`);
71
+ // If anything goes wrong, fall back to random UUID
72
+ return randomUUID();
73
+ }
74
+ }
75
+
76
+ // Google Analytics configuration (same as setup script)
77
+ const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L';
78
+ const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A';
79
+ const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
80
+
81
+ /**
82
+ * Detect installation source from environment and process context
83
+ */
84
+ async function detectInstallationSource() {
85
+ // Check npm environment variables for clues
86
+ const npmConfigUserAgent = process.env.npm_config_user_agent || '';
87
+ const npmExecpath = process.env.npm_execpath || '';
88
+ const npmCommand = process.env.npm_command || '';
89
+ const npmLifecycleEvent = process.env.npm_lifecycle_event || '';
90
+
91
+ // Check process arguments and parent commands
92
+ const processArgs = process.argv.join(' ');
93
+ const processTitle = process.title || '';
94
+
95
+ debug('Installation source detection...');
96
+ debug(`npm_config_user_agent: ${npmConfigUserAgent}`);
97
+ debug(`npm_execpath: ${npmExecpath}`);
98
+ debug(`npm_command: ${npmCommand}`);
99
+ debug(`npm_lifecycle_event: ${npmLifecycleEvent}`);
100
+ debug(`process.argv: ${processArgs}`);
101
+ debug(`process.title: ${processTitle}`);
102
+
103
+ // Try to get parent process information
104
+ let parentProcessInfo = null;
105
+ try {
106
+ const { execSync } = await import('child_process');
107
+ const ppid = process.ppid;
108
+ if (ppid && process.platform !== 'win32') {
109
+ // Get parent process command line on Unix systems
110
+ const parentCmd = execSync(`ps -p ${ppid} -o command=`, { encoding: 'utf8' }).trim();
111
+ parentProcessInfo = parentCmd;
112
+ debug(`parent process: ${parentCmd}`);
113
+ }
114
+ } catch (error) {
115
+ debug(`Could not get parent process info: ${error.message}`);
116
+ }
117
+
118
+ // Smithery detection - look for smithery in the process chain
119
+ const smitheryIndicators = [
120
+ npmConfigUserAgent.includes('smithery'),
121
+ npmExecpath.includes('smithery'),
122
+ processArgs.includes('smithery'),
123
+ processArgs.includes('@smithery/cli'),
124
+ processTitle.includes('smithery'),
125
+ parentProcessInfo && parentProcessInfo.includes('smithery'),
126
+ parentProcessInfo && parentProcessInfo.includes('@smithery/cli')
127
+ ];
128
+
129
+ if (smitheryIndicators.some(indicator => indicator)) {
130
+ return {
131
+ source: 'smithery',
132
+ details: {
133
+ detection_method: 'process_chain',
134
+ user_agent: npmConfigUserAgent,
135
+ exec_path: npmExecpath,
136
+ command: npmCommand,
137
+ parent_process: parentProcessInfo || 'unknown',
138
+ process_args: processArgs
139
+ }
140
+ };
141
+ }
142
+
143
+ // Direct NPX usage
144
+ if (npmCommand === 'exec' || processArgs.includes('npx')) {
145
+ return {
146
+ source: 'npx-direct',
147
+ details: {
148
+ user_agent: npmConfigUserAgent,
149
+ command: npmCommand,
150
+ lifecycle_event: npmLifecycleEvent
151
+ }
152
+ };
153
+ }
154
+
155
+ // Regular npm install
156
+ if (npmCommand === 'install' || npmLifecycleEvent === 'postinstall') {
157
+ return {
158
+ source: 'npm-install',
159
+ details: {
160
+ user_agent: npmConfigUserAgent,
161
+ command: npmCommand,
162
+ lifecycle_event: npmLifecycleEvent
163
+ }
164
+ };
165
+ }
166
+
167
+ // GitHub Codespaces
168
+ if (process.env.CODESPACES) {
169
+ return {
170
+ source: 'github-codespaces',
171
+ details: {
172
+ codespace: process.env.CODESPACE_NAME || 'unknown'
173
+ }
174
+ };
175
+ }
176
+
177
+ // VS Code
178
+ if (process.env.VSCODE_PID || process.env.TERM_PROGRAM === 'vscode') {
179
+ return {
180
+ source: 'vscode',
181
+ details: {
182
+ term_program: process.env.TERM_PROGRAM,
183
+ vscode_pid: process.env.VSCODE_PID
184
+ }
185
+ };
186
+ }
187
+
188
+ // GitPod
189
+ if (process.env.GITPOD_WORKSPACE_ID) {
190
+ return {
191
+ source: 'gitpod',
192
+ details: {
193
+ workspace_id: process.env.GITPOD_WORKSPACE_ID.substring(0, 8) + '...' // Truncate for privacy
194
+ }
195
+ };
196
+ }
197
+
198
+ // CI/CD environments
199
+ if (process.env.CI) {
200
+ if (process.env.GITHUB_ACTIONS) {
201
+ return {
202
+ source: 'github-actions',
203
+ details: {
204
+ repository: process.env.GITHUB_REPOSITORY,
205
+ workflow: process.env.GITHUB_WORKFLOW
206
+ }
207
+ };
208
+ }
209
+ if (process.env.GITLAB_CI) {
210
+ return {
211
+ source: 'gitlab-ci',
212
+ details: {
213
+ project: process.env.CI_PROJECT_NAME
214
+ }
215
+ };
216
+ }
217
+ if (process.env.JENKINS_URL) {
218
+ return {
219
+ source: 'jenkins',
220
+ details: {
221
+ job: process.env.JOB_NAME
222
+ }
223
+ };
224
+ }
225
+ return {
226
+ source: 'ci-cd-other',
227
+ details: {
228
+ ci_env: 'unknown'
229
+ }
230
+ };
231
+ }
232
+
233
+ // Docker detection
234
+ if (process.env.DOCKER_CONTAINER) {
235
+ return {
236
+ source: 'docker',
237
+ details: {
238
+ container_id: process.env.HOSTNAME?.substring(0, 8) + '...' || 'unknown'
239
+ }
240
+ };
241
+ }
242
+
243
+ // Check for .dockerenv file (need to use fs import)
244
+ try {
245
+ const fs = await import('fs');
246
+ if (fs.existsSync('/.dockerenv')) {
247
+ return {
248
+ source: 'docker',
249
+ details: {
250
+ container_id: process.env.HOSTNAME?.substring(0, 8) + '...' || 'unknown'
251
+ }
252
+ };
253
+ }
254
+ } catch (error) {
255
+ // Ignore fs errors
256
+ }
257
+
258
+ // Default fallback
259
+ return {
260
+ source: 'unknown',
261
+ details: {
262
+ user_agent: npmConfigUserAgent || 'none',
263
+ command: npmCommand || 'none',
264
+ lifecycle: npmLifecycleEvent || 'none'
265
+ }
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Send installation tracking to analytics
271
+ */
272
+ async function trackInstallation(installationData) {
273
+ if (!GA_MEASUREMENT_ID || !GA_API_SECRET) {
274
+ debug('Analytics not configured, skipping tracking');
275
+ return;
276
+ }
277
+
278
+ try {
279
+ const uniqueUserId = await getClientId();
280
+ log("user id", uniqueUserId)
281
+ // Prepare GA4 payload
282
+ const payload = {
283
+ client_id: uniqueUserId,
284
+ non_personalized_ads: false,
285
+ timestamp_micros: Date.now() * 1000,
286
+ events: [{
287
+ name: 'package_installed',
288
+ params: {
289
+ timestamp: new Date().toISOString(),
290
+ platform: platform(),
291
+ installation_source: installationData.source,
292
+ installation_details: JSON.stringify(installationData.details),
293
+ package_name: '@wonderwhy-er/desktop-commander',
294
+ install_method: 'npm-lifecycle',
295
+ node_version: process.version,
296
+ npm_version: process.env.npm_version || 'unknown'
297
+ }
298
+ }]
299
+ };
300
+
301
+ const postData = JSON.stringify(payload);
302
+
303
+ const options = {
304
+ method: 'POST',
305
+ headers: {
306
+ 'Content-Type': 'application/json',
307
+ 'Content-Length': Buffer.byteLength(postData)
308
+ }
309
+ };
310
+
311
+ await new Promise((resolve, reject) => {
312
+ const req = https.request(GA_BASE_URL, options);
313
+
314
+ const timeoutId = setTimeout(() => {
315
+ req.destroy();
316
+ reject(new Error('Request timeout'));
317
+ }, 5000);
318
+
319
+ req.on('error', (error) => {
320
+ clearTimeout(timeoutId);
321
+ debug(`Analytics error: ${error.message}`);
322
+ resolve(); // Don't fail installation on analytics error
323
+ });
324
+
325
+ req.on('response', (res) => {
326
+ clearTimeout(timeoutId);
327
+ // Consume the response data to complete the request
328
+ res.on('data', () => {}); // Ignore response data
329
+ res.on('end', () => {
330
+ log(`Installation tracked: ${installationData.source}`);
331
+ resolve();
332
+ });
333
+ });
334
+
335
+ req.write(postData);
336
+ req.end();
337
+ });
338
+
339
+ } catch (error) {
340
+ debug(`Failed to track installation: ${error.message}`);
341
+ // Don't fail the installation process
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Main execution
347
+ */
348
+ async function main() {
349
+ try {
350
+ log('Package installation detected');
351
+
352
+ const installationData = await detectInstallationSource();
353
+ log(`Installation source: ${installationData.source}`);
354
+
355
+ await trackInstallation(installationData);
356
+
357
+ } catch (error) {
358
+ debug(`Installation tracking error: ${error.message}`);
359
+ // Don't fail the installation
360
+ }
361
+ }
362
+
363
+ // Only run if this script is executed directly (not imported)
364
+ if (import.meta.url === `file://${process.argv[1]}`) {
365
+ main();
366
+ }
367
+
368
+ export { detectInstallationSource, trackInstallation };
@@ -123,16 +123,63 @@ export const captureBase = async (captureURL, event, properties) => {
123
123
  if (process.env.MCP_DXT) {
124
124
  isDXT = 'true';
125
125
  }
126
- // Is MCP running in a Docker container
127
- let isDocker = 'false';
128
- if (process.env.MCP_CLIENT_DOCKER) {
129
- isDocker = 'true';
126
+ // Is MCP running in a container - use robust detection
127
+ const { getSystemInfo } = await import('./system-info.js');
128
+ const systemInfo = getSystemInfo();
129
+ const isContainer = systemInfo.docker.isContainer ? 'true' : 'false';
130
+ const containerType = systemInfo.docker.containerType || 'none';
131
+ const orchestrator = systemInfo.docker.orchestrator || 'none';
132
+ // Add container metadata (with privacy considerations)
133
+ let containerName = 'none';
134
+ let containerImage = 'none';
135
+ if (systemInfo.docker.isContainer && systemInfo.docker.containerEnvironment) {
136
+ const env = systemInfo.docker.containerEnvironment;
137
+ // Container name - sanitize to remove potentially sensitive info
138
+ if (env.containerName) {
139
+ // Keep only alphanumeric chars, dashes, and underscores
140
+ // Remove random IDs and UUIDs for privacy
141
+ containerName = env.containerName
142
+ .replace(/[0-9a-f]{8,}/gi, 'ID') // Replace long hex strings with 'ID'
143
+ .replace(/[0-9]{8,}/g, 'ID') // Replace long numeric IDs with 'ID'
144
+ .substring(0, 50); // Limit length
145
+ }
146
+ // Docker image - sanitize registry info for privacy
147
+ if (env.dockerImage) {
148
+ // Remove registry URLs and keep just image:tag format
149
+ containerImage = env.dockerImage
150
+ .replace(/^[^/]+\/[^/]+\//, '') // Remove registry.com/namespace/ prefix
151
+ .replace(/^[^/]+\//, '') // Remove simple registry.com/ prefix
152
+ .replace(/@sha256:.*$/, '') // Remove digest hashes
153
+ .substring(0, 100); // Limit length
154
+ }
155
+ }
156
+ // Detect if we're running through Smithery at runtime
157
+ let runtimeSource = 'unknown';
158
+ const processArgs = process.argv.join(' ');
159
+ try {
160
+ if (processArgs.includes('@smithery/cli') || processArgs.includes('smithery')) {
161
+ runtimeSource = 'smithery-runtime';
162
+ }
163
+ else if (processArgs.includes('npx')) {
164
+ runtimeSource = 'npx-runtime';
165
+ }
166
+ else {
167
+ runtimeSource = 'direct-runtime';
168
+ }
169
+ }
170
+ catch (error) {
171
+ // Ignore detection errors
130
172
  }
131
173
  // Prepare standard properties
132
174
  const baseProperties = {
133
175
  timestamp: new Date().toISOString(),
134
176
  platform: platform(),
135
- isDocker,
177
+ isContainer,
178
+ containerType,
179
+ orchestrator,
180
+ containerName,
181
+ containerImage,
182
+ runtimeSource,
136
183
  isDXT,
137
184
  app_version: VERSION,
138
185
  engagement_time_msec: "100"
@@ -169,13 +216,14 @@ export const captureBase = async (captureURL, event, properties) => {
169
216
  data += chunk;
170
217
  });
171
218
  res.on('end', () => {
172
- if (res.statusCode !== 200 && res.statusCode !== 204) {
219
+ const success = res.statusCode === 200 || res.statusCode === 204;
220
+ if (!success) {
173
221
  // Optional debug logging
174
222
  // console.debug(`GA tracking error: ${res.statusCode} ${data}`);
175
223
  }
176
224
  });
177
225
  });
178
- req.on('error', () => {
226
+ req.on('error', (error) => {
179
227
  // Silently fail - we don't want analytics issues to break functionality
180
228
  });
181
229
  // Set timeout to prevent blocking the app
@@ -186,7 +234,7 @@ export const captureBase = async (captureURL, event, properties) => {
186
234
  req.write(postData);
187
235
  req.end();
188
236
  }
189
- catch {
237
+ catch (error) {
190
238
  // Silently fail - we don't want analytics issues to break functionality
191
239
  }
192
240
  };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Removes common leading whitespace from all lines in a template literal.
3
+ * This function helps clean up indented template literals used for tool descriptions.
4
+ *
5
+ * @param text - The template literal string with potential leading whitespace
6
+ * @returns The dedented string with leading whitespace removed
7
+ */
8
+ export declare function dedent(text: string): string;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Removes common leading whitespace from all lines in a template literal.
3
+ * This function helps clean up indented template literals used for tool descriptions.
4
+ *
5
+ * @param text - The template literal string with potential leading whitespace
6
+ * @returns The dedented string with leading whitespace removed
7
+ */
8
+ export function dedent(text) {
9
+ // Split into lines
10
+ const lines = text.split('\n');
11
+ // Remove first and last lines if they're empty
12
+ if (lines[0] === '')
13
+ lines.shift();
14
+ if (lines[lines.length - 1] === '')
15
+ lines.pop();
16
+ // If no lines remain, return empty string
17
+ if (lines.length === 0)
18
+ return '';
19
+ // Find the minimum indentation (excluding empty lines)
20
+ let minIndent = Infinity;
21
+ for (const line of lines) {
22
+ if (line.trim() === '')
23
+ continue; // Skip empty lines
24
+ const indent = line.match(/^(\s*)/)?.[1]?.length || 0;
25
+ minIndent = Math.min(minIndent, indent);
26
+ }
27
+ // If no indentation found, return as-is
28
+ if (minIndent === 0 || minIndent === Infinity) {
29
+ return lines.join('\n');
30
+ }
31
+ // Remove the common indentation from all lines
32
+ const dedentedLines = lines.map(line => {
33
+ if (line.trim() === '')
34
+ return ''; // Keep empty lines empty
35
+ return line.slice(minIndent);
36
+ });
37
+ return dedentedLines.join('\n');
38
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Centralized logging utility for Desktop Commander
3
+ * Ensures all logging goes through proper channels based on initialization state
4
+ */
5
+ import type { FilteredStdioServerTransport } from '../custom-stdio.js';
6
+ declare global {
7
+ var mcpTransport: FilteredStdioServerTransport | undefined;
8
+ }
9
+ export type LogLevel = 'emergency' | 'alert' | 'critical' | 'error' | 'warning' | 'notice' | 'info' | 'debug';
10
+ /**
11
+ * Log a message using the appropriate method based on MCP initialization state
12
+ */
13
+ export declare function log(level: LogLevel, message: string, data?: any): void;
14
+ /**
15
+ * Convenience functions for different log levels
16
+ */
17
+ export declare const logger: {
18
+ emergency: (message: string, data?: any) => void;
19
+ alert: (message: string, data?: any) => void;
20
+ critical: (message: string, data?: any) => void;
21
+ error: (message: string, data?: any) => void;
22
+ warning: (message: string, data?: any) => void;
23
+ notice: (message: string, data?: any) => void;
24
+ info: (message: string, data?: any) => void;
25
+ debug: (message: string, data?: any) => void;
26
+ };
27
+ /**
28
+ * Log to stderr during early initialization (before MCP is ready)
29
+ * Use this for critical startup messages that must be visible
30
+ * NOTE: This should also be JSON-RPC format
31
+ */
32
+ export declare function logToStderr(level: LogLevel, message: string): void;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Centralized logging utility for Desktop Commander
3
+ * Ensures all logging goes through proper channels based on initialization state
4
+ */
5
+ /**
6
+ * Log a message using the appropriate method based on MCP initialization state
7
+ */
8
+ export function log(level, message, data) {
9
+ try {
10
+ // Check if MCP transport is available
11
+ if (global.mcpTransport) {
12
+ // Always use MCP logging (will buffer if not initialized yet)
13
+ global.mcpTransport.sendLog(level, message, data);
14
+ }
15
+ else {
16
+ // This should rarely happen, but fallback to create a JSON-RPC notification manually
17
+ const notification = {
18
+ jsonrpc: "2.0",
19
+ method: "notifications/message",
20
+ params: {
21
+ level: level,
22
+ logger: "desktop-commander",
23
+ data: data ? { message, ...data } : message
24
+ }
25
+ };
26
+ process.stdout.write(JSON.stringify(notification) + '\n');
27
+ }
28
+ }
29
+ catch (error) {
30
+ // Ultimate fallback - but this should be JSON-RPC too
31
+ const notification = {
32
+ jsonrpc: "2.0",
33
+ method: "notifications/message",
34
+ params: {
35
+ level: "error",
36
+ logger: "desktop-commander",
37
+ data: `[LOG-ERROR] Failed to log message: ${message}`
38
+ }
39
+ };
40
+ process.stdout.write(JSON.stringify(notification) + '\n');
41
+ }
42
+ }
43
+ /**
44
+ * Convenience functions for different log levels
45
+ */
46
+ export const logger = {
47
+ emergency: (message, data) => log('emergency', message, data),
48
+ alert: (message, data) => log('alert', message, data),
49
+ critical: (message, data) => log('critical', message, data),
50
+ error: (message, data) => log('error', message, data),
51
+ warning: (message, data) => log('warning', message, data),
52
+ notice: (message, data) => log('notice', message, data),
53
+ info: (message, data) => log('info', message, data),
54
+ debug: (message, data) => log('debug', message, data),
55
+ };
56
+ /**
57
+ * Log to stderr during early initialization (before MCP is ready)
58
+ * Use this for critical startup messages that must be visible
59
+ * NOTE: This should also be JSON-RPC format
60
+ */
61
+ export function logToStderr(level, message) {
62
+ const notification = {
63
+ jsonrpc: "2.0",
64
+ method: "notifications/message",
65
+ params: {
66
+ level: level,
67
+ logger: "desktop-commander",
68
+ data: message
69
+ }
70
+ };
71
+ process.stdout.write(JSON.stringify(notification) + '\n');
72
+ }
@@ -5,13 +5,19 @@ export interface DockerMount {
5
5
  readOnly: boolean;
6
6
  description: string;
7
7
  }
8
- export interface DockerInfo {
8
+ export interface ContainerInfo {
9
+ isContainer: boolean;
10
+ containerType: 'docker' | 'podman' | 'kubernetes' | 'lxc' | 'systemd-nspawn' | 'other' | null;
11
+ orchestrator: 'kubernetes' | 'docker-compose' | 'docker-swarm' | 'podman-compose' | null;
9
12
  isDocker: boolean;
10
13
  mountPoints: DockerMount[];
11
14
  containerEnvironment?: {
12
15
  dockerImage?: string;
13
16
  containerName?: string;
14
17
  hostPlatform?: string;
18
+ kubernetesNamespace?: string;
19
+ kubernetesPod?: string;
20
+ kubernetesNode?: string;
15
21
  };
16
22
  }
17
23
  export interface SystemInfo {
@@ -22,7 +28,7 @@ export interface SystemInfo {
22
28
  isWindows: boolean;
23
29
  isMacOS: boolean;
24
30
  isLinux: boolean;
25
- docker: DockerInfo;
31
+ docker: ContainerInfo;
26
32
  examplePaths: {
27
33
  home: string;
28
34
  temp: string;