@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.
- package/README.md +88 -5
- package/dist/config-manager.d.ts +2 -0
- package/dist/config-manager.js +5 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/handlers/filesystem-handlers.js +37 -3
- package/dist/handlers/fuzzy-search-log-handlers.d.ts +13 -0
- package/dist/handlers/fuzzy-search-log-handlers.js +179 -0
- package/dist/server.js +219 -42
- package/dist/setup-claude-server.js +34 -26
- package/dist/tools/edit.js +55 -7
- package/dist/tools/filesystem.d.ts +7 -5
- package/dist/tools/filesystem.js +97 -25
- package/dist/tools/schemas.d.ts +9 -0
- package/dist/tools/schemas.js +3 -0
- package/dist/utils/fuzzySearchLogger.d.ts +30 -0
- package/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/utils/trackTools.d.ts +6 -0
- package/dist/utils/trackTools.js +54 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -2
package/dist/tools/filesystem.js
CHANGED
|
@@ -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
|
|
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', {
|
|
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
|
|
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
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/schemas.js
CHANGED
|
@@ -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,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
|
|
1
|
+
export declare const VERSION = "0.2.1";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.1
|
|
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
|
|
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",
|