@wonderwhy-er/desktop-commander 0.2.41 → 0.2.43
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 +0 -2
- package/dist/handlers/filesystem-handlers.js +24 -4
- package/dist/remote-device/remote-channel.d.ts +14 -0
- package/dist/remote-device/remote-channel.js +107 -28
- package/dist/server.d.ts +6 -1
- package/dist/server.js +112 -25
- package/dist/setup-claude-server.js +56 -50
- package/dist/terminal-manager.d.ts +18 -0
- package/dist/terminal-manager.js +130 -18
- package/dist/tools/edit.js +11 -4
- package/dist/tools/filesystem.d.ts +5 -0
- package/dist/tools/filesystem.js +43 -0
- package/dist/tools/fuzzySearch.d.ts +10 -20
- package/dist/tools/fuzzySearch.js +66 -105
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.js +8 -1
- package/dist/tools/pdf/markdown.d.ts +13 -0
- package/dist/tools/pdf/markdown.js +93 -29
- package/dist/tools/schemas.d.ts +20 -0
- package/dist/tools/schemas.js +45 -2
- package/dist/track-installation.js +57 -38
- package/dist/types.d.ts +6 -1
- package/dist/ui/contracts.d.ts +1 -1
- package/dist/ui/contracts.js +4 -1
- package/dist/ui/file-preview/preview-runtime.js +111 -113
- package/dist/ui/file-preview/src/app.js +19 -17
- package/dist/ui/file-preview/src/directory-controller.js +3 -3
- package/dist/ui/file-preview/src/file-type-handlers.js +2 -2
- package/dist/ui/file-preview/src/host/external-actions.d.ts +0 -11
- package/dist/ui/file-preview/src/host/external-actions.js +0 -39
- package/dist/ui/file-preview/src/markdown/controller.js +3 -1
- package/dist/ui/file-preview/src/panel-actions.js +2 -2
- package/dist/uninstall-claude-server.js +54 -47
- package/dist/utils/ab-test.d.ts +4 -0
- package/dist/utils/ab-test.js +6 -0
- package/dist/utils/capture.d.ts +10 -2
- package/dist/utils/capture.js +92 -60
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
|
@@ -10,10 +10,9 @@ import { version as nodeVersion } from 'process';
|
|
|
10
10
|
import * as https from 'https';
|
|
11
11
|
import { randomUUID } from 'crypto';
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
|
13
|
+
// Telemetry proxy configuration
|
|
14
|
+
const TELEMETRY_PROXY_URL = 'https://telemetry.desktopcommander.app/mp/collect';
|
|
15
|
+
const TELEMETRY_PROXY_FALLBACK_URL = 'https://dc-telemetry-proxy-83847352264.europe-west1.run.app/mp/collect';
|
|
17
16
|
|
|
18
17
|
// Generate a unique anonymous ID using UUID - consistent with privacy policy
|
|
19
18
|
let uniqueUserId = 'unknown';
|
|
@@ -302,11 +301,6 @@ async function enhancedGetTrackingProperties(additionalProps = {}) {
|
|
|
302
301
|
async function trackEvent(eventName, additionalProps = {}) {
|
|
303
302
|
const trackingStep = addSetupStep(`track_event_${eventName}`);
|
|
304
303
|
|
|
305
|
-
if (!GA_MEASUREMENT_ID || !GA_API_SECRET) {
|
|
306
|
-
updateSetupStep(trackingStep, 'skipped', new Error('GA not configured'));
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
304
|
// Add retry capability
|
|
311
305
|
const maxRetries = 2;
|
|
312
306
|
let attempt = 0;
|
|
@@ -319,7 +313,7 @@ async function trackEvent(eventName, additionalProps = {}) {
|
|
|
319
313
|
// Get enriched properties
|
|
320
314
|
const eventProperties = await enhancedGetTrackingProperties(additionalProps);
|
|
321
315
|
|
|
322
|
-
// Prepare
|
|
316
|
+
// Prepare telemetry payload
|
|
323
317
|
const payload = {
|
|
324
318
|
client_id: uniqueUserId,
|
|
325
319
|
non_personalized_ads: false,
|
|
@@ -330,7 +324,7 @@ async function trackEvent(eventName, additionalProps = {}) {
|
|
|
330
324
|
}]
|
|
331
325
|
};
|
|
332
326
|
|
|
333
|
-
// Send to
|
|
327
|
+
// Send to telemetry proxy
|
|
334
328
|
const postData = JSON.stringify(payload);
|
|
335
329
|
|
|
336
330
|
const options = {
|
|
@@ -341,44 +335,8 @@ async function trackEvent(eventName, additionalProps = {}) {
|
|
|
341
335
|
}
|
|
342
336
|
};
|
|
343
337
|
|
|
344
|
-
const result = await
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// Set timeout to prevent blocking
|
|
348
|
-
const timeoutId = setTimeout(() => {
|
|
349
|
-
req.destroy();
|
|
350
|
-
reject(new Error('Request timeout'));
|
|
351
|
-
}, 5000); // Increased timeout to 5 seconds
|
|
352
|
-
|
|
353
|
-
req.on('error', (error) => {
|
|
354
|
-
clearTimeout(timeoutId);
|
|
355
|
-
reject(error);
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
req.on('response', (res) => {
|
|
359
|
-
clearTimeout(timeoutId);
|
|
360
|
-
let data = '';
|
|
361
|
-
|
|
362
|
-
res.on('data', (chunk) => {
|
|
363
|
-
data += chunk;
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
res.on('error', (error) => {
|
|
367
|
-
reject(error);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
res.on('end', () => {
|
|
371
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
372
|
-
resolve({ success: true, data });
|
|
373
|
-
} else {
|
|
374
|
-
reject(new Error(`HTTP error ${res.statusCode}: ${data}`));
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
req.write(postData);
|
|
380
|
-
req.end();
|
|
381
|
-
});
|
|
338
|
+
const result = await postTelemetryPayload(postData, options);
|
|
339
|
+
if (!result.success) throw new Error('Telemetry proxy request failed');
|
|
382
340
|
|
|
383
341
|
updateSetupStep(trackingStep, 'completed');
|
|
384
342
|
return result;
|
|
@@ -397,6 +355,54 @@ async function trackEvent(eventName, additionalProps = {}) {
|
|
|
397
355
|
return false;
|
|
398
356
|
}
|
|
399
357
|
|
|
358
|
+
async function postTelemetryPayload(postData, options) {
|
|
359
|
+
for (const endpoint of [TELEMETRY_PROXY_URL, TELEMETRY_PROXY_FALLBACK_URL]) {
|
|
360
|
+
const result = await new Promise((resolve) => {
|
|
361
|
+
let settled = false;
|
|
362
|
+
let timeoutId;
|
|
363
|
+
const finish = (result) => {
|
|
364
|
+
if (settled) return;
|
|
365
|
+
settled = true;
|
|
366
|
+
clearTimeout(timeoutId);
|
|
367
|
+
resolve(result);
|
|
368
|
+
};
|
|
369
|
+
const req = https.request(endpoint, options);
|
|
370
|
+
|
|
371
|
+
timeoutId = setTimeout(() => {
|
|
372
|
+
req.destroy();
|
|
373
|
+
finish({ success: false, data: '' });
|
|
374
|
+
}, 5000);
|
|
375
|
+
|
|
376
|
+
req.on('error', () => {
|
|
377
|
+
finish({ success: false, data: '' });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
req.on('response', (res) => {
|
|
381
|
+
let data = '';
|
|
382
|
+
res.on('data', (chunk) => {
|
|
383
|
+
data += chunk;
|
|
384
|
+
});
|
|
385
|
+
res.on('error', () => {
|
|
386
|
+
finish({ success: false, data: '' });
|
|
387
|
+
});
|
|
388
|
+
res.on('end', () => {
|
|
389
|
+
finish({ success: res.statusCode >= 200 && res.statusCode < 300, data });
|
|
390
|
+
});
|
|
391
|
+
res.on('close', () => {
|
|
392
|
+
finish({ success: false, data: '' });
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
req.write(postData);
|
|
397
|
+
req.end();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (result.success) return result;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return { success: false, data: '' };
|
|
404
|
+
}
|
|
405
|
+
|
|
400
406
|
// Ensure tracking completes before process exits
|
|
401
407
|
async function ensureTrackingCompleted(eventName, additionalProps = {}, timeoutMs = 6000) {
|
|
402
408
|
return new Promise(async (resolve) => {
|
|
@@ -933,4 +939,4 @@ if (process.argv.length >= 2 && process.argv[1] === fileURLToPath(import.meta.ur
|
|
|
933
939
|
process.exit(1);
|
|
934
940
|
}, 1000);
|
|
935
941
|
});
|
|
936
|
-
}
|
|
942
|
+
}
|
|
@@ -5,7 +5,18 @@ interface CompletedSession {
|
|
|
5
5
|
exitCode: number | null;
|
|
6
6
|
startTime: Date;
|
|
7
7
|
endTime: Date;
|
|
8
|
+
evictedLines: number;
|
|
9
|
+
evictedChars: number;
|
|
8
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Output buffering caps. Without a cap, a process emitting enough output makes
|
|
13
|
+
* string concatenation throw "RangeError: Invalid string length" at V8's max
|
|
14
|
+
* string size (~536M chars) inside a stdout 'data' handler — an uncaught
|
|
15
|
+
* exception that kills the whole server (index.ts exits on uncaughtException).
|
|
16
|
+
* The cap also bounds the join() cost in snapshot reads and the periodic
|
|
17
|
+
* process-state scan, both of which are O(total output).
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_BUFFERED_OUTPUT_CHARS: number;
|
|
9
20
|
export interface PaginatedOutputResult {
|
|
10
21
|
lines: string[];
|
|
11
22
|
totalLines: number;
|
|
@@ -15,6 +26,7 @@ export interface PaginatedOutputResult {
|
|
|
15
26
|
isComplete: boolean;
|
|
16
27
|
exitCode?: number | null;
|
|
17
28
|
runtimeMs?: number;
|
|
29
|
+
evictedLines?: number;
|
|
18
30
|
}
|
|
19
31
|
export declare class TerminalManager {
|
|
20
32
|
private sessions;
|
|
@@ -72,6 +84,12 @@ export declare class TerminalManager {
|
|
|
72
84
|
totalChars: number;
|
|
73
85
|
lineCount: number;
|
|
74
86
|
}): string | null;
|
|
87
|
+
/**
|
|
88
|
+
* New output since a snapshot, in absolute (since process start) offsets.
|
|
89
|
+
* If eviction dropped part of the unseen output, returns what the buffer
|
|
90
|
+
* still holds — the oldest unseen chars are lost to the cap.
|
|
91
|
+
*/
|
|
92
|
+
private static outputSinceSnapshot;
|
|
75
93
|
/**
|
|
76
94
|
* Get a session by PID
|
|
77
95
|
* @param pid Process ID
|
package/dist/terminal-manager.js
CHANGED
|
@@ -4,6 +4,45 @@ import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
|
|
|
4
4
|
import { configManager } from './config-manager.js';
|
|
5
5
|
import { capture } from "./utils/capture.js";
|
|
6
6
|
import { analyzeProcessState } from './utils/process-detection.js';
|
|
7
|
+
/**
|
|
8
|
+
* Standard Windows PATHEXT value, used to repair a corrupted PATHEXT before
|
|
9
|
+
* spawning child shells.
|
|
10
|
+
*
|
|
11
|
+
* On some Windows Claude Desktop / DXT launches the server process inherits a
|
|
12
|
+
* broken PATHEXT (observed as ".CPL" only). Because we build the child env from
|
|
13
|
+
* { ...process.env }, that broken value would propagate into every spawned
|
|
14
|
+
* shell, stripping ".EXE" and breaking resolution of git / node / python / rg /
|
|
15
|
+
* etc. (and even full-path .exe invocations under PowerShell). See issue #481.
|
|
16
|
+
*/
|
|
17
|
+
const STANDARD_PATHEXT = '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC';
|
|
18
|
+
/**
|
|
19
|
+
* Return a healthy PATHEXT for spawned Windows shells.
|
|
20
|
+
* - Unset -> use the standard list.
|
|
21
|
+
* - Missing ".EXE" -> corrupted; merge the standard list with whatever was
|
|
22
|
+
* present (preserves any extra extensions, order-stable).
|
|
23
|
+
* - Otherwise -> leave the inherited value untouched.
|
|
24
|
+
*/
|
|
25
|
+
function getRepairedPathExt() {
|
|
26
|
+
const current = process.env.PATHEXT;
|
|
27
|
+
if (!current)
|
|
28
|
+
return STANDARD_PATHEXT;
|
|
29
|
+
const exts = current.split(';').map(e => e.trim().toUpperCase()).filter(Boolean);
|
|
30
|
+
if (!exts.includes('.EXE')) {
|
|
31
|
+
return [...new Set([...STANDARD_PATHEXT.split(';'), ...exts])].join(';');
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Output buffering caps. Without a cap, a process emitting enough output makes
|
|
37
|
+
* string concatenation throw "RangeError: Invalid string length" at V8's max
|
|
38
|
+
* string size (~536M chars) inside a stdout 'data' handler — an uncaught
|
|
39
|
+
* exception that kills the whole server (index.ts exits on uncaughtException).
|
|
40
|
+
* The cap also bounds the join() cost in snapshot reads and the periodic
|
|
41
|
+
* process-state scan, both of which are O(total output).
|
|
42
|
+
*/
|
|
43
|
+
export const MAX_BUFFERED_OUTPUT_CHARS = 50 * 1024 * 1024; // per session; oldest lines evicted first
|
|
44
|
+
const MAX_LINE_CHARS = 1024 * 1024; // force-split longer lines so eviction can work
|
|
45
|
+
const MAX_WAIT_OUTPUT_CHARS = 2 * 1024 * 1024; // start_process wait buffer (prompt/state detection)
|
|
7
46
|
/**
|
|
8
47
|
* Get the appropriate spawn configuration for a given shell
|
|
9
48
|
* This handles login shell flags for different shell types
|
|
@@ -39,6 +78,7 @@ function getShellSpawnArgs(shellPath, command) {
|
|
|
39
78
|
return {
|
|
40
79
|
executable: shellPath,
|
|
41
80
|
args: ['/c', command],
|
|
81
|
+
windowsVerbatim: true,
|
|
42
82
|
useShellOption: false
|
|
43
83
|
};
|
|
44
84
|
}
|
|
@@ -143,6 +183,23 @@ export class TerminalManager {
|
|
|
143
183
|
windowsHide: true // Prevent visible console windows on Windows
|
|
144
184
|
};
|
|
145
185
|
}
|
|
186
|
+
// Repair PATHEXT on Windows before spawning. On some Windows DXT launches
|
|
187
|
+
// the server process inherits a corrupted PATHEXT (e.g. ".CPL"), which we
|
|
188
|
+
// would otherwise propagate via { ...process.env } and break command
|
|
189
|
+
// resolution (git, node, python, rg, ...) in the spawned shell. See #481.
|
|
190
|
+
if (process.platform === 'win32' && spawnOptions.env) {
|
|
191
|
+
spawnOptions.env.PATHEXT = getRepairedPathExt();
|
|
192
|
+
}
|
|
193
|
+
// On Windows, when we invoke cmd.exe directly and pass the user's command as a
|
|
194
|
+
// single argument, Node/libuv applies MSVCRT-style quoting that escapes embedded
|
|
195
|
+
// double quotes as \" . cmd.exe does not understand that escaping, so any command
|
|
196
|
+
// containing quotes (e.g. a quoted path with spaces like "C:\Program Files\app.exe")
|
|
197
|
+
// is corrupted before the shell ever parses it. Passing arguments verbatim lets
|
|
198
|
+
// cmd handle its own quoting. Scoped to shells that set windowsVerbatim (cmd only)
|
|
199
|
+
// because PowerShell/pwsh have different quote rules and must NOT use verbatim.
|
|
200
|
+
if (process.platform === 'win32' && spawnConfig.windowsVerbatim) {
|
|
201
|
+
spawnOptions.windowsVerbatimArguments = true;
|
|
202
|
+
}
|
|
146
203
|
// Spawn the process with appropriate arguments
|
|
147
204
|
const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions);
|
|
148
205
|
let output = '';
|
|
@@ -161,7 +218,10 @@ export class TerminalManager {
|
|
|
161
218
|
outputLines: [], // Line-based buffer
|
|
162
219
|
lastReadIndex: 0, // Track where "new" output starts
|
|
163
220
|
isBlocked: false,
|
|
164
|
-
startTime: new Date()
|
|
221
|
+
startTime: new Date(),
|
|
222
|
+
bufferedChars: 0,
|
|
223
|
+
evictedLines: 0,
|
|
224
|
+
evictedChars: 0
|
|
165
225
|
};
|
|
166
226
|
this.sessions.set(childProcess.pid, session);
|
|
167
227
|
// Timing telemetry
|
|
@@ -203,7 +263,14 @@ export class TerminalManager {
|
|
|
203
263
|
if (!firstOutputTime)
|
|
204
264
|
firstOutputTime = now;
|
|
205
265
|
lastOutputTime = now;
|
|
206
|
-
output
|
|
266
|
+
// `output` only feeds the wait-phase result and prompt/state detection,
|
|
267
|
+
// so stop growing it once resolved and keep only a bounded tail.
|
|
268
|
+
if (!resolved) {
|
|
269
|
+
output += text;
|
|
270
|
+
if (output.length > MAX_WAIT_OUTPUT_CHARS) {
|
|
271
|
+
output = output.slice(-Math.floor(MAX_WAIT_OUTPUT_CHARS / 2));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
207
274
|
// Append to line-based buffer
|
|
208
275
|
this.appendToLineBuffer(session, text);
|
|
209
276
|
// Record output event if collecting timing
|
|
@@ -236,7 +303,12 @@ export class TerminalManager {
|
|
|
236
303
|
if (!firstOutputTime)
|
|
237
304
|
firstOutputTime = now;
|
|
238
305
|
lastOutputTime = now;
|
|
239
|
-
|
|
306
|
+
if (!resolved) {
|
|
307
|
+
output += text;
|
|
308
|
+
if (output.length > MAX_WAIT_OUTPUT_CHARS) {
|
|
309
|
+
output = output.slice(-Math.floor(MAX_WAIT_OUTPUT_CHARS / 2));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
240
312
|
// Append to line-based buffer
|
|
241
313
|
this.appendToLineBuffer(session, text);
|
|
242
314
|
// Record output event if collecting timing
|
|
@@ -283,7 +355,9 @@ export class TerminalManager {
|
|
|
283
355
|
outputLines: [...session.outputLines], // Copy line buffer
|
|
284
356
|
exitCode: code,
|
|
285
357
|
startTime: session.startTime,
|
|
286
|
-
endTime: new Date()
|
|
358
|
+
endTime: new Date(),
|
|
359
|
+
evictedLines: session.evictedLines,
|
|
360
|
+
evictedChars: session.evictedChars
|
|
287
361
|
});
|
|
288
362
|
// Keep only last 100 completed sessions
|
|
289
363
|
if (this.completedSessions.size > 100) {
|
|
@@ -327,6 +401,32 @@ export class TerminalManager {
|
|
|
327
401
|
session.outputLines.push(line);
|
|
328
402
|
}
|
|
329
403
|
}
|
|
404
|
+
// Appended text contributes exactly its length to the joined buffer
|
|
405
|
+
// (its newlines become the join separators).
|
|
406
|
+
session.bufferedChars += text.length;
|
|
407
|
+
// A process printing without newlines grows a single line forever, which
|
|
408
|
+
// eviction can't bound — force-split so no line exceeds MAX_LINE_CHARS.
|
|
409
|
+
// Each inserted break adds one separator to the joined length.
|
|
410
|
+
let lastIndex = session.outputLines.length - 1;
|
|
411
|
+
while (session.outputLines[lastIndex].length > MAX_LINE_CHARS) {
|
|
412
|
+
const overlong = session.outputLines[lastIndex];
|
|
413
|
+
session.outputLines[lastIndex] = overlong.slice(0, MAX_LINE_CHARS);
|
|
414
|
+
session.outputLines.push(overlong.slice(MAX_LINE_CHARS));
|
|
415
|
+
session.bufferedChars += 1;
|
|
416
|
+
lastIndex++;
|
|
417
|
+
}
|
|
418
|
+
// Enforce the per-session cap by evicting the oldest lines. Keeps the
|
|
419
|
+
// buffer far below V8's max string length so concatenation and join()
|
|
420
|
+
// can never throw "Invalid string length" and kill the server.
|
|
421
|
+
while (session.bufferedChars > MAX_BUFFERED_OUTPUT_CHARS && session.outputLines.length > 1) {
|
|
422
|
+
const dropped = session.outputLines.shift();
|
|
423
|
+
const droppedJoinedChars = dropped.length + 1; // +1 for its join separator
|
|
424
|
+
session.bufferedChars -= droppedJoinedChars;
|
|
425
|
+
session.evictedChars += droppedJoinedChars;
|
|
426
|
+
session.evictedLines++;
|
|
427
|
+
if (session.lastReadIndex > 0)
|
|
428
|
+
session.lastReadIndex--;
|
|
429
|
+
}
|
|
330
430
|
}
|
|
331
431
|
/**
|
|
332
432
|
* Read process output with pagination (like file reading)
|
|
@@ -339,15 +439,19 @@ export class TerminalManager {
|
|
|
339
439
|
// First check active sessions
|
|
340
440
|
const session = this.sessions.get(pid);
|
|
341
441
|
if (session) {
|
|
342
|
-
|
|
442
|
+
const result = this.readFromLineBuffer(session.outputLines, offset, length, session.lastReadIndex, (newIndex) => { session.lastReadIndex = newIndex; }, false, undefined);
|
|
443
|
+
result.evictedLines = session.evictedLines;
|
|
444
|
+
return result;
|
|
343
445
|
}
|
|
344
446
|
// Then check completed sessions
|
|
345
447
|
const completedSession = this.completedSessions.get(pid);
|
|
346
448
|
if (completedSession) {
|
|
347
449
|
const runtimeMs = completedSession.endTime.getTime() - completedSession.startTime.getTime();
|
|
348
|
-
|
|
450
|
+
const result = this.readFromLineBuffer(completedSession.outputLines, offset, length, 0, // Completed sessions don't track read position
|
|
349
451
|
() => { }, // No-op for completed sessions
|
|
350
452
|
true, completedSession.exitCode, runtimeMs);
|
|
453
|
+
result.evictedLines = completedSession.evictedLines;
|
|
454
|
+
return result;
|
|
351
455
|
}
|
|
352
456
|
return null;
|
|
353
457
|
}
|
|
@@ -445,8 +549,11 @@ export class TerminalManager {
|
|
|
445
549
|
if (session) {
|
|
446
550
|
const fullOutput = session.outputLines.join('\n');
|
|
447
551
|
return {
|
|
448
|
-
|
|
449
|
-
|
|
552
|
+
// Absolute since process start (includes evicted output), so the
|
|
553
|
+
// offset stays valid even if the cap evicts lines between
|
|
554
|
+
// snapshot and read.
|
|
555
|
+
totalChars: session.evictedChars + fullOutput.length,
|
|
556
|
+
lineCount: session.evictedLines + session.outputLines.length
|
|
450
557
|
};
|
|
451
558
|
}
|
|
452
559
|
return null;
|
|
@@ -460,23 +567,28 @@ export class TerminalManager {
|
|
|
460
567
|
// Check active session first
|
|
461
568
|
const session = this.sessions.get(pid);
|
|
462
569
|
if (session) {
|
|
463
|
-
|
|
464
|
-
if (fullOutput.length <= snapshot.totalChars) {
|
|
465
|
-
return ''; // No new output
|
|
466
|
-
}
|
|
467
|
-
return fullOutput.substring(snapshot.totalChars);
|
|
570
|
+
return TerminalManager.outputSinceSnapshot(session.outputLines, session.evictedChars, snapshot.totalChars);
|
|
468
571
|
}
|
|
469
572
|
// Fallback to completed sessions - process may have finished between snapshot and poll
|
|
470
573
|
const completedSession = this.completedSessions.get(pid);
|
|
471
574
|
if (completedSession) {
|
|
472
|
-
|
|
473
|
-
if (fullOutput.length <= snapshot.totalChars) {
|
|
474
|
-
return ''; // No new output
|
|
475
|
-
}
|
|
476
|
-
return fullOutput.substring(snapshot.totalChars);
|
|
575
|
+
return TerminalManager.outputSinceSnapshot(completedSession.outputLines, completedSession.evictedChars, snapshot.totalChars);
|
|
477
576
|
}
|
|
478
577
|
return null;
|
|
479
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* New output since a snapshot, in absolute (since process start) offsets.
|
|
581
|
+
* If eviction dropped part of the unseen output, returns what the buffer
|
|
582
|
+
* still holds — the oldest unseen chars are lost to the cap.
|
|
583
|
+
*/
|
|
584
|
+
static outputSinceSnapshot(outputLines, evictedChars, snapshotTotalChars) {
|
|
585
|
+
const fullOutput = outputLines.join('\n');
|
|
586
|
+
const newChars = evictedChars + fullOutput.length - snapshotTotalChars;
|
|
587
|
+
if (newChars <= 0) {
|
|
588
|
+
return ''; // No new output
|
|
589
|
+
}
|
|
590
|
+
return fullOutput.substring(Math.max(0, fullOutput.length - newChars));
|
|
591
|
+
}
|
|
480
592
|
/**
|
|
481
593
|
* Get a session by PID
|
|
482
594
|
* @param pid Process ID
|
package/dist/tools/edit.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* 2. Make this file a thin dispatch layer that routes to appropriate FileHandler
|
|
15
15
|
* 3. Unify the editRange() signature to handle both text search/replace and structured edits
|
|
16
16
|
*/
|
|
17
|
-
import { writeFile, readFileInternal, validatePath } from './filesystem.js';
|
|
18
|
-
import {
|
|
17
|
+
import { getDefaultEditorMetadata, writeFile, readFileInternal, validatePath } from './filesystem.js';
|
|
18
|
+
import { runFuzzySearchInWorker, getSimilarityRatio } from './fuzzySearch.js';
|
|
19
19
|
import { capture } from '../utils/capture.js';
|
|
20
20
|
import { createErrorResponse } from '../error-handlers.js';
|
|
21
21
|
import { EditBlockArgsSchema } from "./schemas.js";
|
|
@@ -180,6 +180,8 @@ RECOMMENDATION: For large search/replace operations, consider breaking them into
|
|
|
180
180
|
fileName: path.basename(resolvedEditPath),
|
|
181
181
|
filePath: resolvedEditPath,
|
|
182
182
|
fileType: resolvePreviewFileType(resolvedEditPath),
|
|
183
|
+
sourceTool: 'edit_block',
|
|
184
|
+
...await getDefaultEditorMetadata(resolvedEditPath),
|
|
183
185
|
},
|
|
184
186
|
};
|
|
185
187
|
}
|
|
@@ -200,8 +202,9 @@ RECOMMENDATION: For large search/replace operations, consider breaking them into
|
|
|
200
202
|
if (count === 0) {
|
|
201
203
|
// Track fuzzy search time
|
|
202
204
|
const startTime = performance.now();
|
|
203
|
-
// Perform fuzzy search
|
|
204
|
-
|
|
205
|
+
// Perform fuzzy search in a worker thread so the main event loop stays
|
|
206
|
+
// responsive to pings and parallel tool calls during the scan
|
|
207
|
+
const fuzzyResult = await runFuzzySearchInWorker(content, block.search);
|
|
205
208
|
const similarity = getSimilarityRatio(block.search, fuzzyResult.value);
|
|
206
209
|
// Calculate execution time in milliseconds
|
|
207
210
|
const executionTime = performance.now() - startTime;
|
|
@@ -368,6 +371,8 @@ export async function handleEditBlock(args) {
|
|
|
368
371
|
fileName: path.basename(resolvedRangePath),
|
|
369
372
|
filePath: resolvedRangePath,
|
|
370
373
|
fileType: resolvePreviewFileType(resolvedRangePath),
|
|
374
|
+
sourceTool: 'edit_block',
|
|
375
|
+
...await getDefaultEditorMetadata(resolvedRangePath),
|
|
371
376
|
},
|
|
372
377
|
};
|
|
373
378
|
}
|
|
@@ -403,6 +408,8 @@ export async function handleEditBlock(args) {
|
|
|
403
408
|
fileName: path.basename(resolvedEditRangePath),
|
|
404
409
|
filePath: resolvedEditRangePath,
|
|
405
410
|
fileType: resolvePreviewFileType(resolvedEditRangePath),
|
|
411
|
+
sourceTool: 'edit_block',
|
|
412
|
+
...await getDefaultEditorMetadata(resolvedEditRangePath),
|
|
406
413
|
},
|
|
407
414
|
};
|
|
408
415
|
}
|
|
@@ -71,3 +71,8 @@ export declare function getFileInfo(filePath: string): Promise<Record<string, an
|
|
|
71
71
|
* @param options Options for PDF generation or modification. For modification, can include `sourcePdf`.
|
|
72
72
|
*/
|
|
73
73
|
export declare function writePdf(filePath: string, content: string | PdfOperations[], outputPath?: string, options?: any): Promise<void>;
|
|
74
|
+
type DefaultEditorMetadata = {
|
|
75
|
+
defaultEditorName?: string;
|
|
76
|
+
defaultEditorPath?: string;
|
|
77
|
+
};
|
|
78
|
+
export declare function getDefaultEditorMetadata(filePath: string): Promise<DefaultEditorMetadata>;
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from "fs/promises";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import fetch from 'cross-fetch';
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
5
7
|
import { capture } from '../utils/capture.js';
|
|
6
8
|
import { withTimeout } from '../utils/withTimeout.js';
|
|
7
9
|
import { configManager } from '../config-manager.js';
|
|
@@ -887,3 +889,44 @@ export async function writePdf(filePath, content, outputPath, options = {}) {
|
|
|
887
889
|
throw new Error('Invalid content type for writePdf. Expected string (markdown) or array of operations.');
|
|
888
890
|
}
|
|
889
891
|
}
|
|
892
|
+
const execFileAsync = promisify(execFile);
|
|
893
|
+
const DEFAULT_EDITOR_NEGATIVE_CACHE_MS = 5 * 60 * 1000;
|
|
894
|
+
const defaultEditorCache = new Map();
|
|
895
|
+
function escapeAppleScriptString(value) {
|
|
896
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
897
|
+
}
|
|
898
|
+
export async function getDefaultEditorMetadata(filePath) {
|
|
899
|
+
if (os.platform() !== 'darwin') {
|
|
900
|
+
return {};
|
|
901
|
+
}
|
|
902
|
+
let cacheKey = '';
|
|
903
|
+
try {
|
|
904
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
905
|
+
cacheKey = extension || path.basename(filePath).toLowerCase();
|
|
906
|
+
const cached = defaultEditorCache.get(cacheKey);
|
|
907
|
+
if (cached) {
|
|
908
|
+
if (!cached.expiresAt || cached.expiresAt > Date.now()) {
|
|
909
|
+
return cached.metadata;
|
|
910
|
+
}
|
|
911
|
+
defaultEditorCache.delete(cacheKey);
|
|
912
|
+
}
|
|
913
|
+
const script = `set appAlias to default application of (info for POSIX file "${escapeAppleScriptString(filePath)}")\nreturn (name of (info for appAlias)) & linefeed & POSIX path of appAlias`;
|
|
914
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script], { timeout: 12000 });
|
|
915
|
+
const lines = stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
916
|
+
const defaultEditorName = lines[lines.length - 2]?.replace(/\.app$/i, '') ?? '';
|
|
917
|
+
const defaultEditorPath = lines[lines.length - 1] ?? '';
|
|
918
|
+
if (defaultEditorName && defaultEditorPath.startsWith('/')) {
|
|
919
|
+
const metadata = { defaultEditorName, defaultEditorPath };
|
|
920
|
+
defaultEditorCache.set(cacheKey, { metadata });
|
|
921
|
+
return metadata;
|
|
922
|
+
}
|
|
923
|
+
defaultEditorCache.set(cacheKey, { metadata: {}, expiresAt: Date.now() + DEFAULT_EDITOR_NEGATIVE_CACHE_MS });
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
if (cacheKey) {
|
|
927
|
+
defaultEditorCache.set(cacheKey, { metadata: {}, expiresAt: Date.now() + DEFAULT_EDITOR_NEGATIVE_CACHE_MS });
|
|
928
|
+
}
|
|
929
|
+
// Generic UI fallback is good enough if detection fails.
|
|
930
|
+
}
|
|
931
|
+
return {};
|
|
932
|
+
}
|
|
@@ -1,22 +1,12 @@
|
|
|
1
|
+
import type { FuzzyMatch } from './fuzzySearchCore.js';
|
|
2
|
+
export { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearchCore.js';
|
|
3
|
+
/** Abort fuzzy search in the worker after this many ms to avoid unbounded CPU burn. */
|
|
4
|
+
export declare const FUZZY_SEARCH_TIMEOUT_MS = 30000;
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* @param parentDistance Best distance found so far (default: Infinity)
|
|
8
|
-
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
6
|
+
* Runs the fuzzy search in a Worker thread so the main MCP event loop stays
|
|
7
|
+
* responsive to pings and other tool calls during heavy scans. Rejects if the
|
|
8
|
+
* scan exceeds timeoutMs, terminating the worker so it doesn't linger in the
|
|
9
|
+
* background. Search metrics come back with the result and are captured here,
|
|
10
|
+
* on the main thread, where the client identity is initialized.
|
|
9
11
|
*/
|
|
10
|
-
export declare function
|
|
11
|
-
start: number;
|
|
12
|
-
end: number;
|
|
13
|
-
value: string;
|
|
14
|
-
distance: number;
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Calculates the similarity ratio between two strings
|
|
18
|
-
* @param a First string
|
|
19
|
-
* @param b Second string
|
|
20
|
-
* @returns Similarity ratio (0-1)
|
|
21
|
-
*/
|
|
22
|
-
export declare function getSimilarityRatio(a: string, b: string): number;
|
|
12
|
+
export declare function runFuzzySearchInWorker(text: string, query: string, timeoutMs?: number): Promise<FuzzyMatch>;
|