wave-code 0.0.2

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 (131) hide show
  1. package/README.md +120 -0
  2. package/bin/wave-code.js +16 -0
  3. package/dist/cli.d.ts +6 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +62 -0
  6. package/dist/components/App.d.ts +8 -0
  7. package/dist/components/App.d.ts.map +1 -0
  8. package/dist/components/App.js +10 -0
  9. package/dist/components/BashHistorySelector.d.ts +10 -0
  10. package/dist/components/BashHistorySelector.d.ts.map +1 -0
  11. package/dist/components/BashHistorySelector.js +83 -0
  12. package/dist/components/BashShellManager.d.ts +6 -0
  13. package/dist/components/BashShellManager.d.ts.map +1 -0
  14. package/dist/components/BashShellManager.js +116 -0
  15. package/dist/components/ChatInterface.d.ts +3 -0
  16. package/dist/components/ChatInterface.d.ts.map +1 -0
  17. package/dist/components/ChatInterface.js +31 -0
  18. package/dist/components/CommandOutputDisplay.d.ts +9 -0
  19. package/dist/components/CommandOutputDisplay.d.ts.map +1 -0
  20. package/dist/components/CommandOutputDisplay.js +40 -0
  21. package/dist/components/CommandSelector.d.ts +11 -0
  22. package/dist/components/CommandSelector.d.ts.map +1 -0
  23. package/dist/components/CommandSelector.js +60 -0
  24. package/dist/components/CompressDisplay.d.ts +9 -0
  25. package/dist/components/CompressDisplay.d.ts.map +1 -0
  26. package/dist/components/CompressDisplay.js +17 -0
  27. package/dist/components/DiffViewer.d.ts +9 -0
  28. package/dist/components/DiffViewer.d.ts.map +1 -0
  29. package/dist/components/DiffViewer.js +221 -0
  30. package/dist/components/FileSelector.d.ts +13 -0
  31. package/dist/components/FileSelector.d.ts.map +1 -0
  32. package/dist/components/FileSelector.js +48 -0
  33. package/dist/components/InputBox.d.ts +23 -0
  34. package/dist/components/InputBox.d.ts.map +1 -0
  35. package/dist/components/InputBox.js +124 -0
  36. package/dist/components/McpManager.d.ts +10 -0
  37. package/dist/components/McpManager.d.ts.map +1 -0
  38. package/dist/components/McpManager.js +123 -0
  39. package/dist/components/MemoryDisplay.d.ts +8 -0
  40. package/dist/components/MemoryDisplay.d.ts.map +1 -0
  41. package/dist/components/MemoryDisplay.js +25 -0
  42. package/dist/components/MemoryTypeSelector.d.ts +8 -0
  43. package/dist/components/MemoryTypeSelector.d.ts.map +1 -0
  44. package/dist/components/MemoryTypeSelector.js +38 -0
  45. package/dist/components/MessageList.d.ts +12 -0
  46. package/dist/components/MessageList.d.ts.map +1 -0
  47. package/dist/components/MessageList.js +36 -0
  48. package/dist/components/ToolResultDisplay.d.ts +9 -0
  49. package/dist/components/ToolResultDisplay.d.ts.map +1 -0
  50. package/dist/components/ToolResultDisplay.js +52 -0
  51. package/dist/contexts/useAppConfig.d.ts +11 -0
  52. package/dist/contexts/useAppConfig.d.ts.map +1 -0
  53. package/dist/contexts/useAppConfig.js +13 -0
  54. package/dist/contexts/useChat.d.ts +36 -0
  55. package/dist/contexts/useChat.d.ts.map +1 -0
  56. package/dist/contexts/useChat.js +208 -0
  57. package/dist/hooks/useBashHistorySelector.d.ts +15 -0
  58. package/dist/hooks/useBashHistorySelector.d.ts.map +1 -0
  59. package/dist/hooks/useBashHistorySelector.js +61 -0
  60. package/dist/hooks/useCommandSelector.d.ts +24 -0
  61. package/dist/hooks/useCommandSelector.d.ts.map +1 -0
  62. package/dist/hooks/useCommandSelector.js +98 -0
  63. package/dist/hooks/useFileSelector.d.ts +16 -0
  64. package/dist/hooks/useFileSelector.d.ts.map +1 -0
  65. package/dist/hooks/useFileSelector.js +174 -0
  66. package/dist/hooks/useImageManager.d.ts +13 -0
  67. package/dist/hooks/useImageManager.d.ts.map +1 -0
  68. package/dist/hooks/useImageManager.js +46 -0
  69. package/dist/hooks/useInputHistory.d.ts +11 -0
  70. package/dist/hooks/useInputHistory.d.ts.map +1 -0
  71. package/dist/hooks/useInputHistory.js +64 -0
  72. package/dist/hooks/useInputKeyboardHandler.d.ts +83 -0
  73. package/dist/hooks/useInputKeyboardHandler.d.ts.map +1 -0
  74. package/dist/hooks/useInputKeyboardHandler.js +507 -0
  75. package/dist/hooks/useInputState.d.ts +14 -0
  76. package/dist/hooks/useInputState.d.ts.map +1 -0
  77. package/dist/hooks/useInputState.js +57 -0
  78. package/dist/hooks/useMemoryTypeSelector.d.ts +9 -0
  79. package/dist/hooks/useMemoryTypeSelector.d.ts.map +1 -0
  80. package/dist/hooks/useMemoryTypeSelector.js +27 -0
  81. package/dist/hooks/usePagination.d.ts +20 -0
  82. package/dist/hooks/usePagination.d.ts.map +1 -0
  83. package/dist/hooks/usePagination.js +168 -0
  84. package/dist/index.d.ts +5 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +91 -0
  87. package/dist/plain-cli.d.ts +7 -0
  88. package/dist/plain-cli.d.ts.map +1 -0
  89. package/dist/plain-cli.js +49 -0
  90. package/dist/utils/clipboard.d.ts +22 -0
  91. package/dist/utils/clipboard.d.ts.map +1 -0
  92. package/dist/utils/clipboard.js +347 -0
  93. package/dist/utils/constants.d.ts +17 -0
  94. package/dist/utils/constants.d.ts.map +1 -0
  95. package/dist/utils/constants.js +18 -0
  96. package/dist/utils/logger.d.ts +72 -0
  97. package/dist/utils/logger.d.ts.map +1 -0
  98. package/dist/utils/logger.js +245 -0
  99. package/package.json +60 -0
  100. package/src/cli.tsx +82 -0
  101. package/src/components/App.tsx +31 -0
  102. package/src/components/BashHistorySelector.tsx +163 -0
  103. package/src/components/BashShellManager.tsx +306 -0
  104. package/src/components/ChatInterface.tsx +88 -0
  105. package/src/components/CommandOutputDisplay.tsx +81 -0
  106. package/src/components/CommandSelector.tsx +144 -0
  107. package/src/components/CompressDisplay.tsx +58 -0
  108. package/src/components/DiffViewer.tsx +321 -0
  109. package/src/components/FileSelector.tsx +137 -0
  110. package/src/components/InputBox.tsx +310 -0
  111. package/src/components/McpManager.tsx +328 -0
  112. package/src/components/MemoryDisplay.tsx +62 -0
  113. package/src/components/MemoryTypeSelector.tsx +96 -0
  114. package/src/components/MessageList.tsx +215 -0
  115. package/src/components/ToolResultDisplay.tsx +138 -0
  116. package/src/contexts/useAppConfig.tsx +32 -0
  117. package/src/contexts/useChat.tsx +300 -0
  118. package/src/hooks/useBashHistorySelector.ts +77 -0
  119. package/src/hooks/useCommandSelector.ts +131 -0
  120. package/src/hooks/useFileSelector.ts +227 -0
  121. package/src/hooks/useImageManager.ts +64 -0
  122. package/src/hooks/useInputHistory.ts +74 -0
  123. package/src/hooks/useInputKeyboardHandler.ts +778 -0
  124. package/src/hooks/useInputState.ts +66 -0
  125. package/src/hooks/useMemoryTypeSelector.ts +40 -0
  126. package/src/hooks/usePagination.ts +203 -0
  127. package/src/index.ts +108 -0
  128. package/src/plain-cli.ts +66 -0
  129. package/src/utils/clipboard.ts +384 -0
  130. package/src/utils/constants.ts +22 -0
  131. package/src/utils/logger.ts +301 -0
@@ -0,0 +1,384 @@
1
+ import { unlinkSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { tmpdir } from "os";
4
+
5
+ export interface ClipboardImageResult {
6
+ success: boolean;
7
+ imagePath?: string;
8
+ error?: string;
9
+ mimeType?: string;
10
+ }
11
+
12
+ /**
13
+ * Read image from clipboard
14
+ * @returns Promise<ClipboardImageResult> Result containing image path or error information
15
+ */
16
+ export async function readClipboardImage(): Promise<ClipboardImageResult> {
17
+ try {
18
+ const platform = process.platform;
19
+
20
+ if (platform === "darwin") {
21
+ return await readClipboardImageMac();
22
+ } else if (platform === "win32") {
23
+ return await readClipboardImageWindows();
24
+ } else if (platform === "linux") {
25
+ return await readClipboardImageLinux();
26
+ } else {
27
+ return {
28
+ success: false,
29
+ error: `Clipboard image reading is not supported on platform: ${platform}`,
30
+ };
31
+ }
32
+ } catch (error) {
33
+ return {
34
+ success: false,
35
+ error: `Failed to read clipboard image: ${error instanceof Error ? error.message : String(error)}`,
36
+ };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Read clipboard image on macOS
42
+ */
43
+ async function readClipboardImageMac(): Promise<ClipboardImageResult> {
44
+ const { exec } = await import("child_process");
45
+ const { promisify } = await import("util");
46
+ const execAsync = promisify(exec);
47
+
48
+ try {
49
+ // Try to read image data directly to check if image exists
50
+ const testScript = `
51
+ tell application "System Events"
52
+ try
53
+ set imageData to the clipboard as «class PNGf»
54
+ return true
55
+ on error
56
+ return false
57
+ end try
58
+ end tell
59
+ `;
60
+
61
+ let hasImage = false;
62
+ try {
63
+ const { stdout: testResult } = await execAsync(
64
+ `osascript -e '${testScript}'`,
65
+ );
66
+ hasImage = testResult.trim() === "true";
67
+ } catch {
68
+ hasImage = false;
69
+ }
70
+
71
+ if (!hasImage) {
72
+ return {
73
+ success: false,
74
+ error: "No image found in clipboard",
75
+ };
76
+ }
77
+
78
+ // Generate temporary file path
79
+ const tempFilePath = join(tmpdir(), `clipboard-image-${Date.now()}.png`);
80
+
81
+ // Use osascript to save clipboard image as file
82
+ const saveScript = `
83
+ tell application "System Events"
84
+ try
85
+ set imageData to the clipboard as «class PNGf»
86
+ set fileRef to open for access POSIX file "${tempFilePath}" with write permission
87
+ write imageData to fileRef
88
+ close access fileRef
89
+ return true
90
+ on error errMsg
91
+ try
92
+ close access fileRef
93
+ end try
94
+ error errMsg
95
+ end try
96
+ end tell
97
+ `;
98
+
99
+ await execAsync(`osascript -e '${saveScript}'`);
100
+
101
+ // Verify if file was created successfully
102
+ if (!existsSync(tempFilePath)) {
103
+ return {
104
+ success: false,
105
+ error: "Failed to save clipboard image to temporary file",
106
+ };
107
+ }
108
+
109
+ return {
110
+ success: true,
111
+ imagePath: tempFilePath,
112
+ mimeType: "image/png",
113
+ };
114
+ } catch (error) {
115
+ return {
116
+ success: false,
117
+ error: `Failed to read clipboard image on macOS: ${error instanceof Error ? error.message : String(error)}`,
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Read clipboard image on Windows
124
+ */
125
+ async function readClipboardImageWindows(): Promise<ClipboardImageResult> {
126
+ try {
127
+ const { exec } = await import("child_process");
128
+ const { promisify } = await import("util");
129
+ const execAsync = promisify(exec);
130
+
131
+ // Use PowerShell to check if clipboard contains image
132
+ const checkScript = `
133
+ Add-Type -AssemblyName System.Windows.Forms
134
+ if ([System.Windows.Forms.Clipboard]::ContainsImage()) {
135
+ Write-Output "true"
136
+ } else {
137
+ Write-Output "false"
138
+ }
139
+ `;
140
+
141
+ try {
142
+ const { stdout } = await execAsync(
143
+ `powershell -Command "${checkScript}"`,
144
+ );
145
+ const hasImage = stdout.trim() === "true";
146
+
147
+ if (!hasImage) {
148
+ return {
149
+ success: false,
150
+ error: "No image found in clipboard",
151
+ };
152
+ }
153
+
154
+ // Generate temporary file path
155
+ const tempFilePath = join(tmpdir(), `clipboard-image-${Date.now()}.png`);
156
+
157
+ // Use PowerShell to save clipboard image
158
+ const saveScript = `
159
+ Add-Type -AssemblyName System.Windows.Forms
160
+ Add-Type -AssemblyName System.Drawing
161
+ $image = [System.Windows.Forms.Clipboard]::GetImage()
162
+ if ($image -ne $null) {
163
+ $image.Save("${tempFilePath.replace(/\\/g, "\\\\")}", [System.Drawing.Imaging.ImageFormat]::Png)
164
+ Write-Output "true"
165
+ } else {
166
+ Write-Output "false"
167
+ }
168
+ `;
169
+
170
+ const { stdout: saveResult } = await execAsync(
171
+ `powershell -Command "${saveScript}"`,
172
+ );
173
+
174
+ if (saveResult.trim() !== "true" || !existsSync(tempFilePath)) {
175
+ return {
176
+ success: false,
177
+ error: "Failed to save clipboard image to temporary file",
178
+ };
179
+ }
180
+
181
+ return {
182
+ success: true,
183
+ imagePath: tempFilePath,
184
+ mimeType: "image/png",
185
+ };
186
+ } catch (err) {
187
+ return {
188
+ success: false,
189
+ error: `Failed to access clipboard on Windows: ${err instanceof Error ? err.message : String(err)}`,
190
+ };
191
+ }
192
+ } catch (err) {
193
+ return {
194
+ success: false,
195
+ error: `Failed to read clipboard image on Windows: ${err instanceof Error ? err.message : String(err)}`,
196
+ };
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Read clipboard image on Linux
202
+ */
203
+ async function readClipboardImageLinux(): Promise<ClipboardImageResult> {
204
+ try {
205
+ // Linux can use tools like xclip or wl-clipboard
206
+ const { exec } = await import("child_process");
207
+ const { promisify } = await import("util");
208
+ const execAsync = promisify(exec);
209
+
210
+ // Check if xclip is available
211
+ try {
212
+ await execAsync("which xclip");
213
+ } catch {
214
+ return {
215
+ success: false,
216
+ error:
217
+ "xclip is required for clipboard image operations on Linux. Please install it: sudo apt-get install xclip",
218
+ };
219
+ }
220
+
221
+ // Check if clipboard contains image
222
+ try {
223
+ await execAsync(
224
+ "xclip -selection clipboard -t image/png -o > /dev/null 2>&1",
225
+ );
226
+ } catch {
227
+ return {
228
+ success: false,
229
+ error: "No image found in clipboard",
230
+ };
231
+ }
232
+
233
+ // Generate temporary file path
234
+ const tempFilePath = join(tmpdir(), `clipboard-image-${Date.now()}.png`);
235
+
236
+ // Use xclip to save clipboard image
237
+ try {
238
+ await execAsync(
239
+ `xclip -selection clipboard -t image/png -o > "${tempFilePath}"`,
240
+ );
241
+
242
+ if (!existsSync(tempFilePath)) {
243
+ return {
244
+ success: false,
245
+ error: "Failed to save clipboard image to temporary file",
246
+ };
247
+ }
248
+
249
+ return {
250
+ success: true,
251
+ imagePath: tempFilePath,
252
+ mimeType: "image/png",
253
+ };
254
+ } catch (err) {
255
+ return {
256
+ success: false,
257
+ error: `Failed to save clipboard image: ${err instanceof Error ? err.message : String(err)}`,
258
+ };
259
+ }
260
+ } catch (err) {
261
+ return {
262
+ success: false,
263
+ error: `Failed to read clipboard image on Linux: ${err instanceof Error ? err.message : String(err)}`,
264
+ };
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Clean up temporary image file
270
+ * @param imagePath Path to the image file to clean up
271
+ */
272
+ export function cleanupTempImage(imagePath: string): void {
273
+ try {
274
+ if (existsSync(imagePath)) {
275
+ unlinkSync(imagePath);
276
+ }
277
+ } catch (error) {
278
+ console.warn(`Failed to cleanup temporary image file: ${imagePath}`, error);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Check if clipboard contains image (quick check, does not save file)
284
+ * @returns Promise<boolean> Whether it contains image
285
+ */
286
+ export async function hasClipboardImage(): Promise<boolean> {
287
+ try {
288
+ const platform = process.platform;
289
+
290
+ if (platform === "darwin") {
291
+ return await hasClipboardImageMac();
292
+ } else if (platform === "win32") {
293
+ return await hasClipboardImageWindows();
294
+ } else if (platform === "linux") {
295
+ return await hasClipboardImageLinux();
296
+ } else {
297
+ return false;
298
+ }
299
+ } catch {
300
+ return false;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Check if clipboard contains image on macOS
306
+ */
307
+ async function hasClipboardImageMac(): Promise<boolean> {
308
+ try {
309
+ const { exec } = await import("child_process");
310
+ const { promisify } = await import("util");
311
+ const execAsync = promisify(exec);
312
+
313
+ const checkScript = `
314
+ tell application "System Events"
315
+ try
316
+ set imageData to the clipboard as «class PNGf»
317
+ return true
318
+ on error
319
+ return false
320
+ end try
321
+ end tell
322
+ `;
323
+
324
+ const { stdout } = await execAsync(`osascript -e '${checkScript}'`);
325
+ return stdout.trim() === "true";
326
+ } catch {
327
+ return false;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Check if clipboard contains image on Windows
333
+ */
334
+ async function hasClipboardImageWindows(): Promise<boolean> {
335
+ try {
336
+ const { exec } = await import("child_process");
337
+ const { promisify } = await import("util");
338
+ const execAsync = promisify(exec);
339
+
340
+ const checkScript = `
341
+ Add-Type -AssemblyName System.Windows.Forms
342
+ if ([System.Windows.Forms.Clipboard]::ContainsImage()) {
343
+ Write-Output "true"
344
+ } else {
345
+ Write-Output "false"
346
+ }
347
+ `;
348
+
349
+ const { stdout } = await execAsync(`powershell -Command "${checkScript}"`);
350
+ return stdout.trim() === "true";
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Check if clipboard contains image on Linux
358
+ */
359
+ async function hasClipboardImageLinux(): Promise<boolean> {
360
+ try {
361
+ const { exec } = await import("child_process");
362
+ const { promisify } = await import("util");
363
+ const execAsync = promisify(exec);
364
+
365
+ // Check if xclip is available
366
+ try {
367
+ await execAsync("which xclip");
368
+ } catch {
369
+ return false;
370
+ }
371
+
372
+ // Check if clipboard contains image
373
+ try {
374
+ await execAsync(
375
+ "xclip -selection clipboard -t image/png -o > /dev/null 2>&1",
376
+ );
377
+ return true;
378
+ } catch {
379
+ return false;
380
+ }
381
+ } catch {
382
+ return false;
383
+ }
384
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Application constants definition
3
+ */
4
+
5
+ import path from "path";
6
+ import os from "os";
7
+
8
+ /**
9
+ * Application data storage directory
10
+ * Used to store debug logs, command history and other data
11
+ */
12
+ export const DATA_DIRECTORY = path.join(os.homedir(), ".wave");
13
+
14
+ /**
15
+ * Application log file path
16
+ */
17
+ export const LOG_FILE = path.join(DATA_DIRECTORY, "app.log");
18
+
19
+ /**
20
+ * Pagination related constants
21
+ */
22
+ export const MESSAGES_PER_PAGE = 20; // Number of messages displayed per page
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Logger utility module
3
+ * Supports filtering by log level and keywords
4
+ * Logs are written to files instead of terminal to avoid being cleared by Ink app
5
+ *
6
+ * Performance optimization:
7
+ * - In test environment, you can disable all file and console I/O operations by setting environment variable DISABLE_LOGGER_IO=true
8
+ * - This can significantly improve test execution performance by avoiding unnecessary disk writes
9
+ */
10
+
11
+ import * as fs from "fs";
12
+ import { LOG_FILE, DATA_DIRECTORY } from "./constants.js";
13
+
14
+ const logFile = process.env.LOG_FILE || LOG_FILE;
15
+
16
+ /**
17
+ * Log level enumeration
18
+ */
19
+ export enum LogLevel {
20
+ DEBUG = 0,
21
+ INFO = 1,
22
+ WARN = 2,
23
+ ERROR = 3,
24
+ }
25
+
26
+ /**
27
+ * Log level name mapping
28
+ */
29
+ const LOG_LEVEL_NAMES = {
30
+ [LogLevel.DEBUG]: "DEBUG",
31
+ [LogLevel.INFO]: "INFO",
32
+ [LogLevel.WARN]: "WARN",
33
+ [LogLevel.ERROR]: "ERROR",
34
+ };
35
+
36
+ /**
37
+ * Log configuration interface
38
+ */
39
+ interface LogConfig {
40
+ readonly level: LogLevel;
41
+ readonly keywords: string[];
42
+ }
43
+
44
+ /**
45
+ * Parse log level from environment variable
46
+ */
47
+ const parseLogLevel = (levelStr: string | undefined): LogLevel => {
48
+ if (!levelStr) return LogLevel.INFO;
49
+
50
+ const upperLevel = levelStr.toUpperCase();
51
+ switch (upperLevel) {
52
+ case "DEBUG":
53
+ return LogLevel.DEBUG;
54
+ case "INFO":
55
+ return LogLevel.INFO;
56
+ case "WARN":
57
+ return LogLevel.WARN;
58
+ case "ERROR":
59
+ return LogLevel.ERROR;
60
+ default:
61
+ return LogLevel.INFO;
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Parse keyword filter from environment variable
67
+ */
68
+ const parseKeywords = (keywordsStr: string | undefined): string[] => {
69
+ if (!keywordsStr) return [];
70
+ return keywordsStr
71
+ .split(",")
72
+ .map((k) => k.trim().toLowerCase())
73
+ .filter((k) => k.length > 0);
74
+ };
75
+
76
+ /**
77
+ * Load log configuration from environment variables
78
+ *
79
+ * Supported environment variables:
80
+ * - LOG_LEVEL: Log level (DEBUG, INFO, WARN, ERROR), default INFO
81
+ * - LOG_KEYWORDS: Keyword filter, comma-separated, default no filter
82
+ */
83
+ const logConfig: LogConfig = {
84
+ level: parseLogLevel(process.env.LOG_LEVEL),
85
+ keywords: parseKeywords(process.env.LOG_KEYWORDS),
86
+ };
87
+
88
+ /**
89
+ * Check if log should be recorded
90
+ */
91
+ const shouldLog = (level: LogLevel, message: string): boolean => {
92
+ // Check log level
93
+ if (level < logConfig.level) {
94
+ return false;
95
+ }
96
+
97
+ // If no keyword filter is set, record all logs that meet the level requirement
98
+ if (logConfig.keywords.length === 0) {
99
+ return true;
100
+ }
101
+
102
+ // Check keyword filter
103
+ const lowerMessage = message.toLowerCase();
104
+ return logConfig.keywords.some((keyword) => lowerMessage.includes(keyword));
105
+ };
106
+
107
+ /**
108
+ * Format log arguments
109
+ */
110
+ const formatArg = (arg: unknown): string => {
111
+ if (arg === null) return "null";
112
+ if (arg === undefined) return "undefined";
113
+
114
+ if (arg instanceof Error) {
115
+ // Special handling for Error objects, display stack or message
116
+ return arg.stack || arg.message || String(arg);
117
+ }
118
+
119
+ if (typeof arg === "object") {
120
+ try {
121
+ return JSON.stringify(arg, null, 2);
122
+ } catch {
123
+ // If JSON.stringify fails, fallback to String()
124
+ return String(arg);
125
+ }
126
+ }
127
+
128
+ return String(arg);
129
+ };
130
+
131
+ /**
132
+ * Generic log output function
133
+ */
134
+ const logMessage = (level: LogLevel, ...args: unknown[]): void => {
135
+ const messageText = args.map(formatArg).join(" ");
136
+
137
+ // Check if this log should be recorded
138
+ if (!shouldLog(level, messageText)) {
139
+ return;
140
+ }
141
+
142
+ // If logger I/O operations are disabled, return directly to save performance
143
+ if (process.env.DISABLE_LOGGER_IO === "true") {
144
+ return;
145
+ }
146
+
147
+ const levelName = LOG_LEVEL_NAMES[level];
148
+ const timestamp = new Date().toISOString();
149
+ const formattedMessage = `[${timestamp}] [${levelName}] ${messageText}\n`;
150
+
151
+ try {
152
+ // Ensure directory exists
153
+ if (!fs.existsSync(DATA_DIRECTORY)) {
154
+ fs.mkdirSync(DATA_DIRECTORY, { recursive: true });
155
+ }
156
+
157
+ // Write log to file
158
+ fs.appendFileSync(logFile, formattedMessage);
159
+ } catch (error) {
160
+ // If file write fails, fallback to stderr
161
+ process.stderr.write(
162
+ `[${levelName}] Failed to write to log file: ${error}\n`,
163
+ );
164
+ process.stderr.write(formattedMessage);
165
+ }
166
+ };
167
+
168
+ /**
169
+ * Logger object
170
+ */
171
+ export const logger = {
172
+ /**
173
+ * Debug log output function
174
+ */
175
+ debug: (...args: unknown[]): void => {
176
+ logMessage(LogLevel.DEBUG, ...args);
177
+ },
178
+
179
+ /**
180
+ * Info log output function
181
+ */
182
+ info: (...args: unknown[]): void => {
183
+ logMessage(LogLevel.INFO, ...args);
184
+ },
185
+
186
+ /**
187
+ * Warning log output function
188
+ */
189
+ warn: (...args: unknown[]): void => {
190
+ logMessage(LogLevel.WARN, ...args);
191
+ },
192
+
193
+ /**
194
+ * Error log output function
195
+ */
196
+ error: (...args: unknown[]): void => {
197
+ logMessage(LogLevel.ERROR, ...args);
198
+ },
199
+ };
200
+
201
+ /**
202
+ * Get current log configuration
203
+ */
204
+ export const getLogConfig = (): LogConfig => {
205
+ return logConfig;
206
+ };
207
+
208
+ /**
209
+ * Get log file path
210
+ */
211
+ export const getLogFile = (): string => {
212
+ return logFile;
213
+ };
214
+
215
+ /**
216
+ * Log cleanup configuration
217
+ */
218
+ interface LogCleanupConfig {
219
+ /** Maximum size of current log file (bytes), default 10MB */
220
+ maxFileSize: number;
221
+ /** Number of lines to keep when truncating, default 1000 lines */
222
+ keepLines: number;
223
+ }
224
+
225
+ /**
226
+ * Get log cleanup configuration
227
+ * Can override default configuration with environment variables
228
+ */
229
+ const getCleanupConfig = (): LogCleanupConfig => {
230
+ return {
231
+ maxFileSize: parseInt(process.env.LOG_MAX_FILE_SIZE || "10485760", 10), // 10MB
232
+ keepLines: parseInt(process.env.LOG_KEEP_LINES || "1000", 10),
233
+ };
234
+ };
235
+
236
+ /**
237
+ * Truncate current log file if too large
238
+ * Keep the last specified number of lines
239
+ */
240
+ const truncateLogFileIfNeeded = (config: LogCleanupConfig): void => {
241
+ // If logger I/O operations are disabled, return directly to save performance
242
+ if (process.env.DISABLE_LOGGER_IO === "true") {
243
+ return;
244
+ }
245
+
246
+ try {
247
+ if (!fs.existsSync(logFile)) {
248
+ return;
249
+ }
250
+
251
+ const stats = fs.statSync(logFile);
252
+
253
+ // If file size exceeds limit, truncate file
254
+ if (stats.size > config.maxFileSize) {
255
+ const content = fs.readFileSync(logFile, "utf8");
256
+ const lines = content.split("\n");
257
+
258
+ // Keep the last specified number of lines
259
+ const keepLines = Math.min(config.keepLines, lines.length);
260
+ const truncatedContent = lines.slice(-keepLines).join("\n");
261
+
262
+ // Write truncated content
263
+ fs.writeFileSync(logFile, truncatedContent);
264
+
265
+ // Record truncation operation
266
+ const removedLines = lines.length - keepLines;
267
+ logger.info(
268
+ `Log file truncated: removed ${removedLines} lines, kept last ${keepLines} lines`,
269
+ );
270
+ }
271
+ } catch (error) {
272
+ logger.warn("Failed to truncate log file:", error);
273
+ }
274
+ };
275
+
276
+ /**
277
+ * Execute log cleanup
278
+ * Truncate current log file (if needed)
279
+ *
280
+ * @param customConfig Custom cleanup configuration, if not provided uses default configuration
281
+ */
282
+ export const cleanupLogs = async (
283
+ customConfig?: Partial<LogCleanupConfig>,
284
+ ): Promise<void> => {
285
+ // If logger I/O operations are disabled, return directly to save performance
286
+ if (process.env.DISABLE_LOGGER_IO === "true") {
287
+ return;
288
+ }
289
+
290
+ const config = { ...getCleanupConfig(), ...customConfig };
291
+
292
+ logger.info("Starting log cleanup...", {
293
+ maxFileSize: config.maxFileSize,
294
+ keepLines: config.keepLines,
295
+ });
296
+
297
+ // Truncate current log file (if needed)
298
+ truncateLogFileIfNeeded(config);
299
+
300
+ logger.info("Log cleanup completed");
301
+ };