@blogic-cz/agent-tools 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -15,6 +15,14 @@ These tools wrap each CLI with:
15
15
 
16
16
  ## Installation
17
17
 
18
+ > **Recommended:** Copy the repo URL and tell your AI agent to install it. The agent will set up everything — dependencies, config file, credential guard — in the right places for your project.
19
+ >
20
+ > ```
21
+ > Install @blogic-cz/agent-tools from https://github.com/blogic-cz/agent-tools and set it up for this project.
22
+ > ```
23
+
24
+ ### Manual installation
25
+
18
26
  ```bash
19
27
  bun add @blogic-cz/agent-tools
20
28
  ```
@@ -38,6 +46,7 @@ ls src/ # gh-tool/ db-tool/ k8s-tool/ az-tool/ logs-tool/ session-tool/ credent
38
46
  ```json5
39
47
  {
40
48
  $schema: "https://raw.githubusercontent.com/blogic-cz/agent-tools/main/schemas/agent-tools.schema.json",
49
+ defaultEnvironment: "test", // optional: any string (e.g. "local", "test", "prod")
41
50
  kubernetes: {
42
51
  default: {
43
52
  clusterId: "your-cluster-id",
@@ -56,9 +65,14 @@ ls src/ # gh-tool/ db-tool/ k8s-tool/ az-tool/ logs-tool/ session-tool/ credent
56
65
  3. Run tools:
57
66
 
58
67
  ```bash
59
- npx agent-tools-gh pr list
60
- npx agent-tools-k8s kubectl -n prod-ns get pods
61
- npx agent-tools-logs list --env local
68
+ bunx agent-tools-gh pr status
69
+ bunx agent-tools-k8s kubectl --env test --cmd "get pods"
70
+ bunx agent-tools-logs list --env local
71
+ ```
72
+
73
+ ```bash
74
+ bunx agent-tools-gh pr review-triage # interactive summary of PR feedback
75
+ bunx agent-tools-k8s pods --env test # list pods (structured command)
62
76
  ```
63
77
 
64
78
  4. Hook up the credential guard in your agent config (Claude Code, OpenCode, etc.):
@@ -71,14 +85,14 @@ export default { handleToolExecuteBefore };
71
85
 
72
86
  ## Tools
73
87
 
74
- | Binary | Description |
75
- | --------------------- | --------------------------------------------------------------- |
76
- | `agent-tools-gh` | GitHub CLI wrapper — PR management, issues, workflows |
77
- | `agent-tools-db` | Database query tool — SQL execution, schema introspection |
78
- | `agent-tools-k8s` | Kubernetes tool — kubectl with config-driven context resolution |
79
- | `agent-tools-az` | Azure DevOps tool — pipelines, builds, repos |
80
- | `agent-tools-logs` | Application logs — read local and remote (k8s pod) logs |
81
- | `agent-tools-session` | OpenCode session browser — list, read, search sessions |
88
+ | Binary | Description |
89
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------- |
90
+ | `agent-tools-gh` | GitHub CLI wrapper — PR management, issues, workflows, composite commands (`review-triage`, `reply-and-resolve`) |
91
+ | `agent-tools-db` | Database query tool — SQL execution, schema introspection |
92
+ | `agent-tools-k8s` | Kubernetes tool — kubectl wrapper + structured commands (`pods`, `logs`, `describe`, `exec`, `top`) |
93
+ | `agent-tools-az` | Azure DevOps tool — pipelines, builds, repos |
94
+ | `agent-tools-logs` | Application logs — read local and remote (k8s pod) logs |
95
+ | `agent-tools-session` | OpenCode session browser — list, read, search sessions |
82
96
 
83
97
  All tools support `--help` for full usage documentation.
84
98
 
@@ -86,6 +100,16 @@ All tools support `--help` for full usage documentation.
86
100
 
87
101
  Config is loaded from `agent-tools.json5` (or `agent-tools.json`) by walking up from the current working directory. Missing config = zero-config mode (works for `gh-tool`; others require config).
88
102
 
103
+ ### Global Settings
104
+
105
+ Use `defaultEnvironment` to set the default target for tools that support environments (k8s-tool, logs-tool, db-tool). Passing `--env` explicitly always takes precedence. Note that tools will block implicit production access if `defaultEnvironment` is set to `"prod"`.
106
+
107
+ ```json5
108
+ {
109
+ defaultEnvironment: "test",
110
+ }
111
+ ```
112
+
89
113
  ### IDE Autocompletion
90
114
 
91
115
  Add `$schema` to your config file:
@@ -110,8 +134,8 @@ Each tool section supports multiple named profiles. Select with `--profile <name
110
134
  ```
111
135
 
112
136
  ```bash
113
- npx agent-tools-az pipeline list # uses "default" profile
114
- npx agent-tools-az pipeline list --profile legacy # uses "legacy" profile
137
+ bunx agent-tools-az cmd --cmd "pipelines list" # uses "default" profile
138
+ bunx agent-tools-az cmd --cmd "pipelines list" --profile legacy # uses "legacy" profile
115
139
  ```
116
140
 
117
141
  **Profile resolution:** `--profile` flag > auto-select (single profile) > `"default"` key > error.
@@ -120,34 +144,38 @@ npx agent-tools-az pipeline list --profile legacy # uses "legacy" profile
120
144
 
121
145
  See [`examples/agent-tools.json5`](./examples/agent-tools.json5) for a complete example with all options documented.
122
146
 
123
- ## Environment Variables
147
+ ## Authentication
124
148
 
125
- Secrets are **never** stored in the config file. Use environment variables:
149
+ Each tool uses its own auth method no unified token store:
126
150
 
127
- | Variable | Used By | Description |
128
- | ------------------ | ------- | --------------------------------------------------------- |
129
- | `AGENT_TOOLS_DB_*` | db-tool | DB passwords (name defined by `passwordEnvVar` in config) |
130
- | `GITHUB_TOKEN` | gh-tool | GitHub API token (falls back to `gh` CLI auth) |
151
+ | Tool | Auth Method |
152
+ | ----------- | -------------------------------------------------------------------------------------------- |
153
+ | `gh-tool` | `gh` CLI session (`gh auth login`) or `GITHUB_TOKEN` env var |
154
+ | `k8s-tool` | Existing kubectl context (kubeconfig). Cluster ID from config resolves context automatically |
155
+ | `az-tool` | `az` CLI session (`az login`) |
156
+ | `db-tool` | Password from env var defined by `passwordEnvVar` in config (e.g. `AGENT_TOOLS_DB_PASSWORD`) |
157
+ | `logs-tool` | No auth — reads local files or uses k8s-tool for remote access |
131
158
 
132
- ### Setting up credentials
159
+ Secrets are **never** stored in the config file. The `db-tool` config references env var **names** only:
133
160
 
134
- The config file only references env var **names** (via `passwordEnvVar`), never actual secrets. Set the values in your shell:
161
+ ```json5
162
+ {
163
+ databases: {
164
+ default: {
165
+ passwordEnvVar: "AGENT_TOOLS_DB_PASSWORD", // tool reads process.env[passwordEnvVar] at runtime
166
+ },
167
+ },
168
+ }
169
+ ```
135
170
 
136
- **macOS / Linux** add to `~/.zshrc` or `~/.bashrc`:
171
+ Set the values in your shell:
137
172
 
138
173
  ```bash
139
174
  export AGENT_TOOLS_DB_PASSWORD="your-password"
140
175
  export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
141
176
  ```
142
177
 
143
- **Windows** PowerShell (persistent, user-level):
144
-
145
- ```powershell
146
- [Environment]::SetEnvironmentVariable("AGENT_TOOLS_DB_PASSWORD", "your-password", "User")
147
- [Environment]::SetEnvironmentVariable("GITHUB_TOKEN", "ghp_xxxxxxxxxxxx", "User")
148
- ```
149
-
150
- Restart your terminal after adding env vars. The credential guard ensures these values never leak into agent output.
178
+ The credential guard ensures these values never leak into agent output.
151
179
 
152
180
  ## Credential Guard
153
181
 
@@ -231,6 +259,16 @@ Use the `credentialGuard` config section to extend built-in defaults (arrays are
231
259
 
232
260
  The guard source is at [`src/credential-guard/index.ts`](./src/credential-guard/index.ts). Fork the repo, adjust patterns, submit a PR: https://github.com/blogic-cz/agent-tools
233
261
 
262
+ ## Development & Evaluation
263
+
264
+ ### Run Evaluation Harness
265
+
266
+ The evaluation harness runs a set of test cases against the tools to ensure quality and reliability:
267
+
268
+ ```bash
269
+ bun run tests/eval/run.ts
270
+ ```
271
+
234
272
  ## License
235
273
 
236
274
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, and sessions",
5
5
  "keywords": [
6
6
  "agent",
@@ -38,6 +38,9 @@
38
38
  "./credential-guard": "./src/credential-guard/index.ts",
39
39
  "./config": "./src/config/index.ts"
40
40
  },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
41
44
  "scripts": {
42
45
  "check": "bun check.ts",
43
46
  "check:ci": "bun check.ts ci",
@@ -61,9 +64,6 @@
61
64
  "typescript": "5.9.3",
62
65
  "vitest": "^4.0.18"
63
66
  },
64
- "publishConfig": {
65
- "access": "public"
66
- },
67
67
  "engines": {
68
68
  "bun": ">=1.0.0"
69
69
  }
@@ -49,6 +49,10 @@
49
49
  "credentialGuard": {
50
50
  "description": "Global credential guard configuration merged with built-in defaults.",
51
51
  "$ref": "#/definitions/CredentialGuardConfig"
52
+ },
53
+ "defaultEnvironment": {
54
+ "description": "Optional default environment name used by tools when no --env flag is provided.",
55
+ "type": "string"
52
56
  }
53
57
  },
54
58
  "definitions": {
@@ -54,6 +54,7 @@ export const getBuildTimeline = Effect.fn("Build.getBuildTimeline")(function* (b
54
54
  new AzParseError({
55
55
  message: `Failed to parse build timeline: ${String(e)}`,
56
56
  rawOutput: JSON.stringify(result).slice(0, 500),
57
+ hint: "The Azure DevOps API returned an unexpected response format for build timeline",
57
58
  }),
58
59
  ),
59
60
  );
@@ -107,6 +108,7 @@ export const getBuildLogs = Effect.fn("Build.getBuildLogs")(function* (buildId:
107
108
  new AzParseError({
108
109
  message: `Failed to parse build logs: ${String(e)}`,
109
110
  rawOutput: JSON.stringify(result).slice(0, 500),
111
+ hint: "The Azure DevOps API returned an unexpected response format for build logs",
110
112
  }),
111
113
  ),
112
114
  );
@@ -143,6 +145,7 @@ export const getBuildLogContent = Effect.fn("Build.getBuildLogContent")(function
143
145
  new AzParseError({
144
146
  message: `Failed to parse log content: ${String(e)}`,
145
147
  rawOutput: String(result).slice(0, 500),
148
+ hint: "The Azure DevOps API returned an unexpected format for log content",
146
149
  }),
147
150
  ),
148
151
  );
@@ -245,6 +248,7 @@ export const listPipelineRuns = Effect.fn("Build.listPipelineRuns")(function* (o
245
248
  new AzParseError({
246
249
  message: "Failed to parse JSON from pipeline runs output",
247
250
  rawOutput: rawResult.slice(0, 500),
251
+ hint: "The az CLI returned non-JSON output. Check that the command ran successfully.",
248
252
  }),
249
253
  });
250
254
 
@@ -268,6 +272,7 @@ export const listPipelineRuns = Effect.fn("Build.listPipelineRuns")(function* (o
268
272
  new AzParseError({
269
273
  message: `Failed to parse pipeline runs: ${String(e)}`,
270
274
  rawOutput: rawResult.slice(0, 500),
275
+ hint: "The Azure DevOps API returned an unexpected response format for pipeline runs",
271
276
  }),
272
277
  ),
273
278
  );
@@ -3,6 +3,9 @@ import { Schema } from "effect";
3
3
  export class AzSecurityError extends Schema.TaggedErrorClass<AzSecurityError>()("AzSecurityError", {
4
4
  message: Schema.String,
5
5
  command: Schema.String,
6
+ hint: Schema.optionalKey(Schema.String),
7
+ nextCommand: Schema.optionalKey(Schema.String),
8
+ retryable: Schema.optionalKey(Schema.Boolean),
6
9
  }) {}
7
10
 
8
11
  export class AzCommandError extends Schema.TaggedErrorClass<AzCommandError>()("AzCommandError", {
@@ -10,17 +13,26 @@ export class AzCommandError extends Schema.TaggedErrorClass<AzCommandError>()("A
10
13
  command: Schema.String,
11
14
  exitCode: Schema.optionalKey(Schema.Number),
12
15
  stderr: Schema.optionalKey(Schema.String),
16
+ hint: Schema.optionalKey(Schema.String),
17
+ nextCommand: Schema.optionalKey(Schema.String),
18
+ retryable: Schema.optionalKey(Schema.Boolean),
13
19
  }) {}
14
20
 
15
21
  export class AzTimeoutError extends Schema.TaggedErrorClass<AzTimeoutError>()("AzTimeoutError", {
16
22
  message: Schema.String,
17
23
  command: Schema.String,
18
24
  timeoutMs: Schema.Number,
25
+ hint: Schema.optionalKey(Schema.String),
26
+ nextCommand: Schema.optionalKey(Schema.String),
27
+ retryable: Schema.optionalKey(Schema.Boolean),
19
28
  }) {}
20
29
 
21
30
  export class AzParseError extends Schema.TaggedErrorClass<AzParseError>()("AzParseError", {
22
31
  message: Schema.String,
23
32
  rawOutput: Schema.String,
33
+ hint: Schema.optionalKey(Schema.String),
34
+ nextCommand: Schema.optionalKey(Schema.String),
35
+ retryable: Schema.optionalKey(Schema.Boolean),
24
36
  }) {}
25
37
 
26
38
  export type AzError = AzSecurityError | AzCommandError | AzTimeoutError | AzParseError;
@@ -11,13 +11,111 @@ import {
11
11
  getBuildLogs,
12
12
  getBuildTimeline,
13
13
  } from "./build";
14
- import { AzCommandError } from "./errors";
15
- import { extractOptionValue } from "./extract-option-value";
16
14
  import { AzService, AzServiceLayer } from "./service";
17
15
  import { ConfigServiceLayer } from "../config";
18
16
 
19
- const mainCommand = Command.make(
20
- "az-tool",
17
+ // ---------------------------------------------------------------------------
18
+ // Common flags shared across build subcommands
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const commonBuildFlags = {
22
+ format: formatOption,
23
+ profile: Flag.optional(Flag.string("profile")).pipe(
24
+ Flag.withDescription("Azure DevOps profile name (from agent-tools config)"),
25
+ ),
26
+ };
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Build subcommands
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const buildTimelineCommand = Command.make(
33
+ "timeline",
34
+ {
35
+ ...commonBuildFlags,
36
+ buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
37
+ },
38
+ ({ buildId, format, profile: _profile }) =>
39
+ Effect.gen(function* () {
40
+ const result = yield* getBuildTimeline(buildId);
41
+ yield* Console.log(formatAny(result, format));
42
+ }),
43
+ ).pipe(Command.withDescription("Get build timeline with all records (jobs, stages, tasks)"));
44
+
45
+ const buildFailedJobsCommand = Command.make(
46
+ "failed-jobs",
47
+ {
48
+ ...commonBuildFlags,
49
+ buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
50
+ },
51
+ ({ buildId, format, profile: _profile }) =>
52
+ Effect.gen(function* () {
53
+ const failedJobs = yield* findFailedJobs(buildId);
54
+ yield* Console.log(formatAny({ buildId, failedJobs }, format));
55
+ }),
56
+ ).pipe(Command.withDescription("Find failed or canceled jobs in a build"));
57
+
58
+ const buildLogsCommand = Command.make(
59
+ "logs",
60
+ {
61
+ ...commonBuildFlags,
62
+ buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
63
+ },
64
+ ({ buildId, format, profile: _profile }) =>
65
+ Effect.gen(function* () {
66
+ const result = yield* getBuildLogs(buildId);
67
+ yield* Console.log(formatAny(result, format));
68
+ }),
69
+ ).pipe(Command.withDescription("Get list of build logs"));
70
+
71
+ const buildLogContentCommand = Command.make(
72
+ "log-content",
73
+ {
74
+ ...commonBuildFlags,
75
+ buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
76
+ logId: Flag.integer("log-id").pipe(Flag.withDescription("Log ID")),
77
+ },
78
+ ({ buildId, format, logId, profile: _profile }) =>
79
+ Effect.gen(function* () {
80
+ const content = yield* getBuildLogContent(buildId, logId);
81
+ yield* Console.log(formatAny({ buildId, logId, content }, format));
82
+ }),
83
+ ).pipe(Command.withDescription("Get specific log content by log ID"));
84
+
85
+ const buildSummaryCommand = Command.make(
86
+ "summary",
87
+ {
88
+ ...commonBuildFlags,
89
+ buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
90
+ },
91
+ ({ buildId, format, profile: _profile }) =>
92
+ Effect.gen(function* () {
93
+ const summary = yield* getBuildJobSummary(buildId);
94
+ yield* Console.log(formatAny({ buildId, summary }, format));
95
+ }),
96
+ ).pipe(Command.withDescription("Get job summaries with duration and status information"));
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Build parent command
100
+ // ---------------------------------------------------------------------------
101
+
102
+ const buildCommand = Command.make("build", {}).pipe(
103
+ Command.withDescription("Build helpers for Azure DevOps pipelines"),
104
+ Command.withSubcommands([
105
+ buildTimelineCommand,
106
+ buildFailedJobsCommand,
107
+ buildLogsCommand,
108
+ buildLogContentCommand,
109
+ buildSummaryCommand,
110
+ ]),
111
+ );
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Raw command subcommand (preserves existing --cmd behavior)
115
+ // ---------------------------------------------------------------------------
116
+
117
+ const cmdCommand = Command.make(
118
+ "cmd",
21
119
  {
22
120
  profile: Flag.optional(Flag.string("profile")).pipe(
23
121
  Flag.withDescription("Azure DevOps profile name (from agent-tools config)"),
@@ -36,20 +134,12 @@ const mainCommand = Command.make(
36
134
  Effect.gen(function* () {
37
135
  const projectName = project ? Option.getOrUndefined(project) : undefined;
38
136
 
39
- // Handle dry-run mode
40
137
  if (dryRun) {
41
138
  const fullCommand = `az ${cmd}`;
42
139
  yield* Console.log(`[DRY-RUN] Would execute: ${fullCommand}`);
43
140
  return;
44
141
  }
45
142
 
46
- const buildHelperResult = yield* runBuildHelperCommand(cmd);
47
-
48
- if (buildHelperResult !== undefined) {
49
- yield* Console.log(formatAny(buildHelperResult, format));
50
- return;
51
- }
52
-
53
143
  const az = yield* AzService;
54
144
 
55
145
  const startTime = Date.now();
@@ -68,16 +158,35 @@ const mainCommand = Command.make(
68
158
  );
69
159
  }),
70
160
  ).pipe(
161
+ Command.withDescription(
162
+ `Execute raw az CLI commands directly.
163
+
164
+ EXAMPLES:
165
+ agent-tools-az cmd --cmd "pipelines list"
166
+ agent-tools-az cmd --cmd "repos list" --project my-project
167
+ agent-tools-az cmd --cmd "pipelines runs list --output json"`,
168
+ ),
169
+ );
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Main command with subcommands
173
+ // ---------------------------------------------------------------------------
174
+
175
+ const mainCommand = Command.make("az-tool", {}).pipe(
71
176
  Command.withDescription(
72
177
  `Azure CLI Tool for Coding Agents (READ-ONLY)
73
178
 
74
- Supports raw az wrapper via --cmd and convenience build helpers:
75
- --cmd "build timeline --build-id 123"
76
- --cmd "build failed-jobs --build-id 123"
77
- --cmd "build logs --build-id 123"
78
- --cmd "build log-content --build-id 123 --log-id 45"
79
- --cmd "build summary --build-id 123"`,
179
+ Typed build subcommands:
180
+ az-tool build timeline --build-id 123
181
+ az-tool build failed-jobs --build-id 123
182
+ az-tool build logs --build-id 123
183
+ az-tool build log-content --build-id 123 --log-id 45
184
+ az-tool build summary --build-id 123
185
+
186
+ Raw az wrapper:
187
+ az-tool cmd --cmd "pipelines list"`,
80
188
  ),
189
+ Command.withSubcommands([buildCommand, cmdCommand]),
81
190
  );
82
191
 
83
192
  const cli = Command.run(mainCommand, {
@@ -94,88 +203,3 @@ const program = cli.pipe(Effect.provide(MainLayer), Effect.tapCause(renderCauseT
94
203
  BunRuntime.runMain(program, {
95
204
  disableErrorReporting: true,
96
205
  });
97
-
98
- function runBuildHelperCommand(cmd: string) {
99
- return Effect.gen(function* () {
100
- const words = cmd.trim().split(/\s+/);
101
-
102
- if (words[0] !== "build") {
103
- return undefined;
104
- }
105
-
106
- const action = words[1];
107
- if (!action) {
108
- return yield* invalidBuildCommand(
109
- cmd,
110
- "Missing build action. Use one of: timeline, failed-jobs, logs, log-content, summary",
111
- );
112
- }
113
-
114
- const buildId = yield* parseRequiredIntOption(words, "--build-id", cmd);
115
-
116
- if (action === "timeline") {
117
- const timeline = yield* getBuildTimeline(buildId);
118
- return timeline;
119
- }
120
-
121
- if (action === "failed-jobs") {
122
- const failedJobs = yield* findFailedJobs(buildId);
123
- return { buildId, failedJobs };
124
- }
125
-
126
- if (action === "logs") {
127
- const logs = yield* getBuildLogs(buildId);
128
- return logs;
129
- }
130
-
131
- if (action === "log-content") {
132
- const logId = yield* parseRequiredIntOption(words, "--log-id", cmd);
133
- const content = yield* getBuildLogContent(buildId, logId);
134
- return {
135
- buildId,
136
- logId,
137
- content,
138
- };
139
- }
140
-
141
- if (action === "summary") {
142
- const summary = yield* getBuildJobSummary(buildId);
143
- return { buildId, summary };
144
- }
145
-
146
- return yield* invalidBuildCommand(
147
- cmd,
148
- `Unknown build action '${action}'. Use one of: timeline, failed-jobs, logs, log-content, summary`,
149
- );
150
- });
151
- }
152
-
153
- function invalidBuildCommand(cmd: string, message: string) {
154
- return Effect.fail(
155
- new AzCommandError({
156
- message,
157
- command: cmd,
158
- exitCode: 2,
159
- stderr: message,
160
- }),
161
- );
162
- }
163
-
164
- function parseRequiredIntOption(args: readonly string[], optionName: string, cmd: string) {
165
- return Effect.gen(function* () {
166
- const rawValue = extractOptionValue(args, optionName);
167
- if (!rawValue) {
168
- return yield* invalidBuildCommand(cmd, `Missing required option ${optionName}`);
169
- }
170
-
171
- const parsedValue = Number.parseInt(rawValue, 10);
172
- if (Number.isNaN(parsedValue)) {
173
- return yield* invalidBuildCommand(
174
- cmd,
175
- `Option ${optionName} must be an integer, received '${rawValue}'`,
176
- );
177
- }
178
-
179
- return parsedValue;
180
- });
181
- }
@@ -32,7 +32,7 @@ export class AzService extends ServiceMap.Service<
32
32
  message: "No Azure configuration found. Add an 'azure' section to agent-tools.json5.",
33
33
  command: "unknown",
34
34
  exitCode: -1,
35
- stderr: undefined,
35
+ hint: "Create or update agent-tools.json5 with an 'azure' section containing organization and defaultProject",
36
36
  });
37
37
  return {
38
38
  runCommand: (_cmd: string, _project?: string) => Effect.fail(noConfigError),
@@ -69,7 +69,9 @@ export class AzService extends ServiceMap.Service<
69
69
  message: `Command execution failed: ${platformError.message}`,
70
70
  command: fullCommand,
71
71
  exitCode: -1,
72
- stderr: undefined,
72
+ hint: "Check that the az CLI is installed and authenticated",
73
+ nextCommand: "az login",
74
+ retryable: true,
73
75
  }),
74
76
  ),
75
77
  );
@@ -87,6 +89,7 @@ export class AzService extends ServiceMap.Service<
87
89
  return yield* new AzSecurityError({
88
90
  message: securityCheck.reason ?? "Command not allowed",
89
91
  command: cmd,
92
+ hint: "Only read-only az devops commands are allowed. Use build helpers for common operations.",
90
93
  });
91
94
  }
92
95
 
@@ -125,6 +128,8 @@ export class AzService extends ServiceMap.Service<
125
128
  message: `Command timed out after ${azConfig.timeoutMs ?? 60000}ms`,
126
129
  command: fullCommand,
127
130
  timeoutMs: azConfig.timeoutMs ?? 60000,
131
+ retryable: true,
132
+ hint: "The command took too long. Retry or increase timeoutMs in azure config.",
128
133
  });
129
134
  }
130
135
 
@@ -148,6 +153,7 @@ export class AzService extends ServiceMap.Service<
148
153
  return yield* new AzSecurityError({
149
154
  message: securityCheck.reason ?? "Invoke not allowed",
150
155
  command: `invoke --area ${params.area} --resource ${params.resource}`,
156
+ hint: "Only allowed invoke areas/resources can be used. Check az-tool security config.",
151
157
  });
152
158
  }
153
159
 
@@ -183,6 +189,8 @@ export class AzService extends ServiceMap.Service<
183
189
  message: `Invoke timed out after ${azConfig.timeoutMs ?? 60000}ms`,
184
190
  command: fullCommand,
185
191
  timeoutMs: azConfig.timeoutMs ?? 60000,
192
+ retryable: true,
193
+ hint: "The invoke took too long. Retry or increase timeoutMs in azure config.",
186
194
  });
187
195
  }
188
196
 
@@ -203,6 +211,7 @@ export class AzService extends ServiceMap.Service<
203
211
  new AzParseError({
204
212
  message: `Failed to parse JSON response from invoke`,
205
213
  rawOutput: result.stdout.slice(0, 500),
214
+ hint: "The az CLI returned non-JSON output. Ensure --output json is used.",
206
215
  }),
207
216
  });
208
217
  return jsonData;
@@ -9,4 +9,10 @@ export type {
9
9
  CredentialGuardConfig,
10
10
  } from "./types.ts";
11
11
 
12
- export { ConfigService, ConfigServiceLayer, getToolConfig, loadConfig } from "./loader";
12
+ export {
13
+ ConfigService,
14
+ ConfigServiceLayer,
15
+ getToolConfig,
16
+ getDefaultEnvironment,
17
+ loadConfig,
18
+ } from "./loader";
@@ -65,6 +65,7 @@ const AgentToolsConfigSchema = Schema.Struct({
65
65
  }),
66
66
  ),
67
67
  credentialGuard: Schema.optionalKey(CredentialGuardConfigSchema),
68
+ defaultEnvironment: Schema.optionalKey(Schema.String),
68
69
  });
69
70
 
70
71
  async function findConfigFile(startDirectory: string = process.cwd()): Promise<string | undefined> {
@@ -168,3 +169,7 @@ export function getToolConfig<T>(
168
169
  `Multiple ${section} profiles found: [${keys.join(", ")}]. Use --profile <name> to select one.`,
169
170
  );
170
171
  }
172
+
173
+ export function getDefaultEnvironment(config: AgentToolsConfig | undefined): string | undefined {
174
+ return config?.defaultEnvironment;
175
+ }
@@ -79,4 +79,6 @@ export type AgentToolsConfig = {
79
79
  };
80
80
  /** Global credential guard config (merged with built-in defaults, not per-profile) */
81
81
  credentialGuard?: CredentialGuardConfig;
82
+ /** Optional default environment name (local|test|prod) used by tools when no --env flag is provided */
83
+ defaultEnvironment?: string;
82
84
  };