@blogic-cz/agent-tools 0.8.1 → 0.8.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -456,12 +456,7 @@ export function transformDescribe(textOutput: string): string {
456
456
  return outputLines.join("\n").trim();
457
457
  }
458
458
 
459
- export function transformLogs(textOutput: string): string {
460
- return deduplicateLines(textOutput, {
461
- normalizeTimestamps: true,
462
- normalizeUUIDs: true,
463
- });
464
- }
459
+ export { transformLogOutput as transformLogs } from "#logs/transformers";
465
460
 
466
461
  export function transformGenericKubectl(
467
462
  textOutput: string,
@@ -66,9 +66,28 @@ function toText(value: unknown): string | undefined {
66
66
  return undefined;
67
67
  }
68
68
 
69
- function firstMappedField(obj: unknown, fields: ReadonlyArray<string>): string | undefined {
69
+ function formatTimestamp(value: unknown): string | undefined {
70
+ if (typeof value === "number" && value > 1_000_000_000_000) {
71
+ return new Date(value).toISOString();
72
+ }
73
+ if (typeof value === "number" && value > 1_000_000_000) {
74
+ return new Date(value * 1000).toISOString();
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ function firstMappedField(
80
+ obj: unknown,
81
+ fields: ReadonlyArray<string>,
82
+ isTimestamp = false,
83
+ ): string | undefined {
70
84
  for (const field of fields) {
71
- const value = toText(getByPath(obj, field));
85
+ const raw = getByPath(obj, field);
86
+ if (isTimestamp) {
87
+ const formatted = formatTimestamp(raw);
88
+ if (formatted) return formatted;
89
+ }
90
+ const value = toText(raw);
72
91
  if (value) {
73
92
  return value;
74
93
  }
@@ -84,6 +103,15 @@ function normalizeLevel(rawLevel: string | undefined): LogLevel {
84
103
  return "INFO";
85
104
  }
86
105
 
106
+ // Pino numeric levels: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal
107
+ const numericLevel = Number(normalized);
108
+ if (Number.isFinite(numericLevel)) {
109
+ if (numericLevel >= 50) return "ERROR";
110
+ if (numericLevel >= 40) return "WARN";
111
+ if (numericLevel >= 30) return "INFO";
112
+ return "DEBUG";
113
+ }
114
+
87
115
  if (normalized === "error" || normalized === "err" || normalized === "fatal") {
88
116
  return "ERROR";
89
117
  }
@@ -114,7 +142,7 @@ function formatLine({ level, timestamp, message }: ParsedLogLine): string {
114
142
  function parseJsonLogLine(line: string, originalIndex: number): ParsedLogLine {
115
143
  try {
116
144
  const parsed = JSON.parse(line) as unknown;
117
- const timestamp = firstMappedField(parsed, TIMESTAMP_FIELDS);
145
+ const timestamp = firstMappedField(parsed, TIMESTAMP_FIELDS, true);
118
146
  const level = normalizeLevel(firstMappedField(parsed, LEVEL_FIELDS));
119
147
  const message =
120
148
  firstMappedField(parsed, MESSAGE_FIELDS) ??
@@ -24,3 +24,5 @@ export {
24
24
  stripEmptyColumns,
25
25
  truncateRows,
26
26
  } from "./transform";
27
+
28
+ export { transformLogOutput } from "./log-transform";
@@ -0,0 +1,240 @@
1
+ import { deduplicateLines } from "./transform";
2
+
3
+ type LogLevel = "ERROR" | "WARN" | "INFO" | "DEBUG";
4
+
5
+ type ParsedLogLine = {
6
+ level: LogLevel;
7
+ timestamp?: string;
8
+ message: string;
9
+ originalIndex: number;
10
+ };
11
+
12
+ const TIMESTAMP_FIELDS = ["timestamp", "ts", "time", "@timestamp", "datetime"] as const;
13
+ const LEVEL_FIELDS = ["level", "severity", "lvl", "log.level"] as const;
14
+ const MESSAGE_FIELDS = ["message", "msg", "log", "text"] as const;
15
+ const ERROR_FIELDS = ["error", "err", "stack", "exception", "error.message"] as const;
16
+
17
+ const LEVEL_ORDER: ReadonlyArray<LogLevel> = ["ERROR", "WARN", "INFO", "DEBUG"];
18
+
19
+ function getByPath(obj: unknown, path: string): unknown {
20
+ if (typeof obj !== "object" || obj === null) {
21
+ return undefined;
22
+ }
23
+
24
+ if (!path.includes(".")) {
25
+ return (obj as Record<string, unknown>)[path];
26
+ }
27
+
28
+ const parts = path.split(".");
29
+ let current: unknown = obj;
30
+
31
+ for (const part of parts) {
32
+ if (typeof current !== "object" || current === null) {
33
+ return undefined;
34
+ }
35
+ current = (current as Record<string, unknown>)[part];
36
+ }
37
+
38
+ return current;
39
+ }
40
+
41
+ function toText(value: unknown): string | undefined {
42
+ if (typeof value === "string") {
43
+ const trimmed = value.trim();
44
+ return trimmed.length > 0 ? trimmed : undefined;
45
+ }
46
+
47
+ if (typeof value === "number" || typeof value === "boolean") {
48
+ return String(value);
49
+ }
50
+
51
+ if (value && typeof value === "object") {
52
+ if ("message" in value) {
53
+ const nestedMessage = toText((value as Record<string, unknown>).message);
54
+ if (nestedMessage) {
55
+ return nestedMessage;
56
+ }
57
+ }
58
+
59
+ try {
60
+ return JSON.stringify(value);
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ return undefined;
67
+ }
68
+
69
+ function formatTimestamp(value: unknown): string | undefined {
70
+ if (typeof value === "number" && value > 1_000_000_000_000) {
71
+ return new Date(value).toISOString();
72
+ }
73
+ if (typeof value === "number" && value > 1_000_000_000) {
74
+ return new Date(value * 1000).toISOString();
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ function firstMappedField(
80
+ obj: unknown,
81
+ fields: ReadonlyArray<string>,
82
+ isTimestamp = false,
83
+ ): string | undefined {
84
+ for (const field of fields) {
85
+ const raw = getByPath(obj, field);
86
+ if (isTimestamp) {
87
+ const formatted = formatTimestamp(raw);
88
+ if (formatted) return formatted;
89
+ }
90
+ const value = toText(raw);
91
+ if (value) {
92
+ return value;
93
+ }
94
+ }
95
+
96
+ return undefined;
97
+ }
98
+
99
+ // Pino numeric levels: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal
100
+ function normalizeLevel(rawLevel: string | undefined): LogLevel {
101
+ const normalized = rawLevel?.trim().toLowerCase();
102
+
103
+ if (!normalized) {
104
+ return "INFO";
105
+ }
106
+
107
+ const numericLevel = Number(normalized);
108
+ if (Number.isFinite(numericLevel)) {
109
+ if (numericLevel >= 50) return "ERROR";
110
+ if (numericLevel >= 40) return "WARN";
111
+ if (numericLevel >= 30) return "INFO";
112
+ return "DEBUG";
113
+ }
114
+
115
+ if (normalized === "error" || normalized === "err" || normalized === "fatal") {
116
+ return "ERROR";
117
+ }
118
+
119
+ if (normalized === "warn" || normalized === "warning") {
120
+ return "WARN";
121
+ }
122
+
123
+ if (normalized === "info") {
124
+ return "INFO";
125
+ }
126
+
127
+ if (normalized === "debug" || normalized === "trace") {
128
+ return "DEBUG";
129
+ }
130
+
131
+ return "INFO";
132
+ }
133
+
134
+ function formatLine({ level, timestamp, message }: ParsedLogLine): string {
135
+ if (timestamp) {
136
+ return `[${timestamp}] [${level}] ${message}`;
137
+ }
138
+
139
+ return `[${level}] ${message}`;
140
+ }
141
+
142
+ function parseJsonLogLine(line: string, originalIndex: number): ParsedLogLine {
143
+ try {
144
+ const parsed = JSON.parse(line) as unknown;
145
+ const timestamp = firstMappedField(parsed, TIMESTAMP_FIELDS, true);
146
+ const level = normalizeLevel(firstMappedField(parsed, LEVEL_FIELDS));
147
+ const message =
148
+ firstMappedField(parsed, MESSAGE_FIELDS) ??
149
+ firstMappedField(parsed, ERROR_FIELDS) ??
150
+ line.trim();
151
+
152
+ return {
153
+ level,
154
+ timestamp,
155
+ message,
156
+ originalIndex,
157
+ };
158
+ } catch {
159
+ return {
160
+ level: "INFO",
161
+ message: line.trim(),
162
+ originalIndex,
163
+ };
164
+ }
165
+ }
166
+
167
+ function isJsonMode(lines: ReadonlyArray<string>): boolean {
168
+ const firstNonEmpty = lines.find((line) => line.trim().length > 0);
169
+ if (!firstNonEmpty) {
170
+ return false;
171
+ }
172
+
173
+ try {
174
+ const parsed = JSON.parse(firstNonEmpty) as unknown;
175
+ return typeof parsed === "object" && parsed !== null;
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ function groupHeader(level: LogLevel, count: number): string {
182
+ switch (level) {
183
+ case "ERROR":
184
+ return `--- errors (${count}) ---`;
185
+ case "WARN":
186
+ return `--- warnings (${count}) ---`;
187
+ case "INFO":
188
+ return `--- info (${count}) ---`;
189
+ case "DEBUG":
190
+ return `--- debug (${count}) ---`;
191
+ }
192
+ }
193
+
194
+ function transformJsonLines(lines: ReadonlyArray<string>): string {
195
+ const parsedLines = lines
196
+ .map((line, originalIndex) => ({ line, originalIndex }))
197
+ .filter(({ line }) => line.trim().length > 0)
198
+ .map(({ line, originalIndex }) => parseJsonLogLine(line, originalIndex));
199
+
200
+ if (parsedLines.length === 0) {
201
+ return "";
202
+ }
203
+
204
+ const ordered = LEVEL_ORDER.flatMap((level) =>
205
+ parsedLines
206
+ .filter((entry) => entry.level === level)
207
+ .sort((a, b) => a.originalIndex - b.originalIndex),
208
+ );
209
+
210
+ const output: Array<string> = [];
211
+
212
+ for (const level of LEVEL_ORDER) {
213
+ const group = ordered.filter((entry) => entry.level === level);
214
+ if (group.length === 0) {
215
+ continue;
216
+ }
217
+
218
+ output.push(groupHeader(level, group.length));
219
+ output.push(...group.map(formatLine));
220
+ }
221
+
222
+ return deduplicateLines(output.join("\n"));
223
+ }
224
+
225
+ export function transformLogOutput(rawOutput: string): string {
226
+ if (rawOutput.length === 0) {
227
+ return "";
228
+ }
229
+
230
+ const lines = rawOutput.split(/\r?\n/);
231
+
232
+ if (isJsonMode(lines)) {
233
+ return transformJsonLines(lines);
234
+ }
235
+
236
+ return deduplicateLines(rawOutput, {
237
+ normalizeTimestamps: true,
238
+ normalizeUUIDs: true,
239
+ });
240
+ }