@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,227 @@
1
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2
+ import { Effect, Layer, Option, Ref, ServiceMap, Stream } from "effect";
3
+
4
+ import type { CommandResult, Environment } from "./types";
5
+
6
+ import { K8sCommandError, K8sContextError, K8sTimeoutError } from "./errors";
7
+ import { ConfigService, getToolConfig } from "../config";
8
+ import type { K8sConfig } from "../config";
9
+
10
+ export class K8sService extends ServiceMap.Service<
11
+ K8sService,
12
+ {
13
+ readonly runCommand: (
14
+ cmd: string,
15
+ env: Environment,
16
+ ) => Effect.Effect<string, K8sContextError | K8sCommandError | K8sTimeoutError>;
17
+ readonly runKubectl: (
18
+ cmd: string,
19
+ dryRun: boolean,
20
+ ) => Effect.Effect<CommandResult, K8sContextError | K8sCommandError | K8sTimeoutError>;
21
+ }
22
+ >()("@agent-tools/K8sService") {
23
+ static readonly layer = Layer.effect(
24
+ K8sService,
25
+ Effect.scoped(
26
+ Effect.gen(function* () {
27
+ const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
28
+
29
+ const config = yield* ConfigService;
30
+ const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes");
31
+
32
+ if (!k8sConfig) {
33
+ const noConfigError = new K8sContextError({
34
+ message:
35
+ "No Kubernetes configuration found. Add a 'kubernetes' section to agent-tools.json5.",
36
+ clusterId: "unknown",
37
+ });
38
+ return {
39
+ runCommand: (_cmd: string, _env: Environment) => Effect.fail(noConfigError),
40
+ runKubectl: (_cmd: string, _dryRun: boolean) => Effect.fail(noConfigError),
41
+ };
42
+ }
43
+
44
+ const KUBECTL_TIMEOUT_MS = k8sConfig.timeoutMs ?? 60000;
45
+
46
+ // Create Ref for context caching (replaces module-level let)
47
+ const contextRef = yield* Ref.make<string | null>(null);
48
+
49
+ // Helper that uses executor.spawn() to avoid ChildProcessSpawner requirement in return type
50
+ const runShellCommand = (commandStr: string, timeoutMs: number) =>
51
+ Effect.scoped(
52
+ Effect.gen(function* () {
53
+ const command = ChildProcess.make("sh", ["-c", commandStr], {
54
+ stdout: "pipe",
55
+ stderr: "pipe",
56
+ });
57
+ const process = yield* executor.spawn(command);
58
+
59
+ const stdoutChunk = yield* process.stdout.pipe(
60
+ Stream.decodeText(),
61
+ Stream.runCollect,
62
+ );
63
+ const stdout = stdoutChunk.join("");
64
+
65
+ const stderrChunk = yield* process.stderr.pipe(
66
+ Stream.decodeText(),
67
+ Stream.runCollect,
68
+ );
69
+ const stderr = stderrChunk.join("");
70
+
71
+ const exitCode = yield* process.exitCode;
72
+
73
+ return { stdout, stderr, exitCode };
74
+ }),
75
+ ).pipe(
76
+ Effect.timeoutOption(timeoutMs),
77
+ Effect.mapError(
78
+ (platformError) =>
79
+ new K8sCommandError({
80
+ message: `Command execution failed: ${String(platformError)}`,
81
+ command: commandStr,
82
+ exitCode: -1,
83
+ stderr: undefined,
84
+ }),
85
+ ),
86
+ );
87
+
88
+ const resolveContext = Effect.fn("K8sService.resolveContext")(function* () {
89
+ // Check cache first
90
+ const cached = yield* Ref.get(contextRef);
91
+ if (cached !== null) {
92
+ return cached;
93
+ }
94
+
95
+ const jqCommand = `kubectl config view -o json | jq -r '.contexts[] | select(.context.cluster == "${k8sConfig.clusterId}") | .name' | head -1`;
96
+
97
+ const contextResultOption = yield* runShellCommand(jqCommand, KUBECTL_TIMEOUT_MS);
98
+
99
+ if (Option.isNone(contextResultOption)) {
100
+ return yield* new K8sTimeoutError({
101
+ message: `Context resolution timed out after ${KUBECTL_TIMEOUT_MS}ms`,
102
+ command: jqCommand,
103
+ timeoutMs: KUBECTL_TIMEOUT_MS,
104
+ });
105
+ }
106
+
107
+ const contextResult = contextResultOption.value;
108
+
109
+ if (contextResult.exitCode === 0 && contextResult.stdout.trim()) {
110
+ const resolvedContextValue = contextResult.stdout.trim();
111
+ yield* Ref.set(contextRef, resolvedContextValue);
112
+ return resolvedContextValue;
113
+ }
114
+
115
+ const fallbackCommand = `kubectl config view -o json | jq -r '.contexts[] as $ctx | .clusters[] | select(.name == $ctx.context.cluster and (.cluster.server | contains("${k8sConfig.clusterId}"))) | $ctx.name' | head -1`;
116
+
117
+ const fallbackResultOption = yield* runShellCommand(fallbackCommand, KUBECTL_TIMEOUT_MS);
118
+
119
+ if (Option.isNone(fallbackResultOption)) {
120
+ return yield* new K8sTimeoutError({
121
+ message: `Context resolution timed out after ${KUBECTL_TIMEOUT_MS}ms`,
122
+ command: fallbackCommand,
123
+ timeoutMs: KUBECTL_TIMEOUT_MS,
124
+ });
125
+ }
126
+
127
+ const fallbackResult = fallbackResultOption.value;
128
+
129
+ if (fallbackResult.exitCode === 0 && fallbackResult.stdout.trim()) {
130
+ const resolvedContextValue = fallbackResult.stdout.trim();
131
+ yield* Ref.set(contextRef, resolvedContextValue);
132
+ return resolvedContextValue;
133
+ }
134
+
135
+ return yield* new K8sContextError({
136
+ message: `No kubectl context found for cluster ID: ${k8sConfig.clusterId}. Make sure you have the cluster configured in kubectl.`,
137
+ clusterId: k8sConfig.clusterId,
138
+ });
139
+ });
140
+
141
+ const executeCommand = Effect.fn("K8sService.executeCommand")(function* (cmd: string) {
142
+ const context = yield* resolveContext();
143
+ const fullCommand = `kubectl --context ${context} ${cmd}`;
144
+
145
+ const resultOption = yield* runShellCommand(fullCommand, KUBECTL_TIMEOUT_MS);
146
+
147
+ if (Option.isNone(resultOption)) {
148
+ return yield* new K8sTimeoutError({
149
+ message: `Command timed out after ${KUBECTL_TIMEOUT_MS}ms`,
150
+ command: fullCommand,
151
+ timeoutMs: KUBECTL_TIMEOUT_MS,
152
+ });
153
+ }
154
+
155
+ const result = resultOption.value;
156
+
157
+ return {
158
+ stdout: result.stdout,
159
+ stderr: result.stderr,
160
+ exitCode: result.exitCode,
161
+ command: fullCommand,
162
+ };
163
+ });
164
+
165
+ const runCommand = Effect.fn("K8sService.runCommand")(function* (
166
+ cmd: string,
167
+ _env: Environment,
168
+ ) {
169
+ const result = yield* executeCommand(cmd);
170
+
171
+ if (result.exitCode !== 0) {
172
+ return yield* new K8sCommandError({
173
+ message: result.stderr ?? `kubectl exited with code ${result.exitCode}`,
174
+ command: result.command,
175
+ exitCode: result.exitCode,
176
+ stderr: result.stderr ?? undefined,
177
+ });
178
+ }
179
+
180
+ return result.stdout.trim();
181
+ });
182
+
183
+ const runKubectl = Effect.fn("K8sService.runKubectl")(function* (
184
+ cmd: string,
185
+ dryRun: boolean,
186
+ ) {
187
+ const startTime = Date.now();
188
+
189
+ if (dryRun) {
190
+ const context = yield* resolveContext();
191
+ const fullCommand = `kubectl --context ${context} ${cmd}`;
192
+ return {
193
+ success: true,
194
+ command: fullCommand,
195
+ output: "(dry run - command not executed)",
196
+ executionTimeMs: Date.now() - startTime,
197
+ };
198
+ }
199
+
200
+ const result = yield* executeCommand(cmd);
201
+
202
+ if (result.exitCode !== 0) {
203
+ return yield* new K8sCommandError({
204
+ message: result.stderr ?? `kubectl exited with code ${result.exitCode}`,
205
+ command: result.command,
206
+ exitCode: result.exitCode,
207
+ stderr: result.stderr ?? undefined,
208
+ });
209
+ }
210
+
211
+ return {
212
+ success: true,
213
+ output: result.stdout.trim(),
214
+ command: result.command,
215
+ executionTimeMs: Date.now() - startTime,
216
+ };
217
+ });
218
+
219
+ return { runCommand, runKubectl };
220
+ }),
221
+ ),
222
+ );
223
+ }
224
+
225
+ export const K8sServiceLayer = K8sService.layer;
226
+
227
+ export { K8sCommandError } from "./errors";
@@ -0,0 +1,9 @@
1
+ export type Environment = "test" | "prod";
2
+
3
+ export type CommandResult = {
4
+ success: boolean;
5
+ output?: string;
6
+ error?: string;
7
+ command?: string;
8
+ executionTimeMs: number;
9
+ };
@@ -0,0 +1,29 @@
1
+ import { Schema } from "effect";
2
+
3
+ export class LogsNotFoundError extends Schema.TaggedErrorClass<LogsNotFoundError>()(
4
+ "LogsNotFoundError",
5
+ {
6
+ message: Schema.String,
7
+ path: Schema.String,
8
+ },
9
+ ) {}
10
+
11
+ export class LogsReadError extends Schema.TaggedErrorClass<LogsReadError>()("LogsReadError", {
12
+ message: Schema.String,
13
+ source: Schema.String,
14
+ }) {}
15
+
16
+ export class LogsConfigError extends Schema.TaggedErrorClass<LogsConfigError>()("LogsConfigError", {
17
+ message: Schema.String,
18
+ }) {}
19
+
20
+ export class LogsTimeoutError extends Schema.TaggedErrorClass<LogsTimeoutError>()(
21
+ "LogsTimeoutError",
22
+ {
23
+ message: Schema.String,
24
+ source: Schema.String,
25
+ timeoutMs: Schema.Number,
26
+ },
27
+ ) {}
28
+
29
+ export type LogsError = LogsNotFoundError | LogsReadError | LogsConfigError | LogsTimeoutError;
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Application Logs Tool for Coding Agents
5
+ *
6
+ * Reads application logs from local development or test/prod environments.
7
+ * For test/prod, uses k8s-tool internally to access logs from PVC.
8
+ *
9
+ * Run with --help for full usage documentation.
10
+ *
11
+ * IMPORTANT FOR AI AGENTS:
12
+ * Use this tool to investigate application behavior and errors.
13
+ * Always start with --list to see available log files.
14
+ * Use --format toon for LLM-optimized output (fewer tokens).
15
+ */
16
+
17
+ import { Command, Flag } from "effect/unstable/cli";
18
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
19
+ import { Console, Effect, Layer, Option, Result } from "effect";
20
+
21
+ import type { Environment, LogResult, ReadOptions } from "./types";
22
+
23
+ import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "../shared";
24
+ import { LogsNotFoundError } from "./errors";
25
+ import { LogsService, LogsServiceLayer } from "./service";
26
+
27
+ const profileOption = Flag.optional(
28
+ Flag.string("profile").pipe(
29
+ Flag.withDescription(
30
+ "Named profile from agent-tools.json5 logs section (default: 'default' key or single entry)",
31
+ ),
32
+ ),
33
+ );
34
+
35
+ const buildSource = (
36
+ mode: "list" | "read",
37
+ env: Environment,
38
+ logsConfig: { localDir: string; remotePath: string } | undefined,
39
+ options: ReadOptions | null,
40
+ ): string | undefined => {
41
+ if (!logsConfig) return undefined;
42
+
43
+ if (mode === "list") {
44
+ return env === "local" ? logsConfig.localDir : `${env}:${logsConfig.remotePath}`;
45
+ }
46
+
47
+ if (env === "local") {
48
+ const fileName = options?.file ?? "latest";
49
+ return `${logsConfig.localDir}/${fileName}`;
50
+ }
51
+
52
+ const fileName = options?.file ?? "app.log";
53
+ return `${env}:${logsConfig.remotePath}/${fileName}`;
54
+ };
55
+
56
+ const listCommand = Command.make(
57
+ "list",
58
+ {
59
+ env: Flag.choice("env", ["local", "test", "prod"]).pipe(
60
+ Flag.withDescription("Target environment"),
61
+ ),
62
+ format: formatOption,
63
+ profile: profileOption,
64
+ },
65
+ ({ env, format, profile }) =>
66
+ Effect.gen(function* () {
67
+ const logsService = yield* LogsService;
68
+ const startTime = Date.now();
69
+ const profileName = Option.getOrUndefined(profile);
70
+
71
+ const result = yield* logsService
72
+ .listLogs(env as Environment, profileName)
73
+ .pipe(Effect.result);
74
+ const executionTimeMs = Date.now() - startTime;
75
+
76
+ const logResult: LogResult = Result.match(result, {
77
+ onFailure: (error) => ({
78
+ success: false,
79
+ error: error.message,
80
+ source: error instanceof LogsNotFoundError ? error.path : undefined,
81
+ executionTimeMs,
82
+ }),
83
+ onSuccess: (data) => ({
84
+ success: true,
85
+ data,
86
+ source: buildSource("list", env as Environment, undefined, null),
87
+ executionTimeMs,
88
+ }),
89
+ });
90
+
91
+ yield* Console.log(formatOutput(logResult, format));
92
+ }),
93
+ ).pipe(Command.withDescription("List available log files"));
94
+
95
+ const readCommand = Command.make(
96
+ "read",
97
+ {
98
+ env: Flag.choice("env", ["local", "test", "prod"]).pipe(
99
+ Flag.withDescription("Target environment"),
100
+ ),
101
+ file: Flag.string("file").pipe(
102
+ Flag.withDescription("Specific log file to read"),
103
+ Flag.optional,
104
+ ),
105
+ format: formatOption,
106
+ grep: Flag.string("grep").pipe(
107
+ Flag.withDescription("Filter lines containing pattern"),
108
+ Flag.optional,
109
+ ),
110
+ pretty: Flag.boolean("pretty").pipe(
111
+ Flag.withDescription("Pretty-print JSON log entries"),
112
+ Flag.withDefault(false),
113
+ ),
114
+ profile: profileOption,
115
+ tail: Flag.integer("tail").pipe(
116
+ Flag.withDescription("Show last N lines"),
117
+ Flag.withDefault(100),
118
+ ),
119
+ },
120
+ ({ env, file, format, grep, pretty, profile, tail }) =>
121
+ Effect.gen(function* () {
122
+ const logsService = yield* LogsService;
123
+ const startTime = Date.now();
124
+ const profileName = Option.getOrUndefined(profile);
125
+
126
+ const readOptions: ReadOptions = {
127
+ tail,
128
+ grep: Option.getOrUndefined(grep),
129
+ file: Option.getOrUndefined(file),
130
+ pretty,
131
+ };
132
+
133
+ const result = yield* logsService
134
+ .readLogs(env as Environment, readOptions, profileName)
135
+ .pipe(Effect.result);
136
+ const executionTimeMs = Date.now() - startTime;
137
+
138
+ const logResult: LogResult = Result.match(result, {
139
+ onFailure: (error) => ({
140
+ success: false,
141
+ error: error.message,
142
+ source: error instanceof LogsNotFoundError ? error.path : undefined,
143
+ executionTimeMs,
144
+ }),
145
+ onSuccess: (data) => ({
146
+ success: true,
147
+ data,
148
+ source: buildSource("read", env as Environment, undefined, readOptions),
149
+ executionTimeMs,
150
+ }),
151
+ });
152
+
153
+ yield* Console.log(formatOutput(logResult, format));
154
+ }),
155
+ ).pipe(Command.withDescription("Read application logs"));
156
+
157
+ const mainCommand = Command.make("logs-tool", {}).pipe(
158
+ Command.withDescription("Application Logs Tool for Coding Agents"),
159
+ Command.withSubcommands([listCommand, readCommand]),
160
+ );
161
+
162
+ const cli = Command.run(mainCommand, {
163
+ version: VERSION,
164
+ });
165
+
166
+ export const run = Command.runWith(mainCommand, {
167
+ version: VERSION,
168
+ });
169
+
170
+ const MainLayer = LogsServiceLayer.pipe(Layer.provideMerge(BunServices.layer));
171
+
172
+ const program = cli.pipe(Effect.provide(MainLayer), Effect.tapCause(renderCauseToStderr));
173
+
174
+ BunRuntime.runMain(program, {
175
+ disableErrorReporting: true,
176
+ });