@wonderwhy-er/desktop-commander 0.2.6 → 0.2.7

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 (112) hide show
  1. package/dist/index-dxt.js +10 -47
  2. package/dist/server.js +6 -0
  3. package/dist/tools/filesystem.js +37 -4
  4. package/dist/version.d.ts +1 -1
  5. package/dist/version.js +1 -1
  6. package/package.json +2 -1
  7. package/dist/REPLSessionManager.d.ts +0 -109
  8. package/dist/REPLSessionManager.js +0 -364
  9. package/dist/REPLSessionManager.test.d.ts +0 -1
  10. package/dist/REPLSessionManager.test.js +0 -75
  11. package/dist/client/replClient.d.ts +0 -63
  12. package/dist/client/replClient.js +0 -217
  13. package/dist/client/sshClient.d.ts +0 -82
  14. package/dist/client/sshClient.js +0 -200
  15. package/dist/command-manager.js.map +0 -1
  16. package/dist/config-manager.js.map +0 -1
  17. package/dist/config.js.map +0 -1
  18. package/dist/custom-stdio.js.map +0 -1
  19. package/dist/error-handlers.js.map +0 -1
  20. package/dist/handlers/command-handlers.d.ts +0 -13
  21. package/dist/handlers/command-handlers.js +0 -43
  22. package/dist/handlers/edit-search-handlers.js.map +0 -1
  23. package/dist/handlers/filesystem-handlers.js.map +0 -1
  24. package/dist/handlers/fuzzy-search-log-handlers.d.ts +0 -13
  25. package/dist/handlers/fuzzy-search-log-handlers.js +0 -179
  26. package/dist/handlers/index.js.map +0 -1
  27. package/dist/handlers/process-handlers.js.map +0 -1
  28. package/dist/handlers/repl-handlers.d.ts +0 -21
  29. package/dist/handlers/repl-handlers.js +0 -37
  30. package/dist/handlers/replCommandHandler.d.ts +0 -125
  31. package/dist/handlers/replCommandHandler.js +0 -255
  32. package/dist/handlers/replCommandHandler.test.d.ts +0 -1
  33. package/dist/handlers/replCommandHandler.test.js +0 -103
  34. package/dist/handlers/terminal-handlers.js.map +0 -1
  35. package/dist/index-with-startup-detection.d.ts +0 -5
  36. package/dist/index-with-startup-detection.js +0 -180
  37. package/dist/index.js.map +0 -1
  38. package/dist/logging.d.ts +0 -2
  39. package/dist/logging.js +0 -28
  40. package/dist/polyform-license-src/edit/edit.d.ts +0 -15
  41. package/dist/polyform-license-src/edit/edit.js +0 -163
  42. package/dist/polyform-license-src/edit/fuzzySearch.d.ts +0 -30
  43. package/dist/polyform-license-src/edit/fuzzySearch.js +0 -121
  44. package/dist/polyform-license-src/edit/handlers.d.ts +0 -16
  45. package/dist/polyform-license-src/edit/handlers.js +0 -24
  46. package/dist/polyform-license-src/edit/index.d.ts +0 -12
  47. package/dist/polyform-license-src/edit/index.js +0 -13
  48. package/dist/polyform-license-src/edit/schemas.d.ts +0 -25
  49. package/dist/polyform-license-src/edit/schemas.js +0 -16
  50. package/dist/polyform-license-src/index.d.ts +0 -9
  51. package/dist/polyform-license-src/index.js +0 -10
  52. package/dist/repl-manager.d.ts +0 -73
  53. package/dist/repl-manager.js +0 -407
  54. package/dist/replIntegration.d.ts +0 -14
  55. package/dist/replIntegration.js +0 -27
  56. package/dist/sandbox/index.d.ts +0 -9
  57. package/dist/sandbox/index.js +0 -50
  58. package/dist/sandbox/mac-sandbox.d.ts +0 -19
  59. package/dist/sandbox/mac-sandbox.js +0 -174
  60. package/dist/server.js.map +0 -1
  61. package/dist/setup.log +0 -32
  62. package/dist/terminal-manager.js.map +0 -1
  63. package/dist/tools/client.d.ts +0 -10
  64. package/dist/tools/client.js +0 -13
  65. package/dist/tools/command-block.d.ts +0 -18
  66. package/dist/tools/command-block.js +0 -62
  67. package/dist/tools/config.js.map +0 -1
  68. package/dist/tools/debug-path.d.ts +0 -1
  69. package/dist/tools/debug-path.js +0 -44
  70. package/dist/tools/edit.js.map +0 -1
  71. package/dist/tools/enhanced-read-output.js +0 -69
  72. package/dist/tools/enhanced-send-input.js +0 -111
  73. package/dist/tools/environment.d.ts +0 -55
  74. package/dist/tools/environment.js +0 -65
  75. package/dist/tools/execute.d.ts +0 -10
  76. package/dist/tools/execute.js +0 -158
  77. package/dist/tools/execute.js.map +0 -1
  78. package/dist/tools/filesystem-fixed.d.ts +0 -22
  79. package/dist/tools/filesystem-fixed.js +0 -176
  80. package/dist/tools/filesystem.js.map +0 -1
  81. package/dist/tools/fuzzySearch.js.map +0 -1
  82. package/dist/tools/mime-types.js.map +0 -1
  83. package/dist/tools/pdf-reader.d.ts +0 -13
  84. package/dist/tools/pdf-reader.js +0 -214
  85. package/dist/tools/process.js.map +0 -1
  86. package/dist/tools/progress.d.ts +0 -20
  87. package/dist/tools/progress.js +0 -59
  88. package/dist/tools/repl.d.ts +0 -21
  89. package/dist/tools/repl.js +0 -217
  90. package/dist/tools/schemas.js.map +0 -1
  91. package/dist/tools/search.js.map +0 -1
  92. package/dist/tools/send-input.d.ts +0 -2
  93. package/dist/tools/send-input.js +0 -45
  94. package/dist/types.js.map +0 -1
  95. package/dist/utils/capture.js.map +0 -1
  96. package/dist/utils/early-logger.d.ts +0 -4
  97. package/dist/utils/early-logger.js +0 -35
  98. package/dist/utils/fuzzySearchLogger.js.map +0 -1
  99. package/dist/utils/lineEndingHandler.js.map +0 -1
  100. package/dist/utils/lineEndingHandler_optimized.d.ts +0 -21
  101. package/dist/utils/lineEndingHandler_optimized.js +0 -77
  102. package/dist/utils/mcp-logger.d.ts +0 -30
  103. package/dist/utils/mcp-logger.js +0 -59
  104. package/dist/utils/smithery-detector.d.ts +0 -94
  105. package/dist/utils/smithery-detector.js +0 -292
  106. package/dist/utils/startup-detector.d.ts +0 -65
  107. package/dist/utils/startup-detector.js +0 -390
  108. package/dist/utils/trackTools.js.map +0 -1
  109. package/dist/utils/withTimeout.js.map +0 -1
  110. package/dist/utils.d.ts +0 -26
  111. package/dist/utils.js +0 -227
  112. package/dist/version.js.map +0 -1
package/dist/index-dxt.js CHANGED
@@ -1,76 +1,39 @@
1
1
  #!/usr/bin/env node
2
- // Add immediate logging before any imports
3
- console.error("[DXT] === ENTRY POINT START ===");
4
- console.error(`[DXT] Node version: ${process.version}`);
5
- console.error(`[DXT] Platform: ${process.platform}`);
6
- console.error(`[DXT] Working directory: ${process.cwd()}`);
7
- console.error(`[DXT] __dirname: ${__dirname}`);
8
- console.error(`[DXT] Args: ${JSON.stringify(process.argv)}`);
9
- // Add error handlers immediately
10
- process.on('uncaughtException', (error) => {
11
- console.error(`[DXT] UNCAUGHT EXCEPTION: ${error.message}`);
12
- console.error(`[DXT] Stack: ${error.stack}`);
13
- });
14
- process.on('unhandledRejection', (reason) => {
15
- console.error(`[DXT] UNHANDLED REJECTION: ${String(reason)}`);
16
- });
17
- process.on('exit', (code) => {
18
- console.error(`[DXT] PROCESS EXITING with code: ${code}`);
19
- });
20
- console.error("[DXT] About to import modules...");
21
2
  // Simplified entry point for DXT that avoids configuration loading issues
22
3
  import { FilteredStdioServerTransport } from './custom-stdio.js';
23
- console.error("[DXT] FilteredStdioServerTransport imported");
24
4
  import { server } from './server.js';
25
- console.error("[DXT] Server imported");
26
5
  async function runDXTServer() {
27
6
  try {
28
- console.error("[DXT] === runDXTServer() START ===");
29
- console.error("=== DXT DEBUG START ===");
30
- console.error(`Node version: ${process.version}`);
31
- console.error(`Platform: ${process.platform}`);
32
- console.error(`Working directory: ${process.cwd()}`);
33
- console.error(`__dirname: ${__dirname}`);
34
- console.error(`Args: ${JSON.stringify(process.argv)}`);
35
- console.error("=== DXT DEBUG END ===");
36
- console.error("[DXT] About to create transport...");
7
+ console.error("Starting Desktop Commander MCP for DXT...");
37
8
  // Create transport
38
9
  const transport = new FilteredStdioServerTransport();
39
- console.error("[DXT] Transport created successfully");
40
- // Send structured debug info
41
- transport.sendLog("info", "DXT Server Starting", {
42
- nodeVersion: process.version,
43
- platform: process.platform,
44
- cwd: process.cwd(),
45
- dirname: __dirname,
46
- argv: process.argv
10
+ // Handle process errors gracefully
11
+ process.on('uncaughtException', (error) => {
12
+ console.error(`[DXT] Uncaught exception: ${error.message}`);
13
+ // Don't exit immediately - let the server handle it
14
+ });
15
+ process.on('unhandledRejection', (reason) => {
16
+ console.error(`[DXT] Unhandled rejection: ${reason}`);
17
+ // Don't exit immediately - let the server handle it
47
18
  });
48
- console.error("[DXT] Debug info sent via transport");
49
19
  // Connect server
50
- console.error("[DXT] About to connect server...");
51
20
  console.error("Connecting MCP server...");
52
21
  await server.connect(transport);
53
- console.error("[DXT] Server connected successfully!");
54
22
  console.error("MCP server connected successfully");
55
- transport.sendLog("info", "MCP server connected successfully in DXT mode");
56
- console.error("[DXT] === runDXTServer() END ===");
57
23
  }
58
24
  catch (error) {
59
25
  const errorMessage = error instanceof Error ? error.message : String(error);
60
26
  const errorStack = error instanceof Error ? error.stack : undefined;
61
- console.error(`[DXT] FATAL ERROR in runDXTServer: ${errorMessage}`);
27
+ console.error(`[DXT] FATAL ERROR: ${errorMessage}`);
62
28
  if (errorStack) {
63
29
  console.error(`[DXT] Stack: ${errorStack}`);
64
30
  }
65
31
  process.exit(1);
66
32
  }
67
33
  }
68
- console.error("[DXT] About to call runDXTServer...");
69
34
  // Run the server
70
35
  runDXTServer().catch((error) => {
71
36
  const errorMessage = error instanceof Error ? error.message : String(error);
72
- console.error(`[DXT] Failed to start in catch block: ${errorMessage}`);
73
37
  console.error(`[DXT] Failed to start: ${errorMessage}`);
74
38
  process.exit(1);
75
39
  });
76
- console.error("[DXT] === ENTRY POINT END ===");
package/dist/server.js CHANGED
@@ -365,6 +365,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
365
365
  • start_process("wc -l /path/file.csv") → Line counting
366
366
  • start_process("head -10 /path/file.csv") → File preview
367
367
 
368
+ BINARY FILE SUPPORT:
369
+ For PDF, Excel, Word, archives, databases, and other binary formats, use process tools with appropriate libraries or command-line utilities.
370
+
368
371
  INTERACTIVE PROCESSES FOR DATA ANALYSIS:
369
372
  1. start_process("python3 -i") - Start Python REPL for data work
370
373
  2. start_process("node -i") - Start Node.js REPL for JSON/JS
@@ -438,6 +441,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
438
441
  4. Analyze: interact_with_process(pid, "print(df.describe())")
439
442
  5. Continue: interact_with_process(pid, "df.groupby('column').size()")
440
443
 
444
+ BINARY FILE PROCESSING WORKFLOWS:
445
+ Use appropriate Python libraries (PyPDF2, pandas, docx2txt, etc.) or command-line tools for binary file analysis.
446
+
441
447
  SMART DETECTION:
442
448
  - Automatically waits for REPL prompt (>>>, >, etc.)
443
449
  - Detects errors and completion states
@@ -4,6 +4,7 @@ import os from 'os';
4
4
  import fetch from 'cross-fetch';
5
5
  import { createReadStream } from 'fs';
6
6
  import { createInterface } from 'readline';
7
+ import { isBinaryFile } from 'isbinaryfile';
7
8
  import { capture } from '../utils/capture.js';
8
9
  import { withTimeout } from '../utils/withTimeout.js';
9
10
  import { configManager } from '../config-manager.js';
@@ -78,6 +79,20 @@ async function getDefaultReadLength() {
78
79
  const config = await configManager.getConfig();
79
80
  return config.fileReadLineLimit ?? 1000; // Default to 1000 lines if not set
80
81
  }
82
+ /**
83
+ * Generate instructions for handling binary files
84
+ * @param filePath Path to the binary file
85
+ * @param mimeType MIME type of the file
86
+ * @returns Instruction message for the LLM
87
+ */
88
+ function getBinaryFileInstructions(filePath, mimeType) {
89
+ const fileName = path.basename(filePath);
90
+ return `Cannot read binary file as text: ${fileName} (${mimeType})
91
+
92
+ Use start_process + interact_with_process to analyze binary files with appropriate tools (Node.js or Python libraries, command-line utilities, etc.).
93
+
94
+ The read_file tool only handles text files and images.`;
95
+ }
81
96
  // Initialize allowed directories from configuration
82
97
  async function getAllowedDirs() {
83
98
  try {
@@ -329,6 +344,16 @@ function generateEnhancedStatusMessage(readLines, offset, totalLines, isNegative
329
344
  async function readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true) {
330
345
  const stats = await fs.stat(filePath);
331
346
  const fileSize = stats.size;
347
+ // Check if the file is binary (but allow images to pass through)
348
+ const { isImage } = await getMimeTypeInfo(filePath);
349
+ if (!isImage) {
350
+ const isBinary = await isBinaryFile(filePath);
351
+ if (isBinary) {
352
+ // Return instructions instead of trying to read binary content
353
+ const instructions = getBinaryFileInstructions(filePath, mimeType);
354
+ throw new Error(instructions);
355
+ }
356
+ }
332
357
  // Get total line count for enhanced status messages (only for smaller files)
333
358
  const totalLines = await getFileLineCount(filePath);
334
359
  // For negative offsets (tail behavior), use reverse reading
@@ -574,10 +599,18 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
574
599
  return await readFileWithSmartPositioning(validPath, offset, length, mimeType, true);
575
600
  }
576
601
  catch (error) {
577
- // If UTF-8 reading fails, treat as binary and return base64 but still as text
578
- const buffer = await fs.readFile(validPath);
579
- const content = `Binary file content (base64 encoded):\n${buffer.toString('base64')}`;
580
- return { content, mimeType: 'text/plain', isImage: false };
602
+ // If it's our binary file instruction error, return it as content
603
+ if (error instanceof Error && error.message.includes('Cannot read binary file as text:')) {
604
+ return { content: error.message, mimeType: 'text/plain', isImage: false };
605
+ }
606
+ // If UTF-8 reading fails for other reasons, also check if it's binary
607
+ const isBinary = await isBinaryFile(validPath);
608
+ if (isBinary) {
609
+ const instructions = getBinaryFileInstructions(validPath, mimeType);
610
+ return { content: instructions, mimeType: 'text/plain', isImage: false };
611
+ }
612
+ // Only if it's truly not binary, then we have a real UTF-8 reading error
613
+ throw error;
581
614
  }
582
615
  }
583
616
  };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.6";
1
+ export declare const VERSION = "0.2.7";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.6';
1
+ export const VERSION = '0.2.7';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "license": "MIT",
6
6
  "author": "Eduards Ruzga",
@@ -71,6 +71,7 @@
71
71
  "cross-fetch": "^4.1.0",
72
72
  "fastest-levenshtein": "^1.0.16",
73
73
  "glob": "^10.3.10",
74
+ "isbinaryfile": "^5.0.4",
74
75
  "zod": "^3.24.1",
75
76
  "zod-to-json-schema": "^3.23.5"
76
77
  },
@@ -1,109 +0,0 @@
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 {};
@@ -1,364 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export {};