@wonderwhy-er/desktop-commander 0.1.38 → 0.2.0

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 (50) hide show
  1. package/README.md +89 -6
  2. package/dist/REPLSessionManager.d.ts +109 -0
  3. package/dist/REPLSessionManager.js +364 -0
  4. package/dist/REPLSessionManager.test.d.ts +1 -0
  5. package/dist/REPLSessionManager.test.js +75 -0
  6. package/dist/client/replClient.d.ts +63 -0
  7. package/dist/client/replClient.js +217 -0
  8. package/dist/client/sshClient.d.ts +82 -0
  9. package/dist/client/sshClient.js +200 -0
  10. package/dist/config-manager.d.ts +2 -0
  11. package/dist/config-manager.js +3 -1
  12. package/dist/config.d.ts +1 -0
  13. package/dist/config.js +1 -0
  14. package/dist/handlers/filesystem-handlers.js +37 -3
  15. package/dist/handlers/fuzzy-search-log-handlers.d.ts +13 -0
  16. package/dist/handlers/fuzzy-search-log-handlers.js +179 -0
  17. package/dist/handlers/repl-handlers.d.ts +21 -0
  18. package/dist/handlers/repl-handlers.js +37 -0
  19. package/dist/handlers/replCommandHandler.d.ts +125 -0
  20. package/dist/handlers/replCommandHandler.js +255 -0
  21. package/dist/handlers/replCommandHandler.test.d.ts +1 -0
  22. package/dist/handlers/replCommandHandler.test.js +103 -0
  23. package/dist/repl-manager.d.ts +73 -0
  24. package/dist/repl-manager.js +407 -0
  25. package/dist/replIntegration.d.ts +14 -0
  26. package/dist/replIntegration.js +27 -0
  27. package/dist/server.js +37 -19
  28. package/dist/setup-claude-server.js +0 -20
  29. package/dist/tools/edit.js +129 -29
  30. package/dist/tools/enhanced-read-output.js +69 -0
  31. package/dist/tools/enhanced-send-input.js +111 -0
  32. package/dist/tools/filesystem.d.ts +7 -5
  33. package/dist/tools/filesystem.js +56 -27
  34. package/dist/tools/repl.d.ts +21 -0
  35. package/dist/tools/repl.js +217 -0
  36. package/dist/tools/schemas.d.ts +9 -0
  37. package/dist/tools/schemas.js +3 -0
  38. package/dist/tools/send-input.d.ts +2 -0
  39. package/dist/tools/send-input.js +45 -0
  40. package/dist/utils/fuzzySearchLogger.d.ts +30 -0
  41. package/dist/utils/fuzzySearchLogger.js +126 -0
  42. package/dist/utils/lineEndingHandler.d.ts +21 -0
  43. package/dist/utils/lineEndingHandler.js +77 -0
  44. package/dist/utils/lineEndingHandler_optimized.d.ts +21 -0
  45. package/dist/utils/lineEndingHandler_optimized.js +77 -0
  46. package/dist/utils/trackTools.d.ts +6 -0
  47. package/dist/utils/trackTools.js +54 -0
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/package.json +7 -2
package/README.md CHANGED
@@ -58,6 +58,10 @@ Execute long-running terminal commands on your computer and manage processes thr
58
58
  - Multiple file support
59
59
  - Pattern-based replacements
60
60
  - vscode-ripgrep based recursive code or text search in folders
61
+ - Comprehensive audit logging:
62
+ - All tool calls are automatically logged
63
+ - Log rotation with 10MB size limit
64
+ - Detailed timestamps and arguments
61
65
 
62
66
  ## Installation
63
67
  First, ensure you've downloaded and installed the [Claude Desktop app](https://claude.ai/download) and you have [npm installed](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
@@ -140,24 +144,24 @@ The server provides a comprehensive set of tools organized into several categori
140
144
 
141
145
  | Category | Tool | Description |
142
146
  |----------|------|-------------|
143
- | **Configuration** | `get_config` | Get the complete server configuration as JSON (includes blockedCommands, defaultShell, allowedDirectories) |
144
- | | `set_config_value` | Set a specific configuration value by key. Available settings: <br>• `blockedCommands`: Array of shell commands that cannot be executed<br>• `defaultShell`: Shell to use for commands (e.g., bash, zsh, powershell)<br>• `allowedDirectories`: Array of filesystem paths the server can access for file operations (⚠️ terminal commands can still access files outside these directories) |
147
+ | **Configuration** | `get_config` | Get the complete server configuration as JSON (includes blockedCommands, defaultShell, allowedDirectories, fileReadLineLimit, fileWriteLineLimit, telemetryEnabled) |
148
+ | | `set_config_value` | Set a specific configuration value by key. Available settings: <br>• `blockedCommands`: Array of shell commands that cannot be executed<br>• `defaultShell`: Shell to use for commands (e.g., bash, zsh, powershell)<br>• `allowedDirectories`: Array of filesystem paths the server can access for file operations (⚠️ terminal commands can still access files outside these directories)<br>• `fileReadLineLimit`: Maximum lines to read at once (default: 1000)<br>• `fileWriteLineLimit`: Maximum lines to write at once (default: 50)<br>• `telemetryEnabled`: Enable/disable telemetry (boolean) |
145
149
  | **Terminal** | `execute_command` | Execute a terminal command with configurable timeout and shell selection |
146
150
  | | `read_output` | Read new output from a running terminal session |
147
151
  | | `force_terminate` | Force terminate a running terminal session |
148
152
  | | `list_sessions` | List all active terminal sessions |
149
153
  | | `list_processes` | List all running processes with detailed information |
150
154
  | | `kill_process` | Terminate a running process by PID |
151
- | **Filesystem** | `read_file` | Read contents from local filesystem or URLs (supports text and images) |
155
+ | **Filesystem** | `read_file` | Read contents from local filesystem or URLs with line-based pagination (supports offset and length parameters) |
152
156
  | | `read_multiple_files` | Read multiple files simultaneously |
153
- | | `write_file` | Completely replace file contents (best for large changes) |
157
+ | | `write_file` | Write file contents with options for rewrite or append mode (uses configurable line limits) |
154
158
  | | `create_directory` | Create a new directory or ensure it exists |
155
159
  | | `list_directory` | Get detailed listing of files and directories |
156
160
  | | `move_file` | Move or rename files and directories |
157
161
  | | `search_files` | Find files by name using case-insensitive substring matching |
158
162
  | | `search_code` | Search for text/code patterns within file contents using ripgrep |
159
163
  | | `get_file_info` | Retrieve detailed metadata about a file or directory |
160
- | **Text Editing** | `edit_block` | Apply surgical text replacements (best for changes <20% of file size) |
164
+ | **Text Editing** | `edit_block` | Apply targeted text replacements with enhanced prompting for smaller edits (includes character-level diff feedback) |
161
165
 
162
166
  ### Tool Usage Examples
163
167
 
@@ -181,6 +185,18 @@ console.log("new message");
181
185
  >>>>>>> REPLACE
182
186
  ```
183
187
 
188
+ ### Enhanced Edit Block Features
189
+
190
+ The `edit_block` tool includes several enhancements for better reliability:
191
+
192
+ 1. **Improved Prompting**: Tool descriptions now emphasize making multiple small, focused edits rather than one large change
193
+ 2. **Fuzzy Search Fallback**: When exact matches fail, it performs fuzzy search and provides detailed feedback
194
+ 3. **Character-level Diffs**: Shows exactly what's different using `{-removed-}{+added+}` format
195
+ 4. **Multiple Occurrence Support**: Can replace multiple instances with `expected_replacements` parameter
196
+ 5. **Comprehensive Logging**: All fuzzy searches are logged for analysis and debugging
197
+
198
+ When a search fails, you'll see detailed information about the closest match found, including similarity percentage, execution time, and character differences. All these details are automatically logged for later analysis using the fuzzy search log tools.
199
+
184
200
  ### URL Support
185
201
  - `read_file` can now fetch content from both local files and URLs
186
202
  - Example: `read_file` with `isUrl: true` parameter to read from web resources
@@ -189,6 +205,69 @@ console.log("new message");
189
205
  - Claude can see and analyze the actual image content
190
206
  - Default 30-second timeout for URL requests
191
207
 
208
+ ## Fuzzy Search Log Analysis (npm scripts)
209
+
210
+ The fuzzy search logging system includes convenient npm scripts for analyzing logs outside of the MCP environment:
211
+
212
+ ```bash
213
+ # View recent fuzzy search logs
214
+ npm run logs:view -- --count 20
215
+
216
+ # Analyze patterns and performance
217
+ npm run logs:analyze -- --threshold 0.8
218
+
219
+ # Export logs to CSV or JSON
220
+ npm run logs:export -- --format json --output analysis.json
221
+
222
+ # Clear all logs (with confirmation)
223
+ npm run logs:clear
224
+ ```
225
+
226
+ For detailed documentation on these scripts, see [scripts/README.md](scripts/README.md).
227
+
228
+ ## Fuzzy Search Logs
229
+
230
+ Desktop Commander includes comprehensive logging for fuzzy search operations in the `edit_block` tool. When an exact match isn't found, the system performs a fuzzy search and logs detailed information for analysis.
231
+
232
+ ### What Gets Logged
233
+
234
+ Every fuzzy search operation logs:
235
+ - **Search and found text**: The text you're looking for vs. what was found
236
+ - **Similarity score**: How close the match is (0-100%)
237
+ - **Execution time**: How long the search took
238
+ - **Character differences**: Detailed diff showing exactly what's different
239
+ - **File metadata**: Extension, search/found text lengths
240
+ - **Character codes**: Specific character codes causing differences
241
+
242
+ ### Log Location
243
+
244
+ Logs are automatically saved to:
245
+ - **macOS/Linux**: `~/.claude-server-commander-logs/fuzzy-search.log`
246
+ - **Windows**: `%USERPROFILE%\.claude-server-commander-logs\fuzzy-search.log`
247
+
248
+ ### What You'll Learn
249
+
250
+ The fuzzy search logs help you understand:
251
+ 1. **Why exact matches fail**: Common issues like whitespace differences, line endings, or character encoding
252
+ 2. **Performance patterns**: How search complexity affects execution time
253
+ 3. **File type issues**: Which file extensions commonly have matching problems
254
+ 4. **Character encoding problems**: Specific character codes that cause diffs
255
+
256
+ ## Audit Logging
257
+
258
+ Desktop Commander now includes comprehensive logging for all tool calls:
259
+
260
+ ### What Gets Logged
261
+ - Every tool call is logged with timestamp, tool name, and arguments (sanitized for privacy)
262
+ - Logs are rotated automatically when they reach 10MB in size
263
+
264
+ ### Log Location
265
+ Logs are saved to:
266
+ - **macOS/Linux**: `~/.claude-server-commander/claude_tool_call.log`
267
+ - **Windows**: `%USERPROFILE%\.claude-server-commander\claude_tool_call.log`
268
+
269
+ This audit trail helps with debugging, security monitoring, and understanding how Claude is interacting with your system.
270
+
192
271
  ## Handling Long-Running Commands
193
272
 
194
273
  For commands that may take a while:
@@ -297,7 +376,9 @@ This project extends the MCP Filesystem Server to enable:
297
376
  Created as part of exploring Claude MCPs: https://youtube.com/live/TlbjFDbl5Us
298
377
 
299
378
  ## DONE
300
- - **29-04-2025 Telemetry Opt Out trought configuration** - There is now setting to disable telemetry in config, ask in chat
379
+ - **20-05-2025 v0.1.40 Release** - Added audit logging for all tool calls, improved line-based file operations, enhanced edit_block with better prompting for smaller edits, added explicit telemetry opt-out prompting
380
+ - **05-05-2025 Fuzzy Search Logging** - Added comprehensive logging system for fuzzy search operations with detailed analysis tools, character-level diffs, and performance metrics to help debug edit_block failures
381
+ - **29-04-2025 Telemetry Opt Out through configuration** - There is now setting to disable telemetry in config, ask in chat
301
382
  - **23-04-2025 Enhanced edit functionality** - Improved format, added fuzzy search and multi-occurrence replacements, should fail less and use edit block more often
302
383
  - **16-04-2025 Better configurations** - Improved settings for allowed paths, commands and shell environments
303
384
  - **14-04-2025 Windows environment fixes** - Resolved issues specific to Windows platforms
@@ -333,11 +414,13 @@ The following features are currently being explored:
333
414
  <ul style="list-style-type: none; padding: 0;">
334
415
  <li>🌟 <a href="https://github.com/sponsors/wonderwhy-er"><strong>GitHub Sponsors</strong></a> - Recurring support</li>
335
416
  <li>☕ <a href="https://www.buymeacoffee.com/wonderwhyer"><strong>Buy Me A Coffee</strong></a> - One-time contributions</li>
417
+ <li>💖 <a href="https://www.patreon.com/c/EduardsRuzga"><strong>Patreon</strong></a> - Become a patron and support us monthly</li>
336
418
  <li>⭐ <a href="https://github.com/wonderwhy-er/DesktopCommanderMCP"><strong>Star on GitHub</strong></a> - Help others discover the project</li>
337
419
  </ul>
338
420
  </div>
339
421
  </div>
340
422
 
423
+
341
424
  ### Supporters Hall of Fame
342
425
 
343
426
  Generous supporters are featured here. Thank you for helping make this project possible!
@@ -0,0 +1,109 @@
1
+ interface TerminalManager {
2
+ executeCommand: (command: string, options?: any) => Promise<any>;
3
+ sendInputToProcess: (pid: number, input: string) => boolean;
4
+ getNewOutput: (pid: number) => string;
5
+ terminateProcess: (pid: number) => Promise<boolean>;
6
+ }
7
+ interface SSHOptions {
8
+ username?: string;
9
+ port?: number;
10
+ identity?: string;
11
+ password?: string;
12
+ timeout?: number;
13
+ }
14
+ interface ExecuteOptions {
15
+ timeout?: number;
16
+ stopOnError?: boolean;
17
+ }
18
+ export declare class REPLSessionManager {
19
+ private sessions;
20
+ private terminalManager;
21
+ private defaultPromptPatterns;
22
+ constructor(terminalManager: TerminalManager);
23
+ /**
24
+ * Create a new SSH session
25
+ * @param host - SSH host to connect to
26
+ * @param options - SSH connection options
27
+ * @returns PID of the created SSH session
28
+ */
29
+ createSSHSession(host: string, options?: SSHOptions): Promise<number>;
30
+ /**
31
+ * Create a new REPL session for a specific language
32
+ * @param language - Language for the REPL (python, node, bash)
33
+ * @param options - Configuration options
34
+ * @returns PID of the created session
35
+ */
36
+ createSession(language: string, options?: any): Promise<number>;
37
+ /**
38
+ * Execute code in an existing REPL session
39
+ * @param pid - Process ID of the REPL session
40
+ * @param code - Code to execute
41
+ * @param options - Execution options
42
+ * @returns Results including output and status
43
+ */
44
+ executeCode(pid: number, code: string, options?: ExecuteOptions): Promise<any>;
45
+ /**
46
+ * Send input to a REPL process and wait for output with timeout
47
+ * @param pid - Process ID
48
+ * @param input - Input to send
49
+ * @param language - REPL language
50
+ * @param timeoutMs - Timeout in milliseconds
51
+ * @returns Result object with output and status
52
+ */
53
+ sendAndReadREPL(pid: number, input: string, language: string, timeoutMs?: number): Promise<any>;
54
+ /**
55
+ * Handle multi-line code input for different languages
56
+ * @param pid - Process ID
57
+ * @param code - Multi-line code
58
+ * @param language - REPL language
59
+ * @param timeout - Timeout in milliseconds
60
+ * @returns Result object
61
+ */
62
+ handleMultilineCode(pid: number, code: string, language: string, timeout: number): Promise<any>;
63
+ /**
64
+ * Detect if the REPL output is complete and ready for next input
65
+ * @param output - Current output
66
+ * @param language - REPL language or session type
67
+ * @returns Whether output is complete
68
+ */
69
+ isOutputComplete(output: string, language: string): boolean;
70
+ /**
71
+ * Calculate appropriate timeout based on code complexity
72
+ * @param code - Code to analyze
73
+ * @returns Timeout in milliseconds
74
+ */
75
+ calculateTimeout(code: string): number;
76
+ /**
77
+ * Detect errors in REPL output
78
+ * @param output - REPL output
79
+ * @param language - REPL language
80
+ * @returns Detected error or null
81
+ */
82
+ detectErrors(output: string, language: string): string | null;
83
+ /**
84
+ * Clean and format REPL output
85
+ * @param output - Raw output
86
+ * @param input - Input that was sent
87
+ * @param language - REPL language
88
+ * @returns Cleaned output
89
+ */
90
+ cleanOutput(output: string, input: string, language: string): string;
91
+ /**
92
+ * List all active REPL sessions
93
+ * @returns List of session objects
94
+ */
95
+ listSessions(): Array<any>;
96
+ /**
97
+ * Close a specific REPL session
98
+ * @param pid - Process ID to close
99
+ * @returns Success status
100
+ */
101
+ closeSession(pid: number): Promise<boolean>;
102
+ /**
103
+ * Close all idle sessions older than specified time
104
+ * @param maxIdleMs - Maximum idle time in milliseconds
105
+ * @returns Number of closed sessions
106
+ */
107
+ closeIdleSessions(maxIdleMs?: number): Promise<number>;
108
+ }
109
+ export {};
@@ -0,0 +1,364 @@
1
+ import * as os from 'os';
2
+ export class REPLSessionManager {
3
+ constructor(terminalManager) {
4
+ this.sessions = new Map();
5
+ this.terminalManager = terminalManager;
6
+ this.defaultPromptPatterns = {
7
+ python: /^(>>>|\.\.\.) /m,
8
+ node: /> $/m,
9
+ bash: /[\w\d\-_]+@[\w\d\-_]+:.*[$#] $/m,
10
+ ssh: /[\w\d\-_]+@[\w\d\-_]+:.*[$#] $/m
11
+ };
12
+ }
13
+ /**
14
+ * Create a new SSH session
15
+ * @param host - SSH host to connect to
16
+ * @param options - SSH connection options
17
+ * @returns PID of the created SSH session
18
+ */
19
+ async createSSHSession(host, options = {}) {
20
+ if (!host) {
21
+ throw new Error('SSH host is required');
22
+ }
23
+ const username = options.username || os.userInfo().username;
24
+ const port = options.port || 22;
25
+ let sshCommand = `ssh ${username}@${host}`;
26
+ // Add optional parameters
27
+ if (port !== 22) {
28
+ sshCommand += ` -p ${port}`;
29
+ }
30
+ if (options.identity) {
31
+ sshCommand += ` -i "${options.identity}"`;
32
+ }
33
+ // Start the SSH process
34
+ const result = await this.terminalManager.executeCommand(sshCommand, { timeout: options.timeout || 10000 });
35
+ if (!result || result.pid <= 0) {
36
+ throw new Error(`Failed to start SSH session to ${host}`);
37
+ }
38
+ // Handle password prompt if needed
39
+ if (options.password) {
40
+ // Wait for password prompt
41
+ let output = "";
42
+ const startTime = Date.now();
43
+ const passwordPromptTimeout = 5000;
44
+ while (Date.now() - startTime < passwordPromptTimeout) {
45
+ const newOutput = this.terminalManager.getNewOutput(result.pid);
46
+ if (newOutput && newOutput.length > 0) {
47
+ output += newOutput;
48
+ if (output.toLowerCase().includes('password:')) {
49
+ // Send password
50
+ this.terminalManager.sendInputToProcess(result.pid, options.password + '\n');
51
+ break;
52
+ }
53
+ }
54
+ await new Promise(resolve => setTimeout(resolve, 100));
55
+ }
56
+ }
57
+ // Store session info
58
+ this.sessions.set(result.pid, {
59
+ type: 'ssh',
60
+ host,
61
+ username,
62
+ pid: result.pid,
63
+ startTime: Date.now(),
64
+ lastActivity: Date.now()
65
+ });
66
+ return result.pid;
67
+ }
68
+ /**
69
+ * Create a new REPL session for a specific language
70
+ * @param language - Language for the REPL (python, node, bash)
71
+ * @param options - Configuration options
72
+ * @returns PID of the created session
73
+ */
74
+ async createSession(language, options = {}) {
75
+ // Handle SSH sessions separately
76
+ if (language.toLowerCase() === 'ssh') {
77
+ return this.createSSHSession(options.host, options);
78
+ }
79
+ let command;
80
+ let args = [];
81
+ switch (language.toLowerCase()) {
82
+ case 'python':
83
+ command = process.platform === 'win32' ? 'python' : 'python3';
84
+ args = ['-i'];
85
+ break;
86
+ case 'node':
87
+ command = 'node';
88
+ break;
89
+ case 'bash':
90
+ command = process.platform === 'win32' ? 'cmd' : 'bash';
91
+ break;
92
+ default:
93
+ throw new Error(`Unsupported language: ${language}`);
94
+ }
95
+ // Start the process
96
+ const result = await this.terminalManager.executeCommand(command, { args, timeout: options.timeout || 5000 });
97
+ if (!result || result.pid <= 0) {
98
+ throw new Error(`Failed to start ${language} REPL`);
99
+ }
100
+ // Store session info
101
+ this.sessions.set(result.pid, {
102
+ language,
103
+ pid: result.pid,
104
+ startTime: Date.now(),
105
+ lastActivity: Date.now()
106
+ });
107
+ return result.pid;
108
+ }
109
+ /**
110
+ * Execute code in an existing REPL session
111
+ * @param pid - Process ID of the REPL session
112
+ * @param code - Code to execute
113
+ * @param options - Execution options
114
+ * @returns Results including output and status
115
+ */
116
+ async executeCode(pid, code, options = {}) {
117
+ const session = this.sessions.get(pid);
118
+ if (!session) {
119
+ throw new Error(`No active session with PID ${pid}`);
120
+ }
121
+ // Calculate timeout based on code complexity if not specified
122
+ const timeout = options.timeout || this.calculateTimeout(code);
123
+ // Handle multi-line code
124
+ if (code.includes('\n')) {
125
+ return this.handleMultilineCode(pid, code, session.language || 'bash', timeout);
126
+ }
127
+ else {
128
+ return this.sendAndReadREPL(pid, code, session.language || 'bash', timeout);
129
+ }
130
+ }
131
+ /**
132
+ * Send input to a REPL process and wait for output with timeout
133
+ * @param pid - Process ID
134
+ * @param input - Input to send
135
+ * @param language - REPL language
136
+ * @param timeoutMs - Timeout in milliseconds
137
+ * @returns Result object with output and status
138
+ */
139
+ async sendAndReadREPL(pid, input, language, timeoutMs = 3000) {
140
+ // Send the input with newline if not already present
141
+ const inputToSend = input.endsWith('\n') ? input : input + '\n';
142
+ const success = this.terminalManager.sendInputToProcess(pid, inputToSend);
143
+ if (!success) {
144
+ return {
145
+ success: false,
146
+ output: null,
147
+ error: "Failed to send input to process"
148
+ };
149
+ }
150
+ // Wait for output with timeout
151
+ let output = "";
152
+ const startTime = Date.now();
153
+ // Keep checking for output until timeout
154
+ while (Date.now() - startTime < timeoutMs) {
155
+ const newOutput = this.terminalManager.getNewOutput(pid);
156
+ if (newOutput && newOutput.length > 0) {
157
+ output += newOutput;
158
+ // Check if output is complete (using prompt detection)
159
+ if (this.isOutputComplete(output, language)) {
160
+ break;
161
+ }
162
+ }
163
+ await new Promise(resolve => setTimeout(resolve, 100));
164
+ }
165
+ // Update last activity time
166
+ if (this.sessions.has(pid)) {
167
+ const session = this.sessions.get(pid);
168
+ if (session) {
169
+ session.lastActivity = Date.now();
170
+ }
171
+ }
172
+ // Check for errors
173
+ const error = this.detectErrors(output, language);
174
+ return {
175
+ success: true,
176
+ output: this.cleanOutput(output, input, language),
177
+ timeout: Date.now() - startTime >= timeoutMs,
178
+ error: error
179
+ };
180
+ }
181
+ /**
182
+ * Handle multi-line code input for different languages
183
+ * @param pid - Process ID
184
+ * @param code - Multi-line code
185
+ * @param language - REPL language
186
+ * @param timeout - Timeout in milliseconds
187
+ * @returns Result object
188
+ */
189
+ async handleMultilineCode(pid, code, language, timeout) {
190
+ const lines = code.split('\n');
191
+ let isBlock = false;
192
+ let fullOutput = '';
193
+ // For Python, we need to handle indentation carefully
194
+ if (language.toLowerCase() === 'python') {
195
+ for (let i = 0; i < lines.length; i++) {
196
+ const line = lines[i];
197
+ const isLastLine = i === lines.length - 1;
198
+ // Send line and wait for prompt
199
+ const result = await this.sendAndReadREPL(pid, line, language,
200
+ // If it's the last line and potentially ending a block, wait longer
201
+ isLastLine && isBlock ? timeout : Math.min(1000, timeout));
202
+ fullOutput += result.output || '';
203
+ // Check if we're in a block (Python indentation)
204
+ if (line.trim() && (line.startsWith(' ') || line.startsWith('\t'))) {
205
+ isBlock = true;
206
+ }
207
+ else if (line.trim() === '' && isBlock) {
208
+ // Empty line ends a block
209
+ isBlock = false;
210
+ }
211
+ else if (line.trim() && !line.startsWith(' ') && !line.startsWith('\t')) {
212
+ // Non-indented, non-empty line
213
+ isBlock = false;
214
+ }
215
+ // Handle errors early
216
+ if (result.error) {
217
+ return {
218
+ success: false,
219
+ output: fullOutput,
220
+ error: result.error
221
+ };
222
+ }
223
+ }
224
+ // Ensure block is closed with empty line if needed
225
+ if (isBlock) {
226
+ const finalResult = await this.sendAndReadREPL(pid, '', language, timeout);
227
+ fullOutput += finalResult.output || '';
228
+ }
229
+ }
230
+ else {
231
+ // For other languages, send the entire block at once
232
+ return await this.sendAndReadREPL(pid, code, language, timeout);
233
+ }
234
+ return {
235
+ success: true,
236
+ output: fullOutput,
237
+ error: this.detectErrors(fullOutput, language)
238
+ };
239
+ }
240
+ /**
241
+ * Detect if the REPL output is complete and ready for next input
242
+ * @param output - Current output
243
+ * @param language - REPL language or session type
244
+ * @returns Whether output is complete
245
+ */
246
+ isOutputComplete(output, language) {
247
+ const pattern = this.defaultPromptPatterns[language.toLowerCase()];
248
+ if (!pattern)
249
+ return true; // If no pattern, assume complete
250
+ return pattern.test(output);
251
+ }
252
+ /**
253
+ * Calculate appropriate timeout based on code complexity
254
+ * @param code - Code to analyze
255
+ * @returns Timeout in milliseconds
256
+ */
257
+ calculateTimeout(code) {
258
+ // Base timeout
259
+ let timeout = 2000;
260
+ // Add time for loops
261
+ const loopCount = (code.match(/for|while/g) || []).length;
262
+ timeout += loopCount * 1000;
263
+ // Add time for imports or requires
264
+ const importCount = (code.match(/import|require/g) || []).length;
265
+ timeout += importCount * 2000;
266
+ // Add time based on code length
267
+ timeout += Math.min(code.length * 5, 5000);
268
+ // Cap at reasonable maximum
269
+ return Math.min(timeout, 30000);
270
+ }
271
+ /**
272
+ * Detect errors in REPL output
273
+ * @param output - REPL output
274
+ * @param language - REPL language
275
+ * @returns Detected error or null
276
+ */
277
+ detectErrors(output, language) {
278
+ const errorPatterns = {
279
+ python: /\b(Error|Exception|SyntaxError|ValueError|TypeError)\b:.*$/m,
280
+ node: /\b(Error|SyntaxError|TypeError|ReferenceError)\b:.*$/m,
281
+ bash: /\b(command not found|No such file or directory)\b/m
282
+ };
283
+ const pattern = errorPatterns[language.toLowerCase()];
284
+ if (!pattern)
285
+ return null;
286
+ const match = output.match(pattern);
287
+ return match ? match[0] : null;
288
+ }
289
+ /**
290
+ * Clean and format REPL output
291
+ * @param output - Raw output
292
+ * @param input - Input that was sent
293
+ * @param language - REPL language
294
+ * @returns Cleaned output
295
+ */
296
+ cleanOutput(output, input, language) {
297
+ // Remove echoed input if present
298
+ let cleaned = output;
299
+ // Remove the input echo that might appear in the output
300
+ const inputWithoutNewlines = input.replace(/\n/g, '');
301
+ if (inputWithoutNewlines.length > 0) {
302
+ cleaned = cleaned.replace(inputWithoutNewlines, '');
303
+ }
304
+ // Remove common prompt patterns
305
+ if (language.toLowerCase() === 'python') {
306
+ cleaned = cleaned.replace(/^(>>>|\.\.\.) /mg, '');
307
+ }
308
+ else if (language.toLowerCase() === 'node') {
309
+ cleaned = cleaned.replace(/^> /mg, '');
310
+ }
311
+ // Trim whitespace
312
+ cleaned = cleaned.trim();
313
+ return cleaned;
314
+ }
315
+ /**
316
+ * List all active REPL sessions
317
+ * @returns List of session objects
318
+ */
319
+ listSessions() {
320
+ const result = [];
321
+ this.sessions.forEach((session, pid) => {
322
+ result.push({
323
+ pid,
324
+ language: session.language,
325
+ startTime: session.startTime,
326
+ lastActivity: session.lastActivity,
327
+ idleTime: Date.now() - session.lastActivity
328
+ });
329
+ });
330
+ return result;
331
+ }
332
+ /**
333
+ * Close a specific REPL session
334
+ * @param pid - Process ID to close
335
+ * @returns Success status
336
+ */
337
+ async closeSession(pid) {
338
+ if (!this.sessions.has(pid)) {
339
+ return false;
340
+ }
341
+ const success = await this.terminalManager.terminateProcess(pid);
342
+ if (success) {
343
+ this.sessions.delete(pid);
344
+ }
345
+ return success;
346
+ }
347
+ /**
348
+ * Close all idle sessions older than specified time
349
+ * @param maxIdleMs - Maximum idle time in milliseconds
350
+ * @returns Number of closed sessions
351
+ */
352
+ async closeIdleSessions(maxIdleMs = 30 * 60 * 1000) {
353
+ let closedCount = 0;
354
+ for (const [pid, session] of this.sessions.entries()) {
355
+ const idleTime = Date.now() - session.lastActivity;
356
+ if (idleTime > maxIdleMs) {
357
+ const success = await this.closeSession(pid);
358
+ if (success)
359
+ closedCount++;
360
+ }
361
+ }
362
+ return closedCount;
363
+ }
364
+ }
@@ -0,0 +1 @@
1
+ export {};