@blogic-cz/agent-tools 0.1.0 → 0.2.1

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 (42) hide show
  1. package/README.md +68 -30
  2. package/package.json +7 -4
  3. package/schemas/agent-tools.schema.json +4 -0
  4. package/src/az-tool/build.ts +5 -0
  5. package/src/az-tool/errors.ts +12 -0
  6. package/src/az-tool/index.ts +129 -105
  7. package/src/az-tool/service.ts +13 -4
  8. package/src/config/index.ts +7 -1
  9. package/src/config/loader.ts +8 -0
  10. package/src/config/types.ts +2 -0
  11. package/src/credential-guard/index.ts +2 -1
  12. package/src/db-tool/config-service.ts +2 -2
  13. package/src/db-tool/errors.ts +15 -0
  14. package/src/db-tool/index.ts +47 -8
  15. package/src/db-tool/types.ts +1 -1
  16. package/src/gh-tool/errors.ts +15 -0
  17. package/src/gh-tool/index.ts +5 -1
  18. package/src/gh-tool/issue.ts +1 -1
  19. package/src/gh-tool/pr/commands.ts +58 -3
  20. package/src/gh-tool/pr/core.ts +28 -7
  21. package/src/gh-tool/pr/helpers.ts +1 -1
  22. package/src/gh-tool/pr/index.ts +2 -0
  23. package/src/gh-tool/pr/review.ts +10 -6
  24. package/src/gh-tool/repo.ts +1 -1
  25. package/src/gh-tool/service.ts +5 -0
  26. package/src/gh-tool/workflow.ts +5 -1
  27. package/src/k8s-tool/errors.ts +9 -0
  28. package/src/k8s-tool/index.ts +318 -66
  29. package/src/k8s-tool/service.ts +2 -2
  30. package/src/k8s-tool/types.ts +4 -0
  31. package/src/logs-tool/errors.ts +12 -0
  32. package/src/logs-tool/index.ts +73 -11
  33. package/src/logs-tool/service.ts +4 -4
  34. package/src/logs-tool/types.ts +4 -1
  35. package/src/session-tool/config.ts +1 -1
  36. package/src/session-tool/index.ts +1 -1
  37. package/src/session-tool/service.ts +16 -3
  38. package/src/session-tool/types.ts +1 -1
  39. package/src/shared/bun.ts +1 -1
  40. package/src/shared/error-renderer.ts +21 -11
  41. package/src/shared/index.ts +1 -0
  42. package/src/shared/types.ts +3 -0
@@ -5,80 +5,172 @@ import { Console, Effect, Layer, Option } from "effect";
5
5
 
6
6
  import type { CommandResult } from "./types";
7
7
 
8
- import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "../shared";
8
+ import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "#src/shared";
9
9
  import { K8sService, K8sServiceLayer } from "./service";
10
- import { ConfigService, ConfigServiceLayer, getToolConfig } from "../config";
11
- import type { K8sConfig } from "../config";
10
+ import {
11
+ ConfigService,
12
+ ConfigServiceLayer,
13
+ getDefaultEnvironment,
14
+ getToolConfig,
15
+ } from "#src/config";
16
+ import type { K8sConfig } from "#src/config";
17
+ import { K8sContextError } from "./errors";
18
+
19
+ /**
20
+ * Resolve environment from explicit --env flag, config defaultEnvironment, or fail with hint.
21
+ * Rejects implicit prod: if defaultEnvironment is "prod" and --env was not passed, fail explicitly.
22
+ */
23
+ const resolveEnv = (
24
+ envOption: Option.Option<string>,
25
+ config: Parameters<typeof getDefaultEnvironment>[0],
26
+ ) =>
27
+ Effect.gen(function* () {
28
+ const explicit = Option.getOrUndefined(envOption);
29
+ if (explicit) return explicit;
30
+
31
+ const defaultEnv = getDefaultEnvironment(config);
32
+
33
+ if (defaultEnv === "prod") {
34
+ return yield* new K8sContextError({
35
+ message:
36
+ "Implicit prod access blocked. Config defaultEnvironment is 'prod' but --env was not passed explicitly.",
37
+ clusterId: "(prod-safety)",
38
+ hint: "Pass --env prod explicitly to confirm production access, or change defaultEnvironment to a non-prod value.",
39
+ nextCommand: 'agent-tools-k8s kubectl --env prod --cmd "get pods -n <namespace>"',
40
+ });
41
+ }
42
+
43
+ if (defaultEnv) return defaultEnv;
44
+
45
+ return yield* new K8sContextError({
46
+ message:
47
+ "No environment specified. Use --env <name> or set defaultEnvironment in agent-tools.json5.",
48
+ clusterId: "(not specified)",
49
+ hint: 'Set defaultEnvironment in agent-tools.json5 (e.g. defaultEnvironment: "test") or pass --env explicitly.',
50
+ nextCommand: 'agent-tools-k8s kubectl --env test --cmd "get pods -n <namespace>"',
51
+ });
52
+ });
53
+
54
+ type CommonK8sCommandOptions = {
55
+ readonly env: Option.Option<string>;
56
+ readonly dryRun: boolean;
57
+ readonly format: "toon" | "json";
58
+ readonly profile: Option.Option<string>;
59
+ };
60
+
61
+ const runK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
62
+ Effect.gen(function* () {
63
+ const config = yield* ConfigService;
64
+ const profileName = Option.getOrUndefined(options.profile);
65
+ const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
66
+
67
+ if (!k8sConfig) {
68
+ const result: CommandResult = {
69
+ success: false,
70
+ error: "No Kubernetes configuration found",
71
+ hint: "Add a 'kubernetes' section to agent-tools.json5 with clusterId and namespaces.",
72
+ nextCommand:
73
+ "echo '{ kubernetes: { default: { clusterId: \"my-cluster\" } } }' > agent-tools.json5",
74
+ executionTimeMs: 0,
75
+ };
76
+ yield* Console.log(formatOutput(result, options.format));
77
+ return;
78
+ }
79
+
80
+ const resolvedEnv = yield* resolveEnv(options.env, config);
81
+
82
+ const k8sService = yield* K8sService;
83
+ const result = yield* k8sService.runKubectl(command, options.dryRun).pipe(
84
+ Effect.catchTags({
85
+ K8sContextError: (error) => {
86
+ const errorResult: CommandResult = {
87
+ success: false,
88
+ error: error.message,
89
+ hint: `Verify cluster ID "${k8sConfig.clusterId}" matches a context in kubectl config. Run: kubectl config get-contexts`,
90
+ nextCommand: "kubectl config get-contexts",
91
+ executionTimeMs: 0,
92
+ };
93
+ return Effect.succeed(errorResult);
94
+ },
95
+ K8sCommandError: (error) => {
96
+ const errorResult: CommandResult = {
97
+ success: false,
98
+ error: error.message,
99
+ command: error.command,
100
+ hint:
101
+ error.hint ?? "Check command syntax and ensure the target namespace/resource exists.",
102
+ executionTimeMs: 0,
103
+ };
104
+ return Effect.succeed(errorResult);
105
+ },
106
+ K8sTimeoutError: (error) => {
107
+ const errorResult: CommandResult = {
108
+ success: false,
109
+ error: error.message,
110
+ command: error.command,
111
+ hint:
112
+ error.hint ??
113
+ `Command timed out after ${error.timeoutMs}ms. Consider increasing timeoutMs in config or narrowing the query.`,
114
+ executionTimeMs: error.timeoutMs,
115
+ };
116
+ return Effect.succeed(errorResult);
117
+ },
118
+ }),
119
+ );
120
+
121
+ yield* Console.log(formatOutput({ ...result, environment: resolvedEnv }, options.format));
122
+ });
123
+
124
+ const commonFlags = {
125
+ env: Flag.optional(Flag.string("env")).pipe(
126
+ Flag.withDescription(
127
+ "Target environment (e.g. test, prod). Falls back to defaultEnvironment in config.",
128
+ ),
129
+ ),
130
+ dryRun: Flag.boolean("dry-run").pipe(
131
+ Flag.withAlias("d"),
132
+ Flag.withDescription("Show command without executing"),
133
+ Flag.withDefault(false),
134
+ ),
135
+ format: formatOption,
136
+ profile: Flag.optional(Flag.string("profile")).pipe(
137
+ Flag.withDescription("Kubernetes profile name (if multiple configured)"),
138
+ ),
139
+ };
140
+
141
+ const buildKubectlCommand = (base: string, args: ReadonlyArray<string>) => {
142
+ const extras = args.filter((part) => part.length > 0);
143
+ return extras.length === 0 ? base : `${base} ${extras.join(" ")}`;
144
+ };
145
+
146
+ const resolveStructuredNamespace = (
147
+ namespaceOption: Option.Option<string>,
148
+ envOption: Option.Option<string>,
149
+ profileOption: Option.Option<string>,
150
+ ) =>
151
+ Effect.gen(function* () {
152
+ const explicitNamespace = Option.getOrUndefined(namespaceOption);
153
+ if (explicitNamespace) return explicitNamespace;
154
+
155
+ const config = yield* ConfigService;
156
+ const resolvedEnv = yield* resolveEnv(envOption, config);
157
+ const profileName = Option.getOrUndefined(profileOption);
158
+ const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes", profileName);
159
+
160
+ if (!k8sConfig) return undefined;
161
+
162
+ return k8sConfig.namespaces[resolvedEnv];
163
+ });
12
164
 
13
165
  const kubectlCommand = Command.make(
14
166
  "kubectl",
15
167
  {
16
- env: Flag.choice("env", ["test", "prod"]).pipe(
17
- Flag.withDescription("Target environment: test or prod"),
18
- ),
168
+ ...commonFlags,
19
169
  cmd: Flag.string("cmd").pipe(
20
170
  Flag.withDescription('kubectl command (without "kubectl" prefix)'),
21
171
  ),
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
172
  },
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
- }),
173
+ ({ cmd, dryRun, env, format, profile }) => runK8sCommand(cmd, { dryRun, env, format, profile }),
82
174
  ).pipe(
83
175
  Command.withDescription(
84
176
  `Kubernetes CLI Tool for Coding Agents
@@ -130,9 +222,169 @@ RELATED TOOLS:
130
222
  ),
131
223
  );
132
224
 
225
+ const podsCommand = Command.make(
226
+ "pods",
227
+ {
228
+ ...commonFlags,
229
+ namespace: Flag.string("namespace").pipe(
230
+ Flag.withDescription("Namespace to query"),
231
+ Flag.optional,
232
+ ),
233
+ label: Flag.string("label").pipe(
234
+ Flag.withDescription("Label selector (key=value)"),
235
+ Flag.optional,
236
+ ),
237
+ wide: Flag.boolean("wide").pipe(
238
+ Flag.withDescription("Show additional pod information"),
239
+ Flag.withDefault(false),
240
+ ),
241
+ },
242
+ ({ dryRun, env, format, label, namespace, profile, wide }) =>
243
+ Effect.gen(function* () {
244
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
245
+ const command = buildKubectlCommand("get pods", [
246
+ Option.match(label, {
247
+ onNone: () => "",
248
+ onSome: (value) => `-l ${value}`,
249
+ }),
250
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
251
+ wide ? "-o wide" : "",
252
+ ]);
253
+ return yield* runK8sCommand(command, { dryRun, env, format, profile });
254
+ }),
255
+ ).pipe(Command.withDescription("List pods (get pods) with optional namespace/label/wide output"));
256
+
257
+ const logsCommand = Command.make(
258
+ "logs",
259
+ {
260
+ ...commonFlags,
261
+ pod: Flag.string("pod").pipe(Flag.withDescription("Pod name")),
262
+ namespace: Flag.string("namespace").pipe(
263
+ Flag.withDescription("Namespace containing the pod"),
264
+ Flag.optional,
265
+ ),
266
+ container: Flag.string("container").pipe(
267
+ Flag.withDescription("Container name (for multi-container pods)"),
268
+ Flag.optional,
269
+ ),
270
+ tail: Flag.integer("tail").pipe(Flag.withDescription("Show last N log lines"), Flag.optional),
271
+ follow: Flag.boolean("follow").pipe(
272
+ Flag.withAlias("f"),
273
+ Flag.withDescription("Stream logs in real time"),
274
+ Flag.withDefault(false),
275
+ ),
276
+ },
277
+ ({ container, dryRun, env, follow, format, namespace, pod, profile, tail }) =>
278
+ Effect.gen(function* () {
279
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
280
+ const command = buildKubectlCommand(`logs ${pod}`, [
281
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
282
+ Option.match(container, {
283
+ onNone: () => "",
284
+ onSome: (value) => `-c ${value}`,
285
+ }),
286
+ Option.match(tail, {
287
+ onNone: () => "",
288
+ onSome: (value) => `--tail=${value}`,
289
+ }),
290
+ follow ? "-f" : "",
291
+ ]);
292
+ return yield* runK8sCommand(command, { dryRun, env, format, profile });
293
+ }),
294
+ ).pipe(Command.withDescription("Fetch pod logs with tail/follow/container selectors"));
295
+
296
+ const describeCommand = Command.make(
297
+ "describe",
298
+ {
299
+ ...commonFlags,
300
+ resource: Flag.string("resource").pipe(
301
+ Flag.withDescription("Resource type (pod, deploy, svc, etc.)"),
302
+ ),
303
+ name: Flag.string("name").pipe(Flag.withDescription("Resource name")),
304
+ namespace: Flag.string("namespace").pipe(
305
+ Flag.withDescription("Namespace containing the resource"),
306
+ Flag.optional,
307
+ ),
308
+ },
309
+ ({ dryRun, env, format, name, namespace, profile, resource }) =>
310
+ Effect.gen(function* () {
311
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
312
+ const command = buildKubectlCommand(`describe ${resource} ${name}`, [
313
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
314
+ ]);
315
+ return yield* runK8sCommand(command, { dryRun, env, format, profile });
316
+ }),
317
+ ).pipe(Command.withDescription("Describe a Kubernetes resource by type and name"));
318
+
319
+ const execCommand = Command.make(
320
+ "exec",
321
+ {
322
+ ...commonFlags,
323
+ pod: Flag.string("pod").pipe(Flag.withDescription("Pod name")),
324
+ execCmd: Flag.string("exec-cmd").pipe(
325
+ Flag.withDescription("Command to run inside the pod; wrap in quotes for spaces"),
326
+ ),
327
+ namespace: Flag.string("namespace").pipe(
328
+ Flag.withDescription("Namespace containing the pod"),
329
+ Flag.optional,
330
+ ),
331
+ container: Flag.string("container").pipe(
332
+ Flag.withDescription("Container name (for multi-container pods)"),
333
+ Flag.optional,
334
+ ),
335
+ },
336
+ ({ container, dryRun, env, execCmd, format, namespace, pod, profile }) =>
337
+ Effect.gen(function* () {
338
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
339
+ const command = buildKubectlCommand(`exec ${pod}`, [
340
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
341
+ Option.match(container, {
342
+ onNone: () => "",
343
+ onSome: (value) => `-c ${value}`,
344
+ }),
345
+ `-- ${execCmd}`,
346
+ ]);
347
+ return yield* runK8sCommand(command, { dryRun, env, format, profile });
348
+ }),
349
+ ).pipe(Command.withDescription("Execute a command in a pod (kubectl exec <pod> -- <cmd>)"));
350
+
351
+ const topCommand = Command.make(
352
+ "top",
353
+ {
354
+ ...commonFlags,
355
+ namespace: Flag.string("namespace").pipe(
356
+ Flag.withDescription("Namespace to inspect"),
357
+ Flag.optional,
358
+ ),
359
+ sortBy: Flag.choice("sort-by", ["cpu", "memory"] as const).pipe(
360
+ Flag.withDescription("Sort metrics output when supported by kubectl"),
361
+ Flag.optional,
362
+ ),
363
+ },
364
+ ({ dryRun, env, format, namespace, profile, sortBy }) =>
365
+ Effect.gen(function* () {
366
+ const resolvedNamespace = yield* resolveStructuredNamespace(namespace, env, profile);
367
+ const command = buildKubectlCommand("top pod", [
368
+ resolvedNamespace ? `-n ${resolvedNamespace}` : "",
369
+ Option.match(sortBy, {
370
+ onNone: () => "",
371
+ onSome: (value) => `--sort-by=${value}`,
372
+ }),
373
+ ]);
374
+ return yield* runK8sCommand(command, { dryRun, env, format, profile });
375
+ }),
376
+ ).pipe(Command.withDescription("Show pod CPU/memory usage (kubectl top pod)"));
377
+
133
378
  const mainCommand = Command.make("k8s-tool", {}).pipe(
134
379
  Command.withDescription("Kubernetes CLI Tool for Coding Agents"),
135
- Command.withSubcommands([kubectlCommand]),
380
+ Command.withSubcommands([
381
+ kubectlCommand,
382
+ podsCommand,
383
+ logsCommand,
384
+ describeCommand,
385
+ execCommand,
386
+ topCommand,
387
+ ]),
136
388
  );
137
389
 
138
390
  const cli = Command.run(mainCommand, {
@@ -4,8 +4,8 @@ import { Effect, Layer, Option, Ref, ServiceMap, Stream } from "effect";
4
4
  import type { CommandResult, Environment } from "./types";
5
5
 
6
6
  import { K8sCommandError, K8sContextError, K8sTimeoutError } from "./errors";
7
- import { ConfigService, getToolConfig } from "../config";
8
- import type { K8sConfig } from "../config";
7
+ import { ConfigService, getToolConfig } from "#src/config";
8
+ import type { K8sConfig } from "#src/config";
9
9
 
10
10
  export class K8sService extends ServiceMap.Service<
11
11
  K8sService,
@@ -6,4 +6,8 @@ export type CommandResult = {
6
6
  error?: string;
7
7
  command?: string;
8
8
  executionTimeMs: number;
9
+ hint?: string;
10
+ nextCommand?: string;
11
+ retryable?: boolean;
12
+ environment?: string;
9
13
  };
@@ -5,16 +5,25 @@ export class LogsNotFoundError extends Schema.TaggedErrorClass<LogsNotFoundError
5
5
  {
6
6
  message: Schema.String,
7
7
  path: Schema.String,
8
+ hint: Schema.optionalKey(Schema.String),
9
+ nextCommand: Schema.optionalKey(Schema.String),
10
+ retryable: Schema.optionalKey(Schema.Boolean),
8
11
  },
9
12
  ) {}
10
13
 
11
14
  export class LogsReadError extends Schema.TaggedErrorClass<LogsReadError>()("LogsReadError", {
12
15
  message: Schema.String,
13
16
  source: Schema.String,
17
+ hint: Schema.optionalKey(Schema.String),
18
+ nextCommand: Schema.optionalKey(Schema.String),
19
+ retryable: Schema.optionalKey(Schema.Boolean),
14
20
  }) {}
15
21
 
16
22
  export class LogsConfigError extends Schema.TaggedErrorClass<LogsConfigError>()("LogsConfigError", {
17
23
  message: Schema.String,
24
+ hint: Schema.optionalKey(Schema.String),
25
+ nextCommand: Schema.optionalKey(Schema.String),
26
+ retryable: Schema.optionalKey(Schema.Boolean),
18
27
  }) {}
19
28
 
20
29
  export class LogsTimeoutError extends Schema.TaggedErrorClass<LogsTimeoutError>()(
@@ -23,6 +32,9 @@ export class LogsTimeoutError extends Schema.TaggedErrorClass<LogsTimeoutError>(
23
32
  message: Schema.String,
24
33
  source: Schema.String,
25
34
  timeoutMs: Schema.Number,
35
+ hint: Schema.optionalKey(Schema.String),
36
+ nextCommand: Schema.optionalKey(Schema.String),
37
+ retryable: Schema.optionalKey(Schema.Boolean),
26
38
  },
27
39
  ) {}
28
40
 
@@ -20,8 +20,9 @@ import { Console, Effect, Layer, Option, Result } from "effect";
20
20
 
21
21
  import type { Environment, LogResult, ReadOptions } from "./types";
22
22
 
23
- import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "../shared";
24
- import { LogsNotFoundError } from "./errors";
23
+ import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "#src/shared";
24
+ import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#src/config";
25
+ import { LogsConfigError, LogsNotFoundError, LogsReadError, LogsTimeoutError } from "./errors";
25
26
  import { LogsService, LogsServiceLayer } from "./service";
26
27
 
27
28
  const profileOption = Flag.optional(
@@ -32,6 +33,36 @@ const profileOption = Flag.optional(
32
33
  ),
33
34
  );
34
35
 
36
+ /**
37
+ * Resolve environment from explicit --env flag, config defaultEnvironment, or fail with hint.
38
+ */
39
+ const resolveEnv = (envOption: Option.Option<string>) =>
40
+ Effect.gen(function* () {
41
+ const explicit = Option.getOrUndefined(envOption);
42
+ if (explicit) return explicit;
43
+
44
+ const config = yield* ConfigService;
45
+ const defaultEnv = getDefaultEnvironment(config);
46
+
47
+ if (defaultEnv === "prod") {
48
+ return yield* new LogsConfigError({
49
+ message:
50
+ "Implicit prod access blocked. Config defaultEnvironment is 'prod' but --env was not passed explicitly.",
51
+ hint: "Pass --env prod explicitly to confirm production access, or change defaultEnvironment to a non-prod value.",
52
+ nextCommand: "agent-tools-logs list --env prod",
53
+ });
54
+ }
55
+
56
+ if (defaultEnv) return defaultEnv;
57
+
58
+ return yield* new LogsConfigError({
59
+ message:
60
+ "No environment specified. Use --env <name> or set defaultEnvironment in agent-tools.json5.",
61
+ hint: 'Set defaultEnvironment in agent-tools.json5 (e.g. defaultEnvironment: "local") or pass --env explicitly.',
62
+ nextCommand: "agent-tools-logs list --env local",
63
+ });
64
+ });
65
+
35
66
  const buildSource = (
36
67
  mode: "list" | "read",
37
68
  env: Environment,
@@ -56,20 +87,23 @@ const buildSource = (
56
87
  const listCommand = Command.make(
57
88
  "list",
58
89
  {
59
- env: Flag.choice("env", ["local", "test", "prod"]).pipe(
60
- Flag.withDescription("Target environment"),
90
+ env: Flag.optional(Flag.string("env")).pipe(
91
+ Flag.withDescription(
92
+ "Target environment (e.g. local, test, prod). Falls back to defaultEnvironment in config.",
93
+ ),
61
94
  ),
62
95
  format: formatOption,
63
96
  profile: profileOption,
64
97
  },
65
98
  ({ env, format, profile }) =>
66
99
  Effect.gen(function* () {
100
+ const resolvedEnv = yield* resolveEnv(env);
67
101
  const logsService = yield* LogsService;
68
102
  const startTime = Date.now();
69
103
  const profileName = Option.getOrUndefined(profile);
70
104
 
71
105
  const result = yield* logsService
72
- .listLogs(env as Environment, profileName)
106
+ .listLogs(resolvedEnv as Environment, profileName)
73
107
  .pipe(Effect.result);
74
108
  const executionTimeMs = Date.now() - startTime;
75
109
 
@@ -79,11 +113,22 @@ const listCommand = Command.make(
79
113
  error: error.message,
80
114
  source: error instanceof LogsNotFoundError ? error.path : undefined,
81
115
  executionTimeMs,
116
+ hint:
117
+ error.hint ??
118
+ (error instanceof LogsReadError && error.source === "config"
119
+ ? "Add a 'logs' section to agent-tools.json5 with localDir and remotePath."
120
+ : undefined),
121
+ nextCommand:
122
+ error.nextCommand ??
123
+ (error instanceof LogsReadError && error.source === "config"
124
+ ? "agent-tools-logs list --env local"
125
+ : undefined),
126
+ retryable: error.retryable ?? (error instanceof LogsTimeoutError ? true : undefined),
82
127
  }),
83
128
  onSuccess: (data) => ({
84
129
  success: true,
85
130
  data,
86
- source: buildSource("list", env as Environment, undefined, null),
131
+ source: buildSource("list", resolvedEnv as Environment, undefined, null),
87
132
  executionTimeMs,
88
133
  }),
89
134
  });
@@ -95,8 +140,10 @@ const listCommand = Command.make(
95
140
  const readCommand = Command.make(
96
141
  "read",
97
142
  {
98
- env: Flag.choice("env", ["local", "test", "prod"]).pipe(
99
- Flag.withDescription("Target environment"),
143
+ env: Flag.optional(Flag.string("env")).pipe(
144
+ Flag.withDescription(
145
+ "Target environment (e.g. local, test, prod). Falls back to defaultEnvironment in config.",
146
+ ),
100
147
  ),
101
148
  file: Flag.string("file").pipe(
102
149
  Flag.withDescription("Specific log file to read"),
@@ -119,6 +166,7 @@ const readCommand = Command.make(
119
166
  },
120
167
  ({ env, file, format, grep, pretty, profile, tail }) =>
121
168
  Effect.gen(function* () {
169
+ const resolvedEnv = yield* resolveEnv(env);
122
170
  const logsService = yield* LogsService;
123
171
  const startTime = Date.now();
124
172
  const profileName = Option.getOrUndefined(profile);
@@ -131,7 +179,7 @@ const readCommand = Command.make(
131
179
  };
132
180
 
133
181
  const result = yield* logsService
134
- .readLogs(env as Environment, readOptions, profileName)
182
+ .readLogs(resolvedEnv as Environment, readOptions, profileName)
135
183
  .pipe(Effect.result);
136
184
  const executionTimeMs = Date.now() - startTime;
137
185
 
@@ -141,11 +189,22 @@ const readCommand = Command.make(
141
189
  error: error.message,
142
190
  source: error instanceof LogsNotFoundError ? error.path : undefined,
143
191
  executionTimeMs,
192
+ hint:
193
+ error.hint ??
194
+ (error instanceof LogsReadError && error.source === "config"
195
+ ? "Add a 'logs' section to agent-tools.json5 with localDir and remotePath."
196
+ : undefined),
197
+ nextCommand:
198
+ error.nextCommand ??
199
+ (error instanceof LogsReadError && error.source === "config"
200
+ ? "agent-tools-logs read --env local --file app.log"
201
+ : undefined),
202
+ retryable: error.retryable ?? (error instanceof LogsTimeoutError ? true : undefined),
144
203
  }),
145
204
  onSuccess: (data) => ({
146
205
  success: true,
147
206
  data,
148
- source: buildSource("read", env as Environment, undefined, readOptions),
207
+ source: buildSource("read", resolvedEnv as Environment, undefined, readOptions),
149
208
  executionTimeMs,
150
209
  }),
151
210
  });
@@ -167,7 +226,10 @@ export const run = Command.runWith(mainCommand, {
167
226
  version: VERSION,
168
227
  });
169
228
 
170
- const MainLayer = LogsServiceLayer.pipe(Layer.provideMerge(BunServices.layer));
229
+ const MainLayer = LogsServiceLayer.pipe(
230
+ Layer.provideMerge(ConfigServiceLayer),
231
+ Layer.provideMerge(BunServices.layer),
232
+ );
171
233
 
172
234
  const program = cli.pipe(Effect.provide(MainLayer), Effect.tapCause(renderCauseToStderr));
173
235
 
@@ -3,10 +3,10 @@ import { Effect, Layer, Result, ServiceMap, Stream } from "effect";
3
3
 
4
4
  import type { Environment, LogFile, ReadOptions } from "./types";
5
5
 
6
- import { K8sCommandError } from "../k8s-tool/errors";
7
- import { K8sService, K8sServiceLayer } from "../k8s-tool/service";
8
- import { ConfigService, ConfigServiceLayer, getToolConfig } from "../config/loader";
9
- import type { LogsConfig } from "../config/types";
6
+ import { K8sCommandError } from "#src/k8s-tool/errors";
7
+ import { K8sService, K8sServiceLayer } from "#src/k8s-tool/service";
8
+ import { ConfigService, ConfigServiceLayer, getToolConfig } from "#src/config/loader";
9
+ import type { LogsConfig } from "#src/config/types";
10
10
  import { LogsNotFoundError, LogsReadError, type LogsError } from "./errors";
11
11
 
12
12
  export const parseLogFiles = (output: string): LogFile[] => {
@@ -1,4 +1,4 @@
1
- import type { Environment, OutputFormat } from "../shared";
1
+ import type { Environment, OutputFormat } from "#src/shared";
2
2
  export type { Environment, OutputFormat };
3
3
 
4
4
  export type LogFile = {
@@ -20,6 +20,9 @@ export type LogResult = {
20
20
  error?: string;
21
21
  source?: string;
22
22
  executionTimeMs: number;
23
+ hint?: string;
24
+ nextCommand?: string;
25
+ retryable?: boolean;
23
26
  };
24
27
 
25
28
  export type ParsedArgs =
@@ -2,7 +2,7 @@ import { Effect, Layer, ServiceMap } from "effect";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
5
- import { loadConfig } from "../config/loader";
5
+ import { loadConfig } from "#src/config/loader";
6
6
 
7
7
  /**
8
8
  * Resolves the OpenCode storage base path from config or default.
@@ -13,7 +13,7 @@ import { Console, Effect, Layer, Result } from "effect";
13
13
 
14
14
  import type { MessageSummary, SessionResult } from "./types";
15
15
 
16
- import { formatOption, formatOutput, VERSION } from "../shared";
16
+ import { formatOption, formatOutput, VERSION } from "#src/shared";
17
17
  import { ResolvedPaths, ResolvedPathsLayer } from "./config";
18
18
  import { SessionStorageNotFoundError } from "./errors";
19
19
  import { formatDate, SessionService, SessionServiceLayer, truncate } from "./service";