@wonderwhy-er/desktop-commander 0.2.42 → 0.2.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +22 -0
- package/dist/config-manager.d.ts +30 -1
- package/dist/config-manager.js +55 -8
- package/dist/handlers/filesystem-handlers.js +86 -52
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -0
- package/dist/remote-device/desktop-commander-integration.js +8 -2
- package/dist/remote-device/remote-channel.d.ts +15 -0
- package/dist/remote-device/remote-channel.js +136 -27
- package/dist/server.d.ts +5 -1
- package/dist/server.js +118 -27
- package/dist/terminal-manager.d.ts +18 -0
- package/dist/terminal-manager.js +84 -18
- package/dist/tools/edit.d.ts +1 -1
- package/dist/tools/edit.js +34 -26
- package/dist/tools/filesystem.d.ts +1 -0
- package/dist/tools/filesystem.js +23 -13
- 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/schemas.d.ts +33 -1
- package/dist/tools/schemas.js +61 -3
- package/dist/types.d.ts +3 -1
- package/dist/ui/config-editor/config-editor-runtime.js +49 -49
- package/dist/ui/config-editor/src/app.js +2 -11
- package/dist/ui/file-preview/preview-runtime.js +147 -146
- package/dist/ui/file-preview/src/app.js +172 -53
- package/dist/ui/file-preview/src/directory-controller.js +4 -4
- package/dist/ui/file-preview/src/file-type-handlers.js +2 -2
- package/dist/ui/file-preview/src/markdown/controller.js +4 -1
- package/dist/ui/file-preview/src/panel-actions.js +4 -4
- package/dist/ui/file-preview/src/payload-utils.js +23 -7
- package/dist/utils/capture.d.ts +2 -0
- package/dist/utils/capture.js +32 -7
- package/dist/utils/files/base.d.ts +2 -0
- package/dist/utils/files/image.js +1 -1
- package/dist/utils/files/text.js +24 -19
- package/dist/utils/open-browser.d.ts +1 -1
- package/dist/utils/open-browser.js +5 -2
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/utils/usageTracker.d.ts +5 -1
- package/dist/utils/usageTracker.js +14 -3
- package/dist/utils/welcome-onboarding.d.ts +1 -1
- package/dist/utils/welcome-onboarding.js +2 -2
- package/dist/utils/withTimeout.d.ts +17 -0
- package/dist/utils/withTimeout.js +33 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
package/dist/utils/files/text.js
CHANGED
|
@@ -43,7 +43,7 @@ export class TextFileHandler {
|
|
|
43
43
|
const length = options?.length ?? 1000; // Default from config
|
|
44
44
|
const includeStatusMessage = options?.includeStatusMessage ?? true;
|
|
45
45
|
// Binary detection is done at factory level - just read as text
|
|
46
|
-
return this.readFileWithSmartPositioning(filePath, offset, length, 'text/plain', includeStatusMessage);
|
|
46
|
+
return this.readFileWithSmartPositioning(filePath, offset, length, 'text/plain', includeStatusMessage, options?.signal);
|
|
47
47
|
}
|
|
48
48
|
async write(path, content, mode = 'rewrite') {
|
|
49
49
|
if (mode === 'append') {
|
|
@@ -100,11 +100,11 @@ export class TextFileHandler {
|
|
|
100
100
|
/**
|
|
101
101
|
* Get file line count (for files under size limit)
|
|
102
102
|
*/
|
|
103
|
-
async getFileLineCount(filePath) {
|
|
103
|
+
async getFileLineCount(filePath, signal) {
|
|
104
104
|
try {
|
|
105
105
|
const stats = await fs.stat(filePath);
|
|
106
106
|
if (stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
107
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
107
|
+
const content = await fs.readFile(filePath, { encoding: 'utf8', signal });
|
|
108
108
|
return TextFileHandler.countLines(content);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -179,32 +179,32 @@ export class TextFileHandler {
|
|
|
179
179
|
/**
|
|
180
180
|
* Read file with smart positioning for optimal performance
|
|
181
181
|
*/
|
|
182
|
-
async readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
182
|
+
async readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true, signal) {
|
|
183
183
|
const stats = await fs.stat(filePath);
|
|
184
184
|
const fileSize = stats.size;
|
|
185
|
-
const totalLines = await this.getFileLineCount(filePath);
|
|
185
|
+
const totalLines = await this.getFileLineCount(filePath, signal);
|
|
186
186
|
// For negative offsets (tail behavior), use reverse reading
|
|
187
187
|
if (offset < 0) {
|
|
188
188
|
const requestedLines = Math.abs(offset);
|
|
189
189
|
if (fileSize > FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD &&
|
|
190
190
|
requestedLines <= READ_PERFORMANCE_THRESHOLDS.SMALL_READ_THRESHOLD) {
|
|
191
|
-
return await this.readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
191
|
+
return await this.readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage, totalLines, signal);
|
|
192
192
|
}
|
|
193
193
|
else {
|
|
194
|
-
return await this.readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
|
|
194
|
+
return await this.readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage, totalLines, signal);
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
// For positive offsets
|
|
198
198
|
else {
|
|
199
199
|
if (fileSize < FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD || offset === 0) {
|
|
200
|
-
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
200
|
+
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines, signal);
|
|
201
201
|
}
|
|
202
202
|
else {
|
|
203
203
|
if (offset > READ_PERFORMANCE_THRESHOLDS.DEEP_OFFSET_THRESHOLD) {
|
|
204
|
-
return await this.readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
204
|
+
return await this.readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage, totalLines, signal);
|
|
205
205
|
}
|
|
206
206
|
else {
|
|
207
|
-
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
|
|
207
|
+
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines, signal);
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
}
|
|
@@ -212,7 +212,7 @@ export class TextFileHandler {
|
|
|
212
212
|
/**
|
|
213
213
|
* Read last N lines efficiently by reading file backwards
|
|
214
214
|
*/
|
|
215
|
-
async readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
215
|
+
async readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
216
216
|
const fd = await fs.open(filePath, 'r');
|
|
217
217
|
try {
|
|
218
218
|
const stats = await fd.stat();
|
|
@@ -221,6 +221,11 @@ export class TextFileHandler {
|
|
|
221
221
|
let lines = [];
|
|
222
222
|
let partialLine = '';
|
|
223
223
|
while (position > 0 && lines.length < n) {
|
|
224
|
+
if (signal?.aborted) {
|
|
225
|
+
const err = new Error('Read aborted');
|
|
226
|
+
err.code = 'ABORT_ERR';
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
224
229
|
const readSize = Math.min(READ_PERFORMANCE_THRESHOLDS.CHUNK_SIZE, position);
|
|
225
230
|
position -= readSize;
|
|
226
231
|
const buffer = Buffer.alloc(readSize);
|
|
@@ -247,9 +252,9 @@ export class TextFileHandler {
|
|
|
247
252
|
/**
|
|
248
253
|
* Read from end using readline with circular buffer
|
|
249
254
|
*/
|
|
250
|
-
async readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
255
|
+
async readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
251
256
|
const rl = createInterface({
|
|
252
|
-
input: createReadStream(filePath),
|
|
257
|
+
input: createReadStream(filePath, { signal }),
|
|
253
258
|
crlfDelay: Infinity
|
|
254
259
|
});
|
|
255
260
|
const buffer = new Array(requestedLines);
|
|
@@ -279,9 +284,9 @@ export class TextFileHandler {
|
|
|
279
284
|
/**
|
|
280
285
|
* Read from start/middle using readline
|
|
281
286
|
*/
|
|
282
|
-
async readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
287
|
+
async readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
283
288
|
const rl = createInterface({
|
|
284
|
-
input: createReadStream(filePath),
|
|
289
|
+
input: createReadStream(filePath, { signal }),
|
|
285
290
|
crlfDelay: Infinity
|
|
286
291
|
});
|
|
287
292
|
const result = [];
|
|
@@ -308,10 +313,10 @@ export class TextFileHandler {
|
|
|
308
313
|
/**
|
|
309
314
|
* Read from estimated byte position for very large files
|
|
310
315
|
*/
|
|
311
|
-
async readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
|
|
316
|
+
async readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
312
317
|
// First, do a quick scan to estimate lines per byte
|
|
313
318
|
const rl = createInterface({
|
|
314
|
-
input: createReadStream(filePath),
|
|
319
|
+
input: createReadStream(filePath, { signal }),
|
|
315
320
|
crlfDelay: Infinity
|
|
316
321
|
});
|
|
317
322
|
let sampleLines = 0;
|
|
@@ -324,7 +329,7 @@ export class TextFileHandler {
|
|
|
324
329
|
}
|
|
325
330
|
rl.close();
|
|
326
331
|
if (sampleLines === 0) {
|
|
327
|
-
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines);
|
|
332
|
+
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines, signal);
|
|
328
333
|
}
|
|
329
334
|
// Estimate position
|
|
330
335
|
const avgLineLength = bytesRead / sampleLines;
|
|
@@ -333,7 +338,7 @@ export class TextFileHandler {
|
|
|
333
338
|
try {
|
|
334
339
|
const stats = await fd.stat();
|
|
335
340
|
const startPosition = Math.min(estimatedBytePosition, stats.size);
|
|
336
|
-
const stream = createReadStream(filePath, { start: startPosition });
|
|
341
|
+
const stream = createReadStream(filePath, { start: startPosition, signal });
|
|
337
342
|
const rl2 = createInterface({
|
|
338
343
|
input: stream,
|
|
339
344
|
crlfDelay: Infinity
|
|
@@ -37,7 +37,10 @@ export async function openBrowser(url) {
|
|
|
37
37
|
/**
|
|
38
38
|
* Open the Desktop Commander welcome page
|
|
39
39
|
*/
|
|
40
|
-
export async function openWelcomePage() {
|
|
41
|
-
|
|
40
|
+
export async function openWelcomePage(clientName) {
|
|
41
|
+
// utm_source is auto-captured by the welcome page's PostHog (and GA4), so
|
|
42
|
+
// web analytics can segment by MCP client without any web-side changes.
|
|
43
|
+
const url = 'https://desktopcommander.app/welcome/'
|
|
44
|
+
+ (clientName ? `?utm_source=${encodeURIComponent(clientName)}` : '');
|
|
42
45
|
await openBrowser(url);
|
|
43
46
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for detecting parameters a caller sent that a tool's Zod schema does
|
|
3
|
+
* not accept.
|
|
4
|
+
*
|
|
5
|
+
* Zod's default `z.object()` silently strips unknown keys, so a model that
|
|
6
|
+
* invents a parameter (e.g. `view_range`) gets a normal-looking response with no
|
|
7
|
+
* signal that its input was ignored. These helpers let the dispatcher surface a
|
|
8
|
+
* corrective warning instead.
|
|
9
|
+
*
|
|
10
|
+
* Detection is top-level only. That is sufficient here because none of the tool
|
|
11
|
+
* arg schemas use a top-level `.passthrough()`/catchall — free-form fields
|
|
12
|
+
* (`options`, `params`, `pdfOptions`) are named keys whose contents are
|
|
13
|
+
* intentionally unvalidated.
|
|
14
|
+
*/
|
|
15
|
+
/** List the parameter names a tool schema accepts (empty array if it takes none). */
|
|
16
|
+
export declare function getSupportedParams(schema: unknown): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Return the names of top-level keys in `args` that the schema does not accept.
|
|
19
|
+
* Returns [] when args is not a plain object or the schema's params can't be
|
|
20
|
+
* determined (so we never warn on something we can't reason about).
|
|
21
|
+
*/
|
|
22
|
+
export declare function detectUnsupportedParams(args: unknown, schema: unknown): string[];
|
|
23
|
+
/** Build the corrective warning string shown to the model. */
|
|
24
|
+
export declare function buildUnsupportedParamsWarning(toolName: string, unsupported: string[], supported: string[]): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for detecting parameters a caller sent that a tool's Zod schema does
|
|
3
|
+
* not accept.
|
|
4
|
+
*
|
|
5
|
+
* Zod's default `z.object()` silently strips unknown keys, so a model that
|
|
6
|
+
* invents a parameter (e.g. `view_range`) gets a normal-looking response with no
|
|
7
|
+
* signal that its input was ignored. These helpers let the dispatcher surface a
|
|
8
|
+
* corrective warning instead.
|
|
9
|
+
*
|
|
10
|
+
* Detection is top-level only. That is sufficient here because none of the tool
|
|
11
|
+
* arg schemas use a top-level `.passthrough()`/catchall — free-form fields
|
|
12
|
+
* (`options`, `params`, `pdfOptions`) are named keys whose contents are
|
|
13
|
+
* intentionally unvalidated.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a Zod schema down to the field map of its underlying object schema,
|
|
17
|
+
* unwrapping wrappers (effects/optional/default/nullable/branded) defensively.
|
|
18
|
+
* Returns null when the schema is not (and does not wrap) a plain object schema,
|
|
19
|
+
* in which case we cannot determine the supported parameters and must not warn.
|
|
20
|
+
*/
|
|
21
|
+
function getObjectShape(schema) {
|
|
22
|
+
let current = schema;
|
|
23
|
+
for (let i = 0; current && current._def && i < 10; i++) {
|
|
24
|
+
const typeName = current._def.typeName;
|
|
25
|
+
if (typeName === 'ZodObject') {
|
|
26
|
+
const shape = typeof current.shape === 'function' ? current.shape() : current.shape;
|
|
27
|
+
return shape ?? null;
|
|
28
|
+
}
|
|
29
|
+
if (typeName === 'ZodEffects')
|
|
30
|
+
current = current._def.schema;
|
|
31
|
+
else if (typeName === 'ZodOptional' || typeName === 'ZodNullable' || typeName === 'ZodDefault') {
|
|
32
|
+
current = typeof current._def.innerType === 'function' ? current._def.innerType() : current._def.innerType;
|
|
33
|
+
}
|
|
34
|
+
else if (typeName === 'ZodBranded')
|
|
35
|
+
current = current._def.type;
|
|
36
|
+
else
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/** List the parameter names a tool schema accepts (empty array if it takes none). */
|
|
42
|
+
export function getSupportedParams(schema) {
|
|
43
|
+
const shape = getObjectShape(schema);
|
|
44
|
+
return shape ? Object.keys(shape) : [];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Return the names of top-level keys in `args` that the schema does not accept.
|
|
48
|
+
* Returns [] when args is not a plain object or the schema's params can't be
|
|
49
|
+
* determined (so we never warn on something we can't reason about).
|
|
50
|
+
*/
|
|
51
|
+
export function detectUnsupportedParams(args, schema) {
|
|
52
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
53
|
+
return [];
|
|
54
|
+
const shape = getObjectShape(schema);
|
|
55
|
+
if (shape === null)
|
|
56
|
+
return [];
|
|
57
|
+
const supported = new Set(Object.keys(shape));
|
|
58
|
+
return Object.keys(args).filter((k) => !supported.has(k));
|
|
59
|
+
}
|
|
60
|
+
/** Build the corrective warning string shown to the model. */
|
|
61
|
+
export function buildUnsupportedParamsWarning(toolName, unsupported, supported) {
|
|
62
|
+
const ignored = unsupported.join(', ');
|
|
63
|
+
const supportedList = supported.length > 0 ? supported.join(', ') : '(none)';
|
|
64
|
+
return `You sent parameters not supported by this tool, which were ignored: ${ignored}. `
|
|
65
|
+
+ `Supported parameters for ${toolName}: ${supportedList}.`;
|
|
66
|
+
}
|
|
@@ -38,7 +38,11 @@ declare class UsageTracker {
|
|
|
38
38
|
*/
|
|
39
39
|
getStats(): Promise<ToolUsageStats>;
|
|
40
40
|
/**
|
|
41
|
-
* Save usage stats to config
|
|
41
|
+
* Save usage stats to config.
|
|
42
|
+
* Non-blocking: the tool-call return path must not wait on a disk write. The
|
|
43
|
+
* in-memory stats are already updated by the caller (getStats returns the live
|
|
44
|
+
* config object), so persistence is coalesced in the background. This is what
|
|
45
|
+
* keeps a saturated libuv threadpool from gating tool responses on every call.
|
|
42
46
|
*/
|
|
43
47
|
private saveStats;
|
|
44
48
|
/**
|
|
@@ -45,10 +45,14 @@ class UsageTracker {
|
|
|
45
45
|
return stats || this.getDefaultStats();
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* Save usage stats to config
|
|
48
|
+
* Save usage stats to config.
|
|
49
|
+
* Non-blocking: the tool-call return path must not wait on a disk write. The
|
|
50
|
+
* in-memory stats are already updated by the caller (getStats returns the live
|
|
51
|
+
* config object), so persistence is coalesced in the background. This is what
|
|
52
|
+
* keeps a saturated libuv threadpool from gating tool responses on every call.
|
|
49
53
|
*/
|
|
50
54
|
async saveStats(stats) {
|
|
51
|
-
await configManager.
|
|
55
|
+
await configManager.setValueNonBlocking('usageStats', stats);
|
|
52
56
|
}
|
|
53
57
|
/**
|
|
54
58
|
* Determine which category a tool belongs to
|
|
@@ -327,7 +331,14 @@ class UsageTracker {
|
|
|
327
331
|
async shouldShowOnboarding() {
|
|
328
332
|
// Check feature flag first (remote kill switch)
|
|
329
333
|
const { featureFlagManager } = await import('./feature-flags.js');
|
|
330
|
-
|
|
334
|
+
// Default false: on cold starts (no flag cache yet, e.g. ephemeral Docker
|
|
335
|
+
// containers) this can run before the background flag fetch completes.
|
|
336
|
+
// Treat an unknown flag as "off" so nothing is injected — flags load
|
|
337
|
+
// within seconds, so when the flag is ON onboarding still fires on a
|
|
338
|
+
// later call while the user is new. Deliberately no waiting here to keep
|
|
339
|
+
// tool calls latency-free.
|
|
340
|
+
// See: https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/538
|
|
341
|
+
const onboardingEnabled = featureFlagManager.get('onboarding_injection', false);
|
|
331
342
|
if (!onboardingEnabled) {
|
|
332
343
|
return false;
|
|
333
344
|
}
|
|
@@ -12,7 +12,7 @@ import { capture } from './capture.js';
|
|
|
12
12
|
* 2. Users in the 'showOnboardingPage' A/B variant
|
|
13
13
|
* 3. Haven't seen it yet
|
|
14
14
|
*/
|
|
15
|
-
export async function handleWelcomePageOnboarding() {
|
|
15
|
+
export async function handleWelcomePageOnboarding(clientName) {
|
|
16
16
|
// Check if this is a new install pending A/B decision
|
|
17
17
|
// This flag is set when config is first created and survives process restarts
|
|
18
18
|
const pending = await configManager.getValue('pendingWelcomeOnboarding');
|
|
@@ -48,7 +48,7 @@ export async function handleWelcomePageOnboarding() {
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
try {
|
|
51
|
-
await openWelcomePage();
|
|
51
|
+
await openWelcomePage(clientName);
|
|
52
52
|
await configManager.setValue('sawOnboardingPage', true);
|
|
53
53
|
await configManager.setValue('pendingWelcomeOnboarding', false);
|
|
54
54
|
capture('server_welcome_page_opened', { success: true });
|
|
@@ -9,3 +9,20 @@
|
|
|
9
9
|
* @returns Promise that resolves with the operation result or the default value on timeout
|
|
10
10
|
*/
|
|
11
11
|
export declare function withTimeout<T>(operation: Promise<T>, timeoutMs: number, operationName: string, defaultValue: T): Promise<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Run an operation under a timeout WITH real cancellation.
|
|
14
|
+
*
|
|
15
|
+
* Unlike withTimeout (which only races a timer and leaves the underlying work
|
|
16
|
+
* running — holding its libuv thread/fd until the OS call returns), this passes
|
|
17
|
+
* an AbortSignal into the operation and aborts it when the timeout fires, so a
|
|
18
|
+
* read/stream that honors the signal is cancelled and its resources released.
|
|
19
|
+
*
|
|
20
|
+
* Rejects with an Error whose `.code` is 'ETIMEDOUT' on timeout (so existing
|
|
21
|
+
* ETIMEDOUT handling / permission-error mapping keeps working).
|
|
22
|
+
*
|
|
23
|
+
* Caveat: an operation wedged inside a single un-interruptible syscall only
|
|
24
|
+
* observes the abort once that syscall returns; library reads that ignore the
|
|
25
|
+
* signal (e.g. Excel/PDF parsers) still get the timeout rejection but keep
|
|
26
|
+
* running in the background until they finish on their own.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runWithAbortableTimeout<T>(operation: (signal: AbortSignal) => Promise<T>, timeoutMs: number, operationName: string): Promise<T>;
|
|
@@ -50,3 +50,36 @@ export function withTimeout(operation, timeoutMs, operationName, defaultValue) {
|
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Run an operation under a timeout WITH real cancellation.
|
|
55
|
+
*
|
|
56
|
+
* Unlike withTimeout (which only races a timer and leaves the underlying work
|
|
57
|
+
* running — holding its libuv thread/fd until the OS call returns), this passes
|
|
58
|
+
* an AbortSignal into the operation and aborts it when the timeout fires, so a
|
|
59
|
+
* read/stream that honors the signal is cancelled and its resources released.
|
|
60
|
+
*
|
|
61
|
+
* Rejects with an Error whose `.code` is 'ETIMEDOUT' on timeout (so existing
|
|
62
|
+
* ETIMEDOUT handling / permission-error mapping keeps working).
|
|
63
|
+
*
|
|
64
|
+
* Caveat: an operation wedged inside a single un-interruptible syscall only
|
|
65
|
+
* observes the abort once that syscall returns; library reads that ignore the
|
|
66
|
+
* signal (e.g. Excel/PDF parsers) still get the timeout rejection but keep
|
|
67
|
+
* running in the background until they finish on their own.
|
|
68
|
+
*/
|
|
69
|
+
export function runWithAbortableTimeout(operation, timeoutMs, operationName) {
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
let timeoutId;
|
|
72
|
+
const timeout = new Promise((_, reject) => {
|
|
73
|
+
timeoutId = setTimeout(() => {
|
|
74
|
+
controller.abort();
|
|
75
|
+
const error = new Error(`${operationName} timed out after ${timeoutMs / 1000} seconds`);
|
|
76
|
+
error.code = 'ETIMEDOUT';
|
|
77
|
+
reject(error);
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
});
|
|
80
|
+
const op = operation(controller.signal);
|
|
81
|
+
// Swallow the late abort rejection so it can't surface as an unhandled
|
|
82
|
+
// rejection after the timeout has already settled the race.
|
|
83
|
+
op.catch(() => { });
|
|
84
|
+
return Promise.race([op, timeout]).finally(() => clearTimeout(timeoutId));
|
|
85
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.44";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
1
|
+
export const VERSION = '0.2.44';
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wonderwhy-er/desktop-commander",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.44",
|
|
4
4
|
"description": "MCP server for terminal operations and file editing",
|
|
5
5
|
"mcpName": "io.github.wonderwhy-er/desktop-commander",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Eduards Ruzga",
|
|
8
8
|
"homepage": "https://github.com/wonderwhy-er/DesktopCommanderMCP",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/wonderwhy-er/DesktopCommanderMCP.git"
|
|
12
|
+
},
|
|
9
13
|
"bugs": "https://github.com/wonderwhy-er/DesktopCommanderMCP/issues",
|
|
10
14
|
"type": "module",
|
|
11
15
|
"engines": {
|