@blogic-cz/agent-tools 0.1.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -0
  3. package/package.json +70 -0
  4. package/schemas/agent-tools.schema.json +319 -0
  5. package/src/az-tool/build.ts +295 -0
  6. package/src/az-tool/config.ts +33 -0
  7. package/src/az-tool/errors.ts +26 -0
  8. package/src/az-tool/extract-option-value.ts +12 -0
  9. package/src/az-tool/index.ts +181 -0
  10. package/src/az-tool/security.ts +130 -0
  11. package/src/az-tool/service.ts +292 -0
  12. package/src/az-tool/types.ts +67 -0
  13. package/src/config/index.ts +12 -0
  14. package/src/config/loader.ts +170 -0
  15. package/src/config/types.ts +82 -0
  16. package/src/credential-guard/claude-hook.ts +28 -0
  17. package/src/credential-guard/index.ts +435 -0
  18. package/src/db-tool/config-service.ts +38 -0
  19. package/src/db-tool/errors.ts +40 -0
  20. package/src/db-tool/index.ts +91 -0
  21. package/src/db-tool/schema.ts +69 -0
  22. package/src/db-tool/security.ts +116 -0
  23. package/src/db-tool/service.ts +605 -0
  24. package/src/db-tool/types.ts +33 -0
  25. package/src/gh-tool/config.ts +7 -0
  26. package/src/gh-tool/errors.ts +47 -0
  27. package/src/gh-tool/index.ts +140 -0
  28. package/src/gh-tool/issue.ts +361 -0
  29. package/src/gh-tool/pr/commands.ts +432 -0
  30. package/src/gh-tool/pr/core.ts +497 -0
  31. package/src/gh-tool/pr/helpers.ts +84 -0
  32. package/src/gh-tool/pr/index.ts +19 -0
  33. package/src/gh-tool/pr/review.ts +571 -0
  34. package/src/gh-tool/repo.ts +147 -0
  35. package/src/gh-tool/service.ts +192 -0
  36. package/src/gh-tool/types.ts +97 -0
  37. package/src/gh-tool/workflow.ts +542 -0
  38. package/src/index.ts +1 -0
  39. package/src/k8s-tool/errors.ts +21 -0
  40. package/src/k8s-tool/index.ts +151 -0
  41. package/src/k8s-tool/service.ts +227 -0
  42. package/src/k8s-tool/types.ts +9 -0
  43. package/src/logs-tool/errors.ts +29 -0
  44. package/src/logs-tool/index.ts +176 -0
  45. package/src/logs-tool/service.ts +323 -0
  46. package/src/logs-tool/types.ts +40 -0
  47. package/src/session-tool/config.ts +55 -0
  48. package/src/session-tool/errors.ts +38 -0
  49. package/src/session-tool/index.ts +270 -0
  50. package/src/session-tool/service.ts +210 -0
  51. package/src/session-tool/types.ts +28 -0
  52. package/src/shared/bun.ts +59 -0
  53. package/src/shared/cli.ts +38 -0
  54. package/src/shared/error-renderer.ts +42 -0
  55. package/src/shared/exec.ts +62 -0
  56. package/src/shared/format.ts +27 -0
  57. package/src/shared/index.ts +16 -0
  58. package/src/shared/throttle.ts +35 -0
  59. package/src/shared/types.ts +25 -0
@@ -0,0 +1,542 @@
1
+ import { Command, Flag } from "effect/unstable/cli";
2
+ import { Console, Effect, Option } from "effect";
3
+
4
+ import { formatOption, logFormatted } from "../shared";
5
+ import { GitHubCommandError, GitHubNotFoundError } from "./errors";
6
+ import { GitHubService } from "./service";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ type WorkflowRun = {
13
+ databaseId: number;
14
+ displayTitle: string;
15
+ status: string;
16
+ conclusion: string | null;
17
+ headBranch: string;
18
+ createdAt: string;
19
+ event: string;
20
+ url: string;
21
+ workflowName: string;
22
+ };
23
+
24
+ type WorkflowJob = {
25
+ databaseId: number;
26
+ name: string;
27
+ status: string;
28
+ conclusion: string | null;
29
+ startedAt: string;
30
+ completedAt: string | null;
31
+ url: string;
32
+ steps: Array<{
33
+ name: string;
34
+ status: string;
35
+ conclusion: string | null;
36
+ number: number;
37
+ startedAt: string | null;
38
+ completedAt: string | null;
39
+ }>;
40
+ };
41
+
42
+ type WorkflowRunDetail = WorkflowRun & {
43
+ jobs: WorkflowJob[];
44
+ };
45
+
46
+ type LogEntry = {
47
+ step: string;
48
+ message: string;
49
+ };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Internal handlers
53
+ // ---------------------------------------------------------------------------
54
+
55
+ const listRuns = Effect.fn("workflow.listRuns")(function* (opts: {
56
+ workflow: string | null;
57
+ branch: string | null;
58
+ status: string | null;
59
+ limit: number;
60
+ repo: string | null;
61
+ }) {
62
+ const gh = yield* GitHubService;
63
+
64
+ const args = [
65
+ "run",
66
+ "list",
67
+ "--json",
68
+ "databaseId,displayTitle,status,conclusion,headBranch,createdAt,event,url,workflowName",
69
+ "--limit",
70
+ String(opts.limit),
71
+ ];
72
+
73
+ if (opts.repo !== null) {
74
+ args.push("--repo", opts.repo);
75
+ }
76
+
77
+ if (opts.workflow !== null) {
78
+ args.push("--workflow", opts.workflow);
79
+ }
80
+
81
+ if (opts.branch !== null) {
82
+ args.push("--branch", opts.branch);
83
+ }
84
+
85
+ if (opts.status !== null) {
86
+ args.push("--status", opts.status);
87
+ }
88
+
89
+ return yield* gh.runGhJson<WorkflowRun[]>(args);
90
+ });
91
+
92
+ const viewRun = Effect.fn("workflow.viewRun")(function* (runId: number) {
93
+ const gh = yield* GitHubService;
94
+
95
+ const run = yield* gh.runGhJson<WorkflowRunDetail>([
96
+ "run",
97
+ "view",
98
+ String(runId),
99
+ "--json",
100
+ "databaseId,displayTitle,status,conclusion,headBranch,createdAt,event,url,workflowName,jobs",
101
+ ]);
102
+
103
+ return run;
104
+ });
105
+
106
+ const listJobs = Effect.fn("workflow.listJobs")(function* (runId: number) {
107
+ const gh = yield* GitHubService;
108
+
109
+ const run = yield* gh.runGhJson<{
110
+ jobs: WorkflowJob[];
111
+ }>(["run", "view", String(runId), "--json", "jobs"]);
112
+
113
+ return run.jobs;
114
+ });
115
+
116
+ const fetchLogs = Effect.fn("workflow.fetchLogs")(function* (
117
+ runId: number,
118
+ failedOnly: boolean,
119
+ jobId: number | null = null,
120
+ ) {
121
+ const gh = yield* GitHubService;
122
+ const args = ["run", "view", String(runId)];
123
+
124
+ if (jobId !== null) {
125
+ args.push("--log", "--job", String(jobId));
126
+ } else if (failedOnly) {
127
+ args.push("--log-failed");
128
+ } else {
129
+ args.push("--log");
130
+ }
131
+
132
+ const result = yield* gh.runGh(args);
133
+ return {
134
+ runId,
135
+ failedOnly,
136
+ log: result.stdout,
137
+ };
138
+ });
139
+
140
+ const rerunWorkflow = Effect.fn("workflow.rerunWorkflow")(function* (
141
+ runId: number,
142
+ failedOnly: boolean,
143
+ repo: string | null,
144
+ ) {
145
+ const gh = yield* GitHubService;
146
+
147
+ const args = ["run", "rerun", String(runId)];
148
+ if (failedOnly) {
149
+ args.push("--failed");
150
+ }
151
+ if (repo !== null) {
152
+ args.push("--repo", repo);
153
+ }
154
+
155
+ yield* gh.runGh(args);
156
+
157
+ return {
158
+ rerun: true as const,
159
+ runId,
160
+ failedOnly,
161
+ message: failedOnly
162
+ ? `Rerunning failed jobs for run ${runId}`
163
+ : `Rerunning all jobs for run ${runId}`,
164
+ };
165
+ });
166
+
167
+ const cancelRun = Effect.fn("workflow.cancelRun")(function* (runId: number) {
168
+ const gh = yield* GitHubService;
169
+
170
+ yield* gh.runGh(["run", "cancel", String(runId)]);
171
+
172
+ return {
173
+ cancelled: true as const,
174
+ runId,
175
+ message: `Cancelled run ${runId}`,
176
+ };
177
+ });
178
+
179
+ const watchRun = Effect.fn("workflow.watchRun")(function* (runId: number) {
180
+ const gh = yield* GitHubService;
181
+
182
+ const result = yield* gh.runGh(["run", "watch", String(runId), "--exit-status"]).pipe(
183
+ Effect.catchTag("GitHubCommandError", (error) => {
184
+ // exit-status returns non-zero if run failed, but we still want the output
185
+ if (error.exitCode > 0 && error.stderr === "") {
186
+ return Effect.succeed({
187
+ stdout: "",
188
+ stderr: "",
189
+ exitCode: error.exitCode,
190
+ });
191
+ }
192
+ return Effect.fail(error);
193
+ }),
194
+ );
195
+
196
+ const finalState = yield* viewRun(runId);
197
+
198
+ return {
199
+ runId,
200
+ status: finalState.status,
201
+ conclusion: finalState.conclusion,
202
+ jobs: finalState.jobs.map((job) => ({
203
+ name: job.name,
204
+ status: job.status,
205
+ conclusion: job.conclusion,
206
+ })),
207
+ watchOutput: result.stdout,
208
+ };
209
+ });
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Log parsing utilities (pure functions)
213
+ // ---------------------------------------------------------------------------
214
+
215
+ const TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s?/;
216
+ // eslint-disable-next-line no-control-regex
217
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
218
+
219
+ export function cleanLogLine(line: string): string {
220
+ return line
221
+ .replace(ANSI_RE, "")
222
+ .replace(TIMESTAMP_RE, "")
223
+ .replace(/\r$/, "")
224
+ .replace(/^##\[(command|debug|notice)\]/, "")
225
+ .trim();
226
+ }
227
+
228
+ export function parseRawJobLogs(raw: string): LogEntry[] {
229
+ const entries: LogEntry[] = [];
230
+ let currentStep = "(unknown)";
231
+
232
+ for (const rawLine of raw.split("\n")) {
233
+ const line = rawLine.replace(/\r$/, "");
234
+
235
+ // Step group markers
236
+ const groupMatch = line.match(/##\[group\](.+)/);
237
+ if (groupMatch) {
238
+ currentStep = groupMatch[1].trim();
239
+ continue;
240
+ }
241
+ if (line.includes("##[endgroup]")) continue;
242
+
243
+ const cleaned = cleanLogLine(line);
244
+ if (cleaned.length === 0) continue;
245
+
246
+ entries.push({ step: currentStep, message: cleaned });
247
+ }
248
+
249
+ return entries;
250
+ }
251
+
252
+ export function formatLogEntries(entries: LogEntry[]): string {
253
+ const sections: string[] = [];
254
+ let lastStep = "";
255
+
256
+ for (const entry of entries) {
257
+ if (entry.step !== lastStep) {
258
+ sections.push(`\n=== ${entry.step} ===`);
259
+ lastStep = entry.step;
260
+ }
261
+ sections.push(entry.message);
262
+ }
263
+
264
+ return sections.join("\n").trim();
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Job-level log handlers
269
+ // ---------------------------------------------------------------------------
270
+
271
+ const resolveJobId = Effect.fn("workflow.resolveJobId")(function* (runId: number, jobName: string) {
272
+ const jobs = yield* listJobs(runId);
273
+
274
+ // Exact match first
275
+ const exact = jobs.find((j) => j.name === jobName);
276
+ if (exact) return exact.databaseId;
277
+
278
+ // Case-insensitive partial match
279
+ const lower = jobName.toLowerCase();
280
+ const partial = jobs.filter((j) => j.name.toLowerCase().includes(lower));
281
+
282
+ if (partial.length === 1) return partial[0].databaseId;
283
+
284
+ if (partial.length > 1) {
285
+ return yield* new GitHubCommandError({
286
+ message: `Ambiguous job name "${jobName}". Matches: ${partial.map((j) => j.name).join(", ")}`,
287
+ command: "workflow job-logs",
288
+ exitCode: 1,
289
+ stderr: "",
290
+ });
291
+ }
292
+
293
+ return yield* new GitHubNotFoundError({
294
+ message: `Job "${jobName}" not found in run ${runId}. Available jobs: ${jobs.map((j) => j.name).join(", ")}`,
295
+ identifier: jobName,
296
+ resource: "job",
297
+ });
298
+ });
299
+
300
+ const filterFailedStepEntries = Effect.fn("workflow.filterFailedStepEntries")(function* (
301
+ runId: number,
302
+ jobId: number,
303
+ entries: LogEntry[],
304
+ ) {
305
+ const jobs = yield* listJobs(runId);
306
+ const job = jobs.find((j) => j.databaseId === jobId);
307
+ if (!job) return entries;
308
+
309
+ const failedStepNames = new Set(
310
+ job.steps.filter((s) => s.conclusion === "failure").map((s) => s.name),
311
+ );
312
+
313
+ if (failedStepNames.size === 0) return entries;
314
+
315
+ return entries.filter((e) => failedStepNames.has(e.step));
316
+ });
317
+
318
+ const fetchJobLogs = Effect.fn("workflow.fetchJobLogs")(function* (opts: {
319
+ runId: number;
320
+ job: string;
321
+ failedStepsOnly: boolean;
322
+ format: string;
323
+ }) {
324
+ const gh = yield* GitHubService;
325
+ const { owner, name: repo } = yield* gh.getRepoInfo();
326
+
327
+ const jobId = yield* resolveJobId(opts.runId, opts.job);
328
+
329
+ // Fetch raw logs via API (follows 302 redirect automatically)
330
+ const raw = yield* gh.runGh(["api", `repos/${owner}/${repo}/actions/jobs/${jobId}/logs`]).pipe(
331
+ Effect.map((r) => r.stdout),
332
+ Effect.catchTag("GitHubCommandError", () => {
333
+ // Fallback: use gh run view --log --job
334
+ return fetchLogs(opts.runId, false, jobId).pipe(Effect.map((r) => r.log));
335
+ }),
336
+ );
337
+
338
+ let entries = parseRawJobLogs(raw);
339
+
340
+ if (opts.failedStepsOnly) {
341
+ entries = yield* filterFailedStepEntries(opts.runId, jobId, entries);
342
+ }
343
+
344
+ if (opts.format === "json") {
345
+ return {
346
+ runId: opts.runId,
347
+ job: opts.job,
348
+ jobId,
349
+ entries,
350
+ };
351
+ }
352
+
353
+ return {
354
+ runId: opts.runId,
355
+ job: opts.job,
356
+ jobId,
357
+ formatted: formatLogEntries(entries),
358
+ };
359
+ });
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // CLI Commands
363
+ // ---------------------------------------------------------------------------
364
+
365
+ export const workflowListCommand = Command.make(
366
+ "list",
367
+ {
368
+ branch: Flag.string("branch").pipe(
369
+ Flag.withDescription("Filter by branch name"),
370
+ Flag.optional,
371
+ ),
372
+ format: formatOption,
373
+ limit: Flag.integer("limit").pipe(
374
+ Flag.withDescription("Maximum number of runs to return"),
375
+ Flag.withDefault(10),
376
+ ),
377
+ repo: Flag.string("repo").pipe(
378
+ Flag.withDescription("Target repository (owner/name). Defaults to current repo"),
379
+ Flag.optional,
380
+ ),
381
+ status: Flag.choice("status", [
382
+ "queued",
383
+ "in_progress",
384
+ "completed",
385
+ "action_required",
386
+ "cancelled",
387
+ "failure",
388
+ "neutral",
389
+ "skipped",
390
+ "stale",
391
+ "success",
392
+ "timed_out",
393
+ "waiting",
394
+ ]).pipe(Flag.withDescription("Filter by run status"), Flag.optional),
395
+ workflow: Flag.string("workflow").pipe(
396
+ Flag.withDescription("Filter by workflow file name (e.g., build-and-deploy.yml)"),
397
+ Flag.optional,
398
+ ),
399
+ },
400
+ ({ branch, format, limit, repo, status, workflow }) =>
401
+ Effect.gen(function* () {
402
+ const runs = yield* listRuns({
403
+ branch: Option.getOrNull(branch),
404
+ limit,
405
+ repo: Option.getOrNull(repo),
406
+ status: Option.getOrNull(status),
407
+ workflow: Option.getOrNull(workflow),
408
+ });
409
+ yield* logFormatted(runs, format);
410
+ }),
411
+ ).pipe(
412
+ Command.withDescription("List workflow runs (filter by --workflow, --branch, --status, --repo)"),
413
+ );
414
+
415
+ export const workflowViewCommand = Command.make(
416
+ "view",
417
+ {
418
+ format: formatOption,
419
+ run: Flag.integer("run").pipe(Flag.withDescription("Workflow run ID")),
420
+ },
421
+ ({ format, run }) =>
422
+ Effect.gen(function* () {
423
+ const detail = yield* viewRun(run);
424
+ yield* logFormatted(detail, format);
425
+ }),
426
+ ).pipe(Command.withDescription("View workflow run details including jobs and steps"));
427
+
428
+ export const workflowJobsCommand = Command.make(
429
+ "jobs",
430
+ {
431
+ format: formatOption,
432
+ run: Flag.integer("run").pipe(Flag.withDescription("Workflow run ID")),
433
+ },
434
+ ({ format, run }) =>
435
+ Effect.gen(function* () {
436
+ const jobs = yield* listJobs(run);
437
+ yield* logFormatted(jobs, format);
438
+ }),
439
+ ).pipe(Command.withDescription("List jobs and their steps for a workflow run"));
440
+
441
+ export const workflowLogsCommand = Command.make(
442
+ "logs",
443
+ {
444
+ failedOnly: Flag.boolean("failed-only").pipe(
445
+ Flag.withDescription("Only show logs from failed jobs (default: true)"),
446
+ Flag.withDefault(true),
447
+ ),
448
+ format: formatOption,
449
+ run: Flag.integer("run").pipe(Flag.withDescription("Workflow run ID")),
450
+ },
451
+ ({ failedOnly, format, run }) =>
452
+ Effect.gen(function* () {
453
+ const logs = yield* fetchLogs(run, failedOnly);
454
+
455
+ if (format === "toon" || format === "json") {
456
+ yield* logFormatted(logs, format);
457
+ } else {
458
+ yield* Console.log(logs.log);
459
+ }
460
+ }),
461
+ ).pipe(Command.withDescription("Fetch logs for a workflow run (--failed-only by default)"));
462
+
463
+ export const workflowRerunCommand = Command.make(
464
+ "rerun",
465
+ {
466
+ failedOnly: Flag.boolean("failed-only").pipe(
467
+ Flag.withDescription("Only rerun failed jobs (default: true)"),
468
+ Flag.withDefault(true),
469
+ ),
470
+ format: formatOption,
471
+ repo: Flag.string("repo").pipe(
472
+ Flag.withDescription("Target repository (owner/name). Defaults to current repo"),
473
+ Flag.optional,
474
+ ),
475
+ run: Flag.integer("run").pipe(Flag.withDescription("Workflow run ID to rerun")),
476
+ },
477
+ ({ failedOnly, format, repo, run }) =>
478
+ Effect.gen(function* () {
479
+ const result = yield* rerunWorkflow(run, failedOnly, Option.getOrNull(repo));
480
+ yield* logFormatted(result, format);
481
+ }),
482
+ ).pipe(Command.withDescription("Rerun a workflow run (failed jobs only by default)"));
483
+
484
+ export const workflowCancelCommand = Command.make(
485
+ "cancel",
486
+ {
487
+ format: formatOption,
488
+ run: Flag.integer("run").pipe(Flag.withDescription("Workflow run ID to cancel")),
489
+ },
490
+ ({ format, run }) =>
491
+ Effect.gen(function* () {
492
+ const result = yield* cancelRun(run);
493
+ yield* logFormatted(result, format);
494
+ }),
495
+ ).pipe(Command.withDescription("Cancel an in-progress workflow run"));
496
+
497
+ export const workflowWatchCommand = Command.make(
498
+ "watch",
499
+ {
500
+ format: formatOption,
501
+ run: Flag.integer("run").pipe(Flag.withDescription("Workflow run ID to watch")),
502
+ },
503
+ ({ format, run }) =>
504
+ Effect.gen(function* () {
505
+ const result = yield* watchRun(run);
506
+ yield* logFormatted(result, format);
507
+ }),
508
+ ).pipe(Command.withDescription("Watch a workflow run until it completes, then show final status"));
509
+
510
+ export const workflowJobLogsCommand = Command.make(
511
+ "job-logs",
512
+ {
513
+ failedStepsOnly: Flag.boolean("failed-steps-only").pipe(
514
+ Flag.withDescription("Only show logs from failed steps (default: false)"),
515
+ Flag.withDefault(false),
516
+ ),
517
+ format: formatOption,
518
+ job: Flag.string("job").pipe(
519
+ Flag.withDescription("Job name to fetch logs for (exact or partial match)"),
520
+ ),
521
+ run: Flag.integer("run").pipe(Flag.withDescription("Workflow run ID")),
522
+ },
523
+ ({ failedStepsOnly, format, job, run }) =>
524
+ Effect.gen(function* () {
525
+ const result = yield* fetchJobLogs({
526
+ runId: run,
527
+ job,
528
+ failedStepsOnly,
529
+ format,
530
+ });
531
+
532
+ if ("formatted" in result) {
533
+ yield* Console.log(result.formatted);
534
+ } else {
535
+ yield* logFormatted(result, format);
536
+ }
537
+ }),
538
+ ).pipe(
539
+ Command.withDescription(
540
+ "Fetch parsed, clean logs for a specific job in a workflow run. Resolves job name to ID, strips timestamps/ANSI, groups by step.",
541
+ ),
542
+ );
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export type { AgentToolsConfig } from "./config/index.ts";
@@ -0,0 +1,21 @@
1
+ import { Schema } from "effect";
2
+
3
+ export class K8sContextError extends Schema.TaggedErrorClass<K8sContextError>()("K8sContextError", {
4
+ message: Schema.String,
5
+ clusterId: Schema.String,
6
+ }) {}
7
+
8
+ export class K8sCommandError extends Schema.TaggedErrorClass<K8sCommandError>()("K8sCommandError", {
9
+ message: Schema.String,
10
+ command: Schema.String,
11
+ exitCode: Schema.optionalKey(Schema.Number),
12
+ stderr: Schema.optionalKey(Schema.String),
13
+ }) {}
14
+
15
+ export class K8sTimeoutError extends Schema.TaggedErrorClass<K8sTimeoutError>()("K8sTimeoutError", {
16
+ message: Schema.String,
17
+ command: Schema.String,
18
+ timeoutMs: Schema.Number,
19
+ }) {}
20
+
21
+ export type K8sError = K8sContextError | K8sCommandError | K8sTimeoutError;
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bun
2
+ import { Command, Flag } from "effect/unstable/cli";
3
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
4
+ import { Console, Effect, Layer, Option } from "effect";
5
+
6
+ import type { CommandResult } from "./types";
7
+
8
+ import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "../shared";
9
+ import { K8sService, K8sServiceLayer } from "./service";
10
+ import { ConfigService, ConfigServiceLayer, getToolConfig } from "../config";
11
+ import type { K8sConfig } from "../config";
12
+
13
+ const kubectlCommand = Command.make(
14
+ "kubectl",
15
+ {
16
+ env: Flag.choice("env", ["test", "prod"]).pipe(
17
+ Flag.withDescription("Target environment: test or prod"),
18
+ ),
19
+ cmd: Flag.string("cmd").pipe(
20
+ Flag.withDescription('kubectl command (without "kubectl" prefix)'),
21
+ ),
22
+ dryRun: Flag.boolean("dry-run").pipe(
23
+ Flag.withAlias("d"),
24
+ Flag.withDescription("Show command without executing"),
25
+ Flag.withDefault(false),
26
+ ),
27
+ format: formatOption,
28
+ profile: Flag.optional(Flag.string("profile")).pipe(
29
+ Flag.withDescription("Kubernetes profile name (if multiple configured)"),
30
+ ),
31
+ },
32
+ ({ cmd, dryRun, format, profile }) =>
33
+ Effect.gen(function* () {
34
+ const config = yield* ConfigService;
35
+ const profileName = profile ? Option.getOrUndefined(profile) : undefined;
36
+ const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
37
+
38
+ if (!k8sConfig) {
39
+ const result: CommandResult = {
40
+ success: false,
41
+ error: "No Kubernetes configuration found",
42
+ executionTimeMs: 0,
43
+ };
44
+ yield* Console.log(formatOutput(result, format));
45
+ return;
46
+ }
47
+
48
+ const k8sService = yield* K8sService;
49
+ const result = yield* k8sService.runKubectl(cmd, dryRun).pipe(
50
+ Effect.catchTags({
51
+ K8sContextError: (error) => {
52
+ const result: CommandResult = {
53
+ success: false,
54
+ error: error.message,
55
+ executionTimeMs: 0,
56
+ };
57
+ return Effect.succeed(result);
58
+ },
59
+ K8sCommandError: (error) => {
60
+ const result: CommandResult = {
61
+ success: false,
62
+ error: error.message,
63
+ command: error.command,
64
+ executionTimeMs: 0,
65
+ };
66
+ return Effect.succeed(result);
67
+ },
68
+ K8sTimeoutError: (error) => {
69
+ const result: CommandResult = {
70
+ success: false,
71
+ error: error.message,
72
+ command: error.command,
73
+ executionTimeMs: error.timeoutMs,
74
+ };
75
+ return Effect.succeed(result);
76
+ },
77
+ }),
78
+ );
79
+
80
+ yield* Console.log(formatOutput(result, format));
81
+ }),
82
+ ).pipe(
83
+ Command.withDescription(
84
+ `Kubernetes CLI Tool for Coding Agents
85
+
86
+ Executes kubectl commands against the correct cluster context.
87
+ Supports shell pipes and complex commands.
88
+
89
+ IMPORTANT FOR AI AGENTS:
90
+ Always use this tool instead of kubectl directly to ensure
91
+ commands run against the correct cluster context.
92
+ Use --format toon for LLM-optimized output (fewer tokens).
93
+
94
+ CLUSTER CONFIGURATION:
95
+ Cluster ID: (from agent-tools.json5 kubernetes profile)
96
+ Namespaces: (from agent-tools.json5 kubernetes profile)
97
+ (Context name is resolved dynamically from cluster ID)
98
+
99
+ WORKFLOW FOR AI AGENTS:
100
+ 1. Use this tool for ALL kubectl operations on test/prod
101
+ 2. Pipes are supported - use shell syntax
102
+ 3. Use -n <namespace> for target namespace
103
+
104
+ EXAMPLES:
105
+ # List pods in test namespace
106
+ bun run src/k8s-tool kubectl --env test --cmd "get pods -n my-app-test"
107
+
108
+ # Get pod logs with grep
109
+ bun run src/k8s-tool kubectl --env test --cmd "logs -l app=web-app -n my-app-test --tail=100 | grep error"
110
+
111
+ # Check resource usage
112
+ bun run src/k8s-tool kubectl --env test --cmd "top pod -n my-app-test"
113
+
114
+ # Describe pod with filtered output
115
+ bun run src/k8s-tool kubectl --env test --cmd "describe pod web-app-xxx -n my-app-test | grep -A20 Events"
116
+
117
+ # Execute command in pod
118
+ bun run src/k8s-tool kubectl --env test --cmd "exec web-app-xxx -n my-app-test -- cat /app/logs/app.log | tail -50"
119
+
120
+ # Dry run - show command without executing
121
+ bun run src/k8s-tool kubectl --env test --cmd "get pods -n my-app-test" --dry-run
122
+
123
+ OUTPUT:
124
+ TOON: Token-efficient format for LLM agents - DEFAULT
125
+ JSON: { success, output?, error?, command?, executionTimeMs }
126
+
127
+ RELATED TOOLS:
128
+ - logs-tool: Higher-level tool for reading application logs
129
+ - db-tool: Database queries and schema introspection`,
130
+ ),
131
+ );
132
+
133
+ const mainCommand = Command.make("k8s-tool", {}).pipe(
134
+ Command.withDescription("Kubernetes CLI Tool for Coding Agents"),
135
+ Command.withSubcommands([kubectlCommand]),
136
+ );
137
+
138
+ const cli = Command.run(mainCommand, {
139
+ version: VERSION,
140
+ });
141
+
142
+ const MainLayer = K8sServiceLayer.pipe(
143
+ Layer.provideMerge(ConfigServiceLayer),
144
+ Layer.provideMerge(BunServices.layer),
145
+ );
146
+
147
+ const program = cli.pipe(Effect.provide(MainLayer), Effect.tapCause(renderCauseToStderr));
148
+
149
+ BunRuntime.runMain(program, {
150
+ disableErrorReporting: true,
151
+ });