@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threadpool bootstrap. MUST be the first import in index.ts.
|
|
3
|
+
*
|
|
4
|
+
* Every fs operation (read_file, write_file, edit_block, config/history/log
|
|
5
|
+
* persistence) runs on libuv's threadpool, which defaults to only 4 threads.
|
|
6
|
+
* Under heavy parallel load — e.g. several agents reading/writing files on a
|
|
7
|
+
* slow or cloud-synced filesystem — 4 stalled operations exhaust the pool, and
|
|
8
|
+
* because a stalled syscall keeps its thread until the OS returns (a JS-level
|
|
9
|
+
* timeout does not cancel it), every subsequent fs op queues for minutes. That
|
|
10
|
+
* surfaced as multi-minute tool-call hangs under parallel `claude -p` load.
|
|
11
|
+
*
|
|
12
|
+
* Raising the pool size gives enough headroom that a burst of slow reads no
|
|
13
|
+
* longer starves the rest. libuv reads UV_THREADPOOL_SIZE only when the pool is
|
|
14
|
+
* first initialized (on first submitted work), so this assignment has to happen
|
|
15
|
+
* before ANY threadpool work — hence "first import". A user-provided value is
|
|
16
|
+
* always respected.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_THREADPOOL_SIZE = 16;
|
|
19
|
+
if (!process.env.UV_THREADPOOL_SIZE) {
|
|
20
|
+
process.env.UV_THREADPOOL_SIZE = String(DEFAULT_THREADPOOL_SIZE);
|
|
21
|
+
}
|
|
22
|
+
export {};
|
package/dist/config-manager.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ declare class ConfigManager {
|
|
|
23
23
|
private config;
|
|
24
24
|
private initialized;
|
|
25
25
|
private _isFirstRun;
|
|
26
|
+
private writeChain;
|
|
27
|
+
private saveScheduled;
|
|
26
28
|
constructor();
|
|
27
29
|
/**
|
|
28
30
|
* Initialize configuration - load from disk or create default
|
|
@@ -37,9 +39,26 @@ declare class ConfigManager {
|
|
|
37
39
|
*/
|
|
38
40
|
private getDefaultConfig;
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
42
|
+
* Write the current in-memory config to disk. All writes funnel through
|
|
43
|
+
* writeChain (see saveConfig / scheduleSave) so overlapping saves can never
|
|
44
|
+
* interleave and corrupt the file. Previously every tool call could fire its
|
|
45
|
+
* own independent fs.writeFile of the same path.
|
|
46
|
+
*/
|
|
47
|
+
private writeConfigToDisk;
|
|
48
|
+
/**
|
|
49
|
+
* Awaitable save, serialized on writeChain. Use for explicit, user-driven
|
|
50
|
+
* config changes where the caller wants on-disk confirmation.
|
|
41
51
|
*/
|
|
42
52
|
private saveConfig;
|
|
53
|
+
/**
|
|
54
|
+
* Non-blocking, coalesced save. Returns immediately; the write runs in the
|
|
55
|
+
* background. A burst of calls collapses to at most one queued write behind
|
|
56
|
+
* the in-flight one, so a storm of tool calls can't storm the disk — and,
|
|
57
|
+
* critically, can't pile up behind a saturated libuv threadpool and gate the
|
|
58
|
+
* tool-call response path. Used for high-frequency, non-critical persistence
|
|
59
|
+
* such as usage stats.
|
|
60
|
+
*/
|
|
61
|
+
scheduleSave(): void;
|
|
43
62
|
/**
|
|
44
63
|
* Get the entire config
|
|
45
64
|
*/
|
|
@@ -52,6 +71,16 @@ declare class ConfigManager {
|
|
|
52
71
|
* Set a specific configuration value
|
|
53
72
|
*/
|
|
54
73
|
setValue(key: string, value: any): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Update a value in memory and persist it WITHOUT blocking the caller.
|
|
76
|
+
* The tool-call response path must never wait on a disk write: when the libuv
|
|
77
|
+
* threadpool is saturated (e.g. many parallel reads stalled on a slow/cloud
|
|
78
|
+
* filesystem) an awaited write can't get a thread and would hang the response
|
|
79
|
+
* of even pure-memory tools. The in-memory value is updated synchronously so
|
|
80
|
+
* subsequent reads see it immediately; the write is coalesced in the
|
|
81
|
+
* background. Callers needing on-disk confirmation should use setValue.
|
|
82
|
+
*/
|
|
83
|
+
setValueNonBlocking(key: string, value: any): Promise<void>;
|
|
55
84
|
/**
|
|
56
85
|
* Update multiple configuration values at once
|
|
57
86
|
*/
|
package/dist/config-manager.js
CHANGED
|
@@ -29,6 +29,10 @@ class ConfigManager {
|
|
|
29
29
|
this.config = {};
|
|
30
30
|
this.initialized = false;
|
|
31
31
|
this._isFirstRun = false; // Track if this is the first run (config was just created)
|
|
32
|
+
// Serializes all disk writes so concurrent saves can't corrupt config.json.
|
|
33
|
+
this.writeChain = Promise.resolve();
|
|
34
|
+
// True while a coalesced background write is already queued (see scheduleSave).
|
|
35
|
+
this.saveScheduled = false;
|
|
32
36
|
// Get user's home directory
|
|
33
37
|
// Define config directory and file paths
|
|
34
38
|
this.configPath = CONFIG_FILE;
|
|
@@ -140,16 +144,45 @@ class ConfigManager {
|
|
|
140
144
|
};
|
|
141
145
|
}
|
|
142
146
|
/**
|
|
143
|
-
*
|
|
147
|
+
* Write the current in-memory config to disk. All writes funnel through
|
|
148
|
+
* writeChain (see saveConfig / scheduleSave) so overlapping saves can never
|
|
149
|
+
* interleave and corrupt the file. Previously every tool call could fire its
|
|
150
|
+
* own independent fs.writeFile of the same path.
|
|
151
|
+
*/
|
|
152
|
+
async writeConfigToDisk() {
|
|
153
|
+
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), 'utf8');
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Awaitable save, serialized on writeChain. Use for explicit, user-driven
|
|
157
|
+
* config changes where the caller wants on-disk confirmation.
|
|
144
158
|
*/
|
|
145
159
|
async saveConfig() {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
const write = this.writeChain.then(() => this.writeConfigToDisk());
|
|
161
|
+
// Keep the chain alive even if this write rejects, so later writes still run.
|
|
162
|
+
this.writeChain = write.catch(() => { });
|
|
163
|
+
return write;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Non-blocking, coalesced save. Returns immediately; the write runs in the
|
|
167
|
+
* background. A burst of calls collapses to at most one queued write behind
|
|
168
|
+
* the in-flight one, so a storm of tool calls can't storm the disk — and,
|
|
169
|
+
* critically, can't pile up behind a saturated libuv threadpool and gate the
|
|
170
|
+
* tool-call response path. Used for high-frequency, non-critical persistence
|
|
171
|
+
* such as usage stats.
|
|
172
|
+
*/
|
|
173
|
+
scheduleSave() {
|
|
174
|
+
if (this.saveScheduled)
|
|
175
|
+
return; // a queued write will capture the latest config
|
|
176
|
+
this.saveScheduled = true;
|
|
177
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
178
|
+
this.saveScheduled = false; // let the next burst queue a fresh write
|
|
179
|
+
try {
|
|
180
|
+
await this.writeConfigToDisk();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error('Failed to save config (background):', error);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
153
186
|
}
|
|
154
187
|
/**
|
|
155
188
|
* Get the entire config
|
|
@@ -194,6 +227,20 @@ class ConfigManager {
|
|
|
194
227
|
this.config[key] = value;
|
|
195
228
|
await this.saveConfig();
|
|
196
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Update a value in memory and persist it WITHOUT blocking the caller.
|
|
232
|
+
* The tool-call response path must never wait on a disk write: when the libuv
|
|
233
|
+
* threadpool is saturated (e.g. many parallel reads stalled on a slow/cloud
|
|
234
|
+
* filesystem) an awaited write can't get a thread and would hang the response
|
|
235
|
+
* of even pure-memory tools. The in-memory value is updated synchronously so
|
|
236
|
+
* subsequent reads see it immediately; the write is coalesced in the
|
|
237
|
+
* background. Callers needing on-disk confirmation should use setValue.
|
|
238
|
+
*/
|
|
239
|
+
async setValueNonBlocking(key, value) {
|
|
240
|
+
await this.init();
|
|
241
|
+
this.config[key] = value;
|
|
242
|
+
this.scheduleSave();
|
|
243
|
+
}
|
|
197
244
|
/**
|
|
198
245
|
* Update multiple configuration values at once
|
|
199
246
|
*/
|
|
@@ -41,7 +41,12 @@ function getErrorFromPath(path) {
|
|
|
41
41
|
* Handle read_file command
|
|
42
42
|
*/
|
|
43
43
|
export async function handleReadFile(args) {
|
|
44
|
-
|
|
44
|
+
// Backstop for the whole handler operation. The real control is the
|
|
45
|
+
// 3-minute cancellable read timeout inside readFileFromDisk; this sits just
|
|
46
|
+
// above it (so that one fires first, with cleanup + a useful error) but
|
|
47
|
+
// still below the MCP client's ~4-minute hard cap, so we never leave the
|
|
48
|
+
// client hanging on an opaque timeout.
|
|
49
|
+
const HANDLER_TIMEOUT = 3.5 * 60 * 1000; // 3m30s
|
|
45
50
|
// Add input validation
|
|
46
51
|
if (args === null || args === undefined) {
|
|
47
52
|
return createErrorResponse('No arguments provided for read_file command');
|
|
@@ -95,28 +100,47 @@ export async function handleReadFile(args) {
|
|
|
95
100
|
},
|
|
96
101
|
...pdfContent
|
|
97
102
|
],
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.map((item) => item.text)
|
|
107
|
-
.join("\n"),
|
|
108
|
-
},
|
|
103
|
+
...(parsed.origin === 'ui' ? {
|
|
104
|
+
structuredContent: {
|
|
105
|
+
fileName: path.basename(resolvedFilePath),
|
|
106
|
+
filePath: resolvedFilePath,
|
|
107
|
+
fileType: 'unsupported',
|
|
108
|
+
...await getDefaultEditorMetadata(resolvedFilePath),
|
|
109
|
+
},
|
|
110
|
+
} : {}),
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
113
|
// Handle image files
|
|
112
114
|
if (fileResult.metadata?.isImage) {
|
|
113
115
|
// Return the image bytes in the MCP content array so the host model can
|
|
114
|
-
// actually see the image.
|
|
115
|
-
//
|
|
116
|
+
// actually see the image. The preview widget gets its copy from its own
|
|
117
|
+
// origin:'ui' read — structuredContent stays metadata-only.
|
|
116
118
|
const imageData = typeof fileResult.content === 'string'
|
|
117
119
|
? fileResult.content
|
|
118
120
|
: fileResult.content.toString('base64');
|
|
119
121
|
const imageSummary = `Image file: ${parsed.path} (${fileResult.mimeType})\n`;
|
|
122
|
+
const imageStructuredContent = {
|
|
123
|
+
fileName: path.basename(resolvedFilePath),
|
|
124
|
+
filePath: resolvedFilePath,
|
|
125
|
+
fileType: 'image',
|
|
126
|
+
...await getDefaultEditorMetadata(resolvedFilePath),
|
|
127
|
+
mimeType: fileResult.mimeType,
|
|
128
|
+
};
|
|
129
|
+
// Widget pull (origin: 'ui'): return the base64 in a TEXT block, never
|
|
130
|
+
// an image block. An image content block makes the host inline-vision-
|
|
131
|
+
// render the result, which preempts delivery of this RPC response to
|
|
132
|
+
// the widget — leaving the preview stuck on "Preparing preview…" for
|
|
133
|
+
// exactly the host-rendered types (png/jpeg/gif/webp). Text-only lets
|
|
134
|
+
// the RPC response through so the widget can draw the <img>.
|
|
135
|
+
if (parsed.origin === 'ui') {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: imageData }],
|
|
138
|
+
structuredContent: imageStructuredContent,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// Model-facing read: keep the image block so the host/model sees it.
|
|
142
|
+
// No structuredContent — the widget renders from its own origin:'ui'
|
|
143
|
+
// read, and nothing else consumes it.
|
|
120
144
|
return {
|
|
121
145
|
content: [
|
|
122
146
|
{
|
|
@@ -129,36 +153,32 @@ export async function handleReadFile(args) {
|
|
|
129
153
|
mimeType: fileResult.mimeType
|
|
130
154
|
}
|
|
131
155
|
],
|
|
132
|
-
structuredContent: {
|
|
133
|
-
fileName: path.basename(resolvedFilePath),
|
|
134
|
-
filePath: resolvedFilePath,
|
|
135
|
-
fileType: 'image',
|
|
136
|
-
sourceTool: 'read_file',
|
|
137
|
-
...await getDefaultEditorMetadata(resolvedFilePath),
|
|
138
|
-
content: imageData,
|
|
139
|
-
imageData,
|
|
140
|
-
mimeType: fileResult.mimeType
|
|
141
|
-
}
|
|
142
156
|
};
|
|
143
157
|
}
|
|
144
158
|
else {
|
|
145
159
|
// For all other files, return as text.
|
|
146
160
|
// structuredContent carries only file metadata (no content duplication);
|
|
147
|
-
// the widget reads text from the MCP content array.
|
|
148
|
-
|
|
161
|
+
// the widget reads text from the MCP content array (over the RPC read).
|
|
162
|
+
let textContent = typeof fileResult.content === 'string'
|
|
149
163
|
? fileResult.content
|
|
150
164
|
: fileResult.content.toString('utf8');
|
|
151
165
|
const fileType = fileResult.metadata?.isDirectory ? 'directory' : resolvePreviewFileType(resolvedFilePath);
|
|
166
|
+
// The directory fallback prefixes a "use list_directory instead" hint
|
|
167
|
+
// for the LLM. The widget's own read (a list_directory preview pulls
|
|
168
|
+
// read_file on the dir path) would render that hint as a notice — strip it.
|
|
169
|
+
if (parsed.origin === 'ui' && fileType === 'directory') {
|
|
170
|
+
textContent = textContent.replace(/^This is a directory, not a file\.[^\n]*\n+/, '');
|
|
171
|
+
}
|
|
152
172
|
return {
|
|
153
173
|
content: [{ type: "text", text: textContent }],
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
},
|
|
174
|
+
...(parsed.origin === 'ui' ? {
|
|
175
|
+
structuredContent: {
|
|
176
|
+
fileName: path.basename(resolvedFilePath),
|
|
177
|
+
filePath: resolvedFilePath,
|
|
178
|
+
fileType,
|
|
179
|
+
...await getDefaultEditorMetadata(resolvedFilePath),
|
|
180
|
+
},
|
|
181
|
+
} : {}),
|
|
162
182
|
};
|
|
163
183
|
}
|
|
164
184
|
};
|
|
@@ -237,7 +257,30 @@ export async function handleReadMultipleFiles(args) {
|
|
|
237
257
|
*/
|
|
238
258
|
export async function handleWriteFile(args) {
|
|
239
259
|
try {
|
|
260
|
+
const modeProvided = !!(args && typeof args === 'object' && 'mode' in args);
|
|
240
261
|
const parsed = WriteFileArgsSchema.parse(args);
|
|
262
|
+
// An omitted `mode` falls back to 'rewrite', which silently destroys
|
|
263
|
+
// the target's existing content. The common failure is an LLM caller
|
|
264
|
+
// intending to append but dropping the optional param (issue #541), so
|
|
265
|
+
// require explicit intent when the target already has content;
|
|
266
|
+
// new/empty files keep the zero-friction default.
|
|
267
|
+
if (!modeProvided) {
|
|
268
|
+
let existing = null;
|
|
269
|
+
try {
|
|
270
|
+
existing = await getFileInfo(parsed.path);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Missing file or invalid path — nothing to protect; the write
|
|
274
|
+
// itself will surface any real path error.
|
|
275
|
+
}
|
|
276
|
+
if (existing?.isFile && existing.size > 0) {
|
|
277
|
+
return createErrorResponse(`Write rejected to prevent accidental data loss: ${parsed.path} already exists ` +
|
|
278
|
+
`with content (${existing.size} bytes), and no 'mode' was specified — the default ` +
|
|
279
|
+
`mode 'rewrite' would REPLACE the entire file. Retry with an explicit mode: ` +
|
|
280
|
+
`'append' to add your content to the end of the existing file, or 'rewrite' ` +
|
|
281
|
+
`to replace all existing content.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
241
284
|
// Get the line limit from configuration
|
|
242
285
|
const config = await configManager.getConfig();
|
|
243
286
|
const MAX_LINES = config.fileWriteLineLimit ?? 50; // Default to 50 if not set
|
|
@@ -254,19 +297,13 @@ export async function handleWriteFile(args) {
|
|
|
254
297
|
await writeFile(parsed.path, parsed.content, parsed.mode);
|
|
255
298
|
// Provide more informative message based on mode
|
|
256
299
|
const modeMessage = parsed.mode === 'append' ? 'appended to' : 'wrote to';
|
|
257
|
-
|
|
300
|
+
// No structuredContent: nothing in the UI calls write_file (the widget
|
|
301
|
+
// previews the write via its own read_file pull at tool-result time).
|
|
258
302
|
return {
|
|
259
303
|
content: [{
|
|
260
304
|
type: "text",
|
|
261
305
|
text: `Successfully ${modeMessage} ${parsed.path} (${lineCount} lines) ${errorMessage}`
|
|
262
306
|
}],
|
|
263
|
-
structuredContent: {
|
|
264
|
-
fileName: path.basename(resolvedWritePath),
|
|
265
|
-
filePath: resolvedWritePath,
|
|
266
|
-
fileType: resolvePreviewFileType(resolvedWritePath),
|
|
267
|
-
sourceTool: 'write_file',
|
|
268
|
-
...await getDefaultEditorMetadata(resolvedWritePath),
|
|
269
|
-
},
|
|
270
307
|
};
|
|
271
308
|
}
|
|
272
309
|
catch (error) {
|
|
@@ -303,16 +340,13 @@ export async function handleListDirectory(args) {
|
|
|
303
340
|
const resolvedPath = resolveAbsolutePath(parsed.path);
|
|
304
341
|
return {
|
|
305
342
|
content: [{ type: "text", text: resultText }],
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// render from here and would otherwise show an empty directory.
|
|
314
|
-
content: resultText,
|
|
315
|
-
},
|
|
343
|
+
...(parsed.origin === 'ui' ? {
|
|
344
|
+
structuredContent: {
|
|
345
|
+
fileName: path.basename(resolvedPath),
|
|
346
|
+
filePath: resolvedPath,
|
|
347
|
+
fileType: 'directory',
|
|
348
|
+
},
|
|
349
|
+
} : {}),
|
|
316
350
|
};
|
|
317
351
|
}
|
|
318
352
|
catch (error) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import './bootstrap.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// MUST be first: raises the libuv threadpool size before any fs work is
|
|
3
|
+
// submitted. See src/bootstrap.ts for why import order matters.
|
|
4
|
+
import './bootstrap.js';
|
|
2
5
|
import { FilteredStdioServerTransport } from './custom-stdio.js';
|
|
3
6
|
import { server, flushDeferredMessages } from './server.js';
|
|
4
7
|
import { configManager } from './config-manager.js';
|
|
@@ -2,7 +2,7 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
5
|
+
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { captureRemote } from '../utils/capture.js';
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -24,7 +24,13 @@ export class DesktopCommanderIntegration {
|
|
|
24
24
|
console.debug('[DEBUG] MCP config:', JSON.stringify(config, null, 2));
|
|
25
25
|
try {
|
|
26
26
|
console.debug('[DEBUG] Creating StdioClientTransport');
|
|
27
|
-
|
|
27
|
+
// DC_REMOTE_DEVICE tells the spawned server it is serving remote
|
|
28
|
+
// services, so it suppresses local-only behavior like opening the
|
|
29
|
+
// welcome page in a browser the remote user would never see.
|
|
30
|
+
this.mcpTransport = new StdioClientTransport({
|
|
31
|
+
...config,
|
|
32
|
+
env: { ...getDefaultEnvironment(), ...config.env, DC_REMOTE_DEVICE: 'true' }
|
|
33
|
+
});
|
|
28
34
|
// Create MCP client
|
|
29
35
|
console.debug('[DEBUG] Creating MCP Client');
|
|
30
36
|
this.mcpClient = new Client({
|
|
@@ -20,6 +20,9 @@ export declare class RemoteChannel {
|
|
|
20
20
|
private onToolCall;
|
|
21
21
|
private lastDeviceStatus;
|
|
22
22
|
private lastChannelState;
|
|
23
|
+
private reconnectAttempt;
|
|
24
|
+
private isRecreatingChannel;
|
|
25
|
+
private joiningSince;
|
|
23
26
|
private _user;
|
|
24
27
|
get user(): User | null;
|
|
25
28
|
initialize(url: string, key: string): void;
|
|
@@ -50,10 +53,22 @@ export declare class RemoteChannel {
|
|
|
50
53
|
* This is used for both initial subscription and recreation after socket reconnects.
|
|
51
54
|
*/
|
|
52
55
|
private createChannel;
|
|
56
|
+
/**
|
|
57
|
+
* Compact connection state for logs — e.g. "socket=open(1) ch=errored attempt=3".
|
|
58
|
+
* readyState 1=OPEN (a 1 while joins keep failing = a half-open socket being reused),
|
|
59
|
+
* 3=CLOSED, '-'=no socket. Reads realtime-js internals defensively; never throws.
|
|
60
|
+
*/
|
|
61
|
+
private connState;
|
|
53
62
|
/**
|
|
54
63
|
* Check if channel is connected, recreate if not.
|
|
55
64
|
*/
|
|
56
65
|
private checkConnectionHealth;
|
|
66
|
+
/**
|
|
67
|
+
* Run an async op but reject if it doesn't settle within `ms`, so a hung await
|
|
68
|
+
* can't leave isRecreatingChannel stuck true and disable the watchdog. Mirrors
|
|
69
|
+
* closeWithTimeout() in desktop-commander-integration.ts.
|
|
70
|
+
*/
|
|
71
|
+
private withTimeout;
|
|
57
72
|
/**
|
|
58
73
|
* Recreate the channel by destroying old one and creating fresh instance.
|
|
59
74
|
*/
|