@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,292 @@
1
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2
+ import { Effect, Layer, ServiceMap, Stream, Option } from "effect";
3
+
4
+ import type { InvokeParams } from "./types";
5
+ import type { AzureConfig } from "../config/types";
6
+
7
+ import { DIRECT_AZ_COMMANDS, STANDALONE_AZ_COMMANDS } from "./config";
8
+ import { AzSecurityError, AzCommandError, AzTimeoutError, AzParseError } from "./errors";
9
+ import { isCommandAllowed, isInvokeAllowed } from "./security";
10
+ import { ConfigService, getToolConfig } from "../config";
11
+
12
+ export class AzService extends ServiceMap.Service<
13
+ AzService,
14
+ {
15
+ readonly runCommand: (
16
+ cmd: string,
17
+ project?: string,
18
+ ) => Effect.Effect<string, AzSecurityError | AzCommandError | AzTimeoutError | AzParseError>;
19
+ readonly runInvoke: (
20
+ params: InvokeParams,
21
+ ) => Effect.Effect<unknown, AzSecurityError | AzCommandError | AzTimeoutError | AzParseError>;
22
+ }
23
+ >()("@agent-tools/AzService") {
24
+ static readonly layer = Layer.effect(
25
+ AzService,
26
+ Effect.gen(function* () {
27
+ const config = yield* ConfigService;
28
+ const azConfig = getToolConfig<AzureConfig>(config, "azure");
29
+
30
+ if (!azConfig) {
31
+ const noConfigError = new AzCommandError({
32
+ message: "No Azure configuration found. Add an 'azure' section to agent-tools.json5.",
33
+ command: "unknown",
34
+ exitCode: -1,
35
+ stderr: undefined,
36
+ });
37
+ return {
38
+ runCommand: (_cmd: string, _project?: string) => Effect.fail(noConfigError),
39
+ runInvoke: (_params: InvokeParams) => Effect.fail(noConfigError),
40
+ };
41
+ }
42
+
43
+ const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
44
+
45
+ const runShellCommand = (fullCommand: string, timeoutMs: number) =>
46
+ Effect.scoped(
47
+ Effect.gen(function* () {
48
+ const command = ChildProcess.make("sh", ["-c", fullCommand], {
49
+ stdout: "pipe",
50
+ stderr: "pipe",
51
+ });
52
+ const process = yield* executor.spawn(command);
53
+
54
+ const stdoutChunk = yield* process.stdout.pipe(Stream.decodeText(), Stream.runCollect);
55
+ const stdout = stdoutChunk.join("");
56
+
57
+ const stderrChunk = yield* process.stderr.pipe(Stream.decodeText(), Stream.runCollect);
58
+ const stderr = stderrChunk.join("");
59
+
60
+ const exitCode = yield* process.exitCode;
61
+
62
+ return { stdout, stderr, exitCode };
63
+ }),
64
+ ).pipe(
65
+ Effect.timeoutOption(timeoutMs),
66
+ Effect.mapError(
67
+ (platformError) =>
68
+ new AzCommandError({
69
+ message: `Command execution failed: ${platformError.message}`,
70
+ command: fullCommand,
71
+ exitCode: -1,
72
+ stderr: undefined,
73
+ }),
74
+ ),
75
+ );
76
+
77
+ const resolveProject = (project?: string) => project ?? azConfig.defaultProject;
78
+
79
+ const runCommand = Effect.fn("AzService.runCommand")(function* (
80
+ cmd: string,
81
+ project?: string,
82
+ ) {
83
+ const projectName = resolveProject(project);
84
+
85
+ const securityCheck = isCommandAllowed(cmd);
86
+ if (!securityCheck.allowed) {
87
+ return yield* new AzSecurityError({
88
+ message: securityCheck.reason ?? "Command not allowed",
89
+ command: cmd,
90
+ });
91
+ }
92
+
93
+ const invokeParams = parseInvokeFromCommand(cmd);
94
+ if (invokeParams) {
95
+ const invokeResult = yield* runInvoke({
96
+ ...invokeParams,
97
+ project: projectName,
98
+ });
99
+ return JSON.stringify(invokeResult);
100
+ }
101
+
102
+ const cmdWords = cmd.trim().split(/\s+/);
103
+ const firstWord = cmdWords[0]?.toLowerCase() ?? "";
104
+ const isDirectCommand = DIRECT_AZ_COMMANDS.includes(
105
+ firstWord as (typeof DIRECT_AZ_COMMANDS)[number],
106
+ );
107
+ const isStandaloneCommand = STANDALONE_AZ_COMMANDS.includes(
108
+ firstWord as (typeof STANDALONE_AZ_COMMANDS)[number],
109
+ );
110
+
111
+ let fullCommand: string;
112
+
113
+ if (isStandaloneCommand) {
114
+ fullCommand = `az ${cmd}`;
115
+ } else if (isDirectCommand) {
116
+ fullCommand = `az ${cmd} --organization "${azConfig.organization}" --project "${projectName}"`;
117
+ } else {
118
+ fullCommand = `az devops ${cmd} --organization "${azConfig.organization}" --project "${projectName}"`;
119
+ }
120
+
121
+ const resultOption = yield* runShellCommand(fullCommand, azConfig.timeoutMs ?? 60000);
122
+
123
+ if (Option.isNone(resultOption)) {
124
+ return yield* new AzTimeoutError({
125
+ message: `Command timed out after ${azConfig.timeoutMs ?? 60000}ms`,
126
+ command: fullCommand,
127
+ timeoutMs: azConfig.timeoutMs ?? 60000,
128
+ });
129
+ }
130
+
131
+ const result = resultOption.value;
132
+
133
+ if (result.exitCode !== 0) {
134
+ return yield* new AzCommandError({
135
+ message: result.stderr || `Command failed with exit code ${result.exitCode}`,
136
+ command: fullCommand,
137
+ exitCode: result.exitCode,
138
+ stderr: result.stderr || undefined,
139
+ });
140
+ }
141
+
142
+ return result.stdout;
143
+ });
144
+
145
+ const runInvoke = Effect.fn("AzService.runInvoke")(function* (params: InvokeParams) {
146
+ const securityCheck = isInvokeAllowed(params);
147
+ if (!securityCheck.allowed) {
148
+ return yield* new AzSecurityError({
149
+ message: securityCheck.reason ?? "Invoke not allowed",
150
+ command: `invoke --area ${params.area} --resource ${params.resource}`,
151
+ });
152
+ }
153
+
154
+ let fullCommand = `az devops invoke --area ${params.area} --resource ${params.resource}`;
155
+
156
+ const projectName = resolveProject(params.project);
157
+
158
+ const routeParameters = {
159
+ project: projectName,
160
+ ...params.routeParameters,
161
+ };
162
+
163
+ if (Object.keys(routeParameters).length > 0) {
164
+ const routeParams = Object.entries(routeParameters)
165
+ .map(([k, v]) => `${k}=${v}`)
166
+ .join(" ");
167
+ fullCommand += ` --route-parameters ${routeParams}`;
168
+ }
169
+
170
+ if (params.queryParameters) {
171
+ const queryParams = Object.entries(params.queryParameters)
172
+ .map(([k, v]) => `${k}=${v}`)
173
+ .join(" ");
174
+ fullCommand += ` --query-parameters ${queryParams}`;
175
+ }
176
+
177
+ fullCommand += ` --organization "${azConfig.organization}" --output json`;
178
+
179
+ const resultOption = yield* runShellCommand(fullCommand, azConfig.timeoutMs ?? 60000);
180
+
181
+ if (Option.isNone(resultOption)) {
182
+ return yield* new AzTimeoutError({
183
+ message: `Invoke timed out after ${azConfig.timeoutMs ?? 60000}ms`,
184
+ command: fullCommand,
185
+ timeoutMs: azConfig.timeoutMs ?? 60000,
186
+ });
187
+ }
188
+
189
+ const result = resultOption.value;
190
+
191
+ if (result.exitCode !== 0) {
192
+ return yield* new AzCommandError({
193
+ message: result.stderr || `Invoke failed with exit code ${result.exitCode}`,
194
+ command: fullCommand,
195
+ exitCode: result.exitCode,
196
+ stderr: result.stderr || undefined,
197
+ });
198
+ }
199
+
200
+ const jsonData = yield* Effect.try({
201
+ try: () => JSON.parse(result.stdout) as unknown,
202
+ catch: () =>
203
+ new AzParseError({
204
+ message: `Failed to parse JSON response from invoke`,
205
+ rawOutput: result.stdout.slice(0, 500),
206
+ }),
207
+ });
208
+ return jsonData;
209
+ });
210
+
211
+ return { runCommand, runInvoke };
212
+ }),
213
+ );
214
+ }
215
+
216
+ export const AzServiceLayer = AzService.layer;
217
+
218
+ function parseInvokeFromCommand(cmd: string): InvokeParams | undefined {
219
+ const words = cmd.trim().split(/\s+/);
220
+ const loweredWords = words.map((word) => word.toLowerCase());
221
+
222
+ if (!loweredWords.includes("invoke")) {
223
+ return undefined;
224
+ }
225
+
226
+ const area = extractOptionValue(words, "--area");
227
+ const resource = extractOptionValue(words, "--resource");
228
+
229
+ if (!area || !resource) {
230
+ return undefined;
231
+ }
232
+
233
+ const routeParameters = extractParametersOption(words, "--route-parameters");
234
+ const queryParameters = extractParametersOption(words, "--query-parameters");
235
+ const apiVersion = extractOptionValue(words, "--api-version");
236
+
237
+ const mergedQueryParameters = apiVersion
238
+ ? {
239
+ ...queryParameters,
240
+ "api-version": apiVersion,
241
+ }
242
+ : queryParameters;
243
+
244
+ return {
245
+ area,
246
+ resource,
247
+ ...(routeParameters ? { routeParameters } : {}),
248
+ ...(mergedQueryParameters ? { queryParameters: mergedQueryParameters } : {}),
249
+ };
250
+ }
251
+
252
+ function extractOptionValue(args: readonly string[], optionName: string): string | undefined {
253
+ const optionIndex = args.findIndex((arg) => arg.toLowerCase() === optionName.toLowerCase());
254
+
255
+ if (optionIndex === -1) {
256
+ return undefined;
257
+ }
258
+
259
+ return args[optionIndex + 1];
260
+ }
261
+
262
+ function extractParametersOption(
263
+ args: readonly string[],
264
+ optionName: string,
265
+ ): Record<string, string | number> | undefined {
266
+ const optionIndex = args.findIndex((arg) => arg.toLowerCase() === optionName.toLowerCase());
267
+
268
+ if (optionIndex === -1) {
269
+ return undefined;
270
+ }
271
+
272
+ const result: Record<string, string | number> = {};
273
+
274
+ for (let i = optionIndex + 1; i < args.length; i++) {
275
+ const token = args[i];
276
+ if (!token || token.startsWith("--")) {
277
+ break;
278
+ }
279
+
280
+ const equalsIndex = token.indexOf("=");
281
+ if (equalsIndex === -1) {
282
+ continue;
283
+ }
284
+
285
+ const key = token.slice(0, equalsIndex);
286
+ const rawValue = token.slice(equalsIndex + 1);
287
+ const parsedNumber = Number(rawValue);
288
+ result[key] = Number.isNaN(parsedNumber) ? rawValue : parsedNumber;
289
+ }
290
+
291
+ return Object.keys(result).length > 0 ? result : undefined;
292
+ }
@@ -0,0 +1,67 @@
1
+ export type SecurityCheckResult = {
2
+ allowed: boolean;
3
+ command?: string;
4
+ reason?: string;
5
+ };
6
+
7
+ export type InvokeParams = {
8
+ area: string;
9
+ resource: string;
10
+ project?: string;
11
+ routeParameters?: Record<string, string | number>;
12
+ queryParameters?: Record<string, string | number>;
13
+ };
14
+
15
+ export type BuildJob = {
16
+ id: string;
17
+ parentId?: string | null;
18
+ type: "Job" | "Stage" | "Task" | "Phase" | "Checkpoint";
19
+ name: string;
20
+ state: "pending" | "inProgress" | "completed";
21
+ result?: "succeeded" | "failed" | "canceled" | "skipped" | null;
22
+ startTime?: string | null;
23
+ finishTime?: string | null;
24
+ errorCount?: number | null;
25
+ warningCount?: number | null;
26
+ log?: { id: number; url: string } | null;
27
+ };
28
+
29
+ export type BuildTimeline = {
30
+ records: BuildJob[];
31
+ id: string;
32
+ changeId: number;
33
+ lastChangedBy: string;
34
+ lastChangedOn: string;
35
+ url: string;
36
+ };
37
+
38
+ export type BuildLog = {
39
+ id: number;
40
+ type: string;
41
+ url: string;
42
+ lineCount?: number;
43
+ };
44
+
45
+ export type BuildLogs = {
46
+ count: number;
47
+ value: BuildLog[];
48
+ };
49
+
50
+ export type JobSummary = {
51
+ name: string;
52
+ state: string;
53
+ result?: string;
54
+ stage?: string;
55
+ duration?: string;
56
+ logId?: number;
57
+ };
58
+
59
+ export type PipelineRun = {
60
+ id: number;
61
+ buildNumber: string;
62
+ status: string;
63
+ result?: string;
64
+ sourceBranch: string;
65
+ startTime?: string;
66
+ finishTime?: string;
67
+ };
@@ -0,0 +1,12 @@
1
+ export type {
2
+ AgentToolsConfig,
3
+ AzureConfig,
4
+ K8sConfig,
5
+ DbEnvConfig,
6
+ DatabaseConfig,
7
+ LogsConfig,
8
+ CliToolOverride,
9
+ CredentialGuardConfig,
10
+ } from "./types.ts";
11
+
12
+ export { ConfigService, ConfigServiceLayer, getToolConfig, loadConfig } from "./loader";
@@ -0,0 +1,170 @@
1
+ import { dirname } from "node:path";
2
+
3
+ import { Data, Effect, Layer, Schema, ServiceMap } from "effect";
4
+
5
+ import type { AgentToolsConfig } from "./types.ts";
6
+
7
+ const CliToolOverrideSchema = Schema.Struct({
8
+ tool: Schema.String,
9
+ suggestion: Schema.String,
10
+ });
11
+
12
+ const CredentialGuardConfigSchema = Schema.Struct({
13
+ additionalBlockedPaths: Schema.optionalKey(Schema.Array(Schema.String)),
14
+ additionalAllowedPaths: Schema.optionalKey(Schema.Array(Schema.String)),
15
+ additionalBlockedCliTools: Schema.optionalKey(Schema.Array(CliToolOverrideSchema)),
16
+ additionalDangerousBashPatterns: Schema.optionalKey(Schema.Array(Schema.String)),
17
+ });
18
+
19
+ const AzureConfigSchema = Schema.Struct({
20
+ organization: Schema.String,
21
+ defaultProject: Schema.String,
22
+ timeoutMs: Schema.optionalKey(Schema.Number),
23
+ });
24
+
25
+ const K8sConfigSchema = Schema.Struct({
26
+ clusterId: Schema.String,
27
+ namespaces: Schema.Record(Schema.String, Schema.String),
28
+ timeoutMs: Schema.optionalKey(Schema.Number),
29
+ });
30
+
31
+ const DbEnvConfigSchema = Schema.Struct({
32
+ host: Schema.String,
33
+ port: Schema.Number,
34
+ user: Schema.String,
35
+ database: Schema.String,
36
+ passwordEnvVar: Schema.optionalKey(Schema.String),
37
+ });
38
+
39
+ const DatabaseConfigSchema = Schema.Struct({
40
+ environments: Schema.Record(Schema.String, DbEnvConfigSchema),
41
+ kubectl: Schema.optionalKey(
42
+ Schema.Struct({
43
+ context: Schema.String,
44
+ namespace: Schema.String,
45
+ }),
46
+ ),
47
+ tunnelTimeoutMs: Schema.optionalKey(Schema.Number),
48
+ remotePort: Schema.optionalKey(Schema.Number),
49
+ });
50
+
51
+ const LogsConfigSchema = Schema.Struct({
52
+ localDir: Schema.String,
53
+ remotePath: Schema.String,
54
+ });
55
+
56
+ const AgentToolsConfigSchema = Schema.Struct({
57
+ $schema: Schema.optionalKey(Schema.String),
58
+ azure: Schema.optionalKey(Schema.Record(Schema.String, AzureConfigSchema)),
59
+ kubernetes: Schema.optionalKey(Schema.Record(Schema.String, K8sConfigSchema)),
60
+ database: Schema.optionalKey(Schema.Record(Schema.String, DatabaseConfigSchema)),
61
+ logs: Schema.optionalKey(Schema.Record(Schema.String, LogsConfigSchema)),
62
+ session: Schema.optionalKey(
63
+ Schema.Struct({
64
+ storagePath: Schema.String,
65
+ }),
66
+ ),
67
+ credentialGuard: Schema.optionalKey(CredentialGuardConfigSchema),
68
+ });
69
+
70
+ async function findConfigFile(startDirectory: string = process.cwd()): Promise<string | undefined> {
71
+ let currentDirectory = startDirectory;
72
+
73
+ while (true) {
74
+ const json5Path = `${currentDirectory}/agent-tools.json5`;
75
+ if (await Bun.file(json5Path).exists()) {
76
+ return json5Path;
77
+ }
78
+
79
+ const jsonPath = `${currentDirectory}/agent-tools.json`;
80
+ if (await Bun.file(jsonPath).exists()) {
81
+ return jsonPath;
82
+ }
83
+
84
+ const parentDirectory = dirname(currentDirectory);
85
+ if (parentDirectory === currentDirectory) {
86
+ return undefined;
87
+ }
88
+ currentDirectory = parentDirectory;
89
+ }
90
+ }
91
+
92
+ export async function loadConfig(): Promise<AgentToolsConfig | undefined> {
93
+ const configPath = await findConfigFile();
94
+ if (!configPath) {
95
+ return undefined;
96
+ }
97
+
98
+ const fileContent = await Bun.file(configPath).text();
99
+ const parsed = Bun.JSON5.parse(fileContent);
100
+
101
+ try {
102
+ const decoded = Schema.decodeUnknownSync(AgentToolsConfigSchema)(parsed);
103
+ return decoded as AgentToolsConfig;
104
+ } catch (error) {
105
+ throw new Error(
106
+ `Invalid agent-tools config at ${configPath}: ${
107
+ error instanceof Error ? error.message : String(error)
108
+ }`,
109
+ );
110
+ }
111
+ }
112
+
113
+ export class ConfigService extends ServiceMap.Service<
114
+ ConfigService,
115
+ AgentToolsConfig | undefined
116
+ >()("@agent-tools/ConfigService") {}
117
+
118
+ export class ConfigLoadError extends Data.TaggedError("ConfigLoadError")<{
119
+ readonly cause: unknown;
120
+ }> {}
121
+
122
+ export const ConfigServiceLayer = Layer.effect(
123
+ ConfigService,
124
+ Effect.tryPromise({
125
+ try: () => loadConfig(),
126
+ catch: (error) => new ConfigLoadError({ cause: error }),
127
+ }),
128
+ );
129
+
130
+ type ProfiledSection = keyof Pick<AgentToolsConfig, "azure" | "kubernetes" | "database" | "logs">;
131
+
132
+ export function getToolConfig<T>(
133
+ config: AgentToolsConfig | undefined,
134
+ section: ProfiledSection,
135
+ profile?: string,
136
+ ): T | undefined {
137
+ if (!config) {
138
+ return undefined;
139
+ }
140
+
141
+ const sectionData = config[section] as Record<string, T> | undefined;
142
+ if (!sectionData) {
143
+ return undefined;
144
+ }
145
+
146
+ const keys = Object.keys(sectionData);
147
+ if (keys.length === 0) {
148
+ return undefined;
149
+ }
150
+
151
+ if (profile) {
152
+ return sectionData[profile];
153
+ }
154
+
155
+ if (keys.length === 1) {
156
+ const onlyKey = keys[0];
157
+ if (!onlyKey) {
158
+ return undefined;
159
+ }
160
+ return sectionData[onlyKey];
161
+ }
162
+
163
+ if ("default" in sectionData) {
164
+ return sectionData.default;
165
+ }
166
+
167
+ throw new Error(
168
+ `Multiple ${section} profiles found: [${keys.join(", ")}]. Use --profile <name> to select one.`,
169
+ );
170
+ }
@@ -0,0 +1,82 @@
1
+ /** Azure DevOps profile configuration */
2
+ export type AzureConfig = {
3
+ organization: string;
4
+ defaultProject: string;
5
+ timeoutMs?: number;
6
+ };
7
+
8
+ /** Kubernetes cluster profile configuration */
9
+ export type K8sConfig = {
10
+ clusterId: string;
11
+ /** Named namespaces, e.g. { test: "my-app-test", prod: "my-app-prod" } */
12
+ namespaces: Record<string, string>;
13
+ timeoutMs?: number;
14
+ };
15
+
16
+ /** Single database environment connection details */
17
+ export type DbEnvConfig = {
18
+ host: string;
19
+ port: number;
20
+ user: string;
21
+ database: string;
22
+ /** Name of environment variable holding the password, e.g. "DB_TEST_PWD" */
23
+ passwordEnvVar?: string;
24
+ };
25
+
26
+ /** Database profile configuration */
27
+ export type DatabaseConfig = {
28
+ /** Named database environments, e.g. { local: {...}, test: {...}, prod: {...} } */
29
+ environments: Record<string, DbEnvConfig>;
30
+ kubectl?: {
31
+ context: string;
32
+ namespace: string;
33
+ };
34
+ tunnelTimeoutMs?: number;
35
+ remotePort?: number;
36
+ };
37
+
38
+ /** Logs profile configuration */
39
+ export type LogsConfig = {
40
+ localDir: string;
41
+ remotePath: string;
42
+ };
43
+
44
+ export type CliToolOverride = {
45
+ tool: string;
46
+ suggestion: string;
47
+ };
48
+
49
+ /** Credential guard config - merged with built-in defaults */
50
+ export type CredentialGuardConfig = {
51
+ additionalBlockedPaths?: string[];
52
+ additionalAllowedPaths?: string[];
53
+ additionalBlockedCliTools?: CliToolOverride[];
54
+ additionalDangerousBashPatterns?: string[];
55
+ };
56
+
57
+ /**
58
+ * Root agent-tools configuration.
59
+ *
60
+ * Each tool section (azure, kubernetes, database, logs) is a Record<string, ToolConfig>
61
+ * of named profiles. Tools select a profile via the --profile <name> flag (default = "default" key).
62
+ * If only one profile exists, it is used automatically.
63
+ *
64
+ * session and credentialGuard are global - not per-profile.
65
+ */
66
+ export type AgentToolsConfig = {
67
+ $schema?: string;
68
+ /** Named Azure DevOps profiles. e.g. { default: { organization: "...", defaultProject: "..." } } */
69
+ azure?: Record<string, AzureConfig>;
70
+ /** Named Kubernetes cluster profiles. e.g. { default: {...}, staging: {...} } */
71
+ kubernetes?: Record<string, K8sConfig>;
72
+ /** Named database profiles. e.g. { default: {...}, analytics: {...} } */
73
+ database?: Record<string, DatabaseConfig>;
74
+ /** Named logs profiles. e.g. { default: { localDir: "...", remotePath: "..." } } */
75
+ logs?: Record<string, LogsConfig>;
76
+ /** Global session config (not per-profile) */
77
+ session?: {
78
+ storagePath: string;
79
+ };
80
+ /** Global credential guard config (merged with built-in defaults, not per-profile) */
81
+ credentialGuard?: CredentialGuardConfig;
82
+ };
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Claude Code PreToolUse hook wrapper.
4
+ *
5
+ * Reads JSON from stdin (Claude Code hook protocol), runs the credential guard,
6
+ * and exits with code 2 + stderr message if blocked, or 0 if allowed.
7
+ *
8
+ * Usage in .claude/settings.json:
9
+ * { "hooks": { "PreToolUse": [{ "matcher": ".*", "hooks": [{ "type": "command",
10
+ * "command": "bun node_modules/@blogic-cz/agent-tools/src/credential-guard/claude-hook.ts" }] }] } }
11
+ */
12
+
13
+ import { handleToolExecuteBefore } from "./index";
14
+
15
+ const stdin = await Bun.stdin.text();
16
+
17
+ try {
18
+ const data: {
19
+ tool_name: string;
20
+ tool_input?: Record<string, unknown>;
21
+ } = JSON.parse(stdin);
22
+
23
+ handleToolExecuteBefore({ tool: data.tool_name }, { args: data.tool_input ?? {} });
24
+ } catch (error: unknown) {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ process.stderr.write(message);
27
+ process.exit(2);
28
+ }