@hcu-lab.me/mcp-device 1.0.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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +22 -0
- package/dist/command-manager.d.ts +8 -0
- package/dist/command-manager.js +241 -0
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +102 -0
- package/dist/config-manager.js +291 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +10 -0
- package/dist/custom-stdio.d.ts +51 -0
- package/dist/custom-stdio.js +358 -0
- package/dist/data/official-app-ca.pem +12 -0
- package/dist/data/onboarding-prompts.json +70 -0
- package/dist/device/device-state.d.ts +13 -0
- package/dist/device/device-state.js +100 -0
- package/dist/device/device-status.d.ts +81 -0
- package/dist/device/device-status.js +156 -0
- package/dist/device/device.d.ts +16 -0
- package/dist/device/device.js +190 -0
- package/dist/device/execution-engine.d.ts +144 -0
- package/dist/device/execution-engine.js +195 -0
- package/dist/device/gateway-channel.d.ts +48 -0
- package/dist/device/gateway-channel.js +384 -0
- package/dist/device/gateway-config.d.ts +40 -0
- package/dist/device/gateway-config.js +207 -0
- package/dist/device/gateway-identity.d.ts +35 -0
- package/dist/device/gateway-identity.js +178 -0
- package/dist/device/gateway-pairing.d.ts +25 -0
- package/dist/device/gateway-pairing.js +105 -0
- package/dist/device/gateway-secure-transport.d.ts +25 -0
- package/dist/device/gateway-secure-transport.js +117 -0
- package/dist/device/gateway-tool-adapter.d.ts +17 -0
- package/dist/device/gateway-tool-adapter.js +284 -0
- package/dist/device/gateway-url-policy.d.ts +3 -0
- package/dist/device/gateway-url-policy.js +43 -0
- package/dist/device/linux-service.d.ts +70 -0
- package/dist/device/linux-service.js +268 -0
- package/dist/device/official-trust.d.ts +7 -0
- package/dist/device/official-trust.js +49 -0
- package/dist/device/project-inspection.d.ts +1 -0
- package/dist/device/project-inspection.js +180 -0
- package/dist/device/runtime-owner.d.ts +41 -0
- package/dist/device/runtime-owner.js +177 -0
- package/dist/device/windows-dpapi.d.ts +11 -0
- package/dist/device/windows-dpapi.js +67 -0
- package/dist/device/windows-service.d.ts +56 -0
- package/dist/device/windows-service.js +238 -0
- package/dist/error-handlers.d.ts +7 -0
- package/dist/error-handlers.js +15 -0
- package/dist/handlers/edit-search-handlers.d.ts +6 -0
- package/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/handlers/filesystem-handlers.d.ts +38 -0
- package/dist/handlers/filesystem-handlers.js +440 -0
- package/dist/handlers/history-handlers.d.ts +12 -0
- package/dist/handlers/history-handlers.js +67 -0
- package/dist/handlers/index.d.ts +6 -0
- package/dist/handlers/index.js +7 -0
- package/dist/handlers/process-handlers.d.ts +9 -0
- package/dist/handlers/process-handlers.js +15 -0
- package/dist/handlers/search-handlers.d.ts +17 -0
- package/dist/handlers/search-handlers.js +224 -0
- package/dist/handlers/terminal-handlers.d.ts +21 -0
- package/dist/handlers/terminal-handlers.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +138 -0
- package/dist/mcp-device.d.ts +2 -0
- package/dist/mcp-device.js +7 -0
- package/dist/npm-scripts/remote.d.ts +3 -0
- package/dist/npm-scripts/remote.js +314 -0
- package/dist/search-manager.d.ts +147 -0
- package/dist/search-manager.js +828 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +1563 -0
- package/dist/terminal-manager.d.ts +104 -0
- package/dist/terminal-manager.js +635 -0
- package/dist/tools/config.d.ts +96 -0
- package/dist/tools/config.js +260 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +435 -0
- package/dist/tools/feedback.d.ts +8 -0
- package/dist/tools/feedback.js +132 -0
- package/dist/tools/filesystem.d.ts +79 -0
- package/dist/tools/filesystem.js +947 -0
- package/dist/tools/fuzzySearch.d.ts +12 -0
- package/dist/tools/fuzzySearch.js +74 -0
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.d.ts +24 -0
- package/dist/tools/improved-process-tools.js +627 -0
- package/dist/tools/mime-types.d.ts +3 -0
- package/dist/tools/mime-types.js +28 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +99 -0
- package/dist/tools/pdf/markdown.d.ts +26 -0
- package/dist/tools/pdf/markdown.js +275 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +56 -0
- package/dist/tools/prompts.d.ts +28 -0
- package/dist/tools/prompts.js +259 -0
- package/dist/tools/schemas.d.ts +454 -0
- package/dist/tools/schemas.js +242 -0
- package/dist/tools/usage.d.ts +5 -0
- package/dist/tools/usage.js +24 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +1 -0
- package/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/ui/config-editor/index.html +13 -0
- package/dist/ui/config-editor/src/app.d.ts +43 -0
- package/dist/ui/config-editor/src/app.js +831 -0
- package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/src/array-modal.js +185 -0
- package/dist/ui/config-editor/src/main.d.ts +1 -0
- package/dist/ui/config-editor/src/main.js +2 -0
- package/dist/ui/config-editor/styles.css +587 -0
- package/dist/ui/contracts.d.ts +14 -0
- package/dist/ui/contracts.js +21 -0
- package/dist/ui/file-preview/index.html +16 -0
- package/dist/ui/file-preview/preview-runtime.js +469 -0
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
- package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/ui/file-preview/src/app.d.ts +4 -0
- package/dist/ui/file-preview/src/app.js +680 -0
- package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/src/components/highlighting.js +54 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
- package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
- package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
- package/dist/ui/file-preview/src/directory-controller.js +240 -0
- package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
- package/dist/ui/file-preview/src/document-layout.js +109 -0
- package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
- package/dist/ui/file-preview/src/document-outline.js +97 -0
- package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
- package/dist/ui/file-preview/src/document-workspace.js +33 -0
- package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
- package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
- package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
- package/dist/ui/file-preview/src/host/external-actions.js +55 -0
- package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
- package/dist/ui/file-preview/src/host/selection-context.js +106 -0
- package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/src/image-preview.js +21 -0
- package/dist/ui/file-preview/src/main.d.ts +1 -0
- package/dist/ui/file-preview/src/main.js +5 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
- package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
- package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
- package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
- package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
- package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
- package/dist/ui/file-preview/src/markdown/linking.js +228 -0
- package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
- package/dist/ui/file-preview/src/markdown/outline.js +16 -0
- package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
- package/dist/ui/file-preview/src/markdown/parser.js +38 -0
- package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/preview.js +20 -0
- package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
- package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
- package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/utils.js +15 -0
- package/dist/ui/file-preview/src/model.d.ts +35 -0
- package/dist/ui/file-preview/src/model.js +1 -0
- package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
- package/dist/ui/file-preview/src/panel-actions.js +182 -0
- package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
- package/dist/ui/file-preview/src/path-utils.js +64 -0
- package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
- package/dist/ui/file-preview/src/payload-utils.js +119 -0
- package/dist/ui/file-preview/src/types.d.ts +1 -0
- package/dist/ui/file-preview/src/types.js +1 -0
- package/dist/ui/file-preview/styles.css +1631 -0
- package/dist/ui/resources.d.ts +28 -0
- package/dist/ui/resources.js +86 -0
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/escape-html.d.ts +4 -0
- package/dist/ui/shared/escape-html.js +11 -0
- package/dist/ui/shared/host-context.d.ts +15 -0
- package/dist/ui/shared/host-context.js +51 -0
- package/dist/ui/shared/tool-bridge.d.ts +30 -0
- package/dist/ui/shared/tool-bridge.js +137 -0
- package/dist/ui/shared/tool-shell.d.ts +25 -0
- package/dist/ui/shared/tool-shell.js +107 -0
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/ui/shared/widget-state.d.ts +33 -0
- package/dist/ui/shared/widget-state.js +158 -0
- package/dist/utils/ab-test.d.ts +12 -0
- package/dist/utils/ab-test.js +104 -0
- package/dist/utils/capture.d.ts +35 -0
- package/dist/utils/capture.js +481 -0
- package/dist/utils/dockerPrompt.d.ts +17 -0
- package/dist/utils/dockerPrompt.js +53 -0
- package/dist/utils/feature-flags.d.ts +59 -0
- package/dist/utils/feature-flags.js +215 -0
- package/dist/utils/files/base.d.ts +173 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/docx.d.ts +47 -0
- package/dist/utils/files/docx.js +679 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +437 -0
- package/dist/utils/files/factory.d.ts +41 -0
- package/dist/utils/files/factory.js +113 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +370 -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/logger.d.ts +32 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +42 -0
- package/dist/utils/process-detection.d.ts +23 -0
- package/dist/utils/process-detection.js +150 -0
- package/dist/utils/ripgrep-resolver.d.ts +9 -0
- package/dist/utils/ripgrep-resolver.js +74 -0
- package/dist/utils/system-info.d.ts +71 -0
- package/dist/utils/system-info.js +712 -0
- package/dist/utils/toolHistory.d.ts +86 -0
- package/dist/utils/toolHistory.js +257 -0
- package/dist/utils/trackTools.d.ts +6 -0
- package/dist/utils/trackTools.js +54 -0
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/utils/usageTracker.d.ts +133 -0
- package/dist/utils/usageTracker.js +477 -0
- package/dist/utils/welcome-onboarding.d.ts +11 -0
- package/dist/utils/welcome-onboarding.js +99 -0
- package/dist/utils/withTimeout.d.ts +28 -0
- package/dist/utils/withTimeout.js +85 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
|
|
4
|
+
import { configManager } from './config-manager.js';
|
|
5
|
+
import { capture } from "./utils/capture.js";
|
|
6
|
+
import { analyzeProcessState } from './utils/process-detection.js';
|
|
7
|
+
/**
|
|
8
|
+
* Standard Windows PATHEXT value, used to repair a corrupted PATHEXT before
|
|
9
|
+
* spawning child shells.
|
|
10
|
+
*
|
|
11
|
+
* On some Windows Claude Desktop / DXT launches the server process inherits a
|
|
12
|
+
* broken PATHEXT (observed as ".CPL" only). Because we build the child env from
|
|
13
|
+
* { ...process.env }, that broken value would propagate into every spawned
|
|
14
|
+
* shell, stripping ".EXE" and breaking resolution of git / node / python / rg /
|
|
15
|
+
* etc. (and even full-path .exe invocations under PowerShell). See issue #481.
|
|
16
|
+
*/
|
|
17
|
+
const STANDARD_PATHEXT = '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC';
|
|
18
|
+
/**
|
|
19
|
+
* Return a healthy PATHEXT for spawned Windows shells.
|
|
20
|
+
* - Unset -> use the standard list.
|
|
21
|
+
* - Missing ".EXE" -> corrupted; merge the standard list with whatever was
|
|
22
|
+
* present (preserves any extra extensions, order-stable).
|
|
23
|
+
* - Otherwise -> leave the inherited value untouched.
|
|
24
|
+
*/
|
|
25
|
+
function getRepairedPathExt() {
|
|
26
|
+
const current = process.env.PATHEXT;
|
|
27
|
+
if (!current)
|
|
28
|
+
return STANDARD_PATHEXT;
|
|
29
|
+
const exts = current.split(';').map(e => e.trim().toUpperCase()).filter(Boolean);
|
|
30
|
+
if (!exts.includes('.EXE')) {
|
|
31
|
+
return [...new Set([...STANDARD_PATHEXT.split(';'), ...exts])].join(';');
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Output buffering caps. Without a cap, a process emitting enough output makes
|
|
37
|
+
* string concatenation throw "RangeError: Invalid string length" at V8's max
|
|
38
|
+
* string size (~536M chars) inside a stdout 'data' handler — an uncaught
|
|
39
|
+
* exception that kills the whole server (index.ts exits on uncaughtException).
|
|
40
|
+
* The cap also bounds the join() cost in snapshot reads and the periodic
|
|
41
|
+
* process-state scan, both of which are O(total output).
|
|
42
|
+
*/
|
|
43
|
+
export const MAX_BUFFERED_OUTPUT_CHARS = 50 * 1024 * 1024; // per session; oldest lines evicted first
|
|
44
|
+
const MAX_LINE_CHARS = 1024 * 1024; // force-split longer lines so eviction can work
|
|
45
|
+
const MAX_WAIT_OUTPUT_CHARS = 2 * 1024 * 1024; // start_process wait buffer (prompt/state detection)
|
|
46
|
+
/**
|
|
47
|
+
* Get the appropriate spawn configuration for a given shell
|
|
48
|
+
* This handles login shell flags for different shell types
|
|
49
|
+
*/
|
|
50
|
+
function getShellSpawnArgs(shellPath, command) {
|
|
51
|
+
const shellName = path.basename(shellPath).toLowerCase();
|
|
52
|
+
// Unix shells with login flag support
|
|
53
|
+
if (shellName.includes('bash') || shellName.includes('zsh')) {
|
|
54
|
+
return {
|
|
55
|
+
executable: shellPath,
|
|
56
|
+
args: ['-l', '-c', command],
|
|
57
|
+
useShellOption: false
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// PowerShell Core (cross-platform, supports -Login)
|
|
61
|
+
if (shellName === 'pwsh' || shellName === 'pwsh.exe') {
|
|
62
|
+
return {
|
|
63
|
+
executable: shellPath,
|
|
64
|
+
args: ['-Login', '-Command', command],
|
|
65
|
+
useShellOption: false
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Windows PowerShell 5.1 (no login flag support)
|
|
69
|
+
if (shellName === 'powershell' || shellName === 'powershell.exe') {
|
|
70
|
+
return {
|
|
71
|
+
executable: shellPath,
|
|
72
|
+
args: ['-NoProfile', '-NonInteractive', '-Command', command],
|
|
73
|
+
useShellOption: false
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// CMD
|
|
77
|
+
if (shellName === 'cmd' || shellName === 'cmd.exe') {
|
|
78
|
+
return {
|
|
79
|
+
executable: shellPath,
|
|
80
|
+
args: ['/c', command],
|
|
81
|
+
windowsVerbatim: true,
|
|
82
|
+
useShellOption: false
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Fish shell (uses -l for login, -c for command)
|
|
86
|
+
if (shellName.includes('fish')) {
|
|
87
|
+
return {
|
|
88
|
+
executable: shellPath,
|
|
89
|
+
args: ['-l', '-c', command],
|
|
90
|
+
useShellOption: false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Unknown/other shells - use shell option for safety
|
|
94
|
+
// This provides a fallback for shells we don't explicitly handle
|
|
95
|
+
return {
|
|
96
|
+
executable: command,
|
|
97
|
+
args: [],
|
|
98
|
+
useShellOption: shellPath
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export class TerminalManager {
|
|
102
|
+
constructor() {
|
|
103
|
+
this.sessions = new Map();
|
|
104
|
+
this.completedSessions = new Map();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Send input to a running process
|
|
108
|
+
* @param pid Process ID
|
|
109
|
+
* @param input Text to send to the process
|
|
110
|
+
* @returns Whether input was successfully sent
|
|
111
|
+
*/
|
|
112
|
+
sendInputToProcess(pid, input) {
|
|
113
|
+
const session = this.sessions.get(pid);
|
|
114
|
+
if (!session) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
if (session.process.stdin && !session.process.stdin.destroyed) {
|
|
119
|
+
// Ensure input ends with a newline for most REPLs
|
|
120
|
+
const inputWithNewline = input.endsWith('\n') ? input : input + '\n';
|
|
121
|
+
session.process.stdin.write(inputWithNewline);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error(`Error sending input to process ${pid}:`, error);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async executeCommand(command, timeoutMs = DEFAULT_COMMAND_TIMEOUT, shell, collectTiming = false, workingDirectory) {
|
|
132
|
+
// Get the shell from config if not specified
|
|
133
|
+
let shellToUse = shell;
|
|
134
|
+
if (!shellToUse) {
|
|
135
|
+
try {
|
|
136
|
+
const config = await configManager.getConfig();
|
|
137
|
+
shellToUse = config.defaultShell || true;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// If there's an error getting the config, fall back to default
|
|
141
|
+
shellToUse = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// For REPL interactions, we need to ensure stdin, stdout, and stderr are properly configured
|
|
145
|
+
// Note: No special stdio options needed here, Node.js handles pipes by default
|
|
146
|
+
// Enhance SSH commands automatically
|
|
147
|
+
let enhancedCommand = command;
|
|
148
|
+
if (command.trim().startsWith('ssh ') && !command.includes(' -t')) {
|
|
149
|
+
enhancedCommand = command.replace(/^ssh /, 'ssh -t ');
|
|
150
|
+
console.log(`Enhanced SSH command: ${enhancedCommand}`);
|
|
151
|
+
}
|
|
152
|
+
// Get the appropriate spawn configuration for the shell
|
|
153
|
+
let spawnConfig;
|
|
154
|
+
let spawnOptions;
|
|
155
|
+
if (typeof shellToUse === 'string') {
|
|
156
|
+
// Use shell-specific configuration with login flags where appropriate
|
|
157
|
+
spawnConfig = getShellSpawnArgs(shellToUse, enhancedCommand);
|
|
158
|
+
spawnOptions = {
|
|
159
|
+
env: {
|
|
160
|
+
...process.env,
|
|
161
|
+
TERM: 'xterm-256color' // Better terminal compatibility
|
|
162
|
+
},
|
|
163
|
+
windowsHide: true, // Prevent visible console windows on Windows
|
|
164
|
+
...(workingDirectory ? { cwd: workingDirectory } : {})
|
|
165
|
+
};
|
|
166
|
+
// Add shell option if needed (for unknown shells)
|
|
167
|
+
if (spawnConfig.useShellOption) {
|
|
168
|
+
spawnOptions.shell = spawnConfig.useShellOption;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// Boolean or undefined shell - use default shell option behavior
|
|
173
|
+
spawnConfig = {
|
|
174
|
+
executable: enhancedCommand,
|
|
175
|
+
args: [],
|
|
176
|
+
useShellOption: shellToUse
|
|
177
|
+
};
|
|
178
|
+
spawnOptions = {
|
|
179
|
+
shell: shellToUse,
|
|
180
|
+
env: {
|
|
181
|
+
...process.env,
|
|
182
|
+
TERM: 'xterm-256color'
|
|
183
|
+
},
|
|
184
|
+
windowsHide: true, // Prevent visible console windows on Windows
|
|
185
|
+
...(workingDirectory ? { cwd: workingDirectory } : {})
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// Repair PATHEXT on Windows before spawning. On some Windows DXT launches
|
|
189
|
+
// the server process inherits a corrupted PATHEXT (e.g. ".CPL"), which we
|
|
190
|
+
// would otherwise propagate via { ...process.env } and break command
|
|
191
|
+
// resolution (git, node, python, rg, ...) in the spawned shell. See #481.
|
|
192
|
+
if (process.platform === 'win32' && spawnOptions.env) {
|
|
193
|
+
spawnOptions.env.PATHEXT = getRepairedPathExt();
|
|
194
|
+
}
|
|
195
|
+
// On Windows, when we invoke cmd.exe directly and pass the user's command as a
|
|
196
|
+
// single argument, Node/libuv applies MSVCRT-style quoting that escapes embedded
|
|
197
|
+
// double quotes as \" . cmd.exe does not understand that escaping, so any command
|
|
198
|
+
// containing quotes (e.g. a quoted path with spaces like "C:\Program Files\app.exe")
|
|
199
|
+
// is corrupted before the shell ever parses it. Passing arguments verbatim lets
|
|
200
|
+
// cmd handle its own quoting. Scoped to shells that set windowsVerbatim (cmd only)
|
|
201
|
+
// because PowerShell/pwsh have different quote rules and must NOT use verbatim.
|
|
202
|
+
if (process.platform === 'win32' && spawnConfig.windowsVerbatim) {
|
|
203
|
+
spawnOptions.windowsVerbatimArguments = true;
|
|
204
|
+
}
|
|
205
|
+
// Spawn the process with appropriate arguments
|
|
206
|
+
const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions);
|
|
207
|
+
let output = '';
|
|
208
|
+
// Ensure childProcess.pid is defined before proceeding
|
|
209
|
+
if (!childProcess.pid) {
|
|
210
|
+
// Return a consistent error object instead of throwing
|
|
211
|
+
return {
|
|
212
|
+
pid: -1, // Use -1 to indicate an error state
|
|
213
|
+
output: 'Error: Failed to get process ID. The command could not be executed.',
|
|
214
|
+
isBlocked: false
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const session = {
|
|
218
|
+
pid: childProcess.pid,
|
|
219
|
+
process: childProcess,
|
|
220
|
+
outputLines: [], // Line-based buffer
|
|
221
|
+
lastReadIndex: 0, // Track where "new" output starts
|
|
222
|
+
isBlocked: false,
|
|
223
|
+
startTime: new Date(),
|
|
224
|
+
bufferedChars: 0,
|
|
225
|
+
evictedLines: 0,
|
|
226
|
+
evictedChars: 0
|
|
227
|
+
};
|
|
228
|
+
this.sessions.set(childProcess.pid, session);
|
|
229
|
+
// Timing telemetry
|
|
230
|
+
const startTime = Date.now();
|
|
231
|
+
let firstOutputTime;
|
|
232
|
+
let lastOutputTime;
|
|
233
|
+
const outputEvents = [];
|
|
234
|
+
let exitReason = 'timeout';
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
let resolved = false;
|
|
237
|
+
let periodicCheck = null;
|
|
238
|
+
// Quick prompt patterns for immediate detection
|
|
239
|
+
const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/;
|
|
240
|
+
const resolveOnce = (result) => {
|
|
241
|
+
if (resolved)
|
|
242
|
+
return;
|
|
243
|
+
resolved = true;
|
|
244
|
+
if (periodicCheck)
|
|
245
|
+
clearInterval(periodicCheck);
|
|
246
|
+
// Add timing info if requested
|
|
247
|
+
if (collectTiming) {
|
|
248
|
+
const endTime = Date.now();
|
|
249
|
+
result.timingInfo = {
|
|
250
|
+
startTime,
|
|
251
|
+
endTime,
|
|
252
|
+
totalDurationMs: endTime - startTime,
|
|
253
|
+
exitReason,
|
|
254
|
+
firstOutputTime,
|
|
255
|
+
lastOutputTime,
|
|
256
|
+
timeToFirstOutputMs: firstOutputTime ? firstOutputTime - startTime : undefined,
|
|
257
|
+
outputEvents: outputEvents.length > 0 ? outputEvents : undefined
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
resolve(result);
|
|
261
|
+
};
|
|
262
|
+
childProcess.stdout.on('data', (data) => {
|
|
263
|
+
const text = data.toString();
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
if (!firstOutputTime)
|
|
266
|
+
firstOutputTime = now;
|
|
267
|
+
lastOutputTime = now;
|
|
268
|
+
// `output` only feeds the wait-phase result and prompt/state detection,
|
|
269
|
+
// so stop growing it once resolved and keep only a bounded tail.
|
|
270
|
+
if (!resolved) {
|
|
271
|
+
output += text;
|
|
272
|
+
if (output.length > MAX_WAIT_OUTPUT_CHARS) {
|
|
273
|
+
output = output.slice(-Math.floor(MAX_WAIT_OUTPUT_CHARS / 2));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Append to line-based buffer
|
|
277
|
+
this.appendToLineBuffer(session, text);
|
|
278
|
+
// Record output event if collecting timing
|
|
279
|
+
if (collectTiming) {
|
|
280
|
+
outputEvents.push({
|
|
281
|
+
timestamp: now,
|
|
282
|
+
deltaMs: now - startTime,
|
|
283
|
+
source: 'stdout',
|
|
284
|
+
length: text.length,
|
|
285
|
+
snippet: text.slice(0, 50).replace(/\n/g, '\\n')
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// Immediate check for obvious prompts
|
|
289
|
+
if (quickPromptPatterns.test(text)) {
|
|
290
|
+
session.isBlocked = true;
|
|
291
|
+
exitReason = 'early_exit_quick_pattern';
|
|
292
|
+
if (collectTiming && outputEvents.length > 0) {
|
|
293
|
+
outputEvents[outputEvents.length - 1].matchedPattern = 'quick_pattern';
|
|
294
|
+
}
|
|
295
|
+
resolveOnce({
|
|
296
|
+
pid: childProcess.pid,
|
|
297
|
+
output,
|
|
298
|
+
isBlocked: true
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
childProcess.stderr.on('data', (data) => {
|
|
303
|
+
const text = data.toString();
|
|
304
|
+
const now = Date.now();
|
|
305
|
+
if (!firstOutputTime)
|
|
306
|
+
firstOutputTime = now;
|
|
307
|
+
lastOutputTime = now;
|
|
308
|
+
if (!resolved) {
|
|
309
|
+
output += text;
|
|
310
|
+
if (output.length > MAX_WAIT_OUTPUT_CHARS) {
|
|
311
|
+
output = output.slice(-Math.floor(MAX_WAIT_OUTPUT_CHARS / 2));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Append to line-based buffer
|
|
315
|
+
this.appendToLineBuffer(session, text);
|
|
316
|
+
// Record output event if collecting timing
|
|
317
|
+
if (collectTiming) {
|
|
318
|
+
outputEvents.push({
|
|
319
|
+
timestamp: now,
|
|
320
|
+
deltaMs: now - startTime,
|
|
321
|
+
source: 'stderr',
|
|
322
|
+
length: text.length,
|
|
323
|
+
snippet: text.slice(0, 50).replace(/\n/g, '\\n')
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
// Periodic comprehensive check every 100ms
|
|
328
|
+
periodicCheck = setInterval(() => {
|
|
329
|
+
if (output.trim()) {
|
|
330
|
+
const processState = analyzeProcessState(output, childProcess.pid);
|
|
331
|
+
if (processState.isWaitingForInput) {
|
|
332
|
+
session.isBlocked = true;
|
|
333
|
+
exitReason = 'early_exit_periodic_check';
|
|
334
|
+
resolveOnce({
|
|
335
|
+
pid: childProcess.pid,
|
|
336
|
+
output,
|
|
337
|
+
isBlocked: true
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}, 100);
|
|
342
|
+
// Timeout fallback
|
|
343
|
+
setTimeout(() => {
|
|
344
|
+
session.isBlocked = true;
|
|
345
|
+
exitReason = 'timeout';
|
|
346
|
+
resolveOnce({
|
|
347
|
+
pid: childProcess.pid,
|
|
348
|
+
output,
|
|
349
|
+
isBlocked: true
|
|
350
|
+
});
|
|
351
|
+
}, timeoutMs);
|
|
352
|
+
childProcess.on('exit', (code) => {
|
|
353
|
+
if (childProcess.pid) {
|
|
354
|
+
// Store completed session before removing active session
|
|
355
|
+
this.completedSessions.set(childProcess.pid, {
|
|
356
|
+
pid: childProcess.pid,
|
|
357
|
+
outputLines: [...session.outputLines], // Copy line buffer
|
|
358
|
+
exitCode: code,
|
|
359
|
+
startTime: session.startTime,
|
|
360
|
+
endTime: new Date(),
|
|
361
|
+
evictedLines: session.evictedLines,
|
|
362
|
+
evictedChars: session.evictedChars
|
|
363
|
+
});
|
|
364
|
+
// Keep only last 100 completed sessions
|
|
365
|
+
if (this.completedSessions.size > 100) {
|
|
366
|
+
const oldestKey = Array.from(this.completedSessions.keys())[0];
|
|
367
|
+
this.completedSessions.delete(oldestKey);
|
|
368
|
+
}
|
|
369
|
+
this.sessions.delete(childProcess.pid);
|
|
370
|
+
}
|
|
371
|
+
exitReason = 'process_exit';
|
|
372
|
+
resolveOnce({
|
|
373
|
+
pid: childProcess.pid,
|
|
374
|
+
output,
|
|
375
|
+
isBlocked: false
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Append text to a session's line buffer
|
|
382
|
+
* Handles partial lines and newline splitting
|
|
383
|
+
*/
|
|
384
|
+
appendToLineBuffer(session, text) {
|
|
385
|
+
if (!text)
|
|
386
|
+
return;
|
|
387
|
+
// Split text into lines, keeping track of whether text ends with newline
|
|
388
|
+
const lines = text.split('\n');
|
|
389
|
+
for (let i = 0; i < lines.length; i++) {
|
|
390
|
+
const line = lines[i];
|
|
391
|
+
const isLastFragment = i === lines.length - 1;
|
|
392
|
+
const endsWithNewline = text.endsWith('\n');
|
|
393
|
+
if (session.outputLines.length === 0) {
|
|
394
|
+
// First line ever
|
|
395
|
+
session.outputLines.push(line);
|
|
396
|
+
}
|
|
397
|
+
else if (i === 0) {
|
|
398
|
+
// First fragment - append to last line (might be partial)
|
|
399
|
+
session.outputLines[session.outputLines.length - 1] += line;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// Subsequent lines - add as new lines
|
|
403
|
+
session.outputLines.push(line);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Appended text contributes exactly its length to the joined buffer
|
|
407
|
+
// (its newlines become the join separators).
|
|
408
|
+
session.bufferedChars += text.length;
|
|
409
|
+
// A process printing without newlines grows a single line forever, which
|
|
410
|
+
// eviction can't bound — force-split so no line exceeds MAX_LINE_CHARS.
|
|
411
|
+
// Each inserted break adds one separator to the joined length.
|
|
412
|
+
let lastIndex = session.outputLines.length - 1;
|
|
413
|
+
while (session.outputLines[lastIndex].length > MAX_LINE_CHARS) {
|
|
414
|
+
const overlong = session.outputLines[lastIndex];
|
|
415
|
+
session.outputLines[lastIndex] = overlong.slice(0, MAX_LINE_CHARS);
|
|
416
|
+
session.outputLines.push(overlong.slice(MAX_LINE_CHARS));
|
|
417
|
+
session.bufferedChars += 1;
|
|
418
|
+
lastIndex++;
|
|
419
|
+
}
|
|
420
|
+
// Enforce the per-session cap by evicting the oldest lines. Keeps the
|
|
421
|
+
// buffer far below V8's max string length so concatenation and join()
|
|
422
|
+
// can never throw "Invalid string length" and kill the server.
|
|
423
|
+
while (session.bufferedChars > MAX_BUFFERED_OUTPUT_CHARS && session.outputLines.length > 1) {
|
|
424
|
+
const dropped = session.outputLines.shift();
|
|
425
|
+
const droppedJoinedChars = dropped.length + 1; // +1 for its join separator
|
|
426
|
+
session.bufferedChars -= droppedJoinedChars;
|
|
427
|
+
session.evictedChars += droppedJoinedChars;
|
|
428
|
+
session.evictedLines++;
|
|
429
|
+
if (session.lastReadIndex > 0)
|
|
430
|
+
session.lastReadIndex--;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Read process output with pagination (like file reading)
|
|
435
|
+
* @param pid Process ID
|
|
436
|
+
* @param offset Line offset: 0=from lastReadIndex, positive=absolute, negative=tail
|
|
437
|
+
* @param length Max lines to return
|
|
438
|
+
* @param updateReadIndex Whether to update lastReadIndex (default: true for offset=0)
|
|
439
|
+
*/
|
|
440
|
+
readOutputPaginated(pid, offset = 0, length = 1000) {
|
|
441
|
+
// First check active sessions
|
|
442
|
+
const session = this.sessions.get(pid);
|
|
443
|
+
if (session) {
|
|
444
|
+
const result = this.readFromLineBuffer(session.outputLines, offset, length, session.lastReadIndex, (newIndex) => { session.lastReadIndex = newIndex; }, false, undefined);
|
|
445
|
+
result.evictedLines = session.evictedLines;
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
// Then check completed sessions
|
|
449
|
+
const completedSession = this.completedSessions.get(pid);
|
|
450
|
+
if (completedSession) {
|
|
451
|
+
const runtimeMs = completedSession.endTime.getTime() - completedSession.startTime.getTime();
|
|
452
|
+
const result = this.readFromLineBuffer(completedSession.outputLines, offset, length, 0, // Completed sessions don't track read position
|
|
453
|
+
() => { }, // No-op for completed sessions
|
|
454
|
+
true, completedSession.exitCode, runtimeMs);
|
|
455
|
+
result.evictedLines = completedSession.evictedLines;
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Internal helper to read from a line buffer with offset/length
|
|
462
|
+
*/
|
|
463
|
+
readFromLineBuffer(lines, offset, length, lastReadIndex, updateLastRead, isComplete, exitCode, runtimeMs) {
|
|
464
|
+
const totalLines = lines.length;
|
|
465
|
+
let startIndex;
|
|
466
|
+
let linesToRead;
|
|
467
|
+
if (offset < 0) {
|
|
468
|
+
// Negative offset = start position from end, then read 'length' lines forward
|
|
469
|
+
// e.g., offset=-50, length=10 means: start 50 lines from end, read 10 lines
|
|
470
|
+
const fromEnd = Math.abs(offset);
|
|
471
|
+
startIndex = Math.max(0, totalLines - fromEnd);
|
|
472
|
+
linesToRead = lines.slice(startIndex, startIndex + length);
|
|
473
|
+
// Don't update lastReadIndex for tail reads
|
|
474
|
+
}
|
|
475
|
+
else if (offset === 0) {
|
|
476
|
+
// offset=0 means "from where I last read" (like getNewOutput)
|
|
477
|
+
startIndex = lastReadIndex;
|
|
478
|
+
linesToRead = lines.slice(startIndex, startIndex + length);
|
|
479
|
+
// Update lastReadIndex for "new output" behavior
|
|
480
|
+
updateLastRead(Math.min(startIndex + linesToRead.length, totalLines));
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// Positive offset = absolute position
|
|
484
|
+
startIndex = offset;
|
|
485
|
+
linesToRead = lines.slice(startIndex, startIndex + length);
|
|
486
|
+
// Don't update lastReadIndex for absolute position reads
|
|
487
|
+
}
|
|
488
|
+
const readCount = linesToRead.length;
|
|
489
|
+
const endIndex = startIndex + readCount;
|
|
490
|
+
const remaining = Math.max(0, totalLines - endIndex);
|
|
491
|
+
return {
|
|
492
|
+
lines: linesToRead,
|
|
493
|
+
totalLines,
|
|
494
|
+
readFrom: startIndex,
|
|
495
|
+
readCount,
|
|
496
|
+
remaining,
|
|
497
|
+
isComplete,
|
|
498
|
+
exitCode,
|
|
499
|
+
runtimeMs
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Get total line count for a process
|
|
504
|
+
*/
|
|
505
|
+
getOutputLineCount(pid) {
|
|
506
|
+
const session = this.sessions.get(pid);
|
|
507
|
+
if (session) {
|
|
508
|
+
return session.outputLines.length;
|
|
509
|
+
}
|
|
510
|
+
const completedSession = this.completedSessions.get(pid);
|
|
511
|
+
if (completedSession) {
|
|
512
|
+
return completedSession.outputLines.length;
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Legacy method for backward compatibility
|
|
518
|
+
* Returns all new output since last read
|
|
519
|
+
* @param maxLines Maximum lines to return (default: 1000 for context protection)
|
|
520
|
+
* @deprecated Use readOutputPaginated instead
|
|
521
|
+
*/
|
|
522
|
+
getNewOutput(pid, maxLines = 1000) {
|
|
523
|
+
const result = this.readOutputPaginated(pid, 0, maxLines);
|
|
524
|
+
if (!result)
|
|
525
|
+
return null;
|
|
526
|
+
const output = result.lines.join('\n').trim();
|
|
527
|
+
// For completed sessions, append completion info with runtime
|
|
528
|
+
if (result.isComplete) {
|
|
529
|
+
const runtimeStr = result.runtimeMs !== undefined
|
|
530
|
+
? `\nRuntime: ${(result.runtimeMs / 1000).toFixed(2)}s`
|
|
531
|
+
: '';
|
|
532
|
+
if (output) {
|
|
533
|
+
return `${output}\n\nProcess completed with exit code ${result.exitCode}${runtimeStr}`;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
return `Process completed with exit code ${result.exitCode}${runtimeStr}\n(No output produced)`;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Add truncation warning if there's more output
|
|
540
|
+
if (result.remaining > 0) {
|
|
541
|
+
return `${output}\n\n[Output truncated: ${result.remaining} more lines available. Use read_process_output with offset/length for full output.]`;
|
|
542
|
+
}
|
|
543
|
+
return output || null;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Capture a snapshot of current output state for interaction tracking.
|
|
547
|
+
* Used by interactWithProcess to know what output existed before sending input.
|
|
548
|
+
*/
|
|
549
|
+
captureOutputSnapshot(pid) {
|
|
550
|
+
const session = this.sessions.get(pid);
|
|
551
|
+
if (session) {
|
|
552
|
+
const fullOutput = session.outputLines.join('\n');
|
|
553
|
+
return {
|
|
554
|
+
// Absolute since process start (includes evicted output), so the
|
|
555
|
+
// offset stays valid even if the cap evicts lines between
|
|
556
|
+
// snapshot and read.
|
|
557
|
+
totalChars: session.evictedChars + fullOutput.length,
|
|
558
|
+
lineCount: session.evictedLines + session.outputLines.length
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get output that appeared since a snapshot was taken.
|
|
565
|
+
* This handles the case where output is appended to the last line (REPL prompts).
|
|
566
|
+
* Also checks completed sessions in case process finished between snapshot and poll.
|
|
567
|
+
*/
|
|
568
|
+
getOutputSinceSnapshot(pid, snapshot) {
|
|
569
|
+
// Check active session first
|
|
570
|
+
const session = this.sessions.get(pid);
|
|
571
|
+
if (session) {
|
|
572
|
+
return TerminalManager.outputSinceSnapshot(session.outputLines, session.evictedChars, snapshot.totalChars);
|
|
573
|
+
}
|
|
574
|
+
// Fallback to completed sessions - process may have finished between snapshot and poll
|
|
575
|
+
const completedSession = this.completedSessions.get(pid);
|
|
576
|
+
if (completedSession) {
|
|
577
|
+
return TerminalManager.outputSinceSnapshot(completedSession.outputLines, completedSession.evictedChars, snapshot.totalChars);
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* New output since a snapshot, in absolute (since process start) offsets.
|
|
583
|
+
* If eviction dropped part of the unseen output, returns what the buffer
|
|
584
|
+
* still holds — the oldest unseen chars are lost to the cap.
|
|
585
|
+
*/
|
|
586
|
+
static outputSinceSnapshot(outputLines, evictedChars, snapshotTotalChars) {
|
|
587
|
+
const fullOutput = outputLines.join('\n');
|
|
588
|
+
const newChars = evictedChars + fullOutput.length - snapshotTotalChars;
|
|
589
|
+
if (newChars <= 0) {
|
|
590
|
+
return ''; // No new output
|
|
591
|
+
}
|
|
592
|
+
return fullOutput.substring(Math.max(0, fullOutput.length - newChars));
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Get a session by PID
|
|
596
|
+
* @param pid Process ID
|
|
597
|
+
* @returns The session or undefined if not found
|
|
598
|
+
*/
|
|
599
|
+
getSession(pid) {
|
|
600
|
+
return this.sessions.get(pid);
|
|
601
|
+
}
|
|
602
|
+
forceTerminate(pid) {
|
|
603
|
+
const session = this.sessions.get(pid);
|
|
604
|
+
if (!session) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
session.process.kill('SIGINT');
|
|
609
|
+
setTimeout(() => {
|
|
610
|
+
if (this.sessions.has(pid)) {
|
|
611
|
+
session.process.kill('SIGKILL');
|
|
612
|
+
}
|
|
613
|
+
}, 1000);
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
// Convert error to string, handling both Error objects and other types
|
|
618
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
619
|
+
capture('server_request_error', { error: errorMessage, message: `Failed to terminate process ${pid}:` });
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
listActiveSessions() {
|
|
624
|
+
const now = new Date();
|
|
625
|
+
return Array.from(this.sessions.values()).map(session => ({
|
|
626
|
+
pid: session.pid,
|
|
627
|
+
isBlocked: session.isBlocked,
|
|
628
|
+
runtime: now.getTime() - session.startTime.getTime()
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
listCompletedSessions() {
|
|
632
|
+
return Array.from(this.completedSessions.values());
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
export const terminalManager = new TerminalManager();
|