@blogic-cz/agent-tools 0.7.1 → 0.8.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.
- package/package.json +1 -1
- package/src/az-tool/build.ts +7 -3
- package/src/az-tool/service.ts +9 -1
- package/src/az-tool/transformers.ts +127 -0
- package/src/db-tool/service.ts +16 -4
- package/src/db-tool/transformers.ts +38 -0
- package/src/db-tool/types.ts +2 -0
- package/src/k8s-tool/index.ts +44 -12
- package/src/k8s-tool/transformers.ts +508 -0
- package/src/k8s-tool/types.ts +1 -1
- package/src/logs-tool/service.ts +13 -10
- package/src/logs-tool/transformers.ts +212 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/transform.ts +320 -0
package/package.json
CHANGED
package/src/az-tool/build.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { BuildJob, BuildLogs, BuildTimeline, JobSummary, PipelineRun } from
|
|
|
4
4
|
|
|
5
5
|
import { AzParseError } from "./errors";
|
|
6
6
|
import { AzService } from "./service";
|
|
7
|
+
import { transformBuildLogContent, transformTimeline } from "./transformers";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Get build timeline with all records (jobs, stages, tasks, etc.)
|
|
@@ -59,7 +60,10 @@ export const getBuildTimeline = Effect.fn("Build.getBuildTimeline")(function* (b
|
|
|
59
60
|
),
|
|
60
61
|
);
|
|
61
62
|
|
|
62
|
-
return
|
|
63
|
+
return {
|
|
64
|
+
...parsed,
|
|
65
|
+
records: transformTimeline(parsed.records as BuildJob[]),
|
|
66
|
+
} as BuildTimeline;
|
|
63
67
|
});
|
|
64
68
|
|
|
65
69
|
/**
|
|
@@ -151,14 +155,14 @@ export const getBuildLogContent = Effect.fn("Build.getBuildLogContent")(function
|
|
|
151
155
|
);
|
|
152
156
|
|
|
153
157
|
if (typeof parsed === "string") {
|
|
154
|
-
return parsed;
|
|
158
|
+
return transformBuildLogContent(parsed);
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
if (!Array.isArray(parsed.value)) {
|
|
158
162
|
return "";
|
|
159
163
|
}
|
|
160
164
|
|
|
161
|
-
return parsed.value.join("\n");
|
|
165
|
+
return transformBuildLogContent(parsed.value.join("\n"));
|
|
162
166
|
});
|
|
163
167
|
|
|
164
168
|
/**
|
package/src/az-tool/service.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { AzureConfig } from "#config/types";
|
|
|
7
7
|
import { DIRECT_AZ_COMMANDS, STANDALONE_AZ_COMMANDS } from "./config";
|
|
8
8
|
import { AzSecurityError, AzCommandError, AzTimeoutError, AzParseError } from "./errors";
|
|
9
9
|
import { isCommandAllowed, isInvokeAllowed } from "./security";
|
|
10
|
+
import { transformCmdOutput } from "./transformers";
|
|
10
11
|
import { ConfigService, getToolConfig } from "#config";
|
|
11
12
|
|
|
12
13
|
export class AzService extends ServiceMap.Service<
|
|
@@ -144,7 +145,14 @@ export class AzService extends ServiceMap.Service<
|
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
const output = result.stdout.trim();
|
|
149
|
+
const transformed = transformCmdOutput(output);
|
|
150
|
+
|
|
151
|
+
if (typeof transformed === "string") {
|
|
152
|
+
return transformed;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return JSON.stringify(transformed);
|
|
148
156
|
});
|
|
149
157
|
|
|
150
158
|
const runInvoke = Effect.fn("AzService.runInvoke")(function* (params: InvokeParams) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { deduplicateLines, parseTextTable } from "#shared";
|
|
2
|
+
|
|
3
|
+
import type { BuildJob } from "./types";
|
|
4
|
+
|
|
5
|
+
type ParsedTable = {
|
|
6
|
+
headers: string[];
|
|
7
|
+
rows: Record<string, string>[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const TIMESTAMP_PREFIX_PATTERN = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+/;
|
|
11
|
+
|
|
12
|
+
const TIMESTAMP_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+$/;
|
|
13
|
+
|
|
14
|
+
const ERROR_MARKER_PATTERN = /(##\[error\]|error:|Error:|ERROR|FAILED)/;
|
|
15
|
+
|
|
16
|
+
function stripTimestampPrefix(line: string): string {
|
|
17
|
+
return line.replace(TIMESTAMP_PREFIX_PATTERN, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isNoiseLine(line: string): boolean {
|
|
21
|
+
const trimmed = stripTimestampPrefix(line).trimStart();
|
|
22
|
+
|
|
23
|
+
if (trimmed.startsWith("##[section]")) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (trimmed.startsWith("##[debug]")) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (/^Downloading\b/i.test(trimmed)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return TIMESTAMP_ONLY_PATTERN.test(line);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function looksLikeTextTable(text: string): boolean {
|
|
39
|
+
const firstLine = text.split(/\r?\n/, 1)[0]?.trimEnd() ?? "";
|
|
40
|
+
|
|
41
|
+
if (firstLine.length === 0) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return /^[A-Z][A-Z0-9_ -]*(\s{2,}[A-Z][A-Z0-9_ -]*)+$/.test(firstLine);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function transformBuildLogContent(rawLog: string): string {
|
|
49
|
+
if (rawLog.length === 0) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const hadTrailingNewline = rawLog.endsWith("\n");
|
|
54
|
+
|
|
55
|
+
const deduplicated = deduplicateLines(rawLog, {
|
|
56
|
+
normalizeTimestamps: true,
|
|
57
|
+
normalizeUUIDs: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const keptLines = deduplicated
|
|
61
|
+
.split(/\r?\n/)
|
|
62
|
+
.map((line) => line.trimEnd())
|
|
63
|
+
.filter((line) => line.length > 0)
|
|
64
|
+
.filter((line) => !isNoiseLine(line));
|
|
65
|
+
|
|
66
|
+
const errorLines: string[] = [];
|
|
67
|
+
const nonErrorLines: string[] = [];
|
|
68
|
+
|
|
69
|
+
for (const line of keptLines) {
|
|
70
|
+
if (ERROR_MARKER_PATTERN.test(line)) {
|
|
71
|
+
errorLines.push(line);
|
|
72
|
+
} else {
|
|
73
|
+
nonErrorLines.push(line);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const transformed = [...errorLines, ...nonErrorLines].join("\n");
|
|
78
|
+
|
|
79
|
+
if (hadTrailingNewline && transformed.length > 0) {
|
|
80
|
+
return `${transformed}\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return transformed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function transformCmdOutput(
|
|
87
|
+
rawOutput: string,
|
|
88
|
+
): string | Record<string, unknown> | unknown[] | ParsedTable {
|
|
89
|
+
if (rawOutput.length === 0) {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const trimmed = rawOutput.trim();
|
|
94
|
+
|
|
95
|
+
const parsedJson = (() => {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(trimmed) as unknown;
|
|
98
|
+
} catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
if (typeof parsedJson === "object" && parsedJson !== null) {
|
|
104
|
+
return parsedJson as Record<string, unknown> | unknown[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (looksLikeTextTable(trimmed)) {
|
|
108
|
+
return parseTextTable(trimmed);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lineCount = trimmed.split(/\r?\n/).length;
|
|
112
|
+
if (lineCount > 50) {
|
|
113
|
+
return deduplicateLines(trimmed);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return trimmed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function transformTimeline(records: BuildJob[]): BuildJob[] {
|
|
120
|
+
return records.filter((record) => {
|
|
121
|
+
if (record.type === "Stage" || record.type === "Job") {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (record.errorCount ?? 0) > 0 || (record.warningCount ?? 0) > 0;
|
|
126
|
+
});
|
|
127
|
+
}
|
package/src/db-tool/service.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./errors";
|
|
15
15
|
import { getColumns, getRelationships, getTableNames } from "./schema";
|
|
16
16
|
import { detectSchemaError, isValidTableName, isMutationQuery } from "./security";
|
|
17
|
+
import { transformQueryResult } from "./transformers";
|
|
17
18
|
|
|
18
19
|
const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1"]);
|
|
19
20
|
|
|
@@ -323,6 +324,7 @@ export class DbService extends ServiceMap.Service<
|
|
|
323
324
|
sql: string,
|
|
324
325
|
password: string,
|
|
325
326
|
startTimeMs: number,
|
|
327
|
+
applyTransform = false,
|
|
326
328
|
) {
|
|
327
329
|
const wrappedSql = `SELECT json_agg(t) FROM (${sql}) t;`;
|
|
328
330
|
const command = buildPsqlCommand(config, wrappedSql, password, true);
|
|
@@ -376,7 +378,7 @@ export class DbService extends ServiceMap.Service<
|
|
|
376
378
|
};
|
|
377
379
|
}
|
|
378
380
|
|
|
379
|
-
const
|
|
381
|
+
const rawData = yield* Effect.try({
|
|
380
382
|
try: () => JSON.parse(trimmedOutput) as Record<string, unknown>[],
|
|
381
383
|
catch: () =>
|
|
382
384
|
new DbParseError({
|
|
@@ -385,11 +387,21 @@ export class DbService extends ServiceMap.Service<
|
|
|
385
387
|
}),
|
|
386
388
|
});
|
|
387
389
|
|
|
390
|
+
const transformed = applyTransform
|
|
391
|
+
? transformQueryResult(rawData)
|
|
392
|
+
: {
|
|
393
|
+
data: rawData,
|
|
394
|
+
showing: rawData.length,
|
|
395
|
+
truncated: false,
|
|
396
|
+
total: rawData.length,
|
|
397
|
+
};
|
|
398
|
+
|
|
388
399
|
return {
|
|
389
400
|
success: true,
|
|
390
|
-
data,
|
|
391
|
-
rowCount:
|
|
401
|
+
data: transformed.data,
|
|
402
|
+
rowCount: transformed.showing,
|
|
392
403
|
executionTimeMs: Number(endTime) - startTimeMs,
|
|
404
|
+
...(transformed.truncated ? { truncated: true, total: transformed.total } : {}),
|
|
393
405
|
};
|
|
394
406
|
});
|
|
395
407
|
|
|
@@ -558,7 +570,7 @@ export class DbService extends ServiceMap.Service<
|
|
|
558
570
|
|
|
559
571
|
const queryEffect = mutation
|
|
560
572
|
? executeMutationQuery(config, sql, password, Number(startTimeMs))
|
|
561
|
-
: executeSelectQuery(config, sql, password, Number(startTimeMs));
|
|
573
|
+
: executeSelectQuery(config, sql, password, Number(startTimeMs), true);
|
|
562
574
|
|
|
563
575
|
return yield* runQueryWithOptionalTunnel(config, queryEffect);
|
|
564
576
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { stripEmptyColumns, truncateRows } from "#shared";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_ROW_LIMIT = 50;
|
|
4
|
+
export const MAX_VALUE_LENGTH = 200;
|
|
5
|
+
|
|
6
|
+
export type TransformResult = {
|
|
7
|
+
data: Record<string, unknown>[];
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
total: number;
|
|
10
|
+
showing: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function truncateValue(value: unknown): unknown {
|
|
14
|
+
if (typeof value !== "string") {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (value.length <= MAX_VALUE_LENGTH) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `${value.slice(0, MAX_VALUE_LENGTH)}...`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function transformQueryResult(data: Record<string, unknown>[]): TransformResult {
|
|
26
|
+
const withoutEmptyColumns = stripEmptyColumns(data);
|
|
27
|
+
const truncatedValues = withoutEmptyColumns.map((record) =>
|
|
28
|
+
Object.fromEntries(Object.entries(record).map(([key, value]) => [key, truncateValue(value)])),
|
|
29
|
+
);
|
|
30
|
+
const { rows, truncated, total, showing } = truncateRows(truncatedValues, DEFAULT_ROW_LIMIT);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
data: rows,
|
|
34
|
+
truncated,
|
|
35
|
+
total,
|
|
36
|
+
showing,
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/db-tool/types.ts
CHANGED
package/src/k8s-tool/index.ts
CHANGED
|
@@ -11,6 +11,13 @@ import { K8sService, K8sServiceLayer } from "./service";
|
|
|
11
11
|
import { ConfigService, ConfigServiceLayer, getDefaultEnvironment, getToolConfig } from "#config";
|
|
12
12
|
import type { K8sConfig } from "#config";
|
|
13
13
|
import { K8sContextError } from "./errors";
|
|
14
|
+
import {
|
|
15
|
+
transformDescribe,
|
|
16
|
+
transformGenericKubectl,
|
|
17
|
+
transformLogs,
|
|
18
|
+
transformPods,
|
|
19
|
+
transformTop,
|
|
20
|
+
} from "./transformers";
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* Resolve environment from explicit --env flag, config defaultEnvironment, or fail with hint.
|
|
@@ -54,7 +61,7 @@ type CommonK8sCommandOptions = {
|
|
|
54
61
|
readonly profile: Option.Option<string>;
|
|
55
62
|
};
|
|
56
63
|
|
|
57
|
-
const
|
|
64
|
+
const executeK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
58
65
|
Effect.gen(function* () {
|
|
59
66
|
const config = yield* ConfigService;
|
|
60
67
|
const profileName = Option.getOrUndefined(options.profile);
|
|
@@ -69,8 +76,7 @@ const runK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
|
69
76
|
"echo '{ kubernetes: { default: { clusterId: \"my-cluster\" } } }' > agent-tools.json5",
|
|
70
77
|
executionTimeMs: 0,
|
|
71
78
|
};
|
|
72
|
-
|
|
73
|
-
return;
|
|
79
|
+
return result;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
const resolvedEnv = yield* resolveEnv(options.env, config);
|
|
@@ -126,7 +132,24 @@ const runK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
|
126
132
|
}),
|
|
127
133
|
);
|
|
128
134
|
|
|
129
|
-
|
|
135
|
+
return { ...result, environment: resolvedEnv };
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const runK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
139
|
+
Effect.gen(function* () {
|
|
140
|
+
const result = yield* executeK8sCommand(command, options);
|
|
141
|
+
yield* Console.log(formatOutput(result, options.format));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const logTransformedResult = (
|
|
145
|
+
result: CommandResult,
|
|
146
|
+
format: CommonK8sCommandOptions["format"],
|
|
147
|
+
transform: (output: string) => string | Record<string, unknown>,
|
|
148
|
+
) =>
|
|
149
|
+
Effect.gen(function* () {
|
|
150
|
+
const transformedResult =
|
|
151
|
+
typeof result.output === "string" ? { ...result, output: transform(result.output) } : result;
|
|
152
|
+
yield* Console.log(formatOutput(transformedResult, format));
|
|
130
153
|
});
|
|
131
154
|
|
|
132
155
|
const commonFlags = {
|
|
@@ -178,7 +201,13 @@ const kubectlCommand = Command.make(
|
|
|
178
201
|
Flag.withDescription('kubectl command (without "kubectl" prefix)'),
|
|
179
202
|
),
|
|
180
203
|
},
|
|
181
|
-
({ cmd, dryRun, env, format, profile }) =>
|
|
204
|
+
({ cmd, dryRun, env, format, profile }) =>
|
|
205
|
+
Effect.gen(function* () {
|
|
206
|
+
const result = yield* executeK8sCommand(cmd, { dryRun, env, format, profile });
|
|
207
|
+
return yield* logTransformedResult(result, format, (output) =>
|
|
208
|
+
transformGenericKubectl(output, cmd),
|
|
209
|
+
);
|
|
210
|
+
}),
|
|
182
211
|
).pipe(
|
|
183
212
|
Command.withDescription(
|
|
184
213
|
`Kubernetes CLI Tool for Coding Agents
|
|
@@ -247,18 +276,18 @@ const podsCommand = Command.make(
|
|
|
247
276
|
Flag.withDefault(false),
|
|
248
277
|
),
|
|
249
278
|
},
|
|
250
|
-
({ dryRun, env, format, label, namespace, profile
|
|
279
|
+
({ dryRun, env, format, label, namespace, profile }) =>
|
|
251
280
|
Effect.gen(function* () {
|
|
252
281
|
const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
|
|
253
|
-
const command = buildKubectlCommand("get pods", [
|
|
282
|
+
const command = buildKubectlCommand("get pods -o json", [
|
|
254
283
|
Option.match(label, {
|
|
255
284
|
onNone: () => "",
|
|
256
285
|
onSome: (value) => `-l ${value}`,
|
|
257
286
|
}),
|
|
258
287
|
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
259
|
-
wide ? "-o wide" : "",
|
|
260
288
|
]);
|
|
261
|
-
|
|
289
|
+
const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
|
|
290
|
+
return yield* logTransformedResult(result, format, transformPods);
|
|
262
291
|
}),
|
|
263
292
|
).pipe(Command.withDescription("List pods (get pods) with optional namespace/label/wide output"));
|
|
264
293
|
|
|
@@ -297,7 +326,8 @@ const logsCommand = Command.make(
|
|
|
297
326
|
}),
|
|
298
327
|
follow ? "-f" : "",
|
|
299
328
|
]);
|
|
300
|
-
|
|
329
|
+
const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
|
|
330
|
+
return yield* logTransformedResult(result, format, transformLogs);
|
|
301
331
|
}),
|
|
302
332
|
).pipe(Command.withDescription("Fetch pod logs with tail/follow/container selectors"));
|
|
303
333
|
|
|
@@ -320,7 +350,8 @@ const describeCommand = Command.make(
|
|
|
320
350
|
const command = buildKubectlCommand(`describe ${resource} ${name}`, [
|
|
321
351
|
resolvedNamespace ? `-n ${resolvedNamespace}` : "",
|
|
322
352
|
]);
|
|
323
|
-
|
|
353
|
+
const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
|
|
354
|
+
return yield* logTransformedResult(result, format, transformDescribe);
|
|
324
355
|
}),
|
|
325
356
|
).pipe(Command.withDescription("Describe a Kubernetes resource by type and name"));
|
|
326
357
|
|
|
@@ -379,7 +410,8 @@ const topCommand = Command.make(
|
|
|
379
410
|
onSome: (value) => `--sort-by=${value}`,
|
|
380
411
|
}),
|
|
381
412
|
]);
|
|
382
|
-
|
|
413
|
+
const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
|
|
414
|
+
return yield* logTransformedResult(result, format, transformTop);
|
|
383
415
|
}),
|
|
384
416
|
).pipe(Command.withDescription("Show pod CPU/memory usage (kubectl top pod)"));
|
|
385
417
|
|