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.
Files changed (93) hide show
  1. package/dist/src/cli/mac-mcp-gateway.js +4 -3
  2. package/dist/src/cli/mac-mcp-gateway.js.map +1 -1
  3. package/dist/src/cli/starmux.js +1 -1
  4. package/dist/src/cli/starmux.js.map +1 -1
  5. package/dist/src/runtime-paths.js +3 -3
  6. package/dist/src/runtime-paths.js.map +1 -1
  7. package/dist/vendor/desktop-commander/LICENSE +21 -0
  8. package/dist/vendor/desktop-commander/dist/bootstrap.js +22 -0
  9. package/dist/vendor/desktop-commander/dist/command-manager.js +246 -0
  10. package/dist/vendor/desktop-commander/dist/config-field-definitions.js +37 -0
  11. package/dist/vendor/desktop-commander/dist/config-manager.js +634 -0
  12. package/dist/vendor/desktop-commander/dist/config-recovery.js +81 -0
  13. package/dist/vendor/desktop-commander/dist/config.js +19 -0
  14. package/dist/vendor/desktop-commander/dist/custom-stdio.js +358 -0
  15. package/dist/vendor/desktop-commander/dist/error-handlers.js +15 -0
  16. package/dist/vendor/desktop-commander/dist/handlers/edit-search-handlers.js +6 -0
  17. package/dist/vendor/desktop-commander/dist/handlers/filesystem-handlers.js +450 -0
  18. package/dist/vendor/desktop-commander/dist/handlers/history-handlers.js +67 -0
  19. package/dist/vendor/desktop-commander/dist/handlers/index.js +7 -0
  20. package/dist/vendor/desktop-commander/dist/handlers/process-handlers.js +15 -0
  21. package/dist/vendor/desktop-commander/dist/handlers/search-answers.js +172 -0
  22. package/dist/vendor/desktop-commander/dist/handlers/search-handlers.js +136 -0
  23. package/dist/vendor/desktop-commander/dist/handlers/terminal-handlers.js +35 -0
  24. package/dist/vendor/desktop-commander/dist/index.js +88 -0
  25. package/dist/vendor/desktop-commander/dist/search-manager.js +1262 -0
  26. package/dist/vendor/desktop-commander/dist/server.js +1414 -0
  27. package/dist/vendor/desktop-commander/dist/terminal-manager.js +757 -0
  28. package/dist/vendor/desktop-commander/dist/tools/config.js +211 -0
  29. package/dist/vendor/desktop-commander/dist/tools/edit.js +455 -0
  30. package/dist/vendor/desktop-commander/dist/tools/feedback.js +10 -0
  31. package/dist/vendor/desktop-commander/dist/tools/filesystem.js +1047 -0
  32. package/dist/vendor/desktop-commander/dist/tools/fuzzySearch.js +74 -0
  33. package/dist/vendor/desktop-commander/dist/tools/fuzzySearchCore.js +125 -0
  34. package/dist/vendor/desktop-commander/dist/tools/improved-process-tools.js +726 -0
  35. package/dist/vendor/desktop-commander/dist/tools/mime-types.js +28 -0
  36. package/dist/vendor/desktop-commander/dist/tools/pdf/extract-images.js +131 -0
  37. package/dist/vendor/desktop-commander/dist/tools/pdf/index.js +4 -0
  38. package/dist/vendor/desktop-commander/dist/tools/pdf/lib/pdf2md.js +78 -0
  39. package/dist/vendor/desktop-commander/dist/tools/pdf/manipulations.js +112 -0
  40. package/dist/vendor/desktop-commander/dist/tools/pdf/markdown.js +30 -0
  41. package/dist/vendor/desktop-commander/dist/tools/pdf/utils.js +45 -0
  42. package/dist/vendor/desktop-commander/dist/tools/process.js +90 -0
  43. package/dist/vendor/desktop-commander/dist/tools/prompts.js +259 -0
  44. package/dist/vendor/desktop-commander/dist/tools/schemas.js +242 -0
  45. package/dist/vendor/desktop-commander/dist/tools/usage.js +24 -0
  46. package/dist/vendor/desktop-commander/dist/types.js +1 -0
  47. package/dist/vendor/desktop-commander/dist/ui/config-editor/config-editor-runtime.js +150 -0
  48. package/dist/vendor/desktop-commander/dist/ui/config-editor/index.html +13 -0
  49. package/dist/vendor/desktop-commander/dist/ui/config-editor/styles.css +587 -0
  50. package/dist/vendor/desktop-commander/dist/ui/contracts.js +57 -0
  51. package/dist/vendor/desktop-commander/dist/ui/file-preview/index.html +16 -0
  52. package/dist/vendor/desktop-commander/dist/ui/file-preview/preview-runtime.js +471 -0
  53. package/dist/vendor/desktop-commander/dist/ui/file-preview/shared/preview-file-types.js +59 -0
  54. package/dist/vendor/desktop-commander/dist/ui/file-preview/styles.css +1631 -0
  55. package/dist/vendor/desktop-commander/dist/ui/resources.js +93 -0
  56. package/dist/vendor/desktop-commander/dist/utils/ab-test.js +122 -0
  57. package/dist/vendor/desktop-commander/dist/utils/atomic-write.js +80 -0
  58. package/dist/vendor/desktop-commander/dist/utils/capture.js +40 -0
  59. package/dist/vendor/desktop-commander/dist/utils/dockerPrompt.js +9 -0
  60. package/dist/vendor/desktop-commander/dist/utils/exit-process.js +31 -0
  61. package/dist/vendor/desktop-commander/dist/utils/feature-flags.js +18 -0
  62. package/dist/vendor/desktop-commander/dist/utils/files/base.js +5 -0
  63. package/dist/vendor/desktop-commander/dist/utils/files/binary.js +65 -0
  64. package/dist/vendor/desktop-commander/dist/utils/files/docx.js +694 -0
  65. package/dist/vendor/desktop-commander/dist/utils/files/excel.js +497 -0
  66. package/dist/vendor/desktop-commander/dist/utils/files/factory.js +168 -0
  67. package/dist/vendor/desktop-commander/dist/utils/files/image.js +89 -0
  68. package/dist/vendor/desktop-commander/dist/utils/files/index.js +14 -0
  69. package/dist/vendor/desktop-commander/dist/utils/files/pdf.js +124 -0
  70. package/dist/vendor/desktop-commander/dist/utils/files/text.js +423 -0
  71. package/dist/vendor/desktop-commander/dist/utils/fuzzySearchLogger.js +126 -0
  72. package/dist/vendor/desktop-commander/dist/utils/internal-facts.js +23 -0
  73. package/dist/vendor/desktop-commander/dist/utils/lazy-package.js +44 -0
  74. package/dist/vendor/desktop-commander/dist/utils/lineEndingHandler.js +77 -0
  75. package/dist/vendor/desktop-commander/dist/utils/logger.js +72 -0
  76. package/dist/vendor/desktop-commander/dist/utils/mcp-ui-ab-test.js +62 -0
  77. package/dist/vendor/desktop-commander/dist/utils/open-browser.js +2 -0
  78. package/dist/vendor/desktop-commander/dist/utils/process-detection.js +260 -0
  79. package/dist/vendor/desktop-commander/dist/utils/process-tree.js +223 -0
  80. package/dist/vendor/desktop-commander/dist/utils/rename.js +74 -0
  81. package/dist/vendor/desktop-commander/dist/utils/ripgrep-resolver.js +69 -0
  82. package/dist/vendor/desktop-commander/dist/utils/shell.js +187 -0
  83. package/dist/vendor/desktop-commander/dist/utils/system-info.js +727 -0
  84. package/dist/vendor/desktop-commander/dist/utils/terminal-timestamps.js +18 -0
  85. package/dist/vendor/desktop-commander/dist/utils/toolHistory.js +309 -0
  86. package/dist/vendor/desktop-commander/dist/utils/trackTools.js +55 -0
  87. package/dist/vendor/desktop-commander/dist/utils/unsupportedParams.js +66 -0
  88. package/dist/vendor/desktop-commander/dist/utils/usageTracker.js +225 -0
  89. package/dist/vendor/desktop-commander/dist/utils/welcome-onboarding.js +2 -0
  90. package/dist/vendor/desktop-commander/dist/utils/withTimeout.js +85 -0
  91. package/dist/vendor/desktop-commander/dist/version.js +1 -0
  92. package/dist/vendor/desktop-commander/package.json +15 -0
  93. 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
+ }