watchfix 0.3.0 → 0.4.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.
@@ -27,6 +27,19 @@ logs:
27
27
  type: file
28
28
  path: ./logs/app.log
29
29
 
30
+ # Example NDJSON source (for structured JSON logs like Pino, Bunyan, Winston):
31
+ # - name: app-json
32
+ # type: file
33
+ # path: ./logs/app.ndjson
34
+ # format: ndjson
35
+ # ndjson:
36
+ # messageField: msg # Required: field containing log message
37
+ # timestampField: time # Optional: field with timestamp
38
+ # levelField: level # Optional: field with log level
39
+ # levelFilter: # Optional: only process these levels
40
+ # - error
41
+ # - fatal
42
+
30
43
  # Example docker source:
31
44
  # - name: api
32
45
  # type: docker
@@ -1,29 +1,76 @@
1
1
  import { z } from 'zod';
2
2
  declare const durationSchema: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
3
+ declare const ndjsonConfigSchema: z.ZodObject<{
4
+ messageField: z.ZodString;
5
+ timestampField: z.ZodOptional<z.ZodString>;
6
+ levelField: z.ZodOptional<z.ZodString>;
7
+ levelFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ messageField: string;
10
+ timestampField?: string | undefined;
11
+ levelField?: string | undefined;
12
+ levelFilter?: string[] | undefined;
13
+ }, {
14
+ messageField: string;
15
+ timestampField?: string | undefined;
16
+ levelField?: string | undefined;
17
+ levelFilter?: string[] | undefined;
18
+ }>;
3
19
  declare const fileSourceSchema: z.ZodObject<{
4
20
  name: z.ZodString;
5
21
  type: z.ZodLiteral<"file">;
6
22
  path: z.ZodString;
23
+ format: z.ZodOptional<z.ZodEnum<["text", "ndjson"]>>;
24
+ ndjson: z.ZodOptional<z.ZodObject<{
25
+ messageField: z.ZodString;
26
+ timestampField: z.ZodOptional<z.ZodString>;
27
+ levelField: z.ZodOptional<z.ZodString>;
28
+ levelFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
29
+ }, "strip", z.ZodTypeAny, {
30
+ messageField: string;
31
+ timestampField?: string | undefined;
32
+ levelField?: string | undefined;
33
+ levelFilter?: string[] | undefined;
34
+ }, {
35
+ messageField: string;
36
+ timestampField?: string | undefined;
37
+ levelField?: string | undefined;
38
+ levelFilter?: string[] | undefined;
39
+ }>>;
7
40
  }, "strip", z.ZodTypeAny, {
8
41
  path: string;
9
- name: string;
10
42
  type: "file";
43
+ name: string;
44
+ ndjson?: {
45
+ messageField: string;
46
+ timestampField?: string | undefined;
47
+ levelField?: string | undefined;
48
+ levelFilter?: string[] | undefined;
49
+ } | undefined;
50
+ format?: "text" | "ndjson" | undefined;
11
51
  }, {
12
52
  path: string;
13
- name: string;
14
53
  type: "file";
54
+ name: string;
55
+ ndjson?: {
56
+ messageField: string;
57
+ timestampField?: string | undefined;
58
+ levelField?: string | undefined;
59
+ levelFilter?: string[] | undefined;
60
+ } | undefined;
61
+ format?: "text" | "ndjson" | undefined;
15
62
  }>;
16
63
  declare const dockerSourceSchema: z.ZodObject<{
17
64
  name: z.ZodString;
18
65
  type: z.ZodLiteral<"docker">;
19
66
  container: z.ZodString;
20
67
  }, "strip", z.ZodTypeAny, {
21
- name: string;
22
68
  type: "docker";
69
+ name: string;
23
70
  container: string;
24
71
  }, {
25
- name: string;
26
72
  type: "docker";
73
+ name: string;
27
74
  container: string;
28
75
  }>;
29
76
  declare const commandSourceSchema: z.ZodObject<{
@@ -32,13 +79,13 @@ declare const commandSourceSchema: z.ZodObject<{
32
79
  run: z.ZodString;
33
80
  interval: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
34
81
  }, "strip", z.ZodTypeAny, {
35
- name: string;
36
82
  type: "command";
83
+ name: string;
37
84
  run: string;
38
85
  interval: string;
39
86
  }, {
40
- name: string;
41
87
  type: "command";
88
+ name: string;
42
89
  run: string;
43
90
  interval: string;
44
91
  }>;
@@ -46,25 +93,56 @@ declare const logSourceSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
46
93
  name: z.ZodString;
47
94
  type: z.ZodLiteral<"file">;
48
95
  path: z.ZodString;
96
+ format: z.ZodOptional<z.ZodEnum<["text", "ndjson"]>>;
97
+ ndjson: z.ZodOptional<z.ZodObject<{
98
+ messageField: z.ZodString;
99
+ timestampField: z.ZodOptional<z.ZodString>;
100
+ levelField: z.ZodOptional<z.ZodString>;
101
+ levelFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
102
+ }, "strip", z.ZodTypeAny, {
103
+ messageField: string;
104
+ timestampField?: string | undefined;
105
+ levelField?: string | undefined;
106
+ levelFilter?: string[] | undefined;
107
+ }, {
108
+ messageField: string;
109
+ timestampField?: string | undefined;
110
+ levelField?: string | undefined;
111
+ levelFilter?: string[] | undefined;
112
+ }>>;
49
113
  }, "strip", z.ZodTypeAny, {
50
114
  path: string;
51
- name: string;
52
115
  type: "file";
116
+ name: string;
117
+ ndjson?: {
118
+ messageField: string;
119
+ timestampField?: string | undefined;
120
+ levelField?: string | undefined;
121
+ levelFilter?: string[] | undefined;
122
+ } | undefined;
123
+ format?: "text" | "ndjson" | undefined;
53
124
  }, {
54
125
  path: string;
55
- name: string;
56
126
  type: "file";
127
+ name: string;
128
+ ndjson?: {
129
+ messageField: string;
130
+ timestampField?: string | undefined;
131
+ levelField?: string | undefined;
132
+ levelFilter?: string[] | undefined;
133
+ } | undefined;
134
+ format?: "text" | "ndjson" | undefined;
57
135
  }>, z.ZodObject<{
58
136
  name: z.ZodString;
59
137
  type: z.ZodLiteral<"docker">;
60
138
  container: z.ZodString;
61
139
  }, "strip", z.ZodTypeAny, {
62
- name: string;
63
140
  type: "docker";
141
+ name: string;
64
142
  container: string;
65
143
  }, {
66
- name: string;
67
144
  type: "docker";
145
+ name: string;
68
146
  container: string;
69
147
  }>, z.ZodObject<{
70
148
  name: z.ZodString;
@@ -72,13 +150,13 @@ declare const logSourceSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
72
150
  run: z.ZodString;
73
151
  interval: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
74
152
  }, "strip", z.ZodTypeAny, {
75
- name: string;
76
153
  type: "command";
154
+ name: string;
77
155
  run: string;
78
156
  interval: string;
79
157
  }, {
80
- name: string;
81
158
  type: "command";
159
+ name: string;
82
160
  run: string;
83
161
  interval: string;
84
162
  }>]>;
@@ -117,29 +195,60 @@ declare const configSchema: z.ZodObject<{
117
195
  stderr_is_progress?: boolean | undefined;
118
196
  }>;
119
197
  logs: z.ZodObject<{
120
- sources: z.ZodEffects<z.ZodArray<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
198
+ sources: z.ZodEffects<z.ZodEffects<z.ZodArray<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
121
199
  name: z.ZodString;
122
200
  type: z.ZodLiteral<"file">;
123
201
  path: z.ZodString;
202
+ format: z.ZodOptional<z.ZodEnum<["text", "ndjson"]>>;
203
+ ndjson: z.ZodOptional<z.ZodObject<{
204
+ messageField: z.ZodString;
205
+ timestampField: z.ZodOptional<z.ZodString>;
206
+ levelField: z.ZodOptional<z.ZodString>;
207
+ levelFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
208
+ }, "strip", z.ZodTypeAny, {
209
+ messageField: string;
210
+ timestampField?: string | undefined;
211
+ levelField?: string | undefined;
212
+ levelFilter?: string[] | undefined;
213
+ }, {
214
+ messageField: string;
215
+ timestampField?: string | undefined;
216
+ levelField?: string | undefined;
217
+ levelFilter?: string[] | undefined;
218
+ }>>;
124
219
  }, "strip", z.ZodTypeAny, {
125
220
  path: string;
126
- name: string;
127
221
  type: "file";
222
+ name: string;
223
+ ndjson?: {
224
+ messageField: string;
225
+ timestampField?: string | undefined;
226
+ levelField?: string | undefined;
227
+ levelFilter?: string[] | undefined;
228
+ } | undefined;
229
+ format?: "text" | "ndjson" | undefined;
128
230
  }, {
129
231
  path: string;
130
- name: string;
131
232
  type: "file";
233
+ name: string;
234
+ ndjson?: {
235
+ messageField: string;
236
+ timestampField?: string | undefined;
237
+ levelField?: string | undefined;
238
+ levelFilter?: string[] | undefined;
239
+ } | undefined;
240
+ format?: "text" | "ndjson" | undefined;
132
241
  }>, z.ZodObject<{
133
242
  name: z.ZodString;
134
243
  type: z.ZodLiteral<"docker">;
135
244
  container: z.ZodString;
136
245
  }, "strip", z.ZodTypeAny, {
137
- name: string;
138
246
  type: "docker";
247
+ name: string;
139
248
  container: string;
140
249
  }, {
141
- name: string;
142
250
  type: "docker";
251
+ name: string;
143
252
  container: string;
144
253
  }>, z.ZodObject<{
145
254
  name: z.ZodString;
@@ -147,39 +256,93 @@ declare const configSchema: z.ZodObject<{
147
256
  run: z.ZodString;
148
257
  interval: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
149
258
  }, "strip", z.ZodTypeAny, {
150
- name: string;
151
259
  type: "command";
260
+ name: string;
152
261
  run: string;
153
262
  interval: string;
154
263
  }, {
155
- name: string;
156
264
  type: "command";
265
+ name: string;
157
266
  run: string;
158
267
  interval: string;
159
268
  }>]>, "many">, ({
160
269
  path: string;
161
- name: string;
162
270
  type: "file";
163
- } | {
164
271
  name: string;
272
+ ndjson?: {
273
+ messageField: string;
274
+ timestampField?: string | undefined;
275
+ levelField?: string | undefined;
276
+ levelFilter?: string[] | undefined;
277
+ } | undefined;
278
+ format?: "text" | "ndjson" | undefined;
279
+ } | {
165
280
  type: "docker";
281
+ name: string;
166
282
  container: string;
167
283
  } | {
168
- name: string;
169
284
  type: "command";
285
+ name: string;
170
286
  run: string;
171
287
  interval: string;
172
288
  })[], ({
173
289
  path: string;
174
- name: string;
175
290
  type: "file";
291
+ name: string;
292
+ ndjson?: {
293
+ messageField: string;
294
+ timestampField?: string | undefined;
295
+ levelField?: string | undefined;
296
+ levelFilter?: string[] | undefined;
297
+ } | undefined;
298
+ format?: "text" | "ndjson" | undefined;
299
+ } | {
300
+ type: "docker";
301
+ name: string;
302
+ container: string;
176
303
  } | {
304
+ type: "command";
305
+ name: string;
306
+ run: string;
307
+ interval: string;
308
+ })[]>, ({
309
+ path: string;
310
+ type: "file";
177
311
  name: string;
312
+ ndjson?: {
313
+ messageField: string;
314
+ timestampField?: string | undefined;
315
+ levelField?: string | undefined;
316
+ levelFilter?: string[] | undefined;
317
+ } | undefined;
318
+ format?: "text" | "ndjson" | undefined;
319
+ } | {
178
320
  type: "docker";
321
+ name: string;
179
322
  container: string;
180
323
  } | {
324
+ type: "command";
181
325
  name: string;
326
+ run: string;
327
+ interval: string;
328
+ })[], ({
329
+ path: string;
330
+ type: "file";
331
+ name: string;
332
+ ndjson?: {
333
+ messageField: string;
334
+ timestampField?: string | undefined;
335
+ levelField?: string | undefined;
336
+ levelFilter?: string[] | undefined;
337
+ } | undefined;
338
+ format?: "text" | "ndjson" | undefined;
339
+ } | {
340
+ type: "docker";
341
+ name: string;
342
+ container: string;
343
+ } | {
182
344
  type: "command";
345
+ name: string;
183
346
  run: string;
184
347
  interval: string;
185
348
  })[]>;
@@ -189,15 +352,22 @@ declare const configSchema: z.ZodObject<{
189
352
  }, "strip", z.ZodTypeAny, {
190
353
  sources: ({
191
354
  path: string;
192
- name: string;
193
355
  type: "file";
194
- } | {
195
356
  name: string;
357
+ ndjson?: {
358
+ messageField: string;
359
+ timestampField?: string | undefined;
360
+ levelField?: string | undefined;
361
+ levelFilter?: string[] | undefined;
362
+ } | undefined;
363
+ format?: "text" | "ndjson" | undefined;
364
+ } | {
196
365
  type: "docker";
366
+ name: string;
197
367
  container: string;
198
368
  } | {
199
- name: string;
200
369
  type: "command";
370
+ name: string;
201
371
  run: string;
202
372
  interval: string;
203
373
  })[];
@@ -207,15 +377,22 @@ declare const configSchema: z.ZodObject<{
207
377
  }, {
208
378
  sources: ({
209
379
  path: string;
210
- name: string;
211
380
  type: "file";
212
- } | {
213
381
  name: string;
382
+ ndjson?: {
383
+ messageField: string;
384
+ timestampField?: string | undefined;
385
+ levelField?: string | undefined;
386
+ levelFilter?: string[] | undefined;
387
+ } | undefined;
388
+ format?: "text" | "ndjson" | undefined;
389
+ } | {
214
390
  type: "docker";
391
+ name: string;
215
392
  container: string;
216
393
  } | {
217
- name: string;
218
394
  type: "command";
395
+ name: string;
219
396
  run: string;
220
397
  interval: string;
221
398
  })[];
@@ -295,15 +472,22 @@ declare const configSchema: z.ZodObject<{
295
472
  logs: {
296
473
  sources: ({
297
474
  path: string;
298
- name: string;
299
475
  type: "file";
300
- } | {
301
476
  name: string;
477
+ ndjson?: {
478
+ messageField: string;
479
+ timestampField?: string | undefined;
480
+ levelField?: string | undefined;
481
+ levelFilter?: string[] | undefined;
482
+ } | undefined;
483
+ format?: "text" | "ndjson" | undefined;
484
+ } | {
302
485
  type: "docker";
486
+ name: string;
303
487
  container: string;
304
488
  } | {
305
- name: string;
306
489
  type: "command";
490
+ name: string;
307
491
  run: string;
308
492
  interval: string;
309
493
  })[];
@@ -349,15 +533,22 @@ declare const configSchema: z.ZodObject<{
349
533
  logs: {
350
534
  sources: ({
351
535
  path: string;
352
- name: string;
353
536
  type: "file";
354
- } | {
355
537
  name: string;
538
+ ndjson?: {
539
+ messageField: string;
540
+ timestampField?: string | undefined;
541
+ levelField?: string | undefined;
542
+ levelFilter?: string[] | undefined;
543
+ } | undefined;
544
+ format?: "text" | "ndjson" | undefined;
545
+ } | {
356
546
  type: "docker";
547
+ name: string;
357
548
  container: string;
358
549
  } | {
359
- name: string;
360
550
  type: "command";
551
+ name: string;
361
552
  run: string;
362
553
  interval: string;
363
554
  })[];
@@ -389,5 +580,5 @@ declare const configSchema: z.ZodObject<{
389
580
  } | undefined;
390
581
  }>;
391
582
  type Config = z.infer<typeof configSchema>;
392
- export { commandSourceSchema, configSchema, dockerSourceSchema, durationSchema, fileSourceSchema, logSourceSchema, patternSchema, };
583
+ export { commandSourceSchema, configSchema, dockerSourceSchema, durationSchema, fileSourceSchema, logSourceSchema, ndjsonConfigSchema, patternSchema, };
393
584
  export type { Config };
@@ -17,10 +17,18 @@ const durationSchema = z
17
17
  };
18
18
  return amount * msByUnit[unit] <= MAX_DURATION_MS;
19
19
  }, 'Duration cannot exceed 24 hours');
20
+ const ndjsonConfigSchema = z.object({
21
+ messageField: z.string().min(1),
22
+ timestampField: z.string().min(1).optional(),
23
+ levelField: z.string().min(1).optional(),
24
+ levelFilter: z.array(z.string().min(1)).optional(),
25
+ });
20
26
  const fileSourceSchema = z.object({
21
27
  name: z.string().min(1),
22
28
  type: z.literal('file'),
23
29
  path: z.string().min(1),
30
+ format: z.enum(['text', 'ndjson']).optional(),
31
+ ndjson: ndjsonConfigSchema.optional(),
24
32
  });
25
33
  const dockerSourceSchema = z.object({
26
34
  name: z.string().min(1),
@@ -59,7 +67,15 @@ const configSchema = z.object({
59
67
  .refine((sources) => {
60
68
  const names = sources.map((source) => source.name);
61
69
  return names.length === new Set(names).size;
62
- }, { message: 'Log source names must be unique' }),
70
+ }, { message: 'Log source names must be unique' })
71
+ .refine((sources) => {
72
+ return sources.every((source) => {
73
+ if (source.type === 'file' && source.format === 'ndjson') {
74
+ return source.ndjson !== undefined;
75
+ }
76
+ return true;
77
+ });
78
+ }, { message: 'ndjson config is required when format is "ndjson"' }),
63
79
  context_lines_before: z.number().int().min(0).default(10),
64
80
  context_lines_after: z.number().int().min(0).default(5),
65
81
  max_line_buffer: z.number().int().min(100).default(10000),
@@ -102,4 +118,4 @@ const configSchema = z.object({
102
118
  })
103
119
  .default({}),
104
120
  });
105
- export { commandSourceSchema, configSchema, dockerSourceSchema, durationSchema, fileSourceSchema, logSourceSchema, patternSchema, };
121
+ export { commandSourceSchema, configSchema, dockerSourceSchema, durationSchema, fileSourceSchema, logSourceSchema, ndjsonConfigSchema, patternSchema, };
@@ -9,6 +9,8 @@ export declare class FileSource implements LogSource {
9
9
  private readonly filePath;
10
10
  private readonly logger;
11
11
  private readonly emitter;
12
+ private readonly format;
13
+ private readonly ndjsonConfig;
12
14
  private watcher;
13
15
  private position;
14
16
  private partialLine;
@@ -26,5 +28,6 @@ export declare class FileSource implements LogSource {
26
28
  private readLoop;
27
29
  private readNewLines;
28
30
  private processChunk;
31
+ private handleLine;
29
32
  }
30
33
  export {};
@@ -3,11 +3,14 @@ import path from 'node:path';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import chokidar from 'chokidar';
5
5
  import { Logger } from '../../utils/logger.js';
6
+ import { parseNdjsonLine } from './ndjson.js';
6
7
  export class FileSource {
7
8
  config;
8
9
  filePath;
9
10
  logger;
10
11
  emitter;
12
+ format;
13
+ ndjsonConfig;
11
14
  watcher = null;
12
15
  position = 0;
13
16
  partialLine = '';
@@ -23,6 +26,8 @@ export class FileSource {
23
26
  this.logger =
24
27
  options?.logger ?? new Logger({ terminalEnabled: false, verbosity: 'normal' });
25
28
  this.emitter = new EventEmitter();
29
+ this.format = config.format ?? 'text';
30
+ this.ndjsonConfig = config.ndjson ?? null;
26
31
  }
27
32
  async start() {
28
33
  if (this.watcher) {
@@ -164,12 +169,40 @@ export class FileSource {
164
169
  const lines = combined.split('\n');
165
170
  this.partialLine = lines.pop() ?? '';
166
171
  for (const rawLine of lines) {
167
- const line = rawLine.replace(/\r$/, '');
172
+ this.handleLine(rawLine.replace(/\r$/, ''));
173
+ }
174
+ }
175
+ handleLine(line) {
176
+ if (this.format !== 'ndjson' || !this.ndjsonConfig) {
168
177
  this.emitter.emit('line', {
169
178
  source: this.config.name,
170
179
  line,
171
180
  timestamp: new Date(),
172
181
  });
182
+ return;
183
+ }
184
+ const result = parseNdjsonLine(line, this.ndjsonConfig);
185
+ if (result.success) {
186
+ this.emitter.emit('line', {
187
+ source: this.config.name,
188
+ line: result.data.message,
189
+ timestamp: result.data.timestamp,
190
+ });
191
+ return;
192
+ }
193
+ // Handle different failure reasons
194
+ if (result.reason === 'filtered') {
195
+ // Line was filtered by level - silently skip
196
+ return;
197
+ }
198
+ if (result.reason === 'missing_message') {
199
+ this.logger.debug(`NDJSON line missing message field "${this.ndjsonConfig.messageField}": ${line.slice(0, 100)}`);
173
200
  }
201
+ // For parse errors or missing message, fall back to emitting raw line
202
+ this.emitter.emit('line', {
203
+ source: this.config.name,
204
+ line,
205
+ timestamp: new Date(),
206
+ });
174
207
  }
175
208
  }
@@ -0,0 +1,16 @@
1
+ import type { NdjsonConfig } from './types.js';
2
+ export type ParsedNdjsonLine = {
3
+ message: string;
4
+ timestamp: Date;
5
+ };
6
+ export type NdjsonParseResult = {
7
+ success: true;
8
+ data: ParsedNdjsonLine;
9
+ } | {
10
+ success: false;
11
+ reason: 'parse_error' | 'missing_message' | 'filtered';
12
+ };
13
+ export declare function getNestedValue(obj: unknown, path: string): unknown;
14
+ export declare function parseTimestamp(value: unknown): Date | null;
15
+ export declare function shouldProcessLevel(value: unknown, levelFilter: string[] | undefined): boolean;
16
+ export declare function parseNdjsonLine(line: string, config: NdjsonConfig): NdjsonParseResult;
@@ -0,0 +1,108 @@
1
+ export function getNestedValue(obj, path) {
2
+ const parts = path.split('.');
3
+ let current = obj;
4
+ for (const part of parts) {
5
+ if (current === null || current === undefined) {
6
+ return undefined;
7
+ }
8
+ if (typeof current !== 'object') {
9
+ return undefined;
10
+ }
11
+ current = current[part];
12
+ }
13
+ return current;
14
+ }
15
+ export function parseTimestamp(value) {
16
+ if (value === null || value === undefined) {
17
+ return null;
18
+ }
19
+ if (typeof value === 'string') {
20
+ const date = new Date(value);
21
+ if (!Number.isNaN(date.getTime())) {
22
+ return date;
23
+ }
24
+ return null;
25
+ }
26
+ if (typeof value === 'number') {
27
+ // Handle Unix timestamps in seconds or milliseconds
28
+ // If the number is less than 10^12, treat as seconds; otherwise as milliseconds
29
+ const MS_THRESHOLD = 1e12;
30
+ const timestamp = value < MS_THRESHOLD ? value * 1000 : value;
31
+ const date = new Date(timestamp);
32
+ if (!Number.isNaN(date.getTime())) {
33
+ return date;
34
+ }
35
+ return null;
36
+ }
37
+ return null;
38
+ }
39
+ export function shouldProcessLevel(value, levelFilter) {
40
+ if (!levelFilter || levelFilter.length === 0) {
41
+ return true;
42
+ }
43
+ if (value === null || value === undefined) {
44
+ // If no level is present but filter is configured, skip the line
45
+ return false;
46
+ }
47
+ // Handle string levels (most common)
48
+ if (typeof value === 'string') {
49
+ const lowerValue = value.toLowerCase();
50
+ return levelFilter.some((filter) => filter.toLowerCase() === lowerValue);
51
+ }
52
+ // Handle numeric levels (Bunyan style: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal)
53
+ if (typeof value === 'number') {
54
+ const bunyanLevels = {
55
+ 10: 'trace',
56
+ 20: 'debug',
57
+ 30: 'info',
58
+ 40: 'warn',
59
+ 50: 'error',
60
+ 60: 'fatal',
61
+ };
62
+ const levelName = bunyanLevels[value];
63
+ if (levelName) {
64
+ return levelFilter.some((filter) => filter.toLowerCase() === levelName.toLowerCase());
65
+ }
66
+ // Unknown numeric level - don't match
67
+ return false;
68
+ }
69
+ return false;
70
+ }
71
+ export function parseNdjsonLine(line, config) {
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(line);
75
+ }
76
+ catch {
77
+ return { success: false, reason: 'parse_error' };
78
+ }
79
+ if (parsed === null || typeof parsed !== 'object') {
80
+ return { success: false, reason: 'parse_error' };
81
+ }
82
+ // Check level filter first (before extracting message)
83
+ if (config.levelField) {
84
+ const levelValue = getNestedValue(parsed, config.levelField);
85
+ if (!shouldProcessLevel(levelValue, config.levelFilter)) {
86
+ return { success: false, reason: 'filtered' };
87
+ }
88
+ }
89
+ // Extract message
90
+ const messageValue = getNestedValue(parsed, config.messageField);
91
+ if (messageValue === null || messageValue === undefined) {
92
+ return { success: false, reason: 'missing_message' };
93
+ }
94
+ const message = String(messageValue);
95
+ // Extract timestamp
96
+ let timestamp = new Date();
97
+ if (config.timestampField) {
98
+ const timestampValue = getNestedValue(parsed, config.timestampField);
99
+ const parsedTimestamp = parseTimestamp(timestampValue);
100
+ if (parsedTimestamp) {
101
+ timestamp = parsedTimestamp;
102
+ }
103
+ }
104
+ return {
105
+ success: true,
106
+ data: { message, timestamp },
107
+ };
108
+ }
@@ -3,10 +3,18 @@ export type LogEvent = {
3
3
  line: string;
4
4
  timestamp: Date;
5
5
  };
6
+ export type NdjsonConfig = {
7
+ messageField: string;
8
+ timestampField?: string;
9
+ levelField?: string;
10
+ levelFilter?: string[];
11
+ };
6
12
  export type FileSourceConfig = {
7
13
  name: string;
8
14
  type: 'file';
9
15
  path: string;
16
+ format?: 'text' | 'ndjson';
17
+ ndjson?: NdjsonConfig;
10
18
  };
11
19
  export type DockerSourceConfig = {
12
20
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchfix",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI tool that watches logs, detects errors, and dispatches AI agents to fix them",
5
5
  "keywords": [
6
6
  "cli",