@link-assistant/agent 0.5.3 → 0.6.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/src/util/log.ts CHANGED
@@ -2,7 +2,20 @@ import path from 'path';
2
2
  import fs from 'fs/promises';
3
3
  import { Global } from '../global';
4
4
  import z from 'zod';
5
+ import makeLog, { levels } from 'log-lazy';
6
+ import { Flag } from '../flag/flag.ts';
5
7
 
8
+ /**
9
+ * Logging module with JSON output and lazy evaluation support.
10
+ *
11
+ * Features:
12
+ * - JSON formatted output: All logs are wrapped in { log: { ... } } structure
13
+ * - Lazy evaluation: Use lazy() methods to defer expensive computations
14
+ * - Level control: Respects --verbose flag and log level settings
15
+ * - File logging: Writes to file when not in verbose/print mode
16
+ *
17
+ * The JSON format ensures all output is parsable, separating logs from regular output.
18
+ */
6
19
  export namespace Log {
7
20
  export const Level = z
8
21
  .enum(['DEBUG', 'INFO', 'WARN', 'ERROR'])
@@ -17,16 +30,41 @@ export namespace Log {
17
30
  };
18
31
 
19
32
  let level: Level = 'INFO';
33
+ let jsonOutput = false; // Whether to output JSON format (enabled in verbose mode)
20
34
 
21
35
  function shouldLog(input: Level): boolean {
22
36
  return levelPriority[input] >= levelPriority[level];
23
37
  }
24
38
 
39
+ /**
40
+ * Logger interface with support for both immediate and lazy logging.
41
+ *
42
+ * All logging methods accept either:
43
+ * - A message string/object/Error with optional extra data
44
+ * - A function that returns the data to log (lazy evaluation)
45
+ *
46
+ * Lazy logging: The function is only called if logging is enabled for that level,
47
+ * avoiding expensive computations when logs are disabled.
48
+ */
25
49
  export type Logger = {
26
- debug(message?: any, extra?: Record<string, any>): void;
27
- info(message?: any, extra?: Record<string, any>): void;
28
- error(message?: any, extra?: Record<string, any>): void;
29
- warn(message?: any, extra?: Record<string, any>): void;
50
+ // Unified logging - supports both immediate and lazy (callback) styles
51
+ debug(
52
+ message?: any | (() => { message?: string; [key: string]: any }),
53
+ extra?: Record<string, any>
54
+ ): void;
55
+ info(
56
+ message?: any | (() => { message?: string; [key: string]: any }),
57
+ extra?: Record<string, any>
58
+ ): void;
59
+ error(
60
+ message?: any | (() => { message?: string; [key: string]: any }),
61
+ extra?: Record<string, any>
62
+ ): void;
63
+ warn(
64
+ message?: any | (() => { message?: string; [key: string]: any }),
65
+ extra?: Record<string, any>
66
+ ): void;
67
+
30
68
  tag(key: string, value: string): Logger;
31
69
  clone(): Logger;
32
70
  time(
@@ -54,24 +92,47 @@ export namespace Log {
54
92
  }
55
93
  let write = (msg: any) => Bun.stderr.write(msg);
56
94
 
95
+ // Initialize log-lazy for controlling lazy log execution
96
+ let lazyLogInstance = makeLog({ level: 0 }); // Start disabled
97
+
57
98
  export async function init(options: Options) {
58
99
  if (options.level) level = options.level;
59
100
  cleanup(Global.Path.log);
60
- if (options.print) return;
61
- logpath = path.join(
62
- Global.Path.log,
63
- options.dev
64
- ? 'dev.log'
65
- : new Date().toISOString().split('.')[0].replace(/:/g, '') + '.log'
66
- );
67
- const logfile = Bun.file(logpath);
68
- await fs.truncate(logpath).catch(() => {});
69
- const writer = logfile.writer();
70
- write = async (msg: any) => {
71
- const num = writer.write(msg);
72
- writer.flush();
73
- return num;
74
- };
101
+
102
+ // Always use JSON output format for logs
103
+ jsonOutput = true;
104
+
105
+ // Configure lazy logging level based on verbose flag
106
+ if (Flag.OPENCODE_VERBOSE || options.print) {
107
+ // Enable all levels for lazy logging when verbose
108
+ lazyLogInstance = makeLog({
109
+ level: levels.debug | levels.info | levels.warn | levels.error,
110
+ });
111
+ } else {
112
+ // Disable lazy logging when not verbose
113
+ lazyLogInstance = makeLog({ level: 0 });
114
+ }
115
+
116
+ if (options.print) {
117
+ // In print mode, output to stderr
118
+ // No file logging needed
119
+ } else {
120
+ // In normal mode, write to file
121
+ logpath = path.join(
122
+ Global.Path.log,
123
+ options.dev
124
+ ? 'dev.log'
125
+ : new Date().toISOString().split('.')[0].replace(/:/g, '') + '.log'
126
+ );
127
+ const logfile = Bun.file(logpath);
128
+ await fs.truncate(logpath).catch(() => {});
129
+ const writer = logfile.writer();
130
+ write = async (msg: any) => {
131
+ const num = writer.write(msg);
132
+ writer.flush();
133
+ return num;
134
+ };
135
+ }
75
136
  }
76
137
 
77
138
  async function cleanup(dir: string) {
@@ -97,6 +158,43 @@ export namespace Log {
97
158
  : result;
98
159
  }
99
160
 
161
+ /**
162
+ * Format log entry as JSON object wrapped in { log: { ... } }
163
+ */
164
+ function formatJson(
165
+ logLevel: Level,
166
+ message: any,
167
+ tags: Record<string, any>,
168
+ extra?: Record<string, any>
169
+ ): string {
170
+ const timestamp = new Date().toISOString();
171
+ const logEntry: Record<string, any> = {
172
+ level: logLevel.toLowerCase(),
173
+ timestamp,
174
+ ...tags,
175
+ ...extra,
176
+ };
177
+
178
+ if (message !== undefined && message !== null) {
179
+ if (typeof message === 'string') {
180
+ logEntry.message = message;
181
+ } else if (message instanceof Error) {
182
+ logEntry.message = message.message;
183
+ logEntry.error = {
184
+ name: message.name,
185
+ message: message.message,
186
+ stack: message.stack,
187
+ };
188
+ } else if (typeof message === 'object') {
189
+ Object.assign(logEntry, message);
190
+ } else {
191
+ logEntry.message = String(message);
192
+ }
193
+ }
194
+
195
+ return JSON.stringify({ log: logEntry });
196
+ }
197
+
100
198
  let last = Date.now();
101
199
  export function create(tags?: Record<string, any>) {
102
200
  tags = tags || {};
@@ -109,7 +207,8 @@ export namespace Log {
109
207
  }
110
208
  }
111
209
 
112
- function build(message: any, extra?: Record<string, any>) {
210
+ // Legacy format for file logging (backward compatibility)
211
+ function buildLegacy(message: any, extra?: Record<string, any>) {
113
212
  const prefix = Object.entries({
114
213
  ...tags,
115
214
  ...extra,
@@ -131,27 +230,83 @@ export namespace Log {
131
230
  .join(' ') + '\n'
132
231
  );
133
232
  }
233
+
234
+ // Choose output format based on jsonOutput flag
235
+ function output(
236
+ logLevel: Level,
237
+ message: any,
238
+ extra?: Record<string, any>
239
+ ) {
240
+ if (jsonOutput) {
241
+ // Use our custom JSON formatting for { log: { ... } } format
242
+ write(formatJson(logLevel, message, tags || {}, extra) + '\n');
243
+ } else {
244
+ write(logLevel.padEnd(5) + ' ' + buildLegacy(message, extra));
245
+ }
246
+ }
247
+
134
248
  const result: Logger = {
135
249
  debug(message?: any, extra?: Record<string, any>) {
136
- if (shouldLog('DEBUG')) {
137
- write('DEBUG ' + build(message, extra));
250
+ if (!shouldLog('DEBUG')) return;
251
+
252
+ // Check if message is a function (lazy logging)
253
+ if (typeof message === 'function') {
254
+ lazyLogInstance.debug(() => {
255
+ const data = message();
256
+ const { message: msg, ...extraData } = data;
257
+ output('DEBUG', msg, extraData);
258
+ return '';
259
+ });
260
+ } else {
261
+ output('DEBUG', message, extra);
138
262
  }
139
263
  },
140
264
  info(message?: any, extra?: Record<string, any>) {
141
- if (shouldLog('INFO')) {
142
- write('INFO ' + build(message, extra));
265
+ if (!shouldLog('INFO')) return;
266
+
267
+ // Check if message is a function (lazy logging)
268
+ if (typeof message === 'function') {
269
+ lazyLogInstance.info(() => {
270
+ const data = message();
271
+ const { message: msg, ...extraData } = data;
272
+ output('INFO', msg, extraData);
273
+ return '';
274
+ });
275
+ } else {
276
+ output('INFO', message, extra);
143
277
  }
144
278
  },
145
279
  error(message?: any, extra?: Record<string, any>) {
146
- if (shouldLog('ERROR')) {
147
- write('ERROR ' + build(message, extra));
280
+ if (!shouldLog('ERROR')) return;
281
+
282
+ // Check if message is a function (lazy logging)
283
+ if (typeof message === 'function') {
284
+ lazyLogInstance.error(() => {
285
+ const data = message();
286
+ const { message: msg, ...extraData } = data;
287
+ output('ERROR', msg, extraData);
288
+ return '';
289
+ });
290
+ } else {
291
+ output('ERROR', message, extra);
148
292
  }
149
293
  },
150
294
  warn(message?: any, extra?: Record<string, any>) {
151
- if (shouldLog('WARN')) {
152
- write('WARN ' + build(message, extra));
295
+ if (!shouldLog('WARN')) return;
296
+
297
+ // Check if message is a function (lazy logging)
298
+ if (typeof message === 'function') {
299
+ lazyLogInstance.warn(() => {
300
+ const data = message();
301
+ const { message: msg, ...extraData } = data;
302
+ output('WARN', msg, extraData);
303
+ return '';
304
+ });
305
+ } else {
306
+ output('WARN', message, extra);
153
307
  }
154
308
  },
309
+
155
310
  tag(key: string, value: string) {
156
311
  if (tags) tags[key] = value;
157
312
  return result;
@@ -184,4 +339,26 @@ export namespace Log {
184
339
 
185
340
  return result;
186
341
  }
342
+
343
+ /**
344
+ * Check if JSON output mode is enabled
345
+ */
346
+ export function isJsonOutput(): boolean {
347
+ return jsonOutput;
348
+ }
349
+
350
+ /**
351
+ * Sync lazy logging with verbose flag at runtime
352
+ * Call after Flag.setVerbose() to update lazy logging state
353
+ */
354
+ export function syncWithVerboseFlag(): void {
355
+ if (Flag.OPENCODE_VERBOSE) {
356
+ jsonOutput = true;
357
+ lazyLogInstance = makeLog({
358
+ level: levels.debug | levels.info | levels.warn | levels.error,
359
+ });
360
+ } else {
361
+ lazyLogInstance = makeLog({ level: 0 });
362
+ }
363
+ }
187
364
  }