@blogic-cz/agent-tools 0.9.0 → 0.11.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
@@ -1,6 +1,6 @@
1
1
  # @blogic-cz/agent-tools
2
2
 
3
- Safe CLI wrappers for AI coding agents. 7 tools for GitHub, databases, Kubernetes, Azure DevOps, logs, OpenCode sessions, and audit history — with JSON5 config and a credential guard that blocks agents from touching secrets.
3
+ Safe CLI wrappers for AI coding agents. 8 tools for GitHub, Grafana, databases, Kubernetes, Azure DevOps, logs, OpenCode sessions, and audit history — with JSON5 config and a credential guard that blocks agents from touching secrets.
4
4
 
5
5
  ## Why
6
6
 
@@ -33,6 +33,7 @@ Install per-tool skills for only the tools your project uses:
33
33
 
34
34
  ```bash
35
35
  npx skills add blogic-cz/agent-tools --path skill/gh-tool
36
+ npx skills add blogic-cz/agent-tools --path skill/grafana-tool
36
37
  npx skills add blogic-cz/agent-tools --path skill/db-tool
37
38
  npx skills add blogic-cz/agent-tools --path skill/k8s-tool
38
39
  npx skills add blogic-cz/agent-tools --path skill/az-tool
@@ -61,12 +62,13 @@ Follow these steps to set up agent-tools in the consumer project.
61
62
  - **Non-JavaScript project** (no `package.json`): Skip installation — run tools directly with `bunx`:
62
63
  ```bash
63
64
  bunx gh-tool pr status
65
+ bunx grafana-tool health
64
66
  bunx k8s-tool pods --env test
65
67
  ```
66
68
 
67
69
  **Step 2 — Configure**
68
70
 
69
- Ask the user which tools they need (gh, db, k8s, az, logs, session, audit). Create `agent-tools.json5` in the project root with **only the selected tools configured**. See [`examples/agent-tools.json5`](./examples/agent-tools.json5) for the full config reference with all options documented.
71
+ Ask the user which tools they need (gh, grafana, db, k8s, az, logs, session, audit). Create `agent-tools.json5` in the project root with **only the selected tools configured**. See [`examples/agent-tools.json5`](./examples/agent-tools.json5) for the full config reference with all options documented.
70
72
 
71
73
  Minimal starting config:
72
74
 
@@ -98,6 +100,7 @@ Install **only the per-tool skills the project needs** (recommended), or the all
98
100
  ```bash
99
101
  # Per-tool skills (recommended) — install only what the project uses
100
102
  npx skills add blogic-cz/agent-tools --path skill/gh-tool
103
+ npx skills add blogic-cz/agent-tools --path skill/grafana-tool
101
104
  npx skills add blogic-cz/agent-tools --path skill/db-tool
102
105
  npx skills add blogic-cz/agent-tools --path skill/k8s-tool
103
106
  npx skills add blogic-cz/agent-tools --path skill/az-tool
@@ -115,6 +118,7 @@ Available per-tool skills:
115
118
  | Skill | Install when project uses |
116
119
  | -------------- | ------------------------------------------ |
117
120
  | `gh-tool` | GitHub PRs, issues, workflows, CI checks |
121
+ | `grafana-tool` | Grafana dashboards, alerts, PromQL, LogQL |
118
122
  | `db-tool` | SQL queries, schema introspection |
119
123
  | `k8s-tool` | Kubernetes pods, logs, deployments |
120
124
  | `az-tool` | Azure DevOps pipelines, builds |
@@ -181,6 +185,19 @@ bun run agent-tools/example-tool/index.ts ping
181
185
  remotePath: "/app/logs",
182
186
  },
183
187
  },
188
+ grafana: {
189
+ // Profile name (selected via --profile, or used automatically when it's the only one)
190
+ default: {
191
+ // Environment name (selected via --env)
192
+ environments: {
193
+ local: {
194
+ url: "http://localhost:40300",
195
+ prometheusUid: "prometheus",
196
+ lokiUid: "loki",
197
+ },
198
+ },
199
+ },
200
+ },
184
201
  }
185
202
  ```
186
203
 
@@ -188,6 +205,7 @@ bun run agent-tools/example-tool/index.ts ping
188
205
 
189
206
  ```bash
190
207
  bun gh-tool pr status
208
+ bun grafana-tool dashboards list --env local
191
209
  bun k8s-tool kubectl --env test --cmd "get pods"
192
210
  bun logs-tool list --env local
193
211
  bun audit-tool list --limit 20
@@ -211,6 +229,7 @@ export default { handleToolExecuteBefore };
211
229
  | Binary | Description |
212
230
  | -------------- | ---------------------------------------------------------------------------------------------------------------- |
213
231
  | `gh-tool` | GitHub CLI wrapper — PR management, issues, workflows, composite commands (`review-triage`, `reply-and-resolve`) |
232
+ | `grafana-tool` | Grafana API wrapper — health, dashboards, datasources, alerts, PromQL, and LogQL queries |
214
233
  | `audit-tool` | Audit trail browser — inspect recent tool invocations and purge old entries |
215
234
  | `db-tool` | Database query tool — SQL execution, schema introspection |
216
235
  | `k8s-tool` | Kubernetes tool — kubectl wrapper + structured commands (`pods`, `logs`, `describe`, `exec`, `top`) |
@@ -228,7 +247,7 @@ Every tool invocation is automatically recorded to a local SQLite database — z
228
247
 
229
248
  ### How it works
230
249
 
231
- Each CLI wrapper (`gh`, `k8s`, `db`, `az`, `logs`, `session`, `audit`) writes a row to `~/.agent-tools/audit.sqlite` on every execution. Logging is fire-and-forget — if the database is unavailable or write fails, the tool continues normally. Audit never blocks or slows down your workflow.
250
+ Each CLI wrapper (`gh`, `grafana`, `k8s`, `db`, `az`, `logs`, `session`, `audit`) writes a row to `~/.agent-tools/audit.sqlite` on every execution. Logging is fire-and-forget — if the database is unavailable or write fails, the tool continues normally. Audit never blocks or slows down your workflow.
232
251
 
233
252
  Entries older than `retentionDays` (default: 90) are automatically purged on each write.
234
253
 
@@ -268,16 +287,16 @@ All settings are optional — audit works out of the box with sensible defaults.
268
287
 
269
288
  ### What gets recorded
270
289
 
271
- | Column | Description |
272
- | ----------- | ------------------------------------------------------ |
273
- | `ts` | ISO 8601 timestamp |
274
- | `tool` | Tool name (`gh`, `k8s`, `db`, `az`, `logs`, `session`) |
275
- | `project` | Working directory (`process.cwd()`) |
276
- | `args` | Command-line arguments (JSON array) |
277
- | `duration` | Execution time in milliseconds |
278
- | `success` | `1` (success) or `0` (failure) |
279
- | `error` | Error message if failed, `null` otherwise |
280
- | `exit_code` | Process exit code |
290
+ | Column | Description |
291
+ | ----------- | ----------------------------------------------------------------- |
292
+ | `ts` | ISO 8601 timestamp |
293
+ | `tool` | Tool name (`gh`, `grafana`, `k8s`, `db`, `az`, `logs`, `session`) |
294
+ | `project` | Working directory (`process.cwd()`) |
295
+ | `args` | Command-line arguments (JSON array) |
296
+ | `duration` | Execution time in milliseconds |
297
+ | `success` | `1` (success) or `0` (failure) |
298
+ | `error` | Error message if failed, `null` otherwise |
299
+ | `exit_code` | Process exit code |
281
300
 
282
301
  ## Configuration
283
302
 
@@ -331,13 +350,14 @@ See [`examples/agent-tools.json5`](./examples/agent-tools.json5) for a complete
331
350
 
332
351
  Each tool uses its own auth method — no unified token store:
333
352
 
334
- | Tool | Auth Method |
335
- | ----------- | -------------------------------------------------------------------------------------------- |
336
- | `gh-tool` | `gh` CLI session (`gh auth login`) or `GITHUB_TOKEN` env var |
337
- | `k8s-tool` | Existing kubectl context (kubeconfig). Cluster ID from config resolves context automatically |
338
- | `az-tool` | `az` CLI session (`az login`) |
339
- | `db-tool` | Password from env var defined by `passwordEnvVar` in config (e.g. `AGENT_TOOLS_DB_PASSWORD`) |
340
- | `logs-tool` | No auth reads local files or uses k8s-tool for remote access |
353
+ | Tool | Auth Method |
354
+ | -------------- | -------------------------------------------------------------------------------------------- |
355
+ | `gh-tool` | `gh` CLI session (`gh auth login`) or `GITHUB_TOKEN` env var |
356
+ | `grafana-tool` | Grafana URL from config plus optional token from `tokenEnvVar` |
357
+ | `k8s-tool` | Existing kubectl context (kubeconfig). Cluster ID from config resolves context automatically |
358
+ | `az-tool` | `az` CLI session (`az login`) |
359
+ | `db-tool` | Password from env var defined by `passwordEnvVar` in config (e.g. `AGENT_TOOLS_DB_PASSWORD`) |
360
+ | `logs-tool` | No auth — reads local files or uses k8s-tool for remote access |
341
361
 
342
362
  Secrets are **never** stored in the config file. The `db-tool` config references env var **names** only:
343
363
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -9,6 +9,7 @@
9
9
  "database",
10
10
  "devops",
11
11
  "github",
12
+ "grafana",
12
13
  "kubernetes",
13
14
  "tools"
14
15
  ],
@@ -23,6 +24,7 @@
23
24
  "agent-tools-az": "./src/az-tool/index.ts",
24
25
  "agent-tools-db": "./src/db-tool/index.ts",
25
26
  "agent-tools-gh": "./src/gh-tool/index.ts",
27
+ "agent-tools-grafana": "./src/grafana-tool/index.ts",
26
28
  "agent-tools-k8s": "./src/k8s-tool/index.ts",
27
29
  "agent-tools-logs": "./src/logs-tool/index.ts",
28
30
  "agent-tools-session": "./src/session-tool/index.ts",
@@ -30,6 +32,7 @@
30
32
  "az-tool": "./src/az-tool/index.ts",
31
33
  "db-tool": "./src/db-tool/index.ts",
32
34
  "gh-tool": "./src/gh-tool/index.ts",
35
+ "grafana-tool": "./src/grafana-tool/index.ts",
33
36
  "k8s-tool": "./src/k8s-tool/index.ts",
34
37
  "logs-tool": "./src/logs-tool/index.ts",
35
38
  "session-tool": "./src/session-tool/index.ts"
@@ -72,6 +75,10 @@
72
75
  "types": "./dist/db-tool/*.d.ts",
73
76
  "default": "./src/db-tool/*.ts"
74
77
  },
78
+ "#grafana/*": {
79
+ "types": "./dist/grafana-tool/*.d.ts",
80
+ "default": "./src/grafana-tool/*.ts"
81
+ },
75
82
  "#logs/*": {
76
83
  "types": "./dist/logs-tool/*.d.ts",
77
84
  "default": "./src/logs-tool/*.ts"
@@ -101,6 +108,10 @@
101
108
  "./config": {
102
109
  "types": "./dist/config/index.d.ts",
103
110
  "default": "./src/config/index.ts"
111
+ },
112
+ "./audit": {
113
+ "types": "./dist/shared/audit.d.ts",
114
+ "default": "./src/shared/audit.ts"
104
115
  }
105
116
  },
106
117
  "publishConfig": {
@@ -112,6 +123,7 @@
112
123
  "check:ci": "bun check.ts ci",
113
124
  "format": "oxfmt",
114
125
  "format:check": "oxfmt --check",
126
+ "grafana-tool": "bun src/grafana-tool/index.ts",
115
127
  "gh-tool": "bun src/gh-tool/index.ts",
116
128
  "lint": "oxlint -c ./.oxlintrc.json --deny-warnings",
117
129
  "lint:fix": "oxlint -c ./.oxlintrc.json --fix",
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://raw.githubusercontent.com/blogic-cz/agent-tools/main/schemas/agent-tools.schema.json",
4
4
  "title": "Agent Tools Configuration",
5
- "description": "Root configuration for the agent-tools package. Tool-specific sections (azure, kubernetes, database, logs) are maps of named profiles keyed by profile name. Tools choose a profile via --profile <name> (default key is 'default', single-entry maps can be auto-selected). session, audit, and credentialGuard are global sections.",
5
+ "description": "Root configuration for the agent-tools package. Tool-specific sections (azure, kubernetes, database, grafana, logs) are maps of named profiles keyed by profile name. Tools choose a profile via --profile <name> (default key is 'default', single-entry maps can be auto-selected). session, audit, and credentialGuard are global sections.",
6
6
  "type": "object",
7
7
  "additionalProperties": true,
8
8
  "properties": {
@@ -34,6 +34,14 @@
34
34
  "$ref": "#/definitions/DatabaseConfig"
35
35
  }
36
36
  },
37
+ "grafana": {
38
+ "description": "Named Grafana profiles as Record<string, GrafanaConfig>.",
39
+ "type": "object",
40
+ "additionalProperties": {
41
+ "description": "Grafana profile configuration containing one or more environments.",
42
+ "$ref": "#/definitions/GrafanaConfig"
43
+ }
44
+ },
37
45
  "logs": {
38
46
  "description": "Named logs profiles as Record<string, LogsConfig>.",
39
47
  "type": "object",
@@ -192,6 +200,45 @@
192
200
  },
193
201
  "required": ["environments"]
194
202
  },
203
+ "GrafanaEnvTarget": {
204
+ "description": "Single Grafana environment target.",
205
+ "type": "object",
206
+ "additionalProperties": false,
207
+ "properties": {
208
+ "url": {
209
+ "description": "Base Grafana URL.",
210
+ "type": "string"
211
+ },
212
+ "tokenEnvVar": {
213
+ "description": "Environment variable containing a Grafana API token.",
214
+ "type": "string"
215
+ },
216
+ "prometheusUid": {
217
+ "description": "Grafana datasource UID for Prometheus queries.",
218
+ "type": "string"
219
+ },
220
+ "lokiUid": {
221
+ "description": "Grafana datasource UID for Loki queries.",
222
+ "type": "string"
223
+ }
224
+ },
225
+ "required": ["url"]
226
+ },
227
+ "GrafanaConfig": {
228
+ "description": "Grafana profile configuration.",
229
+ "type": "object",
230
+ "additionalProperties": false,
231
+ "properties": {
232
+ "environments": {
233
+ "description": "Named Grafana environments as Record<string, GrafanaEnvTarget>.",
234
+ "type": "object",
235
+ "additionalProperties": {
236
+ "$ref": "#/definitions/GrafanaEnvTarget"
237
+ }
238
+ }
239
+ },
240
+ "required": ["environments"]
241
+ },
195
242
  "LogsConfig": {
196
243
  "description": "Logs profile configuration.",
197
244
  "type": "object",
@@ -4,6 +4,8 @@ export type {
4
4
  K8sConfig,
5
5
  DbEnvConfig,
6
6
  DatabaseConfig,
7
+ GrafanaConfig,
8
+ GrafanaEnvTarget,
7
9
  LogsConfig,
8
10
  AuditConfig,
9
11
  CliToolOverride,
@@ -54,6 +54,17 @@ const LogsConfigSchema = Schema.Struct({
54
54
  remotePath: Schema.String,
55
55
  });
56
56
 
57
+ const GrafanaEnvTargetSchema = Schema.Struct({
58
+ url: Schema.String,
59
+ tokenEnvVar: Schema.optionalKey(Schema.String),
60
+ prometheusUid: Schema.optionalKey(Schema.String),
61
+ lokiUid: Schema.optionalKey(Schema.String),
62
+ });
63
+
64
+ const GrafanaConfigSchema = Schema.Struct({
65
+ environments: Schema.Record(Schema.String, GrafanaEnvTargetSchema),
66
+ });
67
+
57
68
  const AuditConfigSchema = Schema.Struct({
58
69
  retentionDays: Schema.optionalKey(Schema.Number),
59
70
  dbPath: Schema.optionalKey(Schema.String),
@@ -69,6 +80,7 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
69
80
  "azure",
70
81
  "kubernetes",
71
82
  "database",
83
+ "grafana",
72
84
  "logs",
73
85
  "session",
74
86
  "audit",
@@ -82,6 +94,7 @@ const AgentToolsConfigSchema = Schema.Struct({
82
94
  azure: Schema.optionalKey(Schema.Record(Schema.String, AzureConfigSchema)),
83
95
  kubernetes: Schema.optionalKey(Schema.Record(Schema.String, K8sConfigSchema)),
84
96
  database: Schema.optionalKey(Schema.Record(Schema.String, DatabaseConfigSchema)),
97
+ grafana: Schema.optionalKey(Schema.Record(Schema.String, GrafanaConfigSchema)),
85
98
  logs: Schema.optionalKey(Schema.Record(Schema.String, LogsConfigSchema)),
86
99
  session: Schema.optionalKey(
87
100
  Schema.Struct({
@@ -175,7 +188,10 @@ export const ConfigServiceLayer = Layer.effect(
175
188
  }),
176
189
  );
177
190
 
178
- type ProfiledSection = keyof Pick<AgentToolsConfig, "azure" | "kubernetes" | "database" | "logs">;
191
+ type ProfiledSection = keyof Pick<
192
+ AgentToolsConfig,
193
+ "azure" | "kubernetes" | "database" | "grafana" | "logs"
194
+ >;
179
195
 
180
196
  export function getToolConfig<T>(
181
197
  config: AgentToolsConfig | undefined,
@@ -43,6 +43,19 @@ export type LogsConfig = {
43
43
  remotePath: string;
44
44
  };
45
45
 
46
+ /** Single Grafana environment connection details */
47
+ export type GrafanaEnvTarget = {
48
+ url: string;
49
+ tokenEnvVar?: string;
50
+ prometheusUid?: string;
51
+ lokiUid?: string;
52
+ };
53
+
54
+ /** Grafana profile configuration */
55
+ export type GrafanaConfig = {
56
+ environments: Record<string, GrafanaEnvTarget>;
57
+ };
58
+
46
59
  export type CliToolOverride = {
47
60
  tool: string;
48
61
  suggestion: string;
@@ -86,6 +99,8 @@ export type AgentToolsConfig = {
86
99
  database?: Record<string, DatabaseConfig>;
87
100
  /** Named logs profiles. e.g. { default: { localDir: "...", remotePath: "..." } } */
88
101
  logs?: Record<string, LogsConfig>;
102
+ /** Named Grafana profiles. e.g. { default: { environments: { local: {...}, prod: {...} } } } */
103
+ grafana?: Record<string, GrafanaConfig>;
89
104
  /** Global session config (not per-profile) */
90
105
  session?: {
91
106
  storagePath: string;
@@ -20,6 +20,7 @@ import {
20
20
  prViewCommand,
21
21
  prStatusCommand,
22
22
  prCreateCommand,
23
+ prCloseCommand,
23
24
  prEditCommand,
24
25
  prMergeCommand,
25
26
  prThreadsCommand,
@@ -66,6 +67,7 @@ const prCommand = Command.make("pr", {}).pipe(
66
67
  prViewCommand,
67
68
  prStatusCommand,
68
69
  prCreateCommand,
70
+ prCloseCommand,
69
71
  prEditCommand,
70
72
  prMergeCommand,
71
73
  prThreadsCommand,
@@ -17,6 +17,7 @@ import {
17
17
  } from "#gh/config";
18
18
 
19
19
  import {
20
+ closePR,
20
21
  createPR,
21
22
  detectPRStatus,
22
23
  editPR,
@@ -147,6 +148,44 @@ export const prEditCommand = Command.make(
147
148
  }),
148
149
  ).pipe(Command.withDescription("Edit an existing PR's title, body, or other metadata"));
149
150
 
151
+ export const prCloseCommand = Command.make(
152
+ "close",
153
+ {
154
+ comment: Flag.string("comment").pipe(
155
+ Flag.withDescription("Comment to add when closing"),
156
+ Flag.optional,
157
+ ),
158
+ commentFile: Flag.string("comment-file").pipe(
159
+ Flag.withDescription("Read close comment from a file path or '-' for stdin"),
160
+ Flag.optional,
161
+ ),
162
+ deleteBranch: Flag.boolean("delete-branch").pipe(
163
+ Flag.withDescription("Delete the branch after closing"),
164
+ Flag.withDefault(false),
165
+ ),
166
+ format: formatOption,
167
+ pr: Flag.integer("pr").pipe(Flag.withDescription("PR number to close")),
168
+ },
169
+ ({ comment, commentFile, deleteBranch, format, pr }) =>
170
+ Effect.gen(function* () {
171
+ const resolvedComment = yield* resolveOptionalTextInput(
172
+ "gh-tool pr close",
173
+ Option.getOrNull(comment),
174
+ Option.getOrNull(commentFile),
175
+ "--comment",
176
+ "--comment-file",
177
+ "comment",
178
+ );
179
+
180
+ const result = yield* closePR({
181
+ comment: resolvedComment,
182
+ deleteBranch,
183
+ pr,
184
+ });
185
+ yield* logFormatted(result, format);
186
+ }),
187
+ ).pipe(Command.withDescription("Close a PR with optional comment and branch deletion"));
188
+
150
189
  export const prMergeCommand = Command.make(
151
190
  "merge",
152
191
  {
@@ -606,6 +606,28 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
606
606
  return result;
607
607
  });
608
608
 
609
+ export const closePR = Effect.fn("pr.closePR")(function* (opts: {
610
+ pr: number;
611
+ comment: string | null;
612
+ deleteBranch: boolean;
613
+ }) {
614
+ const gh = yield* GitHubService;
615
+
616
+ const args = ["pr", "close", String(opts.pr)];
617
+
618
+ if (opts.comment !== null) {
619
+ args.push("--comment", opts.comment);
620
+ }
621
+
622
+ if (opts.deleteBranch) {
623
+ args.push("--delete-branch");
624
+ }
625
+
626
+ yield* gh.runGh(args);
627
+
628
+ return yield* viewPR(opts.pr);
629
+ });
630
+
609
631
  export const editPR = Effect.fn("pr.editPR")(function* (opts: {
610
632
  pr: number;
611
633
  title: string | null;
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  prChecksCommand,
3
3
  prChecksFailedCommand,
4
+ prCloseCommand,
4
5
  prCommentCommand,
5
6
  prCommentsCommand,
6
7
  prCreateCommand,
@@ -0,0 +1,159 @@
1
+ import { Console, Effect } from "effect";
2
+ import { Command, Flag } from "effect/unstable/cli";
3
+
4
+ import { formatOption, formatOutput } from "#shared";
5
+
6
+ import {
7
+ envOption,
8
+ formatGrafanaError,
9
+ grafanaFetch,
10
+ profileOption,
11
+ resolveConfig,
12
+ } from "./shared";
13
+
14
+ type AlertRulesResponse = Record<
15
+ string,
16
+ Array<{
17
+ name: string;
18
+ rules: Array<{
19
+ name: string;
20
+ state: string;
21
+ health: string;
22
+ lastEvaluation?: string;
23
+ evaluationTime?: number;
24
+ }>;
25
+ }>
26
+ >;
27
+
28
+ type AlertInstance = {
29
+ labels: Record<string, string>;
30
+ annotations: Record<string, string>;
31
+ state: string;
32
+ activeAt?: string;
33
+ value?: string;
34
+ };
35
+
36
+ const listCommand = Command.make(
37
+ "list",
38
+ { format: formatOption, env: envOption, profile: profileOption },
39
+ ({ format, env, profile }) => {
40
+ const start = Date.now();
41
+
42
+ return Effect.gen(function* () {
43
+ const config = yield* resolveConfig(env, profile);
44
+ const data = yield* grafanaFetch<AlertRulesResponse>(
45
+ config,
46
+ "/api/ruler/grafana/api/v1/rules",
47
+ );
48
+
49
+ const rules = Object.entries(data).flatMap(([namespace, groups]) =>
50
+ groups.flatMap((group) =>
51
+ group.rules.map((rule) => ({
52
+ name: rule.name,
53
+ state: rule.state,
54
+ health: rule.health,
55
+ namespace,
56
+ group: group.name,
57
+ lastEvaluation: rule.lastEvaluation,
58
+ evaluationTime: rule.evaluationTime,
59
+ })),
60
+ ),
61
+ );
62
+
63
+ const result = {
64
+ success: true,
65
+ message: `Found ${rules.length} alert rule(s)`,
66
+ data: { rules, count: rules.length },
67
+ executionTimeMs: Date.now() - start,
68
+ };
69
+
70
+ yield* Console.log(formatOutput(result, format));
71
+ }).pipe(
72
+ Effect.catch((error) =>
73
+ Effect.gen(function* () {
74
+ const result = {
75
+ success: false,
76
+ message: "Failed to list alert rules",
77
+ error: formatGrafanaError(error),
78
+ hint: "Check Grafana is running and accessible",
79
+ executionTimeMs: Date.now() - start,
80
+ };
81
+
82
+ yield* Console.log(formatOutput(result, format));
83
+ }),
84
+ ),
85
+ );
86
+ },
87
+ ).pipe(Command.withDescription("List all alert rules"));
88
+
89
+ const statusCommand = Command.make(
90
+ "status",
91
+ {
92
+ format: formatOption,
93
+ env: envOption,
94
+ profile: profileOption,
95
+ all: Flag.boolean("all").pipe(
96
+ Flag.withDescription("Show all alerts including normal state"),
97
+ Flag.withDefault(false),
98
+ ),
99
+ },
100
+ ({ format, env, profile, all }) => {
101
+ const start = Date.now();
102
+
103
+ return Effect.gen(function* () {
104
+ const config = yield* resolveConfig(env, profile);
105
+ const alerts = yield* grafanaFetch<AlertInstance[]>(
106
+ config,
107
+ "/api/alertmanager/grafana/api/v2/alerts",
108
+ );
109
+
110
+ const filtered = all
111
+ ? alerts
112
+ : alerts.filter((alert) => alert.state === "firing" || alert.state === "pending");
113
+ const firingCount = filtered.filter((alert) => alert.state === "firing").length;
114
+ const pendingCount = filtered.filter((alert) => alert.state === "pending").length;
115
+
116
+ const result = {
117
+ success: true,
118
+ message:
119
+ firingCount === 0 && pendingCount === 0
120
+ ? "No active alerts"
121
+ : `${firingCount} firing, ${pendingCount} pending alert(s)`,
122
+ data: {
123
+ alerts: filtered.map((alert) => ({
124
+ state: alert.state,
125
+ labels: alert.labels,
126
+ annotations: alert.annotations,
127
+ activeAt: alert.activeAt,
128
+ value: alert.value,
129
+ })),
130
+ firingCount,
131
+ pendingCount,
132
+ totalCount: filtered.length,
133
+ },
134
+ executionTimeMs: Date.now() - start,
135
+ };
136
+
137
+ yield* Console.log(formatOutput(result, format));
138
+ }).pipe(
139
+ Effect.catch((error) =>
140
+ Effect.gen(function* () {
141
+ const result = {
142
+ success: false,
143
+ message: "Failed to get alert status",
144
+ error: formatGrafanaError(error),
145
+ hint: "Check Grafana is running and accessible",
146
+ executionTimeMs: Date.now() - start,
147
+ };
148
+
149
+ yield* Console.log(formatOutput(result, format));
150
+ }),
151
+ ),
152
+ );
153
+ },
154
+ ).pipe(Command.withDescription("Show firing and pending alerts"));
155
+
156
+ export const alertsCommand = Command.make("alerts", {}).pipe(
157
+ Command.withDescription("Alert operations"),
158
+ Command.withSubcommands([listCommand, statusCommand]),
159
+ );