starmux 0.12.0 ā 0.13.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/dist/src/cli/mac-mcp-gateway.js +4 -3
- package/dist/src/cli/mac-mcp-gateway.js.map +1 -1
- package/dist/src/cli/starmux.js +1 -1
- package/dist/src/cli/starmux.js.map +1 -1
- package/dist/src/runtime-paths.js +3 -3
- package/dist/src/runtime-paths.js.map +1 -1
- package/dist/vendor/desktop-commander/LICENSE +21 -0
- package/dist/vendor/desktop-commander/dist/bootstrap.js +22 -0
- package/dist/vendor/desktop-commander/dist/command-manager.js +246 -0
- package/dist/vendor/desktop-commander/dist/config-field-definitions.js +37 -0
- package/dist/vendor/desktop-commander/dist/config-manager.js +634 -0
- package/dist/vendor/desktop-commander/dist/config-recovery.js +81 -0
- package/dist/vendor/desktop-commander/dist/config.js +19 -0
- package/dist/vendor/desktop-commander/dist/custom-stdio.js +358 -0
- package/dist/vendor/desktop-commander/dist/error-handlers.js +15 -0
- package/dist/vendor/desktop-commander/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/vendor/desktop-commander/dist/handlers/filesystem-handlers.js +450 -0
- package/dist/vendor/desktop-commander/dist/handlers/history-handlers.js +67 -0
- package/dist/vendor/desktop-commander/dist/handlers/index.js +7 -0
- package/dist/vendor/desktop-commander/dist/handlers/process-handlers.js +15 -0
- package/dist/vendor/desktop-commander/dist/handlers/search-answers.js +172 -0
- package/dist/vendor/desktop-commander/dist/handlers/search-handlers.js +136 -0
- package/dist/vendor/desktop-commander/dist/handlers/terminal-handlers.js +35 -0
- package/dist/vendor/desktop-commander/dist/index.js +88 -0
- package/dist/vendor/desktop-commander/dist/search-manager.js +1262 -0
- package/dist/vendor/desktop-commander/dist/server.js +1414 -0
- package/dist/vendor/desktop-commander/dist/terminal-manager.js +757 -0
- package/dist/vendor/desktop-commander/dist/tools/config.js +211 -0
- package/dist/vendor/desktop-commander/dist/tools/edit.js +455 -0
- package/dist/vendor/desktop-commander/dist/tools/feedback.js +10 -0
- package/dist/vendor/desktop-commander/dist/tools/filesystem.js +1047 -0
- package/dist/vendor/desktop-commander/dist/tools/fuzzySearch.js +74 -0
- package/dist/vendor/desktop-commander/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/vendor/desktop-commander/dist/tools/improved-process-tools.js +726 -0
- package/dist/vendor/desktop-commander/dist/tools/mime-types.js +28 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/extract-images.js +131 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/index.js +4 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/lib/pdf2md.js +78 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/manipulations.js +112 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/markdown.js +30 -0
- package/dist/vendor/desktop-commander/dist/tools/pdf/utils.js +45 -0
- package/dist/vendor/desktop-commander/dist/tools/process.js +90 -0
- package/dist/vendor/desktop-commander/dist/tools/prompts.js +259 -0
- package/dist/vendor/desktop-commander/dist/tools/schemas.js +242 -0
- package/dist/vendor/desktop-commander/dist/tools/usage.js +24 -0
- package/dist/vendor/desktop-commander/dist/types.js +1 -0
- package/dist/vendor/desktop-commander/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/vendor/desktop-commander/dist/ui/config-editor/index.html +13 -0
- package/dist/vendor/desktop-commander/dist/ui/config-editor/styles.css +587 -0
- package/dist/vendor/desktop-commander/dist/ui/contracts.js +57 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/index.html +16 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/preview-runtime.js +471 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/vendor/desktop-commander/dist/ui/file-preview/styles.css +1631 -0
- package/dist/vendor/desktop-commander/dist/ui/resources.js +93 -0
- package/dist/vendor/desktop-commander/dist/utils/ab-test.js +122 -0
- package/dist/vendor/desktop-commander/dist/utils/atomic-write.js +80 -0
- package/dist/vendor/desktop-commander/dist/utils/capture.js +40 -0
- package/dist/vendor/desktop-commander/dist/utils/dockerPrompt.js +9 -0
- package/dist/vendor/desktop-commander/dist/utils/exit-process.js +31 -0
- package/dist/vendor/desktop-commander/dist/utils/feature-flags.js +18 -0
- package/dist/vendor/desktop-commander/dist/utils/files/base.js +5 -0
- package/dist/vendor/desktop-commander/dist/utils/files/binary.js +65 -0
- package/dist/vendor/desktop-commander/dist/utils/files/docx.js +694 -0
- package/dist/vendor/desktop-commander/dist/utils/files/excel.js +497 -0
- package/dist/vendor/desktop-commander/dist/utils/files/factory.js +168 -0
- package/dist/vendor/desktop-commander/dist/utils/files/image.js +89 -0
- package/dist/vendor/desktop-commander/dist/utils/files/index.js +14 -0
- package/dist/vendor/desktop-commander/dist/utils/files/pdf.js +124 -0
- package/dist/vendor/desktop-commander/dist/utils/files/text.js +423 -0
- package/dist/vendor/desktop-commander/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/vendor/desktop-commander/dist/utils/internal-facts.js +23 -0
- package/dist/vendor/desktop-commander/dist/utils/lazy-package.js +44 -0
- package/dist/vendor/desktop-commander/dist/utils/lineEndingHandler.js +77 -0
- package/dist/vendor/desktop-commander/dist/utils/logger.js +72 -0
- package/dist/vendor/desktop-commander/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/vendor/desktop-commander/dist/utils/open-browser.js +2 -0
- package/dist/vendor/desktop-commander/dist/utils/process-detection.js +260 -0
- package/dist/vendor/desktop-commander/dist/utils/process-tree.js +223 -0
- package/dist/vendor/desktop-commander/dist/utils/rename.js +74 -0
- package/dist/vendor/desktop-commander/dist/utils/ripgrep-resolver.js +69 -0
- package/dist/vendor/desktop-commander/dist/utils/shell.js +187 -0
- package/dist/vendor/desktop-commander/dist/utils/system-info.js +727 -0
- package/dist/vendor/desktop-commander/dist/utils/terminal-timestamps.js +18 -0
- package/dist/vendor/desktop-commander/dist/utils/toolHistory.js +309 -0
- package/dist/vendor/desktop-commander/dist/utils/trackTools.js +55 -0
- package/dist/vendor/desktop-commander/dist/utils/unsupportedParams.js +66 -0
- package/dist/vendor/desktop-commander/dist/utils/usageTracker.js +225 -0
- package/dist/vendor/desktop-commander/dist/utils/welcome-onboarding.js +2 -0
- package/dist/vendor/desktop-commander/dist/utils/withTimeout.js +85 -0
- package/dist/vendor/desktop-commander/dist/version.js +1 -0
- package/dist/vendor/desktop-commander/package.json +15 -0
- package/package.json +14 -2
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { terminalManager, MAX_BUFFERED_OUTPUT_CHARS, getProcessWaitLimit } from '../terminal-manager.js';
|
|
2
|
+
import { commandManager } from '../command-manager.js';
|
|
3
|
+
import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema } from './schemas.js';
|
|
4
|
+
import { capture } from "../utils/capture.js";
|
|
5
|
+
import { analyzeProcessState, cleanProcessOutput, formatProcessStateMessage } from '../utils/process-detection.js';
|
|
6
|
+
import { configManager } from '../config-manager.js';
|
|
7
|
+
import { getDefaultShell } from '../utils/shell.js';
|
|
8
|
+
import { terminateProcessTree } from '../utils/process-tree.js';
|
|
9
|
+
import { MAX_PROCESS_WAIT_MS } from '../config.js';
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import fs from 'fs/promises';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
// Get the directory where the MCP is installed (for ES module imports)
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const mcpRoot = path.resolve(__dirname, '..', '..');
|
|
18
|
+
// Track virtual Node sessions (PIDs that are actually Node fallback sessions)
|
|
19
|
+
const virtualNodeSessions = new Map();
|
|
20
|
+
let virtualPidCounter = -1000; // Use negative PIDs for virtual sessions
|
|
21
|
+
/** How long a node:local script's output pipes may stay open after its timeout ended its tree */
|
|
22
|
+
const PIPES_CLOSE_WAIT_MS = 1000;
|
|
23
|
+
/** The answer when some processes of a session could not be ended */
|
|
24
|
+
function terminationFailedResult(pid) {
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text: `Error: Could not terminate every process of session ${pid}; some may still be running` }],
|
|
27
|
+
isError: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Execute Node.js code via temp file (fallback when Python unavailable)
|
|
32
|
+
* Creates temp .mjs file in MCP directory for ES module import access
|
|
33
|
+
*/
|
|
34
|
+
async function executeNodeCode(code, timeout_ms = 30000, sessionPid) {
|
|
35
|
+
const tempFile = path.join(mcpRoot, `.mcp-exec-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
|
|
36
|
+
try {
|
|
37
|
+
await fs.writeFile(tempFile, code, 'utf8');
|
|
38
|
+
const result = await new Promise((resolve) => {
|
|
39
|
+
const proc = spawn(process.execPath, [tempFile], {
|
|
40
|
+
cwd: mcpRoot,
|
|
41
|
+
windowsHide: true // Prevent visible console windows on Windows
|
|
42
|
+
});
|
|
43
|
+
let stdout = '';
|
|
44
|
+
let stderr = '';
|
|
45
|
+
// Not spawn's own timeout option: that kills only the script, leaving the
|
|
46
|
+
// processes it started running and holding its output pipes open, so
|
|
47
|
+
// 'close' never came and the call never returned.
|
|
48
|
+
const timer = setTimeout(async () => {
|
|
49
|
+
if (await terminateProcessTree(proc)) {
|
|
50
|
+
// The tree ended, but a process that left it before the walk (one
|
|
51
|
+
// started by a child that has exited) may still hold the output
|
|
52
|
+
// pipes: 'close' answers if it comes within PIPES_CLOSE_WAIT_MS
|
|
53
|
+
await new Promise((wait) => setTimeout(wait, PIPES_CLOSE_WAIT_MS).unref());
|
|
54
|
+
}
|
|
55
|
+
// Some of the tree survived, or a process outside it holds the output
|
|
56
|
+
// pipes, so 'close' may never come: answer now and stop reading them
|
|
57
|
+
proc.stdout.destroy();
|
|
58
|
+
proc.stderr.destroy();
|
|
59
|
+
resolve({ stdout, stderr, exitCode: 1, treeSurvived: true });
|
|
60
|
+
}, timeout_ms);
|
|
61
|
+
proc.stdout.on('data', (data) => {
|
|
62
|
+
stdout += data.toString();
|
|
63
|
+
});
|
|
64
|
+
proc.stderr.on('data', (data) => {
|
|
65
|
+
stderr += data.toString();
|
|
66
|
+
});
|
|
67
|
+
proc.on('close', (exitCode) => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve({ stdout, stderr, exitCode: exitCode ?? 1 });
|
|
70
|
+
});
|
|
71
|
+
proc.on('error', (err) => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
resolve({ stdout, stderr: stderr + '\n' + err.message, exitCode: 1 });
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// Clean up temp file
|
|
77
|
+
await fs.unlink(tempFile).catch(() => { });
|
|
78
|
+
if (result.treeSurvived) {
|
|
79
|
+
return terminationFailedResult(sessionPid);
|
|
80
|
+
}
|
|
81
|
+
if (result.exitCode !== 0) {
|
|
82
|
+
return {
|
|
83
|
+
content: [{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: `Execution failed (exit code ${result.exitCode}):\n${result.stderr}\n${result.stdout}`
|
|
86
|
+
}],
|
|
87
|
+
isError: true
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Each call runs a fresh script and the session waits for the next one; the output isn't cut
|
|
91
|
+
const outputLines = result.stdout.trim().length > 0 ? result.stdout.replace(/\r?\n$/, '').split('\n').length : 0;
|
|
92
|
+
return {
|
|
93
|
+
content: [{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: result.stdout || '(no output)'
|
|
96
|
+
}],
|
|
97
|
+
structuredContent: {
|
|
98
|
+
pid: sessionPid,
|
|
99
|
+
status: 'waiting_for_input',
|
|
100
|
+
truncated: false,
|
|
101
|
+
shownLines: outputLines,
|
|
102
|
+
totalLines: outputLines,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Clean up temp file on error
|
|
108
|
+
await fs.unlink(tempFile).catch(() => { });
|
|
109
|
+
return {
|
|
110
|
+
content: [{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: `Failed to execute Node.js code: ${error instanceof Error ? error.message : String(error)}`
|
|
113
|
+
}],
|
|
114
|
+
isError: true
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Start a new process (renamed from execute_command)
|
|
120
|
+
* Includes early detection of process waiting for input
|
|
121
|
+
* maxWaitMs: ceiling for this call's wait. The tools always use MAX_PROCESS_WAIT_MS;
|
|
122
|
+
* only in-process callers (tests) pass a smaller one. Not part of the tool schema.
|
|
123
|
+
*/
|
|
124
|
+
export async function startProcess(args, maxWaitMs = MAX_PROCESS_WAIT_MS) {
|
|
125
|
+
const parsed = StartProcessArgsSchema.safeParse(args);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
capture('server_start_process_failed');
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: `Error: Invalid arguments for start_process: ${parsed.error}` }],
|
|
130
|
+
isError: true,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
// Each command's first word as typed, so telemetry replaces a path whole
|
|
135
|
+
// (the base name alone would send the last part of it)
|
|
136
|
+
const commands = commandManager.extractCommands(parsed.data.command, true).join(', ');
|
|
137
|
+
capture('server_start_process', {
|
|
138
|
+
command: commandManager.getBaseCommand(parsed.data.command),
|
|
139
|
+
commands: commands
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
capture('server_start_process', {
|
|
144
|
+
command: commandManager.getBaseCommand(parsed.data.command)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const isAllowed = await commandManager.validateCommand(parsed.data.command);
|
|
148
|
+
if (!isAllowed) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: `Error: Command not allowed: ${parsed.data.command}` }],
|
|
151
|
+
isError: true,
|
|
152
|
+
structuredContent: { blocked: true, command: parsed.data.command },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const commandToRun = parsed.data.command;
|
|
156
|
+
// Handle node:local - runs Node.js code directly on MCP server
|
|
157
|
+
if (commandToRun.trim() === 'node:local') {
|
|
158
|
+
const virtualPid = virtualPidCounter--;
|
|
159
|
+
virtualNodeSessions.set(virtualPid, { timeout_ms: parsed.data.timeout_ms || 30000 });
|
|
160
|
+
return {
|
|
161
|
+
content: [{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: `Node.js session started with PID ${virtualPid} (MCP server execution)
|
|
164
|
+
|
|
165
|
+
IMPORTANT: Each interact_with_process call runs as a FRESH script.
|
|
166
|
+
State is NOT preserved between calls. Include ALL code in ONE call:
|
|
167
|
+
- imports, file reading, processing, and output together.
|
|
168
|
+
|
|
169
|
+
Available libraries:
|
|
170
|
+
- ExcelJS for Excel files: import ExcelJS from 'exceljs'
|
|
171
|
+
- All Node.js built-ins: fs, path, http, crypto, etc.
|
|
172
|
+
|
|
173
|
+
š Ready for code - send complete self-contained script via interact_with_process.`
|
|
174
|
+
}],
|
|
175
|
+
structuredContent: { pid: virtualPid, status: 'waiting_for_input' },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
let shellUsed = parsed.data.shell;
|
|
179
|
+
if (!shellUsed) {
|
|
180
|
+
const config = await configManager.getConfig();
|
|
181
|
+
shellUsed = config.defaultShell || getDefaultShell();
|
|
182
|
+
}
|
|
183
|
+
const result = await terminalManager.executeCommand(commandToRun, parsed.data.timeout_ms, shellUsed, parsed.data.verbose_timing || false, maxWaitMs);
|
|
184
|
+
if (result.pid === -1) {
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: "text", text: result.output }],
|
|
187
|
+
isError: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Whether the process is waiting for input, as detected when the wait ended
|
|
191
|
+
const { processState } = result;
|
|
192
|
+
let statusMessage = '';
|
|
193
|
+
if (processState.isWaitingForInput) {
|
|
194
|
+
statusMessage = `\nš ${formatProcessStateMessage(processState, result.pid)}`;
|
|
195
|
+
}
|
|
196
|
+
else if (processState.isFinished) {
|
|
197
|
+
statusMessage = `\nā
${formatProcessStateMessage(processState, result.pid)}`;
|
|
198
|
+
}
|
|
199
|
+
else if (result.isBlocked) {
|
|
200
|
+
statusMessage = '\nā³ Process is running. Use read_process_output to get more output.';
|
|
201
|
+
}
|
|
202
|
+
// Add timing information if requested
|
|
203
|
+
let timingMessage = '';
|
|
204
|
+
if (result.timingInfo) {
|
|
205
|
+
timingMessage = formatTimingInfo(result.timingInfo);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
content: [{
|
|
209
|
+
type: "text",
|
|
210
|
+
text: `Process started with PID ${result.pid} (shell: ${shellUsed})\nInitial output:\n${result.output}${statusMessage}${timingMessage}`
|
|
211
|
+
}],
|
|
212
|
+
structuredContent: {
|
|
213
|
+
pid: result.pid,
|
|
214
|
+
shell: shellUsed,
|
|
215
|
+
status: getProcessStatus(processState),
|
|
216
|
+
...getWaitCapFields(result.waitCappedAtMs),
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Machine-readable process state for structuredContent, mirroring the
|
|
222
|
+
* status line shown in the text response.
|
|
223
|
+
*/
|
|
224
|
+
function getProcessStatus(state, timedOut = false) {
|
|
225
|
+
if (state.isWaitingForInput)
|
|
226
|
+
return 'waiting_for_input';
|
|
227
|
+
if (state.isFinished)
|
|
228
|
+
return 'finished';
|
|
229
|
+
return timedOut ? 'timeout' : 'running';
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* structuredContent fields telling the caller whether the wait ceiling
|
|
233
|
+
* (not timeout_ms) ended the wait. When it did, status is 'running'
|
|
234
|
+
* and the caller continues with read_process_output.
|
|
235
|
+
*/
|
|
236
|
+
function getWaitCapFields(waitCappedAtMs) {
|
|
237
|
+
return waitCappedAtMs !== undefined
|
|
238
|
+
? { waitCapped: true, waitLimitMs: waitCappedAtMs }
|
|
239
|
+
: { waitCapped: false };
|
|
240
|
+
}
|
|
241
|
+
function formatTimingInfo(timing) {
|
|
242
|
+
let msg = '\n\nš Timing Information:\n';
|
|
243
|
+
msg += ` Exit Reason: ${timing.exitReason}\n`;
|
|
244
|
+
msg += ` Total Duration: ${timing.totalDurationMs}ms\n`;
|
|
245
|
+
if (timing.timeToFirstOutputMs !== undefined) {
|
|
246
|
+
msg += ` Time to First Output: ${timing.timeToFirstOutputMs}ms\n`;
|
|
247
|
+
}
|
|
248
|
+
if (timing.firstOutputTime && timing.lastOutputTime) {
|
|
249
|
+
msg += ` Output Window: ${timing.lastOutputTime - timing.firstOutputTime}ms\n`;
|
|
250
|
+
}
|
|
251
|
+
if (timing.outputEvents && timing.outputEvents.length > 0) {
|
|
252
|
+
msg += `\n Output Events (${timing.outputEvents.length} total):\n`;
|
|
253
|
+
timing.outputEvents.forEach((event, idx) => {
|
|
254
|
+
msg += ` [${idx + 1}] +${event.deltaMs}ms | ${event.source} | ${event.length}b`;
|
|
255
|
+
if (event.matchedPattern) {
|
|
256
|
+
msg += ` | šÆ ${event.matchedPattern}`;
|
|
257
|
+
}
|
|
258
|
+
msg += `\n "${event.snippet}"\n`;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return msg;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Read output from a running process with file-like pagination
|
|
265
|
+
* Supports offset/length parameters for controlled reading
|
|
266
|
+
* maxWaitMs: ceiling for this call's wait. The tools always use MAX_PROCESS_WAIT_MS;
|
|
267
|
+
* only in-process callers (tests) pass a smaller one. Not part of the tool schema.
|
|
268
|
+
*/
|
|
269
|
+
export async function readProcessOutput(args, maxWaitMs = MAX_PROCESS_WAIT_MS) {
|
|
270
|
+
const parsed = ReadProcessOutputArgsSchema.safeParse(args);
|
|
271
|
+
if (!parsed.success) {
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: "text", text: `Error: Invalid arguments for read_process_output: ${parsed.error}` }],
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// Get default line limit from config
|
|
278
|
+
const config = await configManager.getConfig();
|
|
279
|
+
const defaultLength = config.fileReadLineLimit ?? 1000;
|
|
280
|
+
const { pid, timeout_ms = 5000, offset = 0, // 0 = from last read, positive = absolute, negative = tail
|
|
281
|
+
length = defaultLength, // Default from config, same as file reading
|
|
282
|
+
verbose_timing = false } = parsed.data;
|
|
283
|
+
// Timing telemetry
|
|
284
|
+
const startTime = Date.now();
|
|
285
|
+
const { waitMs } = getProcessWaitLimit(timeout_ms, maxWaitMs);
|
|
286
|
+
// For active sessions with no new output yet, optionally wait for output
|
|
287
|
+
const session = terminalManager.getSession(pid);
|
|
288
|
+
if (session && offset === 0) {
|
|
289
|
+
// Wait for new output to arrive (only for "new output" reads, not absolute/tail)
|
|
290
|
+
const waitForOutput = () => {
|
|
291
|
+
return new Promise((resolve) => {
|
|
292
|
+
// Check if there's already new output
|
|
293
|
+
if (terminalManager.hasUnreadOutput(pid)) {
|
|
294
|
+
resolve();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
let resolved = false;
|
|
298
|
+
let interval = null;
|
|
299
|
+
let timeout = null;
|
|
300
|
+
const cleanup = () => {
|
|
301
|
+
if (interval)
|
|
302
|
+
clearInterval(interval);
|
|
303
|
+
if (timeout)
|
|
304
|
+
clearTimeout(timeout);
|
|
305
|
+
};
|
|
306
|
+
const resolveOnce = () => {
|
|
307
|
+
if (resolved)
|
|
308
|
+
return;
|
|
309
|
+
resolved = true;
|
|
310
|
+
cleanup();
|
|
311
|
+
resolve();
|
|
312
|
+
};
|
|
313
|
+
// Poll for new output, or for the exit (the session is then no
|
|
314
|
+
// longer active): a process that exited writes nothing more itself
|
|
315
|
+
interval = setInterval(() => {
|
|
316
|
+
if (terminalManager.hasUnreadOutput(pid) || !terminalManager.getSession(pid)) {
|
|
317
|
+
resolveOnce();
|
|
318
|
+
}
|
|
319
|
+
}, 50);
|
|
320
|
+
// Timeout
|
|
321
|
+
timeout = setTimeout(() => {
|
|
322
|
+
resolveOnce();
|
|
323
|
+
}, waitMs);
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
await waitForOutput();
|
|
327
|
+
}
|
|
328
|
+
// Read output with pagination
|
|
329
|
+
const result = terminalManager.readOutputPaginated(pid, offset, length);
|
|
330
|
+
if (!result) {
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: "text", text: `No session found for PID ${pid}` }],
|
|
333
|
+
isError: true,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
// Join lines back into string
|
|
337
|
+
const output = result.lines.join('\n');
|
|
338
|
+
// Generate status message similar to file reading
|
|
339
|
+
let statusMessage = '';
|
|
340
|
+
if (offset < 0) {
|
|
341
|
+
// Tail read - match file reading format for consistency
|
|
342
|
+
statusMessage = `[Reading last ${result.readCount} lines (total: ${result.totalLines} lines)]`;
|
|
343
|
+
}
|
|
344
|
+
else if (offset === 0) {
|
|
345
|
+
// "New output" read
|
|
346
|
+
if (result.remaining > 0) {
|
|
347
|
+
statusMessage = `[Reading ${result.readCount} new lines from line ${result.readFrom} (total: ${result.totalLines} lines, ${result.remaining} remaining)]`;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
statusMessage = `[Reading ${result.readCount} new lines (total: ${result.totalLines} lines)]`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
// Absolute position read
|
|
355
|
+
statusMessage = `[Reading ${result.readCount} lines from line ${result.readFrom} (total: ${result.totalLines} lines, ${result.remaining} remaining)]`;
|
|
356
|
+
}
|
|
357
|
+
// Surface buffer-cap eviction so the model knows the retained output is not
|
|
358
|
+
// the full output and that line numbers shifted (matches the truncation
|
|
359
|
+
// markers used by other tools).
|
|
360
|
+
if (result.evictedLines && result.evictedLines > 0) {
|
|
361
|
+
const capMB = Math.round(MAX_BUFFERED_OUTPUT_CHARS / 1024 / 1024);
|
|
362
|
+
statusMessage += `\n[WARNING: output exceeded the ${capMB}MB buffer cap; the ${result.evictedLines} earliest lines were evicted and cannot be read. Line numbers and totals refer to the retained buffer only]`;
|
|
363
|
+
}
|
|
364
|
+
// Add process state info
|
|
365
|
+
let processStateMessage = '';
|
|
366
|
+
if (result.isComplete) {
|
|
367
|
+
const runtimeStr = result.runtimeMs !== undefined
|
|
368
|
+
? ` (runtime: ${(result.runtimeMs / 1000).toFixed(2)}s)`
|
|
369
|
+
: '';
|
|
370
|
+
// A process ended by a signal has no exit code: name the signal instead of "exit code null"
|
|
371
|
+
const ending = result.exitCode === null && result.signal ? `signal ${result.signal}` : `exit code ${result.exitCode}`;
|
|
372
|
+
processStateMessage = `\nā
Process completed with ${ending}${runtimeStr}`;
|
|
373
|
+
}
|
|
374
|
+
else if (session) {
|
|
375
|
+
// Analyze state for running processes
|
|
376
|
+
const processState = terminalManager.getProcessState(pid);
|
|
377
|
+
if (processState?.isWaitingForInput) {
|
|
378
|
+
processStateMessage = `\nš ${formatProcessStateMessage(processState, pid)}`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Add timing information if requested
|
|
382
|
+
let timingMessage = '';
|
|
383
|
+
if (verbose_timing) {
|
|
384
|
+
const endTime = Date.now();
|
|
385
|
+
timingMessage = `\n\nš Timing: ${endTime - startTime}ms`;
|
|
386
|
+
}
|
|
387
|
+
const responseText = output || '(No output in requested range)';
|
|
388
|
+
return {
|
|
389
|
+
content: [{
|
|
390
|
+
type: "text",
|
|
391
|
+
text: `${statusMessage}\n\n${responseText}${processStateMessage}${timingMessage}`
|
|
392
|
+
}],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Interact with a running process (renamed from send_input)
|
|
397
|
+
* Automatically detects when process is ready and returns output
|
|
398
|
+
* maxWaitMs: ceiling for this call's wait. The tools always use MAX_PROCESS_WAIT_MS;
|
|
399
|
+
* only in-process callers (tests) pass a smaller one. Not part of the tool schema.
|
|
400
|
+
*/
|
|
401
|
+
export async function interactWithProcess(args, maxWaitMs = MAX_PROCESS_WAIT_MS) {
|
|
402
|
+
const parsed = InteractWithProcessArgsSchema.safeParse(args);
|
|
403
|
+
if (!parsed.success) {
|
|
404
|
+
capture('server_interact_with_process_failed', {
|
|
405
|
+
error: 'Invalid arguments'
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
content: [{ type: "text", text: `Error: Invalid arguments for interact_with_process: ${parsed.error}` }],
|
|
409
|
+
isError: true,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const { pid, input, timeout_ms = 8000, wait_for_prompt = true, verbose_timing = false } = parsed.data;
|
|
413
|
+
// Get config for output line limit
|
|
414
|
+
const config = await configManager.getConfig();
|
|
415
|
+
const maxOutputLines = config.fileReadLineLimit ?? 1000;
|
|
416
|
+
const waitLimit = getProcessWaitLimit(timeout_ms, maxWaitMs);
|
|
417
|
+
// Check if this is a virtual Node session (node:local)
|
|
418
|
+
if (virtualNodeSessions.has(pid)) {
|
|
419
|
+
const session = virtualNodeSessions.get(pid);
|
|
420
|
+
capture('server_interact_with_process_node_fallback', {
|
|
421
|
+
pid: pid,
|
|
422
|
+
inputLength: input.length
|
|
423
|
+
});
|
|
424
|
+
// Execute code via temp file approach
|
|
425
|
+
// Respect per-call timeout if provided, otherwise use session default
|
|
426
|
+
// (parsed.data's: timeout_ms above already holds the 8000ms default for processes),
|
|
427
|
+
// within the process wait ceiling: the call answers only once the script ends
|
|
428
|
+
const effectiveTimeout = Math.min(parsed.data.timeout_ms ?? session.timeout_ms, waitLimit.capMs);
|
|
429
|
+
return executeNodeCode(input, effectiveTimeout, pid);
|
|
430
|
+
}
|
|
431
|
+
// Timing telemetry
|
|
432
|
+
const startTime = Date.now();
|
|
433
|
+
let firstOutputTime;
|
|
434
|
+
let lastOutputTime;
|
|
435
|
+
const outputEvents = [];
|
|
436
|
+
let exitReason = 'timeout';
|
|
437
|
+
try {
|
|
438
|
+
capture('server_interact_with_process', {
|
|
439
|
+
pid: pid,
|
|
440
|
+
inputLength: input.length
|
|
441
|
+
});
|
|
442
|
+
// Capture output snapshot BEFORE sending input
|
|
443
|
+
// This handles REPLs where output is appended to the prompt line
|
|
444
|
+
const outputSnapshot = terminalManager.captureOutputSnapshot(pid);
|
|
445
|
+
// Only a process that reads its input at a prompt prints prompts in its output
|
|
446
|
+
const readAtPrompt = terminalManager.getProcessState(pid)?.isWaitingForInput ?? false;
|
|
447
|
+
// No snapshot means no active session, which can't take input either
|
|
448
|
+
const success = outputSnapshot !== null && terminalManager.sendInputToProcess(pid, input);
|
|
449
|
+
if (!success) {
|
|
450
|
+
return {
|
|
451
|
+
content: [{ type: "text", text: `Error: Failed to send input to process ${pid}. The process may have exited or doesn't accept input.` }],
|
|
452
|
+
isError: true,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
// If not waiting for response, return immediately
|
|
456
|
+
if (!wait_for_prompt) {
|
|
457
|
+
exitReason = 'no_wait';
|
|
458
|
+
let timingMessage = '';
|
|
459
|
+
if (verbose_timing) {
|
|
460
|
+
const endTime = Date.now();
|
|
461
|
+
const timingInfo = {
|
|
462
|
+
startTime,
|
|
463
|
+
endTime,
|
|
464
|
+
totalDurationMs: endTime - startTime,
|
|
465
|
+
exitReason,
|
|
466
|
+
firstOutputTime,
|
|
467
|
+
lastOutputTime,
|
|
468
|
+
timeToFirstOutputMs: undefined,
|
|
469
|
+
outputEvents: undefined
|
|
470
|
+
};
|
|
471
|
+
timingMessage = formatTimingInfo(timingInfo);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
content: [{
|
|
475
|
+
type: "text",
|
|
476
|
+
text: `ā
Input sent to process ${pid}. Use read_process_output to get the response.${timingMessage}`
|
|
477
|
+
}],
|
|
478
|
+
// Not waited for: no output was read
|
|
479
|
+
structuredContent: { pid, status: 'running', truncated: false, shownLines: 0, totalLines: 0 },
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
// Smart waiting with immediate and periodic detection
|
|
483
|
+
let output = "";
|
|
484
|
+
let processState;
|
|
485
|
+
let earlyExit = false;
|
|
486
|
+
let waitCapped = false;
|
|
487
|
+
const waitForResponse = () => {
|
|
488
|
+
return new Promise((resolve) => {
|
|
489
|
+
let resolved = false;
|
|
490
|
+
const pollIntervalMs = 50; // Poll every 50ms for faster response
|
|
491
|
+
// A deadline, not a poll count, so a busy event loop can't stretch the wait past the ceiling
|
|
492
|
+
const deadline = Date.now() + waitLimit.waitMs;
|
|
493
|
+
let interval = null;
|
|
494
|
+
// Advances every poll, so each poll reads only the output that arrived since the
|
|
495
|
+
// previous one (snapshot-based, which handles REPL prompt line appending)
|
|
496
|
+
let readPosition = outputSnapshot;
|
|
497
|
+
let resolveOnce = () => {
|
|
498
|
+
if (resolved)
|
|
499
|
+
return;
|
|
500
|
+
resolved = true;
|
|
501
|
+
if (interval)
|
|
502
|
+
clearInterval(interval);
|
|
503
|
+
resolve();
|
|
504
|
+
};
|
|
505
|
+
// Fast-polling check - check every 50ms for quick responses
|
|
506
|
+
interval = setInterval(() => {
|
|
507
|
+
if (resolved)
|
|
508
|
+
return;
|
|
509
|
+
const read = terminalManager.readOutputSince(pid, readPosition);
|
|
510
|
+
if (read)
|
|
511
|
+
readPosition = read.next;
|
|
512
|
+
const newOutput = read?.output ?? '';
|
|
513
|
+
if (newOutput.length > 0) {
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
if (!firstOutputTime)
|
|
516
|
+
firstOutputTime = now;
|
|
517
|
+
lastOutputTime = now;
|
|
518
|
+
if (verbose_timing) {
|
|
519
|
+
outputEvents.push({
|
|
520
|
+
timestamp: now,
|
|
521
|
+
deltaMs: now - startTime,
|
|
522
|
+
source: 'periodic_poll',
|
|
523
|
+
length: newOutput.length,
|
|
524
|
+
snippet: newOutput.slice(0, 50).replace(/\n/g, '\\n')
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
// Full output since the snapshot, bounded like the session buffer it came from
|
|
528
|
+
output += newOutput;
|
|
529
|
+
if (output.length > MAX_BUFFERED_OUTPUT_CHARS) {
|
|
530
|
+
output = output.slice(-MAX_BUFFERED_OUTPUT_CHARS);
|
|
531
|
+
}
|
|
532
|
+
// Analyze current state from the end of the output since the snapshot
|
|
533
|
+
processState = terminalManager.getProcessState(pid, outputSnapshot) ?? analyzeProcessState(output, pid);
|
|
534
|
+
// Exit early if we detect the process is waiting for input
|
|
535
|
+
if (processState.isWaitingForInput) {
|
|
536
|
+
earlyExit = true;
|
|
537
|
+
exitReason = 'early_exit_periodic_check';
|
|
538
|
+
if (verbose_timing && outputEvents.length > 0) {
|
|
539
|
+
outputEvents[outputEvents.length - 1].matchedPattern = 'periodic_check';
|
|
540
|
+
}
|
|
541
|
+
resolveOnce();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// Also exit once the process has exited, whether or not it wrote
|
|
546
|
+
// anything since the previous poll: no answer can come any more
|
|
547
|
+
const state = terminalManager.getProcessState(pid, outputSnapshot);
|
|
548
|
+
if (state?.isFinished) {
|
|
549
|
+
processState = state;
|
|
550
|
+
exitReason = 'process_finished';
|
|
551
|
+
resolveOnce();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (Date.now() >= deadline) {
|
|
555
|
+
exitReason = 'timeout';
|
|
556
|
+
waitCapped = waitLimit.capped;
|
|
557
|
+
resolveOnce();
|
|
558
|
+
}
|
|
559
|
+
}, pollIntervalMs);
|
|
560
|
+
});
|
|
561
|
+
};
|
|
562
|
+
await waitForResponse();
|
|
563
|
+
// Clean and format output
|
|
564
|
+
let cleanOutput = cleanProcessOutput(output, input, readAtPrompt);
|
|
565
|
+
const timeoutReached = !earlyExit && !processState?.isFinished && !processState?.isWaitingForInput;
|
|
566
|
+
// Apply output line limit to prevent context overflow
|
|
567
|
+
let truncationMessage = '';
|
|
568
|
+
const outputLines = cleanOutput.split('\n');
|
|
569
|
+
const totalLines = cleanOutput.trim().length > 0 ? outputLines.length : 0;
|
|
570
|
+
const shownLines = Math.min(totalLines, maxOutputLines);
|
|
571
|
+
if (outputLines.length > maxOutputLines) {
|
|
572
|
+
const truncatedLines = outputLines.slice(0, maxOutputLines);
|
|
573
|
+
cleanOutput = truncatedLines.join('\n');
|
|
574
|
+
const remainingLines = outputLines.length - maxOutputLines;
|
|
575
|
+
truncationMessage = `\n\nā ļø Output truncated: showing ${maxOutputLines} of ${outputLines.length} lines (${remainingLines} hidden). Use read_process_output with offset/length for full output.`;
|
|
576
|
+
}
|
|
577
|
+
// Determine final state
|
|
578
|
+
if (!processState) {
|
|
579
|
+
processState = terminalManager.getProcessState(pid, outputSnapshot) ?? analyzeProcessState(output, pid);
|
|
580
|
+
}
|
|
581
|
+
let statusMessage = '';
|
|
582
|
+
if (processState.isWaitingForInput) {
|
|
583
|
+
statusMessage = `\nš ${formatProcessStateMessage(processState, pid)}`;
|
|
584
|
+
}
|
|
585
|
+
else if (processState.isFinished) {
|
|
586
|
+
statusMessage = `\nā
${formatProcessStateMessage(processState, pid)}`;
|
|
587
|
+
}
|
|
588
|
+
else if (timeoutReached) {
|
|
589
|
+
statusMessage = '\nā±ļø Response may be incomplete (timeout reached)';
|
|
590
|
+
}
|
|
591
|
+
// Add timing information if requested
|
|
592
|
+
let timingMessage = '';
|
|
593
|
+
if (verbose_timing) {
|
|
594
|
+
const endTime = Date.now();
|
|
595
|
+
const timingInfo = {
|
|
596
|
+
startTime,
|
|
597
|
+
endTime,
|
|
598
|
+
totalDurationMs: endTime - startTime,
|
|
599
|
+
exitReason,
|
|
600
|
+
firstOutputTime,
|
|
601
|
+
lastOutputTime,
|
|
602
|
+
timeToFirstOutputMs: firstOutputTime ? firstOutputTime - startTime : undefined,
|
|
603
|
+
outputEvents: outputEvents.length > 0 ? outputEvents : undefined
|
|
604
|
+
};
|
|
605
|
+
timingMessage = formatTimingInfo(timingInfo);
|
|
606
|
+
}
|
|
607
|
+
// A capped wait didn't reach the caller's timeout_ms: the process is still
|
|
608
|
+
// running and the caller continues with read_process_output
|
|
609
|
+
const structuredContent = {
|
|
610
|
+
pid,
|
|
611
|
+
status: getProcessStatus(processState, timeoutReached && !waitCapped),
|
|
612
|
+
truncated: totalLines > shownLines,
|
|
613
|
+
shownLines,
|
|
614
|
+
totalLines,
|
|
615
|
+
...getWaitCapFields(timeoutReached && waitCapped ? waitLimit.capMs : undefined),
|
|
616
|
+
};
|
|
617
|
+
if (cleanOutput.trim().length === 0 && !timeoutReached) {
|
|
618
|
+
return {
|
|
619
|
+
content: [{
|
|
620
|
+
type: "text",
|
|
621
|
+
text: `ā
Input executed in process ${pid}.\nš (No output produced)${statusMessage}${timingMessage}`
|
|
622
|
+
}],
|
|
623
|
+
structuredContent,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
// Format response with better structure and consistent emojis
|
|
627
|
+
let responseText = `ā
Input executed in process ${pid}`;
|
|
628
|
+
if (cleanOutput && cleanOutput.trim().length > 0) {
|
|
629
|
+
responseText += `:\n\nš¤ Output:\n${cleanOutput}`;
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
responseText += `.\nš (No output produced)`;
|
|
633
|
+
}
|
|
634
|
+
if (statusMessage) {
|
|
635
|
+
responseText += `\n\n${statusMessage}`;
|
|
636
|
+
}
|
|
637
|
+
if (truncationMessage) {
|
|
638
|
+
responseText += truncationMessage;
|
|
639
|
+
}
|
|
640
|
+
if (timingMessage) {
|
|
641
|
+
responseText += timingMessage;
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
content: [{
|
|
645
|
+
type: "text",
|
|
646
|
+
text: responseText
|
|
647
|
+
}],
|
|
648
|
+
structuredContent,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
653
|
+
capture('server_interact_with_process_error', {
|
|
654
|
+
error: errorMessage
|
|
655
|
+
});
|
|
656
|
+
return {
|
|
657
|
+
content: [{ type: "text", text: `Error interacting with process: ${errorMessage}` }],
|
|
658
|
+
isError: true,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Force terminate a process
|
|
664
|
+
*/
|
|
665
|
+
export async function forceTerminate(args) {
|
|
666
|
+
const parsed = ForceTerminateArgsSchema.safeParse(args);
|
|
667
|
+
if (!parsed.success) {
|
|
668
|
+
return {
|
|
669
|
+
content: [{ type: "text", text: `Error: Invalid arguments for force_terminate: ${parsed.error}` }],
|
|
670
|
+
isError: true,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
const pid = parsed.data.pid;
|
|
674
|
+
// Handle virtual Node.js sessions (node:local)
|
|
675
|
+
if (virtualNodeSessions.has(pid)) {
|
|
676
|
+
virtualNodeSessions.delete(pid);
|
|
677
|
+
return {
|
|
678
|
+
content: [{
|
|
679
|
+
type: "text",
|
|
680
|
+
text: `Cleared virtual Node.js session ${pid}`
|
|
681
|
+
}],
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
// Returns once the session's processes are gone, so a success means they no longer run
|
|
685
|
+
const outcome = await terminalManager.forceTerminate(pid);
|
|
686
|
+
if (outcome === 'failed') {
|
|
687
|
+
return terminationFailedResult(pid);
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
content: [{
|
|
691
|
+
type: "text",
|
|
692
|
+
text: outcome === 'terminated'
|
|
693
|
+
? `Successfully initiated termination of session ${pid}`
|
|
694
|
+
: `No active session found for PID ${pid}`
|
|
695
|
+
}],
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* List active sessions
|
|
700
|
+
*/
|
|
701
|
+
export async function listSessions() {
|
|
702
|
+
const sessions = terminalManager.listActiveSessions();
|
|
703
|
+
// Include virtual Node.js sessions
|
|
704
|
+
const virtualSessions = Array.from(virtualNodeSessions.entries()).map(([pid, session]) => ({
|
|
705
|
+
pid,
|
|
706
|
+
type: 'node:local',
|
|
707
|
+
timeout_ms: session.timeout_ms
|
|
708
|
+
}));
|
|
709
|
+
const realSessionsText = sessions.map(s => `PID: ${s.pid}, Blocked: ${s.isBlocked}, Runtime: ${Math.round(s.runtime / 1000)}s`);
|
|
710
|
+
const virtualSessionsText = virtualSessions.map(s => `PID: ${s.pid} (node:local), Timeout: ${s.timeout_ms}ms`);
|
|
711
|
+
const allSessions = [...realSessionsText, ...virtualSessionsText];
|
|
712
|
+
return {
|
|
713
|
+
content: [{
|
|
714
|
+
type: "text",
|
|
715
|
+
text: allSessions.length === 0
|
|
716
|
+
? 'No active sessions'
|
|
717
|
+
: allSessions.join('\n')
|
|
718
|
+
}],
|
|
719
|
+
structuredContent: {
|
|
720
|
+
sessions: [
|
|
721
|
+
...sessions.map(s => ({ pid: s.pid, type: 'process', isBlocked: s.isBlocked, runtimeMs: s.runtime })),
|
|
722
|
+
...virtualSessions.map(s => ({ pid: s.pid, type: s.type, timeoutMs: s.timeout_ms })),
|
|
723
|
+
],
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|