@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.
@@ -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, SearchFilesArgsSchema, GetFileInfoArgsSchema, SearchCodeArgsSchema, GetConfigArgsSchema, SetConfigValueArgsSchema, ListProcessesArgsSchema, EditBlockArgsSchema, GetUsageStatsArgsSchema, GiveFeedbackArgsSchema, } from './tools/schemas.js';
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
- console.error("Loading server.ts");
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
- console.log(`Client connected: ${currentClient.name} v${currentClient.version}`);
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
- console.error("Error in initialization handler:", error);
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
- console.error("Setting up request handlers...");
84
+ logToStderr('info', 'Setting up request handlers...');
83
85
  server.setRequestHandler(ListToolsRequestSchema, async () => {
84
86
  try {
85
- console.error("Generating tools list...");
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: "search_files",
255
+ name: "start_search",
254
256
  description: `
255
- Finds files by name using a case-insensitive substring matching.
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
- Use this instead of 'execute_command' with find/dir/ls for locating files.
258
- Searches through all subdirectories from the starting path.
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
- Has a default timeout of 30 seconds which can be customized using the timeoutMs parameter.
261
- Only searches within allowed directories.
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(SearchFilesArgsSchema),
313
+ inputSchema: zodToJsonSchema(GetMoreSearchResultsArgsSchema),
266
314
  },
267
315
  {
268
- name: "search_code",
316
+ name: "stop_search",
269
317
  description: `
270
- Search for text/code patterns within file contents using ripgrep.
318
+ Stop an active search.
271
319
 
272
- Use this instead of 'execute_command' with grep/find for searching code content.
273
- Fast and powerful search similar to VS Code search functionality.
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
- Supports regular expressions, file pattern filtering, and context lines.
276
- Has a default timeout of 30 seconds which can be customized.
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(SearchCodeArgsSchema),
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
- console.error("Error in list_tools request handler:", error);
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
- capture_call_tool('server_call_tool', {
582
- name
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 "search_files":
682
- result = await handlers.handleSearchFiles(args);
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 "search_code":
685
- result = await handlers.handleSearchCode(args);
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);