@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.
- package/dist/index-dxt.js +10 -47
- package/dist/server.js +6 -0
- package/dist/tools/filesystem.js +37 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/dist/REPLSessionManager.d.ts +0 -109
- package/dist/REPLSessionManager.js +0 -364
- package/dist/REPLSessionManager.test.d.ts +0 -1
- package/dist/REPLSessionManager.test.js +0 -75
- package/dist/client/replClient.d.ts +0 -63
- package/dist/client/replClient.js +0 -217
- package/dist/client/sshClient.d.ts +0 -82
- package/dist/client/sshClient.js +0 -200
- package/dist/command-manager.js.map +0 -1
- package/dist/config-manager.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/custom-stdio.js.map +0 -1
- package/dist/error-handlers.js.map +0 -1
- package/dist/handlers/command-handlers.d.ts +0 -13
- package/dist/handlers/command-handlers.js +0 -43
- package/dist/handlers/edit-search-handlers.js.map +0 -1
- package/dist/handlers/filesystem-handlers.js.map +0 -1
- package/dist/handlers/fuzzy-search-log-handlers.d.ts +0 -13
- package/dist/handlers/fuzzy-search-log-handlers.js +0 -179
- package/dist/handlers/index.js.map +0 -1
- package/dist/handlers/process-handlers.js.map +0 -1
- package/dist/handlers/repl-handlers.d.ts +0 -21
- package/dist/handlers/repl-handlers.js +0 -37
- package/dist/handlers/replCommandHandler.d.ts +0 -125
- package/dist/handlers/replCommandHandler.js +0 -255
- package/dist/handlers/replCommandHandler.test.d.ts +0 -1
- package/dist/handlers/replCommandHandler.test.js +0 -103
- package/dist/handlers/terminal-handlers.js.map +0 -1
- package/dist/index-with-startup-detection.d.ts +0 -5
- package/dist/index-with-startup-detection.js +0 -180
- package/dist/index.js.map +0 -1
- package/dist/logging.d.ts +0 -2
- package/dist/logging.js +0 -28
- package/dist/polyform-license-src/edit/edit.d.ts +0 -15
- package/dist/polyform-license-src/edit/edit.js +0 -163
- package/dist/polyform-license-src/edit/fuzzySearch.d.ts +0 -30
- package/dist/polyform-license-src/edit/fuzzySearch.js +0 -121
- package/dist/polyform-license-src/edit/handlers.d.ts +0 -16
- package/dist/polyform-license-src/edit/handlers.js +0 -24
- package/dist/polyform-license-src/edit/index.d.ts +0 -12
- package/dist/polyform-license-src/edit/index.js +0 -13
- package/dist/polyform-license-src/edit/schemas.d.ts +0 -25
- package/dist/polyform-license-src/edit/schemas.js +0 -16
- package/dist/polyform-license-src/index.d.ts +0 -9
- package/dist/polyform-license-src/index.js +0 -10
- package/dist/repl-manager.d.ts +0 -73
- package/dist/repl-manager.js +0 -407
- package/dist/replIntegration.d.ts +0 -14
- package/dist/replIntegration.js +0 -27
- package/dist/sandbox/index.d.ts +0 -9
- package/dist/sandbox/index.js +0 -50
- package/dist/sandbox/mac-sandbox.d.ts +0 -19
- package/dist/sandbox/mac-sandbox.js +0 -174
- package/dist/server.js.map +0 -1
- package/dist/setup.log +0 -32
- package/dist/terminal-manager.js.map +0 -1
- package/dist/tools/client.d.ts +0 -10
- package/dist/tools/client.js +0 -13
- package/dist/tools/command-block.d.ts +0 -18
- package/dist/tools/command-block.js +0 -62
- package/dist/tools/config.js.map +0 -1
- package/dist/tools/debug-path.d.ts +0 -1
- package/dist/tools/debug-path.js +0 -44
- package/dist/tools/edit.js.map +0 -1
- package/dist/tools/enhanced-read-output.js +0 -69
- package/dist/tools/enhanced-send-input.js +0 -111
- package/dist/tools/environment.d.ts +0 -55
- package/dist/tools/environment.js +0 -65
- package/dist/tools/execute.d.ts +0 -10
- package/dist/tools/execute.js +0 -158
- package/dist/tools/execute.js.map +0 -1
- package/dist/tools/filesystem-fixed.d.ts +0 -22
- package/dist/tools/filesystem-fixed.js +0 -176
- package/dist/tools/filesystem.js.map +0 -1
- package/dist/tools/fuzzySearch.js.map +0 -1
- package/dist/tools/mime-types.js.map +0 -1
- package/dist/tools/pdf-reader.d.ts +0 -13
- package/dist/tools/pdf-reader.js +0 -214
- package/dist/tools/process.js.map +0 -1
- package/dist/tools/progress.d.ts +0 -20
- package/dist/tools/progress.js +0 -59
- package/dist/tools/repl.d.ts +0 -21
- package/dist/tools/repl.js +0 -217
- package/dist/tools/schemas.js.map +0 -1
- package/dist/tools/search.js.map +0 -1
- package/dist/tools/send-input.d.ts +0 -2
- package/dist/tools/send-input.js +0 -45
- package/dist/types.js.map +0 -1
- package/dist/utils/capture.js.map +0 -1
- package/dist/utils/early-logger.d.ts +0 -4
- package/dist/utils/early-logger.js +0 -35
- package/dist/utils/fuzzySearchLogger.js.map +0 -1
- package/dist/utils/lineEndingHandler.js.map +0 -1
- package/dist/utils/lineEndingHandler_optimized.d.ts +0 -21
- package/dist/utils/lineEndingHandler_optimized.js +0 -77
- package/dist/utils/mcp-logger.d.ts +0 -30
- package/dist/utils/mcp-logger.js +0 -59
- package/dist/utils/smithery-detector.d.ts +0 -94
- package/dist/utils/smithery-detector.js +0 -292
- package/dist/utils/startup-detector.d.ts +0 -65
- package/dist/utils/startup-detector.js +0 -390
- package/dist/utils/trackTools.js.map +0 -1
- package/dist/utils/withTimeout.js.map +0 -1
- package/dist/utils.d.ts +0 -26
- package/dist/utils.js +0 -227
- 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("
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -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
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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.
|
|
1
|
+
export declare const VERSION = "0.2.7";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
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.
|
|
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 {};
|