@wonderwhy-er/desktop-commander 0.2.10 → 0.2.11
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 +17 -5
- package/dist/custom-stdio.d.ts +14 -0
- package/dist/custom-stdio.js +140 -13
- package/dist/handlers/edit-search-handlers.d.ts +0 -5
- package/dist/handlers/edit-search-handlers.js +0 -82
- package/dist/handlers/filesystem-handlers.d.ts +0 -4
- package/dist/handlers/filesystem-handlers.js +2 -36
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.js +1 -0
- package/dist/handlers/search-handlers.d.ts +17 -0
- package/dist/handlers/search-handlers.js +219 -0
- package/dist/index.js +43 -24
- package/dist/search-manager.d.ts +107 -0
- package/dist/search-manager.js +467 -0
- package/dist/server.js +99 -31
- package/dist/tools/filesystem.js +59 -1
- package/dist/tools/schemas.d.ts +55 -41
- package/dist/tools/schemas.js +22 -16
- package/dist/tools/search.js +31 -3
- package/dist/utils/capture.js +56 -8
- package/dist/utils/dedent.d.ts +8 -0
- package/dist/utils/dedent.js +38 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/system-info.d.ts +8 -2
- package/dist/utils/system-info.js +247 -30
- package/dist/utils/usageTracker.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { rgPath } from '@vscode/ripgrep';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { validatePath } from './tools/filesystem.js';
|
|
5
|
+
import { capture } from './utils/capture.js';
|
|
6
|
+
/**
|
|
7
|
+
* Search Session Manager - handles ripgrep processes like terminal sessions
|
|
8
|
+
* Supports both file search and content search with progressive results
|
|
9
|
+
*/ export class SearchManager {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.sessions = new Map();
|
|
12
|
+
this.sessionCounter = 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Start a new search session (like start_process)
|
|
16
|
+
* Returns immediately with initial state and results
|
|
17
|
+
*/
|
|
18
|
+
async startSearch(options) {
|
|
19
|
+
const sessionId = `search_${++this.sessionCounter}_${Date.now()}`;
|
|
20
|
+
// Validate path first
|
|
21
|
+
const validPath = await validatePath(options.rootPath);
|
|
22
|
+
// Build ripgrep arguments
|
|
23
|
+
const args = this.buildRipgrepArgs({ ...options, rootPath: validPath });
|
|
24
|
+
// Start ripgrep process
|
|
25
|
+
const rgProcess = spawn(rgPath, args);
|
|
26
|
+
if (!rgProcess.pid) {
|
|
27
|
+
throw new Error('Failed to start ripgrep process');
|
|
28
|
+
}
|
|
29
|
+
// Create session
|
|
30
|
+
const session = {
|
|
31
|
+
id: sessionId,
|
|
32
|
+
process: rgProcess,
|
|
33
|
+
results: [],
|
|
34
|
+
isComplete: false,
|
|
35
|
+
isError: false,
|
|
36
|
+
startTime: Date.now(),
|
|
37
|
+
lastReadTime: Date.now(),
|
|
38
|
+
options,
|
|
39
|
+
buffer: '',
|
|
40
|
+
totalMatches: 0,
|
|
41
|
+
totalContextLines: 0
|
|
42
|
+
};
|
|
43
|
+
this.sessions.set(sessionId, session);
|
|
44
|
+
// Set up process event handlers
|
|
45
|
+
this.setupProcessHandlers(session);
|
|
46
|
+
// Start cleanup interval now that we have a session
|
|
47
|
+
startCleanupIfNeeded();
|
|
48
|
+
// Set up timeout if specified and auto-terminate
|
|
49
|
+
// For exact filename searches, use a shorter default timeout
|
|
50
|
+
const timeoutMs = options.timeout ?? (this.isExactFilename(options.pattern) ? 1500 : undefined);
|
|
51
|
+
let killTimer = null;
|
|
52
|
+
if (timeoutMs) {
|
|
53
|
+
killTimer = setTimeout(() => {
|
|
54
|
+
if (!session.isComplete && !session.process.killed) {
|
|
55
|
+
session.process.kill('SIGTERM');
|
|
56
|
+
}
|
|
57
|
+
}, timeoutMs);
|
|
58
|
+
}
|
|
59
|
+
// Clear timer on process completion
|
|
60
|
+
session.process.once('close', () => {
|
|
61
|
+
if (killTimer) {
|
|
62
|
+
clearTimeout(killTimer);
|
|
63
|
+
killTimer = null;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
session.process.once('error', () => {
|
|
67
|
+
if (killTimer) {
|
|
68
|
+
clearTimeout(killTimer);
|
|
69
|
+
killTimer = null;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
capture('search_session_started', {
|
|
73
|
+
sessionId,
|
|
74
|
+
searchType: options.searchType,
|
|
75
|
+
hasTimeout: !!timeoutMs,
|
|
76
|
+
timeoutMs,
|
|
77
|
+
requestedPath: options.rootPath,
|
|
78
|
+
validatedPath: validPath
|
|
79
|
+
});
|
|
80
|
+
// Wait for first chunk of data or early completion instead of fixed delay
|
|
81
|
+
const firstChunk = new Promise(resolve => {
|
|
82
|
+
const onData = () => {
|
|
83
|
+
session.process.stdout?.off('data', onData);
|
|
84
|
+
resolve();
|
|
85
|
+
};
|
|
86
|
+
session.process.stdout?.once('data', onData);
|
|
87
|
+
setTimeout(resolve, 40); // cap at 40ms instead of 50-100ms
|
|
88
|
+
});
|
|
89
|
+
await firstChunk;
|
|
90
|
+
return {
|
|
91
|
+
sessionId,
|
|
92
|
+
isComplete: session.isComplete,
|
|
93
|
+
isError: session.isError,
|
|
94
|
+
results: [...session.results],
|
|
95
|
+
totalResults: session.totalMatches,
|
|
96
|
+
runtime: Date.now() - session.startTime
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Read search results with offset-based pagination (like read_file)
|
|
101
|
+
* Supports both range reading and tail behavior
|
|
102
|
+
*/
|
|
103
|
+
readSearchResults(sessionId, offset = 0, length = 100) {
|
|
104
|
+
const session = this.sessions.get(sessionId);
|
|
105
|
+
if (!session) {
|
|
106
|
+
throw new Error(`Search session ${sessionId} not found`);
|
|
107
|
+
}
|
|
108
|
+
// Get all results (excluding internal markers)
|
|
109
|
+
const allResults = session.results.filter(r => r.file !== '__LAST_READ_MARKER__');
|
|
110
|
+
// Handle negative offsets (tail behavior) - like file reading
|
|
111
|
+
if (offset < 0) {
|
|
112
|
+
const tailCount = Math.abs(offset);
|
|
113
|
+
const tailResults = allResults.slice(-tailCount);
|
|
114
|
+
return {
|
|
115
|
+
results: tailResults,
|
|
116
|
+
returnedCount: tailResults.length,
|
|
117
|
+
totalResults: session.totalMatches + session.totalContextLines,
|
|
118
|
+
totalMatches: session.totalMatches, // Actual matches only
|
|
119
|
+
isComplete: session.isComplete,
|
|
120
|
+
isError: session.isError && !!session.error?.trim(), // Only error if we have actual errors
|
|
121
|
+
error: session.error?.trim() || undefined,
|
|
122
|
+
hasMoreResults: false, // Tail always returns what's available
|
|
123
|
+
runtime: Date.now() - session.startTime
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Handle positive offsets (range behavior) - like file reading
|
|
127
|
+
const slicedResults = allResults.slice(offset, offset + length);
|
|
128
|
+
const hasMoreResults = offset + length < allResults.length || !session.isComplete;
|
|
129
|
+
session.lastReadTime = Date.now();
|
|
130
|
+
return {
|
|
131
|
+
results: slicedResults,
|
|
132
|
+
returnedCount: slicedResults.length,
|
|
133
|
+
totalResults: session.totalMatches + session.totalContextLines,
|
|
134
|
+
totalMatches: session.totalMatches, // Actual matches only
|
|
135
|
+
isComplete: session.isComplete,
|
|
136
|
+
isError: session.isError && !!session.error?.trim(), // Only error if we have actual errors
|
|
137
|
+
error: session.error?.trim() || undefined,
|
|
138
|
+
hasMoreResults,
|
|
139
|
+
runtime: Date.now() - session.startTime
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Terminate a search session (like force_terminate)
|
|
144
|
+
*/
|
|
145
|
+
terminateSearch(sessionId) {
|
|
146
|
+
const session = this.sessions.get(sessionId);
|
|
147
|
+
if (!session) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (!session.process.killed) {
|
|
151
|
+
session.process.kill('SIGTERM');
|
|
152
|
+
capture('search_session_terminated', { sessionId });
|
|
153
|
+
}
|
|
154
|
+
// Don't delete session immediately - let user read final results
|
|
155
|
+
// It will be cleaned up by cleanup process
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get list of active search sessions (like list_sessions)
|
|
160
|
+
*/
|
|
161
|
+
listSearchSessions() {
|
|
162
|
+
return Array.from(this.sessions.values()).map(session => ({
|
|
163
|
+
id: session.id,
|
|
164
|
+
searchType: session.options.searchType,
|
|
165
|
+
pattern: session.options.pattern,
|
|
166
|
+
isComplete: session.isComplete,
|
|
167
|
+
isError: session.isError,
|
|
168
|
+
runtime: Date.now() - session.startTime,
|
|
169
|
+
totalResults: session.totalMatches + session.totalContextLines
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Clean up completed sessions older than specified time
|
|
174
|
+
* Called automatically by cleanup interval
|
|
175
|
+
*/
|
|
176
|
+
cleanupSessions(maxAge = 5 * 60 * 1000) {
|
|
177
|
+
const cutoffTime = Date.now() - maxAge;
|
|
178
|
+
for (const [sessionId, session] of this.sessions) {
|
|
179
|
+
if (session.isComplete && session.lastReadTime < cutoffTime) {
|
|
180
|
+
this.sessions.delete(sessionId);
|
|
181
|
+
capture('search_session_cleaned_up', { sessionId });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get total number of active sessions (excluding completed ones)
|
|
187
|
+
*/
|
|
188
|
+
getActiveSessionCount() {
|
|
189
|
+
return Array.from(this.sessions.values()).filter(session => !session.isComplete).length;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Detect if pattern looks like an exact filename
|
|
193
|
+
* (has file extension and no glob wildcards)
|
|
194
|
+
*/
|
|
195
|
+
isExactFilename(pattern) {
|
|
196
|
+
return /\.[a-zA-Z0-9]+$/.test(pattern) &&
|
|
197
|
+
!this.isGlobPattern(pattern);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Detect if pattern contains glob wildcards
|
|
201
|
+
*/
|
|
202
|
+
isGlobPattern(pattern) {
|
|
203
|
+
return pattern.includes('*') ||
|
|
204
|
+
pattern.includes('?') ||
|
|
205
|
+
pattern.includes('[') ||
|
|
206
|
+
pattern.includes('{') ||
|
|
207
|
+
pattern.includes(']') ||
|
|
208
|
+
pattern.includes('}');
|
|
209
|
+
}
|
|
210
|
+
buildRipgrepArgs(options) {
|
|
211
|
+
const args = [];
|
|
212
|
+
if (options.searchType === 'content') {
|
|
213
|
+
// Content search mode
|
|
214
|
+
args.push('--json', '--line-number');
|
|
215
|
+
if (options.contextLines && options.contextLines > 0) {
|
|
216
|
+
args.push('-C', options.contextLines.toString());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// File search mode
|
|
221
|
+
args.push('--files');
|
|
222
|
+
}
|
|
223
|
+
// Case-insensitive: content searches use -i flag, file searches use --iglob
|
|
224
|
+
if (options.searchType === 'content' && options.ignoreCase !== false) {
|
|
225
|
+
args.push('-i');
|
|
226
|
+
}
|
|
227
|
+
if (options.includeHidden) {
|
|
228
|
+
args.push('--hidden');
|
|
229
|
+
}
|
|
230
|
+
if (options.maxResults && options.maxResults > 0) {
|
|
231
|
+
args.push('-m', options.maxResults.toString());
|
|
232
|
+
}
|
|
233
|
+
// File pattern filtering (for file type restrictions like *.js, *.d.ts)
|
|
234
|
+
if (options.filePattern) {
|
|
235
|
+
const patterns = options.filePattern
|
|
236
|
+
.split('|')
|
|
237
|
+
.map(p => p.trim())
|
|
238
|
+
.filter(Boolean);
|
|
239
|
+
for (const p of patterns) {
|
|
240
|
+
if (options.searchType === 'content') {
|
|
241
|
+
args.push('-g', p);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// For file search: use --iglob for case-insensitive or --glob for case-sensitive
|
|
245
|
+
if (options.ignoreCase !== false) {
|
|
246
|
+
args.push('--iglob', p);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
args.push('--glob', p);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Handle the main search pattern
|
|
255
|
+
if (options.searchType === 'files') {
|
|
256
|
+
// For file search: determine how to treat the pattern
|
|
257
|
+
const globFlag = options.ignoreCase !== false ? '--iglob' : '--glob';
|
|
258
|
+
if (this.isExactFilename(options.pattern)) {
|
|
259
|
+
// Exact filename: use appropriate glob flag with the exact pattern
|
|
260
|
+
args.push(globFlag, options.pattern);
|
|
261
|
+
}
|
|
262
|
+
else if (this.isGlobPattern(options.pattern)) {
|
|
263
|
+
// Already a glob pattern: use appropriate glob flag as-is
|
|
264
|
+
args.push(globFlag, options.pattern);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// Substring/fuzzy search: wrap with wildcards
|
|
268
|
+
args.push(globFlag, `*${options.pattern}*`);
|
|
269
|
+
}
|
|
270
|
+
// Add the root path for file mode
|
|
271
|
+
args.push(options.rootPath);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
// Content search: terminate options before the pattern to prevent
|
|
275
|
+
// patterns starting with '-' being interpreted as flags
|
|
276
|
+
args.push('--', options.pattern, options.rootPath);
|
|
277
|
+
}
|
|
278
|
+
return args;
|
|
279
|
+
}
|
|
280
|
+
setupProcessHandlers(session) {
|
|
281
|
+
const { process } = session;
|
|
282
|
+
process.stdout?.on('data', (data) => {
|
|
283
|
+
session.buffer += data.toString();
|
|
284
|
+
this.processBufferedOutput(session);
|
|
285
|
+
});
|
|
286
|
+
process.stderr?.on('data', (data) => {
|
|
287
|
+
const errorText = data.toString();
|
|
288
|
+
// Filter meaningful errors
|
|
289
|
+
const filteredErrors = errorText
|
|
290
|
+
.split('\n')
|
|
291
|
+
.filter(line => {
|
|
292
|
+
const trimmed = line.trim();
|
|
293
|
+
// Skip empty lines and lines with just symbols/numbers/colons
|
|
294
|
+
if (!trimmed || trimmed.match(/^[\)\(\s\d:]*$/))
|
|
295
|
+
return false;
|
|
296
|
+
// Skip all ripgrep system errors that start with "rg:"
|
|
297
|
+
if (trimmed.startsWith('rg:'))
|
|
298
|
+
return false;
|
|
299
|
+
return true;
|
|
300
|
+
});
|
|
301
|
+
// Only add to session.error if there are actual meaningful errors after filtering
|
|
302
|
+
if (filteredErrors.length > 0) {
|
|
303
|
+
const meaningfulErrors = filteredErrors.join('\n').trim();
|
|
304
|
+
if (meaningfulErrors) {
|
|
305
|
+
session.error = (session.error || '') + meaningfulErrors + '\n';
|
|
306
|
+
capture('search_session_error', {
|
|
307
|
+
sessionId: session.id,
|
|
308
|
+
error: meaningfulErrors.substring(0, 200)
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
process.on('close', (code) => {
|
|
314
|
+
// Process any remaining buffer content
|
|
315
|
+
if (session.buffer.trim()) {
|
|
316
|
+
this.processBufferedOutput(session, true);
|
|
317
|
+
}
|
|
318
|
+
session.isComplete = true;
|
|
319
|
+
// Only treat as error if:
|
|
320
|
+
// 1. Unexpected exit code (not 0, 1, or 2) AND
|
|
321
|
+
// 2. We have meaningful errors after filtering AND
|
|
322
|
+
// 3. We found no results at all
|
|
323
|
+
if (code !== 0 && code !== 1 && code !== 2) {
|
|
324
|
+
// Codes 0=success, 1=no matches, 2=some files couldn't be searched
|
|
325
|
+
if (session.error?.trim() && session.totalMatches === 0) {
|
|
326
|
+
session.isError = true;
|
|
327
|
+
session.error = session.error || `ripgrep exited with code ${code}`;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// If we have results, don't mark as error even if there were permission issues
|
|
331
|
+
if (session.totalMatches > 0) {
|
|
332
|
+
session.isError = false;
|
|
333
|
+
}
|
|
334
|
+
capture('search_session_completed', {
|
|
335
|
+
sessionId: session.id,
|
|
336
|
+
exitCode: code,
|
|
337
|
+
totalResults: session.totalMatches + session.totalContextLines,
|
|
338
|
+
totalMatches: session.totalMatches,
|
|
339
|
+
runtime: Date.now() - session.startTime
|
|
340
|
+
});
|
|
341
|
+
// Rely on cleanupSessions(maxAge) only; no per-session timer
|
|
342
|
+
});
|
|
343
|
+
process.on('error', (error) => {
|
|
344
|
+
session.isComplete = true;
|
|
345
|
+
session.isError = true;
|
|
346
|
+
session.error = `Process error: ${error.message}`;
|
|
347
|
+
capture('search_session_process_error', {
|
|
348
|
+
sessionId: session.id,
|
|
349
|
+
error: error.message
|
|
350
|
+
});
|
|
351
|
+
// Rely on cleanupSessions(maxAge) only; no per-session timer
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
processBufferedOutput(session, isFinal = false) {
|
|
355
|
+
const lines = session.buffer.split('\n');
|
|
356
|
+
// Keep the last incomplete line in the buffer unless this is final processing
|
|
357
|
+
if (!isFinal) {
|
|
358
|
+
session.buffer = lines.pop() || '';
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
session.buffer = '';
|
|
362
|
+
}
|
|
363
|
+
for (const line of lines) {
|
|
364
|
+
if (!line.trim())
|
|
365
|
+
continue;
|
|
366
|
+
const result = this.parseLine(line, session.options.searchType);
|
|
367
|
+
if (result) {
|
|
368
|
+
session.results.push(result);
|
|
369
|
+
// Separate counting of matches vs context lines
|
|
370
|
+
if (result.type === 'content' && line.includes('"type":"context"')) {
|
|
371
|
+
session.totalContextLines++;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
session.totalMatches++;
|
|
375
|
+
}
|
|
376
|
+
// Early termination for exact filename matches (if enabled)
|
|
377
|
+
if (session.options.earlyTermination !== false && // Default to true
|
|
378
|
+
session.options.searchType === 'files' &&
|
|
379
|
+
this.isExactFilename(session.options.pattern)) {
|
|
380
|
+
const pat = path.normalize(session.options.pattern);
|
|
381
|
+
const filePath = path.normalize(result.file);
|
|
382
|
+
const ignoreCase = session.options.ignoreCase !== false;
|
|
383
|
+
const ends = ignoreCase
|
|
384
|
+
? filePath.toLowerCase().endsWith(pat.toLowerCase())
|
|
385
|
+
: filePath.endsWith(pat);
|
|
386
|
+
if (ends) {
|
|
387
|
+
// Found exact match, terminate search early
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
if (!session.process.killed) {
|
|
390
|
+
session.process.kill('SIGTERM');
|
|
391
|
+
}
|
|
392
|
+
}, 100); // Small delay to allow any remaining results
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
parseLine(line, searchType) {
|
|
400
|
+
if (searchType === 'content') {
|
|
401
|
+
// Parse JSON output from content search
|
|
402
|
+
try {
|
|
403
|
+
const parsed = JSON.parse(line);
|
|
404
|
+
if (parsed.type === 'match') {
|
|
405
|
+
// Handle multiple submatches per line - return first submatch
|
|
406
|
+
const submatch = parsed.data?.submatches?.[0];
|
|
407
|
+
return {
|
|
408
|
+
file: parsed.data.path.text,
|
|
409
|
+
line: parsed.data.line_number,
|
|
410
|
+
match: submatch?.match?.text || parsed.data.lines.text,
|
|
411
|
+
type: 'content'
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (parsed.type === 'context') {
|
|
415
|
+
return {
|
|
416
|
+
file: parsed.data.path.text,
|
|
417
|
+
line: parsed.data.line_number,
|
|
418
|
+
match: parsed.data.lines.text.trim(),
|
|
419
|
+
type: 'content'
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// Handle summary to reconcile totals
|
|
423
|
+
if (parsed.type === 'summary') {
|
|
424
|
+
// Optional: could reconcile totalMatches with parsed.data.stats?.matchedLines
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
// Skip invalid JSON lines
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
// File search - each line is a file path
|
|
436
|
+
return {
|
|
437
|
+
file: line.trim(),
|
|
438
|
+
type: 'file'
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Global search manager instance
|
|
444
|
+
export const searchManager = new SearchManager();
|
|
445
|
+
// Cleanup management - run on fixed schedule
|
|
446
|
+
let cleanupInterval = null;
|
|
447
|
+
/**
|
|
448
|
+
* Start cleanup interval - now runs on fixed schedule
|
|
449
|
+
*/
|
|
450
|
+
function startCleanupIfNeeded() {
|
|
451
|
+
if (!cleanupInterval) {
|
|
452
|
+
cleanupInterval = setInterval(() => {
|
|
453
|
+
searchManager.cleanupSessions();
|
|
454
|
+
}, 5 * 60 * 1000);
|
|
455
|
+
// Also check immediately after a short delay (let search process finish)
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
searchManager.cleanupSessions();
|
|
458
|
+
}, 1000);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Export cleanup function for graceful shutdown
|
|
462
|
+
export function stopSearchManagerCleanup() {
|
|
463
|
+
if (cleanupInterval) {
|
|
464
|
+
clearInterval(cleanupInterval);
|
|
465
|
+
cleanupInterval = null;
|
|
466
|
+
}
|
|
467
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -8,7 +8,7 @@ const OS_GUIDANCE = getOSSpecificGuidance(SYSTEM_INFO);
|
|
|
8
8
|
const DEV_TOOL_GUIDANCE = getDevelopmentToolGuidance(SYSTEM_INFO);
|
|
9
9
|
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
10
|
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,
|
|
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, } from './tools/schemas.js';
|
|
12
12
|
import { getConfig, setConfigValue } from './tools/config.js';
|
|
13
13
|
import { getUsageStats } from './tools/usage.js';
|
|
14
14
|
import { giveFeedbackToDesktopCommander } from './tools/feedback.js';
|
|
@@ -17,7 +17,8 @@ import { usageTracker } from './utils/usageTracker.js';
|
|
|
17
17
|
import { processDockerPrompt } from './utils/dockerPrompt.js';
|
|
18
18
|
import { VERSION } from './version.js';
|
|
19
19
|
import { capture, capture_call_tool } from "./utils/capture.js";
|
|
20
|
-
|
|
20
|
+
import { logToStderr } from './utils/logger.js';
|
|
21
|
+
logToStderr('info', 'Loading server.ts');
|
|
21
22
|
export const server = new Server({
|
|
22
23
|
name: "desktop-commander",
|
|
23
24
|
version: VERSION,
|
|
@@ -55,7 +56,8 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
|
55
56
|
name: clientInfo.name || 'unknown',
|
|
56
57
|
version: clientInfo.version || 'unknown'
|
|
57
58
|
};
|
|
58
|
-
|
|
59
|
+
// Send JSON-RPC notification about client connection
|
|
60
|
+
logToStderr('info', `Client connected: ${currentClient.name} v${currentClient.version}`);
|
|
59
61
|
}
|
|
60
62
|
// Return standard initialization response
|
|
61
63
|
return {
|
|
@@ -73,16 +75,16 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
catch (error) {
|
|
76
|
-
|
|
78
|
+
logToStderr('error', `Error in initialization handler: ${error}`);
|
|
77
79
|
throw error;
|
|
78
80
|
}
|
|
79
81
|
});
|
|
80
82
|
// Export current client info for access by other modules
|
|
81
83
|
export { currentClient };
|
|
82
|
-
|
|
84
|
+
logToStderr('info', 'Setting up request handlers...');
|
|
83
85
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
84
86
|
try {
|
|
85
|
-
|
|
87
|
+
logToStderr('debug', 'Generating tools list...');
|
|
86
88
|
return {
|
|
87
89
|
tools: [
|
|
88
90
|
// Configuration tools
|
|
@@ -250,35 +252,92 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
250
252
|
inputSchema: zodToJsonSchema(MoveFileArgsSchema),
|
|
251
253
|
},
|
|
252
254
|
{
|
|
253
|
-
name: "
|
|
255
|
+
name: "start_search",
|
|
254
256
|
description: `
|
|
255
|
-
|
|
257
|
+
Start a streaming search that can return results progressively.
|
|
258
|
+
|
|
259
|
+
SEARCH TYPES:
|
|
260
|
+
- searchType="files": Find files by name (pattern matches file names)
|
|
261
|
+
- searchType="content": Search inside files for text patterns
|
|
262
|
+
|
|
263
|
+
IMPORTANT PARAMETERS:
|
|
264
|
+
- pattern: What to search for (file names OR content text)
|
|
265
|
+
- filePattern: Optional filter to limit search to specific file types (e.g., "*.js", "package.json")
|
|
266
|
+
- ignoreCase: Case-insensitive search (default: true). Works for both file names and content.
|
|
267
|
+
- earlyTermination: Stop search early when exact filename match is found (optional: defaults to true for file searches, false for content searches)
|
|
268
|
+
|
|
269
|
+
EXAMPLES:
|
|
270
|
+
- Find package.json files: searchType="files", pattern="package.json", filePattern="package.json"
|
|
271
|
+
- Find all JS files: searchType="files", pattern="*.js" (or use filePattern="*.js")
|
|
272
|
+
- Search for "TODO" in code: searchType="content", pattern="TODO", filePattern="*.js|*.ts"
|
|
273
|
+
- Case-sensitive file search: searchType="files", pattern="README", ignoreCase=false
|
|
274
|
+
- Case-insensitive file search: searchType="files", pattern="readme", ignoreCase=true
|
|
275
|
+
- Find exact file, stop after first match: searchType="files", pattern="config.json", earlyTermination=true
|
|
276
|
+
- Find all matching files: searchType="files", pattern="test.js", earlyTermination=false
|
|
277
|
+
|
|
278
|
+
Unlike regular search tools, this starts a background search process and returns
|
|
279
|
+
immediately with a session ID. Use get_more_search_results to get results as they
|
|
280
|
+
come in, and stop_search to stop the search early if needed.
|
|
281
|
+
|
|
282
|
+
Perfect for large directories where you want to see results immediately and
|
|
283
|
+
have the option to cancel if the search takes too long or you find what you need.
|
|
256
284
|
|
|
257
|
-
|
|
258
|
-
|
|
285
|
+
${PATH_GUIDANCE}
|
|
286
|
+
${CMD_PREFIX_DESCRIPTION}`,
|
|
287
|
+
inputSchema: zodToJsonSchema(StartSearchArgsSchema),
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "get_more_search_results",
|
|
291
|
+
description: `
|
|
292
|
+
Get more results from an active search with offset-based pagination.
|
|
259
293
|
|
|
260
|
-
|
|
261
|
-
|
|
294
|
+
Supports partial result reading with:
|
|
295
|
+
- 'offset' (start result index, default: 0)
|
|
296
|
+
* Positive: Start from result N (0-based indexing)
|
|
297
|
+
* Negative: Read last N results from end (tail behavior)
|
|
298
|
+
- 'length' (max results to read, default: 100)
|
|
299
|
+
* Used with positive offsets for range reading
|
|
300
|
+
* Ignored when offset is negative (reads all requested tail results)
|
|
301
|
+
|
|
302
|
+
Examples:
|
|
303
|
+
- offset: 0, length: 100 → First 100 results
|
|
304
|
+
- offset: 200, length: 50 → Results 200-249
|
|
305
|
+
- offset: -20 → Last 20 results
|
|
306
|
+
- offset: -5, length: 10 → Last 5 results (length ignored)
|
|
307
|
+
|
|
308
|
+
Returns only results in the specified range, along with search status.
|
|
309
|
+
Works like read_process_output - call this repeatedly to get progressive
|
|
310
|
+
results from a search started with start_search.
|
|
262
311
|
|
|
263
|
-
${PATH_GUIDANCE}
|
|
264
312
|
${CMD_PREFIX_DESCRIPTION}`,
|
|
265
|
-
inputSchema: zodToJsonSchema(
|
|
313
|
+
inputSchema: zodToJsonSchema(GetMoreSearchResultsArgsSchema),
|
|
266
314
|
},
|
|
267
315
|
{
|
|
268
|
-
name: "
|
|
316
|
+
name: "stop_search",
|
|
269
317
|
description: `
|
|
270
|
-
|
|
318
|
+
Stop an active search.
|
|
271
319
|
|
|
272
|
-
|
|
273
|
-
|
|
320
|
+
Stops the background search process gracefully. Use this when you've found
|
|
321
|
+
what you need or if a search is taking too long. Similar to force_terminate
|
|
322
|
+
for terminal processes.
|
|
274
323
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
Only searches within allowed directories.
|
|
324
|
+
The search will still be available for reading final results until it's
|
|
325
|
+
automatically cleaned up after 5 minutes.
|
|
278
326
|
|
|
279
|
-
${PATH_GUIDANCE}
|
|
280
327
|
${CMD_PREFIX_DESCRIPTION}`,
|
|
281
|
-
inputSchema: zodToJsonSchema(
|
|
328
|
+
inputSchema: zodToJsonSchema(StopSearchArgsSchema),
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "list_searches",
|
|
332
|
+
description: `
|
|
333
|
+
List all active searches.
|
|
334
|
+
|
|
335
|
+
Shows search IDs, search types, patterns, status, and runtime.
|
|
336
|
+
Similar to list_sessions for terminal processes. Useful for managing
|
|
337
|
+
multiple concurrent searches.
|
|
338
|
+
|
|
339
|
+
${CMD_PREFIX_DESCRIPTION}`,
|
|
340
|
+
inputSchema: zodToJsonSchema(ListSearchesArgsSchema),
|
|
282
341
|
},
|
|
283
342
|
{
|
|
284
343
|
name: "get_file_info",
|
|
@@ -570,7 +629,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
570
629
|
};
|
|
571
630
|
}
|
|
572
631
|
catch (error) {
|
|
573
|
-
|
|
632
|
+
logToStderr('error', `Error in list_tools request handler: ${error}`);
|
|
574
633
|
throw error;
|
|
575
634
|
}
|
|
576
635
|
});
|
|
@@ -578,9 +637,12 @@ import * as handlers from './handlers/index.js';
|
|
|
578
637
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
579
638
|
const { name, arguments: args } = request.params;
|
|
580
639
|
try {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
640
|
+
// Prepare telemetry data - add config key for set_config_value
|
|
641
|
+
const telemetryData = { name };
|
|
642
|
+
if (name === 'set_config_value' && args && typeof args === 'object' && 'key' in args) {
|
|
643
|
+
telemetryData.set_config_value_key_name = args.key;
|
|
644
|
+
}
|
|
645
|
+
capture_call_tool('server_call_tool', telemetryData);
|
|
584
646
|
// Track tool call
|
|
585
647
|
trackToolCall(name, args);
|
|
586
648
|
// Using a more structured approach with dedicated handlers
|
|
@@ -678,11 +740,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
678
740
|
case "move_file":
|
|
679
741
|
result = await handlers.handleMoveFile(args);
|
|
680
742
|
break;
|
|
681
|
-
case "
|
|
682
|
-
result = await handlers.
|
|
743
|
+
case "start_search":
|
|
744
|
+
result = await handlers.handleStartSearch(args);
|
|
745
|
+
break;
|
|
746
|
+
case "get_more_search_results":
|
|
747
|
+
result = await handlers.handleGetMoreSearchResults(args);
|
|
748
|
+
break;
|
|
749
|
+
case "stop_search":
|
|
750
|
+
result = await handlers.handleStopSearch(args);
|
|
683
751
|
break;
|
|
684
|
-
case "
|
|
685
|
-
result = await handlers.
|
|
752
|
+
case "list_searches":
|
|
753
|
+
result = await handlers.handleListSearches();
|
|
686
754
|
break;
|
|
687
755
|
case "get_file_info":
|
|
688
756
|
result = await handlers.handleGetFileInfo(args);
|