@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.
- package/README.md +89 -6
- package/dist/REPLSessionManager.d.ts +109 -0
- package/dist/REPLSessionManager.js +364 -0
- package/dist/REPLSessionManager.test.d.ts +1 -0
- package/dist/REPLSessionManager.test.js +75 -0
- package/dist/client/replClient.d.ts +63 -0
- package/dist/client/replClient.js +217 -0
- package/dist/client/sshClient.d.ts +82 -0
- package/dist/client/sshClient.js +200 -0
- package/dist/config-manager.d.ts +2 -0
- package/dist/config-manager.js +3 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/handlers/filesystem-handlers.js +37 -3
- package/dist/handlers/fuzzy-search-log-handlers.d.ts +13 -0
- package/dist/handlers/fuzzy-search-log-handlers.js +179 -0
- package/dist/handlers/repl-handlers.d.ts +21 -0
- package/dist/handlers/repl-handlers.js +37 -0
- package/dist/handlers/replCommandHandler.d.ts +125 -0
- package/dist/handlers/replCommandHandler.js +255 -0
- package/dist/handlers/replCommandHandler.test.d.ts +1 -0
- package/dist/handlers/replCommandHandler.test.js +103 -0
- package/dist/repl-manager.d.ts +73 -0
- package/dist/repl-manager.js +407 -0
- package/dist/replIntegration.d.ts +14 -0
- package/dist/replIntegration.js +27 -0
- package/dist/server.js +37 -19
- package/dist/setup-claude-server.js +0 -20
- package/dist/tools/edit.js +129 -29
- package/dist/tools/enhanced-read-output.js +69 -0
- package/dist/tools/enhanced-send-input.js +111 -0
- package/dist/tools/filesystem.d.ts +7 -5
- package/dist/tools/filesystem.js +56 -27
- package/dist/tools/repl.d.ts +21 -0
- package/dist/tools/repl.js +217 -0
- package/dist/tools/schemas.d.ts +9 -0
- package/dist/tools/schemas.js +3 -0
- package/dist/tools/send-input.d.ts +2 -0
- package/dist/tools/send-input.js +45 -0
- package/dist/utils/fuzzySearchLogger.d.ts +30 -0
- package/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/utils/lineEndingHandler.d.ts +21 -0
- package/dist/utils/lineEndingHandler.js +77 -0
- package/dist/utils/lineEndingHandler_optimized.d.ts +21 -0
- package/dist/utils/lineEndingHandler_optimized.js +77 -0
- package/dist/utils/trackTools.d.ts +6 -0
- package/dist/utils/trackTools.js +54 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
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` |
|
|
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
|
|
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
|
-
- **
|
|
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 {};
|