@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
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { createClient } from '@supabase/supabase-js';
|
|
2
2
|
import { captureRemote } from '../utils/capture.js';
|
|
3
3
|
const HEARTBEAT_INTERVAL = 15000;
|
|
4
|
+
// Cap a single channel recreate so a hung await can't pin the re-entrancy guard
|
|
5
|
+
// true (which would silently disable the connection watchdog).
|
|
6
|
+
const RECREATE_TIMEOUT_MS = 30000;
|
|
7
|
+
// Max time the channel may sit CONTINUOUSLY in 'joining' before we force a recreate.
|
|
8
|
+
// 'joining' is normally healthy (we let realtime-js's rejoin backoff converge), but on a
|
|
9
|
+
// HALF-OPEN socket (readyState OPEN yet dead) realtime-js parks the channel in 'joining'
|
|
10
|
+
// forever and never reconnects the socket — the device then wedges offline silently with
|
|
11
|
+
// no recreate firing. realtime-js's join push times out in ~10s, so a genuine join
|
|
12
|
+
// resolves/errors well within this window; 3 health ticks of unbroken 'joining' means the
|
|
13
|
+
// state machine has stalled and only a fresh socket (via recreate) recovers it.
|
|
14
|
+
const JOINING_WEDGE_TIMEOUT_MS = 30000;
|
|
4
15
|
export class RemoteChannel {
|
|
5
16
|
constructor() {
|
|
6
17
|
this.client = null;
|
|
@@ -14,6 +25,10 @@ export class RemoteChannel {
|
|
|
14
25
|
this.lastDeviceStatus = 'offline';
|
|
15
26
|
// Track last channel state for debug logging
|
|
16
27
|
this.lastChannelState = null;
|
|
28
|
+
// Reconnect diagnostics + guard (see connState() / recreateChannel())
|
|
29
|
+
this.reconnectAttempt = 0; // recreateChannel() attempts since last success
|
|
30
|
+
this.isRecreatingChannel = false; // a recreate is in flight (re-entrancy guard)
|
|
31
|
+
this.joiningSince = null; // ts the channel entered an unbroken 'joining' run; null when not joining
|
|
17
32
|
this._user = null;
|
|
18
33
|
}
|
|
19
34
|
get user() { return this._user; }
|
|
@@ -128,7 +143,7 @@ export class RemoteChannel {
|
|
|
128
143
|
console.debug('[DEBUG] Calling createChannel()');
|
|
129
144
|
// ! Ignore silently in Initialization to reconnect after
|
|
130
145
|
await this.createChannel().catch((error) => {
|
|
131
|
-
console.debug(
|
|
146
|
+
console.debug(`[DEBUG] Failed to create channel, will retry after socket reconnect: ${error?.message || error} — ${this.connState()}`);
|
|
132
147
|
});
|
|
133
148
|
}
|
|
134
149
|
else {
|
|
@@ -162,9 +177,11 @@ export class RemoteChannel {
|
|
|
162
177
|
})
|
|
163
178
|
.subscribe((status, err) => {
|
|
164
179
|
// Debug: Log all subscription status events
|
|
165
|
-
console.debug(`[DEBUG] Channel subscription status: ${status}${err ? ' (error: ' + err + ')' : ''}`);
|
|
180
|
+
console.debug(`[DEBUG] Channel subscription status: ${status}${err ? ' (error: ' + (err?.message || err) + ')' : ''} — ${this.connState()}`);
|
|
166
181
|
if (status === 'SUBSCRIBED') {
|
|
167
|
-
|
|
182
|
+
const recovered = this.reconnectAttempt;
|
|
183
|
+
this.reconnectAttempt = 0;
|
|
184
|
+
console.log(`✅ Channel subscribed${recovered > 0 ? ` (recovered after ${recovered} attempt${recovered === 1 ? '' : 's'})` : ''}`);
|
|
168
185
|
// Update device status on successful connection
|
|
169
186
|
if (this.deviceId) {
|
|
170
187
|
this.setOnlineStatus(this.deviceId, 'online').catch(e => {
|
|
@@ -174,20 +191,43 @@ export class RemoteChannel {
|
|
|
174
191
|
resolve();
|
|
175
192
|
}
|
|
176
193
|
else if (status === 'CHANNEL_ERROR') {
|
|
177
|
-
//
|
|
194
|
+
// CHANNEL_ERROR is the only status carrying a real error message.
|
|
195
|
+
console.error(`❌ Channel error: ${err?.message || 'unknown'} — ${this.connState()}`);
|
|
178
196
|
this.setOnlineStatus(this.deviceId, 'offline');
|
|
179
|
-
captureRemote('remote_channel_subscription_error', { error: err || 'Channel error' }).catch(() => { });
|
|
197
|
+
captureRemote('remote_channel_subscription_error', { error: err?.message || 'Channel error' }).catch(() => { });
|
|
180
198
|
reject(err || new Error('Failed to initialize tool call channel subscription'));
|
|
181
199
|
}
|
|
182
200
|
else if (status === 'TIMED_OUT') {
|
|
183
|
-
console.error(
|
|
201
|
+
console.error(`⏱️ Channel subscription timed out, Reconnecting... — ${this.connState()}`);
|
|
184
202
|
this.setOnlineStatus(this.deviceId, 'offline');
|
|
185
|
-
captureRemote('remote_channel_subscription_timeout', {}).catch(() => { });
|
|
203
|
+
captureRemote('remote_channel_subscription_timeout', { attempt: this.reconnectAttempt }).catch(() => { });
|
|
186
204
|
reject(new Error('Tool call channel subscription timed out'));
|
|
187
205
|
}
|
|
206
|
+
else if (status === 'CLOSED') {
|
|
207
|
+
// Settle the promise so an in-flight recreateChannel() can't await
|
|
208
|
+
// forever (which would wedge the re-entrancy guard / watchdog), and
|
|
209
|
+
// mark the device offline like the other degraded states.
|
|
210
|
+
console.warn(`⚠️ Channel closed — ${this.connState()}`);
|
|
211
|
+
this.setOnlineStatus(this.deviceId, 'offline');
|
|
212
|
+
reject(new Error('Tool call channel closed during subscribe'));
|
|
213
|
+
}
|
|
188
214
|
});
|
|
189
215
|
});
|
|
190
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Compact connection state for logs — e.g. "socket=open(1) ch=errored attempt=3".
|
|
219
|
+
* readyState 1=OPEN (a 1 while joins keep failing = a half-open socket being reused),
|
|
220
|
+
* 3=CLOSED, '-'=no socket. Reads realtime-js internals defensively; never throws.
|
|
221
|
+
*/
|
|
222
|
+
connState() {
|
|
223
|
+
let socket = '?';
|
|
224
|
+
try {
|
|
225
|
+
const rt = this.client?.realtime;
|
|
226
|
+
socket = `${rt?.connectionState?.() ?? '?'}(${rt?.conn?.readyState ?? '-'})`;
|
|
227
|
+
}
|
|
228
|
+
catch { /* best effort */ }
|
|
229
|
+
return `socket=${socket} ch=${this.channel?.state ?? '-'} attempt=${this.reconnectAttempt}`;
|
|
230
|
+
}
|
|
191
231
|
/**
|
|
192
232
|
* Check if channel is connected, recreate if not.
|
|
193
233
|
*/
|
|
@@ -198,41 +238,110 @@ export class RemoteChannel {
|
|
|
198
238
|
const state = this.channel.state;
|
|
199
239
|
// Debug: Log current channel state (only if changed)
|
|
200
240
|
if (!this.lastChannelState || this.lastChannelState !== state) {
|
|
201
|
-
console.debug(`[DEBUG] channel state: ${state}`);
|
|
241
|
+
console.debug(`[DEBUG] channel state: ${state} — ${this.connState()}`);
|
|
202
242
|
this.lastChannelState = state;
|
|
203
243
|
}
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
244
|
+
// 'joined' = healthy. Clear the joining-overstay timer.
|
|
245
|
+
if (state === 'joined') {
|
|
246
|
+
this.joiningSince = null;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// 'joining' = transitional — normally let realtime-js's own rejoin backoff converge
|
|
250
|
+
// instead of tearing the channel down mid-join (recreating on every non-joined state
|
|
251
|
+
// amputates that backoff). BUT bound it: on a half-open socket realtime-js can park
|
|
252
|
+
// the channel in 'joining' indefinitely without ever reconnecting the socket, so the
|
|
253
|
+
// recreate below would never fire and the device wedges offline silently. If 'joining'
|
|
254
|
+
// overstays JOINING_WEDGE_TIMEOUT_MS unbroken, force a recreate — the only path that
|
|
255
|
+
// disconnect()s the dead socket. (connState() in the log shows the half-open socket.)
|
|
256
|
+
if (state === 'joining') {
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
if (this.joiningSince === null)
|
|
259
|
+
this.joiningSince = now;
|
|
260
|
+
const stuckMs = now - this.joiningSince;
|
|
261
|
+
if (stuckMs < JOINING_WEDGE_TIMEOUT_MS)
|
|
262
|
+
return;
|
|
263
|
+
console.debug(`[DEBUG] ⚠️ Channel stuck 'joining' ${Math.round(stuckMs / 1000)}s - forcing recreate — ${this.connState()}`);
|
|
264
|
+
captureRemote('remote_channel_joining_wedge', { stuckMs, attempt: this.reconnectAttempt });
|
|
265
|
+
this.joiningSince = null;
|
|
209
266
|
this.recreateChannel();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Unhealthy: closed, errored, leaving — recreate
|
|
270
|
+
this.joiningSince = null;
|
|
271
|
+
captureRemote('remote_channel_state_health', { state, attempt: this.reconnectAttempt });
|
|
272
|
+
console.debug(`[DEBUG] ⚠️ Channel in unhealthy state '${state}' - recreating... — ${this.connState()}`);
|
|
273
|
+
this.recreateChannel();
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Run an async op but reject if it doesn't settle within `ms`, so a hung await
|
|
277
|
+
* can't leave isRecreatingChannel stuck true and disable the watchdog. Mirrors
|
|
278
|
+
* closeWithTimeout() in desktop-commander-integration.ts.
|
|
279
|
+
*/
|
|
280
|
+
async withTimeout(op, ms, name) {
|
|
281
|
+
let timer;
|
|
282
|
+
try {
|
|
283
|
+
return await Promise.race([
|
|
284
|
+
op(),
|
|
285
|
+
new Promise((_, reject) => {
|
|
286
|
+
timer = setTimeout(() => reject(new Error(`${name} timed out after ${ms}ms`)), ms);
|
|
287
|
+
}),
|
|
288
|
+
]);
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
if (timer)
|
|
292
|
+
clearTimeout(timer);
|
|
210
293
|
}
|
|
211
294
|
}
|
|
212
295
|
/**
|
|
213
296
|
* Recreate the channel by destroying old one and creating fresh instance.
|
|
214
297
|
*/
|
|
215
|
-
recreateChannel() {
|
|
298
|
+
async recreateChannel() {
|
|
216
299
|
if (!this.client || !this.user?.id || !this.onToolCall) {
|
|
217
300
|
console.warn('Cannot recreate channel - missing parameters');
|
|
218
301
|
console.debug('[DEBUG] recreateChannel() aborted - missing prerequisites');
|
|
219
302
|
return;
|
|
220
303
|
}
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
304
|
+
// FIX: re-entrancy guard so a 10s health tick can't stack a second recreate
|
|
305
|
+
// on top of an in-flight one.
|
|
306
|
+
if (this.isRecreatingChannel) {
|
|
307
|
+
console.debug('[DEBUG] recreateChannel() skipped - already in progress');
|
|
308
|
+
return;
|
|
226
309
|
}
|
|
310
|
+
this.isRecreatingChannel = true;
|
|
311
|
+
this.reconnectAttempt++;
|
|
227
312
|
// Create fresh channel
|
|
228
|
-
console.log(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
313
|
+
console.log(`🔄 Recreating channel... (attempt ${this.reconnectAttempt}) — ${this.connState()}`);
|
|
314
|
+
try {
|
|
315
|
+
// Cap the whole recreate: a never-settling await (e.g. a subscribe that only
|
|
316
|
+
// ever emits CLOSED) must not pin isRecreatingChannel=true and silently disable
|
|
317
|
+
// the 10s watchdog. On timeout we reject -> catch -> finally clears the guard.
|
|
318
|
+
await this.withTimeout(async () => {
|
|
319
|
+
// Destroy old channel — AWAIT it so the channel registry empties before we
|
|
320
|
+
// rebuild. (The un-awaited version raced the synchronous new-channel push, so
|
|
321
|
+
// realtime-js never tore the socket down and a half-open one got reused.)
|
|
322
|
+
if (this.channel) {
|
|
323
|
+
console.debug('[DEBUG] Destroying old channel');
|
|
324
|
+
await this.client.removeChannel(this.channel);
|
|
325
|
+
this.channel = null;
|
|
326
|
+
}
|
|
327
|
+
// FIX (core): force a brand-new WebSocket. After idle / wifi-loss the socket can
|
|
328
|
+
// be HALF-OPEN (readyState OPEN but dead); reusing it made every join TIME_OUT
|
|
329
|
+
// forever. disconnect() drops it so the next subscribe() dials a fresh one.
|
|
330
|
+
try {
|
|
331
|
+
await this.client.realtime?.disconnect?.();
|
|
332
|
+
}
|
|
333
|
+
catch { /* best effort */ }
|
|
334
|
+
console.debug('[DEBUG] Calling createChannel() for recreation');
|
|
335
|
+
await this.createChannel();
|
|
336
|
+
}, RECREATE_TIMEOUT_MS, 'recreateChannel');
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
captureRemote('remote_channel_recreate_error', { errMsg: err?.message, attempt: this.reconnectAttempt });
|
|
340
|
+
console.debug(`[DEBUG] Channel recreation failed: ${err?.message} — ${this.connState()}`);
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
this.isRecreatingChannel = false;
|
|
344
|
+
}
|
|
236
345
|
}
|
|
237
346
|
async markCallExecuting(callId) {
|
|
238
347
|
if (!this.client)
|
package/dist/server.d.ts
CHANGED
|
@@ -39,4 +39,8 @@ declare let currentClient: {
|
|
|
39
39
|
version: string;
|
|
40
40
|
};
|
|
41
41
|
declare let currentCallIsRemote: boolean;
|
|
42
|
-
|
|
42
|
+
declare let currentRemoteClient: {
|
|
43
|
+
name?: string;
|
|
44
|
+
version?: string;
|
|
45
|
+
} | null;
|
|
46
|
+
export { currentClient, currentCallIsRemote, currentRemoteClient };
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, InitializeRequestSchema, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
4
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
@@ -8,7 +9,8 @@ const OS_GUIDANCE = getOSSpecificGuidance(SYSTEM_INFO);
|
|
|
8
9
|
const DEV_TOOL_GUIDANCE = getDevelopmentToolGuidance(SYSTEM_INFO);
|
|
9
10
|
const PATH_GUIDANCE = `IMPORTANT: ${getPathGuidance(SYSTEM_INFO)} Relative paths may fail as they depend on the current working directory. Tilde paths (~/...) might not work in all contexts. Unless the user explicitly asks for relative paths, use absolute paths.`;
|
|
10
11
|
const CMD_PREFIX_DESCRIPTION = `This command can be referenced as "DC: ..." or "use Desktop Commander to ..." in your instructions.`;
|
|
11
|
-
import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema, KillProcessArgsSchema, ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, GetFileInfoArgsSchema, GetConfigArgsSchema, SetConfigValueArgsSchema, ListProcessesArgsSchema, EditBlockArgsSchema, GetUsageStatsArgsSchema, GiveFeedbackArgsSchema, StartSearchArgsSchema, GetMoreSearchResultsArgsSchema, StopSearchArgsSchema, ListSearchesArgsSchema, GetPromptsArgsSchema, GetRecentToolCallsArgsSchema, WritePdfArgsSchema, } from './tools/schemas.js';
|
|
12
|
+
import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema, KillProcessArgsSchema, ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, GetFileInfoArgsSchema, GetConfigArgsSchema, SetConfigValueArgsSchema, ListProcessesArgsSchema, EditBlockArgsSchema, GetUsageStatsArgsSchema, GiveFeedbackArgsSchema, StartSearchArgsSchema, GetMoreSearchResultsArgsSchema, StopSearchArgsSchema, ListSearchesArgsSchema, GetPromptsArgsSchema, GetRecentToolCallsArgsSchema, WritePdfArgsSchema, toolArgSchemas, } from './tools/schemas.js';
|
|
13
|
+
import { detectUnsupportedParams, getSupportedParams, buildUnsupportedParamsWarning, } from './utils/unsupportedParams.js';
|
|
12
14
|
import { getConfig, setConfigValue } from './tools/config.js';
|
|
13
15
|
import { getUsageStats } from './tools/usage.js';
|
|
14
16
|
import { giveFeedbackToDesktopCommander } from './tools/feedback.js';
|
|
@@ -19,7 +21,7 @@ import { processDockerPrompt } from './utils/dockerPrompt.js';
|
|
|
19
21
|
import { toolHistory } from './utils/toolHistory.js';
|
|
20
22
|
import { handleWelcomePageOnboarding } from './utils/welcome-onboarding.js';
|
|
21
23
|
import { VERSION } from './version.js';
|
|
22
|
-
import { capture, capture_call_tool } from "./utils/capture.js";
|
|
24
|
+
import { capture, capture_call_tool, runInUiOriginCallContext } from "./utils/capture.js";
|
|
23
25
|
import { logToStderr, logger } from './utils/logger.js';
|
|
24
26
|
import { buildUiToolMeta, CONFIG_EDITOR_RESOURCE_URI, FILE_PREVIEW_RESOURCE_URI, } from './ui/contracts.js';
|
|
25
27
|
import { listUiResources, readUiResource } from './ui/resources.js';
|
|
@@ -86,6 +88,28 @@ let currentCallIsRemote = false;
|
|
|
86
88
|
function setCurrentCallIsRemote(isRemote) {
|
|
87
89
|
currentCallIsRemote = isRemote;
|
|
88
90
|
}
|
|
91
|
+
// The remote caller's client for the in-flight tool call (e.g. openai-mcp,
|
|
92
|
+
// claude-ai). Set per CallTool when the call is remote; null for local calls.
|
|
93
|
+
// Mirrors currentCallIsRemote so telemetry attributes remote events to the
|
|
94
|
+
// actual remote client instead of the device's own currentClient (which stays
|
|
95
|
+
// LOCAL and must not be polluted by remote callers).
|
|
96
|
+
let currentRemoteClient = null;
|
|
97
|
+
/**
|
|
98
|
+
* Set the remote caller's client for the current tool call (null when local).
|
|
99
|
+
* Called once per tool call by the CallTool handler.
|
|
100
|
+
*/
|
|
101
|
+
function setCurrentRemoteClient(clientInfo) {
|
|
102
|
+
currentRemoteClient = clientInfo;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* True when this server instance is serving remote services rather than a
|
|
106
|
+
* local MCP client. The remote-device wrapper marks the server it spawns with
|
|
107
|
+
* DC_REMOTE_DEVICE=true (see remote-device/desktop-commander-integration.ts);
|
|
108
|
+
* the client-name check covers older wrappers that predate the env marker.
|
|
109
|
+
*/
|
|
110
|
+
function isRemoteClientContext(clientName) {
|
|
111
|
+
return process.env.DC_REMOTE_DEVICE === 'true' || clientName === 'desktop-commander-client';
|
|
112
|
+
}
|
|
89
113
|
/**
|
|
90
114
|
* Unified way to update client information
|
|
91
115
|
*/
|
|
@@ -114,13 +138,33 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
|
114
138
|
const clientInfo = request.params?.clientInfo;
|
|
115
139
|
if (clientInfo) {
|
|
116
140
|
await updateCurrentClient(clientInfo);
|
|
117
|
-
// Welcome page for new
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
// Welcome page for new users (A/B test controlled) — all clients except
|
|
142
|
+
// the Desktop Commander app and remote contexts, where the server is
|
|
143
|
+
// spawned by the remote-device wrapper and a locally opened browser
|
|
144
|
+
// would never reach the remote user.
|
|
145
|
+
if (currentClient.name !== 'desktop-commander-app'
|
|
146
|
+
&& currentClient.name !== 'desktop-commander'
|
|
147
|
+
&& !isRemoteClientContext(currentClient.name)
|
|
148
|
+
&& !global.disableOnboarding) {
|
|
149
|
+
await handleWelcomePageOnboarding(currentClient.name);
|
|
121
150
|
}
|
|
122
151
|
}
|
|
123
|
-
|
|
152
|
+
// Raw host environment signals (no PII, undefined when absent). Some
|
|
153
|
+
// hosts share a clientInfo name — Claude Code CLI, Claude Code inside
|
|
154
|
+
// the Claude Desktop app, and Cowork all report 'claude-code' — and
|
|
155
|
+
// these let analytics tell them apart without client-specific
|
|
156
|
+
// branching in code. Verified signatures: CLI → entrypoint 'cli';
|
|
157
|
+
// CC-in-desktop → entrypoint 'claude-desktop'; Cowork → no
|
|
158
|
+
// entrypoint/agent, plugin id 'desktop-commander-inline'.
|
|
159
|
+
// Values truncated to GA4's 100-char param limit (same convention as
|
|
160
|
+
// containerName/containerImage) so an oversized value can never get
|
|
161
|
+
// the whole event rejected.
|
|
162
|
+
capture('run_server_mcp_initialized', {
|
|
163
|
+
host_entrypoint: process.env.CLAUDE_CODE_ENTRYPOINT?.substring(0, 100),
|
|
164
|
+
host_agent: process.env.AI_AGENT?.substring(0, 100),
|
|
165
|
+
host_plugin_id: process.env.CLAUDE_PLUGIN_DATA
|
|
166
|
+
? path.basename(process.env.CLAUDE_PLUGIN_DATA).substring(0, 100) : undefined
|
|
167
|
+
});
|
|
124
168
|
// Negotiate protocol version with client
|
|
125
169
|
const requestedVersion = request.params?.protocolVersion;
|
|
126
170
|
const protocolVersion = (requestedVersion && SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion))
|
|
@@ -147,7 +191,7 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
|
147
191
|
}
|
|
148
192
|
});
|
|
149
193
|
// Export current client info for access by other modules
|
|
150
|
-
export { currentClient, currentCallIsRemote };
|
|
194
|
+
export { currentClient, currentCallIsRemote, currentRemoteClient };
|
|
151
195
|
deferLog('info', 'Setting up request handlers...');
|
|
152
196
|
/**
|
|
153
197
|
* Check if a tool should be included based on current client
|
|
@@ -1106,6 +1150,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1106
1150
|
});
|
|
1107
1151
|
import * as handlers from './handlers/index.js';
|
|
1108
1152
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1153
|
+
const args = request.params.arguments;
|
|
1154
|
+
// Calls fired programmatically by the widget UIs (file preview, config
|
|
1155
|
+
// editor) carry origin:'ui'. They are real tool executions but not agent
|
|
1156
|
+
// actions, so they must produce zero telemetry: running them inside the
|
|
1157
|
+
// UI-origin capture context makes capture() drop every event they raise
|
|
1158
|
+
// (server_call_tool, server_read_file, server_edit_block, ...). Deliberate
|
|
1159
|
+
// UI interactions are tracked separately via mcp_ui_event.
|
|
1160
|
+
const isUiOriginCall = !!(args && typeof args === 'object' && args.origin === 'ui');
|
|
1161
|
+
if (isUiOriginCall) {
|
|
1162
|
+
return runInUiOriginCallContext(() => handleCallToolRequest(request));
|
|
1163
|
+
}
|
|
1164
|
+
return handleCallToolRequest(request);
|
|
1165
|
+
});
|
|
1166
|
+
async function handleCallToolRequest(request) {
|
|
1109
1167
|
const { name, arguments: args } = request.params;
|
|
1110
1168
|
const startTime = Date.now();
|
|
1111
1169
|
// Hoisted above the try so the finally block can read them when emitting the
|
|
@@ -1121,22 +1179,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1121
1179
|
// this call carries the remote marker in _meta.
|
|
1122
1180
|
const isRemoteCall = !!(metadata && typeof metadata === 'object' && metadata.remote);
|
|
1123
1181
|
setCurrentCallIsRemote(isRemoteCall);
|
|
1124
|
-
if (
|
|
1125
|
-
// add remote flag
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1182
|
+
if (isRemoteCall) {
|
|
1183
|
+
// add remote flag (convert to string for telemetry)
|
|
1184
|
+
telemetryData.remote = String(metadata.remote);
|
|
1185
|
+
// Remote calls carry the originating MCP client (e.g. openai-mcp,
|
|
1186
|
+
// claude-ai) in _meta.clientInfo. Attribute this call to that remote
|
|
1187
|
+
// client — NOT the device's own currentClient. Fall back to a sentinel
|
|
1188
|
+
// when it's absent so the call is visibly remote-but-unattributed
|
|
1189
|
+
// rather than masquerading as the local device client. We deliberately
|
|
1190
|
+
// do NOT call updateCurrentClient here: currentClient tracks the LOCAL
|
|
1191
|
+
// client and must not be polluted (nor its transport reconfigured) by
|
|
1192
|
+
// remote callers.
|
|
1193
|
+
const remoteClient = metadata.clientInfo && (metadata.clientInfo.name || metadata.clientInfo.version)
|
|
1194
|
+
? metadata.clientInfo
|
|
1195
|
+
: { name: 'remote-unknown', version: 'unknown' };
|
|
1196
|
+
setCurrentRemoteClient(remoteClient);
|
|
1197
|
+
telemetryData.client_name = remoteClient.name;
|
|
1198
|
+
telemetryData.client_version = remoteClient.version;
|
|
1199
|
+
}
|
|
1200
|
+
else {
|
|
1201
|
+
// Local call — clear any remote attribution left by a prior call.
|
|
1202
|
+
setCurrentRemoteClient(null);
|
|
1136
1203
|
}
|
|
1137
1204
|
if (name === 'set_config_value' && args && typeof args === 'object' && 'key' in args) {
|
|
1138
1205
|
telemetryData.set_config_value_key_name = args.key;
|
|
1139
|
-
telemetryData.call_origin = args.origin === 'ui' ? 'ui' : 'llm';
|
|
1140
1206
|
}
|
|
1141
1207
|
if (name === 'get_prompts' && args && typeof args === 'object') {
|
|
1142
1208
|
const promptArgs = args;
|
|
@@ -1435,6 +1501,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1435
1501
|
// Check if should prompt about Docker environment
|
|
1436
1502
|
result = await processDockerPrompt(result, name);
|
|
1437
1503
|
}
|
|
1504
|
+
// If the caller sent parameters this tool does not support, Zod silently
|
|
1505
|
+
// strips them. Prepend a corrective warning so the model knows they were
|
|
1506
|
+
// ignored and which parameters are actually supported.
|
|
1507
|
+
try {
|
|
1508
|
+
const argSchema = toolArgSchemas[name];
|
|
1509
|
+
if (argSchema && result && Array.isArray(result.content)) {
|
|
1510
|
+
const unsupported = detectUnsupportedParams(args, argSchema);
|
|
1511
|
+
if (unsupported.length > 0) {
|
|
1512
|
+
const warning = buildUnsupportedParamsWarning(name, unsupported, getSupportedParams(argSchema));
|
|
1513
|
+
result.content = [
|
|
1514
|
+
{ type: "text", text: warning },
|
|
1515
|
+
...result.content,
|
|
1516
|
+
];
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
catch {
|
|
1521
|
+
// Never let the advisory warning break an otherwise-successful call.
|
|
1522
|
+
}
|
|
1438
1523
|
return result;
|
|
1439
1524
|
}
|
|
1440
1525
|
catch (error) {
|
|
@@ -1454,12 +1539,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1454
1539
|
// Single tool-call telemetry event, fired AFTER execution so it can carry
|
|
1455
1540
|
// timing. In a finally so it still fires on the hard-crash path (the catch
|
|
1456
1541
|
// above). Only missed if a tool never returns or throws (a true hang).
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1542
|
+
// Not emitted for track_ui_event (it is just the transport for
|
|
1543
|
+
// mcp_ui_event) — and UI-origin calls are dropped wholesale by the
|
|
1544
|
+
// capture layer, so server_call_tool reflects only genuine
|
|
1545
|
+
// agent-driven tool calls.
|
|
1546
|
+
if (name !== 'track_ui_event') {
|
|
1547
|
+
capture_call_tool('server_call_tool', {
|
|
1548
|
+
...telemetryData,
|
|
1549
|
+
duration_ms: Date.now() - startTime,
|
|
1550
|
+
is_error: String(isError),
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1462
1553
|
}
|
|
1463
|
-
}
|
|
1554
|
+
}
|
|
1464
1555
|
// Add no-op handlers so Visual Studio initialization succeeds
|
|
1465
1556
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [] }));
|
|
@@ -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
|