@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,219 @@
1
+ import { searchManager } from '../search-manager.js';
2
+ import { StartSearchArgsSchema, GetMoreSearchResultsArgsSchema, StopSearchArgsSchema } from '../tools/schemas.js';
3
+ import { capture } from '../utils/capture.js';
4
+ /**
5
+ * Handle start_search command
6
+ */
7
+ export async function handleStartSearch(args) {
8
+ const parsed = StartSearchArgsSchema.safeParse(args);
9
+ if (!parsed.success) {
10
+ return {
11
+ content: [{ type: "text", text: `Invalid arguments for start_search: ${parsed.error}` }],
12
+ isError: true,
13
+ };
14
+ }
15
+ try {
16
+ const result = await searchManager.startSearch({
17
+ rootPath: parsed.data.path,
18
+ pattern: parsed.data.pattern,
19
+ searchType: parsed.data.searchType,
20
+ filePattern: parsed.data.filePattern,
21
+ ignoreCase: parsed.data.ignoreCase,
22
+ maxResults: parsed.data.maxResults,
23
+ includeHidden: parsed.data.includeHidden,
24
+ contextLines: parsed.data.contextLines,
25
+ timeout: parsed.data.timeout_ms,
26
+ earlyTermination: parsed.data.earlyTermination,
27
+ });
28
+ const searchTypeText = parsed.data.searchType === 'content' ? 'content search' : 'file search';
29
+ let output = `Started ${searchTypeText} session: ${result.sessionId}\n`;
30
+ output += `Pattern: "${parsed.data.pattern}"\n`;
31
+ output += `Path: ${parsed.data.path}\n`;
32
+ output += `Status: ${result.isComplete ? 'COMPLETED' : 'RUNNING'}\n`;
33
+ output += `Runtime: ${Math.round(result.runtime)}ms\n`;
34
+ output += `Total results: ${result.totalResults}\n\n`;
35
+ if (result.results.length > 0) {
36
+ output += "Initial results:\n";
37
+ for (const searchResult of result.results.slice(0, 10)) {
38
+ if (searchResult.type === 'content') {
39
+ output += `šŸ“„ ${searchResult.file}:${searchResult.line} - ${searchResult.match?.substring(0, 100)}${searchResult.match && searchResult.match.length > 100 ? '...' : ''}\n`;
40
+ }
41
+ else {
42
+ output += `šŸ“ ${searchResult.file}\n`;
43
+ }
44
+ }
45
+ if (result.results.length > 10) {
46
+ output += `... and ${result.results.length - 10} more results\n`;
47
+ }
48
+ }
49
+ if (result.isComplete) {
50
+ output += `\nāœ… Search completed.`;
51
+ }
52
+ else {
53
+ output += `\nšŸ”„ Search in progress. Use get_more_search_results to get more results.`;
54
+ }
55
+ return {
56
+ content: [{ type: "text", text: output }],
57
+ };
58
+ }
59
+ catch (error) {
60
+ const errorMessage = error instanceof Error ? error.message : String(error);
61
+ capture('search_session_start_error', { error: errorMessage });
62
+ return {
63
+ content: [{ type: "text", text: `Error starting search session: ${errorMessage}` }],
64
+ isError: true,
65
+ };
66
+ }
67
+ }
68
+ /**
69
+ * Handle get_more_search_results command
70
+ */
71
+ export async function handleGetMoreSearchResults(args) {
72
+ const parsed = GetMoreSearchResultsArgsSchema.safeParse(args);
73
+ if (!parsed.success) {
74
+ return {
75
+ content: [{ type: "text", text: `Invalid arguments for get_more_search_results: ${parsed.error}` }],
76
+ isError: true,
77
+ };
78
+ }
79
+ try {
80
+ const results = searchManager.readSearchResults(parsed.data.sessionId, parsed.data.offset, parsed.data.length);
81
+ // Only return error if we have no results AND there's an actual error
82
+ // Permission errors should not block returning found results
83
+ if (results.isError && results.totalResults === 0 && results.error?.trim()) {
84
+ return {
85
+ content: [{
86
+ type: "text",
87
+ text: `Search session ${parsed.data.sessionId} encountered an error: ${results.error}`
88
+ }],
89
+ isError: true,
90
+ };
91
+ }
92
+ // Format results for display
93
+ let output = `Search session: ${parsed.data.sessionId}\n`;
94
+ output += `Status: ${results.isComplete ? 'COMPLETED' : 'IN PROGRESS'}\n`;
95
+ output += `Runtime: ${Math.round(results.runtime / 1000)}s\n`;
96
+ output += `Total results found: ${results.totalResults} (${results.totalMatches} matches)\n`;
97
+ const offset = parsed.data.offset;
98
+ if (offset < 0) {
99
+ // Negative offset - tail behavior
100
+ output += `Showing last ${results.returnedCount} results\n\n`;
101
+ }
102
+ else {
103
+ // Positive offset - range behavior
104
+ const startPos = offset;
105
+ const endPos = startPos + results.returnedCount - 1;
106
+ output += `Showing results ${startPos}-${endPos}\n\n`;
107
+ }
108
+ if (results.results.length === 0) {
109
+ if (results.isComplete) {
110
+ output += results.totalResults === 0 ? "No matches found." : "No results in this range.";
111
+ }
112
+ else {
113
+ output += "No results yet, search is still running...";
114
+ }
115
+ }
116
+ else {
117
+ output += "Results:\n";
118
+ for (const result of results.results) {
119
+ if (result.type === 'content') {
120
+ output += `šŸ“„ ${result.file}:${result.line} - ${result.match?.substring(0, 100)}${result.match && result.match.length > 100 ? '...' : ''}\n`;
121
+ }
122
+ else {
123
+ output += `šŸ“ ${result.file}\n`;
124
+ }
125
+ }
126
+ }
127
+ // Add pagination hints
128
+ if (offset >= 0 && results.hasMoreResults) {
129
+ const nextOffset = offset + results.returnedCount;
130
+ output += `\nšŸ“– More results available. Use get_more_search_results with offset: ${nextOffset}`;
131
+ }
132
+ if (results.isComplete) {
133
+ output += `\nāœ… Search completed.`;
134
+ }
135
+ return {
136
+ content: [{ type: "text", text: output }],
137
+ };
138
+ }
139
+ catch (error) {
140
+ const errorMessage = error instanceof Error ? error.message : String(error);
141
+ return {
142
+ content: [{ type: "text", text: `Error reading search results: ${errorMessage}` }],
143
+ isError: true,
144
+ };
145
+ }
146
+ }
147
+ /**
148
+ * Handle stop_search command
149
+ */
150
+ export async function handleStopSearch(args) {
151
+ const parsed = StopSearchArgsSchema.safeParse(args);
152
+ if (!parsed.success) {
153
+ return {
154
+ content: [{ type: "text", text: `Invalid arguments for stop_search: ${parsed.error}` }],
155
+ isError: true,
156
+ };
157
+ }
158
+ try {
159
+ const success = searchManager.terminateSearch(parsed.data.sessionId);
160
+ if (success) {
161
+ return {
162
+ content: [{
163
+ type: "text",
164
+ text: `Search session ${parsed.data.sessionId} terminated successfully.`
165
+ }],
166
+ };
167
+ }
168
+ else {
169
+ return {
170
+ content: [{
171
+ type: "text",
172
+ text: `Search session ${parsed.data.sessionId} not found or already completed.`
173
+ }],
174
+ };
175
+ }
176
+ }
177
+ catch (error) {
178
+ const errorMessage = error instanceof Error ? error.message : String(error);
179
+ return {
180
+ content: [{ type: "text", text: `Error terminating search session: ${errorMessage}` }],
181
+ isError: true,
182
+ };
183
+ }
184
+ }
185
+ /**
186
+ * Handle list_searches command
187
+ */
188
+ export async function handleListSearches() {
189
+ try {
190
+ const sessions = searchManager.listSearchSessions();
191
+ if (sessions.length === 0) {
192
+ return {
193
+ content: [{ type: "text", text: "No active searches." }],
194
+ };
195
+ }
196
+ let output = `Active Searches (${sessions.length}):\n\n`;
197
+ for (const session of sessions) {
198
+ const status = session.isComplete
199
+ ? (session.isError ? 'āŒ ERROR' : 'āœ… COMPLETED')
200
+ : 'šŸ”„ RUNNING';
201
+ output += `Session: ${session.id}\n`;
202
+ output += ` Type: ${session.searchType}\n`;
203
+ output += ` Pattern: "${session.pattern}"\n`;
204
+ output += ` Status: ${status}\n`;
205
+ output += ` Runtime: ${Math.round(session.runtime / 1000)}s\n`;
206
+ output += ` Results: ${session.totalResults}\n\n`;
207
+ }
208
+ return {
209
+ content: [{ type: "text", text: output }],
210
+ };
211
+ }
212
+ catch (error) {
213
+ const errorMessage = error instanceof Error ? error.message : String(error);
214
+ return {
215
+ content: [{ type: "text", text: `Error listing search sessions: ${errorMessage}` }],
216
+ isError: true,
217
+ };
218
+ }
219
+ }
package/dist/index.js CHANGED
@@ -2,47 +2,10 @@
2
2
  import { FilteredStdioServerTransport } from './custom-stdio.js';
3
3
  import { server } from './server.js';
4
4
  import { configManager } from './config-manager.js';
5
- import { join, dirname } from 'path';
6
- import { fileURLToPath, pathToFileURL } from 'url';
7
- import { platform } from 'os';
5
+ import { runSetup } from './npm-scripts/setup.js';
6
+ import { runUninstall } from './npm-scripts/uninstall.js';
8
7
  import { capture } from './utils/capture.js';
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
- const isWindows = platform() === 'win32';
12
- // Helper function to properly convert file paths to URLs, especially for Windows
13
- function createFileURL(filePath) {
14
- if (isWindows) {
15
- // Ensure path uses forward slashes for URL format
16
- const normalizedPath = filePath.replace(/\\/g, '/');
17
- // Ensure path has proper file:// prefix
18
- if (normalizedPath.startsWith('/')) {
19
- return new URL(`file://${normalizedPath}`);
20
- }
21
- else {
22
- return new URL(`file:///${normalizedPath}`);
23
- }
24
- }
25
- else {
26
- // For non-Windows, we can use the built-in function
27
- return pathToFileURL(filePath);
28
- }
29
- }
30
- async function runSetup() {
31
- try {
32
- // Fix for Windows ESM path issue
33
- const setupScriptPath = join(__dirname, 'setup-claude-server.js');
34
- const setupScriptUrl = createFileURL(setupScriptPath);
35
- // Now import using the URL format
36
- const { default: setupModule } = await import(setupScriptUrl.href);
37
- if (typeof setupModule === 'function') {
38
- await setupModule();
39
- }
40
- }
41
- catch (error) {
42
- console.error('Error running setup:', error);
43
- process.exit(1);
44
- }
45
- }
8
+ import { logToStderr, logger } from './utils/logger.js';
46
9
  async function runServer() {
47
10
  try {
48
11
  // Check if first argument is "setup"
@@ -50,6 +13,24 @@ async function runServer() {
50
13
  await runSetup();
51
14
  return;
52
15
  }
16
+ // Check if first argument is "remove"
17
+ if (process.argv[2] === 'remove') {
18
+ await runUninstall();
19
+ return;
20
+ }
21
+ try {
22
+ logToStderr('info', 'Loading configuration...');
23
+ await configManager.loadConfig();
24
+ logToStderr('info', 'Configuration loaded successfully');
25
+ }
26
+ catch (configError) {
27
+ logToStderr('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
28
+ if (configError instanceof Error && configError.stack) {
29
+ logToStderr('debug', `Stack trace: ${configError.stack}`);
30
+ }
31
+ logToStderr('warning', 'Continuing with in-memory configuration only');
32
+ // Continue anyway - we'll use an in-memory config
33
+ }
53
34
  const transport = new FilteredStdioServerTransport();
54
35
  // Export transport for use throughout the application
55
36
  global.mcpTransport = transport;
@@ -58,13 +39,13 @@ async function runServer() {
58
39
  const errorMessage = error instanceof Error ? error.message : String(error);
59
40
  // If this is a JSON parsing error, log it to stderr but don't crash
60
41
  if (errorMessage.includes('JSON') && errorMessage.includes('Unexpected token')) {
61
- process.stderr.write(`[desktop-commander] JSON parsing error: ${errorMessage}\n`);
42
+ logger.error(`JSON parsing error: ${errorMessage}`);
62
43
  return; // Don't exit on JSON parsing errors
63
44
  }
64
45
  capture('run_server_uncaught_exception', {
65
46
  error: errorMessage
66
47
  });
67
- process.stderr.write(`[desktop-commander] Uncaught exception: ${errorMessage}\n`);
48
+ logger.error(`Uncaught exception: ${errorMessage}`);
68
49
  process.exit(1);
69
50
  });
70
51
  // Handle unhandled rejections
@@ -72,40 +53,45 @@ async function runServer() {
72
53
  const errorMessage = reason instanceof Error ? reason.message : String(reason);
73
54
  // If this is a JSON parsing error, log it to stderr but don't crash
74
55
  if (errorMessage.includes('JSON') && errorMessage.includes('Unexpected token')) {
75
- process.stderr.write(`[desktop-commander] JSON parsing rejection: ${errorMessage}\n`);
56
+ logger.error(`JSON parsing rejection: ${errorMessage}`);
76
57
  return; // Don't exit on JSON parsing errors
77
58
  }
78
59
  capture('run_server_unhandled_rejection', {
79
60
  error: errorMessage
80
61
  });
81
- process.stderr.write(`[desktop-commander] Unhandled rejection: ${errorMessage}\n`);
62
+ logger.error(`Unhandled rejection: ${errorMessage}`);
82
63
  process.exit(1);
83
64
  });
84
65
  capture('run_server_start');
85
- try {
86
- console.error("Loading configuration...");
87
- await configManager.loadConfig();
88
- console.error("Configuration loaded successfully");
89
- }
90
- catch (configError) {
91
- console.error(`Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
92
- console.error(configError instanceof Error && configError.stack ? configError.stack : 'No stack trace available');
93
- console.error("Continuing with in-memory configuration only");
94
- // Continue anyway - we'll use an in-memory config
95
- }
96
- console.error("Connecting server...");
66
+ logToStderr('info', 'Connecting server...');
67
+ // Set up event-driven initialization completion handler
68
+ server.oninitialized = () => {
69
+ // This callback is triggered after the client sends the "initialized" notification
70
+ // At this point, the MCP protocol handshake is fully complete
71
+ transport.enableNotifications();
72
+ // Use the transport to send a proper JSON-RPC notification
73
+ transport.sendLog('info', 'MCP fully initialized, notifications enabled');
74
+ };
97
75
  await server.connect(transport);
98
- console.error("Server connected successfully");
76
+ logToStderr('info', 'Server connected successfully');
99
77
  }
100
78
  catch (error) {
101
79
  const errorMessage = error instanceof Error ? error.message : String(error);
102
- console.error(`FATAL ERROR: ${errorMessage}`);
103
- console.error(error instanceof Error && error.stack ? error.stack : 'No stack trace available');
104
- process.stderr.write(JSON.stringify({
105
- type: 'error',
106
- timestamp: new Date().toISOString(),
107
- message: `Failed to start server: ${errorMessage}`
108
- }) + '\n');
80
+ logger.error(`FATAL ERROR: ${errorMessage}`);
81
+ if (error instanceof Error && error.stack) {
82
+ logger.debug(error.stack);
83
+ }
84
+ // Send a structured error notification
85
+ const errorNotification = {
86
+ jsonrpc: "2.0",
87
+ method: "notifications/message",
88
+ params: {
89
+ level: "error",
90
+ logger: "desktop-commander",
91
+ data: `Failed to start server: ${errorMessage} (${new Date().toISOString()})`
92
+ }
93
+ };
94
+ process.stdout.write(JSON.stringify(errorNotification) + '\n');
109
95
  capture('run_server_failed_start_error', {
110
96
  error: errorMessage
111
97
  });
@@ -0,0 +1 @@
1
+ export declare function runSetup(): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { join, dirname } from 'path';
2
+ import { fileURLToPath, pathToFileURL } from 'url';
3
+ import { platform } from 'os';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = dirname(__filename);
6
+ const isWindows = platform() === 'win32';
7
+ // Helper function to properly convert file paths to URLs, especially for Windows
8
+ function createFileURL(filePath) {
9
+ if (isWindows) {
10
+ // Ensure path uses forward slashes for URL format
11
+ const normalizedPath = filePath.replace(/\\/g, '/');
12
+ // Ensure path has proper file:// prefix
13
+ if (normalizedPath.startsWith('/')) {
14
+ return new URL(`file://${normalizedPath}`);
15
+ }
16
+ else {
17
+ return new URL(`file:///${normalizedPath}`);
18
+ }
19
+ }
20
+ else {
21
+ // For non-Windows, we can use the built-in function
22
+ return pathToFileURL(filePath);
23
+ }
24
+ }
25
+ export async function runSetup() {
26
+ try {
27
+ // Fix for Windows ESM path issue - go up one level from npm-scripts to main dist
28
+ const setupScriptPath = join(__dirname, '..', 'setup-claude-server.js');
29
+ const setupScriptUrl = createFileURL(setupScriptPath);
30
+ // Now import using the URL format
31
+ const { default: setupModule } = await import(setupScriptUrl.href);
32
+ if (typeof setupModule === 'function') {
33
+ await setupModule();
34
+ }
35
+ }
36
+ catch (error) {
37
+ console.error('Error running setup:', error);
38
+ process.exit(1);
39
+ }
40
+ }
@@ -0,0 +1 @@
1
+ export declare function runUninstall(): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { join, dirname } from 'path';
2
+ import { fileURLToPath, pathToFileURL } from 'url';
3
+ import { platform } from 'os';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = dirname(__filename);
6
+ const isWindows = platform() === 'win32';
7
+ // Helper function to properly convert file paths to URLs, especially for Windows
8
+ function createFileURL(filePath) {
9
+ if (isWindows) {
10
+ // Ensure path uses forward slashes for URL format
11
+ const normalizedPath = filePath.replace(/\\/g, '/');
12
+ // Ensure path has proper file:// prefix
13
+ if (normalizedPath.startsWith('/')) {
14
+ return new URL(`file://${normalizedPath}`);
15
+ }
16
+ else {
17
+ return new URL(`file:///${normalizedPath}`);
18
+ }
19
+ }
20
+ else {
21
+ // For non-Windows, we can use the built-in function
22
+ return pathToFileURL(filePath);
23
+ }
24
+ }
25
+ export async function runUninstall() {
26
+ try {
27
+ // Fix for Windows ESM path issue - go up one level from npm-scripts to main dist
28
+ const uninstallScriptPath = join(__dirname, '..', 'uninstall-claude-server.js');
29
+ const uninstallScriptUrl = createFileURL(uninstallScriptPath);
30
+ // Now import using the URL format
31
+ const { default: uninstallModule } = await import(uninstallScriptUrl.href);
32
+ if (typeof uninstallModule === 'function') {
33
+ await uninstallModule();
34
+ }
35
+ }
36
+ catch (error) {
37
+ console.error('Error running uninstall:', error);
38
+ process.exit(1);
39
+ }
40
+ }
@@ -0,0 +1,107 @@
1
+ import { ChildProcess } from 'child_process';
2
+ export interface SearchResult {
3
+ file: string;
4
+ line?: number;
5
+ match?: string;
6
+ type: 'file' | 'content';
7
+ }
8
+ export interface SearchSession {
9
+ id: string;
10
+ process: ChildProcess;
11
+ results: SearchResult[];
12
+ isComplete: boolean;
13
+ isError: boolean;
14
+ error?: string;
15
+ startTime: number;
16
+ lastReadTime: number;
17
+ options: SearchSessionOptions;
18
+ buffer: string;
19
+ totalMatches: number;
20
+ totalContextLines: number;
21
+ }
22
+ export interface SearchSessionOptions {
23
+ rootPath: string;
24
+ pattern: string;
25
+ searchType: 'files' | 'content';
26
+ filePattern?: string;
27
+ ignoreCase?: boolean;
28
+ maxResults?: number;
29
+ includeHidden?: boolean;
30
+ contextLines?: number;
31
+ timeout?: number;
32
+ earlyTermination?: boolean;
33
+ }
34
+ /**
35
+ * Search Session Manager - handles ripgrep processes like terminal sessions
36
+ * Supports both file search and content search with progressive results
37
+ */ export declare class SearchManager {
38
+ private sessions;
39
+ private sessionCounter;
40
+ /**
41
+ * Start a new search session (like start_process)
42
+ * Returns immediately with initial state and results
43
+ */
44
+ startSearch(options: SearchSessionOptions): Promise<{
45
+ sessionId: string;
46
+ isComplete: boolean;
47
+ isError: boolean;
48
+ results: SearchResult[];
49
+ totalResults: number;
50
+ runtime: number;
51
+ }>;
52
+ /**
53
+ * Read search results with offset-based pagination (like read_file)
54
+ * Supports both range reading and tail behavior
55
+ */
56
+ readSearchResults(sessionId: string, offset?: number, length?: number): {
57
+ results: SearchResult[];
58
+ returnedCount: number;
59
+ totalResults: number;
60
+ totalMatches: number;
61
+ isComplete: boolean;
62
+ isError: boolean;
63
+ error?: string;
64
+ hasMoreResults: boolean;
65
+ runtime: number;
66
+ };
67
+ /**
68
+ * Terminate a search session (like force_terminate)
69
+ */
70
+ terminateSearch(sessionId: string): boolean;
71
+ /**
72
+ * Get list of active search sessions (like list_sessions)
73
+ */
74
+ listSearchSessions(): Array<{
75
+ id: string;
76
+ searchType: string;
77
+ pattern: string;
78
+ isComplete: boolean;
79
+ isError: boolean;
80
+ runtime: number;
81
+ totalResults: number;
82
+ }>;
83
+ /**
84
+ * Clean up completed sessions older than specified time
85
+ * Called automatically by cleanup interval
86
+ */
87
+ cleanupSessions(maxAge?: number): void;
88
+ /**
89
+ * Get total number of active sessions (excluding completed ones)
90
+ */
91
+ getActiveSessionCount(): number;
92
+ /**
93
+ * Detect if pattern looks like an exact filename
94
+ * (has file extension and no glob wildcards)
95
+ */
96
+ private isExactFilename;
97
+ /**
98
+ * Detect if pattern contains glob wildcards
99
+ */
100
+ private isGlobPattern;
101
+ private buildRipgrepArgs;
102
+ private setupProcessHandlers;
103
+ private processBufferedOutput;
104
+ private parseLine;
105
+ }
106
+ export declare const searchManager: SearchManager;
107
+ export declare function stopSearchManagerCleanup(): void;