@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.
- package/README.md +14 -55
- package/dist/config-manager.d.ts +5 -0
- package/dist/config-manager.js +9 -0
- package/dist/custom-stdio.d.ts +1 -0
- package/dist/custom-stdio.js +19 -0
- package/dist/handlers/filesystem-handlers.d.ts +4 -0
- package/dist/handlers/filesystem-handlers.js +120 -14
- package/dist/handlers/node-handlers.d.ts +6 -0
- package/dist/handlers/node-handlers.js +73 -0
- package/dist/index.js +5 -3
- package/dist/search-manager.d.ts +25 -0
- package/dist/search-manager.js +212 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +188 -73
- package/dist/terminal-manager.d.ts +56 -2
- package/dist/terminal-manager.js +169 -13
- package/dist/tools/edit.d.ts +28 -4
- package/dist/tools/edit.js +87 -4
- package/dist/tools/filesystem.d.ts +23 -12
- package/dist/tools/filesystem.js +201 -416
- package/dist/tools/improved-process-tools.d.ts +2 -2
- package/dist/tools/improved-process-tools.js +244 -214
- package/dist/tools/mime-types.d.ts +1 -0
- package/dist/tools/mime-types.js +7 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +96 -0
- package/dist/tools/pdf/markdown.d.ts +7 -0
- package/dist/tools/pdf/markdown.js +37 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/schemas.d.ts +167 -12
- package/dist/tools/schemas.js +54 -5
- package/dist/types.d.ts +2 -1
- package/dist/utils/ab-test.d.ts +8 -0
- package/dist/utils/ab-test.js +76 -0
- package/dist/utils/capture.js +5 -0
- package/dist/utils/feature-flags.js +7 -4
- package/dist/utils/files/base.d.ts +167 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +416 -0
- package/dist/utils/files/factory.d.ts +40 -0
- package/dist/utils/files/factory.js +101 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +357 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +43 -0
- package/dist/utils/ripgrep-resolver.js +3 -2
- package/dist/utils/system-info.d.ts +5 -0
- package/dist/utils/system-info.js +71 -3
- package/dist/utils/usageTracker.js +6 -0
- package/dist/utils/welcome-onboarding.d.ts +9 -0
- package/dist/utils/welcome-onboarding.js +37 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
9
|
-
*
|
|
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(
|
|
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
|
|
114
|
-
*
|
|
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
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
resolveOnce(
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
266
|
+
// Read output with pagination
|
|
267
|
+
const result = terminalManager.readOutputPaginated(pid, offset, length);
|
|
268
|
+
if (!result) {
|
|
310
269
|
return {
|
|
311
|
-
content: [{ type: "text", text: `
|
|
270
|
+
content: [{ type: "text", text: `No session found for PID ${pid}` }],
|
|
312
271
|
isError: true,
|
|
313
272
|
};
|
|
314
273
|
}
|
|
315
|
-
//
|
|
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 (
|
|
318
|
-
|
|
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 (
|
|
321
|
-
|
|
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
|
|
324
|
-
|
|
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
|
-
|
|
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
|
|
317
|
+
const responseText = output || '(No output in requested range)';
|
|
343
318
|
return {
|
|
344
319
|
content: [{
|
|
345
320
|
type: "text",
|
|
346
|
-
text: `${responseText}${
|
|
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
|
-
|
|
435
|
-
|
|
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(
|
|
442
|
+
length: newOutput.length - lastOutputLength,
|
|
443
|
+
snippet: newOutput.slice(lastOutputLength, lastOutputLength + 50).replace(/\n/g, '\\n')
|
|
447
444
|
});
|
|
448
445
|
}
|
|
449
|
-
output
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
568
|
-
: `No active session found for 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:
|
|
610
|
+
text: allSessions.length === 0
|
|
581
611
|
? 'No active sessions'
|
|
582
|
-
:
|
|
612
|
+
: allSessions.join('\n')
|
|
583
613
|
}],
|
|
584
614
|
};
|
|
585
615
|
}
|
package/dist/tools/mime-types.js
CHANGED
|
@@ -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[]>>;
|