@wonderwhy-er/desktop-commander 0.1.39 → 0.2.1

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.
@@ -208,38 +208,41 @@ export async function readFileFromUrl(url) {
208
208
  /**
209
209
  * Read file content from the local filesystem
210
210
  * @param filePath Path to the file
211
- * @param returnMetadata Whether to return metadata with the content
211
+ * @param offset Starting line number to read from (default: 0)
212
+ * @param length Maximum number of lines to read (default: from config or 1000)
212
213
  * @returns File content or file result with metadata
213
214
  */
214
- export async function readFileFromDisk(filePath) {
215
+ export async function readFileFromDisk(filePath, offset = 0, length) {
216
+ // Add validation for required parameters
217
+ if (!filePath || typeof filePath !== 'string') {
218
+ throw new Error('Invalid file path provided');
219
+ }
215
220
  // Import the MIME type utilities
216
221
  const { getMimeType, isImageFile } = await import('./mime-types.js');
222
+ // Get default length from config if not provided
223
+ if (length === undefined) {
224
+ const config = await configManager.getConfig();
225
+ length = config.fileReadLineLimit ?? 1000; // Default to 1000 lines if not set
226
+ }
217
227
  const validPath = await validatePath(filePath);
218
228
  // Get file extension for telemetry using path module consistently
219
229
  const fileExtension = path.extname(validPath).toLowerCase();
220
230
  // Check file size before attempting to read
221
231
  try {
222
232
  const stats = await fs.stat(validPath);
223
- const MAX_SIZE = 100 * 1024; // 100KB limit
224
- if (stats.size > MAX_SIZE) {
225
- const message = `File too large (${(stats.size / 1024).toFixed(2)}KB > ${MAX_SIZE / 1024}KB limit)`;
226
- // Capture file extension in telemetry without capturing the file path
227
- capture('server_read_file_large', { fileExtension: fileExtension });
228
- return {
229
- content: message,
230
- mimeType: 'text/plain',
231
- isImage: false
232
- };
233
- }
234
233
  // Capture file extension in telemetry without capturing the file path
235
- capture('server_read_file', { fileExtension: fileExtension });
234
+ capture('server_read_file', {
235
+ fileExtension: fileExtension,
236
+ offset: offset,
237
+ length: length,
238
+ fileSize: stats.size
239
+ });
236
240
  }
237
241
  catch (error) {
238
242
  console.error('error catch ' + error);
239
243
  const errorMessage = error instanceof Error ? error.message : String(error);
240
244
  capture('server_read_file_error', { error: errorMessage, fileExtension: fileExtension });
241
245
  // If we can't stat the file, continue anyway and let the read operation handle errors
242
- //console.error(`Failed to stat file ${validPath}:`, error);
243
246
  }
244
247
  // Detect the MIME type based on file extension
245
248
  const mimeType = getMimeType(validPath);
@@ -249,15 +252,47 @@ export async function readFileFromDisk(filePath) {
249
252
  const readOperation = async () => {
250
253
  if (isImage) {
251
254
  // For image files, read as Buffer and convert to base64
255
+ // Images are always read in full, ignoring offset and length
252
256
  const buffer = await fs.readFile(validPath);
253
257
  const content = buffer.toString('base64');
254
258
  return { content, mimeType, isImage };
255
259
  }
256
260
  else {
257
- // For all other files, try to read as UTF-8 text
261
+ // For all other files, try to read as UTF-8 text with line-based offset and length
258
262
  try {
263
+ // Read the entire file first
259
264
  const buffer = await fs.readFile(validPath);
260
- const content = buffer.toString('utf-8');
265
+ const fullContent = buffer.toString('utf-8');
266
+ // Split into lines for line-based access
267
+ const lines = fullContent.split('\n');
268
+ const totalLines = lines.length;
269
+ // Apply line-based offset and length - handle beyond-file-size scenario
270
+ let startLine = Math.min(offset, totalLines);
271
+ let endLine = Math.min(startLine + length, totalLines);
272
+ // If startLine equals totalLines (reading beyond end), adjust to show some content
273
+ // Only do this if we're not trying to read the whole file
274
+ if (startLine === totalLines && offset > 0 && length < Number.MAX_SAFE_INTEGER) {
275
+ // Show last few lines instead of nothing
276
+ const lastLinesCount = Math.min(10, totalLines); // Show last 10 lines or fewer if file is smaller
277
+ startLine = Math.max(0, totalLines - lastLinesCount);
278
+ endLine = totalLines;
279
+ }
280
+ const selectedLines = lines.slice(startLine, endLine);
281
+ const truncatedContent = selectedLines.join('\n');
282
+ // Add an informational message if truncated or adjusted
283
+ let content = truncatedContent;
284
+ // Only add informational message for normal reads (not when reading entire file)
285
+ const isEntireFileRead = offset === 0 && length >= Number.MAX_SAFE_INTEGER;
286
+ if (!isEntireFileRead) {
287
+ if (offset >= totalLines && totalLines > 0) {
288
+ // Reading beyond end of file case
289
+ content = `[NOTICE: Offset ${offset} exceeds file length (${totalLines} lines). Showing last ${endLine - startLine} lines instead.]\n\n${truncatedContent}`;
290
+ }
291
+ else if (offset > 0 || endLine < totalLines) {
292
+ // Normal partial read case
293
+ content = `[Reading ${endLine - startLine} lines from line ${startLine} of ${totalLines} total lines]\n\n${truncatedContent}`;
294
+ }
295
+ }
261
296
  return { content, mimeType, isImage };
262
297
  }
263
298
  catch (error) {
@@ -279,22 +314,37 @@ export async function readFileFromDisk(filePath) {
279
314
  /**
280
315
  * Read a file from either the local filesystem or a URL
281
316
  * @param filePath Path to the file or URL
282
- * @param returnMetadata Whether to return metadata with the content
283
317
  * @param isUrl Whether the path is a URL
318
+ * @param offset Starting line number to read from (default: 0)
319
+ * @param length Maximum number of lines to read (default: from config or 1000)
284
320
  * @returns File content or file result with metadata
285
321
  */
286
- export async function readFile(filePath, isUrl) {
322
+ export async function readFile(filePath, isUrl, offset, length) {
287
323
  return isUrl
288
324
  ? readFileFromUrl(filePath)
289
- : readFileFromDisk(filePath);
325
+ : readFileFromDisk(filePath, offset, length);
290
326
  }
291
- export async function writeFile(filePath, content) {
327
+ export async function writeFile(filePath, content, mode = 'rewrite') {
292
328
  const validPath = await validatePath(filePath);
293
329
  // Get file extension for telemetry
294
330
  const fileExtension = path.extname(validPath).toLowerCase();
295
- // Capture file extension in telemetry without capturing the file path
296
- capture('server_write_file', { fileExtension: fileExtension });
297
- await fs.writeFile(validPath, content);
331
+ // Calculate content metrics
332
+ const contentBytes = Buffer.from(content).length;
333
+ const lineCount = content.split('\n').length;
334
+ // Capture file extension and operation details in telemetry without capturing the file path
335
+ capture('server_write_file', {
336
+ fileExtension: fileExtension,
337
+ mode: mode,
338
+ contentBytes: contentBytes,
339
+ lineCount: lineCount
340
+ });
341
+ // Use different fs methods based on mode
342
+ if (mode === 'append') {
343
+ await fs.appendFile(validPath, content);
344
+ }
345
+ else {
346
+ await fs.writeFile(validPath, content);
347
+ }
298
348
  }
299
349
  export async function readMultipleFiles(paths) {
300
350
  return Promise.all(paths.map(async (filePath) => {
@@ -388,7 +438,8 @@ export async function searchFiles(rootPath, pattern) {
388
438
  export async function getFileInfo(filePath) {
389
439
  const validPath = await validatePath(filePath);
390
440
  const stats = await fs.stat(validPath);
391
- return {
441
+ // Basic file info
442
+ const info = {
392
443
  size: stats.size,
393
444
  created: stats.birthtime,
394
445
  modified: stats.mtime,
@@ -397,6 +448,27 @@ export async function getFileInfo(filePath) {
397
448
  isFile: stats.isFile(),
398
449
  permissions: stats.mode.toString(8).slice(-3),
399
450
  };
451
+ // For text files that aren't too large, also count lines
452
+ if (stats.isFile() && stats.size < 10 * 1024 * 1024) { // Limit to 10MB files
453
+ try {
454
+ // Import the MIME type utilities
455
+ const { getMimeType, isImageFile } = await import('./mime-types.js');
456
+ const mimeType = getMimeType(validPath);
457
+ // Only count lines for non-image, likely text files
458
+ if (!isImageFile(mimeType)) {
459
+ const content = await fs.readFile(validPath, 'utf8');
460
+ const lineCount = content.split('\n').length;
461
+ info.lineCount = lineCount;
462
+ info.lastLine = lineCount - 1; // Zero-indexed last line
463
+ info.appendPosition = lineCount; // Position to append at end
464
+ }
465
+ }
466
+ catch (error) {
467
+ // If reading fails, just skip the line count
468
+ // This could happen for binary files or very large files
469
+ }
470
+ }
471
+ return info;
400
472
  }
401
473
  // This function has been replaced with configManager.getConfig()
402
474
  // Use get_config tool to retrieve allowedDirectories
@@ -49,12 +49,18 @@ export declare const KillProcessArgsSchema: z.ZodObject<{
49
49
  export declare const ReadFileArgsSchema: z.ZodObject<{
50
50
  path: z.ZodString;
51
51
  isUrl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
52
+ offset: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
53
+ length: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
52
54
  }, "strip", z.ZodTypeAny, {
53
55
  path: string;
56
+ length: number;
54
57
  isUrl: boolean;
58
+ offset: number;
55
59
  }, {
56
60
  path: string;
61
+ length?: number | undefined;
57
62
  isUrl?: boolean | undefined;
63
+ offset?: number | undefined;
58
64
  }>;
59
65
  export declare const ReadMultipleFilesArgsSchema: z.ZodObject<{
60
66
  paths: z.ZodArray<z.ZodString, "many">;
@@ -66,12 +72,15 @@ export declare const ReadMultipleFilesArgsSchema: z.ZodObject<{
66
72
  export declare const WriteFileArgsSchema: z.ZodObject<{
67
73
  path: z.ZodString;
68
74
  content: z.ZodString;
75
+ mode: z.ZodDefault<z.ZodEnum<["rewrite", "append"]>>;
69
76
  }, "strip", z.ZodTypeAny, {
70
77
  path: string;
71
78
  content: string;
79
+ mode: "rewrite" | "append";
72
80
  }, {
73
81
  path: string;
74
82
  content: string;
83
+ mode?: "rewrite" | "append" | undefined;
75
84
  }>;
76
85
  export declare const CreateDirectoryArgsSchema: z.ZodObject<{
77
86
  path: z.ZodString;
@@ -28,6 +28,8 @@ export const KillProcessArgsSchema = z.object({
28
28
  export const ReadFileArgsSchema = z.object({
29
29
  path: z.string(),
30
30
  isUrl: z.boolean().optional().default(false),
31
+ offset: z.number().optional().default(0),
32
+ length: z.number().optional().default(1000),
31
33
  });
32
34
  export const ReadMultipleFilesArgsSchema = z.object({
33
35
  paths: z.array(z.string()),
@@ -35,6 +37,7 @@ export const ReadMultipleFilesArgsSchema = z.object({
35
37
  export const WriteFileArgsSchema = z.object({
36
38
  path: z.string(),
37
39
  content: z.string(),
40
+ mode: z.enum(['rewrite', 'append']).default('rewrite'),
38
41
  });
39
42
  export const CreateDirectoryArgsSchema = z.object({
40
43
  path: z.string(),
@@ -0,0 +1,30 @@
1
+ export interface FuzzySearchLogEntry {
2
+ timestamp: Date;
3
+ searchText: string;
4
+ foundText: string;
5
+ similarity: number;
6
+ executionTime: number;
7
+ exactMatchCount: number;
8
+ expectedReplacements: number;
9
+ fuzzyThreshold: number;
10
+ belowThreshold: boolean;
11
+ diff: string;
12
+ searchLength: number;
13
+ foundLength: number;
14
+ fileExtension: string;
15
+ characterCodes: string;
16
+ uniqueCharacterCount: number;
17
+ diffLength: number;
18
+ }
19
+ declare class FuzzySearchLogger {
20
+ private logPath;
21
+ private initialized;
22
+ constructor();
23
+ private ensureLogFile;
24
+ log(entry: FuzzySearchLogEntry): Promise<void>;
25
+ getLogPath(): Promise<string>;
26
+ getRecentLogs(count?: number): Promise<string[]>;
27
+ clearLog(): Promise<void>;
28
+ }
29
+ export declare const fuzzySearchLogger: FuzzySearchLogger;
30
+ export {};
@@ -0,0 +1,126 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ class FuzzySearchLogger {
5
+ constructor() {
6
+ this.initialized = false;
7
+ // Create log file in a dedicated directory
8
+ const logDir = path.join(os.homedir(), '.claude-server-commander-logs');
9
+ this.logPath = path.join(logDir, 'fuzzy-search.log');
10
+ }
11
+ async ensureLogFile() {
12
+ if (this.initialized)
13
+ return;
14
+ try {
15
+ // Create log directory if it doesn't exist
16
+ const logDir = path.dirname(this.logPath);
17
+ await fs.mkdir(logDir, { recursive: true });
18
+ // Check if log file exists, create with headers if not
19
+ try {
20
+ await fs.access(this.logPath);
21
+ }
22
+ catch {
23
+ // File doesn't exist, create with headers
24
+ const headers = [
25
+ 'timestamp',
26
+ 'searchText',
27
+ 'foundText',
28
+ 'similarity',
29
+ 'executionTime',
30
+ 'exactMatchCount',
31
+ 'expectedReplacements',
32
+ 'fuzzyThreshold',
33
+ 'belowThreshold',
34
+ 'diff',
35
+ 'searchLength',
36
+ 'foundLength',
37
+ 'fileExtension',
38
+ 'characterCodes',
39
+ 'uniqueCharacterCount',
40
+ 'diffLength'
41
+ ].join('\t');
42
+ await fs.writeFile(this.logPath, headers + '\n');
43
+ }
44
+ this.initialized = true;
45
+ }
46
+ catch (error) {
47
+ console.error('Failed to initialize fuzzy search log file:', error);
48
+ throw error;
49
+ }
50
+ }
51
+ async log(entry) {
52
+ try {
53
+ await this.ensureLogFile();
54
+ // Convert entry to tab-separated string
55
+ const logLine = [
56
+ entry.timestamp.toISOString(),
57
+ entry.searchText.replace(/\n/g, '\\n').replace(/\t/g, '\\t'),
58
+ entry.foundText.replace(/\n/g, '\\n').replace(/\t/g, '\\t'),
59
+ entry.similarity.toString(),
60
+ entry.executionTime.toString(),
61
+ entry.exactMatchCount.toString(),
62
+ entry.expectedReplacements.toString(),
63
+ entry.fuzzyThreshold.toString(),
64
+ entry.belowThreshold.toString(),
65
+ entry.diff.replace(/\n/g, '\\n').replace(/\t/g, '\\t'),
66
+ entry.searchLength.toString(),
67
+ entry.foundLength.toString(),
68
+ entry.fileExtension,
69
+ entry.characterCodes,
70
+ entry.uniqueCharacterCount.toString(),
71
+ entry.diffLength.toString()
72
+ ].join('\t');
73
+ await fs.appendFile(this.logPath, logLine + '\n');
74
+ }
75
+ catch (error) {
76
+ console.error('Failed to write to fuzzy search log:', error);
77
+ }
78
+ }
79
+ async getLogPath() {
80
+ await this.ensureLogFile();
81
+ return this.logPath;
82
+ }
83
+ async getRecentLogs(count = 10) {
84
+ try {
85
+ await this.ensureLogFile();
86
+ const content = await fs.readFile(this.logPath, 'utf-8');
87
+ const lines = content.split('\n').filter(line => line.trim());
88
+ // Return last N lines (excluding header)
89
+ return lines.slice(-count - 1, -1);
90
+ }
91
+ catch (error) {
92
+ console.error('Failed to read fuzzy search logs:', error);
93
+ return [];
94
+ }
95
+ }
96
+ async clearLog() {
97
+ try {
98
+ // Recreate with just headers
99
+ const headers = [
100
+ 'timestamp',
101
+ 'searchText',
102
+ 'foundText',
103
+ 'similarity',
104
+ 'executionTime',
105
+ 'exactMatchCount',
106
+ 'expectedReplacements',
107
+ 'fuzzyThreshold',
108
+ 'belowThreshold',
109
+ 'diff',
110
+ 'searchLength',
111
+ 'foundLength',
112
+ 'fileExtension',
113
+ 'characterCodes',
114
+ 'uniqueCharacterCount',
115
+ 'diffLength'
116
+ ].join('\t');
117
+ await fs.writeFile(this.logPath, headers + '\n');
118
+ console.log('Fuzzy search log cleared');
119
+ }
120
+ catch (error) {
121
+ console.error('Failed to clear fuzzy search log:', error);
122
+ }
123
+ }
124
+ }
125
+ // Singleton instance
126
+ export const fuzzySearchLogger = new FuzzySearchLogger();
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Track tool calls and save them to a log file
3
+ * @param toolName Name of the tool being called
4
+ * @param args Arguments passed to the tool (optional)
5
+ */
6
+ export declare function trackToolCall(toolName: string, args?: unknown): Promise<void>;
@@ -0,0 +1,54 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { TOOL_CALL_FILE, TOOL_CALL_FILE_MAX_SIZE } from '../config.js';
4
+ // Ensure the directory for the log file exists
5
+ const logDir = path.dirname(TOOL_CALL_FILE);
6
+ await fs.promises.mkdir(logDir, { recursive: true });
7
+ /**
8
+ * Track tool calls and save them to a log file
9
+ * @param toolName Name of the tool being called
10
+ * @param args Arguments passed to the tool (optional)
11
+ */
12
+ export async function trackToolCall(toolName, args) {
13
+ try {
14
+ // Get current timestamp
15
+ const timestamp = new Date().toISOString();
16
+ // Format the log entry
17
+ const logEntry = `${timestamp} | ${toolName.padEnd(20, ' ')}${args ? `\t| Arguments: ${JSON.stringify(args)}` : ''}\n`;
18
+ // Check if file exists and get its size
19
+ let fileSize = 0;
20
+ try {
21
+ const stats = await fs.promises.stat(TOOL_CALL_FILE);
22
+ fileSize = stats.size;
23
+ }
24
+ catch (err) {
25
+ // File doesn't exist yet, size remains 0
26
+ }
27
+ // If file size is 10MB or larger, rotate the log file
28
+ if (fileSize >= TOOL_CALL_FILE_MAX_SIZE) {
29
+ const fileExt = path.extname(TOOL_CALL_FILE);
30
+ const fileBase = path.basename(TOOL_CALL_FILE, fileExt);
31
+ const dirName = path.dirname(TOOL_CALL_FILE);
32
+ // Create a timestamp-based filename for the old log
33
+ const date = new Date();
34
+ const rotateTimestamp = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}_${String(date.getHours()).padStart(2, '0')}-${String(date.getMinutes()).padStart(2, '0')}-${String(date.getSeconds()).padStart(2, '0')}`;
35
+ const newFileName = path.join(dirName, `${fileBase}_${rotateTimestamp}${fileExt}`);
36
+ // Rename the current file
37
+ await fs.promises.rename(TOOL_CALL_FILE, newFileName);
38
+ }
39
+ // Append to log file (if file was renamed, this will create a new file)
40
+ await fs.promises.appendFile(TOOL_CALL_FILE, logEntry, 'utf8');
41
+ }
42
+ catch (error) {
43
+ const errorMessage = error instanceof Error ? error.message : String(error);
44
+ const { capture } = await import('./capture.js');
45
+ // Send a final telemetry event noting that the user has opted out
46
+ // This helps us track opt-out rates while respecting the user's choice
47
+ await capture('server_track_tool_call_error', {
48
+ error: errorMessage,
49
+ toolName
50
+ });
51
+ // Don't let logging errors affect the main functionality
52
+ console.error(`Error logging tool call: ${error instanceof Error ? error.message : String(error)}`);
53
+ }
54
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.39";
1
+ export declare const VERSION = "0.2.1";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.1.39';
1
+ export const VERSION = '0.2.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.1.39",
3
+ "version": "0.2.1",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "license": "MIT",
6
6
  "author": "Eduards Ruzga",
@@ -34,7 +34,11 @@
34
34
  "test": "node test/run-all-tests.js",
35
35
  "link:local": "npm run build && npm link",
36
36
  "unlink:local": "npm unlink",
37
- "inspector": "npx @modelcontextprotocol/inspector dist/index.js"
37
+ "inspector": "npx @modelcontextprotocol/inspector dist/index.js",
38
+ "logs:view": "npm run build && node scripts/view-fuzzy-logs.js",
39
+ "logs:analyze": "npm run build && node scripts/analyze-fuzzy-logs.js",
40
+ "logs:clear": "npm run build && node scripts/clear-fuzzy-logs.js",
41
+ "logs:export": "npm run build && node scripts/export-fuzzy-logs.js"
38
42
  },
39
43
  "publishConfig": {
40
44
  "access": "public"
@@ -69,6 +73,7 @@
69
73
  },
70
74
  "devDependencies": {
71
75
  "@types/node": "^20.17.24",
76
+ "commander": "^13.1.0",
72
77
  "nexe": "^5.0.0-beta.4",
73
78
  "nodemon": "^3.0.2",
74
79
  "shx": "^0.3.4",