@blogic-cz/agent-tools 1.5.3 → 1.5.5

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 (51) hide show
  1. package/README.md +7 -6
  2. package/dist/audit-tool/main.d.ts +3 -0
  3. package/dist/audit-tool/main.d.ts.map +1 -0
  4. package/dist/az-tool/main.d.ts +3 -0
  5. package/dist/az-tool/main.d.ts.map +1 -0
  6. package/dist/azdo-tool/main.d.ts +3 -0
  7. package/dist/azdo-tool/main.d.ts.map +1 -0
  8. package/dist/credential-guard/index.d.ts.map +1 -1
  9. package/dist/db-tool/main.d.ts +3 -0
  10. package/dist/db-tool/main.d.ts.map +1 -0
  11. package/dist/gh-tool/main.d.ts +3 -0
  12. package/dist/gh-tool/main.d.ts.map +1 -0
  13. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  14. package/dist/gh-tool/workflow.d.ts.map +1 -1
  15. package/dist/k8s-tool/main.d.ts +3 -0
  16. package/dist/k8s-tool/main.d.ts.map +1 -0
  17. package/dist/logs-tool/index.d.ts +1 -19
  18. package/dist/logs-tool/index.d.ts.map +1 -1
  19. package/dist/logs-tool/main.d.ts +21 -0
  20. package/dist/logs-tool/main.d.ts.map +1 -0
  21. package/dist/observability-tool/main.d.ts +3 -0
  22. package/dist/observability-tool/main.d.ts.map +1 -0
  23. package/dist/session-tool/index.d.ts +1 -11
  24. package/dist/session-tool/index.d.ts.map +1 -1
  25. package/dist/session-tool/main.d.ts +13 -0
  26. package/dist/session-tool/main.d.ts.map +1 -0
  27. package/dist/shared/effect-version.d.ts +4 -0
  28. package/dist/shared/effect-version.d.ts.map +1 -0
  29. package/package.json +2 -2
  30. package/src/audit-tool/index.ts +3 -109
  31. package/src/audit-tool/main.ts +111 -0
  32. package/src/az-tool/index.ts +3 -151
  33. package/src/az-tool/main.ts +153 -0
  34. package/src/azdo-tool/index.ts +3 -220
  35. package/src/azdo-tool/main.ts +222 -0
  36. package/src/credential-guard/index.ts +570 -96
  37. package/src/db-tool/index.ts +3 -260
  38. package/src/db-tool/main.ts +262 -0
  39. package/src/gh-tool/index.ts +3 -256
  40. package/src/gh-tool/main.ts +258 -0
  41. package/src/gh-tool/pr/core.ts +52 -2
  42. package/src/gh-tool/workflow.ts +1 -0
  43. package/src/k8s-tool/index.ts +3 -487
  44. package/src/k8s-tool/main.ts +489 -0
  45. package/src/logs-tool/index.ts +3 -247
  46. package/src/logs-tool/main.ts +249 -0
  47. package/src/observability-tool/index.ts +3 -34
  48. package/src/observability-tool/main.ts +36 -0
  49. package/src/session-tool/index.ts +3 -337
  50. package/src/session-tool/main.ts +339 -0
  51. package/src/shared/effect-version.ts +32 -0
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env bun
2
+ import { Command, Flag } from "effect/unstable/cli";
3
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
4
+ import { Effect, Layer, Option } from "effect";
5
+
6
+ import type { CommandResult } from "./types";
7
+
8
+ import {
9
+ makeSchemaCommand,
10
+ formatOption,
11
+ formatOutput,
12
+ logText,
13
+ renderCauseToStderr,
14
+ VERSION,
15
+ } from "#shared";
16
+ import { AuditServiceLayer, withAudit } from "#shared/audit";
17
+ import { K8sService, K8sServiceLayer } from "./service";
18
+ import { ConfigService, ConfigServiceLayer, getDefaultEnvironment, getToolConfig } from "#config";
19
+ import type { K8sConfig } from "#config";
20
+ import { K8sContextError } from "./errors";
21
+ import { hasKubernetesConfig, selectK8sProfile } from "./profile";
22
+ import {
23
+ transformDescribe,
24
+ transformGenericKubectl,
25
+ transformLogs,
26
+ transformPods,
27
+ transformTop,
28
+ } from "./transformers";
29
+
30
+ /**
31
+ * Resolve environment from explicit --env flag, config defaultEnvironment, or fail with hint.
32
+ * Rejects implicit prod: if defaultEnvironment is "prod" and --env was not passed, fail explicitly.
33
+ */
34
+ const resolveEnv = (
35
+ envOption: Option.Option<string>,
36
+ config: Parameters<typeof getDefaultEnvironment>[0],
37
+ ) =>
38
+ Effect.gen(function* () {
39
+ const explicit = Option.getOrUndefined(envOption);
40
+ if (explicit) return explicit;
41
+
42
+ const defaultEnv = getDefaultEnvironment(config);
43
+
44
+ if (defaultEnv === "prod") {
45
+ return yield* new K8sContextError({
46
+ message:
47
+ "Implicit prod access blocked. Config defaultEnvironment is 'prod' but --env was not passed explicitly.",
48
+ clusterId: "(prod-safety)",
49
+ hint: "Pass --env prod explicitly to confirm production access, or change defaultEnvironment to a non-prod value.",
50
+ nextCommand: 'agent-tools-k8s kubectl --env prod --cmd "get pods -n <namespace>"',
51
+ });
52
+ }
53
+
54
+ if (defaultEnv) return defaultEnv;
55
+
56
+ return yield* new K8sContextError({
57
+ message:
58
+ "No environment specified. Use --env <name> or set defaultEnvironment in agent-tools.json5.",
59
+ clusterId: "(not specified)",
60
+ hint: 'Set defaultEnvironment in agent-tools.json5 (e.g. defaultEnvironment: "test") or pass --env explicitly.',
61
+ nextCommand: 'agent-tools-k8s kubectl --env test --cmd "get pods -n <namespace>"',
62
+ });
63
+ });
64
+
65
+ const resolveEnvAndProfile = (
66
+ envOption: Option.Option<string>,
67
+ profileOption: Option.Option<string>,
68
+ config: Parameters<typeof getDefaultEnvironment>[0],
69
+ ) =>
70
+ Effect.gen(function* () {
71
+ const resolvedEnv = yield* resolveEnv(envOption, config);
72
+ const profileName = selectK8sProfile(
73
+ config?.kubernetes,
74
+ Option.getOrUndefined(profileOption),
75
+ resolvedEnv,
76
+ );
77
+ return { env: resolvedEnv, profile: profileName };
78
+ });
79
+
80
+ type CommonK8sCommandOptions = {
81
+ readonly env: Option.Option<string>;
82
+ readonly dryRun: boolean;
83
+ readonly format: "toon" | "json";
84
+ readonly profile: Option.Option<string>;
85
+ };
86
+
87
+ const noKubernetesConfigResult = (): CommandResult => ({
88
+ success: false,
89
+ error: "No Kubernetes configuration found",
90
+ hint: "Add a 'kubernetes' section to agent-tools.json5 with clusterId and namespaces.",
91
+ nextCommand:
92
+ "echo '{ kubernetes: { default: { clusterId: \"my-cluster\" } } }' > agent-tools.json5",
93
+ executionTimeMs: 0,
94
+ });
95
+
96
+ const executeK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
97
+ Effect.gen(function* () {
98
+ const config = yield* ConfigService;
99
+
100
+ // Check for a missing 'kubernetes' section before resolving --env, so that case still
101
+ // surfaces "add a kubernetes section" instead of an env-resolution error (e.g. prod-safety).
102
+ if (!hasKubernetesConfig(config?.kubernetes)) {
103
+ return noKubernetesConfigResult();
104
+ }
105
+
106
+ const { env: resolvedEnv, profile: profileName } = yield* resolveEnvAndProfile(
107
+ options.env,
108
+ options.profile,
109
+ config,
110
+ );
111
+ const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
112
+
113
+ if (!k8sConfig) {
114
+ return noKubernetesConfigResult();
115
+ }
116
+
117
+ const k8sService = yield* K8sService;
118
+ const result = yield* k8sService.runKubectl(command, options.dryRun, profileName).pipe(
119
+ Effect.catchTags({
120
+ K8sContextError: (error) => {
121
+ const errorResult: CommandResult = {
122
+ success: false,
123
+ error: error.message,
124
+ hint:
125
+ error.hint ??
126
+ `Verify cluster ID "${k8sConfig.clusterId}" matches a context in kubectl config. Run: kubectl config get-contexts`,
127
+ ...(error.hint ? {} : { nextCommand: "kubectl config get-contexts" }),
128
+ executionTimeMs: 0,
129
+ };
130
+ return Effect.succeed(errorResult);
131
+ },
132
+ K8sCommandError: (error) => {
133
+ const errorResult: CommandResult = {
134
+ success: false,
135
+ error: error.message,
136
+ command: error.command,
137
+ hint:
138
+ error.hint ?? "Check command syntax and ensure the target namespace/resource exists.",
139
+ executionTimeMs: 0,
140
+ };
141
+ return Effect.succeed(errorResult);
142
+ },
143
+ K8sTimeoutError: (error) => {
144
+ const errorResult: CommandResult = {
145
+ success: false,
146
+ error: error.message,
147
+ command: error.command,
148
+ hint:
149
+ error.hint ??
150
+ `Command timed out after ${error.timeoutMs}ms. Consider increasing timeoutMs in config or narrowing the query.`,
151
+ executionTimeMs: error.timeoutMs,
152
+ };
153
+ return Effect.succeed(errorResult);
154
+ },
155
+ K8sDangerousCommandError: (error) => {
156
+ const errorResult: CommandResult = {
157
+ success: false,
158
+ error: error.message,
159
+ command: error.command,
160
+ hint:
161
+ error.hint ??
162
+ "AI agents can only run read-only kubectl commands. For mutating operations, use kubectl directly or ask a human operator.",
163
+ executionTimeMs: 0,
164
+ };
165
+ return Effect.succeed(errorResult);
166
+ },
167
+ }),
168
+ );
169
+
170
+ return { ...result, environment: resolvedEnv };
171
+ });
172
+
173
+ const runK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
174
+ Effect.gen(function* () {
175
+ const result = yield* executeK8sCommand(command, options);
176
+ yield* logText(formatOutput(result, options.format));
177
+ });
178
+
179
+ const logTransformedResult = (
180
+ result: CommandResult,
181
+ format: CommonK8sCommandOptions["format"],
182
+ transform: (output: string) => string | Record<string, unknown>,
183
+ ) =>
184
+ Effect.gen(function* () {
185
+ const transformedResult =
186
+ typeof result.output === "string" ? { ...result, output: transform(result.output) } : result;
187
+ yield* logText(formatOutput(transformedResult, format));
188
+ });
189
+
190
+ const commonFlags = {
191
+ env: Flag.optional(Flag.String("env")).pipe(
192
+ Flag.withDescription(
193
+ "Target environment (e.g. test, prod). Falls back to defaultEnvironment in config.",
194
+ ),
195
+ ),
196
+ dryRun: Flag.Boolean("dry-run").pipe(
197
+ Flag.withAlias("d"),
198
+ Flag.withDescription("Show command without executing"),
199
+ Flag.withDefault(false),
200
+ ),
201
+ format: formatOption,
202
+ profile: Flag.optional(Flag.String("profile")).pipe(
203
+ Flag.withDescription("Kubernetes profile name (if multiple configured)"),
204
+ ),
205
+ };
206
+
207
+ const buildKubectlCommand = (base: string, args: ReadonlyArray<string>) => {
208
+ const extras = args.filter((part) => part.length > 0);
209
+ return extras.length === 0 ? base : `${base} ${extras.join(" ")}`;
210
+ };
211
+
212
+ const resolveStructuredNamespace = (
213
+ namespaceOption: Option.Option<string>,
214
+ envOption: Option.Option<string>,
215
+ profileOption: Option.Option<string>,
216
+ ) =>
217
+ Effect.gen(function* () {
218
+ const explicitNamespace = Option.getOrUndefined(namespaceOption);
219
+ if (explicitNamespace) return explicitNamespace;
220
+
221
+ const config = yield* ConfigService;
222
+ const { env: resolvedEnv, profile: profileName } = yield* resolveEnvAndProfile(
223
+ envOption,
224
+ profileOption,
225
+ config,
226
+ );
227
+ const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
228
+
229
+ if (!k8sConfig) return undefined;
230
+
231
+ return k8sConfig.namespaces[resolvedEnv];
232
+ });
233
+
234
+ const kubectlCommand = Command.make(
235
+ "kubectl",
236
+ {
237
+ ...commonFlags,
238
+ cmd: Flag.String("cmd").pipe(
239
+ Flag.withDescription('kubectl command (without "kubectl" prefix)'),
240
+ ),
241
+ },
242
+ ({ cmd, dryRun, env, format, profile }) =>
243
+ Effect.gen(function* () {
244
+ const result = yield* executeK8sCommand(cmd, { dryRun, env, format, profile });
245
+ return yield* logTransformedResult(result, format, (output) =>
246
+ transformGenericKubectl(output, cmd),
247
+ );
248
+ }),
249
+ ).pipe(
250
+ Command.withDescription(
251
+ `Kubernetes CLI Tool for Coding Agents
252
+
253
+ Executes kubectl commands against the correct cluster context.
254
+ Parses commands into arguments and rejects shell syntax.
255
+
256
+ IMPORTANT FOR AI AGENTS:
257
+ Always use this tool instead of kubectl directly to ensure
258
+ commands run against the correct cluster context.
259
+ Use --format toon for LLM-optimized output (fewer tokens).
260
+
261
+ CLUSTER CONFIGURATION:
262
+ Cluster ID: (from agent-tools.json5 kubernetes profile)
263
+ Namespaces: (from agent-tools.json5 kubernetes profile)
264
+ (Context name is resolved dynamically from cluster ID)
265
+
266
+ WORKFLOW FOR AI AGENTS:
267
+ 1. Use this tool for ALL kubectl operations on test/prod
268
+ 2. Do not use pipes, chaining, substitution, or shell interpreters
269
+ 3. Use logs-tool for remote log filtering
270
+ 4. Use -n <namespace> for target namespace
271
+
272
+ EXAMPLES:
273
+ # List pods in test namespace
274
+ bun run src/k8s-tool kubectl --env test --cmd "get pods -n my-app-test"
275
+
276
+ # Get pod logs (use logs-tool when filtering is required)
277
+ bun run src/k8s-tool kubectl --env test --cmd "logs -l app=web-app -n my-app-test --tail=100"
278
+
279
+ # Check resource usage
280
+ bun run src/k8s-tool kubectl --env test --cmd "top pod -n my-app-test"
281
+
282
+ # Describe a pod
283
+ bun run src/k8s-tool kubectl --env test --cmd "describe pod web-app-xxx -n my-app-test"
284
+
285
+ # Execute an allowlisted diagnostic in a pod
286
+ bun run src/k8s-tool kubectl --env test --cmd "exec web-app-xxx -n my-app-test -- redis-cli INFO commandstats"
287
+
288
+ # Dry run - show command without executing
289
+ bun run src/k8s-tool kubectl --env test --cmd "get pods -n my-app-test" --dry-run
290
+
291
+ OUTPUT:
292
+ TOON: Token-efficient format for LLM agents - DEFAULT
293
+ JSON: { success, output?, error?, command?, executionTimeMs }
294
+
295
+ RELATED TOOLS:
296
+ - logs-tool: Higher-level tool for reading application logs
297
+ - db-tool: Database queries and schema introspection`,
298
+ ),
299
+ );
300
+
301
+ const podsCommand = Command.make(
302
+ "pods",
303
+ {
304
+ ...commonFlags,
305
+ namespace: Flag.String("namespace").pipe(
306
+ Flag.withDescription("Namespace to query"),
307
+ Flag.optional,
308
+ ),
309
+ label: Flag.String("label").pipe(
310
+ Flag.withDescription("Label selector (key=value)"),
311
+ Flag.optional,
312
+ ),
313
+ wide: Flag.Boolean("wide").pipe(
314
+ Flag.withDescription("Show additional pod information"),
315
+ Flag.withDefault(false),
316
+ ),
317
+ },
318
+ ({ dryRun, env, format, label, namespace, profile }) =>
319
+ Effect.gen(function* () {
320
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
321
+ const command = buildKubectlCommand("get pods -o json", [
322
+ Option.match(label, {
323
+ onNone: () => "",
324
+ onSome: (value) => `-l ${value}`,
325
+ }),
326
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
327
+ ]);
328
+ const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
329
+ return yield* logTransformedResult(result, format, transformPods);
330
+ }),
331
+ ).pipe(Command.withDescription("List pods (get pods) with optional namespace/label/wide output"));
332
+
333
+ const logsCommand = Command.make(
334
+ "logs",
335
+ {
336
+ ...commonFlags,
337
+ pod: Flag.String("pod").pipe(Flag.withDescription("Pod name")),
338
+ namespace: Flag.String("namespace").pipe(
339
+ Flag.withDescription("Namespace containing the pod"),
340
+ Flag.optional,
341
+ ),
342
+ container: Flag.String("container").pipe(
343
+ Flag.withDescription("Container name (for multi-container pods)"),
344
+ Flag.optional,
345
+ ),
346
+ tail: Flag.Int("tail").pipe(Flag.withDescription("Show last N log lines"), Flag.optional),
347
+ follow: Flag.Boolean("follow").pipe(
348
+ Flag.withAlias("f"),
349
+ Flag.withDescription("Stream logs in real time"),
350
+ Flag.withDefault(false),
351
+ ),
352
+ },
353
+ ({ container, dryRun, env, follow, format, namespace, pod, profile, tail }) =>
354
+ Effect.gen(function* () {
355
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
356
+ const command = buildKubectlCommand(`logs ${pod}`, [
357
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
358
+ Option.match(container, {
359
+ onNone: () => "",
360
+ onSome: (value) => `-c ${value}`,
361
+ }),
362
+ Option.match(tail, {
363
+ onNone: () => "",
364
+ onSome: (value) => `--tail=${value}`,
365
+ }),
366
+ follow ? "-f" : "",
367
+ ]);
368
+ const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
369
+ return yield* logTransformedResult(result, format, transformLogs);
370
+ }),
371
+ ).pipe(Command.withDescription("Fetch pod logs with tail/follow/container selectors"));
372
+
373
+ const describeCommand = Command.make(
374
+ "describe",
375
+ {
376
+ ...commonFlags,
377
+ resource: Flag.String("resource").pipe(
378
+ Flag.withDescription("Resource type (pod, deploy, svc, etc.)"),
379
+ ),
380
+ name: Flag.String("name").pipe(Flag.withDescription("Resource name")),
381
+ namespace: Flag.String("namespace").pipe(
382
+ Flag.withDescription("Namespace containing the resource"),
383
+ Flag.optional,
384
+ ),
385
+ },
386
+ ({ dryRun, env, format, name, namespace, profile, resource }) =>
387
+ Effect.gen(function* () {
388
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
389
+ const command = buildKubectlCommand(`describe ${resource} ${name}`, [
390
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
391
+ ]);
392
+ const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
393
+ return yield* logTransformedResult(result, format, transformDescribe);
394
+ }),
395
+ ).pipe(Command.withDescription("Describe a Kubernetes resource by type and name"));
396
+
397
+ const execCommand = Command.make(
398
+ "exec",
399
+ {
400
+ ...commonFlags,
401
+ pod: Flag.String("pod").pipe(Flag.withDescription("Pod name")),
402
+ execCmd: Flag.String("exec-cmd").pipe(
403
+ Flag.withDescription("Allowlisted diagnostic: redis-cli PING/INFO or ls"),
404
+ ),
405
+ namespace: Flag.String("namespace").pipe(
406
+ Flag.withDescription("Namespace containing the pod"),
407
+ Flag.optional,
408
+ ),
409
+ container: Flag.String("container").pipe(
410
+ Flag.withDescription("Container name (for multi-container pods)"),
411
+ Flag.optional,
412
+ ),
413
+ },
414
+ ({ container, dryRun, env, execCmd, format, namespace, pod, profile }) =>
415
+ Effect.gen(function* () {
416
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
417
+ const command = buildKubectlCommand(`exec ${pod}`, [
418
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
419
+ Option.match(container, {
420
+ onNone: () => "",
421
+ onSome: (value) => `-c ${value}`,
422
+ }),
423
+ `-- ${execCmd}`,
424
+ ]);
425
+ return yield* runK8sCommand(command, { dryRun, env, format, profile });
426
+ }),
427
+ ).pipe(Command.withDescription("Run an allowlisted diagnostic in a pod"));
428
+
429
+ const topCommand = Command.make(
430
+ "top",
431
+ {
432
+ ...commonFlags,
433
+ namespace: Flag.String("namespace").pipe(
434
+ Flag.withDescription("Namespace to inspect"),
435
+ Flag.optional,
436
+ ),
437
+ sortBy: Flag.Literals("sort-by", ["cpu", "memory"] as const).pipe(
438
+ Flag.withDescription("Sort metrics output when supported by kubectl"),
439
+ Flag.optional,
440
+ ),
441
+ },
442
+ ({ dryRun, env, format, namespace, profile, sortBy }) =>
443
+ Effect.gen(function* () {
444
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
445
+ const command = buildKubectlCommand("top pod", [
446
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
447
+ Option.match(sortBy, {
448
+ onNone: () => "",
449
+ onSome: (value) => `--sort-by=${value}`,
450
+ }),
451
+ ]);
452
+ const result = yield* executeK8sCommand(command, { dryRun, env, format, profile });
453
+ return yield* logTransformedResult(result, format, transformTop);
454
+ }),
455
+ ).pipe(Command.withDescription("Show pod CPU/memory usage (kubectl top pod)"));
456
+
457
+ const commandsCommand = makeSchemaCommand(() => mainCommand);
458
+
459
+ const mainCommand = Command.make("k8s-tool", {}).pipe(
460
+ Command.withDescription("Kubernetes CLI Tool for Coding Agents"),
461
+ Command.withSubcommands([
462
+ kubectlCommand,
463
+ podsCommand,
464
+ logsCommand,
465
+ describeCommand,
466
+ execCommand,
467
+ topCommand,
468
+ commandsCommand,
469
+ ]),
470
+ );
471
+
472
+ const cli = Command.run(mainCommand, {
473
+ version: VERSION,
474
+ });
475
+
476
+ const MainLayer = K8sServiceLayer.pipe(
477
+ Layer.provideMerge(ConfigServiceLayer),
478
+ Layer.provideMerge(BunServices.layer),
479
+ Layer.provideMerge(AuditServiceLayer),
480
+ );
481
+
482
+ const program = withAudit("k8s", cli).pipe(
483
+ Effect.provide(MainLayer),
484
+ Effect.tapCause(renderCauseToStderr),
485
+ );
486
+
487
+ BunRuntime.runMain(program, {
488
+ disableErrorReporting: true,
489
+ });