@wonderwhy-er/desktop-commander 0.2.2 → 0.2.4

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.
Files changed (51) hide show
  1. package/README.md +27 -4
  2. package/dist/config-manager.d.ts +10 -0
  3. package/dist/config-manager.js +7 -1
  4. package/dist/handlers/edit-search-handlers.js +25 -6
  5. package/dist/handlers/filesystem-handlers.js +2 -4
  6. package/dist/handlers/terminal-handlers.d.ts +8 -4
  7. package/dist/handlers/terminal-handlers.js +16 -10
  8. package/dist/index-dxt.d.ts +2 -0
  9. package/dist/index-dxt.js +76 -0
  10. package/dist/index-with-startup-detection.d.ts +5 -0
  11. package/dist/index-with-startup-detection.js +180 -0
  12. package/dist/server.d.ts +5 -0
  13. package/dist/server.js +381 -65
  14. package/dist/terminal-manager.d.ts +7 -0
  15. package/dist/terminal-manager.js +93 -18
  16. package/dist/tools/client.d.ts +10 -0
  17. package/dist/tools/client.js +13 -0
  18. package/dist/tools/config.d.ts +1 -1
  19. package/dist/tools/config.js +21 -3
  20. package/dist/tools/edit.js +4 -3
  21. package/dist/tools/environment.d.ts +55 -0
  22. package/dist/tools/environment.js +65 -0
  23. package/dist/tools/feedback.d.ts +8 -0
  24. package/dist/tools/feedback.js +132 -0
  25. package/dist/tools/filesystem.d.ts +10 -0
  26. package/dist/tools/filesystem.js +410 -60
  27. package/dist/tools/improved-process-tools.d.ts +24 -0
  28. package/dist/tools/improved-process-tools.js +453 -0
  29. package/dist/tools/schemas.d.ts +20 -2
  30. package/dist/tools/schemas.js +20 -3
  31. package/dist/tools/usage.d.ts +5 -0
  32. package/dist/tools/usage.js +24 -0
  33. package/dist/utils/capture.d.ts +2 -0
  34. package/dist/utils/capture.js +40 -9
  35. package/dist/utils/early-logger.d.ts +4 -0
  36. package/dist/utils/early-logger.js +35 -0
  37. package/dist/utils/mcp-logger.d.ts +30 -0
  38. package/dist/utils/mcp-logger.js +59 -0
  39. package/dist/utils/process-detection.d.ts +23 -0
  40. package/dist/utils/process-detection.js +150 -0
  41. package/dist/utils/smithery-detector.d.ts +94 -0
  42. package/dist/utils/smithery-detector.js +292 -0
  43. package/dist/utils/startup-detector.d.ts +65 -0
  44. package/dist/utils/startup-detector.js +390 -0
  45. package/dist/utils/system-info.d.ts +30 -0
  46. package/dist/utils/system-info.js +146 -0
  47. package/dist/utils/usageTracker.d.ts +85 -0
  48. package/dist/utils/usageTracker.js +280 -0
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +4 -1
@@ -2,11 +2,37 @@ import { spawn } from 'child_process';
2
2
  import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
3
3
  import { configManager } from './config-manager.js';
4
4
  import { capture } from "./utils/capture.js";
5
+ import { analyzeProcessState } from './utils/process-detection.js';
5
6
  export class TerminalManager {
6
7
  constructor() {
7
8
  this.sessions = new Map();
8
9
  this.completedSessions = new Map();
9
10
  }
11
+ /**
12
+ * Send input to a running process
13
+ * @param pid Process ID
14
+ * @param input Text to send to the process
15
+ * @returns Whether input was successfully sent
16
+ */
17
+ sendInputToProcess(pid, input) {
18
+ const session = this.sessions.get(pid);
19
+ if (!session) {
20
+ return false;
21
+ }
22
+ try {
23
+ if (session.process.stdin && !session.process.stdin.destroyed) {
24
+ // Ensure input ends with a newline for most REPLs
25
+ const inputWithNewline = input.endsWith('\n') ? input : input + '\n';
26
+ session.process.stdin.write(inputWithNewline);
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ catch (error) {
32
+ console.error(`Error sending input to process ${pid}:`, error);
33
+ return false;
34
+ }
35
+ }
10
36
  async executeCommand(command, timeoutMs = DEFAULT_COMMAND_TIMEOUT, shell) {
11
37
  // Get the shell from config if not specified
12
38
  let shellToUse = shell;
@@ -20,13 +46,26 @@ export class TerminalManager {
20
46
  shellToUse = true;
21
47
  }
22
48
  }
49
+ // For REPL interactions, we need to ensure stdin, stdout, and stderr are properly configured
50
+ // Note: No special stdio options needed here, Node.js handles pipes by default
51
+ // Enhance SSH commands automatically
52
+ let enhancedCommand = command;
53
+ if (command.trim().startsWith('ssh ') && !command.includes(' -t')) {
54
+ enhancedCommand = command.replace(/^ssh /, 'ssh -t ');
55
+ console.log(`Enhanced SSH command: ${enhancedCommand}`);
56
+ }
23
57
  const spawnOptions = {
24
- shell: shellToUse
58
+ shell: shellToUse,
59
+ env: {
60
+ ...process.env,
61
+ TERM: 'xterm-256color' // Better terminal compatibility
62
+ }
25
63
  };
26
- const process = spawn(command, [], spawnOptions);
64
+ // Spawn the process with an empty array of arguments and our options
65
+ const childProcess = spawn(enhancedCommand, [], spawnOptions);
27
66
  let output = '';
28
- // Ensure process.pid is defined before proceeding
29
- if (!process.pid) {
67
+ // Ensure childProcess.pid is defined before proceeding
68
+ if (!childProcess.pid) {
30
69
  // Return a consistent error object instead of throwing
31
70
  return {
32
71
  pid: -1, // Use -1 to indicate an error state
@@ -35,37 +74,73 @@ export class TerminalManager {
35
74
  };
36
75
  }
37
76
  const session = {
38
- pid: process.pid,
39
- process,
77
+ pid: childProcess.pid,
78
+ process: childProcess,
40
79
  lastOutput: '',
41
80
  isBlocked: false,
42
81
  startTime: new Date()
43
82
  };
44
- this.sessions.set(process.pid, session);
83
+ this.sessions.set(childProcess.pid, session);
45
84
  return new Promise((resolve) => {
46
- process.stdout.on('data', (data) => {
85
+ let resolved = false;
86
+ let periodicCheck = null;
87
+ // Quick prompt patterns for immediate detection
88
+ const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/;
89
+ const resolveOnce = (result) => {
90
+ if (resolved)
91
+ return;
92
+ resolved = true;
93
+ if (periodicCheck)
94
+ clearInterval(periodicCheck);
95
+ resolve(result);
96
+ };
97
+ childProcess.stdout.on('data', (data) => {
47
98
  const text = data.toString();
48
99
  output += text;
49
100
  session.lastOutput += text;
101
+ // Immediate check for obvious prompts
102
+ if (quickPromptPatterns.test(text)) {
103
+ session.isBlocked = true;
104
+ resolveOnce({
105
+ pid: childProcess.pid,
106
+ output,
107
+ isBlocked: true
108
+ });
109
+ }
50
110
  });
51
- process.stderr.on('data', (data) => {
111
+ childProcess.stderr.on('data', (data) => {
52
112
  const text = data.toString();
53
113
  output += text;
54
114
  session.lastOutput += text;
55
115
  });
116
+ // Periodic comprehensive check every 100ms
117
+ periodicCheck = setInterval(() => {
118
+ if (output.trim()) {
119
+ const processState = analyzeProcessState(output, childProcess.pid);
120
+ if (processState.isWaitingForInput) {
121
+ session.isBlocked = true;
122
+ resolveOnce({
123
+ pid: childProcess.pid,
124
+ output,
125
+ isBlocked: true
126
+ });
127
+ }
128
+ }
129
+ }, 100);
130
+ // Timeout fallback
56
131
  setTimeout(() => {
57
132
  session.isBlocked = true;
58
- resolve({
59
- pid: process.pid,
133
+ resolveOnce({
134
+ pid: childProcess.pid,
60
135
  output,
61
136
  isBlocked: true
62
137
  });
63
138
  }, timeoutMs);
64
- process.on('exit', (code) => {
65
- if (process.pid) {
139
+ childProcess.on('exit', (code) => {
140
+ if (childProcess.pid) {
66
141
  // Store completed session before removing active session
67
- this.completedSessions.set(process.pid, {
68
- pid: process.pid,
142
+ this.completedSessions.set(childProcess.pid, {
143
+ pid: childProcess.pid,
69
144
  output: output + session.lastOutput, // Combine all output
70
145
  exitCode: code,
71
146
  startTime: session.startTime,
@@ -76,10 +151,10 @@ export class TerminalManager {
76
151
  const oldestKey = Array.from(this.completedSessions.keys())[0];
77
152
  this.completedSessions.delete(oldestKey);
78
153
  }
79
- this.sessions.delete(process.pid);
154
+ this.sessions.delete(childProcess.pid);
80
155
  }
81
- resolve({
82
- pid: process.pid,
156
+ resolveOnce({
157
+ pid: childProcess.pid,
83
158
  output,
84
159
  isBlocked: false
85
160
  });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Simple tool to get current client information
3
+ */
4
+ export declare function getCurrentClient(): Promise<{
5
+ content: {
6
+ type: "text";
7
+ text: string;
8
+ }[];
9
+ isError: boolean;
10
+ }>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Simple tool to get current client information
3
+ */
4
+ import { currentClient } from '../server.js';
5
+ export async function getCurrentClient() {
6
+ return {
7
+ content: [{
8
+ type: "text",
9
+ text: `Current client: ${currentClient.name} v${currentClient.version}`
10
+ }],
11
+ isError: false,
12
+ };
13
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Get the entire config
2
+ * Get the entire config including system information
3
3
  */
4
4
  export declare function getConfig(): Promise<{
5
5
  content: {
@@ -1,17 +1,35 @@
1
1
  import { configManager } from '../config-manager.js';
2
2
  import { SetConfigValueArgsSchema } from './schemas.js';
3
+ import { getSystemInfo } from '../utils/system-info.js';
4
+ import { currentClient } from '../server.js';
3
5
  /**
4
- * Get the entire config
6
+ * Get the entire config including system information
5
7
  */
6
8
  export async function getConfig() {
7
9
  console.error('getConfig called');
8
10
  try {
9
11
  const config = await configManager.getConfig();
10
- console.error(`getConfig result: ${JSON.stringify(config, null, 2)}`);
12
+ // Add system information and current client to the config response
13
+ const systemInfo = getSystemInfo();
14
+ const configWithSystemInfo = {
15
+ ...config,
16
+ currentClient,
17
+ systemInfo: {
18
+ platform: systemInfo.platform,
19
+ platformName: systemInfo.platformName,
20
+ defaultShell: systemInfo.defaultShell,
21
+ pathSeparator: systemInfo.pathSeparator,
22
+ isWindows: systemInfo.isWindows,
23
+ isMacOS: systemInfo.isMacOS,
24
+ isLinux: systemInfo.isLinux,
25
+ examplePaths: systemInfo.examplePaths
26
+ }
27
+ };
28
+ console.error(`getConfig result: ${JSON.stringify(configWithSystemInfo, null, 2)}`);
11
29
  return {
12
30
  content: [{
13
31
  type: "text",
14
- text: `Current configuration:\n${JSON.stringify(config, null, 2)}`
32
+ text: `Current configuration:\n${JSON.stringify(configWithSystemInfo, null, 2)}`
15
33
  }],
16
34
  };
17
35
  }
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile } from './filesystem.js';
1
+ import { writeFile, readFileInternal, validatePath } from './filesystem.js';
2
2
  import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js';
3
3
  import { capture } from '../utils/capture.js';
4
4
  import { EditBlockArgsSchema } from "./schemas.js";
@@ -86,8 +86,9 @@ export async function performSearchReplace(filePath, block, expectedReplacements
86
86
  }],
87
87
  };
88
88
  }
89
- // Read file as plain string
90
- const { content } = await readFile(filePath, false, 0, Number.MAX_SAFE_INTEGER);
89
+ // Read file directly to preserve line endings - critical for edit operations
90
+ const validPath = await validatePath(filePath);
91
+ const content = await readFileInternal(validPath, 0, Number.MAX_SAFE_INTEGER);
91
92
  // Make sure content is a string
92
93
  if (typeof content !== 'string') {
93
94
  capture('server_edit_block_content_not_string', { fileExtension: fileExtension, expectedReplacements });
@@ -0,0 +1,55 @@
1
+ export declare function getEnvironmentInfo(): {
2
+ processInfo: {
3
+ pid: number;
4
+ ppid: number;
5
+ cwd: string;
6
+ nodeVersion: string;
7
+ versions: NodeJS.ProcessVersions;
8
+ argv: string[];
9
+ uptime: number;
10
+ memoryUsage: NodeJS.MemoryUsage;
11
+ execPath: string;
12
+ execArgv: string[];
13
+ platform: NodeJS.Platform;
14
+ arch: NodeJS.Architecture;
15
+ title: string;
16
+ };
17
+ systemEnvironment: {
18
+ platform: NodeJS.Platform;
19
+ arch: string;
20
+ hostname: string;
21
+ totalmem: number;
22
+ freemem: number;
23
+ cpus: import("os").CpuInfo[];
24
+ userInfo: import("os").UserInfo<string>;
25
+ networkInterfaces: NodeJS.Dict<import("os").NetworkInterfaceInfo[]>;
26
+ environmentVariables: NodeJS.ProcessEnv;
27
+ loadavg: number[];
28
+ uptime: number;
29
+ tmpdir: string;
30
+ homedir: string;
31
+ endianness: "BE" | "LE";
32
+ release: string;
33
+ type: string;
34
+ };
35
+ runtimeContext: {
36
+ timestamp: string;
37
+ timezone: string;
38
+ locale: string;
39
+ executionTime: number;
40
+ };
41
+ error?: undefined;
42
+ } | {
43
+ error: string;
44
+ processInfo: {
45
+ pid: number;
46
+ cwd: string;
47
+ };
48
+ systemEnvironment: {
49
+ platform: NodeJS.Platform;
50
+ };
51
+ runtimeContext: {
52
+ timestamp: string;
53
+ executionTime: number;
54
+ };
55
+ };
@@ -0,0 +1,65 @@
1
+ import { platform, arch, totalmem, freemem, cpus, userInfo, networkInterfaces, hostname, loadavg, uptime as osUptime, tmpdir, homedir, endianness, release, type } from 'os';
2
+ import { cwd, env, argv, pid, ppid, version, versions, memoryUsage, uptime } from 'process';
3
+ export function getEnvironmentInfo() {
4
+ const startTime = Date.now();
5
+ try {
6
+ // Process Information
7
+ const processInfo = {
8
+ pid: pid,
9
+ ppid: ppid,
10
+ cwd: cwd(),
11
+ nodeVersion: version,
12
+ versions: versions,
13
+ argv: argv,
14
+ uptime: uptime(),
15
+ memoryUsage: memoryUsage(),
16
+ execPath: process.execPath,
17
+ execArgv: process.execArgv,
18
+ platform: process.platform,
19
+ arch: process.arch,
20
+ title: process.title
21
+ };
22
+ // System Environment
23
+ const systemEnvironment = {
24
+ platform: platform(),
25
+ arch: arch(),
26
+ hostname: hostname(),
27
+ totalmem: totalmem(),
28
+ freemem: freemem(),
29
+ cpus: cpus(),
30
+ userInfo: userInfo(),
31
+ networkInterfaces: networkInterfaces(),
32
+ environmentVariables: env,
33
+ loadavg: loadavg(),
34
+ uptime: osUptime(),
35
+ tmpdir: tmpdir(),
36
+ homedir: homedir(),
37
+ endianness: endianness(),
38
+ release: release(),
39
+ type: type()
40
+ };
41
+ // Runtime Context
42
+ const runtimeContext = {
43
+ timestamp: new Date().toISOString(),
44
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
45
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
46
+ executionTime: Date.now() - startTime
47
+ };
48
+ return {
49
+ processInfo,
50
+ systemEnvironment,
51
+ runtimeContext
52
+ };
53
+ }
54
+ catch (error) {
55
+ return {
56
+ error: error instanceof Error ? error.message : String(error),
57
+ processInfo: { pid, cwd: cwd() },
58
+ systemEnvironment: { platform: platform() },
59
+ runtimeContext: {
60
+ timestamp: new Date().toISOString(),
61
+ executionTime: Date.now() - startTime
62
+ }
63
+ };
64
+ }
65
+ }
@@ -0,0 +1,8 @@
1
+ import { ServerResult } from '../types.js';
2
+ interface FeedbackParams {
3
+ }
4
+ /**
5
+ * Open feedback form in browser with optional pre-filled data
6
+ */
7
+ export declare function giveFeedbackToDesktopCommander(params?: FeedbackParams): Promise<ServerResult>;
8
+ export {};
@@ -0,0 +1,132 @@
1
+ import { usageTracker } from '../utils/usageTracker.js';
2
+ import { capture } from '../utils/capture.js';
3
+ import { configManager } from '../config-manager.js';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import * as os from 'os';
7
+ const execAsync = promisify(exec);
8
+ /**
9
+ * Open feedback form in browser with optional pre-filled data
10
+ */
11
+ export async function giveFeedbackToDesktopCommander(params = {}) {
12
+ try {
13
+ // Get usage stats for context
14
+ const stats = await usageTracker.getStats();
15
+ // Capture feedback tool usage event
16
+ await capture('feedback_tool_called', {
17
+ total_calls: stats.totalToolCalls,
18
+ successful_calls: stats.successfulCalls,
19
+ failed_calls: stats.failedCalls,
20
+ days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
21
+ total_sessions: stats.totalSessions,
22
+ platform: os.platform(),
23
+ });
24
+ // Build Tally.so URL with pre-filled parameters
25
+ const tallyUrl = await buildTallyUrl(params, stats);
26
+ // Open URL in default browser
27
+ const success = await openUrlInBrowser(tallyUrl);
28
+ if (success) {
29
+ // Capture successful browser opening
30
+ await capture('feedback_form_opened_successfully', {
31
+ total_calls: stats.totalToolCalls,
32
+ platform: os.platform()
33
+ });
34
+ // Mark that user has given feedback (or at least opened the form)
35
+ await usageTracker.markFeedbackGiven();
36
+ return {
37
+ content: [{
38
+ type: "text",
39
+ text: `🎉 **Feedback form opened in your browser!**\n\n` +
40
+ `Thank you for taking the time to share your experience with Desktop Commander. ` +
41
+ `Your feedback helps us build better features and improve the tool for everyone.\n\n` +
42
+ `The form has been pre-filled with the information you provided. ` +
43
+ `You can modify or add any additional details before submitting.\n\n` +
44
+ `**Form URL**: ${tallyUrl.length > 100 ? tallyUrl.substring(0, 100) + '...' : tallyUrl}`
45
+ }]
46
+ };
47
+ }
48
+ else {
49
+ // Capture browser opening failure
50
+ await capture('feedback_form_open_failed', {
51
+ total_calls: stats.totalToolCalls,
52
+ platform: os.platform(),
53
+ error_type: 'browser_open_failed'
54
+ });
55
+ return {
56
+ content: [{
57
+ type: "text",
58
+ text: `⚠️ **Couldn't open browser automatically**\n\n` +
59
+ `Please copy and paste this URL into your browser to access the feedback form:\n\n` +
60
+ `${tallyUrl}\n\n` +
61
+ `The form has been pre-filled with your information. Thank you for your feedback!`
62
+ }]
63
+ };
64
+ }
65
+ }
66
+ catch (error) {
67
+ // Capture error event
68
+ await capture('feedback_tool_error', {
69
+ error_message: error instanceof Error ? error.message : String(error),
70
+ error_type: error instanceof Error ? error.constructor.name : 'unknown'
71
+ });
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: `❌ **Error opening feedback form**: ${error instanceof Error ? error.message : String(error)}\n\n` +
76
+ `You can still access our feedback form directly at: https://tally.so/r/mYB6av\n\n` +
77
+ `We appreciate your willingness to provide feedback!`
78
+ }],
79
+ isError: true
80
+ };
81
+ }
82
+ }
83
+ /**
84
+ * Build Tally.so URL with pre-filled parameters
85
+ */
86
+ async function buildTallyUrl(params, stats) {
87
+ const baseUrl = 'https://tally.so/r/mYB6av';
88
+ const urlParams = new URLSearchParams();
89
+ // Only auto-filled hidden fields remain
90
+ urlParams.set('tool_call_count', stats.totalToolCalls.toString());
91
+ // Calculate days using
92
+ const daysUsing = Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24));
93
+ urlParams.set('days_using', daysUsing.toString());
94
+ // Add platform info
95
+ urlParams.set('platform', os.platform());
96
+ // Add client_id from analytics config
97
+ try {
98
+ const clientId = await configManager.getValue('clientId') || 'unknown';
99
+ urlParams.set('client_id', clientId);
100
+ }
101
+ catch (error) {
102
+ // Fallback if config read fails
103
+ urlParams.set('client_id', 'unknown');
104
+ }
105
+ return `${baseUrl}?${urlParams.toString()}`;
106
+ }
107
+ /**
108
+ * Open URL in default browser (cross-platform)
109
+ */
110
+ async function openUrlInBrowser(url) {
111
+ try {
112
+ const platform = os.platform();
113
+ let command;
114
+ switch (platform) {
115
+ case 'darwin': // macOS
116
+ command = `open "${url}"`;
117
+ break;
118
+ case 'win32': // Windows
119
+ command = `start "" "${url}"`;
120
+ break;
121
+ default: // Linux and others
122
+ command = `xdg-open "${url}"`;
123
+ break;
124
+ }
125
+ await execAsync(command);
126
+ return true;
127
+ }
128
+ catch (error) {
129
+ console.error('Failed to open browser:', error);
130
+ return false;
131
+ }
132
+ }
@@ -36,6 +36,16 @@ export declare function readFileFromDisk(filePath: string, offset?: number, leng
36
36
  * @returns File content or file result with metadata
37
37
  */
38
38
  export declare function readFile(filePath: string, isUrl?: boolean, offset?: number, length?: number): Promise<FileResult>;
39
+ /**
40
+ * Read file content without status messages for internal operations
41
+ * This function preserves exact file content including original line endings,
42
+ * which is essential for edit operations that need to maintain file formatting.
43
+ * @param filePath Path to the file
44
+ * @param offset Starting line number to read from (default: 0)
45
+ * @param length Maximum number of lines to read (default: from config or 1000)
46
+ * @returns File content without status headers, with preserved line endings
47
+ */
48
+ export declare function readFileInternal(filePath: string, offset?: number, length?: number): Promise<string>;
39
49
  export declare function writeFile(filePath: string, content: string, mode?: 'rewrite' | 'append'): Promise<void>;
40
50
  export interface MultiFileResult {
41
51
  path: string;