@wonderwhy-er/desktop-commander 0.1.38 → 0.2.0

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.
Files changed (50) hide show
  1. package/README.md +89 -6
  2. package/dist/REPLSessionManager.d.ts +109 -0
  3. package/dist/REPLSessionManager.js +364 -0
  4. package/dist/REPLSessionManager.test.d.ts +1 -0
  5. package/dist/REPLSessionManager.test.js +75 -0
  6. package/dist/client/replClient.d.ts +63 -0
  7. package/dist/client/replClient.js +217 -0
  8. package/dist/client/sshClient.d.ts +82 -0
  9. package/dist/client/sshClient.js +200 -0
  10. package/dist/config-manager.d.ts +2 -0
  11. package/dist/config-manager.js +3 -1
  12. package/dist/config.d.ts +1 -0
  13. package/dist/config.js +1 -0
  14. package/dist/handlers/filesystem-handlers.js +37 -3
  15. package/dist/handlers/fuzzy-search-log-handlers.d.ts +13 -0
  16. package/dist/handlers/fuzzy-search-log-handlers.js +179 -0
  17. package/dist/handlers/repl-handlers.d.ts +21 -0
  18. package/dist/handlers/repl-handlers.js +37 -0
  19. package/dist/handlers/replCommandHandler.d.ts +125 -0
  20. package/dist/handlers/replCommandHandler.js +255 -0
  21. package/dist/handlers/replCommandHandler.test.d.ts +1 -0
  22. package/dist/handlers/replCommandHandler.test.js +103 -0
  23. package/dist/repl-manager.d.ts +73 -0
  24. package/dist/repl-manager.js +407 -0
  25. package/dist/replIntegration.d.ts +14 -0
  26. package/dist/replIntegration.js +27 -0
  27. package/dist/server.js +37 -19
  28. package/dist/setup-claude-server.js +0 -20
  29. package/dist/tools/edit.js +129 -29
  30. package/dist/tools/enhanced-read-output.js +69 -0
  31. package/dist/tools/enhanced-send-input.js +111 -0
  32. package/dist/tools/filesystem.d.ts +7 -5
  33. package/dist/tools/filesystem.js +56 -27
  34. package/dist/tools/repl.d.ts +21 -0
  35. package/dist/tools/repl.js +217 -0
  36. package/dist/tools/schemas.d.ts +9 -0
  37. package/dist/tools/schemas.js +3 -0
  38. package/dist/tools/send-input.d.ts +2 -0
  39. package/dist/tools/send-input.js +45 -0
  40. package/dist/utils/fuzzySearchLogger.d.ts +30 -0
  41. package/dist/utils/fuzzySearchLogger.js +126 -0
  42. package/dist/utils/lineEndingHandler.d.ts +21 -0
  43. package/dist/utils/lineEndingHandler.js +77 -0
  44. package/dist/utils/lineEndingHandler_optimized.d.ts +21 -0
  45. package/dist/utils/lineEndingHandler_optimized.js +77 -0
  46. package/dist/utils/trackTools.d.ts +6 -0
  47. package/dist/utils/trackTools.js +54 -0
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/package.json +7 -2
@@ -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,14 +252,30 @@ 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 {
259
- const content = await fs.readFile(validPath, "utf-8");
263
+ // Read the entire file first
264
+ const buffer = await fs.readFile(validPath);
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
270
+ const startLine = Math.min(offset, totalLines);
271
+ const endLine = Math.min(startLine + length, totalLines);
272
+ const selectedLines = lines.slice(startLine, endLine);
273
+ const truncatedContent = selectedLines.join('\n');
274
+ // Add an informational message if truncated
275
+ let content = truncatedContent;
276
+ if (offset > 0 || endLine < totalLines) {
277
+ content = `[Reading ${endLine - startLine} lines from line ${offset} of ${totalLines} total lines]\n\n${truncatedContent}`;
278
+ }
260
279
  return { content, mimeType, isImage };
261
280
  }
262
281
  catch (error) {
@@ -278,22 +297,32 @@ export async function readFileFromDisk(filePath) {
278
297
  /**
279
298
  * Read a file from either the local filesystem or a URL
280
299
  * @param filePath Path to the file or URL
281
- * @param returnMetadata Whether to return metadata with the content
282
300
  * @param isUrl Whether the path is a URL
301
+ * @param offset Starting line number to read from (default: 0)
302
+ * @param length Maximum number of lines to read (default: from config or 1000)
283
303
  * @returns File content or file result with metadata
284
304
  */
285
- export async function readFile(filePath, isUrl) {
305
+ export async function readFile(filePath, isUrl, offset, length) {
286
306
  return isUrl
287
307
  ? readFileFromUrl(filePath)
288
- : readFileFromDisk(filePath);
308
+ : readFileFromDisk(filePath, offset, length);
289
309
  }
290
- export async function writeFile(filePath, content) {
310
+ export async function writeFile(filePath, content, mode = 'rewrite') {
291
311
  const validPath = await validatePath(filePath);
292
312
  // Get file extension for telemetry
293
313
  const fileExtension = path.extname(validPath).toLowerCase();
294
- // Capture file extension in telemetry without capturing the file path
295
- capture('server_write_file', { fileExtension: fileExtension });
296
- await fs.writeFile(validPath, content, "utf-8");
314
+ // Capture file extension and operation details in telemetry without capturing the file path
315
+ capture('server_write_file', {
316
+ fileExtension: fileExtension,
317
+ mode: mode,
318
+ });
319
+ // Use different fs methods based on mode
320
+ if (mode === 'append') {
321
+ await fs.appendFile(validPath, content);
322
+ }
323
+ else {
324
+ await fs.writeFile(validPath, content);
325
+ }
297
326
  }
298
327
  export async function readMultipleFiles(paths) {
299
328
  return Promise.all(paths.map(async (filePath) => {
@@ -339,9 +368,9 @@ export async function searchFiles(rootPath, pattern) {
339
368
  }
340
369
  catch (error) {
341
370
  // Only sanitize for telemetry, not for the returned error
342
- capture('server_search_files_error', {
371
+ capture('server_search_read_dir_error', {
343
372
  errorType: error instanceof Error ? error.name : 'Unknown',
344
- errorMessage: 'Error reading directory',
373
+ error: 'Error reading directory',
345
374
  isReadDirError: true
346
375
  });
347
376
  return; // Skip this directory on error
@@ -377,7 +406,7 @@ export async function searchFiles(rootPath, pattern) {
377
406
  // For telemetry only - sanitize error info
378
407
  capture('server_search_files_error', {
379
408
  errorType: error instanceof Error ? error.name : 'Unknown',
380
- errorMessage: 'Error with root path',
409
+ error: 'Error with root path',
381
410
  isRootPathError: true
382
411
  });
383
412
  // Re-throw the original error for the caller
@@ -0,0 +1,21 @@
1
+ import { ServerResult } from '../types.js';
2
+ /**
3
+ * Create a new REPL session
4
+ */
5
+ export declare function createREPLSession(args: unknown): Promise<ServerResult>;
6
+ /**
7
+ * Execute code in a REPL session
8
+ */
9
+ export declare function executeREPLCode(args: unknown): Promise<ServerResult>;
10
+ /**
11
+ * Terminate a REPL session
12
+ */
13
+ export declare function terminateREPLSession(args: unknown): Promise<ServerResult>;
14
+ /**
15
+ * List all active REPL sessions
16
+ */
17
+ export declare function listREPLSessions(args: unknown): Promise<ServerResult>;
18
+ /**
19
+ * Get information about a specific REPL session
20
+ */
21
+ export declare function getREPLSessionInfo(args: unknown): Promise<ServerResult>;
@@ -0,0 +1,217 @@
1
+ import { replManager } from '../repl-manager.js';
2
+ import { capture } from '../utils/capture.js';
3
+ import { CreateREPLSessionArgsSchema, ExecuteREPLCodeArgsSchema, TerminateREPLSessionArgsSchema, ListREPLSessionsArgsSchema, GetREPLSessionInfoArgsSchema } from './schemas.js';
4
+ /**
5
+ * Create a new REPL session
6
+ */
7
+ export async function createREPLSession(args) {
8
+ const parsed = CreateREPLSessionArgsSchema.safeParse(args);
9
+ if (!parsed.success) {
10
+ capture('server_create_repl_session_failed', {
11
+ error: 'Invalid arguments'
12
+ });
13
+ return {
14
+ content: [{ type: "text", text: `Error: Invalid arguments for create_repl_session: ${parsed.error}` }],
15
+ isError: true,
16
+ };
17
+ }
18
+ try {
19
+ const pid = await replManager.createSession(parsed.data.language, parsed.data.timeout);
20
+ return {
21
+ content: [{
22
+ type: "text",
23
+ text: `Successfully created ${parsed.data.language} REPL session with PID ${pid}`
24
+ }],
25
+ };
26
+ }
27
+ catch (error) {
28
+ const errorMessage = error instanceof Error ? error.message : String(error);
29
+ capture('server_create_repl_session_error', {
30
+ error: errorMessage
31
+ });
32
+ return {
33
+ content: [{ type: "text", text: `Error creating REPL session: ${errorMessage}` }],
34
+ isError: true,
35
+ };
36
+ }
37
+ }
38
+ /**
39
+ * Execute code in a REPL session
40
+ */
41
+ export async function executeREPLCode(args) {
42
+ const parsed = ExecuteREPLCodeArgsSchema.safeParse(args);
43
+ if (!parsed.success) {
44
+ capture('server_execute_repl_code_failed', {
45
+ error: 'Invalid arguments'
46
+ });
47
+ return {
48
+ content: [{ type: "text", text: `Error: Invalid arguments for execute_repl_code: ${parsed.error}` }],
49
+ isError: true,
50
+ };
51
+ }
52
+ try {
53
+ const result = await replManager.executeCode(parsed.data.pid, parsed.data.code, {
54
+ timeout: parsed.data.timeout,
55
+ waitForPrompt: parsed.data.waitForPrompt,
56
+ ignoreErrors: parsed.data.ignoreErrors,
57
+ multiline: parsed.data.multiline
58
+ });
59
+ if (!result.success) {
60
+ return {
61
+ content: [{
62
+ type: "text",
63
+ text: result.error
64
+ ? `Error executing code: ${result.error}`
65
+ : `Failed to execute code in REPL session ${parsed.data.pid}` +
66
+ (result.timeout ? ` (timed out)` : '') +
67
+ (result.output ? `\nOutput received: ${result.output}` : '')
68
+ }],
69
+ isError: true,
70
+ };
71
+ }
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: `Code executed in REPL session ${parsed.data.pid}` +
76
+ (result.timeout ? ` (timed out, partial output below)` : '') +
77
+ `\n\nOutput:\n${result.output || '(No output)'}`
78
+ }],
79
+ };
80
+ }
81
+ catch (error) {
82
+ const errorMessage = error instanceof Error ? error.message : String(error);
83
+ capture('server_execute_repl_code_error', {
84
+ error: errorMessage
85
+ });
86
+ return {
87
+ content: [{ type: "text", text: `Error executing REPL code: ${errorMessage}` }],
88
+ isError: true,
89
+ };
90
+ }
91
+ }
92
+ /**
93
+ * Terminate a REPL session
94
+ */
95
+ export async function terminateREPLSession(args) {
96
+ const parsed = TerminateREPLSessionArgsSchema.safeParse(args);
97
+ if (!parsed.success) {
98
+ capture('server_terminate_repl_session_failed', {
99
+ error: 'Invalid arguments'
100
+ });
101
+ return {
102
+ content: [{ type: "text", text: `Error: Invalid arguments for terminate_repl_session: ${parsed.error}` }],
103
+ isError: true,
104
+ };
105
+ }
106
+ try {
107
+ const success = await replManager.terminateSession(parsed.data.pid);
108
+ if (!success) {
109
+ return {
110
+ content: [{ type: "text", text: `Failed to terminate REPL session ${parsed.data.pid}. The session may have already ended.` }],
111
+ isError: true,
112
+ };
113
+ }
114
+ return {
115
+ content: [{
116
+ type: "text",
117
+ text: `Successfully terminated REPL session ${parsed.data.pid}`
118
+ }],
119
+ };
120
+ }
121
+ catch (error) {
122
+ const errorMessage = error instanceof Error ? error.message : String(error);
123
+ capture('server_terminate_repl_session_error', {
124
+ error: errorMessage
125
+ });
126
+ return {
127
+ content: [{ type: "text", text: `Error terminating REPL session: ${errorMessage}` }],
128
+ isError: true,
129
+ };
130
+ }
131
+ }
132
+ /**
133
+ * List all active REPL sessions
134
+ */
135
+ export async function listREPLSessions(args) {
136
+ const parsed = ListREPLSessionsArgsSchema.safeParse(args);
137
+ if (!parsed.success) {
138
+ capture('server_list_repl_sessions_failed', {
139
+ error: 'Invalid arguments'
140
+ });
141
+ return {
142
+ content: [{ type: "text", text: `Error: Invalid arguments for list_repl_sessions: ${parsed.error}` }],
143
+ isError: true,
144
+ };
145
+ }
146
+ try {
147
+ const sessions = replManager.listSessions();
148
+ if (sessions.length === 0) {
149
+ return {
150
+ content: [{
151
+ type: "text",
152
+ text: `No active REPL sessions found`
153
+ }],
154
+ };
155
+ }
156
+ const sessionsText = sessions.map(s => `PID: ${s.pid}, Language: ${s.language}, Runtime: ${Math.round(s.runtime / 1000)}s`).join('\n');
157
+ return {
158
+ content: [{
159
+ type: "text",
160
+ text: `Active REPL sessions:\n${sessionsText}`
161
+ }],
162
+ };
163
+ }
164
+ catch (error) {
165
+ const errorMessage = error instanceof Error ? error.message : String(error);
166
+ capture('server_list_repl_sessions_error', {
167
+ error: errorMessage
168
+ });
169
+ return {
170
+ content: [{ type: "text", text: `Error listing REPL sessions: ${errorMessage}` }],
171
+ isError: true,
172
+ };
173
+ }
174
+ }
175
+ /**
176
+ * Get information about a specific REPL session
177
+ */
178
+ export async function getREPLSessionInfo(args) {
179
+ const parsed = GetREPLSessionInfoArgsSchema.safeParse(args);
180
+ if (!parsed.success) {
181
+ capture('server_get_repl_session_info_failed', {
182
+ error: 'Invalid arguments'
183
+ });
184
+ return {
185
+ content: [{ type: "text", text: `Error: Invalid arguments for get_repl_session_info: ${parsed.error}` }],
186
+ isError: true,
187
+ };
188
+ }
189
+ try {
190
+ const info = replManager.getSessionInfo(parsed.data.pid);
191
+ if (!info) {
192
+ return {
193
+ content: [{
194
+ type: "text",
195
+ text: `No active REPL session found with PID ${parsed.data.pid}`
196
+ }],
197
+ isError: true,
198
+ };
199
+ }
200
+ return {
201
+ content: [{
202
+ type: "text",
203
+ text: `REPL Session ${parsed.data.pid}:\nLanguage: ${info.language}\nRuntime: ${Math.round(info.runtime / 1000)}s`
204
+ }],
205
+ };
206
+ }
207
+ catch (error) {
208
+ const errorMessage = error instanceof Error ? error.message : String(error);
209
+ capture('server_get_repl_session_info_error', {
210
+ error: errorMessage
211
+ });
212
+ return {
213
+ content: [{ type: "text", text: `Error getting REPL session info: ${errorMessage}` }],
214
+ isError: true,
215
+ };
216
+ }
217
+ }
@@ -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,2 @@
1
+ import { ServerResult } from '../types.js';
2
+ export declare function sendInput(args: unknown): Promise<ServerResult>;
@@ -0,0 +1,45 @@
1
+ import { terminalManager } from '../terminal-manager.js';
2
+ import { SendInputArgsSchema } from './schemas.js';
3
+ import { capture } from "../utils/capture.js";
4
+ export async function sendInput(args) {
5
+ const parsed = SendInputArgsSchema.safeParse(args);
6
+ if (!parsed.success) {
7
+ capture('server_send_input_failed', {
8
+ error: 'Invalid arguments'
9
+ });
10
+ return {
11
+ content: [{ type: "text", text: `Error: Invalid arguments for send_input: ${parsed.error}` }],
12
+ isError: true,
13
+ };
14
+ }
15
+ try {
16
+ capture('server_send_input', {
17
+ pid: parsed.data.pid,
18
+ inputLength: parsed.data.input.length
19
+ });
20
+ // Try to send input to the process
21
+ const success = terminalManager.sendInputToProcess(parsed.data.pid, parsed.data.input);
22
+ if (!success) {
23
+ return {
24
+ content: [{ type: "text", text: `Error: Failed to send input to process ${parsed.data.pid}. The process may have exited or doesn't accept input.` }],
25
+ isError: true,
26
+ };
27
+ }
28
+ return {
29
+ content: [{
30
+ type: "text",
31
+ text: `Successfully sent input to process ${parsed.data.pid}. Use read_output to get the process response.`
32
+ }],
33
+ };
34
+ }
35
+ catch (error) {
36
+ const errorMessage = error instanceof Error ? error.message : String(error);
37
+ capture('server_send_input_error', {
38
+ error: errorMessage
39
+ });
40
+ return {
41
+ content: [{ type: "text", text: `Error sending input: ${errorMessage}` }],
42
+ isError: true,
43
+ };
44
+ }
45
+ }
@@ -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,21 @@
1
+ /**
2
+ * Line ending types
3
+ */
4
+ export type LineEndingStyle = '\r\n' | '\n' | '\r';
5
+ /**
6
+ * Detect the line ending style used in a file - Optimized version
7
+ * This algorithm uses early termination for maximum performance
8
+ */
9
+ export declare function detectLineEnding(content: string): LineEndingStyle;
10
+ /**
11
+ * Normalize line endings to match the target style
12
+ */
13
+ export declare function normalizeLineEndings(text: string, targetLineEnding: LineEndingStyle): string;
14
+ /**
15
+ * Analyze line ending usage in content
16
+ */
17
+ export declare function analyzeLineEndings(content: string): {
18
+ style: LineEndingStyle;
19
+ count: number;
20
+ hasMixed: boolean;
21
+ };