@wonderwhy-er/desktop-commander 0.2.23 → 0.2.25

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 (70) hide show
  1. package/README.md +14 -55
  2. package/dist/config-manager.d.ts +5 -0
  3. package/dist/config-manager.js +9 -0
  4. package/dist/custom-stdio.d.ts +1 -0
  5. package/dist/custom-stdio.js +19 -0
  6. package/dist/handlers/filesystem-handlers.d.ts +4 -0
  7. package/dist/handlers/filesystem-handlers.js +120 -14
  8. package/dist/handlers/node-handlers.d.ts +6 -0
  9. package/dist/handlers/node-handlers.js +73 -0
  10. package/dist/index.js +5 -3
  11. package/dist/search-manager.d.ts +25 -0
  12. package/dist/search-manager.js +212 -0
  13. package/dist/server.d.ts +11 -0
  14. package/dist/server.js +188 -73
  15. package/dist/terminal-manager.d.ts +56 -2
  16. package/dist/terminal-manager.js +169 -13
  17. package/dist/tools/edit.d.ts +28 -4
  18. package/dist/tools/edit.js +87 -4
  19. package/dist/tools/filesystem.d.ts +23 -12
  20. package/dist/tools/filesystem.js +201 -416
  21. package/dist/tools/improved-process-tools.d.ts +2 -2
  22. package/dist/tools/improved-process-tools.js +244 -214
  23. package/dist/tools/mime-types.d.ts +1 -0
  24. package/dist/tools/mime-types.js +7 -0
  25. package/dist/tools/pdf/extract-images.d.ts +34 -0
  26. package/dist/tools/pdf/extract-images.js +132 -0
  27. package/dist/tools/pdf/index.d.ts +6 -0
  28. package/dist/tools/pdf/index.js +3 -0
  29. package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
  30. package/dist/tools/pdf/lib/pdf2md.js +76 -0
  31. package/dist/tools/pdf/manipulations.d.ts +13 -0
  32. package/dist/tools/pdf/manipulations.js +96 -0
  33. package/dist/tools/pdf/markdown.d.ts +7 -0
  34. package/dist/tools/pdf/markdown.js +37 -0
  35. package/dist/tools/pdf/utils.d.ts +12 -0
  36. package/dist/tools/pdf/utils.js +34 -0
  37. package/dist/tools/schemas.d.ts +167 -12
  38. package/dist/tools/schemas.js +54 -5
  39. package/dist/types.d.ts +2 -1
  40. package/dist/utils/ab-test.d.ts +8 -0
  41. package/dist/utils/ab-test.js +76 -0
  42. package/dist/utils/capture.js +5 -0
  43. package/dist/utils/feature-flags.js +7 -4
  44. package/dist/utils/files/base.d.ts +167 -0
  45. package/dist/utils/files/base.js +5 -0
  46. package/dist/utils/files/binary.d.ts +21 -0
  47. package/dist/utils/files/binary.js +65 -0
  48. package/dist/utils/files/excel.d.ts +24 -0
  49. package/dist/utils/files/excel.js +416 -0
  50. package/dist/utils/files/factory.d.ts +40 -0
  51. package/dist/utils/files/factory.js +101 -0
  52. package/dist/utils/files/image.d.ts +21 -0
  53. package/dist/utils/files/image.js +78 -0
  54. package/dist/utils/files/index.d.ts +10 -0
  55. package/dist/utils/files/index.js +13 -0
  56. package/dist/utils/files/pdf.d.ts +32 -0
  57. package/dist/utils/files/pdf.js +142 -0
  58. package/dist/utils/files/text.d.ts +63 -0
  59. package/dist/utils/files/text.js +357 -0
  60. package/dist/utils/open-browser.d.ts +9 -0
  61. package/dist/utils/open-browser.js +43 -0
  62. package/dist/utils/ripgrep-resolver.js +3 -2
  63. package/dist/utils/system-info.d.ts +5 -0
  64. package/dist/utils/system-info.js +71 -3
  65. package/dist/utils/usageTracker.js +6 -0
  66. package/dist/utils/welcome-onboarding.d.ts +9 -0
  67. package/dist/utils/welcome-onboarding.js +37 -0
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +14 -3
@@ -5,8 +5,8 @@ import { ServerResult } from '../types.js';
5
5
  */
6
6
  export declare function startProcess(args: unknown): Promise<ServerResult>;
7
7
  /**
8
- * Read output from a running process (renamed from read_output)
9
- * Includes early detection of process waiting for input
8
+ * Read output from a running process with file-like pagination
9
+ * Supports offset/length parameters for controlled reading
10
10
  */
11
11
  export declare function readProcessOutput(args: unknown): Promise<ServerResult>;
12
12
  /**
@@ -5,6 +5,75 @@ import { capture } from "../utils/capture.js";
5
5
  import { analyzeProcessState, cleanProcessOutput, formatProcessStateMessage } from '../utils/process-detection.js';
6
6
  import * as os from 'os';
7
7
  import { configManager } from '../config-manager.js';
8
+ import { spawn } from 'child_process';
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ // Get the directory where the MCP is installed (for ES module imports)
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const mcpRoot = path.resolve(__dirname, '..', '..');
16
+ // Track virtual Node sessions (PIDs that are actually Node fallback sessions)
17
+ const virtualNodeSessions = new Map();
18
+ let virtualPidCounter = -1000; // Use negative PIDs for virtual sessions
19
+ /**
20
+ * Execute Node.js code via temp file (fallback when Python unavailable)
21
+ * Creates temp .mjs file in MCP directory for ES module import access
22
+ */
23
+ async function executeNodeCode(code, timeout_ms = 30000) {
24
+ const tempFile = path.join(mcpRoot, `.mcp-exec-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
25
+ try {
26
+ await fs.writeFile(tempFile, code, 'utf8');
27
+ const result = await new Promise((resolve) => {
28
+ const proc = spawn(process.execPath, [tempFile], {
29
+ cwd: mcpRoot,
30
+ timeout: timeout_ms
31
+ });
32
+ let stdout = '';
33
+ let stderr = '';
34
+ proc.stdout.on('data', (data) => {
35
+ stdout += data.toString();
36
+ });
37
+ proc.stderr.on('data', (data) => {
38
+ stderr += data.toString();
39
+ });
40
+ proc.on('close', (exitCode) => {
41
+ resolve({ stdout, stderr, exitCode: exitCode ?? 1 });
42
+ });
43
+ proc.on('error', (err) => {
44
+ resolve({ stdout, stderr: stderr + '\n' + err.message, exitCode: 1 });
45
+ });
46
+ });
47
+ // Clean up temp file
48
+ await fs.unlink(tempFile).catch(() => { });
49
+ if (result.exitCode !== 0) {
50
+ return {
51
+ content: [{
52
+ type: "text",
53
+ text: `Execution failed (exit code ${result.exitCode}):\n${result.stderr}\n${result.stdout}`
54
+ }],
55
+ isError: true
56
+ };
57
+ }
58
+ return {
59
+ content: [{
60
+ type: "text",
61
+ text: result.stdout || '(no output)'
62
+ }]
63
+ };
64
+ }
65
+ catch (error) {
66
+ // Clean up temp file on error
67
+ await fs.unlink(tempFile).catch(() => { });
68
+ return {
69
+ content: [{
70
+ type: "text",
71
+ text: `Failed to execute Node.js code: ${error instanceof Error ? error.message : String(error)}`
72
+ }],
73
+ isError: true
74
+ };
75
+ }
76
+ }
8
77
  /**
9
78
  * Start a new process (renamed from execute_command)
10
79
  * Includes early detection of process waiting for input
@@ -37,6 +106,28 @@ export async function startProcess(args) {
37
106
  isError: true,
38
107
  };
39
108
  }
109
+ const commandToRun = parsed.data.command;
110
+ // Handle node:local - runs Node.js code directly on MCP server
111
+ if (commandToRun.trim() === 'node:local') {
112
+ const virtualPid = virtualPidCounter--;
113
+ virtualNodeSessions.set(virtualPid, { timeout_ms: parsed.data.timeout_ms || 30000 });
114
+ return {
115
+ content: [{
116
+ type: "text",
117
+ text: `Node.js session started with PID ${virtualPid} (MCP server execution)
118
+
119
+ IMPORTANT: Each interact_with_process call runs as a FRESH script.
120
+ State is NOT preserved between calls. Include ALL code in ONE call:
121
+ - imports, file reading, processing, and output together.
122
+
123
+ Available libraries:
124
+ - ExcelJS for Excel files: import ExcelJS from 'exceljs'
125
+ - All Node.js built-ins: fs, path, http, crypto, etc.
126
+
127
+ 🔄 Ready for code - send complete self-contained script via interact_with_process.`
128
+ }],
129
+ };
130
+ }
40
131
  let shellUsed = parsed.data.shell;
41
132
  if (!shellUsed) {
42
133
  const config = await configManager.getConfig();
@@ -56,7 +147,7 @@ export async function startProcess(args) {
56
147
  }
57
148
  }
58
149
  }
59
- const result = await terminalManager.executeCommand(parsed.data.command, parsed.data.timeout_ms, shellUsed, parsed.data.verbose_timing || false);
150
+ const result = await terminalManager.executeCommand(commandToRun, parsed.data.timeout_ms, shellUsed, parsed.data.verbose_timing || false);
60
151
  if (result.pid === -1) {
61
152
  return {
62
153
  content: [{ type: "text", text: result.output }],
@@ -110,8 +201,8 @@ function formatTimingInfo(timing) {
110
201
  return msg;
111
202
  }
112
203
  /**
113
- * Read output from a running process (renamed from read_output)
114
- * Includes early detection of process waiting for input
204
+ * Read output from a running process with file-like pagination
205
+ * Supports offset/length parameters for controlled reading
115
206
  */
116
207
  export async function readProcessOutput(args) {
117
208
  const parsed = ReadProcessOutputArgsSchema.safeParse(args);
@@ -121,229 +212,113 @@ export async function readProcessOutput(args) {
121
212
  isError: true,
122
213
  };
123
214
  }
124
- const { pid, timeout_ms = 5000, verbose_timing = false } = parsed.data;
125
- const session = terminalManager.getSession(pid);
126
- if (!session) {
127
- // Check if this is a completed session
128
- const completedOutput = terminalManager.getNewOutput(pid);
129
- if (completedOutput) {
130
- return {
131
- content: [{
132
- type: "text",
133
- text: completedOutput
134
- }],
135
- };
136
- }
137
- // Neither active nor completed session found
138
- return {
139
- content: [{ type: "text", text: `No session found for PID ${pid}` }],
140
- isError: true,
141
- };
142
- }
143
- let output = "";
144
- let timeoutReached = false;
145
- let earlyExit = false;
146
- let processState;
215
+ // Get default line limit from config
216
+ const config = await configManager.getConfig();
217
+ const defaultLength = config.fileReadLineLimit ?? 1000;
218
+ const { pid, timeout_ms = 5000, offset = 0, // 0 = from last read, positive = absolute, negative = tail
219
+ length = defaultLength, // Default from config, same as file reading
220
+ verbose_timing = false } = parsed.data;
147
221
  // Timing telemetry
148
222
  const startTime = Date.now();
149
- let firstOutputTime;
150
- let lastOutputTime;
151
- const outputEvents = [];
152
- let exitReason = 'timeout';
153
- try {
154
- const outputPromise = new Promise((resolve) => {
155
- const initialOutput = terminalManager.getNewOutput(pid);
156
- if (initialOutput && initialOutput.length > 0) {
157
- const now = Date.now();
158
- if (!firstOutputTime)
159
- firstOutputTime = now;
160
- lastOutputTime = now;
161
- if (verbose_timing) {
162
- outputEvents.push({
163
- timestamp: now,
164
- deltaMs: now - startTime,
165
- source: 'initial_poll',
166
- length: initialOutput.length,
167
- snippet: initialOutput.slice(0, 50).replace(/\n/g, '\\n')
168
- });
169
- }
170
- // Immediate check on existing output
171
- const state = analyzeProcessState(initialOutput, pid);
172
- if (state.isWaitingForInput) {
173
- earlyExit = true;
174
- processState = state;
175
- exitReason = 'early_exit_periodic_check';
176
- }
177
- resolve(initialOutput);
178
- return;
179
- }
180
- let resolved = false;
181
- let interval = null;
182
- let timeout = null;
183
- // Quick prompt patterns for immediate detection
184
- const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/;
185
- const cleanup = () => {
186
- if (interval)
187
- clearInterval(interval);
188
- if (timeout)
189
- clearTimeout(timeout);
190
- };
191
- let resolveOnce = (value, isTimeout = false) => {
192
- if (resolved)
223
+ // For active sessions with no new output yet, optionally wait for output
224
+ const session = terminalManager.getSession(pid);
225
+ if (session && offset === 0) {
226
+ // Wait for new output to arrive (only for "new output" reads, not absolute/tail)
227
+ const waitForOutput = () => {
228
+ return new Promise((resolve) => {
229
+ // Check if there's already new output
230
+ const currentLines = terminalManager.getOutputLineCount(pid) || 0;
231
+ if (currentLines > session.lastReadIndex) {
232
+ resolve();
193
233
  return;
194
- resolved = true;
195
- cleanup();
196
- timeoutReached = isTimeout;
197
- if (isTimeout)
198
- exitReason = 'timeout';
199
- resolve(value);
200
- };
201
- // Monitor for new output with immediate detection
202
- const session = terminalManager.getSession(pid);
203
- if (session && session.process && session.process.stdout && session.process.stderr) {
204
- const immediateDetector = (data, source) => {
205
- const text = data.toString();
206
- const now = Date.now();
207
- if (!firstOutputTime)
208
- firstOutputTime = now;
209
- lastOutputTime = now;
210
- if (verbose_timing) {
211
- outputEvents.push({
212
- timestamp: now,
213
- deltaMs: now - startTime,
214
- source,
215
- length: text.length,
216
- snippet: text.slice(0, 50).replace(/\n/g, '\\n')
217
- });
218
- }
219
- // Immediate check for obvious prompts
220
- if (quickPromptPatterns.test(text)) {
221
- const newOutput = terminalManager.getNewOutput(pid) || text;
222
- const state = analyzeProcessState(output + newOutput, pid);
223
- if (state.isWaitingForInput) {
224
- earlyExit = true;
225
- processState = state;
226
- exitReason = 'early_exit_quick_pattern';
227
- if (verbose_timing && outputEvents.length > 0) {
228
- outputEvents[outputEvents.length - 1].matchedPattern = 'quick_pattern';
229
- }
230
- resolveOnce(newOutput);
231
- return;
232
- }
233
- }
234
- };
235
- const stdoutDetector = (data) => immediateDetector(data, 'stdout');
236
- const stderrDetector = (data) => immediateDetector(data, 'stderr');
237
- session.process.stdout.on('data', stdoutDetector);
238
- session.process.stderr.on('data', stderrDetector);
239
- // Cleanup immediate detectors when done
240
- const originalResolveOnce = resolveOnce;
241
- const cleanupDetectors = () => {
242
- if (session.process.stdout) {
243
- session.process.stdout.off('data', stdoutDetector);
244
- }
245
- if (session.process.stderr) {
246
- session.process.stderr.off('data', stderrDetector);
247
- }
248
- };
249
- // Override resolveOnce to include cleanup
250
- const resolveOnceWithCleanup = (value, isTimeout = false) => {
251
- cleanupDetectors();
252
- originalResolveOnce(value, isTimeout);
234
+ }
235
+ let resolved = false;
236
+ let interval = null;
237
+ let timeout = null;
238
+ const cleanup = () => {
239
+ if (interval)
240
+ clearInterval(interval);
241
+ if (timeout)
242
+ clearTimeout(timeout);
253
243
  };
254
- // Replace the local resolveOnce reference
255
- resolveOnce = resolveOnceWithCleanup;
256
- }
257
- interval = setInterval(() => {
258
- const newOutput = terminalManager.getNewOutput(pid);
259
- if (newOutput && newOutput.length > 0) {
260
- const now = Date.now();
261
- if (!firstOutputTime)
262
- firstOutputTime = now;
263
- lastOutputTime = now;
264
- if (verbose_timing) {
265
- outputEvents.push({
266
- timestamp: now,
267
- deltaMs: now - startTime,
268
- source: 'periodic_poll',
269
- length: newOutput.length,
270
- snippet: newOutput.slice(0, 50).replace(/\n/g, '\\n')
271
- });
272
- }
273
- const currentOutput = output + newOutput;
274
- const state = analyzeProcessState(currentOutput, pid);
275
- // Early exit if process is clearly waiting for input
276
- if (state.isWaitingForInput) {
277
- earlyExit = true;
278
- processState = state;
279
- exitReason = 'early_exit_periodic_check';
280
- if (verbose_timing && outputEvents.length > 0) {
281
- outputEvents[outputEvents.length - 1].matchedPattern = 'periodic_check';
282
- }
283
- resolveOnce(newOutput);
284
- return;
285
- }
286
- output = currentOutput;
287
- // Continue collecting if still running
288
- if (!state.isFinished) {
244
+ const resolveOnce = () => {
245
+ if (resolved)
289
246
  return;
247
+ resolved = true;
248
+ cleanup();
249
+ resolve();
250
+ };
251
+ // Poll for new output
252
+ interval = setInterval(() => {
253
+ const newLineCount = terminalManager.getOutputLineCount(pid) || 0;
254
+ if (newLineCount > session.lastReadIndex) {
255
+ resolveOnce();
290
256
  }
291
- // Process finished
292
- processState = state;
293
- exitReason = 'process_finished';
294
- resolveOnce(newOutput);
295
- }
296
- }, 50); // Check every 50ms for faster response
297
- timeout = setTimeout(() => {
298
- const finalOutput = terminalManager.getNewOutput(pid) || "";
299
- resolveOnce(finalOutput, true);
300
- }, timeout_ms);
301
- });
302
- const newOutput = await outputPromise;
303
- output += newOutput;
304
- // Analyze final state if not already done
305
- if (!processState) {
306
- processState = analyzeProcessState(output, pid);
307
- }
257
+ }, 50);
258
+ // Timeout
259
+ timeout = setTimeout(() => {
260
+ resolveOnce();
261
+ }, timeout_ms);
262
+ });
263
+ };
264
+ await waitForOutput();
308
265
  }
309
- catch (error) {
266
+ // Read output with pagination
267
+ const result = terminalManager.readOutputPaginated(pid, offset, length);
268
+ if (!result) {
310
269
  return {
311
- content: [{ type: "text", text: `Error reading output: ${error}` }],
270
+ content: [{ type: "text", text: `No session found for PID ${pid}` }],
312
271
  isError: true,
313
272
  };
314
273
  }
315
- // Format response based on what we detected
274
+ // Join lines back into string
275
+ const output = result.lines.join('\n');
276
+ // Generate status message similar to file reading
316
277
  let statusMessage = '';
317
- if (earlyExit && processState?.isWaitingForInput) {
318
- statusMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`;
278
+ if (offset < 0) {
279
+ // Tail read - match file reading format for consistency
280
+ statusMessage = `[Reading last ${result.readCount} lines (total: ${result.totalLines} lines)]`;
319
281
  }
320
- else if (processState?.isFinished) {
321
- statusMessage = `\n✅ ${formatProcessStateMessage(processState, pid)}`;
282
+ else if (offset === 0) {
283
+ // "New output" read
284
+ if (result.remaining > 0) {
285
+ statusMessage = `[Reading ${result.readCount} new lines from line ${result.readFrom} (total: ${result.totalLines} lines, ${result.remaining} remaining)]`;
286
+ }
287
+ else {
288
+ statusMessage = `[Reading ${result.readCount} new lines (total: ${result.totalLines} lines)]`;
289
+ }
322
290
  }
323
- else if (timeoutReached) {
324
- statusMessage = '\n⏱️ Timeout reached - process may still be running';
291
+ else {
292
+ // Absolute position read
293
+ statusMessage = `[Reading ${result.readCount} lines from line ${result.readFrom} (total: ${result.totalLines} lines, ${result.remaining} remaining)]`;
294
+ }
295
+ // Add process state info
296
+ let processStateMessage = '';
297
+ if (result.isComplete) {
298
+ const runtimeStr = result.runtimeMs !== undefined
299
+ ? ` (runtime: ${(result.runtimeMs / 1000).toFixed(2)}s)`
300
+ : '';
301
+ processStateMessage = `\n✅ Process completed with exit code ${result.exitCode}${runtimeStr}`;
302
+ }
303
+ else if (session) {
304
+ // Analyze state for running processes
305
+ const fullOutput = session.outputLines.join('\n');
306
+ const processState = analyzeProcessState(fullOutput, pid);
307
+ if (processState.isWaitingForInput) {
308
+ processStateMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`;
309
+ }
325
310
  }
326
311
  // Add timing information if requested
327
312
  let timingMessage = '';
328
313
  if (verbose_timing) {
329
314
  const endTime = Date.now();
330
- const timingInfo = {
331
- startTime,
332
- endTime,
333
- totalDurationMs: endTime - startTime,
334
- exitReason,
335
- firstOutputTime,
336
- lastOutputTime,
337
- timeToFirstOutputMs: firstOutputTime ? firstOutputTime - startTime : undefined,
338
- outputEvents: outputEvents.length > 0 ? outputEvents : undefined
339
- };
340
- timingMessage = formatTimingInfo(timingInfo);
315
+ timingMessage = `\n\n📊 Timing: ${endTime - startTime}ms`;
341
316
  }
342
- const responseText = output || 'No new output available';
317
+ const responseText = output || '(No output in requested range)';
343
318
  return {
344
319
  content: [{
345
320
  type: "text",
346
- text: `${responseText}${statusMessage}${timingMessage}`
321
+ text: `${statusMessage}\n\n${responseText}${processStateMessage}${timingMessage}`
347
322
  }],
348
323
  };
349
324
  }
@@ -363,6 +338,21 @@ export async function interactWithProcess(args) {
363
338
  };
364
339
  }
365
340
  const { pid, input, timeout_ms = 8000, wait_for_prompt = true, verbose_timing = false } = parsed.data;
341
+ // Get config for output line limit
342
+ const config = await configManager.getConfig();
343
+ const maxOutputLines = config.fileReadLineLimit ?? 1000;
344
+ // Check if this is a virtual Node session (node:local)
345
+ if (virtualNodeSessions.has(pid)) {
346
+ const session = virtualNodeSessions.get(pid);
347
+ capture('server_interact_with_process_node_fallback', {
348
+ pid: pid,
349
+ inputLength: input.length
350
+ });
351
+ // Execute code via temp file approach
352
+ // Respect per-call timeout if provided, otherwise use session default
353
+ const effectiveTimeout = timeout_ms ?? session.timeout_ms;
354
+ return executeNodeCode(input, effectiveTimeout);
355
+ }
366
356
  // Timing telemetry
367
357
  const startTime = Date.now();
368
358
  let firstOutputTime;
@@ -374,6 +364,9 @@ export async function interactWithProcess(args) {
374
364
  pid: pid,
375
365
  inputLength: input.length
376
366
  });
367
+ // Capture output snapshot BEFORE sending input
368
+ // This handles REPLs where output is appended to the prompt line
369
+ const outputSnapshot = terminalManager.captureOutputSnapshot(pid);
377
370
  const success = terminalManager.sendInputToProcess(pid, input);
378
371
  if (!success) {
379
372
  return {
@@ -419,6 +412,7 @@ export async function interactWithProcess(args) {
419
412
  const pollIntervalMs = 50; // Poll every 50ms for faster response
420
413
  const maxAttempts = Math.ceil(timeout_ms / pollIntervalMs);
421
414
  let interval = null;
415
+ let lastOutputLength = 0; // Track output length to detect new output
422
416
  let resolveOnce = () => {
423
417
  if (resolved)
424
418
  return;
@@ -431,8 +425,11 @@ export async function interactWithProcess(args) {
431
425
  interval = setInterval(() => {
432
426
  if (resolved)
433
427
  return;
434
- const newOutput = terminalManager.getNewOutput(pid);
435
- if (newOutput && newOutput.length > 0) {
428
+ // Use snapshot-based reading to handle REPL prompt line appending
429
+ const newOutput = outputSnapshot
430
+ ? terminalManager.getOutputSinceSnapshot(pid, outputSnapshot)
431
+ : terminalManager.getNewOutput(pid);
432
+ if (newOutput && newOutput.length > lastOutputLength) {
436
433
  const now = Date.now();
437
434
  if (!firstOutputTime)
438
435
  firstOutputTime = now;
@@ -442,11 +439,12 @@ export async function interactWithProcess(args) {
442
439
  timestamp: now,
443
440
  deltaMs: now - startTime,
444
441
  source: 'periodic_poll',
445
- length: newOutput.length,
446
- snippet: newOutput.slice(0, 50).replace(/\n/g, '\\n')
442
+ length: newOutput.length - lastOutputLength,
443
+ snippet: newOutput.slice(lastOutputLength, lastOutputLength + 50).replace(/\n/g, '\\n')
447
444
  });
448
445
  }
449
- output += newOutput;
446
+ output = newOutput; // Replace with full output since snapshot
447
+ lastOutputLength = newOutput.length;
450
448
  // Analyze current state
451
449
  processState = analyzeProcessState(output, pid);
452
450
  // Exit early if we detect the process is waiting for input
@@ -476,8 +474,17 @@ export async function interactWithProcess(args) {
476
474
  };
477
475
  await waitForResponse();
478
476
  // Clean and format output
479
- const cleanOutput = cleanProcessOutput(output, input);
477
+ let cleanOutput = cleanProcessOutput(output, input);
480
478
  const timeoutReached = !earlyExit && !processState?.isFinished && !processState?.isWaitingForInput;
479
+ // Apply output line limit to prevent context overflow
480
+ let truncationMessage = '';
481
+ const outputLines = cleanOutput.split('\n');
482
+ if (outputLines.length > maxOutputLines) {
483
+ const truncatedLines = outputLines.slice(0, maxOutputLines);
484
+ cleanOutput = truncatedLines.join('\n');
485
+ const remainingLines = outputLines.length - maxOutputLines;
486
+ truncationMessage = `\n\n⚠️ Output truncated: showing ${maxOutputLines} of ${outputLines.length} lines (${remainingLines} hidden). Use read_process_output with offset/length for full output.`;
487
+ }
481
488
  // Determine final state
482
489
  if (!processState) {
483
490
  processState = analyzeProcessState(output, pid);
@@ -527,6 +534,9 @@ export async function interactWithProcess(args) {
527
534
  if (statusMessage) {
528
535
  responseText += `\n\n${statusMessage}`;
529
536
  }
537
+ if (truncationMessage) {
538
+ responseText += truncationMessage;
539
+ }
530
540
  if (timingMessage) {
531
541
  responseText += timingMessage;
532
542
  }
@@ -559,13 +569,24 @@ export async function forceTerminate(args) {
559
569
  isError: true,
560
570
  };
561
571
  }
562
- const success = terminalManager.forceTerminate(parsed.data.pid);
572
+ const pid = parsed.data.pid;
573
+ // Handle virtual Node.js sessions (node:local)
574
+ if (virtualNodeSessions.has(pid)) {
575
+ virtualNodeSessions.delete(pid);
576
+ return {
577
+ content: [{
578
+ type: "text",
579
+ text: `Cleared virtual Node.js session ${pid}`
580
+ }],
581
+ };
582
+ }
583
+ const success = terminalManager.forceTerminate(pid);
563
584
  return {
564
585
  content: [{
565
586
  type: "text",
566
587
  text: success
567
- ? `Successfully initiated termination of session ${parsed.data.pid}`
568
- : `No active session found for PID ${parsed.data.pid}`
588
+ ? `Successfully initiated termination of session ${pid}`
589
+ : `No active session found for PID ${pid}`
569
590
  }],
570
591
  };
571
592
  }
@@ -574,12 +595,21 @@ export async function forceTerminate(args) {
574
595
  */
575
596
  export async function listSessions() {
576
597
  const sessions = terminalManager.listActiveSessions();
598
+ // Include virtual Node.js sessions
599
+ const virtualSessions = Array.from(virtualNodeSessions.entries()).map(([pid, session]) => ({
600
+ pid,
601
+ type: 'node:local',
602
+ timeout_ms: session.timeout_ms
603
+ }));
604
+ const realSessionsText = sessions.map(s => `PID: ${s.pid}, Blocked: ${s.isBlocked}, Runtime: ${Math.round(s.runtime / 1000)}s`);
605
+ const virtualSessionsText = virtualSessions.map(s => `PID: ${s.pid} (node:local), Timeout: ${s.timeout_ms}ms`);
606
+ const allSessions = [...realSessionsText, ...virtualSessionsText];
577
607
  return {
578
608
  content: [{
579
609
  type: "text",
580
- text: sessions.length === 0
610
+ text: allSessions.length === 0
581
611
  ? 'No active sessions'
582
- : sessions.map(s => `PID: ${s.pid}, Blocked: ${s.isBlocked}, Runtime: ${Math.round(s.runtime / 1000)}s`).join('\n')
612
+ : allSessions.join('\n')
583
613
  }],
584
614
  };
585
615
  }
@@ -1,2 +1,3 @@
1
1
  export declare function getMimeType(filePath: string): string;
2
+ export declare function isPdfFile(mimeType: string): boolean;
2
3
  export declare function isImageFile(mimeType: string): boolean;
@@ -1,6 +1,9 @@
1
1
  // Simple MIME type detection based on file extension
2
2
  export function getMimeType(filePath) {
3
3
  const extension = filePath.toLowerCase().split('.').pop() || '';
4
+ if (extension === "pdf") {
5
+ return "application/pdf";
6
+ }
4
7
  // Image types - only the formats we can display
5
8
  const imageTypes = {
6
9
  'png': 'image/png',
@@ -16,6 +19,10 @@ export function getMimeType(filePath) {
16
19
  // Default to text/plain for all other files
17
20
  return 'text/plain';
18
21
  }
22
+ export function isPdfFile(mimeType) {
23
+ const [baseType] = mimeType.toLowerCase().split(';');
24
+ return baseType.trim() === 'application/pdf';
25
+ }
19
26
  export function isImageFile(mimeType) {
20
27
  return mimeType.startsWith('image/');
21
28
  }
@@ -0,0 +1,34 @@
1
+ export interface ImageInfo {
2
+ /** Object ID within PDF */
3
+ objId: number;
4
+ width: number;
5
+ height: number;
6
+ /** Raw image data as base64 */
7
+ data: string;
8
+ /** MIME type of the image */
9
+ mimeType: string;
10
+ /** Original size in bytes before compression */
11
+ originalSize?: number;
12
+ /** Compressed size in bytes */
13
+ compressedSize?: number;
14
+ }
15
+ export interface PageImages {
16
+ pageNumber: number;
17
+ images: ImageInfo[];
18
+ }
19
+ export interface ImageCompressionOptions {
20
+ /** Output format: 'jpeg' | 'webp' */
21
+ format?: 'jpeg' | 'webp';
22
+ /** Quality for lossy formats (0-100, default 85) */
23
+ quality?: number;
24
+ /** Maximum dimension to resize to (maintains aspect ratio) */
25
+ maxDimension?: number;
26
+ }
27
+ /**
28
+ * Optimized image extraction from PDF using unpdf's built-in extractImages method
29
+ * @param pdfBuffer PDF file as Uint8Array
30
+ * @param pageNumbers Optional array of specific page numbers to process
31
+ * @param compressionOptions Image compression settings
32
+ * @returns Record of page numbers to extracted images
33
+ */
34
+ export declare function extractImagesFromPdf(pdfBuffer: Uint8Array, pageNumbers?: number[], compressionOptions?: ImageCompressionOptions): Promise<Record<number, ImageInfo[]>>;